mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
1 Commits
v4.0.8.192
...
v4.0.0.748
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ac5c64af1 |
@@ -1,13 +0,0 @@
|
||||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Sonarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8989],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,6 @@ dotnet_diagnostic.CA1819.severity = suggestion
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
||||
dotnet_diagnostic.CA1823.severity = suggestion
|
||||
dotnet_diagnostic.CA1824.severity = suggestion
|
||||
dotnet_diagnostic.CA1825.severity = suggestion
|
||||
dotnet_diagnostic.CA2000.severity = suggestion
|
||||
dotnet_diagnostic.CA2002.severity = suggestion
|
||||
dotnet_diagnostic.CA2007.severity = suggestion
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Bug Report
|
||||
description: 'Only bug reports for v4 will be accepted, older versions are no longer receiving bug fixes and support issues will be closed immediately.'
|
||||
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Sonarr v2 is EOL & unsupported.'
|
||||
labels: ['needs-triage']
|
||||
body:
|
||||
- type: checkboxes
|
||||
@@ -37,8 +37,8 @@ body:
|
||||
label: Environment
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: Ubuntu 22.04
|
||||
- **Sonarr**: Sonarr 4.0.0.766
|
||||
- **OS**: Ubuntu 20.04
|
||||
- **Sonarr**: Sonarr 3.0.6.1265
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
|
||||
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,15 +1,14 @@
|
||||
#### Database Migration
|
||||
YES | NO
|
||||
|
||||
#### Description
|
||||
A few sentences describing the overall goals of the pull request's commits.
|
||||
|
||||
<!-- Remove any of the following sections if they are not used -->
|
||||
|
||||
#### Screenshots for UI Changes
|
||||
|
||||
|
||||
#### Database Migration
|
||||
YES - ###
|
||||
#### Todos
|
||||
- [ ] Tests
|
||||
- [ ] Wiki Updates
|
||||
|
||||
|
||||
#### Issues Fixed or Closed by this PR
|
||||
* Closes #
|
||||
|
||||
*
|
||||
|
||||
29
.github/actions/archive/action.yml
vendored
29
.github/actions/archive/action.yml
vendored
@@ -1,29 +0,0 @@
|
||||
name: Archive
|
||||
description: Archive binaries for deployment
|
||||
|
||||
inputs:
|
||||
os:
|
||||
description: 'OS that the packaging is running on'
|
||||
required: true
|
||||
artifact:
|
||||
description: 'Binary artifact'
|
||||
required: true
|
||||
archive_type:
|
||||
description: 'File type to use for the final package'
|
||||
required: true
|
||||
branch:
|
||||
description: 'Git branch used for this build'
|
||||
required: true
|
||||
major_version:
|
||||
description: 'Sonarr major version'
|
||||
required: true
|
||||
version:
|
||||
description: 'Sonarr version'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Archive Artifact
|
||||
uses: thedoctor0/zip-release@0.7.5
|
||||
|
||||
78
.github/actions/package/action.yml
vendored
78
.github/actions/package/action.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Package
|
||||
description: Packages binaries for deployment
|
||||
|
||||
inputs:
|
||||
platform:
|
||||
description: 'Binary platform'
|
||||
required: true
|
||||
framework:
|
||||
description: '.net framework'
|
||||
required: true
|
||||
artifact:
|
||||
description: 'Binary artifact'
|
||||
required: true
|
||||
branch:
|
||||
description: 'Git branch used for this build'
|
||||
required: true
|
||||
major_version:
|
||||
description: 'Sonarr major version'
|
||||
required: true
|
||||
version:
|
||||
description: 'Sonarr version'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
path: _output
|
||||
|
||||
- name: Download UI Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build_ui
|
||||
path: _output/UI
|
||||
|
||||
- name: Configure Environment Variables
|
||||
shell: bash
|
||||
run: |
|
||||
echo "FRAMEWORK=${{ inputs.framework }}" >> "$GITHUB_ENV"
|
||||
echo "BRANCH=${{ inputs.branch }}" >> "$GITHUB_ENV"
|
||||
echo "SONARR_MAJOR_VERSION=${{ inputs.major_version }}" >> "$GITHUB_ENV"
|
||||
echo "SONARR_VERSION=${{ inputs.version }}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Create Packages
|
||||
shell: bash
|
||||
run: $GITHUB_ACTION_PATH/package.sh
|
||||
|
||||
- name: Create Windows Installer (x64)
|
||||
if: ${{ inputs.platform == 'windows' }}
|
||||
working-directory: distribution/windows/setup
|
||||
shell: cmd
|
||||
run: |
|
||||
SET RUNTIME=win-x64
|
||||
|
||||
build.bat
|
||||
|
||||
- name: Create Windows Installer (x86)
|
||||
if: ${{ inputs.platform == 'windows' }}
|
||||
working-directory: distribution/windows/setup
|
||||
shell: cmd
|
||||
run: |
|
||||
SET RUNTIME=win-x86
|
||||
|
||||
build.bat
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release_${{ inputs.platform }}
|
||||
compression-level: 0
|
||||
if-no-files-found: error
|
||||
path: |
|
||||
_artifacts/*.exe
|
||||
_artifacts/*.tar.gz
|
||||
_artifacts/*.zip
|
||||
67
.github/actions/package/package.sh
vendored
67
.github/actions/package/package.sh
vendored
@@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
outputFolder=_output
|
||||
artifactsFolder=_artifacts
|
||||
uiFolder="$outputFolder/UI"
|
||||
framework="${FRAMEWORK:=net6.0}"
|
||||
|
||||
rm -rf $artifactsFolder
|
||||
mkdir $artifactsFolder
|
||||
|
||||
for runtime in _output/*
|
||||
do
|
||||
name="${runtime##*/}"
|
||||
folderName="$runtime/$framework"
|
||||
sonarrFolder="$folderName/Sonarr"
|
||||
archiveName="Sonarr.$BRANCH.$SONARR_VERSION.$name"
|
||||
|
||||
if [[ "$name" == 'UI' ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Creating package for $name"
|
||||
|
||||
echo "Copying UI"
|
||||
cp -r $uiFolder $sonarrFolder
|
||||
|
||||
echo "Setting permissions"
|
||||
find $sonarrFolder -name "ffprobe" -exec chmod a+x {} \;
|
||||
find $sonarrFolder -name "Sonarr" -exec chmod a+x {} \;
|
||||
find $sonarrFolder -name "Sonarr.Update" -exec chmod a+x {} \;
|
||||
|
||||
if [[ "$name" == *"osx"* ]]; then
|
||||
echo "Creating macOS package"
|
||||
|
||||
packageName="$name-app"
|
||||
packageFolder="$outputFolder/$packageName"
|
||||
|
||||
rm -rf $packageFolder
|
||||
mkdir $packageFolder
|
||||
|
||||
cp -r distribution/macOS/Sonarr.app $packageFolder
|
||||
mkdir -p $packageFolder/Sonarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $sonarrFolder/* $packageFolder/Sonarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $packageFolder/Sonarr.app/Contents/MacOS/Sonarr.Update
|
||||
|
||||
echo "Packaging macOS app Artifact"
|
||||
(cd $packageFolder; zip -rq "../../$artifactsFolder/$archiveName-app.zip" ./Sonarr.app)
|
||||
fi
|
||||
|
||||
echo "Packaging Artifact"
|
||||
if [[ "$name" == *"linux"* ]] || [[ "$name" == *"osx"* ]] || [[ "$name" == *"freebsd"* ]]; then
|
||||
tar -zcf "./$artifactsFolder/$archiveName.tar.gz" -C $folderName Sonarr
|
||||
fi
|
||||
|
||||
if [[ "$name" == *"win"* ]]; then
|
||||
if [ "$RUNNER_OS" = "Windows" ]
|
||||
then
|
||||
(cd $folderName; 7z a -tzip "../../../$artifactsFolder/$archiveName.zip" ./Sonarr)
|
||||
else
|
||||
(cd $folderName; zip -rq "../../../$artifactsFolder/$archiveName.zip" ./Sonarr)
|
||||
fi
|
||||
fi
|
||||
done
|
||||
18
.github/actions/publish-test-artifact/action.yml
vendored
18
.github/actions/publish-test-artifact/action.yml
vendored
@@ -1,18 +0,0 @@
|
||||
name: Publish Test Artifact
|
||||
description: Publishes a test artifact
|
||||
|
||||
inputs:
|
||||
framework:
|
||||
description: '.net framework'
|
||||
required: true
|
||||
runtime:
|
||||
description: '.net runtime'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tests-${{ inputs.runtime }}
|
||||
path: _tests/${{ inputs.framework }}/${{ inputs.runtime }}/publish/**/*
|
||||
87
.github/actions/test/action.yml
vendored
87
.github/actions/test/action.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: Test
|
||||
description: Runs unit/integration tests
|
||||
|
||||
inputs:
|
||||
use_postgres:
|
||||
description: 'Whether postgres should be used for the database'
|
||||
os:
|
||||
description: 'OS that the tests are running on'
|
||||
required: true
|
||||
artifact:
|
||||
description: 'Test binary artifact'
|
||||
required: true
|
||||
pattern:
|
||||
description: 'Pattern for DLLs'
|
||||
required: true
|
||||
filter:
|
||||
description: 'Filter for tests'
|
||||
required: true
|
||||
integration_tests:
|
||||
description: 'True if running integration tests'
|
||||
binary_artifact:
|
||||
description: 'Binary artifact for integration tests'
|
||||
binary_path:
|
||||
description: 'Path witin binary artifact for integration tests'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
|
||||
- name: Setup Postgres
|
||||
if: ${{ inputs.use_postgres }}
|
||||
uses: ikalnytskyi/action-setup-postgres@v4
|
||||
|
||||
- name: Setup Test Variables
|
||||
shell: bash
|
||||
run: |
|
||||
echo "RESULTS_NAME=${{ inputs.integration_tests && 'integation-' || 'unit-' }}${{ inputs.artifact }}${{ inputs.use_postgres && '-postgres' }}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Setup Postgres Environment Variables
|
||||
if: ${{ inputs.use_postgres }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Sonarr__Postgres__Host=localhost" >> "$GITHUB_ENV"
|
||||
echo "Sonarr__Postgres__Port=5432" >> "$GITHUB_ENV"
|
||||
echo "Sonarr__Postgres__User=postgres" >> "$GITHUB_ENV"
|
||||
echo "Sonarr__Postgres__Password=postgres" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.artifact }}
|
||||
path: _tests
|
||||
|
||||
- name: Download Binary Artifact
|
||||
if: ${{ inputs.integration_tests }}
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ inputs.binary_artifact }}
|
||||
path: _output
|
||||
|
||||
- name: Set up binary artifact
|
||||
if: ${{ inputs.binary_path != '' }}
|
||||
shell: bash
|
||||
run: mv ./_output/${{inputs.binary_path}} _tests/bin
|
||||
|
||||
- name: Make executable
|
||||
if: startsWith(inputs.os, 'windows') != true
|
||||
shell: bash
|
||||
run: chmod +x ./_tests/Sonarr.Test.Dummy && chmod +x ./_tests/ffprobe
|
||||
|
||||
- name: Make Sonarr binary executable
|
||||
if: ${{ inputs.integration_tests && !startsWith(inputs.os, 'windows') }}
|
||||
shell: bash
|
||||
run: chmod +x ./_tests/bin/Sonarr
|
||||
|
||||
- name: Run tests
|
||||
shell: bash
|
||||
run: dotnet test ./_tests/Sonarr.*.Test.dll --filter "${{ inputs.filter }}" --logger "trx;LogFileName=${{ env.RESULTS_NAME }}.trx" --logger "GitHubActions;summary.includePassedTests=true;summary.includeSkippedTests=true"
|
||||
|
||||
- name: Upload Test Results
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: results-${{ env.RESULTS_NAME }}
|
||||
path: TestResults/*.trx
|
||||
12
.github/dependabot.yml
vendored
12
.github/dependabot.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
18
.github/labeler.yml
vendored
18
.github/labeler.yml
vendored
@@ -1,23 +1,17 @@
|
||||
'connection':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: src/NzbDrone.Core/Notifications/**/*
|
||||
- src/NzbDrone.Core/Notifications/**/*
|
||||
|
||||
'db-migration':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: src/NzbDrone.Core/Datastore/Migration/*
|
||||
- src/NzbDrone.Core/Datastore/Migration/*
|
||||
|
||||
'download-client':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: src/NzbDrone.Core/Download/Clients/**/*
|
||||
- src/NzbDrone.Core/Download/Clients/**/*
|
||||
|
||||
'indexer':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: src/NzbDrone.Core/Indexers/**/*
|
||||
- src/NzbDrone.Core/Indexers/**/*
|
||||
|
||||
'parsing':
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: src/NzbDrone.Core/Parser/**/*
|
||||
- src/NzbDrone.Core/Parser/**/*
|
||||
|
||||
'ui-only':
|
||||
- changed-files:
|
||||
- any-glob-to-all-files: frontend/**/*
|
||||
- all: ['frontend/**/*']
|
||||
|
||||
9
.github/release.yml
vendored
9
.github/release.yml
vendored
@@ -1,9 +0,0 @@
|
||||
changelog:
|
||||
exclude:
|
||||
authors:
|
||||
- Weblate
|
||||
- SonarrBot
|
||||
categories:
|
||||
- title: Changes
|
||||
labels:
|
||||
- '*'
|
||||
10
.github/workflows/api_docs.yml
vendored
10
.github/workflows/api_docs.yml
vendored
@@ -26,11 +26,17 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v3
|
||||
id: setup-dotnet
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
|
||||
- name: Create temporary global.json
|
||||
run: |
|
||||
echo '{"sdk":{"version": "${{ steps.setup-dotnet.outputs.dotnet-version }}" } }' > ./global.json
|
||||
|
||||
- name: Create openapi.json
|
||||
run: ./docs.sh Linux
|
||||
|
||||
257
.github/workflows/build.yml
vendored
257
.github/workflows/build.yml
vendored
@@ -1,257 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- main
|
||||
paths-ignore:
|
||||
- "src/Sonarr.Api.*/openapi.json"
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
paths-ignore:
|
||||
- "src/NzbDrone.Core/Localization/Core/**"
|
||||
- "src/Sonarr.Api.*/openapi.json"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
FRAMEWORK: net6.0
|
||||
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
|
||||
SONARR_MAJOR_VERSION: 4
|
||||
VERSION: 4.0.8
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
runs-on: windows-latest
|
||||
outputs:
|
||||
framework: ${{ steps.variables.outputs.framework }}
|
||||
major_version: ${{ steps.variables.outputs.major_version }}
|
||||
version: ${{ steps.variables.outputs.version }}
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
|
||||
- name: Setup Environment Variables
|
||||
id: variables
|
||||
shell: bash
|
||||
run: |
|
||||
# Add 800 to the build number because GitHub won't let us pick an arbitrary starting point
|
||||
SONARR_VERSION="${{ env.VERSION }}.$((${{ github.run_number }}+800))"
|
||||
DOTNET_VERSION=$(jq -r '.sdk.version' global.json)
|
||||
|
||||
echo "SDK_PATH=${{ env.DOTNET_ROOT }}/sdk/${DOTNET_VERSION}" >> "$GITHUB_ENV"
|
||||
echo "SONARR_VERSION=$SONARR_VERSION" >> "$GITHUB_ENV"
|
||||
echo "BRANCH=${RAW_BRANCH_NAME//\//-}" >> "$GITHUB_ENV"
|
||||
|
||||
echo "framework=${{ env.FRAMEWORK }}" >> "$GITHUB_OUTPUT"
|
||||
echo "major_version=${{ env.SONARR_MAJOR_VERSION }}" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$SONARR_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Enable Extra Platforms In SDK
|
||||
shell: bash
|
||||
run: ./build.sh --enable-extra-platforms-in-sdk
|
||||
|
||||
- name: Build Backend
|
||||
shell: bash
|
||||
run: ./build.sh --backend --enable-extra-platforms --packages
|
||||
|
||||
# Test Artifacts
|
||||
|
||||
- name: Publish win-x64 Test Artifact
|
||||
uses: ./.github/actions/publish-test-artifact
|
||||
with:
|
||||
framework: ${{ env.FRAMEWORK }}
|
||||
runtime: win-x64
|
||||
|
||||
- name: Publish linux-x64 Test Artifact
|
||||
uses: ./.github/actions/publish-test-artifact
|
||||
with:
|
||||
framework: ${{ env.FRAMEWORK }}
|
||||
runtime: linux-x64
|
||||
|
||||
- name: Publish osx-arm64 Test Artifact
|
||||
uses: ./.github/actions/publish-test-artifact
|
||||
with:
|
||||
framework: ${{ env.FRAMEWORK }}
|
||||
runtime: osx-arm64
|
||||
|
||||
# Build Artifacts (grouped by OS)
|
||||
|
||||
- name: Publish FreeBSD Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build_freebsd
|
||||
path: _artifacts/freebsd-*/**/*
|
||||
- name: Publish Linux Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build_linux
|
||||
path: _artifacts/linux-*/**/*
|
||||
- name: Publish macOS Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build_macos
|
||||
path: _artifacts/osx-*/**/*
|
||||
- name: Publish Windows Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build_windows
|
||||
path: _artifacts/win-*/**/*
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Volta
|
||||
uses: volta-cli/action@v4
|
||||
|
||||
- name: Yarn Install
|
||||
run: yarn install
|
||||
|
||||
- name: Lint
|
||||
run: yarn lint
|
||||
|
||||
- name: Stylelint
|
||||
run: yarn stylelint -f github
|
||||
|
||||
- name: Build
|
||||
run: yarn build --env production
|
||||
|
||||
- name: Publish UI Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build_ui
|
||||
path: _output/UI/**/*
|
||||
|
||||
unit_test:
|
||||
needs: backend
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact: tests-linux-x64
|
||||
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||
- os: macos-latest
|
||||
artifact: tests-osx-arm64
|
||||
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||
- os: windows-latest
|
||||
artifact: tests-win-x64
|
||||
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test
|
||||
uses: ./.github/actions/test
|
||||
with:
|
||||
os: ${{ matrix.os }}
|
||||
artifact: ${{ matrix.artifact }}
|
||||
pattern: Sonarr.*.Test.dll
|
||||
filter: ${{ matrix.filter }}
|
||||
|
||||
unit_test_postgres:
|
||||
needs: backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test
|
||||
uses: ./.github/actions/test
|
||||
with:
|
||||
os: ubuntu-latest
|
||||
artifact: tests-linux-x64
|
||||
pattern: Sonarr.*.Test.dll
|
||||
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
|
||||
use_postgres: true
|
||||
|
||||
integration_test:
|
||||
needs: backend
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
artifact: tests-linux-x64
|
||||
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
|
||||
binary_artifact: build_linux
|
||||
binary_path: linux-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||
- os: macos-latest
|
||||
artifact: tests-osx-arm64
|
||||
filter: TestCategory!=ManualTest&TestCategory!=WINDOWS&TestCategory=IntegrationTest
|
||||
binary_artifact: build_macos
|
||||
binary_path: osx-arm64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||
- os: windows-latest
|
||||
artifact: tests-win-x64
|
||||
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory=IntegrationTest
|
||||
binary_artifact: build_windows
|
||||
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Test
|
||||
uses: ./.github/actions/test
|
||||
with:
|
||||
os: ${{ matrix.os }}
|
||||
artifact: ${{ matrix.artifact }}
|
||||
pattern: Sonarr.*.Test.dll
|
||||
filter: ${{ matrix.filter }}
|
||||
integration_tests: true
|
||||
binary_artifact: ${{ matrix.binary_artifact }}
|
||||
binary_path: ${{ matrix.binary_path }}
|
||||
|
||||
deploy:
|
||||
if: ${{ github.ref_name == 'develop' || github.ref_name == 'main' }}
|
||||
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test]
|
||||
secrets: inherit
|
||||
uses: ./.github/workflows/deploy.yml
|
||||
with:
|
||||
framework: ${{ needs.backend.outputs.framework }}
|
||||
branch: ${{ github.ref_name }}
|
||||
major_version: ${{ needs.backend.outputs.major_version }}
|
||||
version: ${{ needs.backend.outputs.version }}
|
||||
|
||||
notify:
|
||||
name: Discord Notification
|
||||
needs:
|
||||
[
|
||||
backend,
|
||||
frontend,
|
||||
unit_test,
|
||||
unit_test_postgres,
|
||||
integration_test,
|
||||
deploy,
|
||||
]
|
||||
if: ${{ !cancelled() && (github.ref_name == 'develop' || github.ref_name == 'main') }}
|
||||
env:
|
||||
STATUS: ${{ contains(needs.*.result, 'failure') && 'failure' || 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Notify
|
||||
uses: tsickert/discord-webhook@v6.0.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
username: "GitHub Actions"
|
||||
avatar-url: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
|
||||
embed-title: "${{ github.workflow }}: ${{ env.STATUS == 'success' && 'Success' || 'Failure' }}"
|
||||
embed-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
embed-description: |
|
||||
**Branch** ${{ github.ref }}
|
||||
**Build** ${{ needs.backend.outputs.version }}
|
||||
embed-color: ${{ env.STATUS == 'success' && '3066993' || '15158332' }}
|
||||
26
.github/workflows/conflict_labeler.yml
vendored
26
.github/workflows/conflict_labeler.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Merge Conflict Labeler
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
pull_request_target:
|
||||
branches:
|
||||
- develop
|
||||
types: [synchronize]
|
||||
|
||||
jobs:
|
||||
label:
|
||||
name: Labeling
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'Sonarr/Sonarr' }}
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Apply label
|
||||
uses: eps1lon/actions-label-merge-conflict@v3
|
||||
with:
|
||||
dirtyLabel: 'merge-conflict'
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
160
.github/workflows/deploy.yml
vendored
160
.github/workflows/deploy.yml
vendored
@@ -1,160 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
framework:
|
||||
description: '.net framework'
|
||||
type: string
|
||||
required: true
|
||||
branch:
|
||||
description: 'Git branch used for this build'
|
||||
type: string
|
||||
required: true
|
||||
major_version:
|
||||
description: 'Sonarr major version'
|
||||
type: string
|
||||
required: true
|
||||
version:
|
||||
description: 'Sonarr version'
|
||||
type: string
|
||||
required: true
|
||||
secrets:
|
||||
SERVICES_API_KEY:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
package:
|
||||
strategy:
|
||||
matrix:
|
||||
platform: [freebsd, linux, macos, windows]
|
||||
include:
|
||||
- platform: freebsd
|
||||
os: ubuntu-latest
|
||||
- platform: linux
|
||||
os: ubuntu-latest
|
||||
- platform: macos
|
||||
os: ubuntu-latest
|
||||
- platform: windows
|
||||
os: windows-latest
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Package
|
||||
uses: ./.github/actions/package
|
||||
with:
|
||||
framework: ${{ inputs.framework }}
|
||||
platform: ${{ matrix.platform }}
|
||||
artifact: build_${{ matrix.platform }}
|
||||
branch: ${{ inputs.branch }}
|
||||
major_version: ${{ inputs.major_version }}
|
||||
version: ${{ inputs.version }}
|
||||
|
||||
release:
|
||||
needs: package
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: _artifacts
|
||||
pattern: release_*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Get Previous Release
|
||||
id: previous-release
|
||||
uses: cardinalby/git-get-release-action@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
latest: true
|
||||
prerelease: ${{ inputs.branch != 'main' }}
|
||||
|
||||
- name: Generate Release Notes
|
||||
id: generate-release-notes
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
result-encoding: string
|
||||
script: |
|
||||
const { data } = await github.rest.repos.generateReleaseNotes({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
tag_name: 'v${{ inputs.version }}',
|
||||
target_commitish: '${{ github.sha }}',
|
||||
previous_tag_name: '${{ steps.previous-release.outputs.tag_name }}',
|
||||
})
|
||||
return data.body
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: _artifacts/Sonarr.*
|
||||
commit: ${{ github.sha }}
|
||||
generateReleaseNotes: false
|
||||
body: ${{ steps.generate-release-notes.outputs.result }}
|
||||
name: ${{ inputs.version }}
|
||||
prerelease: ${{ inputs.branch != 'main' }}
|
||||
skipIfReleaseExists: true
|
||||
tag: v${{ inputs.version }}
|
||||
|
||||
- name: Publish to Services
|
||||
shell: bash
|
||||
working-directory: _artifacts
|
||||
run: |
|
||||
branch=${{ inputs.branch }}
|
||||
version=${{ inputs.version }}
|
||||
lastCommit=${{ github.sha }}
|
||||
|
||||
hashes="["
|
||||
|
||||
addHash() {
|
||||
path=$1
|
||||
os=$2
|
||||
arch=$3
|
||||
type=$4
|
||||
|
||||
local hash=$(sha256sum *.$version.$path | awk '{ print $1; }')
|
||||
echo "{ \""Os\"": \""$os\"", \""Arch\"": \""$arch\"", \""Type\"": \""$type\"", \""Hash\"": \""$hash\"" }"
|
||||
}
|
||||
|
||||
hashes="$hashes $(addHash "linux-arm.tar.gz" "linux" "arm" "archive")"
|
||||
hashes="$hashes, $(addHash "linux-arm64.tar.gz" "linux" "arm64" "archive")"
|
||||
hashes="$hashes, $(addHash "linux-x64.tar.gz" "linux" "x64" "archive")"
|
||||
# hashes="$hashes, $(addHash "linux-x86.tar.gz" "linux" "x86" "archive")"
|
||||
|
||||
# hashes="$hashes, $(addHash "linux-musl-arm.tar.gz" "linuxmusl" "arm" "archive")"
|
||||
hashes="$hashes, $(addHash "linux-musl-arm64.tar.gz" "linuxmusl" "arm64" "archive")"
|
||||
hashes="$hashes, $(addHash "linux-musl-x64.tar.gz" "linuxmusl" "x64" "archive")"
|
||||
|
||||
hashes="$hashes, $(addHash "osx-arm64.tar.gz" "osx" "arm64" "archive")"
|
||||
hashes="$hashes, $(addHash "osx-x64.tar.gz" "osx" "x64" "archive")"
|
||||
|
||||
hashes="$hashes, $(addHash "osx-arm64-app.zip" "osx" "arm64" "installer")"
|
||||
hashes="$hashes, $(addHash "osx-x64-app.zip" "osx" "x64" "installer")"
|
||||
|
||||
hashes="$hashes, $(addHash "win-x64.zip" "windows" "x64" "archive")"
|
||||
hashes="$hashes, $(addHash "win-x86.zip" "windows" "x86" "archive")"
|
||||
|
||||
hashes="$hashes, $(addHash "win-x64-installer.exe" "windows" "x64" "installer")"
|
||||
hashes="$hashes, $(addHash "win-x86-installer.exe" "windows" "x86" "installer")"
|
||||
|
||||
hashes="$hashes, $(addHash "freebsd-x64.tar.gz" "freebsd" "x64" "archive")"
|
||||
|
||||
hashes="$hashes ]"
|
||||
|
||||
json="{\""branch\"":\""$branch\"", \""version\"":\""$version\"", \""lastCommit\"":\""$lastCommit\"", \""hashes\"":$hashes, \""gitHubRelease\"":true}"
|
||||
url="https://services.sonarr.tv/v1/update"
|
||||
|
||||
echo "Publishing update $version ($branch) to: $url"
|
||||
echo "$json"
|
||||
|
||||
curl -H "Content-Type: application/json" -H "X-Api-Key: ${{ secrets.SERVICES_API_KEY }}" -X POST -d "$json" --fail-with-body $url
|
||||
3
.github/workflows/labeler.yml
vendored
3
.github/workflows/labeler.yml
vendored
@@ -8,6 +8,5 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Sonarr/Sonarr'
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@v4
|
||||
|
||||
13
.github/workflows/lock.yml
vendored
13
.github/workflows/lock.yml
vendored
@@ -8,15 +8,14 @@ on:
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Sonarr/Sonarr'
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5
|
||||
- uses: dessant/lock-threads@v2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '90'
|
||||
exclude-issue-created-before: ''
|
||||
exclude-any-issue-labels: 'one-day-maybe'
|
||||
add-issue-labels: ''
|
||||
issue-comment: ''
|
||||
issue-lock-inactive-days: '90'
|
||||
issue-exclude-created-before: ''
|
||||
issue-exclude-labels: 'one-day-maybe'
|
||||
issue-lock-labels: ''
|
||||
issue-lock-comment: ''
|
||||
issue-lock-reason: 'resolved'
|
||||
process-only: ''
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -127,7 +127,6 @@ coverage*.xml
|
||||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
#VS outout folders
|
||||
bin
|
||||
|
||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
||||
26
.vscode/launch.json
vendored
26
.vscode/launch.json
vendored
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Sonarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Sonarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
||||
44
.vscode/tasks.json
vendored
44
.vscode/tasks.json
vendored
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Sonarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Sonarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Sonarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
47
build.sh
47
build.sh
@@ -4,18 +4,19 @@ set -e
|
||||
outputFolder='_output'
|
||||
testPackageFolder='_tests'
|
||||
artifactsFolder="_artifacts";
|
||||
framework="${FRAMEWORK:=net6.0}"
|
||||
|
||||
ProgressStart()
|
||||
{
|
||||
echo "::group::$1"
|
||||
echo "##teamcity[blockOpened name='$1']"
|
||||
echo "##teamcity[progressStart '$1']"
|
||||
echo "Start '$1'"
|
||||
}
|
||||
|
||||
ProgressEnd()
|
||||
{
|
||||
echo "Finish '$1'"
|
||||
echo "::endgroup::"
|
||||
echo "##teamcity[progressFinish '$1']"
|
||||
echo "##teamcity[blockClosed name='$1']"
|
||||
}
|
||||
|
||||
UpdateVersionNumber()
|
||||
@@ -139,7 +140,7 @@ PackageLinux()
|
||||
|
||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
||||
if [ "$framework" = "$framework" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
||||
fi
|
||||
@@ -167,7 +168,7 @@ PackageMacOS()
|
||||
|
||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||
cp $folder/Sonarr.Mono.* $folder/Sonarr.Update
|
||||
if [ "$framework" = "$framework" ]; then
|
||||
if [ "$framework" = "net6.0" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Sonarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Sonarr.Update
|
||||
fi
|
||||
@@ -399,20 +400,20 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "$framework" "win-x64"
|
||||
PackageTests "$framework" "win-x86"
|
||||
PackageTests "$framework" "linux-x64"
|
||||
PackageTests "$framework" "linux-musl-x64"
|
||||
PackageTests "$framework" "osx-x64"
|
||||
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"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "$framework" "freebsd-x64"
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
|
||||
UploadTestArtifacts "$framework"
|
||||
UploadTestArtifacts "net6.0"
|
||||
fi
|
||||
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
@@ -434,22 +435,22 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "$framework" "win-x64"
|
||||
Package "$framework" "win-x86"
|
||||
Package "$framework" "linux-x64"
|
||||
Package "$framework" "linux-musl-x64"
|
||||
Package "$framework" "linux-arm64"
|
||||
Package "$framework" "linux-musl-arm64"
|
||||
Package "$framework" "linux-arm"
|
||||
Package "$framework" "osx-x64"
|
||||
Package "$framework" "osx-arm64"
|
||||
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" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "$framework" "freebsd-x64"
|
||||
Package "net6.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
|
||||
UploadArtifacts "$framework"
|
||||
UploadArtifacts "net6.0"
|
||||
fi
|
||||
|
||||
64
distribution/debian.sh
Normal file
64
distribution/debian.sh
Normal file
@@ -0,0 +1,64 @@
|
||||
fromdos ./debian/*
|
||||
chmod ugo-x ./debian/*
|
||||
cp -r ./debian ./debian_backup
|
||||
|
||||
BuildVersion=${dependent_build_number:-4.10.0.999}
|
||||
BuildBranch=${dependent_build_branch:-main}
|
||||
BootstrapVersion=`echo "$BuildVersion" | cut -d. -f1,2,3`
|
||||
BootstrapUpdater="BuiltIn"
|
||||
PackageUpdater="apt"
|
||||
|
||||
echo Version: "$BuildVersion" Branch: "$BuildBranch"
|
||||
|
||||
rm -r ./sonarr_bin/Sonarr.Update
|
||||
chmod -R ugo-x,ugo+rwX,go-w ./sonarr_bin/*
|
||||
|
||||
echo Updating changelog for $BuildVersion
|
||||
sed -i "s:{version}:$BuildVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$PackageUpdater:g" debian/preinst debian/postinst debian/postrm
|
||||
sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
||||
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
||||
|
||||
echo Running debuild for $BuildVersion
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
debuild -b
|
||||
else
|
||||
debuild -us -uc -b
|
||||
fi
|
||||
|
||||
# Restore debian directory to the original files
|
||||
rm -rf ./debian
|
||||
mv ./debian_backup ./debian
|
||||
|
||||
echo Updating changelog for $BootstrapVersion
|
||||
sed -i "s:{version}:$BootstrapVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$BootstrapUpdater:g" debian/preinst debian/postinst debian/postrm
|
||||
sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
||||
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
||||
|
||||
echo Running debuild for $BootstrapVersion
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
debuild -b
|
||||
else
|
||||
debuild -us -uc -b
|
||||
fi
|
||||
|
||||
echo Moving stuff around
|
||||
mv ../sonarr_*.deb ./
|
||||
mv ../sonarr_*.changes ./
|
||||
rm ../sonarr_*.build
|
||||
|
||||
if [ -z "${TEST_OUTPUT}" ]; then
|
||||
echo Signing Package
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
||||
|
||||
echo running alien
|
||||
alien -r -v ./*.deb
|
||||
else
|
||||
echo "Exporting packages to ${TEST_OUTPUT}"
|
||||
dpkg -e "sonarr_${BuildVersion}_all.deb" ${TEST_OUTPUT}/sonarr-build
|
||||
dpkg -e "sonarr_${BootstrapVersion}_all.deb" ${TEST_OUTPUT}/sonarr-release
|
||||
|
||||
cp *.deb ${TEST_OUTPUT}/
|
||||
fi
|
||||
6
distribution/debian/.editorconfig
Normal file
6
distribution/debian/.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
[*]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
5
distribution/debian/changelog
Normal file
5
distribution/debian/changelog
Normal file
@@ -0,0 +1,5 @@
|
||||
sonarr ({version}) {branch}; urgency=low
|
||||
|
||||
* Automatic Release.
|
||||
|
||||
-- Sonarr <hello@sonarr.tv> Sun, 28 Jan 2018 00:00:00 -0700
|
||||
1
distribution/debian/compat
Normal file
1
distribution/debian/compat
Normal file
@@ -0,0 +1 @@
|
||||
10
|
||||
17
distribution/debian/config
Normal file
17
distribution/debian/config
Normal file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
db_beginblock
|
||||
db_input high sonarr/owning_user || true
|
||||
db_input high sonarr/owning_group || true
|
||||
db_endblock
|
||||
db_go
|
||||
|
||||
db_beginblock
|
||||
db_input low sonarr/owning_umask || true
|
||||
db_input low sonarr/config_directory || true
|
||||
db_endblock
|
||||
db_go
|
||||
|
||||
exit 0
|
||||
18
distribution/debian/control
Normal file
18
distribution/debian/control
Normal file
@@ -0,0 +1,18 @@
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Sonarr <hello@sonarr.tv>
|
||||
Source: sonarr
|
||||
Homepage: https://sonarr.tv
|
||||
Vcs-Git: git@github.com:Sonarr/Sonarr.git
|
||||
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
||||
Build-Depends: debhelper (>= 9),
|
||||
dh-systemd (>= 1.5)
|
||||
|
||||
Package: sonarr
|
||||
Architecture: all
|
||||
Provides: nzbdrone
|
||||
Conflicts: nzbdrone
|
||||
Replaces: nzbdrone
|
||||
Depends: adduser, libsqlite3-0 (>= 3.7), ${cli:Depends}, ${misc:Depends}
|
||||
Suggests: sqlite3 (>= 3.7)
|
||||
Description: Internet PVR
|
||||
24
distribution/debian/copyright
Normal file
24
distribution/debian/copyright
Normal file
@@ -0,0 +1,24 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: sonarr
|
||||
Source: https://github.com/Sonarr/Sonarr
|
||||
|
||||
Files: *
|
||||
Copyright: 2010-2016 Sonarr <hello@sonarr.tv>
|
||||
|
||||
License: GPL-3.0+
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
||||
1
distribution/debian/files
Normal file
1
distribution/debian/files
Normal file
@@ -0,0 +1 @@
|
||||
sonarr_3.0.0.0_all.deb web optional
|
||||
2
distribution/debian/install
Normal file
2
distribution/debian/install
Normal file
@@ -0,0 +1,2 @@
|
||||
sonarr_bin/* usr/lib/sonarr/bin
|
||||
package_info usr/lib/sonarr
|
||||
@@ -1,182 +0,0 @@
|
||||
#!/bin/bash
|
||||
### Description: Sonarr .NET Debian install
|
||||
### Originally written for Radarr by: DoctorArr - doctorarr@the-rowlands.co.uk on 2021-10-01 v1.0
|
||||
### Updates for servarr suite made by Bakerboy448, DoctorArr, brightghost, aeramor and VP-EN
|
||||
### Version v1.0.0 2023-12-29 - StevieTV - adapted from servarr script for Sonarr installs
|
||||
### Version V1.0.1 2024-01-02 - StevieTV - remove UTF8-BOM
|
||||
### Version V1.0.2 2024-01-03 - markus101 - Get user input from /dev/tty
|
||||
### Version V1.0.3 2024-01-06 - StevieTV - exit script when it is ran from install directory
|
||||
|
||||
### Boilerplate Warning
|
||||
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
#EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
#MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
#NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
#LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
#OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
#WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
scriptversion="1.0.3"
|
||||
scriptdate="2024-01-06"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Running Sonarr Install Script - Version [$scriptversion] as of [$scriptdate]"
|
||||
|
||||
# Am I root?, need root!
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root."
|
||||
exit
|
||||
fi
|
||||
|
||||
app="sonarr"
|
||||
app_port="8989"
|
||||
app_prereq="curl sqlite3 wget"
|
||||
app_umask="0002"
|
||||
branch="main"
|
||||
|
||||
# Constants
|
||||
### Update these variables as required for your specific instance
|
||||
installdir="/opt" # {Update me if needed} Install Location
|
||||
bindir="${installdir}/${app^}" # Full Path to Install Location
|
||||
datadir="/var/lib/$app/" # {Update me if needed} AppData directory to use
|
||||
app_bin=${app^} # Binary Name of the app
|
||||
|
||||
# This script should not be ran from installdir, otherwise later in the script the extracted files will be removed before they can be moved to installdir.
|
||||
if [ "$installdir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ] || [ "$bindir" == "$(dirname -- "$( readlink -f -- "$0"; )")" ]; then
|
||||
echo "You should not run this script from the intended install directory. The script will exit. Please re-run it from another directory"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Prompt User
|
||||
read -r -p "What user should ${app^} run as? (Default: $app): " app_uid < /dev/tty
|
||||
app_uid=$(echo "$app_uid" | tr -d ' ')
|
||||
app_uid=${app_uid:-$app}
|
||||
# Prompt Group
|
||||
read -r -p "What group should ${app^} run as? (Default: media): " app_guid < /dev/tty
|
||||
app_guid=$(echo "$app_guid" | tr -d ' ')
|
||||
app_guid=${app_guid:-media}
|
||||
|
||||
echo "This will install [${app^}] to [$bindir] and use [$datadir] for the AppData Directory"
|
||||
echo "${app^} will run as the user [$app_uid] and group [$app_guid]. By continuing, you've confirmed that that user and group will have READ and WRITE access to your Media Library and Download Client Completed Download directories"
|
||||
read -n 1 -r -s -p $'Press enter to continue or ctrl+c to exit...\n' < /dev/tty
|
||||
|
||||
# Create User / Group as needed
|
||||
if [ "$app_guid" != "$app_uid" ]; then
|
||||
if ! getent group "$app_guid" >/dev/null; then
|
||||
groupadd "$app_guid"
|
||||
fi
|
||||
fi
|
||||
if ! getent passwd "$app_uid" >/dev/null; then
|
||||
adduser --system --no-create-home --ingroup "$app_guid" "$app_uid"
|
||||
echo "Created and added User [$app_uid] to Group [$app_guid]"
|
||||
fi
|
||||
if ! getent group "$app_guid" | grep -qw "$app_uid"; then
|
||||
echo "User [$app_uid] did not exist in Group [$app_guid]"
|
||||
usermod -a -G "$app_guid" "$app_uid"
|
||||
echo "Added User [$app_uid] to Group [$app_guid]"
|
||||
fi
|
||||
|
||||
# Stop the App if running
|
||||
if service --status-all | grep -Fq "$app"; then
|
||||
systemctl stop "$app"
|
||||
systemctl disable "$app".service
|
||||
echo "Stopped existing $app"
|
||||
fi
|
||||
|
||||
# Create Appdata Directory
|
||||
|
||||
# AppData
|
||||
mkdir -p "$datadir"
|
||||
chown -R "$app_uid":"$app_guid" "$datadir"
|
||||
chmod 775 "$datadir"
|
||||
echo "Directories created"
|
||||
# Download and install the App
|
||||
|
||||
# prerequisite packages
|
||||
echo ""
|
||||
echo "Installing pre-requisite Packages"
|
||||
# shellcheck disable=SC2086
|
||||
apt update && apt install -y $app_prereq
|
||||
echo ""
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
# get arch
|
||||
dlbase="https://services.sonarr.tv/v1/download/$branch/latest?version=4&os=linux"
|
||||
case "$ARCH" in
|
||||
"amd64") DLURL="${dlbase}&arch=x64" ;;
|
||||
"armhf") DLURL="${dlbase}&arch=arm" ;;
|
||||
"arm64") DLURL="${dlbase}&arch=arm64" ;;
|
||||
*)
|
||||
echo "Arch not supported"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
echo ""
|
||||
echo "Removing previous tarballs"
|
||||
# -f to Force so we fail if it doesnt exist
|
||||
rm -f "${app^}".*.tar.gz
|
||||
echo ""
|
||||
echo "Downloading..."
|
||||
wget --content-disposition "$DLURL"
|
||||
tar -xvzf "${app^}".*.tar.gz
|
||||
echo ""
|
||||
echo "Installation files downloaded and extracted"
|
||||
|
||||
# remove existing installs
|
||||
echo "Removing existing installation"
|
||||
rm -rf "$bindir"
|
||||
echo "Installing..."
|
||||
mv "${app^}" $installdir
|
||||
chown "$app_uid":"$app_guid" -R "$bindir"
|
||||
chmod 775 "$bindir"
|
||||
rm -rf "${app^}.*.tar.gz"
|
||||
# Ensure we check for an update in case user installs older version or different branch
|
||||
touch "$datadir"/update_required
|
||||
chown "$app_uid":"$app_guid" "$datadir"/update_required
|
||||
echo "App Installed"
|
||||
# Configure Autostart
|
||||
|
||||
# Remove any previous app .service
|
||||
echo "Removing old service file"
|
||||
rm -rf /etc/systemd/system/"$app".service
|
||||
|
||||
# Create app .service with correct user startup
|
||||
echo "Creating service file"
|
||||
cat <<EOF | tee /etc/systemd/system/"$app".service >/dev/null
|
||||
[Unit]
|
||||
Description=${app^} Daemon
|
||||
After=syslog.target network.target
|
||||
[Service]
|
||||
User=$app_uid
|
||||
Group=$app_guid
|
||||
UMask=$app_umask
|
||||
Type=simple
|
||||
ExecStart=$bindir/$app_bin -nobrowser -data=$datadir
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# Start the App
|
||||
echo "Service file created. Attempting to start the app"
|
||||
systemctl -q daemon-reload
|
||||
systemctl enable --now -q "$app"
|
||||
|
||||
# Finish Update/Installation
|
||||
host=$(hostname -I)
|
||||
ip_local=$(grep -oP '^\S*' <<<"$host")
|
||||
echo ""
|
||||
echo "Install complete"
|
||||
sleep 10
|
||||
STATUS="$(systemctl is-active "$app")"
|
||||
if [ "${STATUS}" = "active" ]; then
|
||||
echo "Browse to http://$ip_local:$app_port for the ${app^} GUI"
|
||||
else
|
||||
echo "${app^} failed to start"
|
||||
fi
|
||||
|
||||
# Exit
|
||||
exit 0
|
||||
111
distribution/debian/postinst
Normal file
111
distribution/debian/postinst
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
BUILD_VERSION={version}
|
||||
UPDATER={updater}
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
db_get sonarr/owning_user
|
||||
USER="$RET"
|
||||
db_get sonarr/owning_group
|
||||
GROUP="$RET"
|
||||
db_get sonarr/owning_umask
|
||||
UMASK="$RET"
|
||||
db_get sonarr/config_directory
|
||||
CONFDIR="$RET"
|
||||
|
||||
# Add User and Group
|
||||
if ! getent group "$GROUP" >/dev/null; then
|
||||
groupadd "$GROUP"
|
||||
fi
|
||||
if ! getent passwd "$USER" >/dev/null; then
|
||||
adduser --system --no-create-home --ingroup "$GROUP" "$USER"
|
||||
fi
|
||||
|
||||
if [ $1 = "configure" ]; then
|
||||
# Migrate old Sonarr v3 alpha data dir from /var/opt/sonarr or user home
|
||||
if [ -d "/var/opt/sonarr" ] && [ "$CONFDIR" != "/var/opt/sonarr" ] && [ ! -d "$CONFDIR" ]; then
|
||||
varoptRoot="/var/opt/sonarr"
|
||||
varoptAppData="$varoptRoot/.config/Sonarr"
|
||||
sonarrUserHome=`getent passwd $USER | cut -d ':' -f 6`
|
||||
sonarrAppData="$sonarrUserHome/.config/Sonarr"
|
||||
if [ -f "$varoptRoot/sonarr.db" ]; then
|
||||
# Handle /var/opt/sonarr/sonarr.db
|
||||
mv "$varoptRoot" "$CONFDIR"
|
||||
elif [ -f "$varoptAppData/sonarr.db" ]; then
|
||||
# Handle /var/opt/sonarr/.config/Sonarr/sonarr.db
|
||||
mv "$varoptAppData" "$CONFDIR"
|
||||
rm -rf "$varoptRoot"
|
||||
elif [ -f "$sonarrAppData/sonarr.db" ]; then
|
||||
# Handle ~/.config/Sonarr/sonarr.db
|
||||
mv "$sonarrAppData" "$CONFDIR"
|
||||
rm -rf "$sonarrAppData"
|
||||
else
|
||||
mv "$varoptRoot" "$CONFDIR"
|
||||
fi
|
||||
chown -R $USER:$GROUP "$CONFDIR"
|
||||
chmod -R 775 "$CONFDIR"
|
||||
fi
|
||||
|
||||
# Migrate old NzbDrone data dir
|
||||
if [ -d "/usr/lib/sonarr/nzbdrone-appdata" ] && [ ! -d "$CONFDIR" ]; then
|
||||
NZBDRONE_DATA=`readlink /usr/lib/sonarr/nzbdrone-appdata`
|
||||
if [ -f "$NZBDRONE_DATA/config.xml" ] && [ -f "$NZBDRONE_DATA/nzbdrone.db" ]; then
|
||||
echo "Found NzbDrone database in $NZBDRONE_DATA, copying to $CONFDIR."
|
||||
mkdir -p "$CONFDIR"
|
||||
cp $NZBDRONE_DATA/config.xml $NZBDRONE_DATA/nzbdrone.db* "$CONFDIR/"
|
||||
chown -R $USER:$GROUP "$CONFDIR"
|
||||
chmod -R 775 "$CONFDIR"
|
||||
else
|
||||
echo "Missing NzbDrone database in $NZBDRONE_DATA, skipping migration."
|
||||
fi
|
||||
rm /usr/lib/sonarr/nzbdrone-appdata
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create data directory
|
||||
if [ ! -d "$CONFDIR" ]; then
|
||||
mkdir -p "$CONFDIR"
|
||||
fi
|
||||
|
||||
# Set permissions on data directory (always do this instead only on creation in case user was changed via dpkg-reconfigure)
|
||||
chown -R $USER:$GROUP "$CONFDIR"
|
||||
|
||||
#BEGIN BUILTIN UPDATER
|
||||
# Apply patch if present
|
||||
if [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin_patch/release_info ]; then
|
||||
# It shouldn't be possible to get a wrong bin_patch, but let's check anyway and throw it away if it's wrong
|
||||
currentVersion=`cat /usr/lib/sonarr/bin_patch/release_info | grep 'ReleaseVersion=' | cut -d= -f 2`
|
||||
currentRelease=`echo "$currentVersion" | cut -d. -f1,2,3`
|
||||
currentBuild=`echo "$currentVersion" | cut -d. -f4`
|
||||
targetVersion=$BUILD_VERSION
|
||||
targetRelease=`echo "$targetVersion" | cut -d. -f1,2,3`
|
||||
targetBuild=`echo "$targetVersion" | cut -d. -f4`
|
||||
|
||||
if [ "$currentRelease" = "$targetRelease" ] && [ "$currentBuild" -gt "$targetBuild" ]; then
|
||||
echo "Applying $currentVersion from BuiltIn updater instead of downgrading to $targetVersion"
|
||||
rm -rf /usr/lib/sonarr/bin
|
||||
mv /usr/lib/sonarr/bin_patch /usr/lib/sonarr/bin
|
||||
else
|
||||
rm -rf /usr/lib/sonarr/bin_patch
|
||||
fi
|
||||
fi
|
||||
#END BUILTIN UPDATER
|
||||
|
||||
# Set permissions on /usr/lib/sonarr
|
||||
chown -R $USER:$GROUP /usr/lib/sonarr
|
||||
|
||||
# Update sonarr.service file
|
||||
sed -i "s:User=\w*:User=$USER:g; s:Group=\w*:Group=$GROUP:g; s:UMask=[0-9]*:UMask=$UMASK:g; s:-data=.*$:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
|
||||
|
||||
#BEGIN BUILTIN UPDATER
|
||||
if [ "$UPDATER" = "BuiltIn" ]; then
|
||||
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
|
||||
touch $CONFDIR/update_required
|
||||
chown $USER:$GROUP $CONFDIR/update_required
|
||||
fi
|
||||
#END BUILTIN UPDATER
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
36
distribution/debian/postrm
Normal file
36
distribution/debian/postrm
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
BUILD_VERSION={version}
|
||||
UPDATER={updater}
|
||||
|
||||
if [ $1 = "abort-install" ]; then
|
||||
# preinst was aborted, possibly due to NzbDrone still running.
|
||||
# Nothing to do here
|
||||
:
|
||||
fi
|
||||
|
||||
# The bin directory is expected to be empty, unless the BuiltIn updater added files.
|
||||
if [ $1 = "remove" ] && [ -d /usr/lib/sonarr/bin ]; then
|
||||
rm -rf /usr/lib/sonarr/bin
|
||||
fi
|
||||
|
||||
#BEGIN BUILTIN UPDATER
|
||||
# Remove any existing patch if still present
|
||||
if [ $1 = "remove" ] && [ -d /usr/lib/sonarr/bin_patch ]; then
|
||||
rm -rf /usr/lib/sonarr/bin_patch
|
||||
fi
|
||||
#END BUILTIN UPDATER
|
||||
|
||||
# Purge the entire sonarr configuration directory.
|
||||
# TODO: Maybe move a minimal backup to tmp?
|
||||
if [ $1 = "purge" ] && [ -e /usr/share/debconf/confmodule ]; then
|
||||
. /usr/share/debconf/confmodule
|
||||
db_get sonarr/config_directory
|
||||
CONFDIR="$RET"
|
||||
if [ -d "$CONFDIR" ]; then
|
||||
rm -rf "$CONFDIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
#DEBHELPER#
|
||||
91
distribution/debian/preinst
Normal file
91
distribution/debian/preinst
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
BUILD_VERSION={version}
|
||||
UPDATER={updater}
|
||||
|
||||
# Deal with existing nzbdrone installs
|
||||
#
|
||||
# Existing nzbdrone packages do not have startup scripts and the process might still be running.
|
||||
# If the user manually installed nzbdrone then the process might still be running too.
|
||||
if [ $1 = "install" ]; then
|
||||
psNzbDrone=`ps ax -o'user:20,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
|
||||
if [ ! -z "$psNzbDrone" ]; then
|
||||
# Get the user and optional systemd unit
|
||||
psNzbDroneUser=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 1`
|
||||
psNzbDroneUnit=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 4`
|
||||
# Get the appdata from the cmdline or get it from the user dir
|
||||
droneAppData=`echo "$psNzbDrone" | tr ' ' '\n' | grep -- "-data=" | cut -d= -f 2`
|
||||
if [ "$droneAppData" = "" ]; then
|
||||
droneUserHome=`getent passwd $psNzbDroneUser | cut -d ':' -f 6`
|
||||
droneAppData="$droneUserHome/.config/NzbDrone"
|
||||
fi
|
||||
|
||||
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
|
||||
if [ "$psNzbDroneUnit" = "sonarr.service" ]; then
|
||||
# Conflicts with our new sonarr.service so we have to remove it
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and removing..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
if [ -f "/etc/systemd/system/$psNzbDroneUnit" ]; then
|
||||
rm /etc/systemd/system/$psNzbDroneUnit
|
||||
fi
|
||||
if [ -f "/usr/lib/systemd/system/$psNzbDroneUnit" ]; then
|
||||
rm /usr/lib/systemd/system/$psNzbDroneUnit
|
||||
fi
|
||||
deb-systemd-helper purge $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-helper unmask $psNzbDroneUnit >/dev/null
|
||||
systemctl --system daemon-reload >/dev/null || true
|
||||
else
|
||||
# Just disable it, so the user can revisit the settings later
|
||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
||||
fi
|
||||
else
|
||||
# We don't support auto migration for other startup methods, so bail.
|
||||
# This leaves the sonarr package in an incomplete state.
|
||||
echo "ps: $psNzbDrone"
|
||||
echo "Error: An existing Sonarr v2 (NzbDrone) process is running. Remove the NzbDrone auto-startup prior to installing sonarr."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We don't have the debconf configuration yet so we can't migrate the data.
|
||||
# Instead we symlink so postinst knows where it's at.
|
||||
if [ -f "/usr/lib/sonarr/nzbdrone-appdata" ]; then
|
||||
rm "/usr/lib/sonarr/nzbdrone-appdata"
|
||||
else
|
||||
mkdir -p "/usr/lib/sonarr"
|
||||
fi
|
||||
ln -s $droneAppData /usr/lib/sonarr/nzbdrone-appdata
|
||||
fi
|
||||
fi
|
||||
|
||||
#BEGIN BUILTIN UPDATER
|
||||
# Check for supported upgrade paths
|
||||
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin/release_info ]; then
|
||||
# If we allow the Built-In updater to upgrade from 3.0.1.123 to 3.0.2.500 and now apt is catching up to 3.0.2.425
|
||||
# then we need to deal with that 500->425 'downgrade'.
|
||||
# We do that by preserving the binaries and using those instead for postinst.
|
||||
|
||||
currentVersion=`cat /usr/lib/sonarr/bin/release_info | grep 'ReleaseVersion=' | cut -d= -f 2`
|
||||
currentRelease=`echo "$currentVersion" | cut -d. -f1,2,3`
|
||||
currentBuild=`echo "$currentVersion" | cut -d. -f4`
|
||||
targetVersion=$BUILD_VERSION
|
||||
targetRelease=`echo "$targetVersion" | cut -d. -f1,2,3`
|
||||
targetBuild=`echo "$targetVersion" | cut -d. -f4`
|
||||
|
||||
if [ -d /usr/lib/sonarr/bin_patch ]; then
|
||||
rm -rf /usr/lib/sonarr/bin_patch
|
||||
fi
|
||||
|
||||
# Check if the existing version is already an upgrade for the included build
|
||||
if [ "$currentRelease" = "$targetRelease" ] && [ "$currentBuild" -gt "$targetBuild" ]; then
|
||||
echo "Preserving $currentVersion from BuiltIn updater instead of downgrading to $targetVersion"
|
||||
cp -r /usr/lib/sonarr/bin /usr/lib/sonarr/bin_patch
|
||||
fi
|
||||
fi
|
||||
#END BUILTIN UPDATER
|
||||
|
||||
#DEBHELPER#
|
||||
|
||||
exit 0
|
||||
21
distribution/debian/rules
Normal file
21
distribution/debian/rules
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd --with=cli
|
||||
|
||||
# No init script, only systemd
|
||||
override_dh_installinit:
|
||||
true
|
||||
|
||||
# Sonarr likes debug symbols for logging
|
||||
override_dh_clistrip:
|
||||
|
||||
override_dh_makeclilibs:
|
||||
|
||||
override_dh_clideps:
|
||||
dh_clideps -d -r $(patsubst %,--exclude-moduleref=%,$(EXCLUDE_MODULEREFS))
|
||||
2
distribution/debian/sonarr.clideps-override
Normal file
2
distribution/debian/sonarr.clideps-override
Normal file
@@ -0,0 +1,2 @@
|
||||
ignores msbuild
|
||||
ignores libc6
|
||||
@@ -11,7 +11,7 @@ Group=sonarr
|
||||
UMask=002
|
||||
|
||||
Type=simple
|
||||
ExecStart=/opt/Sonarr/Sonarr -nobrowser -data=/var/lib/sonarr
|
||||
ExecStart=/usr/lib/sonarr/bin/Sonarr -nobrowser -data=/var/lib/sonarr
|
||||
TimeoutStopSec=20
|
||||
KillMode=process
|
||||
Restart=on-failure
|
||||
|
||||
27
distribution/debian/templates
Normal file
27
distribution/debian/templates
Normal file
@@ -0,0 +1,27 @@
|
||||
Template: sonarr/owning_user
|
||||
Type: string
|
||||
Default: sonarr
|
||||
Description: Sonarr user:
|
||||
Specify the user that is used to run Sonarr. The user will be created if it does not already exist.
|
||||
The default 'sonarr' should work fine for most users. You can specify the user group next.
|
||||
|
||||
Template: sonarr/owning_group
|
||||
Type: string
|
||||
Default: sonarr
|
||||
Description: Sonarr group:
|
||||
Specify the group that is used to run Sonarr. The group will be created if it does not already exist.
|
||||
If the user doesn't already exist then this group will be used as the user's primary group.
|
||||
Any media files created by Sonarr will be writeable by this group.
|
||||
It's advisable to keep the group the same between download client, Sonarr and media centers.
|
||||
|
||||
Template: sonarr/owning_umask
|
||||
Type: string
|
||||
Default: 0002
|
||||
Description: Sonarr umask:
|
||||
Specifies the umask of the files created by Sonarr. 0002 means the files will be created with 664 as permissions.
|
||||
|
||||
Template: sonarr/config_directory
|
||||
Type: string
|
||||
Default: /var/lib/sonarr
|
||||
Description: Config directory:
|
||||
Specify the directory where Sonarr stores the internal database and metadata. Media content will be stored elsewhere.
|
||||
@@ -23,11 +23,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.0</string>
|
||||
<string>3.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>xmmd</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4.0</string>
|
||||
<string>3.0</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<string>YES</string>
|
||||
</dict>
|
||||
|
||||
@@ -3,5 +3,8 @@
|
||||
@REM SET BRANCH=develop
|
||||
@REM SET FRAMEWORK=net6.0
|
||||
@REM SET RUNTIME=win-x64
|
||||
|
||||
echo ##teamcity[progressStart 'Building setup file']
|
||||
inno\ISCC.exe sonarr.iss
|
||||
echo ##teamcity[progressFinish 'Building setup file']
|
||||
|
||||
echo ##teamcity[publishArtifacts 'distribution\windows\setup\output\*%RUNTIME%*.exe']
|
||||
|
||||
@@ -40,7 +40,7 @@ Compression=lzma2/normal
|
||||
AppContact={#ForumsURL}
|
||||
VersionInfoVersion={#MajorVersion}
|
||||
SetupLogging=yes
|
||||
OutputDir="..\..\..\_artifacts"
|
||||
OutputDir=output
|
||||
AppverName={#AppName}
|
||||
|
||||
[Languages]
|
||||
@@ -53,8 +53,8 @@ Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts
|
||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
|
||||
[Files]
|
||||
Source: "..\..\..\_output\{#Runtime}\{#Framework}\Sonarr\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\..\..\_output\{#Runtime}\{#Framework}\Sonarr\*"; Excludes: "Sonarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Sonarr\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "..\..\..\_artifacts\{#Runtime}\{#Framework}\Sonarr\*"; Excludes: "Sonarr.Update"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||
|
||||
[Icons]
|
||||
|
||||
13
docs.sh
13
docs.sh
@@ -1,7 +1,6 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
@@ -25,23 +24,17 @@ slnFile=src/Sonarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
application=Sonarr.Console.dll
|
||||
else
|
||||
application=Sonarr.dll
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
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 6.5.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/Sonarr.dll" v3 &
|
||||
|
||||
sleep 45
|
||||
sleep 30
|
||||
|
||||
kill %1
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -21,7 +20,6 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||
|
||||
class Blocklist extends Component {
|
||||
@@ -116,13 +114,9 @@ class Blocklist extends Component {
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlocklistExecuting,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -167,15 +161,6 @@ class Blocklist extends Component {
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={BlocklistFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
@@ -195,11 +180,7 @@ class Blocklist extends Component {
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
{
|
||||
selectedFilterKey === 'all' ?
|
||||
translate('NoHistoryBlocklist') :
|
||||
translate('BlocklistFilterHasNoItems')
|
||||
}
|
||||
{translate('NoHistoryBlocklist')}
|
||||
</Alert>
|
||||
}
|
||||
|
||||
@@ -270,15 +251,11 @@ Blocklist.propTypes = {
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlocklistPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
onClearBlocklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blocklist;
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Blocklist from './Blocklist';
|
||||
@@ -14,12 +13,10 @@ import Blocklist from './Blocklist';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blocklist,
|
||||
createCustomFiltersSelector('blocklist'),
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, customFilters, isClearingBlocklistExecuting) => {
|
||||
(blocklist, isClearingBlocklistExecuting) => {
|
||||
return {
|
||||
isClearingBlocklistExecuting,
|
||||
customFilters,
|
||||
...blocklist
|
||||
};
|
||||
}
|
||||
@@ -100,10 +97,6 @@ class BlocklistConnector extends Component {
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setBlocklistFilter({ selectedFilterKey });
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
};
|
||||
@@ -129,7 +122,6 @@ class BlocklistConnector extends Component {
|
||||
onPageSelect={this.onPageSelect}
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||
{...this.props}
|
||||
@@ -150,7 +142,6 @@ BlocklistConnector.propTypes = {
|
||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||
removeBlocklistItems: PropTypes.func.isRequired,
|
||||
setBlocklistSort: PropTypes.func.isRequired,
|
||||
setBlocklistFilter: PropTypes.func.isRequired,
|
||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlocklist: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||
|
||||
function createBlocklistSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.blocklist.items,
|
||||
(blocklistItems) => {
|
||||
return blocklistItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.blocklist.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface BlocklistFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||
const sectionItems = useSelector(createBlocklistSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'blocklist';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setBlocklistFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryEventTypeCell.css';
|
||||
|
||||
function getIconName(eventType, data) {
|
||||
function getIconName(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return icons.DOWNLOADING;
|
||||
@@ -17,7 +17,7 @@ function getIconName(eventType, data) {
|
||||
case 'downloadFailed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'episodeFileDeleted':
|
||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||
return icons.DELETE;
|
||||
case 'episodeFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
|
||||
case 'downloadFailed':
|
||||
return translate('DownloadFailedEpisodeTooltip');
|
||||
case 'episodeFileDeleted':
|
||||
return data.reason === 'MissingFromDisk' ? translate('EpisodeFileMissingTooltip') : translate('EpisodeFileDeletedTooltip');
|
||||
return translate('EpisodeFileDeletedTooltip');
|
||||
case 'episodeFileRenamed':
|
||||
return translate('EpisodeFileRenamedTooltip');
|
||||
case 'downloadIgnored':
|
||||
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, data }) {
|
||||
const iconName = getIconName(eventType, data);
|
||||
const iconName = getIconName(eventType);
|
||||
const iconKind = getIconKind(eventType);
|
||||
const tooltip = getTooltip(eventType, data);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class HistoryRow extends Component {
|
||||
onMarkAsFailedPress
|
||||
} = this.props;
|
||||
|
||||
if (!series || !episode) {
|
||||
if (!episode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -217,7 +217,6 @@ class Queue extends Component {
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
maxPageSize={200}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
@@ -306,16 +305,9 @@ class Queue extends Component {
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
@@ -323,7 +315,7 @@ class Queue extends Component {
|
||||
return !!(item && item.seriesId && item.episodeId);
|
||||
})
|
||||
)}
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
|
||||
@@ -99,9 +99,7 @@ class QueueRow extends Component {
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
@@ -364,15 +362,6 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -421,7 +410,6 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!series}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -452,9 +440,7 @@ QueueRow.propTypes = {
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
sizeleft: PropTypes.number,
|
||||
|
||||
@@ -70,11 +70,6 @@ function QueueStatus(props) {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = translate('Downloaded');
|
||||
|
||||
if (trackedDownloadState === 'importBlocked') {
|
||||
title += ` - ${translate('UnableToImportAutomatically')}`;
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
|
||||
171
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
171
frontend/src/Activity/Queue/RemoveQueueItemModal.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('RemoveQueueItem', { sourceTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
@@ -1,231 +0,0 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
remove: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
skipRedownload: boolean;
|
||||
}
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
selectedCount?: number;
|
||||
onRemovePress(props: RemovePressProps): void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
selectedCount,
|
||||
onRemovePress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedCount === 1) {
|
||||
return {
|
||||
title: translate('RemoveSelectedItem'),
|
||||
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: translate('RemoveSelectedItems'),
|
||||
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||
selectedCount,
|
||||
}),
|
||||
};
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
hint: multipleSelected
|
||||
? translate('RemoveMultipleFromDownloadClientHint')
|
||||
: translate('RemoveFromDownloadClientHint'),
|
||||
},
|
||||
{
|
||||
key: 'changeCategory',
|
||||
value: translate('ChangeCategory'),
|
||||
isDisabled: !canChangeCategory,
|
||||
hint: multipleSelected
|
||||
? translate('ChangeCategoryMultipleHint')
|
||||
: translate('ChangeCategoryHint'),
|
||||
},
|
||||
{
|
||||
key: 'ignore',
|
||||
value: multipleSelected
|
||||
? translate('IgnoreDownloads')
|
||||
: translate('IgnoreDownload'),
|
||||
isDisabled: !canIgnore,
|
||||
hint: multipleSelected
|
||||
? translate('IgnoreDownloadsHint')
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
hint: translate('DoNotBlocklistHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
isDisabled: isPending,
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [isPending, multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
remove: removalMethod === 'removeFromClient',
|
||||
changeCategory: removalMethod === 'changeCategory',
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>{message}</div>
|
||||
|
||||
{isPending ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="removalMethod"
|
||||
value={removalMethod}
|
||||
values={removalMethodOptions}
|
||||
isDisabled={!canChangeCategory && !canIgnore}
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{multipleSelected
|
||||
? translate('BlocklistReleases')
|
||||
: translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="blocklistMethod"
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
174
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
174
frontend/src/Activity/Queue/RemoveQueueItemsModal.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
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 { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
||||
@@ -24,11 +24,7 @@ function TimeleftCell(props) {
|
||||
} = props;
|
||||
|
||||
if (status === 'delay') {
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates
|
||||
});
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
@@ -44,11 +40,7 @@ function TimeleftCell(props) {
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
const date = getRelativeDate({
|
||||
date: estimatedCompletionTime,
|
||||
shortDateFormat,
|
||||
showRelativeDates
|
||||
});
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
|
||||
@@ -69,11 +69,6 @@
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.originalLanguageName,
|
||||
.network {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.tvdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@ interface CssExports {
|
||||
'alreadyExistsIcon': string;
|
||||
'content': string;
|
||||
'icons': string;
|
||||
'network': string;
|
||||
'originalLanguageName': string;
|
||||
'overlay': string;
|
||||
'overview': string;
|
||||
'poster': string;
|
||||
|
||||
@@ -55,7 +55,6 @@ class AddNewSeriesSearchResult extends Component {
|
||||
titleSlug,
|
||||
year,
|
||||
network,
|
||||
originalLanguage,
|
||||
status,
|
||||
overview,
|
||||
statistics,
|
||||
@@ -146,37 +145,14 @@ class AddNewSeriesSearchResult extends Component {
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
votes={ratings.votes}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
originalLanguage?.name ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.LANGUAGE}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.originalLanguageName}>
|
||||
{originalLanguage.name}
|
||||
</span>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
network ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.NETWORK}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.network}>
|
||||
{network}
|
||||
</span>
|
||||
{network}
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
@@ -242,7 +218,6 @@ AddNewSeriesSearchResult.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
network: PropTypes.string,
|
||||
originalLanguage: PropTypes.object,
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
|
||||
@@ -118,7 +118,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
className={styles.addErrorAlert}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{translate('AddRootFolderError')}
|
||||
{translate('RootFolderLoadError')}
|
||||
|
||||
<ul>
|
||||
{
|
||||
|
||||
@@ -12,10 +12,11 @@ function App({ store, history }) {
|
||||
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<ApplyTheme />
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
<ApplyTheme>
|
||||
<PageConnector>
|
||||
<AppRoutes app={App} />
|
||||
</PageConnector>
|
||||
</ApplyTheme>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
</DocumentTitle>
|
||||
|
||||
@@ -30,7 +30,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Status from 'System/Status/Status';
|
||||
import Tasks from 'System/Tasks/Tasks';
|
||||
import Updates from 'System/Updates/Updates';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||
@@ -248,7 +248,7 @@ function AppRoutes(props) {
|
||||
|
||||
<Route
|
||||
path="/system/updates"
|
||||
component={Updates}
|
||||
component={UpdatesConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
||||
50
frontend/src/App/ApplyTheme.js
Normal file
50
frontend/src/App/ApplyTheme.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui.item.theme || window.Sonarr.theme,
|
||||
(
|
||||
theme
|
||||
) => {
|
||||
return {
|
||||
theme
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ theme, children }) {
|
||||
// Update the CSS Variables
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
||||
|
||||
// Loop through each array key and set the CSS Variables
|
||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
||||
// Based on our snippet from MDN
|
||||
document.documentElement.style.setProperty(
|
||||
`--${cssVariableKey}`,
|
||||
arrayOfVariableValues[index]
|
||||
);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables(theme);
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
ApplyTheme.propTypes = {
|
||||
theme: PropTypes.string.isRequired,
|
||||
children: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(ApplyTheme);
|
||||
@@ -1,37 +0,0 @@
|
||||
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import themes from 'Styles/Themes';
|
||||
import AppState from './State/AppState';
|
||||
|
||||
interface ApplyThemeProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createThemeSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.ui.item.theme || window.Sonarr.theme,
|
||||
(theme) => {
|
||||
return theme;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||
const theme = useSelector(createThemeSelector());
|
||||
|
||||
const updateCSSVariables = useCallback(() => {
|
||||
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, [theme]);
|
||||
|
||||
// On Component Mount and Component Update
|
||||
useEffect(() => {
|
||||
updateCSSVariables();
|
||||
}, [updateCSSVariables, theme]);
|
||||
|
||||
return <Fragment>{children}</Fragment>;
|
||||
}
|
||||
|
||||
export default ApplyTheme;
|
||||
@@ -19,7 +19,6 @@ export interface AppSectionSaveState {
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
pageSize: number;
|
||||
totalRecords?: number;
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
@@ -39,7 +38,6 @@ export interface AppSectionItemState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
pendingChanges: Partial<T>;
|
||||
item: T;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||
@@ -45,18 +44,7 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
version: string;
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
blocklist: BlocklistAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
|
||||
|
||||
interface BlocklistAppState
|
||||
extends AppSectionState<Blocklist>,
|
||||
AppSectionFilterState<Blocklist> {}
|
||||
|
||||
export default BlocklistAppState;
|
||||
@@ -20,7 +20,6 @@ export interface SeriesIndexAppState {
|
||||
showTitle: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
@@ -35,7 +34,6 @@ export interface SeriesIndexAppState {
|
||||
showSeasonCount: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
showTags: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,27 +3,20 @@ import AppSectionState, {
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
PagedAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||
import Indexer from 'typings/Indexer';
|
||||
import IndexerFlag from 'typings/IndexerFlag';
|
||||
import Notification from 'typings/Notification';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import General from 'typings/Settings/General';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export type GeneralAppState = AppSectionItemState<General>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
AppSectionDeleteState,
|
||||
@@ -42,30 +35,12 @@ export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export interface ImportListOptionsSettingsAppState
|
||||
extends AppSectionItemState<ImportListOptionsSettings>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface ImportListExclusionsSettingsAppState
|
||||
extends AppSectionState<ImportListExclusion>,
|
||||
AppSectionSaveState,
|
||||
PagedAppSectionState,
|
||||
AppSectionDeleteState {
|
||||
pendingChanges: Partial<ImportListExclusion>;
|
||||
}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
advancedSettings: boolean;
|
||||
downloadClients: DownloadClientAppState;
|
||||
general: GeneralAppState;
|
||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||
importListOptions: ImportListOptionsSettingsAppState;
|
||||
importLists: ImportListAppState;
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
languages: LanguageSettingsAppState;
|
||||
notifications: NotificationAppState;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import SystemStatus from 'typings/SystemStatus';
|
||||
import Update from 'typings/Update';
|
||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
export type UpdateAppState = AppSectionState<Update>;
|
||||
|
||||
interface SystemAppState {
|
||||
updates: UpdateAppState;
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
|
||||
if (view === calendarViews.WEEK) {
|
||||
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
||||
} else if (view === calendarViews.FORECAST) {
|
||||
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
|
||||
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -52,10 +52,6 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
|
||||
.statusContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:global(.fullColor) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
|
||||
@@ -102,12 +102,7 @@ class CalendarEvent extends Component {
|
||||
{series.title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
styles.statusContainer,
|
||||
fullColorEvents && 'fullColor'
|
||||
)}
|
||||
>
|
||||
<div className={styles.statusContainer}>
|
||||
{
|
||||
missingAbsoluteNumber ?
|
||||
<Icon
|
||||
@@ -133,7 +128,6 @@ class CalendarEvent extends Component {
|
||||
<span className={styles.statusIcon}>
|
||||
<CalendarEventQueueDetails
|
||||
{...queueItem}
|
||||
fullColorEvents={fullColorEvents}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
@@ -156,7 +150,7 @@ class CalendarEvent extends Component {
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.EPISODE_FILE}
|
||||
kind={kinds.WARNING}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
title={translate('QualityCutoffNotMet')}
|
||||
/> :
|
||||
null
|
||||
@@ -166,8 +160,9 @@ class CalendarEvent extends Component {
|
||||
episodeNumber === 1 && seasonNumber > 0 ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.PREMIERE}
|
||||
name={icons.INFO}
|
||||
kind={kinds.INFO}
|
||||
darken={fullColorEvents}
|
||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||
/> :
|
||||
null
|
||||
@@ -178,8 +173,8 @@ class CalendarEvent extends Component {
|
||||
finaleType ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON}
|
||||
kind={finaleType === 'series' ? kinds.DANGER : kinds.WARNING}
|
||||
name={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
title={getFinaleTypeName(finaleType)}
|
||||
/> :
|
||||
null
|
||||
@@ -192,6 +187,7 @@ class CalendarEvent extends Component {
|
||||
className={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={kinds.PINK}
|
||||
darken={fullColorEvents}
|
||||
title={translate('Special')}
|
||||
/> :
|
||||
null
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
.expandContainer,
|
||||
.collapseContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -51,15 +50,6 @@
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.statusContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:global(.fullColor) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ interface CssExports {
|
||||
'onAir': string;
|
||||
'premiere': string;
|
||||
'seriesTitle': string;
|
||||
'statusContainer': string;
|
||||
'statusIcon': string;
|
||||
'unaired': string;
|
||||
'unmonitored': string;
|
||||
|
||||
@@ -145,51 +145,45 @@ class CalendarEventGroup extends Component {
|
||||
{series.title}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
styles.statusContainer,
|
||||
fullColorEvents && 'fullColor'
|
||||
)}
|
||||
>
|
||||
{
|
||||
isMissingAbsoluteNumber &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.WARNING}
|
||||
title={translate('EpisodeMissingAbsoluteNumber')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
isMissingAbsoluteNumber &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.WARNING}
|
||||
title={translate('EpisodeMissingAbsoluteNumber')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
anyDownloading &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.DOWNLOADING}
|
||||
title={translate('AnEpisodeIsDownloading')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
anyDownloading &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.DOWNLOADING}
|
||||
title={translate('AnEpisodeIsDownloading')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.PREMIERE}
|
||||
kind={kinds.INFO}
|
||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||
/>
|
||||
}
|
||||
{
|
||||
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={kinds.INFO}
|
||||
darken={fullColorEvents}
|
||||
title={seasonNumber === 1 ? translate('SeriesPremiere') : translate('SeasonPremiere')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
showFinaleIcon &&
|
||||
lastEpisode.finaleType ?
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={lastEpisode.finaleType === 'series' ? icons.FINALE_SERIES : icons.FINALE_SEASON}
|
||||
kind={lastEpisode.finaleType === 'series' ? kinds.DANGER : kinds.WARNING}
|
||||
title={getFinaleTypeName(lastEpisode.finaleType)}
|
||||
/> : null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
showFinaleIcon &&
|
||||
lastEpisode.finaleType ?
|
||||
<Icon
|
||||
containerClassName={styles.statusIcon}
|
||||
name={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
title={getFinaleTypeName(lastEpisode.finaleType)}
|
||||
/> : null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.airingInfo}>
|
||||
@@ -224,19 +218,16 @@ class CalendarEventGroup extends Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
showEpisodeInformation ?
|
||||
showEpisodeInformation &&
|
||||
<Link
|
||||
className={styles.expandContainer}
|
||||
component="div"
|
||||
onPress={this.onExpandPress}
|
||||
>
|
||||
|
||||
<Icon
|
||||
name={icons.EXPAND}
|
||||
/>
|
||||
|
||||
</Link> :
|
||||
null
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ function createIsDownloadingSelector() {
|
||||
(state) => state.queue.details,
|
||||
(episodeIds, details) => {
|
||||
return details.items.some((item) => {
|
||||
return !!(item.episodeId && episodeIds.includes(item.episodeId));
|
||||
return item.episode && episodeIds.includes(item.episode.id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -22,20 +22,9 @@ function Legend(props) {
|
||||
if (showFinaleIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name={translate('SeasonFinale')}
|
||||
icon={icons.FINALE_SEASON}
|
||||
kind={kinds.WARNING}
|
||||
fullColorEvents={fullColorEvents}
|
||||
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
||||
/>
|
||||
);
|
||||
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name={translate('SeriesFinale')}
|
||||
icon={icons.FINALE_SERIES}
|
||||
kind={kinds.DANGER}
|
||||
fullColorEvents={fullColorEvents}
|
||||
name="Finale"
|
||||
icon={icons.INFO}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
tooltip={translate('CalendarLegendSeriesFinaleTooltip')}
|
||||
/>
|
||||
);
|
||||
@@ -44,10 +33,10 @@ function Legend(props) {
|
||||
if (showSpecialIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name={translate('Special')}
|
||||
name="Special"
|
||||
icon={icons.INFO}
|
||||
kind={kinds.PINK}
|
||||
fullColorEvents={fullColorEvents}
|
||||
darken={fullColorEvents}
|
||||
tooltip={translate('SpecialEpisode')}
|
||||
/>
|
||||
);
|
||||
@@ -56,10 +45,9 @@ function Legend(props) {
|
||||
if (showCutoffUnmetIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name={translate('Cutoff Not Met')}
|
||||
name="Cutoff Not Met"
|
||||
icon={icons.EPISODE_FILE}
|
||||
kind={kinds.WARNING}
|
||||
fullColorEvents={fullColorEvents}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
tooltip={translate('QualityCutoffNotMet')}
|
||||
/>
|
||||
);
|
||||
@@ -124,10 +112,10 @@ function Legend(props) {
|
||||
|
||||
<div>
|
||||
<LegendIconItem
|
||||
name={translate('Premiere')}
|
||||
icon={icons.PREMIERE}
|
||||
name="Premiere"
|
||||
icon={icons.INFO}
|
||||
kind={kinds.INFO}
|
||||
fullColorEvents={fullColorEvents}
|
||||
darken={true}
|
||||
tooltip={translate('CalendarLegendSeriesPremiereTooltip')}
|
||||
/>
|
||||
|
||||
@@ -141,12 +129,6 @@ function Legend(props) {
|
||||
{iconsToShow[2]}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
iconsToShow.length > 3 &&
|
||||
<div>
|
||||
{iconsToShow[3]}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,4 @@
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
|
||||
&:global(.fullColorEvents) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -7,9 +6,9 @@ import styles from './LegendIconItem.css';
|
||||
function LegendIconItem(props) {
|
||||
const {
|
||||
name,
|
||||
fullColorEvents,
|
||||
icon,
|
||||
kind,
|
||||
darken,
|
||||
tooltip
|
||||
} = props;
|
||||
|
||||
@@ -19,11 +18,9 @@ function LegendIconItem(props) {
|
||||
title={tooltip}
|
||||
>
|
||||
<Icon
|
||||
className={classNames(
|
||||
styles.icon,
|
||||
fullColorEvents && 'fullColorEvents'
|
||||
)}
|
||||
className={styles.icon}
|
||||
name={icon}
|
||||
darken={darken}
|
||||
kind={kind}
|
||||
/>
|
||||
|
||||
@@ -34,10 +31,14 @@ function LegendIconItem(props) {
|
||||
|
||||
LegendIconItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
fullColorEvents: PropTypes.bool.isRequired,
|
||||
icon: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
darken: PropTypes.bool.isRequired,
|
||||
tooltip: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
LegendIconItem.defaultProps = {
|
||||
darken: false
|
||||
};
|
||||
|
||||
export default LegendIconItem;
|
||||
|
||||
@@ -13,8 +13,6 @@ export interface CommandBody {
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
seriesId?: number;
|
||||
seriesIds?: number[];
|
||||
seasonNumber?: number;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
||||
@@ -6,7 +6,7 @@ export const CLEAR_LOGS = 'ClearLog';
|
||||
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
||||
export const DELETE_LOG_FILES = 'DeleteLogFiles';
|
||||
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
|
||||
export const DOWNLOADED_EPISODES_SCAN = 'DownloadedEpisodesScan';
|
||||
export const DOWNLOADED_EPSIODES_SCAN = 'DownloadedEpisodesScan';
|
||||
export const EPISODE_SEARCH = 'EpisodeSearch';
|
||||
export const INTERACTIVE_IMPORT = 'ManualImport';
|
||||
export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';
|
||||
|
||||
@@ -117,7 +117,7 @@ class FileBrowserModalContent extends Component {
|
||||
className={styles.mappedDrivesWarning}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService', { url: 'https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server' })} />
|
||||
<InlineMarkdown data={translate('MappedNetworkDrivesWindowsService')} />
|
||||
</Alert>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { maxBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -9,7 +8,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRow from './FilterBuilderRow';
|
||||
import styles from './FilterBuilderModalContent.css';
|
||||
|
||||
@@ -51,7 +49,7 @@ class FilterBuilderModalContent extends Component {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = maxBy(customFilters, 'id');
|
||||
const last = customFilters[customFilters.length -1];
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
@@ -109,7 +107,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
labelErrors: [
|
||||
{
|
||||
message: translate('LabelIsRequired')
|
||||
message: 'Label is required'
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -147,13 +145,13 @@ class FilterBuilderModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('CustomFilter')}
|
||||
Custom Filter
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{translate('Label')}
|
||||
Label
|
||||
</div>
|
||||
|
||||
<div className={styles.labelInputContainer}>
|
||||
@@ -167,9 +165,7 @@ class FilterBuilderModalContent extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.label}>
|
||||
{translate('Filters')}
|
||||
</div>
|
||||
<div className={styles.label}>Filters</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{
|
||||
@@ -196,7 +192,7 @@ class FilterBuilderModalContent extends Component {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onCancelPress}>
|
||||
{translate('Cancel')}
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
@@ -204,7 +200,7 @@ class FilterBuilderModalContent extends Component {
|
||||
error={saveError}
|
||||
onPress={this.onSaveFilterPress}
|
||||
>
|
||||
{translate('Save')}
|
||||
Save
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -3,7 +3,6 @@ import React, { Component } from 'react';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
@@ -225,7 +224,7 @@ class FilterBuilderRow extends Component {
|
||||
key: name,
|
||||
value: typeof label === 'function' ? label() : label
|
||||
};
|
||||
}).sort(sortByProp('value'));
|
||||
}).sort((a, b) => a.value.localeCompare(b.value));
|
||||
|
||||
const ValueComponent = getRowValueConnector(selectedFilterBuilderProp);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { filterBuilderTypes } from 'Helpers/Props';
|
||||
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
function createTagListSelector() {
|
||||
@@ -38,7 +38,7 @@ function createTagListSelector() {
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(sortByProp('name'));
|
||||
}, []).sort(sortByName);
|
||||
}
|
||||
|
||||
return _.uniqBy(items, 'id');
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Series from 'Series/Series';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
@@ -11,7 +11,7 @@ function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
|
||||
const tagList = allSeries
|
||||
.map((series) => ({ id: series.id, name: series.title }))
|
||||
.sort(sortByProp('name'));
|
||||
.sort(sortByName);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
||||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// Assume that delete and then unmounting means the delete was successful.
|
||||
// Moving this check to a ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CustomFilter from './CustomFilter';
|
||||
import styles from './CustomFiltersModalContent.css';
|
||||
@@ -31,24 +30,22 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => sortByProp(a, b, 'label'))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
customFilters.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
@@ -23,11 +23,10 @@ function createMapStateToProps() {
|
||||
|
||||
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
|
||||
|
||||
const values = _.map(filteredItems.sort(sortByProp('name')), (downloadClient) => {
|
||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||
return {
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name,
|
||||
hint: `(${downloadClient.id})`
|
||||
value: downloadClient.name
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
|
||||
@@ -271,32 +271,26 @@ class EnhancedSelectInput extends Component {
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
};
|
||||
|
||||
onSelect = (newValue) => {
|
||||
const { name, value, values, onChange } = this.props;
|
||||
const additionalProperties = values.find((v) => v.key === newValue)?.additionalProperties;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
let arrayValue = null;
|
||||
const index = value.indexOf(newValue);
|
||||
|
||||
onSelect = (value) => {
|
||||
if (Array.isArray(this.props.value)) {
|
||||
let newValue = null;
|
||||
const index = this.props.value.indexOf(value);
|
||||
if (index === -1) {
|
||||
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
|
||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
||||
} else {
|
||||
arrayValue = [...value];
|
||||
arrayValue.splice(index, 1);
|
||||
newValue = [...this.props.value];
|
||||
newValue.splice(index, 1);
|
||||
}
|
||||
onChange({
|
||||
name,
|
||||
value: arrayValue,
|
||||
additionalProperties
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
});
|
||||
} else {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue,
|
||||
additionalProperties
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -491,7 +485,7 @@ class EnhancedSelectInput extends Component {
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
|
||||
@@ -9,8 +9,7 @@ import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
const importantFieldNames = [
|
||||
'baseUrl',
|
||||
'apiPath',
|
||||
'apiKey',
|
||||
'authToken'
|
||||
'apiKey'
|
||||
];
|
||||
|
||||
function getProviderDataKey(providerData) {
|
||||
@@ -35,9 +34,7 @@ function getSelectOptions(items) {
|
||||
key: option.value,
|
||||
value: option.name,
|
||||
hint: option.hint,
|
||||
parentKey: option.parentValue,
|
||||
isDisabled: option.isDisabled,
|
||||
additionalProperties: option.additionalProperties
|
||||
parentKey: option.parentValue
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -150,7 +147,7 @@ EnhancedSelectInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -11,7 +11,6 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
|
||||
@@ -22,7 +21,6 @@ import PasswordInput from './PasswordInput';
|
||||
import PathInputConnector from './PathInputConnector';
|
||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||
import SeriesTagInput from './SeriesTagInput';
|
||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
import TagSelectInputConnector from './TagSelectInputConnector';
|
||||
@@ -73,9 +71,6 @@ function getComponent(type) {
|
||||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInput;
|
||||
|
||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||
return DownloadClientSelectInputConnector;
|
||||
|
||||
@@ -88,9 +83,6 @@ function getComponent(type) {
|
||||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.SERIES_TAG:
|
||||
return SeriesTagInput;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
@@ -272,7 +264,6 @@ FormInputGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
isDisabled: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
min: PropTypes.number,
|
||||
@@ -287,7 +278,6 @@ FormInputGroup.propTypes = {
|
||||
includeNoChange: PropTypes.bool,
|
||||
includeNoChangeDisabled: PropTypes.bool,
|
||||
selectedValueOptions: PropTypes.object,
|
||||
indexerFlags: PropTypes.number,
|
||||
pending: PropTypes.bool,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||
createSelector(
|
||||
(state: AppState) => state.settings.indexerFlags,
|
||||
(indexerFlags) => {
|
||||
const value = indexerFlags.items.reduce((acc: number[], { id }) => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if ((selectedFlags & id) === id) {
|
||||
acc.push(id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const values = indexerFlags.items.map(({ id, name }) => ({
|
||||
key: id,
|
||||
value: name,
|
||||
}));
|
||||
|
||||
return {
|
||||
value,
|
||||
values,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface IndexerFlagsSelectInputProps {
|
||||
name: string;
|
||||
indexerFlags: number;
|
||||
onChange(payload: object): void;
|
||||
}
|
||||
|
||||
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
||||
const { indexerFlags, onChange } = props;
|
||||
|
||||
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
||||
|
||||
const onChangeWrapper = useCallback(
|
||||
({ name, value }: { name: string; value: number[] }) => {
|
||||
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
||||
|
||||
onChange({ name, value: indexerFlags });
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...props}
|
||||
value={value}
|
||||
values={values}
|
||||
onChange={onChangeWrapper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default IndexerFlagsSelectInput;
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
@@ -20,7 +20,7 @@ function createMapStateToProps() {
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
|
||||
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||
return {
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user