1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-04-17 21:26:13 -04:00

Compare commits

..

72 Commits

Author SHA1 Message Date
Mark McDowall
6afd3bd344 Bump version to 4.0.7 2024-07-14 16:44:07 -07:00
Weblate
acaf5cd353 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Rauniik <raunerjakub@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-14 16:43:56 -07:00
Sonarr
e97e5bfe8f Automated API Docs update
ignore-downstream
2024-07-09 22:22:48 -07:00
martylukyy
678872b879 Fixed: Parsing of some Web releases 2024-07-10 01:19:07 -04:00
Mark McDowall
10e9735c1c New: Update AutoTags on series update
Closes #6783
2024-07-10 01:02:23 -04:00
Mark McDowall
293a1bc618 New: Custom colon replacement option
Closes #6898
2024-07-10 01:02:04 -04:00
Bogdan
0c883f7886 Fixed: Removing pending release without blocklisting 2024-07-09 22:01:23 -07:00
Mark McDowall
46c7de379c New: Group updates for the same series for Kodi and Emby / Jellyfin 2024-07-09 22:00:57 -07:00
Mark McDowall
a83b521766 New: 'On Import Complete' notification when all episodes in a release are imported
Closes #363
2024-07-09 22:00:57 -07:00
Bogdan
1d06e40acb New: Queued episode count for seasons in series details 2024-07-09 21:59:41 -07:00
Weblate
bfcdc89f6a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Serhii Matrunchyk <serhii@digitalidea.studio>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/uk/
Translation: Servarr/Sonarr
2024-07-09 21:59:25 -07:00
Sonarr
67943edfbc Automated API Docs update
ignore-downstream
2024-07-05 16:35:21 -07:00
Mark McDowall
04f8595498 Custom Import List improvements
Fixed: Add placeholder title for Custom Import List title
New: Support 'title' property for Custom Import List
2024-07-05 16:35:08 -07:00
Bogdan
81ac73299a Fixed: Bulk series deletion for unmonitored series
Closes #6933
2024-07-05 19:34:56 -04:00
Mark McDowall
a779a5fad2 Fixed: Parsing of anime season releases with 3-digit number in title 2024-07-05 16:34:06 -07:00
Mark McDowall
bfe6a740fa Fixed: Parsing of anime releases using standard numbering
Closes #6925
2024-07-05 16:34:06 -07:00
Mark McDowall
c9ea40b874 New: Parse VFI as French
Closes #6927
2024-07-05 16:34:00 -07:00
Bogdan
4ee0ae1418 Fixed: History with unknown series 2024-07-05 16:33:50 -07:00
Bogdan
ac1da45ecd Fixed: Calculate Custom Formats after user specified options in Manual Import 2024-07-05 19:33:33 -04:00
Weblate
5c327d5be3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: quek76 <quek@libertysurf.fr>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translation: Servarr/Sonarr
2024-07-05 16:31:56 -07:00
Mark McDowall
55c1ce2e3d Bump version to 4.0.6 2024-06-30 15:42:47 -07:00
Weblate
fd7f0ea973 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Kshitij Burman <kburman6@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: damienmillet <contact@damien-millet.dev>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-06-30 15:42:41 -07:00
Bogdan
d5dff8e8d6 Fixed: Trimming disabled logs database
Closes #6918
2024-06-30 13:49:41 -04:00
Bogdan
8099ba10af Fixed: Already imported downloads appearing in Queue briefly 2024-06-30 13:47:00 -04:00
Mark McDowall
143ccb1e2a Remove seriesTitle from EpisodeResource
Closes #6841
2024-06-28 06:22:10 -07:00
Mark McDowall
29480d9544 Fixed: Don't use cleaned up release title for release title 2024-06-28 06:22:04 -07:00
Mark McDowall
6de536a7ad Fixed: Limit Queue maximum page size to 200
Closes #6899
2024-06-26 09:45:43 -07:00
Mark McDowall
bce848facf Fixed: Reprocessing items that were previously blocked during importing 2024-06-26 09:45:28 -07:00
Mark McDowall
ea4fe392a0 New: Remove websites in parentheses before parsing 2024-06-25 15:52:24 -07:00
Mark McDowall
45fe585944 Fixed: Prevent errors parsing releases in unexpected formats 2024-06-25 15:52:24 -07:00
Mark McDowall
a0d2933134 New: Ignore Deluge torrents without a title
Closes #6885
2024-06-25 18:52:12 -04:00
Mark McDowall
4c622fd412 New: Ability to select Plex Media Server from plex.tv
Closes #6887
2024-06-25 15:51:57 -07:00
Bogdan
fb060730c7 Fixed: Exclude invalid releases from Newznab and Torznab parsers 2024-06-25 15:51:41 -07:00
Mark McDowall
6d5ff9c4d6 New: Improve UI status when downloads cannot be imported automatically
Closes #6873
2024-06-25 18:51:20 -04:00
Mark McDowall
63bed3e670 New: Parse anime seasons with trailing number in title
Closes #6883
2024-06-25 15:51:03 -07:00
Weblate
e684c10432 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Taylan Tatlı <taylantatli90@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translation: Servarr/Sonarr
2024-06-25 15:50:56 -07:00
Bogdan
d2509798e9 New: Display stats for delete multiple series modal 2024-06-17 20:40:04 -07:00
Bogdan
6c39855ebe Fix UpdatePackageProviderFixture for v4
ignore-downstream
2024-06-17 20:39:33 -07:00
Bogdan
a30e9da767 New: Ignore inaccessible folders when getting folders 2024-06-17 20:39:33 -07:00
Mark McDowall
f8e81396d4 Fixed: Importing from IMDb list 2024-06-17 20:39:22 -07:00
Mark McDowall
7fccf590a8 Fixed: Adding series with unknown items in queue 2024-06-17 23:39:13 -04:00
Stephan Sundermann
e1b937e8d5 New: Add TMDB ID support
Closes #6866
2024-06-17 23:38:41 -04:00
Bogdan
c331c8bd11 Ignore Grabbed from API docs
Run application in docs.sh specific to platform
2024-06-10 20:30:26 -07:00
Mark McDowall
52b72925f9 Fixed: Improve error messaging if config file isn't formatted correctly
Closes #6860
2024-06-10 20:30:13 -07:00
Mark McDowall
378fedcd9d Fixed: Skip invalid series paths during validation 2024-06-10 23:30:03 -04:00
Bogdan
a90ab1a8fd Fixed: Ignore case when resolving indexer by name in release push 2024-06-10 20:29:46 -07:00
Bogdan
0edc5ba99a Fixed: Ignore case for name validation in providers 2024-06-10 20:29:46 -07:00
Bogdan
ea54ade9bf New: Refresh cache for tracked queue on series add 2024-06-10 20:29:41 -07:00
Weblate
e07eb05e8b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlbertCoolGuy <Albert.rosenstand@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: xuzhihui <5894940@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-06-10 20:29:28 -07:00
Bogdan
d9b771ab0b Fixed: Error sending Manual Interaction Required when series is unknown 2024-05-31 20:11:31 -04:00
Mark McDowall
6b08e849b8 Search for raw and clean titles for Newznab/Torznab indexers that support raw title searching 2024-05-31 17:10:32 -07:00
Mark McDowall
9c1f48ebc9 Fixed: Include full series title in episode search 2024-05-31 17:10:32 -07:00
Mark McDowall
fd3dd1ab7d New: Genres and Images for Webhooks and Notifiarr
Closes #6822
2024-05-31 17:10:13 -07:00
yammes08
11e5c5a11b Fixed: SDR Files Being Parsed As HLG 2024-05-31 20:09:53 -04:00
Weblate
48f0291884 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Ano10 <arnaudthommeray+github@ik.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: r0bertreh <Robert.reh@live.de>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translation: Servarr/Sonarr
2024-05-31 17:09:11 -07:00
Mark McDowall
af0e55aef4 Bump version to 4.0.5 2024-05-29 16:23:16 -07:00
Weblate
39a439eb4c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Bao Trinh <servarr@baodtrinh.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Co-authored-by: thegamingcat13 <sandervanbeek2004@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-29 16:23:04 -07:00
Weblate
66940b283b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-24 06:21:13 -07:00
Mark McDowall
2a662afaef Fixed: Time for episodes airing today being blank 2024-05-24 06:19:38 -07:00
Weblate
62a9c2519b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-22 20:09:18 -07:00
Mark McDowall
ca372bee25 Fixed: Queue and Calendar not loading 2024-05-22 20:07:31 -07:00
Sonarr
0904a0737e Automated API Docs update
ignore-downstream
2024-05-21 17:09:20 -07:00
Bogdan
70bc26dc19 Disable workflows on forks
ignore-downstream
2024-05-21 17:06:44 -07:00
Bogdan
a2e0002a08 Replace multiple occurrences in branch env variable
ignore-downstream
2024-05-21 17:06:44 -07:00
Bogdan
d7ceb11a64 Fixed: Trimming slashes from UrlBase when using environment variable 2024-05-21 17:06:44 -07:00
Bogdan
cc5b5463f2 Ignore Grabbed with STJson 2024-05-21 17:06:36 -07:00
Bogdan
9b4ff657af Update the wanted section for missing and cutoff unmet 2024-05-21 17:06:36 -07:00
Bogdan
aea50fa47e Bump Npgsql to 7.0.7
ignore-downstream
2024-05-21 17:06:28 -07:00
Mark McDowall
05edd44ed6 New: Include time for episode/season/series history 2024-05-21 17:06:18 -07:00
Bogdan
4440aa3cac New: Root folder exists validation for import lists 2024-05-21 17:06:09 -07:00
Bogdan
084fcc2295 Implement equality checks for providers 2024-05-21 17:05:48 -07:00
Weblate
536ff142c3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dani Talens <databio@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Ransack6086 <servarr.jubilant150@slmail.me>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yi Cao <caoyi06@qq.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: topnew <sznetim@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/sonarr/zh_CN/
Translation: Servarr/Sonarr
2024-05-21 17:05:34 -07:00
267 changed files with 4054 additions and 1363 deletions

View File

@@ -6,13 +6,13 @@ on:
- develop
- main
paths-ignore:
- 'src/Sonarr.Api.*/openapi.json'
- "src/Sonarr.Api.*/openapi.json"
pull_request:
branches:
- develop
paths-ignore:
- 'src/NzbDrone.Core/Localization/Core/**'
- 'src/Sonarr.Api.*/openapi.json'
- "src/NzbDrone.Core/Localization/Core/**"
- "src/Sonarr.Api.*/openapi.json"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -22,7 +22,7 @@ env:
FRAMEWORK: net6.0
RAW_BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
SONARR_MAJOR_VERSION: 4
VERSION: 4.0.4
VERSION: 4.0.7
jobs:
backend:
@@ -32,105 +32,105 @@ jobs:
major_version: ${{ steps.variables.outputs.major_version }}
version: ${{ steps.variables.outputs.version }}
steps:
- name: Check out
uses: actions/checkout@v4
- name: Check out
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@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)
- 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 "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"
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: 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
- name: Build Backend
shell: bash
run: ./build.sh --backend --enable-extra-platforms --packages
# Test Artifacts
# Test Artifacts
- name: Publish win-x64 Test Artifact
uses: ./.github/actions/publish-test-artifact
with:
framework: ${{ env.FRAMEWORK }}
runtime: win-x64
- 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 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
- 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-*/**/*
# 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: Check out
uses: actions/checkout@v4
- name: Volta
uses: volta-cli/action@v4
- name: Volta
uses: volta-cli/action@v4
- name: Yarn Install
run: yarn install
- name: Yarn Install
run: yarn install
- name: Lint
run: yarn lint
- name: Lint
run: yarn lint
- name: Stylelint
run: yarn stylelint -f github
- name: Stylelint
run: yarn stylelint -f github
- name: Build
run: yarn build --env production
- name: Build
run: yarn build --env production
- name: Publish UI Artifact
uses: actions/upload-artifact@v4
with:
name: build_ui
path: _output/UI/**/*
- name: Publish UI Artifact
uses: actions/upload-artifact@v4
with:
name: build_ui
path: _output/UI/**/*
unit_test:
needs: backend
@@ -150,32 +150,32 @@ jobs:
filter: TestCategory!=ManualTest&TestCategory!=LINUX&TestCategory!=IntegrationTest&TestCategory!=AutomationTest
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v4
- 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 }}
- 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: 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
- 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
@@ -201,19 +201,19 @@ jobs:
binary_path: win-x64/${{ needs.backend.outputs.framework }}/Sonarr
runs-on: ${{ matrix.os }}
steps:
- name: Check out
uses: actions/checkout@v4
- 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 }}
- 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' }}
@@ -228,7 +228,15 @@ jobs:
notify:
name: Discord Notification
needs: [backend, frontend, unit_test, unit_test_postgres, integration_test, deploy]
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' }}
@@ -239,10 +247,10 @@ jobs:
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'
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-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
embed-description: |
**Branch** ${{ github.ref }}
**Build** ${{ needs.backend.outputs.version }}

View File

@@ -8,5 +8,6 @@ jobs:
contents: read
pull-requests: write
runs-on: ubuntu-latest
if: github.repository == 'Sonarr/Sonarr'
steps:
- uses: actions/labeler@v5

View File

@@ -8,6 +8,7 @@ on:
jobs:
lock:
runs-on: ubuntu-latest
if: github.repository == 'Sonarr/Sonarr'
steps:
- uses: dessant/lock-threads@v5
with:

12
docs.sh
View File

@@ -25,17 +25,23 @@ 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.5.0 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/Sonarr.dll" v3 &
dotnet tool run swagger tofile --output ./src/Sonarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
sleep 30
sleep 45
kill %1

View File

@@ -77,7 +77,7 @@ class HistoryRow extends Component {
onMarkAsFailedPress
} = this.props;
if (!episode) {
if (!series || !episode) {
return null;
}

View File

@@ -217,6 +217,7 @@ class Queue extends Component {
>
<TableOptionsModalWrapper
columns={columns}
maxPageSize={200}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>

View File

@@ -70,6 +70,11 @@ 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;

View File

@@ -118,6 +118,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
{
key: 'blocklistAndSearch',
value: translate('BlocklistAndSearch'),
isDisabled: isPending,
hint: multipleSelected
? translate('BlocklistAndSearchMultipleHint')
: translate('BlocklistAndSearchHint'),
@@ -130,7 +131,7 @@ function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
: translate('BlocklistOnlyHint'),
},
];
}, [multipleSelected]);
}, [isPending, multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {

View File

@@ -24,7 +24,11 @@ function TimeleftCell(props) {
} = props;
if (status === 'delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
@@ -40,7 +44,11 @@ function TimeleftCell(props) {
}
if (status === 'downloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates
});
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (

View File

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

View File

@@ -271,26 +271,32 @@ class EnhancedSelectInput extends Component {
this.setState({ isOpen: !this.state.isOpen });
};
onSelect = (value) => {
if (Array.isArray(this.props.value)) {
let newValue = null;
const index = this.props.value.indexOf(value);
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);
if (index === -1) {
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
arrayValue = values.map((v) => v.key).filter((v) => (v === newValue) || value.includes(v));
} else {
newValue = [...this.props.value];
newValue.splice(index, 1);
arrayValue = [...value];
arrayValue.splice(index, 1);
}
this.props.onChange({
name: this.props.name,
value: newValue
onChange({
name,
value: arrayValue,
additionalProperties
});
} else {
this.setState({ isOpen: false });
this.props.onChange({
name: this.props.name,
value
onChange({
name,
value: newValue,
additionalProperties
});
}
};
@@ -485,7 +491,7 @@ class EnhancedSelectInput extends Component {
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
const depth = hasParent ? 1 : 0;
const parentSelected = hasParent && value.includes(v.parentKey);
const parentSelected = hasParent && Array.isArray(value) && value.includes(v.parentKey);
return (
<OptionComponent
key={v.key}

View File

@@ -9,7 +9,8 @@ import EnhancedSelectInput from './EnhancedSelectInput';
const importantFieldNames = [
'baseUrl',
'apiPath',
'apiKey'
'apiKey',
'authToken'
];
function getProviderDataKey(providerData) {
@@ -34,7 +35,9 @@ function getSelectOptions(items) {
key: option.value,
value: option.name,
hint: option.hint,
parentKey: option.parentValue
parentKey: option.parentValue,
isDisabled: option.isDisabled,
additionalProperties: option.additionalProperties
};
});
}
@@ -147,7 +150,7 @@ EnhancedSelectInputConnector.propTypes = {
provider: PropTypes.string.isRequired,
providerData: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
selectOptionsProviderAction: PropTypes.string,
onChange: PropTypes.func.isRequired,

View File

@@ -88,6 +88,15 @@
}
}
.purple {
border-color: var(--purple);
background-color: var(--purple);
&.outline {
color: var(--purple);
}
}
/** Sizes **/
.small {

View File

@@ -11,6 +11,7 @@ interface CssExports {
'medium': string;
'outline': string;
'primary': string;
'purple': string;
'small': string;
'success': string;
'warning': string;

View File

@@ -21,6 +21,7 @@ function createCleanSeriesSelector() {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
tags = []
} = series;
@@ -33,6 +34,7 @@ function createCleanSeriesSelector() {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
firstCharacter: title.charAt(0).toLowerCase(),
tags: tags.reduce((acc, id) => {
const matchingTag = allTags.find((tag) => tag.id === id);

View File

@@ -14,6 +14,7 @@ function SeriesSearchResult(props) {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
tags
} = props;
@@ -73,6 +74,14 @@ function SeriesSearchResult(props) {
null
}
{
match.key === 'tmdbId' && tmdbId ?
<div className={styles.alternateTitle}>
TmdbId: {tmdbId}
</div> :
null
}
{
tag ?
<div className={styles.tagContainer}>
@@ -97,6 +106,7 @@ SeriesSearchResult.propTypes = {
tvdbId: PropTypes.number,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string,
tmdbId: PropTypes.number,
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
match: PropTypes.object.isRequired
};

View File

@@ -13,6 +13,7 @@ const fuseOptions = {
'tvdbId',
'tvMazeId',
'imdbId',
'tmdbId',
'tags.label'
]
};

View File

@@ -244,7 +244,7 @@ class SignalRConnector extends Component {
handleWantedCutoff = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'cutoffUnmet',
section: 'wanted.cutoffUnmet',
updateOnly: true,
...body.resource
});
@@ -254,7 +254,7 @@ class SignalRConnector extends Component {
handleWantedMissing = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'missing',
section: 'wanted.missing',
updateOnly: true,
...body.resource
});

View File

@@ -15,6 +15,7 @@ class RelativeDateCell extends PureComponent {
className,
date,
includeSeconds,
includeTime,
showRelativeDates,
shortDateFormat,
longDateFormat,
@@ -39,7 +40,7 @@ class RelativeDateCell extends PureComponent {
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
{getRelativeDate({ date, shortDateFormat, showRelativeDates, timeFormat, includeSeconds, includeTime, timeForToday: true })}
</Component>
);
}
@@ -49,6 +50,7 @@ RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired,
date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired,
includeTime: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
@@ -60,6 +62,7 @@ RelativeDateCell.propTypes = {
RelativeDateCell.defaultProps = {
className: styles.cell,
includeSeconds: false,
includeTime: false,
component: TableRowCell
};

View File

@@ -49,11 +49,12 @@ class TableOptionsModal extends Component {
onPageSizeChange = ({ value }) => {
let pageSizeError = null;
const maxPageSize = this.props.maxPageSize ?? 250;
if (value < 5) {
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
} else if (value > 250) {
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' });
} else if (value > maxPageSize) {
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: `${maxPageSize}` });
} else {
this.props.onTableOptionChange({ pageSize: value });
}
@@ -248,6 +249,7 @@ TableOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
pageSize: PropTypes.number,
maxPageSize: PropTypes.number,
canModifyColumns: PropTypes.bool.isRequired,
optionsComponent: PropTypes.elementType,
onTableOptionChange: PropTypes.func.isRequired,

View File

@@ -111,6 +111,8 @@ class EpisodeHistoryRow extends Component {
<RelativeDateCellConnector
date={date}
includeSeconds={true}
includeTime={true}
/>
<TableRowCell className={styles.actions}>

View File

@@ -14,4 +14,9 @@
.deleteFilesMessage {
margin-top: 20px;
color: var(--dangerColor);
.deleteCount {
margin-top: 20px;
color: var(--warningColor);
}
}

View File

@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteCount': string;
'deleteFilesMessage': string;
'folderPath': string;
'pathContainer': string;

View File

@@ -50,15 +50,15 @@ class DeleteSeriesModalContent extends Component {
const {
title,
path,
statistics,
statistics = {},
deleteOptions,
onModalClose,
onDeleteOptionChange
} = this.props;
const {
episodeFileCount,
sizeOnDisk
episodeFileCount = 0,
sizeOnDisk = 0
} = statistics;
const deleteFiles = this.state.deleteFiles;
@@ -108,16 +108,20 @@ class DeleteSeriesModalContent extends Component {
</FormGroup>
{
deleteFiles &&
deleteFiles ?
<div className={styles.deleteFilesMessage}>
<div><InlineMarkdown data={translate('DeleteSeriesFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
{
!!episodeFileCount &&
<div>{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}</div>
}
</div>
}
{
episodeFileCount ?
<div className={styles.deleteCount}>
{translate('DeleteSeriesFolderEpisodeCount', { episodeFileCount, size: formatBytes(sizeOnDisk) })}
</div> :
null
}
</div> :
null
}
</ModalBody>
<ModalFooter>

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { useSelector } from 'react-redux';
import Label from 'Components/Label';
import { kinds, sizes } from 'Helpers/Props';
import createSeriesQueueItemsDetailsSelector, {
SeriesQueueDetails,
} from 'Series/Index/createSeriesQueueDetailsSelector';
function getEpisodeCountKind(
monitored: boolean,
episodeFileCount: number,
episodeCount: number,
isDownloading: boolean
) {
if (isDownloading) {
return kinds.PURPLE;
}
if (episodeFileCount === episodeCount && episodeCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
interface SeasonProgressLabelProps {
seriesId: number;
seasonNumber: number;
monitored: boolean;
episodeCount: number;
episodeFileCount: number;
}
function SeasonProgressLabel({
seriesId,
seasonNumber,
monitored,
episodeCount,
episodeFileCount,
}: SeasonProgressLabelProps) {
const queueDetails: SeriesQueueDetails = useSelector(
createSeriesQueueItemsDetailsSelector(seriesId, seasonNumber)
);
const newDownloads = queueDetails.count - queueDetails.episodesWithFiles;
const text = newDownloads
? `${episodeFileCount} + ${newDownloads} / ${episodeCount}`
: `${episodeFileCount} / ${episodeCount}`;
return (
<Label
kind={getEpisodeCountKind(
monitored,
episodeFileCount,
episodeCount,
queueDetails.count > 0
)}
size={sizes.LARGE}
>
<span>{text}</span>
</Label>
);
}
export default SeasonProgressLabel;

View File

@@ -175,6 +175,7 @@ class SeriesDetails extends Component {
tvdbId,
tvMazeId,
imdbId,
tmdbId,
title,
runtime,
ratings,
@@ -566,6 +567,7 @@ class SeriesDetails extends Component {
tvdbId={tvdbId}
tvMazeId={tvMazeId}
imdbId={imdbId}
tmdbId={tmdbId}
/>
}
kind={kinds.INVERSE}
@@ -719,6 +721,7 @@ SeriesDetails.propTypes = {
tvdbId: PropTypes.number.isRequired,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string,
tmdbId: PropTypes.number,
title: PropTypes.string.isRequired,
runtime: PropTypes.number.isRequired,
ratings: PropTypes.object.isRequired,

View File

@@ -9,7 +9,8 @@ function SeriesDetailsLinks(props) {
const {
tvdbId,
tvMazeId,
imdbId
imdbId,
tmdbId
} = props;
return (
@@ -71,6 +72,22 @@ function SeriesDetailsLinks(props) {
</Label>
</Link>
}
{
!!tmdbId &&
<Link
className={styles.link}
to={`https://www.themoviedb.org/tv/${tmdbId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
TMDB
</Label>
</Link>
}
</div>
);
}
@@ -78,7 +95,8 @@ function SeriesDetailsLinks(props) {
SeriesDetailsLinks.propTypes = {
tvdbId: PropTypes.number.isRequired,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string
imdbId: PropTypes.string,
tmdbId: PropTypes.number
};
export default SeriesDetailsLinks;

View File

@@ -2,7 +2,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -15,7 +14,7 @@ import SpinnerIcon from 'Components/SpinnerIcon';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import Popover from 'Components/Tooltip/Popover';
import { align, icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
@@ -27,6 +26,7 @@ import translate from 'Utilities/String/translate';
import getToggledRange from 'Utilities/Table/getToggledRange';
import EpisodeRowConnector from './EpisodeRowConnector';
import SeasonInfo from './SeasonInfo';
import SeasonProgressLabel from './SeasonProgressLabel';
import styles from './SeriesDetailsSeason.css';
function getSeasonStatistics(episodes) {
@@ -64,18 +64,6 @@ function getSeasonStatistics(episodes) {
};
}
function getEpisodeCountKind(monitored, episodeFileCount, episodeCount) {
if (episodeFileCount === episodeCount && episodeCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
class SeriesDetailsSeason extends Component {
//
@@ -265,12 +253,13 @@ class SeriesDetailsSeason extends Component {
className={styles.episodeCountTooltip}
canFlip={true}
anchor={
<Label
kind={getEpisodeCountKind(monitored, episodeFileCount, episodeCount)}
size={sizes.LARGE}
>
<span>{episodeFileCount} / {episodeCount}</span>
</Label>
<SeasonProgressLabel
seriesId={seriesId}
seasonNumber={seasonNumber}
monitored={monitored}
episodeCount={episodeCount}
episodeFileCount={episodeFileCount}
/>
}
title={translate('SeasonInformation')}
body={

View File

@@ -14,7 +14,7 @@ function SeriesHistoryModal(props) {
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
size={sizes.EXTRA_EXTRA_LARGE}
onModalClose={onModalClose}
>
<SeriesHistoryModalContentConnector

View File

@@ -135,6 +135,8 @@ class SeriesHistoryRow extends Component {
<RelativeDateCellConnector
date={date}
includeSeconds={true}
includeTime={true}
/>
<TableRowCell className={styles.actions}>

View File

@@ -138,7 +138,10 @@ function getInfoRowProps(
}),
iconName: icons.CALENDAR,
label:
getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
}) ?? '',
@@ -156,7 +159,10 @@ function getInfoRowProps(
}),
iconName: icons.ADD,
label:
getRelativeDate(added, shortDateFormat, showRelativeDates, {
getRelativeDate({
date: added,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
}) ?? '',
@@ -232,15 +238,13 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
<SeriesIndexOverviewInfoRow
title={formatDateTime(nextAiring, longDateFormat, timeFormat)}
iconName={icons.SCHEDULED}
label={getRelativeDate(
nextAiring,
label={getRelativeDate({
date: nextAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true,
}
)}
timeFormat,
timeForToday: true,
})}
/>
)}

View File

@@ -217,7 +217,10 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
timeFormat
)}`}
>
{getRelativeDate(nextAiring, shortDateFormat, showRelativeDates, {
{getRelativeDate({
date: nextAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
})}

View File

@@ -80,7 +80,10 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
timeFormat
)}`}
>
{getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
{getRelativeDate({
date: previousAiring,
shortDateFormat,
showRelativeDates,
timeFormat,
timeForToday: true,
})}
@@ -89,15 +92,13 @@ function SeriesIndexPosterInfo(props: SeriesIndexPosterInfoProps) {
}
if (sortKey === 'added' && added) {
const addedDate = getRelativeDate(
added,
const addedDate = getRelativeDate({
date: added,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false,
}
);
timeFormat,
timeForToday: false,
});
return (
<div

View File

@@ -10,4 +10,15 @@
.path {
margin-left: 5px;
color: var(--dangerColor);
font-weight: bold;
}
.statistics {
margin-left: 5px;
color: var(--warningColor);
}
.deleteFilesMessage {
margin-top: 20px;
color: var(--warningColor);
}

View File

@@ -1,9 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'deleteFilesMessage': string;
'message': string;
'path': string;
'pathContainer': string;
'statistics': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -16,6 +16,7 @@ import Series from 'Series/Series';
import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import { CheckInputChanged } from 'typings/inputs';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteSeriesModalContent.css';
@@ -85,6 +86,23 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
onModalClose,
]);
const { totalEpisodeFileCount, totalSizeOnDisk } = useMemo(() => {
return series.reduce(
(acc, { statistics = {} }) => {
const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics;
acc.totalEpisodeFileCount += episodeFileCount;
acc.totalSizeOnDisk += sizeOnDisk;
return acc;
},
{
totalEpisodeFileCount: 0,
totalSizeOnDisk: 0,
}
);
}, [series]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('DeleteSelectedSeries')}</ModalHeader>
@@ -136,20 +154,44 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
</div>
<ul>
{series.map((s) => {
{series.map(({ title, path, statistics = {} }) => {
const { episodeFileCount = 0, sizeOnDisk = 0 } = statistics;
return (
<li key={s.title}>
<span>{s.title}</span>
<li key={title}>
<span>{title}</span>
{deleteFiles && (
<span className={styles.pathContainer}>
-<span className={styles.path}>{s.path}</span>
<span>
<span className={styles.pathContainer}>
-<span className={styles.path}>{path}</span>
</span>
{!!episodeFileCount && (
<span className={styles.statistics}>
(
{translate('DeleteSeriesFolderEpisodeCount', {
episodeFileCount,
size: formatBytes(sizeOnDisk),
})}
)
</span>
)}
</span>
)}
</li>
);
})}
</ul>
{deleteFiles && !!totalEpisodeFileCount ? (
<div className={styles.deleteFilesMessage}>
{translate('DeleteSeriesFolderEpisodeCount', {
episodeFileCount: totalEpisodeFileCount,
size: formatBytes(totalSizeOnDisk),
})}
</div>
) : null}
</ModalBody>
<ModalFooter>

View File

@@ -70,6 +70,7 @@ interface Series extends ModelBase {
tvdbId: number;
tvMazeId: number;
tvRageId: number;
tmdbId: number;
useSceneNumbering: boolean;
year: number;
isSaving?: boolean;

View File

@@ -138,7 +138,8 @@ class Naming extends Component {
{ key: 1, value: translate('ReplaceWithDash') },
{ key: 2, value: translate('ReplaceWithSpaceDash') },
{ key: 3, value: translate('ReplaceWithSpaceDashSpace') },
{ key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') }
{ key: 4, value: translate('SmartReplace'), hint: translate('SmartReplaceHint') },
{ key: 5, value: translate('Custom'), hint: translate('CustomColonReplacementFormatHint') }
];
const standardEpisodeFormatHelpTexts = [];
@@ -262,6 +263,22 @@ class Naming extends Component {
null
}
{
replaceIllegalCharacters && settings.colonReplacementFormat.value === 5 ?
<FormGroup>
<FormLabel>{translate('ColonReplacement')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="customColonReplacementFormat"
helpText={translate('CustomColonReplacementFormatHelpText')}
onChange={onInputChange}
{...settings.customColonReplacementFormat}
/>
</FormGroup> :
null
}
{
renameEpisodes &&
<div>

View File

@@ -99,6 +99,7 @@ const seriesTokens = [
const seriesIdTokens = [
{ token: '{ImdbId}', example: 'tt12345' },
{ token: '{TvdbId}', example: '12345' },
{ token: '{TmdbId}', example: '11223' },
{ token: '{TvMazeId}', example: '54321' }
];

View File

@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
saveNotification,
setNotificationFieldValue,
setNotificationFieldValues,
setNotificationValue,
testNotification,
toggleAdvancedSettings
@@ -27,7 +27,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
setNotificationValue,
setNotificationFieldValue,
setNotificationFieldValues,
saveNotification,
testNotification,
toggleAdvancedSettings
@@ -51,8 +51,8 @@ class EditNotificationModalContentConnector extends Component {
this.props.setNotificationValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setNotificationFieldValue({ name, value });
onFieldChange = ({ name, value, additionalProperties = {} }) => {
this.props.setNotificationFieldValues({ properties: { ...additionalProperties, [name]: value } });
};
onSavePress = () => {
@@ -91,7 +91,7 @@ EditNotificationModalContentConnector.propTypes = {
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setNotificationValue: PropTypes.func.isRequired,
setNotificationFieldValue: PropTypes.func.isRequired,
setNotificationFieldValues: PropTypes.func.isRequired,
saveNotification: PropTypes.func.isRequired,
testNotification: PropTypes.func.isRequired,
toggleAdvancedSettings: PropTypes.func.isRequired,

View File

@@ -59,6 +59,7 @@ class Notification extends Component {
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
@@ -71,6 +72,7 @@ class Notification extends Component {
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnImportComplete,
supportsOnRename,
supportsOnSeriesAdd,
supportsOnSeriesDelete,
@@ -105,7 +107,7 @@ class Notification extends Component {
{
supportsOnDownload && onDownload ?
<Label kind={kinds.SUCCESS}>
{translate('OnImport')}
{translate('OnFileImport')}
</Label> :
null
}
@@ -118,6 +120,14 @@ class Notification extends Component {
null
}
{
supportsOnImportComplete && onImportComplete ?
<Label kind={kinds.SUCCESS}>
{translate('OnImportComplete')}
</Label> :
null
}
{
supportsOnRename && onRename ?
<Label kind={kinds.SUCCESS}>
@@ -191,7 +201,7 @@ class Notification extends Component {
}
{
!onGrab && !onDownload && !onRename && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
!onGrab && !onDownload && !onRename && !onImportComplete && !onHealthIssue && !onHealthRestored && !onApplicationUpdate && !onSeriesAdd && !onSeriesDelete && !onEpisodeFileDelete && !onManualInteractionRequired ?
<Label
kind={kinds.DISABLED}
outline={true}
@@ -233,6 +243,7 @@ Notification.propTypes = {
onGrab: PropTypes.bool.isRequired,
onDownload: PropTypes.bool.isRequired,
onUpgrade: PropTypes.bool.isRequired,
onImportComplete: PropTypes.bool.isRequired,
onRename: PropTypes.bool.isRequired,
onSeriesAdd: PropTypes.bool.isRequired,
onSeriesDelete: PropTypes.bool.isRequired,
@@ -244,6 +255,7 @@ Notification.propTypes = {
onManualInteractionRequired: PropTypes.bool.isRequired,
supportsOnGrab: PropTypes.bool.isRequired,
supportsOnDownload: PropTypes.bool.isRequired,
supportsOnImportComplete: PropTypes.bool.isRequired,
supportsOnSeriesAdd: PropTypes.bool.isRequired,
supportsOnSeriesDelete: PropTypes.bool.isRequired,
supportsOnEpisodeFileDelete: PropTypes.bool.isRequired,

View File

@@ -18,6 +18,7 @@ function NotificationEventItems(props) {
onGrab,
onDownload,
onUpgrade,
onImportComplete,
onRename,
onSeriesAdd,
onSeriesDelete,
@@ -30,6 +31,7 @@ function NotificationEventItems(props) {
supportsOnGrab,
supportsOnDownload,
supportsOnUpgrade,
supportsOnImportComplete,
supportsOnRename,
supportsOnSeriesAdd,
supportsOnSeriesDelete,
@@ -66,7 +68,7 @@ function NotificationEventItems(props) {
<FormInputGroup
type={inputTypes.CHECK}
name="onDownload"
helpText={translate('OnImport')}
helpText={translate('OnFileImport')}
isDisabled={!supportsOnDownload.value}
{...onDownload}
onChange={onInputChange}
@@ -87,6 +89,17 @@ function NotificationEventItems(props) {
</div>
}
<div>
<FormInputGroup
type={inputTypes.CHECK}
name="onImportComplete"
helpText={translate('OnImportComplete')}
isDisabled={!supportsOnImportComplete.value}
{...onImportComplete}
onChange={onInputChange}
/>
</div>
<div>
<FormInputGroup
type={inputTypes.CHECK}

View File

@@ -0,0 +1,25 @@
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
function createSetProviderFieldValuesReducer(section) {
return (state, { payload }) => {
if (section === payload.section) {
const { properties } = payload;
const newState = getSectionState(state, section);
newState.pendingChanges = Object.assign({}, newState.pendingChanges);
const fields = Object.assign({}, newState.pendingChanges.fields || {});
Object.keys(properties).forEach((name) => {
fields[name] = properties[name];
});
newState.pendingChanges.fields = fields;
return updateSectionState(state, section, newState);
}
return state;
};
}
export default createSetProviderFieldValuesReducer;

View File

@@ -5,6 +5,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetProviderFieldValuesReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValuesReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
@@ -22,6 +23,7 @@ export const FETCH_NOTIFICATION_SCHEMA = 'settings/notifications/fetchNotificati
export const SELECT_NOTIFICATION_SCHEMA = 'settings/notifications/selectNotificationSchema';
export const SET_NOTIFICATION_VALUE = 'settings/notifications/setNotificationValue';
export const SET_NOTIFICATION_FIELD_VALUE = 'settings/notifications/setNotificationFieldValue';
export const SET_NOTIFICATION_FIELD_VALUES = 'settings/notifications/setNotificationFieldValues';
export const SAVE_NOTIFICATION = 'settings/notifications/saveNotification';
export const CANCEL_SAVE_NOTIFICATION = 'settings/notifications/cancelSaveNotification';
export const DELETE_NOTIFICATION = 'settings/notifications/deleteNotification';
@@ -55,6 +57,13 @@ export const setNotificationFieldValue = createAction(SET_NOTIFICATION_FIELD_VAL
};
});
export const setNotificationFieldValues = createAction(SET_NOTIFICATION_FIELD_VALUES, (payload) => {
return {
section,
...payload
};
});
//
// Details
@@ -99,6 +108,7 @@ export default {
reducers: {
[SET_NOTIFICATION_VALUE]: createSetSettingValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SET_NOTIFICATION_FIELD_VALUES]: createSetProviderFieldValuesReducer(section),
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
@@ -106,6 +116,7 @@ export default {
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onImportComplete = selectedSchema.supportsOnImportComplete;
selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.onSeriesAdd = selectedSchema.supportsOnSeriesAdd;
selectedSchema.onSeriesDelete = selectedSchema.supportsOnSeriesDelete;

View File

@@ -1,43 +0,0 @@
import moment from 'moment';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
if (!date) {
return null;
}
const isTodayDate = isToday(date);
if (isTodayDate && timeForToday && timeFormat) {
return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds });
}
if (!showRelativeDates) {
return moment(date).format(shortDateFormat);
}
if (isYesterday(date)) {
return translate('Yesterday');
}
if (isTodayDate) {
return translate('Today');
}
if (isTomorrow(date)) {
return translate('Tomorrow');
}
if (isInNextWeek(date)) {
return moment(date).format('dddd');
}
return moment(date).format(shortDateFormat);
}
export default getRelativeDate;

View File

@@ -0,0 +1,82 @@
import moment from 'moment';
import formatTime from 'Utilities/Date/formatTime';
import isInNextWeek from 'Utilities/Date/isInNextWeek';
import isToday from 'Utilities/Date/isToday';
import isTomorrow from 'Utilities/Date/isTomorrow';
import isYesterday from 'Utilities/Date/isYesterday';
import translate from 'Utilities/String/translate';
import formatDateTime from './formatDateTime';
interface GetRelativeDateOptions {
date?: string;
shortDateFormat: string;
showRelativeDates: boolean;
timeFormat?: string;
includeSeconds?: boolean;
timeForToday?: boolean;
includeTime?: boolean;
}
function getRelativeDate({
date,
shortDateFormat,
showRelativeDates,
timeFormat,
includeSeconds = false,
timeForToday = false,
includeTime = false,
}: GetRelativeDateOptions) {
if (!date) {
return null;
}
if ((includeTime || timeForToday) && !timeFormat) {
throw new Error(
"getRelativeDate: 'timeFormat' is required when 'includeTime' or 'timeForToday' is true"
);
}
const isTodayDate = isToday(date);
const time = timeFormat
? formatTime(date, timeFormat, {
includeMinuteZero: true,
includeSeconds,
})
: '';
if (isTodayDate && timeForToday) {
return time;
}
if (!showRelativeDates) {
return moment(date).format(shortDateFormat);
}
if (isYesterday(date)) {
return includeTime
? translate('YesterdayAt', { time })
: translate('Yesterday');
}
if (isTodayDate) {
return includeTime ? translate('TodayAt', { time }) : translate('Today');
}
if (isTomorrow(date)) {
return includeTime
? translate('TomorrowAt', { time })
: translate('Tomorrow');
}
if (isInNextWeek(date)) {
const day = moment(date).format('dddd');
return includeTime ? translate('DayOfWeekAt', { day, time }) : day;
}
return includeTime
? formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds })
: moment(date).format(shortDateFormat);
}
export default getRelativeDate;

View File

@@ -153,7 +153,11 @@ namespace NzbDrone.Common.Disk
{
Ensure.That(path, () => path).IsValidPath(PathValidationType.CurrentOs);
return Directory.EnumerateDirectories(path);
return Directory.EnumerateDirectories(path, "*", new EnumerationOptions
{
AttributesToSkip = FileAttributes.System,
IgnoreInaccessible = true
});
}
public IEnumerable<string> GetFiles(string path, bool recursive)

View File

@@ -71,6 +71,10 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests
Mocker.GetMock<IProvideImportItemService>()
.Setup(s => s.ProvideImportItem(It.IsAny<DownloadClientItem>(), It.IsAny<DownloadClientItem>()))
.Returns<DownloadClientItem, DownloadClientItem>((i, p) => i);
Mocker.GetMock<IEpisodeService>()
.Setup(s => s.GetEpisodes(It.IsAny<IEnumerable<int>>()))
.Returns(new List<Episode>());
}
private RemoteEpisode BuildRemoteEpisode()
@@ -366,7 +370,7 @@ namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests
Mocker.GetMock<IEventAggregator>()
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never());
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending);
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportBlocked);
}
private void AssertImported()

View File

@@ -608,5 +608,47 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2);
allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3);
}
[Test]
public async Task episode_search_should_include_series_title_when_not_a_direct_title_match()
{
_xemSeries.Title = "Sonarr's Title";
_xemSeries.CleanTitle = "sonarrstitle";
WithEpisode(1, 12, 2, 3);
Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.FindByTvdbId(It.IsAny<int>()))
.Returns(new List<SceneMapping>
{
new SceneMapping
{
TvdbId = _xemSeries.TvdbId,
SearchTerm = "Sonarrs Title",
ParseTerm = _xemSeries.CleanTitle,
SeasonNumber = 1,
SceneSeasonNumber = 1,
SceneOrigin = "tvdb",
Type = "ServicesProvider"
}
});
var allCriteria = WatchForSearchCriteria();
await Subject.EpisodeSearch(_xemEpisodes.First(), false, false);
Mocker.GetMock<ISceneMappingService>()
.Verify(v => v.FindByTvdbId(_xemSeries.Id), Times.Once());
allCriteria.Should().HaveCount(2);
allCriteria.First().Should().BeOfType<SingleEpisodeSearchCriteria>();
allCriteria.First().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(1);
allCriteria.First().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(12);
allCriteria.Last().Should().BeOfType<SingleEpisodeSearchCriteria>();
allCriteria.Last().As<SingleEpisodeSearchCriteria>().SeasonNumber.Should().Be(2);
allCriteria.Last().As<SingleEpisodeSearchCriteria>().EpisodeNumber.Should().Be(3);
}
}
}

View File

@@ -36,7 +36,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_singleEpisodeSearchCriteria = new SingleEpisodeSearchCriteria
{
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string> { "Monkey Island" },
SeasonNumber = 1,
EpisodeNumber = 2
@@ -44,14 +44,14 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_seasonSearchCriteria = new SeasonSearchCriteria
{
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string> { "Monkey Island" },
SeasonNumber = 1,
};
_animeSearchCriteria = new AnimeEpisodeSearchCriteria()
{
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string>() { "Monkey+Island" },
AbsoluteEpisodeNumber = 100,
SeasonNumber = 5,
@@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
_animeSeasonSearchCriteria = new AnimeSeasonSearchCriteria()
{
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40" },
Series = new Tv.Series { TvRageId = 10, TvdbId = 20, TvMazeId = 30, ImdbId = "t40", TmdbId = 50 },
SceneTitles = new List<string> { "Monkey Island" },
SeasonNumber = 3,
};
@@ -268,6 +268,19 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
page.Url.Query.Should().Contain("imdbid=t40");
}
[Test]
public void should_search_by_tmdb_if_supported()
{
_capabilities.SupportedTvSearchParameters = new[] { "q", "tmdbid", "season", "ep" };
var results = Subject.GetSearchRequests(_singleEpisodeSearchCriteria);
results.GetTier(0).Should().HaveCount(1);
var page = results.GetAllTiers().First().First();
page.Url.Query.Should().Contain("tmdbid=50");
}
[Test]
public void should_prefer_search_by_tvdbid_if_rid_supported()
{

View File

@@ -107,7 +107,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
[TestCase(10, "", "", "", null, HdrFormat.None)]
[TestCase(10, "bt709", "bt709", "", null, HdrFormat.None)]
[TestCase(8, "bt2020", "smpte2084", "", null, HdrFormat.None)]
[TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.Hlg10)]
[TestCase(10, "bt2020", "bt2020-10", "", null, HdrFormat.None)]
[TestCase(10, "bt2020", "arib-std-b67", "", null, HdrFormat.Hlg10)]
[TestCase(10, "bt2020", "smpte2084", "", null, HdrFormat.Pq10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", null, HdrFormat.Pq10)]

View File

@@ -5,7 +5,6 @@ using FluentValidation.Results;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Validation;
using NzbDrone.Test.Common;
@@ -15,9 +14,9 @@ namespace NzbDrone.Core.Test.NotificationTests
[TestFixture]
public class NotificationBaseFixture : TestBase
{
private class TestSetting : IProviderConfig
private class TestSetting : NotificationSettingsBase<TestSetting>
{
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult();
}

View File

@@ -34,6 +34,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
Subject.Definition = new NotificationDefinition();
Subject.Definition.Settings = new XbmcSettings
{
Host = "localhost",
UpdateLibrary = true
};
}
@@ -49,6 +50,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
Subject.Definition.Settings = new XbmcSettings
{
Host = "localhost",
UpdateLibrary = true,
CleanLibrary = true
};
@@ -58,6 +60,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
public void should_not_clean_if_no_episode_was_replaced()
{
Subject.OnDownload(_downloadMessage);
Subject.ProcessQueue();
Mocker.GetMock<IXbmcService>().Verify(v => v.Clean(It.IsAny<XbmcSettings>()), Times.Never());
}
@@ -67,6 +70,7 @@ namespace NzbDrone.Core.Test.NotificationTests.Xbmc
{
GivenOldFiles();
Subject.OnDownload(_downloadMessage);
Subject.ProcessQueue();
Mocker.GetMock<IXbmcService>().Verify(v => v.Clean(It.IsAny<XbmcSettings>()), Times.Once());
}

View File

@@ -90,5 +90,18 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be(expected);
}
[TestCase("Series: Title", ColonReplacementFormat.Custom, "\ua789", "Series\ua789 Title")]
[TestCase("Series: Title", ColonReplacementFormat.Custom, "", "Series Title")]
public void should_replace_colon_with_custom_format(string seriesName, ColonReplacementFormat replacementFormat, string customFormat, string expected)
{
_series.Title = seriesName;
_namingConfig.StandardEpisodeFormat = "{Series Title}";
_namingConfig.ColonReplacementFormat = replacementFormat;
_namingConfig.CustomColonReplacementFormat = customFormat;
Subject.BuildFileName(new List<Episode> { _episode1 }, _series, _episodeFile)
.Should().Be(expected);
}
}
}

View File

@@ -56,5 +56,14 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Subject.GetSeriesFolder(_series)
.Should().Be($"Series Title ({_series.TvMazeId})");
}
[Test]
public void should_add_tmdb_id()
{
_namingConfig.SeriesFolderFormat = "{Series Title} ({TmdbId})";
Subject.GetSeriesFolder(_series)
.Should().Be($"Series Title ({_series.TmdbId})");
}
}
}

View File

@@ -39,6 +39,13 @@ namespace NzbDrone.Core.Test.ParserTests
ExceptionVerification.IgnoreWarns();
}
[TestCase("علم نف) أ.دعادل الأبيض ٢٠٢٤ ٣ ٣")]
[TestCase("ror-240618_1007-1022-")]
public void should_parse_unknown_formats_without_error(string title)
{
Parser.Parser.ParseTitle(title).Should().NotBeNull();
}
[Test]
public void should_not_parse_md5()
{

View File

@@ -64,6 +64,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Title.S01.720p.VFF.WEB-DL.AAC2.0.H.264-BTN")]
[TestCase("Title.S01.720p.VFQ.WEB-DL.AAC2.0.H.264-BTN")]
[TestCase("Title.S01.720p.TRUEFRENCH.WEB-DL.AAC2.0.H.264-BTN")]
[TestCase("Series In The Middle S01 Multi VFI VO 1080p WEB x265 HEVC AAC 5.1-Papaya")]
public void should_parse_language_french(string postTitle)
{
var result = LanguageParser.ParseLanguages(postTitle);

View File

@@ -254,6 +254,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series Title Season 2 (WEB 1080p HEVC Opus) [Netaro]", false)]
[TestCase("Series.Title.S01E01.Erste.Begegnungen.German.DD51.Synced.DL.1080p.HBOMaxHD.AVC-TVS", false)]
[TestCase("Series.Title.S01E05.Tavora.greift.an.German.DL.1080p.DisneyHD.h264-4SF", false)]
[TestCase("Series.Title.S02E04.German.Dubbed.DL.AAC.1080p.WEB.AVC-GROUP", false)]
public void should_parse_webdl1080p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.WEBDL1080p, proper);
@@ -279,6 +280,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[HorribleSubs] Series Title! S01 [Web][MKV][h264][2160p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
[TestCase("Series Title S02 2013 WEB-DL 4k H265 AAC 2Audio-HDSWEB", false)]
[TestCase("Series.Title.S02E02.This.Year.Will.Be.Different.2160p.WEB.H.265", false)]
[TestCase("Series.Title.S02E04.German.Dubbed.DL.AAC.2160p.DV.HDR.WEB.HEVC-GROUP", false)]
public void should_parse_webdl2160p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Quality.WEBDL2160p, proper);

View File

@@ -33,6 +33,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Series No More S01 2023 1080p WEB-DL AVC AC3 2.0 Dual Audio -ZR-", "Series No More", 1)]
[TestCase("Series Title / S1E1-8 of 8 [2024, WEB-DL 1080p] + Original + RUS", "Series Title", 1)]
[TestCase("Series Title / S2E1-16 of 16 [2022, WEB-DL] RUS", "Series Title", 2)]
[TestCase("[hchcsen] Mobile Series 00 S01 [BD Remux Dual Audio 1080p AVC 2xFLAC] (Kidou Senshi Gundam 00 Season 1)", "Mobile Series 00", 1)]
[TestCase("[HorribleRips] Mobile Series 00 S1 [1080p]", "Mobile Series 00", 1)]
[TestCase("[Zoombie] Zom 100: Bucket List of the Dead S01 [Web][MKV][h265 10-bit][1080p][AC3 2.0][Softsubs (Zoombie)]", "Zom 100: Bucket List of the Dead", 1)]
public void should_parse_full_season_release(string postTitle, string title, int season)
{
var result = Parser.Parser.ParseTitle(postTitle);

View File

@@ -168,6 +168,10 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Босх: Спадок / Series: Legacy / S2E1 of 10 (2023) WEB-DL 1080p Ukr/Eng | sub Eng", "Series: Legacy", 2, 1)]
[TestCase("Titles.s06e01.1999.BDRip.1080p.Ukr.Eng.AC3.Hurtom.TNU.Tenax555", "Titles", 6, 1)]
[TestCase("Titles.s06.01.1999.BDRip.1080p.Ukr.Eng.AC3.Hurtom.TNU.Tenax555", "Titles", 6, 1)]
[TestCase("[Judas] Series Title (2024) - S01E14", "Series Title (2024)", 1, 14)]
[TestCase("[ReleaseGroup] SeriesTitle S01E1 Webdl 1080p", "SeriesTitle", 1, 1)]
[TestCase("[SubsPlus+] Series no Chill - S02E01 (NF WEB 1080p AVC AAC)", "Series no Chill", 2, 1)]
[TestCase("[SubsPlus+] Series no Chill - S02E01v2 (NF WEB 1080p AVC AAC)", "Series no Chill", 2, 1)]
// [TestCase("", "", 0, 0)]
public void should_parse_single_episode(string postTitle, string title, int seasonNumber, int episodeNumber)

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("[www.test-hyphen.ca] - Series (2011) S01", "Series (2011)")]
[TestCase("test123.ca - Series Time S02 720p HDTV x264 CRON", "Series Time")]
[TestCase("[www.test-hyphen123.co.za] - Series Title S01E01", "Series Title")]
[TestCase("(seriesawake.com) Series Super - 57 [720p] [English Subbed]", "Series Super")]
public void should_not_parse_url_in_name(string postTitle, string title)
{

View File

@@ -44,6 +44,7 @@ namespace NzbDrone.Core.Test.QueueTests
_trackedDownloads = Builder<TrackedDownload>.CreateListOfSize(1)
.All()
.With(v => v.IsTrackable = true)
.With(v => v.DownloadItem = downloadItem)
.With(v => v.RemoteEpisode = remoteEpisode)
.Build()

View File

@@ -137,6 +137,20 @@ namespace NzbDrone.Core.Test.TvTests
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TvMazeId == newSeriesInfo.TvMazeId), It.IsAny<bool>(), It.IsAny<bool>()));
}
[Test]
public void should_update_tmdb_id_if_changed()
{
var newSeriesInfo = _series.JsonClone();
newSeriesInfo.TmdbId = _series.TmdbId + 1;
GivenNewSeriesInfo(newSeriesInfo);
Subject.Execute(new RefreshSeriesCommand(new List<int> { _series.Id }));
Mocker.GetMock<ISeriesService>()
.Verify(v => v.UpdateSeries(It.Is<Series>(s => s.TmdbId == newSeriesInfo.TmdbId), It.IsAny<bool>(), It.IsAny<bool>()));
}
[Test]
public void should_log_error_if_tvdb_id_not_found()
{

View File

@@ -5,6 +5,7 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.AutoTagging;
using NzbDrone.Core.Organizer;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
@@ -28,6 +29,10 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests
.With(s => s.Path = @"C:\Test\name".AsOsAgnostic())
.With(s => s.RootFolderPath = "")
.Build().ToList();
Mocker.GetMock<IAutoTaggingService>()
.Setup(s => s.GetTagChanges(It.IsAny<Series>()))
.Returns(new AutoTaggingChanges());
}
[Test]
@@ -79,5 +84,23 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests
Subject.UpdateSeries(series, false);
}
[Test]
public void should_add_and_remove_tags()
{
_series[0].Tags = new HashSet<int> { 1, 2 };
Mocker.GetMock<IAutoTaggingService>()
.Setup(s => s.GetTagChanges(_series[0]))
.Returns(new AutoTaggingChanges
{
TagsToAdd = new HashSet<int> { 3 },
TagsToRemove = new HashSet<int> { 1 }
});
var result = Subject.UpdateSeries(_series, false);
result[0].Tags.Should().BeEquivalentTo(new[] { 2, 3 });
}
}
}

View File

@@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.AutoTagging;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
@@ -31,6 +33,14 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests
new Season { SeasonNumber = 1, Monitored = true },
new Season { SeasonNumber = 2, Monitored = true }
};
Mocker.GetMock<IAutoTaggingService>()
.Setup(s => s.GetTagChanges(It.IsAny<Series>()))
.Returns(new AutoTaggingChanges());
Mocker.GetMock<ISeriesRepository>()
.Setup(s => s.Update(It.IsAny<Series>()))
.Returns<Series>(r => r);
}
private void GivenExistingSeries()
@@ -68,5 +78,28 @@ namespace NzbDrone.Core.Test.TvTests.SeriesServiceTests
Mocker.GetMock<IEpisodeService>()
.Verify(v => v.SetEpisodeMonitoredBySeason(_fakeSeries.Id, It.IsAny<int>(), It.IsAny<bool>()), Times.Once());
}
[Test]
public void should_add_and_remove_tags()
{
GivenExistingSeries();
var seasonNumber = 1;
var monitored = false;
_fakeSeries.Tags = new HashSet<int> { 1, 2 };
_fakeSeries.Seasons.Single(s => s.SeasonNumber == seasonNumber).Monitored = monitored;
Mocker.GetMock<IAutoTaggingService>()
.Setup(s => s.GetTagChanges(_fakeSeries))
.Returns(new AutoTaggingChanges
{
TagsToAdd = new HashSet<int> { 3 },
TagsToRemove = new HashSet<int> { 1 }
});
var result = Subject.UpdateSeries(_fakeSeries);
result.Tags.Should().BeEquivalentTo(new[] { 2, 3 });
}
}
}

View File

@@ -48,11 +48,11 @@ namespace NzbDrone.Core.Test.UpdateTests
{
const string branch = "main";
UseRealHttp();
var recent = Subject.GetRecentUpdates(branch, new Version(3, 0), null);
var recent = Subject.GetRecentUpdates(branch, new Version(4, 0), null);
recent.Should().NotBeEmpty();
recent.Should().OnlyContain(c => c.Hash.IsNotNullOrWhiteSpace());
recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.3."));
recent.Should().OnlyContain(c => c.FileName.Contains($"Sonarr.{c.Branch}.4."));
recent.Should().OnlyContain(c => c.ReleaseDate.Year >= 2014);
recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.New != null);
recent.Where(c => c.Changes != null).Should().OnlyContain(c => c.Changes.Fixed != null);

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace NzbDrone.Core.Annotations
@@ -59,13 +60,27 @@ namespace NzbDrone.Core.Annotations
public string Value { get; set; }
}
public class FieldSelectOption
public class FieldSelectOption<T>
where T : struct
{
public int Value { get; set; }
public T Value { get; set; }
public string Name { get; set; }
public int Order { get; set; }
public string Hint { get; set; }
public int? ParentValue { get; set; }
public T? ParentValue { get; set; }
public bool? IsDisabled { get; set; }
public Dictionary<string, object> AdditionalProperties { get; set; }
}
public class FieldSelectStringOption
{
public string Value { get; set; }
public string Name { get; set; }
public int Order { get; set; }
public string Hint { get; set; }
public string ParentValue { get; set; }
public bool? IsDisabled { get; set; }
public Dictionary<string, object> AdditionalProperties { get; set; }
}
public enum FieldType

View File

@@ -242,7 +242,7 @@ namespace NzbDrone.Core.Configuration
{
get
{
var urlBase = _serverOptions.UrlBase ?? GetValue("UrlBase", "").Trim('/');
var urlBase = (_serverOptions.UrlBase ?? GetValue("UrlBase", "")).Trim('/');
if (urlBase.IsNullOrWhiteSpace())
{
@@ -419,13 +419,21 @@ namespace NzbDrone.Core.Configuration
throw new InvalidConfigFileException($"{_configFile} is corrupt. Please delete the config file and Sonarr will recreate it.");
}
return XDocument.Parse(_diskProvider.ReadAllText(_configFile));
var xDoc = XDocument.Parse(_diskProvider.ReadAllText(_configFile));
var config = xDoc.Descendants(CONFIG_ELEMENT_NAME).ToList();
if (config.Count != 1)
{
throw new InvalidConfigFileException($"{_configFile} is invalid. Please delete the config file and Sonarr will recreate it.");
}
return xDoc;
}
var xDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
xDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
var newXDoc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"));
newXDoc.Add(new XElement(CONFIG_ELEMENT_NAME));
return xDoc;
return newXDoc;
}
}
catch (XmlException ex)

View File

@@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(206)]
public class add_tmdbid : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Series").AddColumn("TmdbId").AsInt32().WithDefaultValue(0);
Create.Index().OnTable("Series").OnColumn("TmdbId");
}
}
}

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(209)]
public class add_on_import_complete_to_notifications : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Notifications").AddColumn("OnImportComplete").AsBoolean().WithDefaultValue(false);
}
}
}

View File

@@ -0,0 +1,14 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(211)]
public class add_custom_colon_replacement_to_naming_config : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("NamingConfig").AddColumn("CustomColonReplacementFormat").AsString().WithDefaultValue("");
}
}
}

View File

@@ -89,6 +89,7 @@ namespace NzbDrone.Core.Datastore
.Ignore(x => x.ImplementationName)
.Ignore(i => i.SupportsOnGrab)
.Ignore(i => i.SupportsOnDownload)
.Ignore(i => i.SupportsOnImportComplete)
.Ignore(i => i.SupportsOnUpgrade)
.Ignore(i => i.SupportsOnRename)
.Ignore(i => i.SupportsOnSeriesAdd)

View File

@@ -25,6 +25,11 @@ namespace NzbDrone.Core.Download.Aggregation
public RemoteEpisode Augment(RemoteEpisode remoteEpisode)
{
if (remoteEpisode == null)
{
return null;
}
foreach (var augmenter in _augmenters)
{
try

View File

@@ -1,6 +1,5 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Aria2
@@ -13,9 +12,9 @@ namespace NzbDrone.Core.Download.Clients.Aria2
}
}
public class Aria2Settings : IProviderConfig
public class Aria2Settings : DownloadClientSettingsBase<Aria2Settings>
{
private static readonly Aria2SettingsValidator Validator = new Aria2SettingsValidator();
private static readonly Aria2SettingsValidator Validator = new ();
public Aria2Settings()
{
@@ -44,7 +43,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
[FieldDefinition(5, Label = "Directory", Type = FieldType.Textbox, HelpText = "DownloadClientAriaSettingsDirectoryHelpText")]
public string Directory { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -2,7 +2,6 @@ using System.ComponentModel;
using FluentValidation;
using Newtonsoft.Json;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
@@ -18,7 +17,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
}
}
public class TorrentBlackholeSettings : IProviderConfig
public class TorrentBlackholeSettings : DownloadClientSettingsBase<TorrentBlackholeSettings>
{
public TorrentBlackholeSettings()
{
@@ -26,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
ReadOnly = true;
}
private static readonly TorrentBlackholeSettingsValidator Validator = new TorrentBlackholeSettingsValidator();
private static readonly TorrentBlackholeSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "TorrentBlackholeTorrentFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")]
[FieldToken(TokenField.HelpText, "TorrentBlackholeTorrentFolder", "extension", ".torrent")]
@@ -48,7 +47,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
[FieldDefinition(4, Label = "TorrentBlackholeSaveMagnetFilesReadOnly", Type = FieldType.Checkbox, HelpText = "TorrentBlackholeSaveMagnetFilesReadOnlyHelpText")]
public bool ReadOnly { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,6 +1,5 @@
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
@@ -15,9 +14,9 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
}
}
public class UsenetBlackholeSettings : IProviderConfig
public class UsenetBlackholeSettings : DownloadClientSettingsBase<UsenetBlackholeSettings>
{
private static readonly UsenetBlackholeSettingsValidator Validator = new UsenetBlackholeSettingsValidator();
private static readonly UsenetBlackholeSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "UsenetBlackholeNzbFolder", Type = FieldType.Path, HelpText = "BlackholeFolderHelpText")]
[FieldToken(TokenField.HelpText, "UsenetBlackholeNzbFolder", "extension", ".nzb")]
@@ -26,7 +25,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
[FieldDefinition(1, Label = "BlackholeWatchFolder", Type = FieldType.Path, HelpText = "BlackholeWatchFolderHelpText")]
public string WatchFolder { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -124,14 +124,23 @@ namespace NzbDrone.Core.Download.Clients.Deluge
}
var items = new List<DownloadClientItem>();
var ignoredCount = 0;
foreach (var torrent in torrents)
{
if (torrent.Hash == null)
// Silently ignore torrents with no hash
if (torrent.Hash.IsNullOrWhiteSpace())
{
continue;
}
// Ignore torrents without a name, but track to log a single warning for all invalid torrents.
if (torrent.Name.IsNullOrWhiteSpace())
{
ignoredCount++;
continue;
}
var item = new DownloadClientItem();
item.DownloadId = torrent.Hash.ToUpper();
item.Title = torrent.Name;
@@ -189,6 +198,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge
items.Add(item);
}
if (ignoredCount > 0)
{
_logger.Warn("{0} torrent(s) were ignored becuase they did not have a title, check Deluge and remove any invalid torrents");
}
return items;
}

View File

@@ -1,6 +1,5 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Deluge
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge
}
}
public class DelugeSettings : IProviderConfig
public class DelugeSettings : DownloadClientSettingsBase<DelugeSettings>
{
private static readonly DelugeSettingsValidator Validator = new DelugeSettingsValidator();
private static readonly DelugeSettingsValidator Validator = new ();
public DelugeSettings()
{
@@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
[FieldDefinition(11, Label = "DownloadClientDelugeSettingsDirectoryCompleted", Type = FieldType.Textbox, Advanced = true, HelpText = "DownloadClientDelugeSettingsDirectoryCompletedHelpText")]
public string CompletedDirectory { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -0,0 +1,30 @@
using System;
using Equ;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients
{
public abstract class DownloadClientSettingsBase<TSettings> : IProviderConfig, IEquatable<TSettings>
where TSettings : DownloadClientSettingsBase<TSettings>
{
private static readonly MemberwiseEqualityComparer<TSettings> Comparer = MemberwiseEqualityComparer<TSettings>.ByProperties;
public abstract NzbDroneValidationResult Validate();
public bool Equals(TSettings other)
{
return Comparer.Equals(this as TSettings, other);
}
public override bool Equals(object obj)
{
return Equals(obj as TSettings);
}
public override int GetHashCode()
{
return Comparer.GetHashCode(this as TSettings);
}
}
}

View File

@@ -1,8 +1,7 @@
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.DownloadStation
@@ -26,9 +25,9 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
}
}
public class DownloadStationSettings : IProviderConfig
public class DownloadStationSettings : DownloadClientSettingsBase<DownloadStationSettings>
{
private static readonly DownloadStationSettingsValidator Validator = new DownloadStationSettingsValidator();
private static readonly DownloadStationSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "Host", Type = FieldType.Textbox)]
public string Host { get; set; }
@@ -58,7 +57,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
this.Port = 5000;
}
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -3,7 +3,6 @@ using System.Linq;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Download.Clients.Flood.Models;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Flood
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.Flood
}
}
public class FloodSettings : IProviderConfig
public class FloodSettings : DownloadClientSettingsBase<FloodSettings>
{
private static readonly FloodSettingsValidator Validator = new FloodSettingsValidator();
private static readonly FloodSettingsValidator Validator = new ();
public FloodSettings()
{
@@ -69,7 +68,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
[FieldDefinition(10, Label = "DownloadClientFloodSettingsStartOnAdd", Type = FieldType.Checkbox)]
public bool StartOnAdd { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -2,7 +2,6 @@ using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
@@ -34,9 +33,9 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
}
}
public class FreeboxDownloadSettings : IProviderConfig
public class FreeboxDownloadSettings : DownloadClientSettingsBase<FreeboxDownloadSettings>
{
private static readonly FreeboxDownloadSettingsValidator Validator = new FreeboxDownloadSettingsValidator();
private static readonly FreeboxDownloadSettingsValidator Validator = new ();
public FreeboxDownloadSettings()
{
@@ -84,7 +83,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
[FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,7 +1,6 @@
using FluentValidation;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Hadouken
@@ -22,9 +21,9 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
}
}
public class HadoukenSettings : IProviderConfig
public class HadoukenSettings : DownloadClientSettingsBase<HadoukenSettings>
{
private static readonly HadoukenSettingsValidator Validator = new HadoukenSettingsValidator();
private static readonly HadoukenSettingsValidator Validator = new ();
public HadoukenSettings()
{
@@ -57,7 +56,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
[FieldDefinition(6, Label = "Category", Type = FieldType.Textbox, HelpText = "DownloadClientSettingsCategoryHelpText")]
public string Category { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,7 +1,6 @@
using FluentValidation;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.NzbVortex
@@ -23,9 +22,9 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
}
}
public class NzbVortexSettings : IProviderConfig
public class NzbVortexSettings : DownloadClientSettingsBase<NzbVortexSettings>
{
private static readonly NzbVortexSettingsValidator Validator = new NzbVortexSettingsValidator();
private static readonly NzbVortexSettingsValidator Validator = new ();
public NzbVortexSettings()
{
@@ -59,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
[FieldDefinition(6, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(NzbVortexPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")]
public int OlderTvPriority { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,7 +1,6 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Nzbget
@@ -21,9 +20,9 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
}
}
public class NzbgetSettings : IProviderConfig
public class NzbgetSettings : DownloadClientSettingsBase<NzbgetSettings>
{
private static readonly NzbgetSettingsValidator Validator = new NzbgetSettingsValidator();
private static readonly NzbgetSettingsValidator Validator = new ();
public NzbgetSettings()
{
@@ -67,7 +66,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
[FieldDefinition(9, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox, HelpText = "DownloadClientNzbgetSettingsAddPausedHelpText")]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,6 +1,5 @@
using FluentValidation;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
@@ -15,9 +14,9 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
}
}
public class PneumaticSettings : IProviderConfig
public class PneumaticSettings : DownloadClientSettingsBase<PneumaticSettings>
{
private static readonly PneumaticSettingsValidator Validator = new PneumaticSettingsValidator();
private static readonly PneumaticSettingsValidator Validator = new ();
[FieldDefinition(0, Label = "DownloadClientPneumaticSettingsNzbFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsNzbFolderHelpText")]
public string NzbFolder { get; set; }
@@ -25,7 +24,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
[FieldDefinition(1, Label = "DownloadClientPneumaticSettingsStrmFolder", Type = FieldType.Path, HelpText = "DownloadClientPneumaticSettingsStrmFolderHelpText")]
public string StrmFolder { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,7 +1,6 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.QBittorrent
@@ -19,9 +18,9 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
}
}
public class QBittorrentSettings : IProviderConfig
public class QBittorrentSettings : DownloadClientSettingsBase<QBittorrentSettings>
{
private static readonly QBittorrentSettingsValidator Validator = new QBittorrentSettingsValidator();
private static readonly QBittorrentSettingsValidator Validator = new ();
public QBittorrentSettings()
{
@@ -74,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
[FieldDefinition(13, Label = "DownloadClientQbittorrentSettingsContentLayout", Type = FieldType.Select, SelectOptions = typeof(QBittorrentContentLayout), HelpText = "DownloadClientQbittorrentSettingsContentLayoutHelpText")]
public int ContentLayout { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,7 +1,6 @@
using FluentValidation;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Sabnzbd
@@ -32,9 +31,9 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
}
}
public class SabnzbdSettings : IProviderConfig
public class SabnzbdSettings : DownloadClientSettingsBase<SabnzbdSettings>
{
private static readonly SabnzbdSettingsValidator Validator = new SabnzbdSettingsValidator();
private static readonly SabnzbdSettingsValidator Validator = new ();
public SabnzbdSettings()
{
@@ -78,7 +77,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
[FieldDefinition(9, Label = "DownloadClientSettingsOlderPriority", Type = FieldType.Select, SelectOptions = typeof(SabnzbdPriority), HelpText = "DownloadClientSettingsOlderPriorityEpisodeHelpText")]
public int OlderTvPriority { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,10 +1,10 @@
using System;
using System;
namespace NzbDrone.Core.Download.Clients
{
public class TorrentSeedConfiguration
{
public static TorrentSeedConfiguration DefaultConfiguration = new TorrentSeedConfiguration();
public static TorrentSeedConfiguration DefaultConfiguration = new ();
public double? Ratio { get; set; }
public TimeSpan? SeedTime { get; set; }

View File

@@ -2,7 +2,6 @@ using System.Text.RegularExpressions;
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.Transmission
@@ -24,9 +23,9 @@ namespace NzbDrone.Core.Download.Clients.Transmission
}
}
public class TransmissionSettings : IProviderConfig
public class TransmissionSettings : DownloadClientSettingsBase<TransmissionSettings>
{
private static readonly TransmissionSettingsValidator Validator = new TransmissionSettingsValidator();
private static readonly TransmissionSettingsValidator Validator = new ();
public TransmissionSettings()
{
@@ -72,7 +71,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
[FieldDefinition(10, Label = "DownloadClientSettingsAddPaused", Type = FieldType.Checkbox)]
public bool AddPaused { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,6 +1,5 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.RTorrent
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
}
}
public class RTorrentSettings : IProviderConfig
public class RTorrentSettings : DownloadClientSettingsBase<RTorrentSettings>
{
private static readonly RTorrentSettingsValidator Validator = new RTorrentSettingsValidator();
private static readonly RTorrentSettingsValidator Validator = new ();
public RTorrentSettings()
{
@@ -70,7 +69,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
[FieldDefinition(11, Label = "DownloadClientRTorrentSettingsAddStopped", Type = FieldType.Checkbox, HelpText = "DownloadClientRTorrentSettingsAddStoppedHelpText")]
public bool AddStopped { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -1,7 +1,6 @@
using FluentValidation;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.ThingiProvider;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Download.Clients.UTorrent
@@ -17,9 +16,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
}
}
public class UTorrentSettings : IProviderConfig
public class UTorrentSettings : DownloadClientSettingsBase<UTorrentSettings>
{
private static readonly UTorrentSettingsValidator Validator = new UTorrentSettingsValidator();
private static readonly UTorrentSettingsValidator Validator = new ();
public UTorrentSettings()
{
@@ -65,7 +64,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
[FieldToken(TokenField.HelpText, "DownloadClientSettingsInitialState", "clientName", "uTorrent")]
public int IntialState { get; set; }
public NzbDroneValidationResult Validate()
public override NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}

View File

@@ -34,6 +34,8 @@ namespace NzbDrone.Core.Download
private readonly IParsingService _parsingService;
private readonly ISeriesService _seriesService;
private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported;
private readonly IEpisodeService _episodeService;
private readonly IMediaFileService _mediaFileService;
private readonly Logger _logger;
public CompletedDownloadService(IEventAggregator eventAggregator,
@@ -43,6 +45,8 @@ namespace NzbDrone.Core.Download
IParsingService parsingService,
ISeriesService seriesService,
ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported,
IEpisodeService episodeService,
IMediaFileService mediaFileService,
Logger logger)
{
_eventAggregator = eventAggregator;
@@ -52,6 +56,8 @@ namespace NzbDrone.Core.Download
_parsingService = parsingService;
_seriesService = seriesService;
_trackedDownloadAlreadyImported = trackedDownloadAlreadyImported;
_episodeService = episodeService;
_mediaFileService = mediaFileService;
_logger = logger;
}
@@ -64,8 +70,8 @@ namespace NzbDrone.Core.Download
SetImportItem(trackedDownload);
// Only process tracked downloads that are still downloading
if (trackedDownload.State != TrackedDownloadState.Downloading)
// Only process tracked downloads that are still downloading or have been blocked for importing due to an issue with matching
if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked)
{
return;
}
@@ -96,7 +102,7 @@ namespace NzbDrone.Core.Download
if (series == null)
{
trackedDownload.Warn("Series title mismatch; automatic import is not possible. Check the download troubleshooting entry on the wiki for common causes.");
SendManualInteractionRequiredNotification(trackedDownload);
SetStateToImportBlocked(trackedDownload);
return;
}
@@ -108,7 +114,7 @@ namespace NzbDrone.Core.Download
if (seriesMatchType == SeriesMatchType.Id && releaseSource != ReleaseSourceType.InteractiveSearch)
{
trackedDownload.Warn("Found matching series via grab history, but release was matched to series by ID. Automatic import is not possible. See the FAQ for details.");
SendManualInteractionRequiredNotification(trackedDownload);
SetStateToImportBlocked(trackedDownload);
return;
}
@@ -129,7 +135,7 @@ namespace NzbDrone.Core.Download
if (trackedDownload.RemoteEpisode == null)
{
trackedDownload.Warn("Unable to parse download, automatic import is not possible.");
SendManualInteractionRequiredNotification(trackedDownload);
SetStateToImportBlocked(trackedDownload);
return;
}
@@ -187,7 +193,7 @@ namespace NzbDrone.Core.Download
if (statusMessages.Any())
{
trackedDownload.Warn(statusMessages.ToArray());
SendManualInteractionRequiredNotification(trackedDownload);
SetStateToImportBlocked(trackedDownload);
}
}
@@ -198,11 +204,23 @@ namespace NzbDrone.Core.Download
.Count() >= Math.Max(1,
trackedDownload.RemoteEpisode.Episodes.Count);
var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId)
.OrderByDescending(h => h.Date)
.ToList();
var grabbedHistory = historyItems.Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList();
var releaseInfo = grabbedHistory.Count > 0 ? new GrabbedReleaseInfo(grabbedHistory) : null;
if (allEpisodesImported)
{
_logger.Debug("All episodes were imported for {0}", trackedDownload.DownloadItem.Title);
trackedDownload.State = TrackedDownloadState.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, trackedDownload.RemoteEpisode.Series.Id));
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload,
trackedDownload.RemoteEpisode.Series.Id,
importResults.Where(c => c.Result == ImportResultType.Imported).Select(c => c.EpisodeFile).ToList(),
releaseInfo));
return true;
}
@@ -216,12 +234,9 @@ namespace NzbDrone.Core.Download
// safe, but commenting for future benefit.
var atLeastOneEpisodeImported = importResults.Any(c => c.Result == ImportResultType.Imported);
var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId)
.OrderByDescending(h => h.Date)
.ToList();
var allEpisodesImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems);
var episodes = _episodeService.GetEpisodes(trackedDownload.RemoteEpisode.Episodes.Select(e => e.Id));
var files = _mediaFileService.GetFiles(episodes.Select(e => e.EpisodeFileId).Distinct());
if (allEpisodesImportedInHistory)
{
@@ -245,7 +260,7 @@ namespace NzbDrone.Core.Download
}
trackedDownload.State = TrackedDownloadState.Imported;
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, trackedDownload.RemoteEpisode.Series.Id));
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload, trackedDownload.RemoteEpisode.Series.Id, files, releaseInfo));
return true;
}
@@ -254,8 +269,10 @@ namespace NzbDrone.Core.Download
return false;
}
private void SendManualInteractionRequiredNotification(TrackedDownload trackedDownload)
private void SetStateToImportBlocked(TrackedDownload trackedDownload)
{
trackedDownload.State = TrackedDownloadState.ImportBlocked;
if (!trackedDownload.HasNotifiedManualInteractionRequired)
{
var grabbedHistories = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId).Where(h => h.EventType == EpisodeHistoryEventType.Grabbed).ToList();

View File

@@ -1,14 +1,35 @@
using NzbDrone.Core.Indexers;
using System;
using Equ;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.Download
{
public class DownloadClientDefinition : ProviderDefinition
public class DownloadClientDefinition : ProviderDefinition, IEquatable<DownloadClientDefinition>
{
private static readonly MemberwiseEqualityComparer<DownloadClientDefinition> Comparer = MemberwiseEqualityComparer<DownloadClientDefinition>.ByProperties;
[MemberwiseEqualityIgnore]
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; } = 1;
public bool RemoveCompletedDownloads { get; set; } = true;
public bool RemoveFailedDownloads { get; set; } = true;
public bool Equals(DownloadClientDefinition other)
{
return Comparer.Equals(this, other);
}
public override bool Equals(object obj)
{
return Equals(obj as DownloadClientDefinition);
}
public override int GetHashCode()
{
return Comparer.GetHashCode(this);
}
}
}

View File

@@ -1,17 +1,24 @@
using NzbDrone.Common.Messaging;
using System.Collections.Generic;
using NzbDrone.Common.Messaging;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model;
namespace NzbDrone.Core.Download
{
public class DownloadCompletedEvent : IEvent
{
public TrackedDownload TrackedDownload { get; private set; }
public int SeriesId { get; set; }
public int SeriesId { get; private set; }
public List<EpisodeFile> EpisodeFiles { get; private set; }
public GrabbedReleaseInfo Release { get; private set; }
public DownloadCompletedEvent(TrackedDownload trackedDownload, int seriesId)
public DownloadCompletedEvent(TrackedDownload trackedDownload, int seriesId, List<EpisodeFile> episodeFiles, GrabbedReleaseInfo release)
{
TrackedDownload = trackedDownload;
SeriesId = seriesId;
EpisodeFiles = episodeFiles;
Release = release;
}
}
}

View File

@@ -73,8 +73,8 @@ namespace NzbDrone.Core.Download
public void Check(TrackedDownload trackedDownload)
{
// Only process tracked downloads that are still downloading
if (trackedDownload.State != TrackedDownloadState.Downloading)
// Only process tracked downloads that are still downloading or import is blocked (if they fail after attempting to be processed)
if (trackedDownload.State != TrackedDownloadState.Downloading && trackedDownload.State != TrackedDownloadState.ImportBlocked)
{
return;
}

View File

@@ -122,7 +122,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
_trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition,
downloadItem);
if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading)
if (trackedDownload is { State: TrackedDownloadState.Downloading or TrackedDownloadState.ImportBlocked })
{
_failedDownloadService.Check(trackedDownload);
_completedDownloadService.Check(trackedDownload);

View File

@@ -40,6 +40,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
public enum TrackedDownloadState
{
Downloading,
ImportBlocked,
ImportPending,
Importing,
Imported,

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;

View File

@@ -28,6 +28,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads
public class TrackedDownloadService : ITrackedDownloadService,
IHandle<EpisodeInfoRefreshedEvent>,
IHandle<SeriesAddedEvent>,
IHandle<SeriesDeletedEvent>
{
private readonly IParsingService _parsingService;
@@ -278,12 +279,29 @@ namespace NzbDrone.Core.Download.TrackedDownloads
}
}
public void Handle(SeriesAddedEvent message)
{
var cachedItems = _cache.Values
.Where(t =>
t.RemoteEpisode?.Series == null ||
message.Series?.TvdbId == t.RemoteEpisode.Series.TvdbId)
.ToList();
if (cachedItems.Any())
{
cachedItems.ForEach(UpdateCachedItem);
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(GetTrackedDownloads()));
}
}
public void Handle(SeriesDeletedEvent message)
{
var cachedItems = _cache.Values.Where(t =>
t.RemoteEpisode?.Series != null &&
message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id))
.ToList();
var cachedItems = _cache.Values
.Where(t =>
t.RemoteEpisode?.Series != null &&
message.Series.Any(s => s.Id == t.RemoteEpisode.Series.Id || s.TvdbId == t.RemoteEpisode.Series.TvdbId))
.ToList();
if (cachedItems.Any())
{

Some files were not shown because too many files have changed in this diff Show More