1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-18 16:24:34 -04:00

Compare commits

..

60 Commits

Author SHA1 Message Date
Robin Dadswell
bf8d21cd52 initial additions 2021-12-07 16:37:59 +00:00
hugepants
5855773842 Fixed: Grammar in tooltip of download button
When hovering over the tooltip of an unclicked download button (e.g. in interactive search), it hasn't yet been "added" yet so it should be "add".
And once clicked, the tooltip should show "download queue" rather than "downloaded queue" as "downloaded" implies it has already been downloaded.
2021-12-07 08:15:59 -06:00
Robin Dadswell
73f2da72f3 New: Placeholders in notification fields 2021-12-07 09:59:35 +00:00
Robin Dadswell
7dda481824 New: Frontend Placeholders from the Backend 2021-12-07 09:59:35 +00:00
ta264
4d6c3369c6 New: Add osx-arm64 and linux-musl-arm builds
(cherry picked from commit 4ce405728a3ae32c8d0282e8e2b758084e8e5864)
2021-12-06 17:13:41 -06:00
Taloth Saldono
5c7756b575 Fixed: Release Push api broken when no indexer id is specified
(cherry picked from commit 20306a38e1d0f6a7954978cf07cde8e185f79c78)
2021-12-05 23:25:17 -06:00
Weblate
cf8aa09615 Translated using Weblate (Portuguese (Brazil)) [skip ci]
Currently translated at 100.0% (1113 of 1113 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2021-12-02 17:49:35 -06:00
ta264
cdde7d4d8b Workaround .net error serializing new object()
See https://github.com/dotnet/runtime/issues/61995
2021-12-02 21:36:07 +00:00
Weblate
e139708bb2 Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 99.5% (1108 of 1113 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1113 of 1113 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 99.7% (1110 of 1113 strings)

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2021-11-30 22:09:38 -06:00
Qstick
47206dd2bd Cleanup MovieImport Tests 2021-11-28 23:25:13 -06:00
Qstick
9442666493 Fixed: Duplicates on index before housekeeper runs when manual importing over existing file 2021-11-28 22:06:24 -06:00
Qstick
005ad00caf Speed up History by Movie endpoint by avoiding multiple db calls
Closes #6704
2021-11-28 16:38:45 -06:00
bakerboy448
2ae056e727 README updates [skip ci] 2021-11-28 15:55:40 -06:00
Qstick
c538424229 New: Reanalyze media files if file size changes
Fixes #6757
Fixes #6765
Fixes #4482

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
2021-11-28 15:42:44 -06:00
Qstick
507e8ec814 New: Parse 960p as 720p instead of 480p, parse 540p
Fixes #6304
Fixes #6767
2021-11-28 01:38:10 -06:00
Qstick
9d6614b14a New: Support AKA release titles
Co-Authored-By: aeonnoea <46950349+0aeonnoea0@users.noreply.github.com>
2021-11-28 01:37:21 -06:00
Csaba
f9dab9d780 Translated using Weblate (Hungarian) [skip ci]
Currently translated at 100.0% (1113 of 1113 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
2021-11-28 01:31:42 -06:00
Oskari Lavinto
18e0656d21 Translated using Weblate (Finnish) [skip ci]
Currently translated at 99.7% (1110 of 1113 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
2021-11-28 01:31:42 -06:00
Thomas Citharel
759d14cf99 Translated using Weblate (French) [skip ci]
Currently translated at 98.1% (1092 of 1113 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
2021-11-28 01:31:42 -06:00
reloxx
1238f60a5e Translated using Weblate (German) [skip ci]
Currently translated at 100.0% (1113 of 1113 strings)

Translation: Servarr/Radarr
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
2021-11-28 01:31:42 -06:00
Weblate
2d28828e5e Translated using Weblate (Finnish) [skip ci]
Currently translated at 99.7% (1110 of 1113 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 99.7% (1110 of 1113 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 96.6% (1073 of 1110 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 100.0% (1110 of 1110 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (1110 of 1110 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 99.8% (1108 of 1110 strings)

Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: bosci9 <rbos97@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translation: Servarr/Radarr
2021-11-28 01:31:42 -06:00
Qstick
9a395b52ac New: Per download client setting to Remove Completed/Failed
Fixes #6322
Fixes #6328
Fixes #6331
Fixes #6337
2021-11-24 12:51:10 -06:00
Taloth Saldono
e9dffb4819 Fixed SeedConfigProvider cache refresh after indexer settings change
(cherry picked from commit 8e46362ff9de34c3468290861f0606d8fecc1a44)
2021-11-24 12:51:10 -06:00
Qstick
8b93038937 Fixed: Properly handle provider delete in UI 2021-11-24 07:41:51 -06:00
Qstick
c4cf38255e Fixed: Combine content headers with response headers 2021-11-23 19:21:59 -06:00
leaty
1c0621af0a New: Removing rtorrent downloads when seeding criteria have been met
(cherry picked from commit 411be4d0116f0739bb9c71235312d0c5a26dd3a2)

Fixes: #6320
Fixes: #5219
2021-11-23 15:06:44 -06:00
geogolem
dd80a64560 Lidarr, Radarr, and Prowlarr (maybe Readarr too) all use
the same name for their Authentication cookies
.AspNetCore.Forms

Sonarr uses a different name "SonarrAuth"

this commit makes Radarr use "RadarrAuth"...

similar changes should likely be ported to Lidarr, Prowlarr (and possibly Readarr)
2021-11-23 14:10:52 -06:00
bakerboy448
beb22844c9 Update Github Templates [skip ci] [common] 2021-11-23 14:08:53 -06:00
ta264
399f242f87 Fixed: Restarting windows service from UI 2021-11-23 19:52:37 +00:00
ta264
6befbec381 Fixed: Tray app restart 2021-11-23 19:52:37 +00:00
ta264
8b8f79d6c3 New: Use native .NET socks proxy
(cherry picked from commit d93110336fea31565129b356c90043761f8c2c5b)
2021-11-23 19:52:37 +00:00
ta264
e54d4765dd Other package updates
(cherry picked from commit ffefdcaef9f77f76a969ac201abad30b45077ff7)
2021-11-23 19:52:37 +00:00
ta264
4068cfcabb New: Upgrade to .NET 6
(cherry picked from commit 242922f1c351312e0eaae68c4c0ff6c80c16b297)
2021-11-23 19:52:37 +00:00
ta264
c5b736e422 Use modern HttpClient 2021-11-23 19:52:37 +00:00
Roxedus
025634cd19 Complete reject reason for multi-part 2021-11-22 10:43:21 +00:00
Weblate
9c86c20c00 Translated using Weblate (Finnish) [skip ci]
Currently translated at 96.1% (1067 of 1110 strings)

Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translation: Servarr/Radarr
2021-11-20 11:14:23 -06:00
Weblate
7bf44e2771 Merge 2021-11-20 16:46:20 +00:00
Mark McDowall
b18daebc8a Rename QueryTitles to CleanSceneTitles in SearchCriteriaBase
(cherry picked from commit 747a4164e24e9861cacad39cf7d94db398747b38)
2021-11-19 17:46:01 -06:00
ta264
183d3d0872 Fixed: Error adding import list exclusions from discover
Fixes #6739
2021-11-18 21:55:25 +00:00
Weblate
f1de24ccc8 Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 99.8% (1108 of 1110 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 99.8% (1108 of 1110 strings)

Update translation files  [skip ci]

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2021-11-18 01:59:33 -06:00
bakerboy448
498d9086b5 Fixed: Only blocklist pending releases when option is checked
(cherry picked from commit d11c691a7389d8984ddcbbd299d3192690719a1a)

Fixes #6730
Closes #6732
2021-11-17 16:32:48 +00:00
Robin Dadswell
2737937d37 Fixed: Blocklist error on blocklisting release 2021-11-17 12:15:05 +00:00
Weblate
3c9e818933 Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]
Currently translated at 99.8% (1108 of 1110 strings)

Translated using Weblate (Norwegian Bokmål) [skip ci]

Currently translated at 13.2% (147 of 1110 strings)

Translated using Weblate (Arabic) [skip ci]

Currently translated at 96.3% (1070 of 1110 strings)

Translated using Weblate (Portuguese (Brazil)) [skip ci]

Currently translated at 99.7% (1107 of 1110 strings)

Translated using Weblate (Thai) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Bulgarian) [skip ci]

Currently translated at 93.8% (1042 of 1110 strings)

Translated using Weblate (Hindi) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Romanian) [skip ci]

Currently translated at 95.6% (1062 of 1110 strings)

Translated using Weblate (Vietnamese) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Turkish) [skip ci]

Currently translated at 95.9% (1065 of 1110 strings)

Translated using Weblate (Swedish) [skip ci]

Currently translated at 96.3% (1069 of 1110 strings)

Translated using Weblate (Russian) [skip ci]

Currently translated at 97.3% (1081 of 1110 strings)

Translated using Weblate (Portuguese) [skip ci]

Currently translated at 94.9% (1054 of 1110 strings)

Translated using Weblate (Polish) [skip ci]

Currently translated at 96.0% (1066 of 1110 strings)

Translated using Weblate (Dutch) [skip ci]

Currently translated at 99.0% (1099 of 1110 strings)

Translated using Weblate (Korean) [skip ci]

Currently translated at 22.7% (252 of 1110 strings)

Translated using Weblate (Japanese) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Icelandic) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (1110 of 1110 strings)

Translated using Weblate (Hungarian) [skip ci]

Currently translated at 100.0% (1110 of 1110 strings)

Translated using Weblate (Hebrew) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Finnish) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 95.9% (1065 of 1110 strings)

Translated using Weblate (Greek) [skip ci]

Currently translated at 95.9% (1065 of 1110 strings)

Translated using Weblate (Danish) [skip ci]

Currently translated at 95.9% (1065 of 1110 strings)

Translated using Weblate (Czech) [skip ci]

Currently translated at 96.1% (1067 of 1110 strings)

Translated using Weblate (Italian) [skip ci]

Currently translated at 96.5% (1072 of 1110 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 97.2% (1080 of 1110 strings)

Translated using Weblate (Spanish) [skip ci]

Currently translated at 97.2% (1080 of 1110 strings)

Translated using Weblate (French) [skip ci]

Currently translated at 98.1% (1089 of 1110 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 100.0% (1110 of 1110 strings)

Translated using Weblate (German) [skip ci]

Currently translated at 100.0% (1110 of 1110 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 99.8% (1108 of 1110 strings)

Translated using Weblate (Chinese (Simplified) (zh_CN)) [skip ci]

Currently translated at 99.8% (1108 of 1110 strings)

Update translation files  [skip ci]

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Csaba <csab0825@gmail.com>
Co-authored-by: Panagiotis Christoforakis <panagiotischristoforakis@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: dmunozv04 <dmunozv04@gmail.com>
Co-authored-by: mm519897405 <baiya@vip.qq.com>
Co-authored-by: reloxx <reloxx@interia.pl>
Co-authored-by: shenghuoshou <1015886870@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2021-11-17 08:45:43 +00:00
bakerboy448
a8b563de7b Fixed: Cleanse APIKey from Signalr logging
(cherry picked from commit 2983b600268176db56208d6fb014b97ad81bb576)
2021-11-15 09:34:06 -06:00
Mark McDowall
823fe2261e Rename NzbSearchService to ReleaseSearchService
(cherry picked from commit eb4a9f624e716fa932da7f7e5ff975e2d9f02655)
2021-11-14 17:55:28 +00:00
Qstick
329d141128 Translation sync 2021-11-14 11:06:58 -06:00
ta264
d33bed6a36 Fixed: Null reference error getting video file info 2021-11-09 21:03:33 +00:00
Qstick
3acc6a3f9d Bump macOS build agent to 10.15 2021-11-09 18:37:57 -06:00
Qstick
8d0a26e284 Fix Migration NullRef in MigratePrimaries
Fixes #6716
2021-11-09 18:06:43 -06:00
ta264
f0f8a4ffaf New: MediaInfo -> FFProbe
* New: MediaInfo -> FFProbe

* Detect HDR format

* Fix migration for users that tested early ffmpeg
2021-11-09 06:31:05 +00:00
Robin Dadswell
e7ff13085e New: Support On Delete Notifications for Emby 2021-11-07 16:10:46 +00:00
Taloth Saldono
6a8f6dc5f7 Clarified Aria2 RPC Path field.
closes #6709

(cherry picked from commit 44a160fa067940770525312063ce7d749f6fd212)
2021-11-06 18:06:18 -05:00
Qstick
13c03d9958 Fixed: History failed endpoint uses Id in Route
Fixes #6708
2021-11-06 09:28:11 -05:00
Robin Dadswell
5ce1829709 Fixed: Translate strings for single vs multiple options 2021-11-05 13:57:10 +00:00
Mark McDowall
86da4e87ea Fixed: Blocklisting pending releases
Closes #4598

(cherry picked from commit 3f60e28c42d946691c4d44eb391b7888466538d9)
2021-11-05 13:57:10 +00:00
ta264
a219b4a1b8 Fixed: Forms login persists across restarts in docker
[common]
2021-11-02 14:51:43 +00:00
Robin Dadswell
84dd10f032 New: Added Genres to Movie Details page 2021-10-31 21:48:08 -05:00
Qstick
f844cbecaf Bump FluentMigrator to 3.3.1
[common]
2021-10-31 20:58:38 -05:00
bakerboy448
279692f9b1 Fixed: Incorrectly parsing RePACKPOST as Group
(cherry picked from commit c30ce3580a3125317bf56c1e16c23e24c59aecc8)
2021-10-31 10:38:24 -05:00
Mark McDowall
6fcbdc5ba3 New: Release group column on movie details files tab
(cherry picked from commit 30aa5f9070bc9611bb9459d9d96bf293c971f891)
2021-10-31 10:37:29 -05:00
271 changed files with 4565 additions and 3522 deletions

View File

@@ -1,5 +1,4 @@
name: Bug Report
title: "[BUG]: "
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
@@ -64,12 +63,11 @@ body:
required: true
- type: textarea
attributes:
label: Anything else?
label: Trace Logs?
description: |
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
Links? References? Anything that will give us more context about the issue you are encountering!
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true

View File

@@ -1,5 +1,4 @@
name: Feature Request
title: "[FEAT]: "
description: 'Suggest an idea for Radarr'
labels: ['Type: Feature Request', 'Status: Needs Triage']
body:

View File

@@ -1,19 +1,21 @@
# Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/radarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)
[![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors)
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
## Major Features Include:
## Major Features Include
* Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet
@@ -21,53 +23,60 @@ Radarr is a movie collection manager for Usenet and BitTorrent users. It can mon
* Automatically importing downloaded movies
* Recognizing Special Editions, Director's Cut, etc.
* Identifying releases with hardcoded subs
* QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported
* Full integration with Kodi, Plex (notification, library update)
* A beautiful UI
* Identifying releases with AKA movie names
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
* Full integration with Kodi and Plex (notifications, library updates)
* Importing Metadata such as trailers or subtitles
* Adding metadata such as posters and information for Kodi and others to use
* Advanced customization for profiles, such that Radarr will always download the copy you want
* A beautiful UI
## Support
Note: GitHub Issues are for Bugs and Feature Requests Only
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
Note: GitHub Issues are for Bugs and Feature Requests Only
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Radarr/Radarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
## Contributors & Developers
[API Documentation](https://radarr.video/docs/api/)
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Radarr/Radarr/graphs/contributors"><img src="https://opencollective.com/Radarr/contributors.svg?width=890&button=false" /></a>
This project exists thanks to all the people who contribute.
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/radarr/contributing)
[![Contributors List](https://opencollective.com/Radarr/contributors.svg?width=890&button=false)](https://github.com/Radarr/Radarr/graphs/contributors)
## Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Radarr#backer)
<img src="https://opencollective.com/Radarr/backers.svg?width=890"></a>
[![Backers List](https://opencollective.com/Radarr/backers.svg?width=890)](https://opencollective.com/Radarr#backer)
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Radarr#sponsor)
<img src="https://opencollective.com/Radarr/sponsors.svg?width=890"></a>
[![Sponsors List](https://opencollective.com/Radarr/sponsors.svg?width=890)](https://opencollective.com/Radarr#sponsor)
## Mega Sponsors
<img src="https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890"></a>
[![Mega Sponsors List](https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/Radarr#mega-sponsor)
## JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2021
* Copyright 2010-2022

View File

@@ -13,7 +13,7 @@ variables:
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '5.0.401'
dotnetVersion: '6.0.100'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger:
@@ -67,7 +67,7 @@ stages:
enableAnalysis: 'true'
Mac:
osName: 'Mac'
imageName: 'macos-10.14'
imageName: 'macos-10.15'
enableAnalysis: 'false'
Windows:
osName: 'Windows'
@@ -111,23 +111,23 @@ stages:
artifact: '$(osName)Backend'
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/win-x64/publish'
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: WindowsCoreTests
displayName: Publish Windows Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: LinuxCoreTests
displayName: Publish Linux Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: MacCoreTests
displayName: Publish MacOS Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
@@ -144,7 +144,7 @@ stages:
imageName: 'ubuntu-18.04'
Mac:
osName: 'Mac'
imageName: 'macos-10.14'
imageName: 'macos-10.15'
Windows:
osName: 'Windows'
imageName: 'windows-2019'
@@ -203,12 +203,12 @@ stages:
- bash: ./build.sh --packages
displayName: Create Packages
- bash: |
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x86
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x86
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create .NET Core Windows installer
- bash: |
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x64
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x64
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create .NET Core Windows installer
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller'
@@ -241,6 +241,7 @@ stages:
- bash: ./build.sh --packages --enable-bsd
displayName: Create Packages
- bash: |
find . -name "ffprobe" -exec chmod a+x {} \;
find . -name "Radarr" -exec chmod a+x {} \;
find . -name "Radarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits
@@ -250,29 +251,44 @@ stages:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2
displayName: Create Windows x86 Core zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS Core app
displayName: Create MacOS x64 Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS Core tar
displayName: Create MacOS x64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS arm64 Core app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2
displayName: Create MacOS arm64 Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create Linux Core tar
inputs:
@@ -280,7 +296,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2
displayName: Create Linux Musl Core tar
inputs:
@@ -288,7 +304,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2
displayName: Create ARM32 Linux Core tar
inputs:
@@ -296,7 +312,15 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2
displayName: Create ARM32 Linux Musl Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2
displayName: Create ARM64 Linux Core tar
inputs:
@@ -304,7 +328,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create ARM64 Linux Musl Core tar
inputs:
@@ -312,7 +336,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar
inputs:
@@ -320,7 +344,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'Packages'
displayName: Publish Packages
@@ -383,7 +407,7 @@ stages:
osName: 'Mac'
testName: 'MacCore'
poolName: 'Azure Pipelines'
imageName: 'macos-10.14'
imageName: 'macos-10.15'
WindowsCore:
osName: 'Windows'
testName: 'WindowsCore'
@@ -417,16 +441,13 @@ stages:
buildType: 'current'
artifactName: '$(testName)Tests'
targetPath: $(testsFolder)
- bash: |
wget https://mediaarea.net/repo/deb/repo-mediaarea_1.0-11_all.deb
sudo dpkg -i repo-mediaarea_1.0-11_all.deb
sudo apt-get update
sudo apt-get install -y --allow-unauthenticated libmediainfo-dev libmediainfo0v5 mediainfo
displayName: Install mediainfo
condition: and(succeeded(), eq(variables['testName'], 'LinuxCore'))
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
chmod a+x _tests/ffprobe
displayName: Make ffprobe Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
@@ -474,6 +495,9 @@ stages:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: |
chmod a+x _tests/ffprobe
displayName: Make ffprobe Executable
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
@@ -517,7 +541,7 @@ stages:
MacCore:
osName: 'Mac'
testName: 'MacCore'
imageName: 'macos-10.14'
imageName: 'macos-10.15'
pattern: 'Radarr.*.osx-core-x64.tar.gz'
WindowsCore:
osName: 'Windows'
@@ -692,7 +716,7 @@ stages:
failBuild: true
Mac:
osName: 'Mac'
imageName: 'macos-10.14'
imageName: 'macos-10.15'
pattern: 'Radarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows:
@@ -865,8 +889,8 @@ stages:
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |
./build.sh --backend -f net5.0 -r win-x64
TEST_DIR=_tests/net5.0/win-x64/publish/ ./test.sh Windows Unit Coverage
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')

View File

@@ -129,7 +129,7 @@ PackageLinux()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net5.0" ]; then
if [ "$framework" = "net6.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -140,12 +140,13 @@ PackageLinux()
PackageMacOS()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating MacOS Package for $framework"
ProgressStart "Creating MacOS Package for $framework $runtime"
local folder=$artifactsFolder/macos/$framework/Radarr
local folder=$artifactsFolder/$runtime/$framework/Radarr
PackageFiles "$folder" "$framework" "osx-x64"
PackageFiles "$folder" "$framework" "$runtime"
echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.*
@@ -156,7 +157,7 @@ PackageMacOS()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net5.0" ]; then
if [ "$framework" = "net6.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -167,10 +168,11 @@ PackageMacOS()
PackageMacOSApp()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating macOS App Package for $framework"
ProgressStart "Creating macOS App Package for $framework $runtime"
local folder=$artifactsFolder/macos-app/$framework
local folder="$artifactsFolder/$runtime-app/$framework"
rm -rf $folder
mkdir -p $folder
@@ -178,7 +180,7 @@ PackageMacOSApp()
mkdir -p $folder/Radarr.app/Contents/MacOS
echo "Copying Binaries"
cp -r $artifactsFolder/macos/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
cp -r $artifactsFolder/$runtime/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
echo "Removing Update Folder"
rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update
@@ -225,8 +227,8 @@ Package()
PackageWindows "$framework" "$runtime"
;;
osx)
PackageMacOS "$framework"
PackageMacOSApp "$framework"
PackageMacOS "$framework" "$runtime"
PackageMacOSApp "$framework" "$runtime"
;;
esac
}
@@ -326,14 +328,14 @@ then
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
PackageTests "net5.0" "win-x64"
PackageTests "net5.0" "win-x86"
PackageTests "net5.0" "linux-x64"
PackageTests "net5.0" "linux-musl-x64"
PackageTests "net5.0" "osx-x64"
PackageTests "net6.0" "win-x64"
PackageTests "net6.0" "win-x86"
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_BSD" = "YES" ];
then
PackageTests "net5.0" "freebsd-x64"
PackageTests "net6.0" "freebsd-x64"
fi
else
PackageTests "$FRAMEWORK" "$RID"
@@ -362,17 +364,19 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
Package "net5.0" "win-x64"
Package "net5.0" "win-x86"
Package "net5.0" "linux-x64"
Package "net5.0" "linux-musl-x64"
Package "net5.0" "linux-arm64"
Package "net5.0" "linux-musl-arm64"
Package "net5.0" "linux-arm"
Package "net5.0" "osx-x64"
Package "net6.0" "win-x64"
Package "net6.0" "win-x86"
Package "net6.0" "linux-x64"
Package "net6.0" "linux-musl-x64"
Package "net6.0" "linux-arm64"
Package "net6.0" "linux-musl-arm64"
Package "net6.0" "linux-arm"
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
if [ "$ENABLE_BSD" = "YES" ];
then
Package "net5.0" "freebsd-x64"
Package "net6.0" "freebsd-x64"
fi
else
Package "$FRAMEWORK" "$RID"

View File

@@ -282,6 +282,17 @@ class Queue extends Component {
return !!(item && item.movieId);
})
)}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>

View File

@@ -332,6 +332,7 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!movie}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>

View File

@@ -66,7 +66,8 @@ class RemoveQueueItemModal extends Component {
const {
isOpen,
sourceTitle,
canIgnore
canIgnore,
isPending
} = this.props;
const { remove, blocklist } = this.state;
@@ -89,18 +90,22 @@ class RemoveQueueItemModal extends Component {
{translate('RemoveFromQueueText', [sourceTitle])}
</div>
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
{
isPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
@@ -137,6 +142,7 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -67,7 +67,8 @@ class RemoveQueueItemsModal extends Component {
const {
isOpen,
selectedCount,
canIgnore
canIgnore,
allPending
} = this.props;
const { remove, blocklist } = this.state;
@@ -82,30 +83,34 @@ class RemoveQueueItemsModal extends Component {
onModalClose={this.onModalClose}
>
<ModalHeader>
Remove Selected Item{selectedCount > 1 ? 's' : ''}
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
{translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', [selectedCount, selectedCount > 1 ? 's' : ''])}
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
</div>
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
{
allPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
Blocklist Release{selectedCount > 1 ? 's' : ''}
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
@@ -141,6 +146,7 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -16,4 +16,9 @@
color: #3a3f51;
font-size: 21px;
line-height: inherit;
&.small {
color: #909293;
font-size: 18px;
}
}

View File

@@ -1,5 +1,7 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { sizes } from 'Helpers/Props';
import styles from './FieldSet.css';
class FieldSet extends Component {
@@ -9,13 +11,14 @@ class FieldSet extends Component {
render() {
const {
size,
legend,
children
} = this.props;
return (
<fieldset className={styles.fieldSet}>
<legend className={styles.legend}>
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
{legend}
</legend>
{children}
@@ -26,8 +29,13 @@ class FieldSet extends Component {
}
FieldSet.propTypes = {
size: PropTypes.oneOf(sizes.all).isRequired,
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
children: PropTypes.node
};
FieldSet.defaultProps = {
size: sizes.MEDIUM
};
export default FieldSet;

View File

@@ -64,6 +64,7 @@ function ProviderFieldFormGroup(props) {
label,
helpText,
helpLink,
placeholder,
value,
type,
advanced,
@@ -96,6 +97,7 @@ function ProviderFieldFormGroup(props) {
label={label}
helpText={helpText}
helpLink={helpLink}
placeholder={placeholder}
value={value}
values={getSelectValues(selectOptions)}
errors={errors}
@@ -121,6 +123,7 @@ ProviderFieldFormGroup.propTypes = {
label: PropTypes.string.isRequired,
helpText: PropTypes.string,
helpLink: PropTypes.string,
placeholder: PropTypes.string,
value: PropTypes.any,
type: PropTypes.string.isRequired,
advanced: PropTypes.bool.isRequired,

View File

@@ -151,7 +151,8 @@
.qualityProfileName,
.statusName,
.studio,
.collection {
.collection,
.genres {
font-weight: 300;
font-size: 17px;
}

View File

@@ -266,6 +266,7 @@ class MovieDetails extends Component {
qualityProfileId,
monitored,
studio,
genres,
collection,
overview,
youTubeTrailerId,
@@ -582,6 +583,19 @@ class MovieDetails extends Component {
</span>
</InfoLabel>
}
{
!!genres.length && !isSmallScreen &&
<InfoLabel
className={styles.detailsInfoLabel}
title={translate('Genres')}
size={sizes.LARGE}
>
<span className={styles.genres}>
{genres.join(', ')}
</span>
</InfoLabel>
}
</div>
<Measure onMeasure={this.onMeasure}>
@@ -766,6 +780,7 @@ MovieDetails.propTypes = {
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
studio: PropTypes.string,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
collection: PropTypes.object,
youTubeTrailerId: PropTypes.string,
isAvailable: PropTypes.bool.isRequired,
@@ -798,6 +813,7 @@ MovieDetails.propTypes = {
};
MovieDetails.defaultProps = {
genres: [],
tags: [],
isSaving: false,
sizeOnDisk: 0

View File

@@ -33,3 +33,9 @@
width: 100px;
}
.releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px;
}

View File

@@ -74,6 +74,7 @@ class MovieFileEditorRow extends Component {
mediaInfo,
relativePath,
size,
releaseGroup,
quality,
qualityCutoffNotMet,
customFormats,
@@ -155,6 +156,12 @@ class MovieFileEditorRow extends Component {
}
</TableRowCell>
<TableRowCell
className={styles.releaseGroup}
>
{releaseGroup}
</TableRowCell>
<TableRowCell
className={styles.formats}
>
@@ -216,6 +223,7 @@ MovieFileEditorRow.propTypes = {
size: PropTypes.number.isRequired,
relativePath: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired,
releaseGroup: PropTypes.string,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -39,6 +39,11 @@ const columns = [
label: translate('Quality'),
isVisible: true
},
{
name: 'releaseGroup',
label: translate('ReleaseGroup'),
isVisible: true
},
{
name: 'quality.customFormats',
label: translate('Formats'),

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -13,7 +14,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css';
@@ -45,7 +46,10 @@ class EditDownloadClientModalContent extends Component {
implementationName,
name,
enable,
protocol,
priority,
removeCompletedDownloads,
removeFailedDownloads,
fields,
message
} = item;
@@ -136,6 +140,38 @@ class EditDownloadClientModalContent extends Component {
/>
</FormGroup>
<FieldSet
size={sizes.SMALL}
legend={translate('CompletedDownloadHandling')}
>
<FormGroup>
<FormLabel>{translate('RemoveCompleted')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeCompletedDownloads"
helpText={translate('RemoveCompletedDownloadsHelpText')}
{...removeCompletedDownloads}
onChange={onInputChange}
/>
</FormGroup>
{
protocol.value !== 'torrent' &&
<FormGroup>
<FormLabel>{translate('RemoveFailed')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeFailedDownloads"
helpText={translate('RemoveFailedDownloadsHelpText')}
{...removeFailedDownloads}
onChange={onInputChange}
/>
</FormGroup>
}
</FieldSet>
</Form>
}
</ModalBody>

View File

@@ -1,12 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, sizes } from 'Helpers/Props';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function DownloadClientOptions(props) {
@@ -34,11 +35,15 @@ function DownloadClientOptions(props) {
}
{
hasSettings && !isFetching && !error &&
hasSettings && !isFetching && !error && advancedSettings &&
<div>
<FieldSet legend={translate('CompletedDownloadHandling')}>
<Form>
<FormGroup size={sizes.MEDIUM}>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
@@ -50,22 +55,6 @@ function DownloadClientOptions(props) {
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Remove')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeCompletedDownloads"
helpText={translate('RemoveCompletedDownloadsHelpText')}
onChange={onInputChange}
{...settings.removeCompletedDownloads}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
@@ -102,23 +91,10 @@ function DownloadClientOptions(props) {
{...settings.autoRedownloadFailed}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('Remove')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="removeFailedDownloads"
helpText={translate('RemoveFailedDownloadsHelpText')}
onChange={onInputChange}
{...settings.removeFailedDownloads}
/>
</FormGroup>
</Form>
<Alert kind={kinds.INFO}>
{translate('RemoveDownloadsAlert')}
</Alert>
</FieldSet>
</div>
}

View File

@@ -14,6 +14,7 @@ function createRemoveItemHandler(section, url) {
const ajaxOptions = {
url: `${url}/${id}?${$.param(queryParams, true)}`,
dataType: 'text',
method: 'DELETE'
};

View File

@@ -600,6 +600,7 @@ export const actionHandlers = handleThunks({
const promise = createAjaxRequest({
url: '/exclusions/bulk',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(exclusions)
}).request;

View File

@@ -30,7 +30,7 @@
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@fortawesome/react-fontawesome": "0.1.14",
"@microsoft/signalr": "5.0.10",
"@microsoft/signalr": "6.0.0",
"@sentry/browser": "6.13.2",
"@sentry/integrations": "6.13.2",
"classnames": "2.3.1",
@@ -82,6 +82,7 @@
},
"devDependencies": {
"@babel/core": "7.13.16",
"@babel/eslint-parser": "7.13.14",
"@babel/plugin-proposal-class-properties": "7.13.0",
"@babel/plugin-proposal-decorators": "7.13.15",
"@babel/plugin-proposal-export-default-from": "7.12.13",
@@ -94,7 +95,6 @@
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.13.15",
"@babel/preset-react": "7.13.13",
"@babel/eslint-parser": "7.13.14",
"autoprefixer": "10.2.5",
"babel-loader": "8.2.2",
"babel-plugin-inline-classnames": "2.0.1",
@@ -125,12 +125,12 @@
"run-sequence": "2.2.1",
"streamqueue": "1.1.2",
"style-loader": "2.0.0",
"stylelint": "13.13.0",
"stylelint-order": "4.1.0",
"url-loader": "4.1.1",
"webpack": "5.35.1",
"webpack-cli": "4.6.0",
"webpack-livereload-plugin": "3.0.1",
"worker-loader": "3.0.8",
"stylelint": "13.13.0",
"stylelint-order": "4.1.0"
"worker-loader": "3.0.8"
}
}

View File

@@ -2,10 +2,11 @@
<!-- Common to all Radarr Projects -->
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<PlatformTarget>AnyCPU</PlatformTarget>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;linux-x64;linux-musl-x64;linux-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
<RuntimeIdentifiers>win-x64;win-x86;osx-x64;osx-arm64;linux-x64;linux-musl-x64;linux-arm;linux-musl-arm;linux-arm64;linux-musl-arm64</RuntimeIdentifiers>
<RadarrRootDir>$(MSBuildThisFileDirectory)..\</RadarrRootDir>
@@ -89,13 +90,13 @@
<!-- Standard testing packages -->
<ItemGroup Condition="'$(TestProject)'=='true'">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="NunitXml.TestLogger" Version="2.1.62" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Include="NunitXml.TestLogger" Version="3.0.117" />
</ItemGroup>
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net5.0'">
<ItemGroup Condition="'$(TestProject)'=='true' and '$(TargetFramework)'=='net6.0'">
<PackageReference Include="coverlet.collector" Version="3.0.4-preview.27.ge7cb7c3b40" />
</ItemGroup>

View File

@@ -6,7 +6,7 @@
<add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
<add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
<add key="SQLite" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/SQLite/nuget/v3/index.json" />
<add key="FluentMigrator" value="https://pkgs.dev.azure.com/fluentmigrator/fluentmigrator/_packaging/fluentmigrator/nuget/v3/index.json" />
<add key="coverlet-nightly" value="https://pkgs.dev.azure.com/Servarr/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
<add key="FFMpegCore" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/FFMpegCore/nuget/v3/index.json" />
</packageSources>
</configuration>

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBuilder" Version="6.1.0" />

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Selenium.Support" Version="3.141.0" />

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using FluentAssertions;
using Moq;
@@ -15,8 +16,11 @@ using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy;
using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common;
using NzbDrone.Test.Common.Categories;
using HttpClient = NzbDrone.Common.Http.HttpClient;
namespace NzbDrone.Common.Test.Http
{
@@ -31,6 +35,8 @@ namespace NzbDrone.Common.Test.Http
private string _httpBinHost;
private string _httpBinHost2;
private System.Net.Http.HttpClient _httpClient = new ();
[OneTimeSetUp]
public void FixtureSetUp()
{
@@ -38,7 +44,7 @@ namespace NzbDrone.Common.Test.Http
var mainHost = "httpbin.servarr.com";
// Use mirrors for tests that use two hosts
var candidates = new[] { "eu.httpbin.org", /* "httpbin.org", */ "www.httpbin.org" };
var candidates = new[] { "httpbin1.servarr.com" };
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
_httpBinHost = mainHost;
@@ -46,29 +52,20 @@ namespace NzbDrone.Common.Test.Http
TestLogger.Info($"{candidates.Length} TestSites available.");
_httpBinSleep = _httpBinHosts.Length < 2 ? 100 : 10;
_httpBinSleep = 10;
}
private bool IsTestSiteAvailable(string site)
{
try
{
var req = WebRequest.Create($"https://{site}/get") as HttpWebRequest;
var res = req.GetResponse() as HttpWebResponse;
var res = _httpClient.GetAsync($"https://{site}/get").GetAwaiter().GetResult();
if (res.StatusCode != HttpStatusCode.OK)
{
return false;
}
try
{
req = WebRequest.Create($"https://{site}/status/429") as HttpWebRequest;
res = req.GetResponse() as HttpWebResponse;
}
catch (WebException ex)
{
res = ex.Response as HttpWebResponse;
}
res = _httpClient.GetAsync($"https://{site}/status/429").GetAwaiter().GetResult();
if (res == null || res.StatusCode != (HttpStatusCode)429)
{
@@ -95,10 +92,13 @@ namespace NzbDrone.Common.Test.Http
Mocker.GetMock<IOsInfo>().Setup(c => c.Name).Returns("TestOS");
Mocker.GetMock<IOsInfo>().Setup(c => c.Version).Returns("9.0.0");
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Enabled);
Mocker.SetConstant<IUserAgentBuilder>(Mocker.Resolve<UserAgentBuilder>());
Mocker.SetConstant<ICacheManager>(Mocker.Resolve<CacheManager>());
Mocker.SetConstant<ICreateManagedWebProxy>(Mocker.Resolve<ManagedWebProxyFactory>());
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.GetMock<IConfigService>().Object, TestLogger));
Mocker.SetConstant<IRateLimitService>(Mocker.Resolve<RateLimitService>());
Mocker.SetConstant<IEnumerable<IHttpRequestInterceptor>>(Array.Empty<IHttpRequestInterceptor>());
Mocker.SetConstant<IHttpDispatcher>(Mocker.Resolve<TDispatcher>());
@@ -138,6 +138,28 @@ namespace NzbDrone.Common.Test.Http
response.Content.Should().NotBeNullOrWhiteSpace();
}
[TestCase(CertificateValidationType.Enabled)]
[TestCase(CertificateValidationType.DisabledForLocalAddresses)]
public void bad_ssl_should_fail_when_remote_validation_enabled(CertificateValidationType validationType)
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(validationType);
var request = new HttpRequest($"https://expired.badssl.com");
Assert.Throws<HttpRequestException>(() => Subject.Execute(request));
ExceptionVerification.ExpectedErrors(2);
}
[Test]
public void bad_ssl_should_pass_if_remote_validation_disabled()
{
Mocker.GetMock<IConfigService>().SetupGet(x => x.CertificateValidation).Returns(CertificateValidationType.Disabled);
var request = new HttpRequest($"https://expired.badssl.com");
Subject.Execute(request);
ExceptionVerification.ExpectedErrors(0);
}
[Test]
public void should_execute_typed_get()
{
@@ -162,15 +184,44 @@ namespace NzbDrone.Common.Test.Http
response.Resource.Data.Should().Be(message);
}
[TestCase("gzip")]
public void should_execute_get_using_gzip(string compression)
[Test]
public void should_execute_post_with_content_type()
{
var request = new HttpRequest($"https://{_httpBinHost}/{compression}");
var message = "{ my: 1 }";
var request = new HttpRequest($"https://{_httpBinHost}/post");
request.SetContent(message);
request.Headers.ContentType = "application/json";
var response = Subject.Post<HttpBinResource>(request);
response.Resource.Data.Should().Be(message);
}
[Test]
public void should_execute_get_using_gzip()
{
var request = new HttpRequest($"https://{_httpBinHost}/gzip");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Be(compression);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("gzip");
response.Resource.Gzipped.Should().BeTrue();
response.Resource.Brotli.Should().BeFalse();
}
[Test]
public void should_execute_get_using_brotli()
{
var request = new HttpRequest($"https://{_httpBinHost}/brotli");
var response = Subject.Get<HttpBinResource>(request);
response.Resource.Headers["Accept-Encoding"].ToString().Should().Contain("br");
response.Resource.Gzipped.Should().BeFalse();
response.Resource.Brotli.Should().BeTrue();
}
[TestCase(HttpStatusCode.Unauthorized)]
@@ -337,13 +388,38 @@ namespace NzbDrone.Common.Test.Http
{
var file = GetTempFilePath();
Assert.Throws<WebException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
Assert.Throws<HttpException>(() => Subject.DownloadFile("https://download.sonarr.tv/wrongpath", file));
File.Exists(file).Should().BeFalse();
ExceptionVerification.ExpectedWarns(1);
}
[Test]
public void should_not_write_redirect_content_to_stream()
{
var file = GetTempFilePath();
using (var fileStream = new FileStream(file, FileMode.Create))
{
var request = new HttpRequest($"http://{_httpBinHost}/redirect/1");
request.AllowAutoRedirect = false;
request.ResponseStream = fileStream;
var response = Subject.Get(request);
response.StatusCode.Should().Be(HttpStatusCode.Moved);
}
ExceptionVerification.ExpectedErrors(1);
File.Exists(file).Should().BeTrue();
var fileInfo = new FileInfo(file);
fileInfo.Length.Should().Be(0);
}
[Test]
public void should_send_cookie()
{
@@ -743,7 +819,7 @@ namespace NzbDrone.Common.Test.Http
{
try
{
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeUriString(malformedCookie)}";
string url = $"https://{_httpBinHost}/response-headers?Set-Cookie={Uri.EscapeDataString(malformedCookie)}";
var requestSet = new HttpRequest(url);
requestSet.AllowAutoRedirect = false;
@@ -773,6 +849,7 @@ namespace NzbDrone.Common.Test.Http
public string Url { get; set; }
public string Data { get; set; }
public bool Gzipped { get; set; }
public bool Brotli { get; set; }
}
public class HttpCookieResource

View File

@@ -62,6 +62,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
[TestCase("Hardlinking episode file: /home/mySecret/Downloads to /media/abc.mkv")]
[TestCase("Hardlink '/home/mySecret/Downloads/abs.mkv' to '/media/abc.mkv' failed.")]
[TestCase("https://notifiarr.com/notifier.php: api=1234530f-422f-4aac-b6b3-01233210aaaa&radarr_health_issue_message=Download")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]
// Announce URLs (passkeys) Magnet & Tracker
[TestCase(@"magnet_uri"":""magnet:?xt=urn:btih:9pr04sgkillroyimaveql2tyu8xyui&dn=&tr=https%3a%2f%2fxxx.yyy%2f9pr04sg601233210imaveql2tyu8xyui%2fannounce""}")]

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Host\Radarr.Host.csproj" />

View File

@@ -3,6 +3,8 @@ using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Composition.Extensions;
using NzbDrone.Common.EnvironmentInfo;
@@ -25,12 +27,15 @@ namespace NzbDrone.Common.Test
.AddNzbDroneLogger()
.AutoAddServices(Bootstrap.ASSEMBLIES)
.AddDummyDatabase()
.AddStartupContext(new StartupContext("first", "second"))
.GetServiceProvider();
.AddStartupContext(new StartupContext("first", "second"));
container.GetRequiredService<IAppFolderFactory>().Register();
container.RegisterInstance<IHostLifetime>(new Mock<IHostLifetime>().Object);
Mocker.SetConstant<System.IServiceProvider>(container);
var serviceProvider = container.GetServiceProvider();
serviceProvider.GetRequiredService<IAppFolderFactory>().Register();
Mocker.SetConstant<System.IServiceProvider>(serviceProvider);
var handlers = Subject.BuildAll<IHandle<ApplicationStartedEvent>>()
.Select(c => c.GetType().FullName);

View File

@@ -106,7 +106,7 @@ namespace NzbDrone.Common
Stream inStream = File.OpenRead(compressedFile);
Stream gzipStream = new GZipInputStream(inStream);
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream);
TarArchive tarArchive = TarArchive.CreateInputTarArchive(gzipStream, null);
tarArchive.ExtractContents(destination);
tarArchive.Close();

View File

@@ -75,10 +75,6 @@ namespace NzbDrone.Common.Composition
{
mappedName = "libsqlite3.so.0";
}
else if (libraryName == "mediainfo")
{
mappedName = "libmediainfo.so.0";
}
}
return NativeLibrary.Load(mappedName, assembly, dllImportSearchPath);

View File

@@ -1,9 +1,9 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Principal;
using System.ServiceProcess;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using NLog;
using NzbDrone.Common.Processes;
@@ -14,16 +14,13 @@ namespace NzbDrone.Common.EnvironmentInfo
private readonly Logger _logger;
private readonly DateTime _startTime = DateTime.UtcNow;
public RuntimeInfo(IServiceProvider serviceProvider, Logger logger)
public RuntimeInfo(IHostLifetime hostLifetime, Logger logger)
{
_logger = logger;
IsWindowsService = !IsUserInteractive &&
OsInfo.IsWindows &&
serviceProvider.ServiceExist(ServiceProvider.SERVICE_NAME) &&
serviceProvider.GetStatus(ServiceProvider.SERVICE_NAME) == ServiceControllerStatus.StartPending;
IsWindowsService = hostLifetime is WindowsServiceLifetime;
// net5.0 will return Radarr.dll for entry assembly, we need the actual
// net6.0 will return Radarr.dll for entry assembly, we need the actual
// executable name (Radarr on linux). On mono this will return the location of
// the mono executable itself, which is not what we want.
var entry = Process.GetCurrentProcess().MainModule;

View File

@@ -6,13 +6,6 @@ namespace NzbDrone.Common.Extensions
{
public static class EnumerableExtensions
{
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
var knownKeys = new HashSet<TKey>();
return source.Where(element => knownKeys.Add(keySelector(element)));
}
public static IEnumerable<TFirst> IntersectBy<TFirst, TSecond, TKey>(this IEnumerable<TFirst> first,
Func<TFirst, TKey> firstKeySelector,
IEnumerable<TSecond> second,

View File

@@ -287,6 +287,11 @@ namespace NzbDrone.Common.Extensions
return appFolderInfo.AppDataFolder;
}
public static string GetDataProtectionPath(this IAppFolderInfo appFolderInfo)
{
return Path.Combine(GetAppDataPath(appFolderInfo), "asp");
}
public static string GetLogFolder(this IAppFolderInfo appFolderInfo)
{
return Path.Combine(GetAppDataPath(appFolderInfo), "logs");

View File

@@ -0,0 +1,11 @@
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace NzbDrone.Common.Http.Dispatchers
{
public interface ICertificateValidationService
{
bool ShouldByPassValidationError(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors);
}
}

View File

@@ -5,6 +5,5 @@ namespace NzbDrone.Common.Http.Dispatchers
public interface IHttpDispatcher
{
HttpResponse GetResponse(HttpRequest request, CookieContainer cookies);
void DownloadFile(string url, string fileName);
}
}

View File

@@ -1,12 +1,13 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Reflection;
using System.Net.Http;
using System.Net.Security;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using NLog.Fluent;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http.Proxy;
@@ -14,200 +15,191 @@ namespace NzbDrone.Common.Http.Dispatchers
{
public class ManagedHttpDispatcher : IHttpDispatcher
{
private const string NO_PROXY_KEY = "no-proxy";
private const int connection_establish_timeout = 2000;
private static bool useIPv6 = Socket.OSSupportsIPv6;
private static bool hasResolvedIPv6Availability;
private readonly IHttpProxySettingsProvider _proxySettingsProvider;
private readonly ICreateManagedWebProxy _createManagedWebProxy;
private readonly ICertificateValidationService _certificateValidationService;
private readonly IUserAgentBuilder _userAgentBuilder;
private readonly IPlatformInfo _platformInfo;
private readonly ICached<System.Net.Http.HttpClient> _httpClientCache;
private readonly Logger _logger;
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider, ICreateManagedWebProxy createManagedWebProxy, IUserAgentBuilder userAgentBuilder, IPlatformInfo platformInfo, Logger logger)
public ManagedHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
ICreateManagedWebProxy createManagedWebProxy,
ICertificateValidationService certificateValidationService,
IUserAgentBuilder userAgentBuilder,
ICacheManager cacheManager,
Logger logger)
{
_proxySettingsProvider = proxySettingsProvider;
_createManagedWebProxy = createManagedWebProxy;
_certificateValidationService = certificateValidationService;
_userAgentBuilder = userAgentBuilder;
_platformInfo = platformInfo;
_logger = logger;
_httpClientCache = cacheManager.GetCache<System.Net.Http.HttpClient>(typeof(ManagedHttpDispatcher));
}
public HttpResponse GetResponse(HttpRequest request, CookieContainer cookies)
{
var webRequest = (HttpWebRequest)WebRequest.Create((Uri)request.Url);
var requestMessage = new HttpRequestMessage(request.Method, (Uri)request.Url);
requestMessage.Headers.UserAgent.ParseAdd(_userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent));
requestMessage.Headers.ConnectionClose = !request.ConnectionKeepAlive;
// Deflate is not a standard and could break depending on implementation.
// we should just stick with the more compatible Gzip
//http://stackoverflow.com/questions/8490718/how-to-decompress-stream-deflated-with-java-util-zip-deflater-in-net
webRequest.AutomaticDecompression = DecompressionMethods.GZip;
webRequest.Method = request.Method.ToString();
webRequest.UserAgent = _userAgentBuilder.GetUserAgent(request.UseSimplifiedUserAgent);
webRequest.KeepAlive = request.ConnectionKeepAlive;
webRequest.AllowAutoRedirect = false;
webRequest.CookieContainer = cookies;
if (request.RequestTimeout != TimeSpan.Zero)
var cookieHeader = cookies.GetCookieHeader((Uri)request.Url);
if (cookieHeader.IsNotNullOrWhiteSpace())
{
webRequest.Timeout = (int)Math.Ceiling(request.RequestTimeout.TotalMilliseconds);
requestMessage.Headers.Add("Cookie", cookieHeader);
}
webRequest.Proxy = GetProxy(request.Url);
using var cts = new CancellationTokenSource();
if (request.RequestTimeout != TimeSpan.Zero)
{
cts.CancelAfter(request.RequestTimeout);
}
else
{
// The default for System.Net.Http.HttpClient
cts.CancelAfter(TimeSpan.FromSeconds(100));
}
if (request.ContentData != null)
{
requestMessage.Content = new ByteArrayContent(request.ContentData);
}
if (request.Headers != null)
{
AddRequestHeaders(webRequest, request.Headers);
AddRequestHeaders(requestMessage, request.Headers);
}
HttpWebResponse httpWebResponse;
var httpClient = GetClient(request.Url);
HttpResponseMessage responseMessage;
try
{
if (request.ContentData != null)
{
webRequest.ContentLength = request.ContentData.Length;
using (var writeStream = webRequest.GetRequestStream())
{
writeStream.Write(request.ContentData, 0, request.ContentData.Length);
}
}
httpWebResponse = (HttpWebResponse)webRequest.GetResponse();
responseMessage = httpClient.Send(requestMessage, cts.Token);
}
catch (WebException e)
catch (HttpRequestException e)
{
httpWebResponse = (HttpWebResponse)e.Response;
if (httpWebResponse == null)
{
// The default messages for WebException on mono are pretty horrible.
if (e.Status == WebExceptionStatus.NameResolutionFailure)
{
throw new WebException($"DNS Name Resolution Failure: '{webRequest.RequestUri.Host}'", e.Status);
}
else if (e.ToString().Contains("TLS Support not"))
{
throw new TlsFailureException(webRequest, e);
}
else if (e.ToString().Contains("The authentication or decryption has failed."))
{
throw new TlsFailureException(webRequest, e);
}
else if (OsInfo.IsNotWindows)
{
throw new WebException($"{e.Message}: '{webRequest.RequestUri}'", e, e.Status, e.Response);
}
else
{
throw;
}
}
_logger.Error(e, "HttpClient error");
throw;
}
byte[] data = null;
using (var responseStream = httpWebResponse.GetResponseStream())
using (var responseStream = responseMessage.Content.ReadAsStream())
{
if (responseStream != null && responseStream != Stream.Null)
{
try
{
data = responseStream.ToBytes();
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
{
// A target ResponseStream was specified, write to that instead.
// But only on the OK status code, since we don't want to write failures and redirects.
responseStream.CopyTo(request.ResponseStream);
}
else
{
data = responseStream.ToBytes();
}
}
catch (Exception ex)
{
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, httpWebResponse);
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
}
}
}
return new HttpResponse(request, new HttpHeader(httpWebResponse.Headers), data, httpWebResponse.StatusCode);
var headers = responseMessage.Headers.ToNameValueCollection();
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode);
}
public void DownloadFile(string url, string fileName)
protected virtual System.Net.Http.HttpClient GetClient(HttpUri uri)
{
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
var uri = new HttpUri(url);
using (var webClient = new GZipWebClient())
{
webClient.Headers.Add(HttpRequestHeader.UserAgent, _userAgentBuilder.GetUserAgent());
webClient.Proxy = GetProxy(uri);
webClient.DownloadFile(uri.FullUri, fileName);
stopWatch.Stop();
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
}
catch (WebException e)
{
_logger.Warn("Failed to get response from: {0} {1}", url, e.Message);
throw;
}
catch (Exception e)
{
_logger.Warn(e, "Failed to get response from: " + url);
throw;
}
}
protected virtual IWebProxy GetProxy(HttpUri uri)
{
IWebProxy proxy = null;
var proxySettings = _proxySettingsProvider.GetProxySettings(uri);
var key = proxySettings?.Key ?? NO_PROXY_KEY;
return _httpClientCache.Get(key, () => CreateHttpClient(proxySettings));
}
protected virtual System.Net.Http.HttpClient CreateHttpClient(HttpProxySettings proxySettings)
{
var handler = new SocketsHttpHandler()
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli,
UseCookies = false, // sic - we don't want to use a shared cookie container
AllowAutoRedirect = false,
MaxConnectionsPerServer = 12,
ConnectCallback = onConnect,
SslOptions = new SslClientAuthenticationOptions
{
RemoteCertificateValidationCallback = _certificateValidationService.ShouldByPassValidationError
}
};
if (proxySettings != null)
{
proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
handler.Proxy = _createManagedWebProxy.GetWebProxy(proxySettings);
}
return proxy;
var client = new System.Net.Http.HttpClient(handler)
{
Timeout = Timeout.InfiniteTimeSpan
};
return client;
}
protected virtual void AddRequestHeaders(HttpWebRequest webRequest, HttpHeader headers)
protected virtual void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
{
foreach (var header in headers)
{
switch (header.Key)
{
case "Accept":
webRequest.Accept = header.Value;
webRequest.Headers.Accept.ParseAdd(header.Value);
break;
case "Connection":
webRequest.Connection = header.Value;
webRequest.Headers.Connection.Clear();
webRequest.Headers.Connection.Add(header.Value);
break;
case "Content-Length":
webRequest.ContentLength = Convert.ToInt64(header.Value);
AddContentHeader(webRequest, "Content-Length", header.Value);
break;
case "Content-Type":
webRequest.ContentType = header.Value;
AddContentHeader(webRequest, "Content-Type", header.Value);
break;
case "Date":
webRequest.Date = HttpHeader.ParseDateTime(header.Value);
webRequest.Headers.Remove("Date");
webRequest.Headers.Date = HttpHeader.ParseDateTime(header.Value);
break;
case "Expect":
webRequest.Expect = header.Value;
webRequest.Headers.Expect.ParseAdd(header.Value);
break;
case "Host":
webRequest.Host = header.Value;
webRequest.Headers.Host = header.Value;
break;
case "If-Modified-Since":
webRequest.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
webRequest.Headers.IfModifiedSince = HttpHeader.ParseDateTime(header.Value);
break;
case "Range":
throw new NotImplementedException();
case "Referer":
webRequest.Referer = header.Value;
webRequest.Headers.Add("Referer", header.Value);
break;
case "Transfer-Encoding":
webRequest.TransferEncoding = header.Value;
webRequest.Headers.TransferEncoding.ParseAdd(header.Value);
break;
case "User-Agent":
throw new NotSupportedException("User-Agent other than Radarr not allowed.");
@@ -219,5 +211,79 @@ namespace NzbDrone.Common.Http.Dispatchers
}
}
}
private void AddContentHeader(HttpRequestMessage request, string header, string value)
{
var headers = request.Content?.Headers;
if (headers == null)
{
return;
}
headers.Remove(header);
headers.Add(header, value);
}
private static async ValueTask<Stream> onConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// Until .NET supports an implementation of Happy Eyeballs (https://tools.ietf.org/html/rfc8305#section-2), let's make IPv4 fallback work in a simple way.
// This issue is being tracked at https://github.com/dotnet/runtime/issues/26177 and expected to be fixed in .NET 6.
if (useIPv6)
{
try
{
var localToken = cancellationToken;
if (!hasResolvedIPv6Availability)
{
// to make things move fast, use a very low timeout for the initial ipv6 attempt.
var quickFailCts = new CancellationTokenSource(connection_establish_timeout);
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, quickFailCts.Token);
localToken = linkedTokenSource.Token;
}
return await attemptConnection(AddressFamily.InterNetworkV6, context, localToken);
}
catch
{
// very naively fallback to ipv4 permanently for this execution based on the response of the first connection attempt.
// note that this may cause users to eventually get switched to ipv4 (on a random failure when they are switching networks, for instance)
// but in the interest of keeping this implementation simple, this is acceptable.
useIPv6 = false;
}
finally
{
hasResolvedIPv6Availability = true;
}
}
// fallback to IPv4.
return await attemptConnection(AddressFamily.InterNetwork, context, cancellationToken);
}
private static async ValueTask<Stream> attemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
{
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
NoDelay = true
};
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
// The stream should take the ownership of the underlying socket,
// closing it when it's disposed.
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
}
}

View File

@@ -1,15 +0,0 @@
using System;
using System.Net;
namespace NzbDrone.Common.Http
{
public class GZipWebClient : WebClient
{
protected override WebRequest GetWebRequest(Uri address)
{
var request = (HttpWebRequest)base.GetWebRequest(address);
request.AutomaticDecompression = DecompressionMethods.GZip;
return request;
}
}
}

View File

@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.EnvironmentInfo;
@@ -119,8 +121,6 @@ namespace NzbDrone.Common.Http
var stopWatch = Stopwatch.StartNew();
PrepareRequestCookies(request, cookieContainer);
var response = _httpDispatcher.GetResponse(request, cookieContainer);
HandleResponseCookies(response, cookieContainer);
@@ -187,57 +187,98 @@ namespace NzbDrone.Common.Http
}
}
private void PrepareRequestCookies(HttpRequest request, CookieContainer cookieContainer)
private void HandleResponseCookies(HttpResponse response, CookieContainer container)
{
// Don't collect persistnet cookies for intermediate/redirected urls.
/*lock (_cookieContainerCache)
foreach (Cookie cookie in container.GetAllCookies())
{
var presistentContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
var persistentCookies = presistentContainer.GetCookies((Uri)request.Url);
var existingCookies = cookieContainer.GetCookies((Uri)request.Url);
cookie.Expired = true;
}
cookieContainer.Add(persistentCookies);
cookieContainer.Add(existingCookies);
}*/
}
private void HandleResponseCookies(HttpResponse response, CookieContainer cookieContainer)
{
var cookieHeaders = response.GetCookieHeaders();
if (cookieHeaders.Empty())
{
return;
}
AddCookiesToContainer(response.Request.Url, cookieHeaders, container);
if (response.Request.StoreResponseCookie)
{
lock (_cookieContainerCache)
{
var persistentCookieContainer = _cookieContainerCache.Get("container", () => new CookieContainer());
foreach (var cookieHeader in cookieHeaders)
{
try
{
persistentCookieContainer.SetCookies((Uri)response.Request.Url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", response.Request.Url);
}
}
AddCookiesToContainer(response.Request.Url, cookieHeaders, persistentCookieContainer);
}
}
}
private void AddCookiesToContainer(HttpUri url, string[] cookieHeaders, CookieContainer container)
{
foreach (var cookieHeader in cookieHeaders)
{
try
{
container.SetCookies((Uri)url, cookieHeader);
}
catch (Exception ex)
{
_logger.Debug(ex, "Invalid cookie in {0}", url);
}
}
}
public void DownloadFile(string url, string fileName)
{
_httpDispatcher.DownloadFile(url, fileName);
var fileNamePart = fileName + ".part";
try
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Directory != null && !fileInfo.Directory.Exists)
{
fileInfo.Directory.Create();
}
_logger.Debug("Downloading [{0}] to [{1}]", url, fileName);
var stopWatch = Stopwatch.StartNew();
using (var fileStream = new FileStream(fileNamePart, FileMode.Create, FileAccess.ReadWrite))
{
var request = new HttpRequest(url);
request.AllowAutoRedirect = true;
request.ResponseStream = fileStream;
var response = Get(request);
if (response.Headers.ContentType != null && response.Headers.ContentType.Contains("text/html"))
{
throw new HttpException(request, response, "Site responded with html content.");
}
}
stopWatch.Stop();
if (File.Exists(fileName))
{
File.Delete(fileName);
}
File.Move(fileNamePart, fileName);
_logger.Debug("Downloading Completed. took {0:0}s", stopWatch.Elapsed.Seconds);
}
finally
{
if (File.Exists(fileNamePart))
{
File.Delete(fileNamePart);
}
}
}
public HttpResponse Get(HttpRequest request)
{
request.Method = HttpMethod.GET;
request.Method = HttpMethod.Get;
return Execute(request);
}
@@ -251,13 +292,13 @@ namespace NzbDrone.Common.Http
public HttpResponse Head(HttpRequest request)
{
request.Method = HttpMethod.HEAD;
request.Method = HttpMethod.Head;
return Execute(request);
}
public HttpResponse Post(HttpRequest request)
{
request.Method = HttpMethod.POST;
request.Method = HttpMethod.Post;
return Execute(request);
}

View File

@@ -1,14 +1,30 @@
using System;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Common.Http
{
public static class WebHeaderCollectionExtensions
{
public static NameValueCollection ToNameValueCollection(this HttpHeaders headers)
{
var result = new NameValueCollection();
foreach (var header in headers)
{
result.Add(header.Key, header.Value.ConcatToString(";"));
}
return result;
}
}
public class HttpHeader : NameValueCollection, IEnumerable<KeyValuePair<string, string>>, IEnumerable
{
public HttpHeader(NameValueCollection headers)
@@ -16,6 +32,11 @@ namespace NzbDrone.Common.Http
{
}
public HttpHeader(HttpHeaders headers)
: base(headers.ToNameValueCollection())
{
}
public HttpHeader()
{
}

View File

@@ -1,14 +0,0 @@
namespace NzbDrone.Common.Http
{
public enum HttpMethod
{
GET,
POST,
PUT,
DELETE,
HEAD,
OPTIONS,
PATCH,
MERGE
}
}

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
@@ -11,6 +13,7 @@ namespace NzbDrone.Common.Http
{
public HttpRequest(string url, HttpAccept httpAccept = null)
{
Method = HttpMethod.Get;
Url = new HttpUri(url);
Headers = new HttpHeader();
AllowAutoRedirect = true;
@@ -49,6 +52,7 @@ namespace NzbDrone.Common.Http
public TimeSpan RequestTimeout { get; set; }
public TimeSpan RateLimit { get; set; }
public string RateLimitKey { get; set; }
public Stream ResponseStream { get; set; }
public override string ToString()
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using NzbDrone.Common.Extensions;
@@ -35,7 +36,7 @@ namespace NzbDrone.Common.Http
{
BaseUrl = new HttpUri(baseUrl);
ResourceUrl = string.Empty;
Method = HttpMethod.GET;
Method = HttpMethod.Get;
QueryParams = new List<KeyValuePair<string, string>>();
SuffixQueryParams = new List<KeyValuePair<string, string>>();
Segments = new Dictionary<string, string>();
@@ -271,7 +272,7 @@ namespace NzbDrone.Common.Http
public virtual HttpRequestBuilder Post()
{
Method = HttpMethod.POST;
Method = HttpMethod.Post;
return this;
}
@@ -362,7 +363,7 @@ namespace NzbDrone.Common.Http
public virtual HttpRequestBuilder AddFormParameter(string key, object value)
{
if (Method != HttpMethod.POST)
if (Method != HttpMethod.Post)
{
throw new NotSupportedException("HttpRequest Method must be POST to add FormParameter.");
}
@@ -378,7 +379,7 @@ namespace NzbDrone.Common.Http
public virtual HttpRequestBuilder AddFormUpload(string name, string fileName, byte[] data, string contentType = "application/octet-stream")
{
if (Method != HttpMethod.POST)
if (Method != HttpMethod.Post)
{
throw new NotSupportedException("HttpRequest Method must be POST to add FormUpload.");
}

View File

@@ -1,6 +1,7 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Newtonsoft.Json;
using NzbDrone.Common.Serializer;
@@ -17,14 +18,14 @@ namespace NzbDrone.Common.Http
public JsonRpcRequestBuilder(string baseUrl)
: base(baseUrl)
{
Method = HttpMethod.POST;
Method = HttpMethod.Post;
JsonParameters = new List<object>();
}
public JsonRpcRequestBuilder(string baseUrl, string method, IEnumerable<object> parameters)
: base(baseUrl)
{
Method = HttpMethod.POST;
Method = HttpMethod.Post;
JsonMethod = method;
JsonParameters = parameters.ToList();
}

View File

@@ -1,9 +1,5 @@
using System;
using System.Linq;
using System;
using System.Net;
using System.Net.Sockets;
using com.LandonKey.SocksWebProxy;
using com.LandonKey.SocksWebProxy.Proxy;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
@@ -33,54 +29,37 @@ namespace NzbDrone.Common.Http.Proxy
}
private IWebProxy CreateWebProxy(HttpProxySettings proxySettings)
{
var uri = GetProxyUri(proxySettings);
if (uri == null)
{
return null;
}
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
{
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
}
else
{
return new WebProxy(uri, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
}
}
private Uri GetProxyUri(HttpProxySettings proxySettings)
{
switch (proxySettings.Type)
{
case ProxyType.Http:
if (proxySettings.Username.IsNotNullOrWhiteSpace() && proxySettings.Password.IsNotNullOrWhiteSpace())
{
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray, new NetworkCredential(proxySettings.Username, proxySettings.Password));
}
else
{
return new WebProxy(proxySettings.Host + ":" + proxySettings.Port, proxySettings.BypassLocalAddress, proxySettings.BypassListAsArray);
}
return new Uri("http://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks4:
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Four, proxySettings.Username, proxySettings.Password), false);
return new Uri("socks4://" + proxySettings.Host + ":" + proxySettings.Port);
case ProxyType.Socks5:
return new SocksWebProxy(new ProxyConfig(IPAddress.Loopback, GetNextFreePort(), GetProxyIpAddress(proxySettings.Host), proxySettings.Port, ProxyConfig.SocksVersion.Five, proxySettings.Username, proxySettings.Password), false);
return new Uri("socks5://" + proxySettings.Host + ":" + proxySettings.Port);
default:
return null;
}
return null;
}
private static IPAddress GetProxyIpAddress(string host)
{
IPAddress ipAddress;
if (!IPAddress.TryParse(host, out ipAddress))
{
try
{
ipAddress = Dns.GetHostEntry(host).AddressList.OrderByDescending(a => a.AddressFamily == AddressFamily.InterNetwork).First();
}
catch (Exception e)
{
throw new InvalidOperationException(string.Format("Unable to resolve proxy hostname '{0}' to a valid IP address.", host), e);
}
}
return ipAddress;
}
private static int GetNextFreePort()
{
var listener = new TcpListener(IPAddress.Loopback, 0);
listener.Start();
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
listener.Stop();
return port;
}
}
}

View File

@@ -11,7 +11,7 @@ namespace NzbDrone.Common.Instrumentation
private static readonly Regex[] CleansingRules = new[]
{
// Url
new Regex(@"(?<=\?|&|: )(apikey|token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&|: )(apikey|(?:access[-_]?)?token|passkey|auth|authkey|user|uid|api|[a-z_]*apikey|account|passwd)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"(?<=\?|&)[^=]*?(username|password)=(?<secret>[^&=]+?)(?= |&|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/(?!rss)(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),
new Regex(@"torrentleech\.org/rss/download/[0-9]+/(?<secret>[0-9a-z]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase),

View File

@@ -1,24 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="4.7.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="DotNet4.SocksProxy" Version="1.4.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NLog" Version="4.7.11" />
<PackageReference Include="DryIoc.dll" Version="4.8.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NLog" Version="4.7.12" />
<PackageReference Include="NLog.Extensions.Logging" Version="1.7.4" />
<PackageReference Include="Sentry" Version="3.9.3" />
<PackageReference Include="SharpZipLib" Version="1.2.0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="Sentry" Version="3.11.1" />
<PackageReference Include="SharpZipLib" Version="1.3.3" />
<PackageReference Include="System.Text.Json" Version="6.0.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="5.0.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="5.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="6.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
<ApplicationIcon>..\NzbDrone.Host\Radarr.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="!$(RuntimeIdentifier.StartsWith('win'))">
<AssemblyName>Radarr</AssemblyName>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.net>
<connectionManagement>
<add address="*" maxconnection="100" />
</connectionManagement>
</system.net>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
</startup>
<runtime>
<loadFromRemoteSources enabled="true" />
</runtime>
</configuration>

View File

@@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,131 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Download.Clients.RTorrent;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class cdh_per_downloadclientFixture : MigrationTest<cdh_per_downloadclient>
{
[Test]
public void should_set_cdh_to_enabled()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "Deluge",
Implementation = "Deluge",
Priority = 1,
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
MovieCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
});
});
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeFalse();
items.First().RemoveFailedDownloads.Should().BeTrue();
}
[Test]
public void should_set_cdh_to_disabled_when_globally_disabled()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Config").Row(new
{
Key = "removecompleteddownloads",
Value = "True"
});
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "Deluge",
Implementation = "Deluge",
Priority = 1,
Settings = new DelugeSettings85
{
Host = "127.0.0.1",
MovieCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "DelugeSettings"
});
});
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeTrue();
items.First().RemoveFailedDownloads.Should().BeTrue();
}
[Test]
public void should_disable_remove_for_existing_rtorrent()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("DownloadClients").Row(new
{
Enable = 1,
Name = "RTorrent",
Implementation = "RTorrent",
Priority = 1,
Settings = new RTorrentSettings
{
Host = "127.0.0.1",
MovieCategory = "abc",
UrlBase = "/my/"
}.ToJson(),
ConfigContract = "RTorrentSettings"
});
});
var items = db.Query<DownloadClientDefinition158>("SELECT * FROM DownloadClients");
items.Should().HaveCount(1);
items.First().RemoveCompletedDownloads.Should().BeFalse();
items.First().RemoveFailedDownloads.Should().BeTrue();
}
}
public class DownloadClientDefinition158
{
public int Id { get; set; }
public bool Enable { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string Implementation { get; set; }
public JsonElement Settings { get; set; }
public string ConfigContract { get; set; }
public bool RemoveCompletedDownloads { get; set; }
public bool RemoveFailedDownloads { get; set; }
}
public class DelugeSettings85
{
public string Host { get; set; }
public int Port { get; set; }
public string UrlBase { get; set; }
public string Password { get; set; }
public string MovieCategory { get; set; }
public int RecentTvPriority { get; set; }
public int OlderTvPriority { get; set; }
public bool UseSsl { get; set; }
}
}

View File

@@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
var remoteMovie = new RemoteMovie();
remoteMovie.ParsedMovieInfo = new ParsedMovieInfo();
remoteMovie.ParsedMovieInfo.MovieTitle = "A Movie";
remoteMovie.ParsedMovieInfo.MovieTitles = new List<string> { "A Movie" };
remoteMovie.ParsedMovieInfo.Year = 1998;
remoteMovie.ParsedMovieInfo.Quality = quality;

View File

@@ -1,8 +1,9 @@
using System.Collections.Generic;
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
@@ -31,8 +32,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
};
Mocker
.GetMock<IIndexerRepository>()
.GetMock<IIndexerFactory>()
.Setup(m => m.Get(It.IsAny<int>()))
.Throws(new ModelNotFoundException(typeof(IndexerDefinition), -1));
Mocker
.GetMock<IIndexerFactory>()
.Setup(m => m.Get(1))
.Returns(_fakeIndexerDefinition);
_specification = Mocker.Resolve<IndexerTagSpecification>();
@@ -98,5 +104,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria()).Accepted.Should().BeFalse();
}
[Test]
public void release_without_indexerid_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeMovie.Tags = new HashSet<int> { 123, 789 };
_fakeRelease.IndexerId = 0;
_specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
[Test]
public void release_with_invalid_indexerid_should_return_true()
{
_fakeIndexerDefinition.Tags = new HashSet<int> { 456 };
_fakeMovie.Tags = new HashSet<int> { 123, 789 };
_fakeRelease.IndexerId = 2;
_specification.IsSatisfiedBy(_parseResultMulti, new MovieSearchCriteria { MonitoredEpisodesOnly = true }).Accepted.Should().BeTrue();
}
}
}

View File

@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
{
Quality = quality,
Year = 1998,
MovieTitle = "A Movie",
MovieTitles = new List<string> { "A Movie" },
},
Movie = movie,

View File

@@ -52,7 +52,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
_pending.Add(new PendingRelease
{
Id = id,
ParsedMovieInfo = new ParsedMovieInfo { MovieTitle = title, Year = year },
ParsedMovieInfo = new ParsedMovieInfo { MovieTitles = new List<string> { title }, Year = year },
MovieId = _movie.Id
});
}

View File

@@ -49,13 +49,13 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
ParsedMovieInfo = new ParsedMovieInfo()
{
MovieTitle = "A Movie",
MovieTitles = new List<string> { "A Movie" },
Year = 1998
}
};
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.Is<ParsedMovieInfo>(i => i.MovieTitle == "A Movie"), It.IsAny<string>(), null))
.Setup(s => s.Map(It.Is<ParsedMovieInfo>(i => i.PrimaryMovieTitle == "A Movie"), It.IsAny<string>(), null))
.Returns(new MappingResult { RemoteMovie = remoteEpisode });
ParseMovieTitle();
@@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
ParsedMovieInfo = new ParsedMovieInfo()
{
MovieTitle = "A Movie",
MovieTitles = { "A Movie" },
Year = 1998
}
};
@@ -156,7 +156,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
ParsedMovieInfo = new ParsedMovieInfo()
{
MovieTitle = "A Movie",
MovieTitles = { "A Movie" },
Year = 1998
}
};

View File

@@ -11,6 +11,7 @@ using NzbDrone.Common.TPL;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Http;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Security;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.Framework
@@ -25,7 +26,8 @@ namespace NzbDrone.Core.Test.Framework
Mocker.SetConstant<IHttpProxySettingsProvider>(new HttpProxySettingsProvider(Mocker.Resolve<ConfigService>()));
Mocker.SetConstant<ICreateManagedWebProxy>(new ManagedWebProxyFactory(Mocker.Resolve<CacheManager>()));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<IPlatformInfo>(), TestLogger));
Mocker.SetConstant<ICertificateValidationService>(new X509CertificateValidationService(Mocker.Resolve<ConfigService>(), TestLogger));
Mocker.SetConstant<IHttpDispatcher>(new ManagedHttpDispatcher(Mocker.Resolve<IHttpProxySettingsProvider>(), Mocker.Resolve<ICreateManagedWebProxy>(), Mocker.Resolve<ICertificateValidationService>(), Mocker.Resolve<UserAgentBuilder>(), Mocker.Resolve<CacheManager>(), TestLogger));
Mocker.SetConstant<IHttpClient>(new HttpClient(Array.Empty<IHttpRequestInterceptor>(), Mocker.Resolve<CacheManager>(), Mocker.Resolve<RateLimitService>(), Mocker.Resolve<IHttpDispatcher>(), TestLogger));
Mocker.SetConstant<IRadarrCloudRequestBuilder>(new RadarrCloudRequestBuilder());
}

View File

@@ -70,84 +70,5 @@ namespace NzbDrone.Core.Test.HistoryTests
downloadHistory.Should().HaveCount(1);
}
[Test]
public void should_get_movie_history()
{
var historyMovie1 = Builder<MovieHistory>.CreateNew()
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
.With(c => c.Languages = new List<Language> { Language.English })
.With(c => c.MovieId = 12)
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
.BuildNew();
var historyMovie2 = Builder<MovieHistory>.CreateNew()
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
.With(c => c.Languages = new List<Language> { Language.English })
.With(c => c.MovieId = 13)
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
.BuildNew();
Subject.Insert(historyMovie1);
Subject.Insert(historyMovie2);
var movieHistory = Subject.GetByMovieId(12, null);
movieHistory.Should().HaveCount(1);
}
[Test]
public void should_sort_movie_history_by_date()
{
var historyFirst = Builder<MovieHistory>.CreateNew()
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
.With(c => c.Languages = new List<Language> { Language.English })
.With(c => c.MovieId = 12)
.With(c => c.EventType = MovieHistoryEventType.MovieFileRenamed)
.With(c => c.Date = DateTime.UtcNow)
.BuildNew();
var historySecond = Builder<MovieHistory>.CreateNew()
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
.With(c => c.Languages = new List<Language> { Language.English })
.With(c => c.MovieId = 12)
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
.With(c => c.Date = DateTime.UtcNow.AddMinutes(10))
.BuildNew();
Subject.Insert(historyFirst);
Subject.Insert(historySecond);
var movieHistory = Subject.GetByMovieId(12, null);
movieHistory.Should().HaveCount(2);
movieHistory.First().EventType.Should().Be(MovieHistoryEventType.Grabbed);
}
[Test]
public void should_delete_history_items_by_movieId()
{
var items = Builder<MovieHistory>.CreateListOfSize(5)
.TheFirst(1)
.With(c => c.MovieId = _movie2.Id)
.TheRest()
.With(c => c.MovieId = _movie1.Id)
.All()
.With(c => c.Id = 0)
.With(c => c.Quality = new QualityModel(Quality.Bluray1080p))
.With(c => c.Languages = new List<Language> { Language.English })
.With(c => c.EventType = MovieHistoryEventType.Grabbed)
.BuildListOfNew();
Db.InsertMany(items);
Subject.DeleteForMovies(new List<int> { _movie1.Id });
var removedItems = Subject.GetByMovieId(_movie1.Id, null);
var nonRemovedItems = Subject.GetByMovieId(_movie2.Id, null);
removedItems.Should().HaveCount(0);
nonRemovedItems.Should().HaveCount(1);
}
}
}

View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.IndexerSearchTests
{
public class ReleaseSearchServiceFixture : CoreTest<ReleaseSearchService>
{
private Mock<IIndexer> _mockIndexer;
private Movie _movie;
[SetUp]
public void SetUp()
{
_mockIndexer = Mocker.GetMock<IIndexer>();
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = 1 });
_mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true);
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.AutomaticSearchEnabled(true))
.Returns(new List<IIndexer> { _mockIndexer.Object });
Mocker.GetMock<IMakeDownloadDecision>()
.Setup(s => s.GetSearchDecision(It.IsAny<List<Parser.Model.ReleaseInfo>>(), It.IsAny<SearchCriteriaBase>()))
.Returns(new List<DownloadDecision>());
_movie = Builder<Movie>.CreateNew()
.With(v => v.Monitored = true)
.Build();
Mocker.GetMock<IMovieService>()
.Setup(v => v.GetMovie(_movie.Id))
.Returns(_movie);
Mocker.GetMock<IMovieTranslationService>()
.Setup(s => s.GetAllTranslationsForMovie(It.IsAny<int>()))
.Returns(new List<MovieTranslation>());
}
private List<SearchCriteriaBase> WatchForSearchCriteria()
{
var result = new List<SearchCriteriaBase>();
_mockIndexer.Setup(v => v.Fetch(It.IsAny<MovieSearchCriteria>()))
.Callback<MovieSearchCriteria>(s => result.Add(s))
.Returns(new List<Parser.Model.ReleaseInfo>());
return result;
}
[Test]
public void Tags_IndexerTags_MovieNoTags_IndexerNotIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1,
Tags = new HashSet<int> { 3 }
});
var allCriteria = WatchForSearchCriteria();
Subject.MovieSearch(_movie, true, false);
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
criteria.Count.Should().Be(0);
}
[Test]
public void Tags_IndexerNoTags_MovieTags_IndexerIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1
});
_movie = Builder<Movie>.CreateNew()
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 3 })
.Build();
Mocker.GetMock<IMovieService>()
.Setup(v => v.GetMovie(_movie.Id))
.Returns(_movie);
var allCriteria = WatchForSearchCriteria();
Subject.MovieSearch(_movie, true, false);
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
criteria.Count.Should().Be(1);
}
[Test]
public void Tags_IndexerAndMovieTagsMatch_IndexerIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1,
Tags = new HashSet<int> { 1, 2, 3 }
});
_movie = Builder<Movie>.CreateNew()
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 3, 4, 5 })
.Build();
Mocker.GetMock<IMovieService>()
.Setup(v => v.GetMovie(_movie.Id))
.Returns(_movie);
var allCriteria = WatchForSearchCriteria();
Subject.MovieSearch(_movie, true, false);
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
criteria.Count.Should().Be(1);
}
[Test]
public void Tags_IndexerAndMovieTagsMismatch_IndexerNotIncluded()
{
_mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition
{
Id = 1,
Tags = new HashSet<int> { 1, 2, 3 }
});
_movie = Builder<Movie>.CreateNew()
.With(v => v.Monitored = true)
.With(v => v.Tags = new HashSet<int> { 4, 5, 6 })
.Build();
Mocker.GetMock<IMovieService>()
.Setup(v => v.GetMovie(_movie.Id))
.Returns(_movie);
var allCriteria = WatchForSearchCriteria();
Subject.MovieSearch(_movie, true, false);
var criteria = allCriteria.OfType<MovieSearchCriteria>().ToList();
criteria.Count.Should().Be(0);
}
}
}

View File

@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
public void should_replace_some_special_characters(string input, string expected)
{
Subject.SceneTitles = new List<string> { input };
Subject.QueryTitles.First().Should().Be(expected);
Subject.CleanSceneTitles.First().Should().Be(expected);
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -30,7 +31,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
var recentFeed = ReadAllText(@"Files/Indexers/FileList/RecentFeed.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Linq;
using System.Net.Http;
using System.Text;
using FluentAssertions;
using Moq;
@@ -34,7 +35,7 @@ namespace NzbDrone.Core.Test.IndexerTests.HDBitsTests
var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), responseJson));
var torrents = Subject.FetchRecent();

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -88,7 +89,7 @@ namespace NzbDrone.Core.Test.IndexerTests.IPTorrentsTests
var recentFeed = ReadAllText(@"Files/Indexers/IPTorrents/IPTorrents.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -41,7 +42,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NewznabTests
var recentFeed = ReadAllText(@"Files/Indexers/Newznab/newznab_nzb_su.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -30,7 +31,7 @@ namespace NzbDrone.Core.Test.IndexerTests.NyaaTests
var recentFeed = ReadAllText(@"Files/Indexers/Nyaa/Nyaa.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Test.IndexerTests.OmgwtfnzbsTests
var recentFeed = ReadAllText(@"Files/Indexers/Omgwtfnzbs/Omgwtfnzbs.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();

View File

@@ -1,4 +1,5 @@
using System.Linq;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -34,11 +35,11 @@ namespace NzbDrone.Core.Test.IndexerTests.PTPTests
var responseJson = ReadAllText(fileName);
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.POST)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Post)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), authStream.ToString()));
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader { ContentType = HttpAccept.Json.Value }, responseJson));
var torrents = Subject.FetchRecent();

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -35,7 +36,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
var recentFeed = ReadAllText(@"Files/Indexers/Rarbg/RecentFeed_v2.json");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
@@ -62,7 +63,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
public void should_parse_error_20_as_empty_results()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 20, error: \"some message\" }"));
var releases = Subject.FetchRecent();
@@ -74,7 +75,7 @@ namespace NzbDrone.Core.Test.IndexerTests.RarbgTests
public void should_warn_on_unknown_error()
{
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), "{ error_code: 25, error: \"some message\" }"));
var releases = Subject.FetchRecent();

View File

@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Net.Http;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -42,7 +43,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_hdaccess_net.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
@@ -71,7 +72,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();
@@ -101,7 +102,7 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests
var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_animetosho.xml");
Mocker.GetMock<IHttpClient>()
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.GET)))
.Setup(o => o.Execute(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
.Returns<HttpRequest>(r => new HttpResponse(r, new HttpHeader(), recentFeed));
var releases = Subject.FetchRecent();

View File

@@ -45,6 +45,10 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Mocker.GetMock<IRootFolderService>()
.Setup(s => s.GetBestRootFolderPath(It.IsAny<string>()))
.Returns(_rootFolder);
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesByMovie(It.IsAny<int>()))
.Returns(new List<MovieFile>());
}
private void GivenRootFolder(params string[] subfolders)
@@ -117,7 +121,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.Clean(It.IsAny<Movie>(), It.IsAny<List<string>>()), Times.Never());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie), Times.Never());
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie, false), Times.Never());
}
[Test]
@@ -152,7 +156,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -177,7 +181,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -197,7 +201,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -229,7 +233,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
.Verify(v => v.Clean(It.IsAny<Movie>(), It.IsAny<List<string>>()), Times.Once());
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie), Times.Never());
.Verify(v => v.GetImportDecisions(It.IsAny<List<string>>(), _movie, false), Times.Never());
}
[Test]
@@ -247,7 +251,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -270,7 +274,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 4), _movie, false), Times.Once());
}
[Test]
@@ -289,7 +293,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -309,7 +313,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -326,7 +330,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -343,7 +347,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -362,7 +366,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie, false), Times.Once());
}
[Test]
@@ -379,7 +383,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 2), _movie, false), Times.Once());
}
[Test]
@@ -397,7 +401,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
[Test]
@@ -414,7 +418,7 @@ namespace NzbDrone.Core.Test.MediaFiles.DiskScanServiceTests
Subject.Scan(_movie);
Mocker.GetMock<IMakeImportDecision>()
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie), Times.Once());
.Verify(v => v.GetImportDecisions(It.Is<List<string>>(l => l.Count == 1), _movie, false), Times.Once());
}
}
}

View File

@@ -1,321 +0,0 @@
using System.Globalization;
using System.Threading;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
[TestFixture]
public class FormatAudioChannelsFixture : TestBase
{
[Test]
public void should_subtract_one_from_AudioChannels_as_total_channels_if_LFE_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 6,
AudioChannelPositions = null,
AudioChannelPositionsTextContainer = "Front: L C R, Side: L R, LFE"
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_use_AudioChannels_as_total_channels_if_LFE_not_in_AudioChannelPositionsText()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = null,
AudioChannelPositionsTextContainer = "Front: L R"
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_return_0_if_schema_revision_is_less_than_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = null,
AudioChannelPositionsTextContainer = null,
SchemaRevision = 2
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(0);
}
[Test]
public void should_use_AudioChannels_if_schema_revision_is_3_and_other_properties_are_null()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = null,
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_use_AudioChannels_if_schema_revision_is_3_and_AudioChannelPositions_is_0()
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "FLAC",
AudioChannelsContainer = 6,
AudioChannelPositions = "0/0/0",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_sum_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "2/0/0",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_AudioChannelPositions_including_decimal()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "3/2/0.1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_format_8_channel_object_based_as_71_if_dtsx()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 8,
AudioChannelsStream = 0,
AudioFormat = "DTS",
AudioAdditionalFeatures = "XLL X",
AudioChannelPositions = "Object Based",
AudioChannelPositionsTextContainer = "Object Based",
AudioChannelPositionsTextStream = "Object Based",
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_format_8_channel_blank_as_71_if_dtsx()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 8,
AudioChannelsStream = 0,
AudioFormat = "DTS",
AudioAdditionalFeatures = "XLL X",
AudioChannelPositions = "",
AudioChannelPositionsTextContainer = "",
AudioChannelPositionsTextStream = "Object Based",
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_format_6_channel_zero_as_51_if_flac()
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "FLAC",
AudioChannelsContainer = 6,
AudioChannelPositions = "0/0/0",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_ignore_culture_on_channel_summary()
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "3/2/0.1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_handle_AudioChannelPositions_three_digits()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "3/2/0.2.1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_cleanup_extraneous_text_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "Object Based / 3/2/2.1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_skip_empty_groups_in_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = " / 2/0/0.0",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2);
}
[Test]
public void should_sum_first_series_of_numbers_from_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "3/2/2.1 / 3/2/2.1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_sum_first_series_of_numbers_from_AudioChannelPositions_with_three_digits()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "3/2/0.2.1 / 3/2/0.1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_sum_dual_mono_representation_AudioChannelPositions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "1+1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(2.0m);
}
[Test]
public void should_use_AudioChannelPositionText_when_AudioChannelChannelPosition_is_invalid()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 6,
AudioChannelPositions = "15 objects",
AudioChannelPositionsTextContainer = "15 objects / Front: L C R, Side: L R, LFE",
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_remove_atmos_objects_from_AudioChannelPostions()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 2,
AudioChannelPositions = "15 objects / 3/2.1",
AudioChannelPositionsTextContainer = null,
SchemaRevision = 3
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(5.1m);
}
[Test]
public void should_use_audio_stream_text_when_exists()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 6,
AudioChannelsStream = 8,
AudioChannelPositions = null,
AudioChannelPositionsTextContainer = null,
AudioChannelPositionsTextStream = "Front: L C R, Side: L R, Back: L R, LFE",
SchemaRevision = 6
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(7.1m);
}
[Test]
public void should_use_audio_stream_channels_when_exists()
{
var mediaInfoModel = new MediaInfoModel
{
AudioChannelsContainer = 6,
AudioChannelsStream = 8,
AudioChannelPositions = null,
AudioChannelPositionsTextContainer = null,
AudioChannelPositionsTextStream = null,
SchemaRevision = 6
};
MediaInfoFormatter.FormatAudioChannels(mediaInfoModel).Should().Be(8m);
}
}
}

View File

@@ -10,45 +10,31 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
private static string sceneName = "My.Series.S01E01-Sonarr";
[TestCase("AC-3", "AC3")]
[TestCase("E-AC-3", "EAC3")]
[TestCase("MPEG Audio", "MPEG Audio")]
[TestCase("DTS", "DTS")]
public void should_format_audio_format_legacy(string audioFormat, string expectedFormat)
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = audioFormat
};
[TestCase("mp2, , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")]
[TestCase("vorbis, , ", "DB Super HDTV", "Vorbis")]
[TestCase("pcm_s16le, , ", "DW DVDRip XviD-idTV, ", "PCM")]
[TestCase("truehd, , ", "", "TrueHD")]
[TestCase("truehd, , ", "TrueHD", "TrueHD")]
[TestCase("truehd, thd+, ", "Atmos", "TrueHD Atmos")]
[TestCase("truehd, thd+, ", "TrueHD.Atmos.7.1", "TrueHD Atmos")]
[TestCase("truehd, thd+, ", "", "TrueHD Atmos")]
[TestCase("wmav1, , ", "Droned.wmv", "WMA")]
[TestCase("wmav2, , ", "B.N.S04E18.720p.WEB-DL", "WMA")]
[TestCase("opus, , ", "Roadkill Ep3x11 - YouTube.webm", "Opus")]
[TestCase("mp3, , ", "climbing.mp4", "MP3")]
[TestCase("dts, , DTS-HD MA", "DTS-HD.MA", "DTS-HD MA")]
[TestCase("dts, , DTS:X", "DTS-X", "DTS-X")]
[TestCase("dts, , DTS-HD MA", "DTS-HD.MA", "DTS-HD MA")]
[TestCase("dts, , DTS-ES", "DTS-ES", "DTS-ES")]
[TestCase("dts, , DTS-ES", "DTS", "DTS-ES")]
[TestCase("dts, , DTS-HD HRA", "DTSHD-HRA", "DTS-HD HRA")]
[TestCase("dts, , DTS", "DTS", "DTS")]
[TestCase("eac3, ec+3, ", "EAC3.Atmos", "EAC3 Atmos")]
[TestCase("eac3, , ", "DDP5.1", "EAC3")]
[TestCase("ac3, , ", "DD5.1", "AC3")]
[TestCase("adpcm_ima_qt, , ", "Custom?", "PCM")]
[TestCase("adpcm_ms, , ", "Custom", "PCM")]
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
}
[TestCase("MPEG Audio, A_MPEG/L2, , , ", "droned.s01e03.swedish.720p.hdtv.x264-prince", "MP2")]
[TestCase("Vorbis, A_VORBIS, , Xiph.Org libVorbis I 20101101 (Schaufenugget), ", "DB Super HDTV", "Vorbis")]
[TestCase("PCM, 1, , , ", "DW DVDRip XviD-idTV, ", "PCM")] // Dubbed most likely
[TestCase("TrueHD, A_TRUEHD, , , ", "", "TrueHD")]
[TestCase("MLP FBA, A_TRUEHD, , , ", "TrueHD", "TrueHD")]
[TestCase("MLP FBA, A_TRUEHD, , , 16-ch", "Atmos", "TrueHD Atmos")]
[TestCase("Atmos / TrueHD, A_TRUEHD, , , ", "TrueHD.Atmos.7.1", "TrueHD Atmos")]
[TestCase("Atmos / TrueHD / AC-3, 131, , , ", "", "TrueHD Atmos")]
[TestCase("WMA, 161, , , ", "Droned.wmv", "WMA")]
[TestCase("WMA, 162, Pro, , ", "B.N.S04E18.720p.WEB-DL", "WMA")]
[TestCase("Opus, A_OPUS, , , ", "Roadkill Ep3x11 - YouTube.webm", "Opus")]
[TestCase("mp3 , 0, , , ", "climbing.mp4", "MP3")]
[TestCase("DTS, A_DTS, , , XLL", "DTS-HD.MA", "DTS-HD MA")]
[TestCase("DTS, A_DTS, , , XLL X", "DTS-X", "DTS-X")]
[TestCase("DTS, A_DTS, , , ES XLL", "DTS-HD.MA", "DTS-HD MA")]
[TestCase("DTS, A_DTS, , , ES", "DTS-ES", "DTS-ES")]
[TestCase("DTS, A_DTS, , , ES XXCH", "DTS", "DTS-ES")]
[TestCase("DTS, A_DTS, , , XBR", "DTSHD-HRA", "DTS-HD HRA")]
[TestCase("DTS, A_DTS, , , DTS", "DTS", "DTS")]
[TestCase("E-AC-3, A_EAC3, , , JOC", "EAC3.Atmos", "EAC3 Atmos")]
[TestCase("E-AC-3, A_EAC3, , , ", "DDP5.1", "EAC3")]
[TestCase("AC-3, A_AC3, , , ", "DD5.1", "AC3")]
[TestCase("A_QUICKTIME, A_QUICKTIME, , , ", "", "")]
[TestCase("ADPCM, 2, , , ", "Custom?", "PCM")]
[TestCase("ADPCM, ima4, , , ", "Custom", "PCM")]
public void should_format_audio_format(string audioFormatPack, string sceneName, string expectedFormat)
{
var split = audioFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
@@ -56,26 +42,12 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
{
AudioFormat = split[0],
AudioCodecID = split[1],
AudioProfile = split[2],
AudioCodecLibrary = split[3],
AudioAdditionalFeatures = split[4]
AudioProfile = split[2]
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
}
[Test]
public void should_return_MP3_for_MPEG_Audio_with_Layer_3_for_the_profile()
{
var mediaInfoModel = new MediaInfoModel
{
AudioFormat = "MPEG Audio",
AudioProfile = "Layer 3"
};
MediaInfoFormatter.FormatAudioCodec(mediaInfoModel, sceneName).Should().Be("MP3");
}
[Test]
public void should_return_AudioFormat_by_default()
{

View File

@@ -8,94 +8,55 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestFixture]
public class FormatVideoCodecFixture : TestBase
{
[TestCase("AVC", null, "x264")]
[TestCase("AVC", "source.title.x264.720p-Sonarr", "x264")]
[TestCase("AVC", "source.title.h264.720p-Sonarr", "h264")]
[TestCase("V_MPEGH/ISO/HEVC", null, "x265")]
[TestCase("V_MPEGH/ISO/HEVC", "source.title.x265.720p-Sonarr", "x265")]
[TestCase("V_MPEGH/ISO/HEVC", "source.title.h265.720p-Sonarr", "h265")]
[TestCase("MPEG-2 Video", null, "MPEG2")]
public void should_format_video_codec_with_source_title_legacy(string videoCodec, string sceneName, string expectedFormat)
{
var mediaInfoModel = new MediaInfoModel
{
VideoCodec = videoCodec
};
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
}
[TestCase("MPEG Video, 2, Main@High, ", "Droned.S01E02.1080i.HDTV.DD5.1.MPEG2-NTb", "MPEG2")]
[TestCase("MPEG Video, V_MPEG2, Main@High, ", "", "MPEG2")]
[TestCase("MPEG Video, , , ", "The.Simpsons.S13E04.INTERNAL-ANiVCD.mpg", "MPEG")]
[TestCase("VC-1, WVC1, Advanced@L4, ", "B.N.S04E18.720p.WEB-DL", "VC1")]
[TestCase("VC-1, V_MS/VFW/FOURCC / WVC1, Advanced@L3, ", "", "VC1")]
[TestCase("VC-1, WMV3, MP@LL, ", "It's Always Sunny S07E13 The Gang's RevengeHDTV.XviD-2HD.avi", "VC1")]
[TestCase("V.MPEG4/ISO/AVC, V.MPEG4/ISO/AVC, , ", "pd.2015.S03E08.720p.iP.WEBRip.AAC2.0.H264-BTW", "h264")]
[TestCase("AVC / AVC, V_MPEG4/ISO/AVC, High@L4, ", "Resistance.2019.S01E03.1080p.RTE.WEB-DL.AAC2.0.x264-RTN", "x264")]
[TestCase("WMV1, WMV1, , ", "Droned.wmv", "WMV")]
[TestCase("WMV2, WMV2, , ", "Droned.wmv", "WMV")]
[TestCase("xvid, xvid, , ", "", "XviD")]
[TestCase("div3, div3, , ", "spsm.dvdrip.divx.avi'.", "DivX")]
[TestCase("VP6, 4, , ", "Top Gear - S12E01 - Lorries - SD TV.flv", "VP6")]
[TestCase("VP7, VP70, General, ", "Sweet Seymour.avi", "VP7")]
[TestCase("VP8, V_VP8, , ", "Dick.mkv", "VP8")]
[TestCase("VP9, V_VP9, , ", "Roadkill Ep3x11 - YouTube.webm", "VP9")]
[TestCase("x264, x264, , ", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")]
[TestCase("V_MPEGH/ISO/HEVC, V_MPEGH/ISO/HEVC, , ", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")]
[TestCase("MPEG-4 Visual, 20, Simple@L1, Lavc52.29.0", "Will.And.Grace.S08E14.WS.DVDrip.XviD.I.Love.L.Gay-Obfuscated", "XviD")]
[TestCase("MPEG-4 Visual, 20, Advanced Simple@L5, XviD0046", "", "XviD")]
[TestCase("MPEG-4 Visual, 20, , ", "", "")]
[TestCase("MPEG-4 Visual, mp4v-20, Simple@L1, Lavc57.48.101", "", "")]
[TestCase("mp4v, mp4v, , ", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")]
[TestCase("V_QUICKTIME, V_QUICKTIME, , ", "Custom", "")]
[TestCase("MPEG-4 Visual, FMP4, , ", "", "")]
[TestCase("MPEG-4 Visual, MP42, , ", "", "")]
[TestCase("mp43, V_MS/VFW/FOURCC / mp43, , ", "Bubble.Guppies.S01E13.480p.WEB-DL.H.264-BTN-Custom", "")]
[TestCase("mpeg2video, ", "Droned.S01E02.1080i.HDTV.DD5.1.MPEG2-NTb", "MPEG2")]
[TestCase("mpeg2video, ", "", "MPEG2")]
[TestCase("mpeg1video, ", "The.Simpsons.S13E04.INTERNAL-ANiVCD.mpg", "MPEG")]
[TestCase("vc1, WVC1", "B.N.S04E18.720p.WEB-DL", "VC1")]
[TestCase("vc1, V_MS/VFW/FOURCC/WVC1", "", "VC1")]
[TestCase("vc1, WMV3", "It's Always Sunny S07E13 The Gang's RevengeHDTV.XviD-2HD.avi", "VC1")]
[TestCase("h264, V.MPEG4/ISO/AVC", "pd.2015.S03E08.720p.iP.WEBRip.AAC2.0.H264-BTW", "h264")]
[TestCase("h264, V_MPEG4/ISO/AVC", "Resistance.2019.S01E03.1080p.RTE.WEB-DL.AAC2.0.x264-RTN", "x264")]
[TestCase("wmv1, WMV1", "Droned.wmv", "WMV")]
[TestCase("wmv2, WMV2", "Droned.wmv", "WMV")]
[TestCase("mpeg4, XVID", "", "XviD")]
[TestCase("mpeg4, DIV3", "spsm.dvdrip.divx.avi'.", "DivX")]
[TestCase("vp6, 4", "Top Gear - S12E01 - Lorries - SD TV.flv", "VP6")]
[TestCase("vp7, VP70", "Sweet Seymour.avi", "VP7")]
[TestCase("vp8, V_VP8", "Dick.mkv", "VP8")]
[TestCase("vp9, V_VP9", "Roadkill Ep3x11 - YouTube.webm", "VP9")]
[TestCase("h264, x264", "Ghost Advent - S04E05 - Stanley Hotel SDTV.avi", "x264")]
[TestCase("hevc, V_MPEGH/ISO/HEVC", "The BBT S11E12 The Matrimonial Metric 1080p 10bit AMZN WEB-DL", "h265")]
[TestCase("mpeg4, mp4v-20", "", "")]
[TestCase("mpeg4, XVID", "American.Chopper.S06E07.Mountain.Creek.Bike.DSR.XviD-KRS", "XviD")]
[TestCase("rpza, V_QUICKTIME", "Custom", "")]
[TestCase("mpeg4, FMP4", "", "")]
[TestCase("mpeg4, MP42", "", "")]
[TestCase("mpeg4, mp43", "Bubble.Guppies.S01E13.480p.WEB-DL.H.264-BTN-Custom", "")]
public void should_format_video_format(string videoFormatPack, string sceneName, string expectedFormat)
{
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
var mediaInfoModel = new MediaInfoModel
{
VideoFormat = split[0],
VideoCodecID = split[1],
VideoProfile = split[2],
VideoCodecLibrary = split[3]
VideoCodecID = split[1]
};
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
}
[TestCase("AVC, AVC, , x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag
[TestCase("HEVC, HEVC, , x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag
[TestCase("AVC, AVC, , ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown
[TestCase("HEVC, HEVC, , ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown
[TestCase("AVC, AVC, , ", "Some.Video.S01E01", "h264")] // Default value
[TestCase("HEVC, HEVC, , ", "Some.Video.S01E01", "h265")] // Default value
[TestCase("h264, x264", "Some.Video.S01E01.h264", "x264")] // Force mediainfo tag
[TestCase("hevc, x265", "Some.Video.S01E01.h265", "x265")] // Force mediainfo tag
[TestCase("h264, ", "Some.Video.S01E01.x264", "x264")] // Not seen in practice, but honor tag if otherwise unknown
[TestCase("hevc, ", "Some.Video.S01E01.x265", "x265")] // Not seen in practice, but honor tag if otherwise unknown
[TestCase("h264, ", "Some.Video.S01E01", "h264")] // Default value
[TestCase("hevc, ", "Some.Video.S01E01", "h265")] // Default value
public void should_format_video_format_fallbacks(string videoFormatPack, string sceneName, string expectedFormat)
{
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
var mediaInfoModel = new MediaInfoModel
{
VideoFormat = split[0],
VideoCodecID = split[1],
VideoProfile = split[2],
VideoCodecLibrary = split[3]
};
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);
}
[TestCase("MPEG-4 Visual, 20, , Intel(R) MPEG-4 encoder based on Intel(R) IPP 6.1 build 137.20[6.1.137.763]", "", "")]
public void should_warn_on_unknown_video_format(string videoFormatPack, string sceneName, string expectedFormat)
{
var split = videoFormatPack.Split(new string[] { ", " }, System.StringSplitOptions.None);
var mediaInfoModel = new MediaInfoModel
{
VideoFormat = split[0],
VideoCodecID = split[1],
VideoProfile = split[2],
VideoCodecLibrary = split[3]
VideoCodecID = split[1]
};
MediaInfoFormatter.FormatVideoCodec(mediaInfoModel, sceneName).Should().Be(expectedFormat);

View File

@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Test.Common;
@@ -8,28 +8,18 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo.MediaInfoFormatterTests
[TestFixture]
public class FormatVideoDynamicRangeFixture : TestBase
{
[TestCase(8, "", "", "", "", "")]
[TestCase(8, "BT.601 NTSC", "BT.709", "", "", "")]
[TestCase(10, "BT.2020", "PQ", "", "", "HDR")]
[TestCase(8, "BT.2020", "PQ", "", "", "")]
[TestCase(10, "BT.601 NTSC", "PQ", "", "", "")]
[TestCase(10, "BT.2020", "BT.709", "", "", "")]
[TestCase(10, "BT.2020", "HLG", "", "", "HDR")]
[TestCase(10, "", "", "Dolby Vision", "", "HDR")]
[TestCase(10, "", "", "SMPTE ST 2086", "HDR10", "HDR")]
[TestCase(8, "", "", "Dolby Vision", "", "HDR")]
[TestCase(8, "", "", "SMPTE ST 2086", "HDR10", "HDR")]
[TestCase(10, "BT.2020", "PQ", "Dolby Vision / SMPTE ST 2086", "Blu-ray / HDR10", "HDR")]
public void should_format_video_dynamic_range(int bitDepth, string colourPrimaries, string transferCharacteristics, string hdrFormat, string hdrFormatCompatibility, string expectedVideoDynamicRange)
[TestCase(HdrFormat.None, "")]
[TestCase(HdrFormat.Hlg10, "HDR")]
[TestCase(HdrFormat.Pq10, "HDR")]
[TestCase(HdrFormat.Hdr10, "HDR")]
[TestCase(HdrFormat.Hdr10Plus, "HDR")]
[TestCase(HdrFormat.DolbyVision, "HDR")]
public void should_format_video_dynamic_range(HdrFormat format, string expectedVideoDynamicRange)
{
var mediaInfo = new MediaInfoModel
{
VideoBitDepth = bitDepth,
VideoColourPrimaries = colourPrimaries,
VideoTransferCharacteristics = transferCharacteristics,
VideoHdrFormat = hdrFormat,
VideoHdrFormatCompatibility = hdrFormatCompatibility,
SchemaRevision = 7
VideoHdrFormat = format,
SchemaRevision = 8
};
MediaInfoFormatter.FormatVideoDynamicRange(mediaInfo).Should().Be(expectedVideoDynamicRange);

View File

@@ -1,8 +1,13 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using FFMpegCore;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.MediaInfo;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common.Categories;
@@ -40,33 +45,26 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
var info = Subject.GetMediaInfo(path);
info.VideoCodec.Should().BeNull();
info.VideoFormat.Should().Be("AVC");
info.VideoFormat.Should().Be("h264");
info.VideoCodecID.Should().Be("avc1");
info.VideoProfile.Should().Be("Baseline@L2.1");
info.VideoCodecLibrary.Should().Be("");
info.AudioFormat.Should().Be("AAC");
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
info.AudioProfile.Should().BeOneOf("", "LC");
info.AudioCodecLibrary.Should().Be("");
info.AudioBitrate.Should().Be(128000);
info.AudioChannelsContainer.Should().Be(2);
info.AudioChannelsStream.Should().Be(0);
info.AudioChannelPositionsTextContainer.Should().Be("Front: L R");
info.AudioChannelPositionsTextStream.Should().Be("");
info.AudioLanguages.Should().Be("English");
info.VideoProfile.Should().Be("Constrained Baseline");
info.AudioFormat.Should().Be("aac");
info.AudioCodecID.Should().Be("mp4a");
info.AudioProfile.Should().Be("LC");
info.AudioBitrate.Should().Be(125488);
info.AudioChannels.Should().Be(2);
info.AudioChannelPositions.Should().Be("stereo");
info.AudioLanguages.Should().BeEquivalentTo("eng");
info.Height.Should().Be(320);
info.RunTime.Seconds.Should().Be(10);
info.ScanType.Should().Be("Progressive");
info.Subtitles.Should().Be("");
info.VideoBitrate.Should().Be(193329);
info.Subtitles.Should().BeEmpty();
info.VideoBitrate.Should().Be(193328);
info.VideoFps.Should().Be(24);
info.Width.Should().Be(480);
info.VideoColourPrimaries.Should().Be("BT.601 NTSC");
info.VideoTransferCharacteristics.Should().Be("BT.709");
info.AudioAdditionalFeatures.Should().BeOneOf("", "LC");
info.VideoHdrFormat.Should().BeEmpty();
info.VideoHdrFormatCompatibility.Should().BeEmpty();
info.VideoBitDepth.Should().Be(8);
info.VideoColourPrimaries.Should().Be("smpte170m");
info.VideoTransferCharacteristics.Should().Be("bt709");
}
[Test]
@@ -83,45 +81,48 @@ namespace NzbDrone.Core.Test.MediaFiles.MediaInfo
var info = Subject.GetMediaInfo(path);
info.VideoCodec.Should().BeNull();
info.VideoFormat.Should().Be("AVC");
info.VideoFormat.Should().Be("h264");
info.VideoCodecID.Should().Be("avc1");
info.VideoProfile.Should().Be("Baseline@L2.1");
info.VideoCodecLibrary.Should().Be("");
info.AudioFormat.Should().Be("AAC");
info.AudioCodecID.Should().BeOneOf("40", "mp4a-40-2");
info.AudioProfile.Should().BeOneOf("", "LC");
info.AudioCodecLibrary.Should().Be("");
info.AudioBitrate.Should().Be(128000);
info.AudioChannelsContainer.Should().Be(2);
info.AudioChannelsStream.Should().Be(0);
info.AudioChannelPositionsTextContainer.Should().Be("Front: L R");
info.AudioChannelPositionsTextStream.Should().Be("");
info.AudioLanguages.Should().Be("English");
info.VideoProfile.Should().Be("Constrained Baseline");
info.AudioFormat.Should().Be("aac");
info.AudioCodecID.Should().Be("mp4a");
info.AudioProfile.Should().Be("LC");
info.AudioBitrate.Should().Be(125488);
info.AudioChannels.Should().Be(2);
info.AudioChannelPositions.Should().Be("stereo");
info.AudioLanguages.Should().BeEquivalentTo("eng");
info.Height.Should().Be(320);
info.RunTime.Seconds.Should().Be(10);
info.ScanType.Should().Be("Progressive");
info.Subtitles.Should().Be("");
info.VideoBitrate.Should().Be(193329);
info.Subtitles.Should().BeEmpty();
info.VideoBitrate.Should().Be(193328);
info.VideoFps.Should().Be(24);
info.Width.Should().Be(480);
info.VideoColourPrimaries.Should().Be("BT.601 NTSC");
info.VideoTransferCharacteristics.Should().Be("BT.709");
info.AudioAdditionalFeatures.Should().BeOneOf("", "LC");
info.VideoHdrFormat.Should().BeEmpty();
info.VideoHdrFormatCompatibility.Should().BeEmpty();
info.VideoColourPrimaries.Should().Be("smpte170m");
info.VideoTransferCharacteristics.Should().Be("bt709");
}
[Test]
public void should_dispose_file_after_scanning_mediainfo()
[TestCase(8, "", "", "", HdrFormat.None)]
[TestCase(10, "", "", "", HdrFormat.None)]
[TestCase(10, "bt709", "bt709", "", HdrFormat.None)]
[TestCase(8, "bt2020", "smpte2084", "", HdrFormat.None)]
[TestCase(10, "bt2020", "bt2020-10", "", HdrFormat.Hlg10)]
[TestCase(10, "bt2020", "arib-std-b67", "", HdrFormat.Hlg10)]
[TestCase(10, "bt2020", "smpte2084", "", HdrFormat.Pq10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.SideData", HdrFormat.Pq10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.MasteringDisplayMetadata", HdrFormat.Hdr10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.ContentLightLevelMetadata", HdrFormat.Hdr10)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.HdrDynamicMetadataSpmte2094", HdrFormat.Hdr10Plus)]
[TestCase(10, "bt2020", "smpte2084", "FFMpegCore.DoviConfigurationRecordSideData", HdrFormat.DolbyVision)]
public void should_detect_hdr_correctly(int bitDepth, string colourPrimaries, string transferFunction, string sideDataTypes, HdrFormat expected)
{
var path = Path.Combine(TestContext.CurrentContext.TestDirectory, "Files", "Media", "H264_sample.mp4");
var assembly = Assembly.GetAssembly(typeof(FFProbe));
var types = sideDataTypes.Split(",").Select(x => x.Trim()).ToList();
var sideData = types.Where(x => x.IsNotNullOrWhiteSpace()).Select(x => assembly.CreateInstance(x)).Cast<SideData>().ToList();
var info = Subject.GetMediaInfo(path);
var result = VideoFileInfoReader.GetHdrFormat(bitDepth, colourPrimaries, transferFunction, sideData);
var stream = new FileStream(path, FileMode.Open, FileAccess.Write);
stream.Close();
result.Should().Be(expected);
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
@@ -25,7 +26,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
public void should_return_language_for_single_known_language()
{
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
.With(m => m.AudioLanguages = "English")
.With(m => m.AudioLanguages = new List<string> { "eng" })
.Build();
var localMovie = Builder<LocalMovie>.CreateNew()
@@ -42,7 +43,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
public void should_only_return_one_when_language_duplicated()
{
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
.With(m => m.AudioLanguages = "English / English")
.With(m => m.AudioLanguages = new List<string> { "eng", "eng" })
.Build();
var localMovie = Builder<LocalMovie>.CreateNew()
@@ -59,7 +60,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
public void should_return_null_if_all_unknown()
{
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
.With(m => m.AudioLanguages = "Pirate / Pirate")
.With(m => m.AudioLanguages = new List<string> { "pirate", "pirate" })
.Build();
var localMovie = Builder<LocalMovie>.CreateNew()
@@ -75,7 +76,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
public void should_return_known_languages_only()
{
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
.With(m => m.AudioLanguages = "English / Pirate")
.With(m => m.AudioLanguages = new List<string> { "eng", "pirate" })
.Build();
var localMovie = Builder<LocalMovie>.CreateNew()
@@ -92,7 +93,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augm
public void should_return_multiple_known_languages()
{
var mediaInfo = Builder<MediaInfoModel>.CreateNew()
.With(m => m.AudioLanguages = "English / German")
.With(m => m.AudioLanguages = new List<string> { "eng", "ger" })
.Build();
var localMovie = Builder<LocalMovie>.CreateNew()

View File

@@ -9,7 +9,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MediaFiles.EpisodeImport.Aggregation.Aggregators.Augmenters.Quality
namespace NzbDrone.Core.Test.MediaFiles.MovieImport.Aggregation.Aggregators.Augmenters.Quality
{
[TestFixture]
public class AugmentQualityFromReleaseNameFixture : CoreTest<AugmentQualityFromReleaseName>

View File

@@ -20,7 +20,7 @@ using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.MediaFiles
namespace NzbDrone.Core.Test.MediaFiles.MovieImport
{
[TestFixture]
@@ -76,6 +76,13 @@ namespace NzbDrone.Core.Test.MediaFiles
_approvedDecisions.ForEach(a => a.LocalMovie.Path = Path.Combine(_downloadClientItem.OutputPath.ToString(), Path.GetFileName(a.LocalMovie.Path)));
}
private void GivenExistingFileOnDisk()
{
Mocker.GetMock<IMediaFileService>()
.Setup(s => s.GetFilesWithRelativePath(It.IsAny<int>(), It.IsAny<string>()))
.Returns(new List<MovieFile>());
}
[Test]
public void should_not_import_any_if_there_are_no_approved_decisions()
{
@@ -87,12 +94,16 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_import_each_approved()
{
GivenExistingFileOnDisk();
Subject.Import(_approvedDecisions, false).Should().HaveCount(1);
}
[Test]
public void should_only_import_approved()
{
GivenExistingFileOnDisk();
var all = new List<ImportDecision>();
all.AddRange(_rejectedDecisions);
all.AddRange(_approvedDecisions);
@@ -104,8 +115,10 @@ namespace NzbDrone.Core.Test.MediaFiles
}
[Test]
public void should_only_import_each_episode_once()
public void should_only_import_each_movie_once()
{
GivenExistingFileOnDisk();
var all = new List<ImportDecision>();
all.AddRange(_approvedDecisions);
all.Add(new ImportDecision(_approvedDecisions.First().LocalMovie));
@@ -137,6 +150,8 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_not_move_existing_files()
{
GivenExistingFileOnDisk();
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, false);
Mocker.GetMock<IUpgradeMediaFiles>()
@@ -225,6 +240,8 @@ namespace NzbDrone.Core.Test.MediaFiles
[Test]
public void should_import_larger_files_first()
{
GivenExistingFileOnDisk();
var fileDecision = _approvedDecisions.First();
fileDecision.LocalMovie.Size = 1.Gigabytes();
@@ -321,10 +338,10 @@ namespace NzbDrone.Core.Test.MediaFiles
{
var name = "Transformers.2007.720p.BluRay.x264-Radarr";
var outputPath = Path.Combine(@"C:\Test\Unsorted\movies\".AsOsAgnostic(), name);
var localEpisode = _approvedDecisions.First().LocalMovie;
var localMovie = _approvedDecisions.First().LocalMovie;
localEpisode.FolderMovieInfo = new ParsedMovieInfo { OriginalTitle = name };
localEpisode.Path = Path.Combine(outputPath, "subfolder", name + ".mkv");
localMovie.FolderMovieInfo = new ParsedMovieInfo { OriginalTitle = name };
localMovie.Path = Path.Combine(outputPath, "subfolder", name + ".mkv");
Subject.Import(new List<ImportDecision> { _approvedDecisions.First() }, true, null);

View File

@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Test.MediaFiles.MovieImport
_fileInfo = new ParsedMovieInfo
{
MovieTitle = "The Office",
MovieTitles = new List<string> { "The Office" },
Year = 2018,
Quality = _quality
};

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.AlternativeTitles;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.MovieTests.MovieServiceTests
{
[TestFixture]
public class FindByTitleFixture : CoreTest<MovieService>
{
private List<Movie> _candidates;
[SetUp]
public void Setup()
{
_candidates = Builder<Movie>.CreateListOfSize(3)
.TheFirst(1)
.With(x => x.CleanTitle = "batman")
.With(x => x.Year = 2000)
.TheNext(1)
.With(x => x.CleanTitle = "batman")
.With(x => x.Year = 1999)
.TheRest()
.With(x => x.CleanTitle = "darkknight")
.With(x => x.Year = 2008)
.With(x => x.AlternativeTitles = new List<AlternativeTitle>
{
new AlternativeTitle
{
CleanTitle = "batman"
}
})
.Build()
.ToList();
}
[Test]
public void should_find_by_title_year()
{
var movie = Subject.FindByTitle(new List<string> { "batman" }, 2000, new List<string>(), _candidates);
movie.Should().NotBeNull();
movie.Year.Should().Be(2000);
}
[Test]
public void should_find_candidates_by_alt_titles()
{
var movie = Subject.FindByTitle(new List<string> { "batman" }, 2008, new List<string>(), _candidates);
movie.Should().NotBeNull();
movie.Year.Should().Be(2008);
}
}
}

View File

@@ -65,7 +65,7 @@ namespace NzbDrone.Core.Test.NotificationTests
[Test]
public void should_add_collection_movie_if_valid_mediainfo()
{
GiventValidMediaInfo(Quality.Bluray1080p, "3/2/0.1", "DTS", "Interlaced");
GiventValidMediaInfo(Quality.Bluray1080p, "5.1", "DTS", "Progressive");
Subject.AddMovieToCollection(_traktSettings, _downloadMessage.Movie, _downloadMessage.MovieFile);
@@ -73,7 +73,7 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "5.1" &&
t.Movies.First().Resolution == "hd_1080i" &&
t.Movies.First().Resolution == "hd_1080p" &&
t.Movies.First().MediaType == "bluray"),
It.IsAny<string>()), Times.Once());
}
@@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.NotificationTests
[Test]
public void should_format_audio_channels_to_one_decimal_when_adding_collection_movie()
{
GiventValidMediaInfo(Quality.Bluray1080p, "2/0/0", "DTS", "Interlaced");
GiventValidMediaInfo(Quality.Bluray1080p, "2.0", "DTS", "Progressive");
Subject.AddMovieToCollection(_traktSettings, _downloadMessage.Movie, _downloadMessage.MovieFile);
@@ -89,7 +89,7 @@ namespace NzbDrone.Core.Test.NotificationTests
.Verify(v => v.AddToCollection(It.Is<TraktCollectMoviesResource>(t =>
t.Movies.First().Audio == "dts" &&
t.Movies.First().AudioChannels == "2.0" &&
t.Movies.First().Resolution == "hd_1080i" &&
t.Movies.First().Resolution == "hd_1080p" &&
t.Movies.First().MediaType == "bluray"),
It.IsAny<string>()), Times.Once());
}

View File

@@ -19,6 +19,7 @@ using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
{
[Platform(Exclude = "Win")]
[TestFixture]
public class FileNameBuilderFixture : CoreTest<FileNameBuilder>
@@ -368,35 +369,44 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_movieFile.MediaInfo = new MediaInfoModel()
{
VideoFormat = "AVC",
AudioFormat = "DTS",
AudioLanguages = "English/Spanish",
Subtitles = "English/Spanish/Italian"
VideoFormat = "h264",
AudioFormat = "dts",
AudioLanguages = new List<string> { "eng", "spa" },
Subtitles = new List<string> { "eng", "spa", "ita" }
};
Subject.BuildFileName(_movie, _movieFile)
.Should().Be("South.Park.H264.DTS[EN+ES].[EN+ES+IT]");
}
[TestCase("Norwegian Bokmal", "NB")]
[TestCase("Swedis", "SV")]
[TestCase("Chinese", "ZH")]
[TestCase("nob", "NB")]
[TestCase("swe", "SV")]
[TestCase("zho", "ZH")]
[TestCase("chi", "ZH")]
[TestCase("fre", "FR")]
[TestCase("rum", "RO")]
[TestCase("per", "FA")]
[TestCase("ger", "DE")]
[TestCase("cze", "CS")]
[TestCase("ice", "IS")]
[TestCase("dut", "NL")]
[TestCase("nor", "NO")]
public void should_format_languagecodes_properly(string language, string code)
{
_namingConfig.StandardMovieFormat = "{Movie.Title}.{MEDIAINFO.FULL}";
_movieFile.MediaInfo = new MediaInfoModel()
{
VideoCodec = "AVC",
AudioFormat = "DTS",
AudioChannelsContainer = 6,
AudioLanguages = "English",
Subtitles = language,
VideoFormat = "h264",
AudioFormat = "dts",
AudioChannels = 6,
AudioLanguages = new List<string> { "eng" },
Subtitles = new List<string> { language },
SchemaRevision = 3
};
Subject.BuildFileName(_movie, _movieFile)
.Should().Be($"South.Park.X264.DTS.[{code}]");
.Should().Be($"South.Park.H264.DTS.[{code}]");
}
[Test]
@@ -406,16 +416,17 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_movieFile.MediaInfo = new MediaInfoModel()
{
VideoFormat = "AVC",
AudioFormat = "DTS",
AudioLanguages = "English",
Subtitles = "English/Spanish/Italian"
VideoFormat = "h264",
AudioFormat = "dts",
AudioLanguages = new List<string> { "eng" },
Subtitles = new List<string> { "eng", "spa", "ita" }
};
Subject.BuildFileName(_movie, _movieFile)
.Should().Be("South.Park.H264.DTS.[EN+ES+IT]");
}
[Ignore("not currently supported")]
[Test]
public void should_format_mediainfo_3d_properly()
{
@@ -423,11 +434,11 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
_movieFile.MediaInfo = new MediaInfoModel()
{
VideoFormat = "AVC",
VideoFormat = "h264",
VideoMultiViewCount = 2,
AudioFormat = "DTS",
AudioLanguages = "English",
Subtitles = "English/Spanish/Italian"
AudioFormat = "dts",
AudioLanguages = new List<string> { "eng" },
Subtitles = new List<string> { "eng", "spa", "ita" }
};
Subject.BuildFileName(_movie, _movieFile)
@@ -631,8 +642,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be(releaseGroup);
}
[TestCase("English", "")]
[TestCase("English/German", "[EN+DE]")]
[TestCase("eng", "")]
[TestCase("eng/deu", "[EN+DE]")]
public void should_format_audio_languages(string audioLanguages, string expected)
{
_movieFile.ReleaseGroup = null;
@@ -645,8 +656,8 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be(expected);
}
[TestCase("English", "[EN]")]
[TestCase("English/German", "[EN+DE]")]
[TestCase("eng", "[EN]")]
[TestCase("eng/deu", "[EN+DE]")]
public void should_format_audio_languages_all(string audioLanguages, string expected)
{
_movieFile.ReleaseGroup = null;
@@ -659,19 +670,15 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
.Should().Be(expected);
}
[TestCase(8, "BT.601 NTSC", "BT.709", "South.Park")]
[TestCase(10, "BT.2020", "PQ", "South.Park.HDR")]
[TestCase(10, "BT.2020", "HLG", "South.Park.HDR")]
[TestCase(0, null, null, "South.Park")]
public void should_include_hdr_for_mediainfo_videodynamicrange_with_valid_properties(int bitDepth,
string colourPrimaries,
string transferCharacteristics,
string expectedName)
[TestCase(HdrFormat.None, "South.Park")]
[TestCase(HdrFormat.Hlg10, "South.Park.HDR")]
[TestCase(HdrFormat.Hdr10, "South.Park.HDR")]
public void should_include_hdr_for_mediainfo_videodynamicrange_with_valid_properties(HdrFormat hdrFormat, string expectedName)
{
_namingConfig.StandardMovieFormat =
"{Movie.Title}.{MediaInfo VideoDynamicRange}";
GivenMediaInfoModel(videoBitDepth: bitDepth, videoColourPrimaries: colourPrimaries, videoTransferCharacteristics: transferCharacteristics);
GivenMediaInfoModel(hdrFormat: hdrFormat);
Subject.BuildFileName(_movie, _movieFile)
.Should().Be(expectedName);
@@ -743,26 +750,24 @@ namespace NzbDrone.Core.Test.OrganizerTests.FileNameBuilderTests
Mocker.GetMock<IUpdateMediaInfo>().Verify(v => v.Update(_movieFile, _movie), Times.Never());
}
private void GivenMediaInfoModel(string videoCodec = "AVC",
string audioCodec = "DTS",
private void GivenMediaInfoModel(string videoCodec = "h264",
string audioCodec = "dts",
int audioChannels = 6,
int videoBitDepth = 8,
string videoColourPrimaries = "",
string videoTransferCharacteristics = "",
string audioLanguages = "English",
string subtitles = "English/Spanish/Italian",
HdrFormat hdrFormat = HdrFormat.None,
string audioLanguages = "eng",
string subtitles = "eng/spa/ita",
int schemaRevision = 5)
{
_movieFile.MediaInfo = new MediaInfoModel
{
VideoCodec = videoCodec,
VideoFormat = videoCodec,
AudioFormat = audioCodec,
AudioChannelsContainer = audioChannels,
AudioLanguages = audioLanguages,
Subtitles = subtitles,
AudioChannels = audioChannels,
AudioLanguages = audioLanguages.Split("/").ToList(),
Subtitles = subtitles.Split("/").ToList(),
VideoBitDepth = videoBitDepth,
VideoColourPrimaries = videoColourPrimaries,
VideoTransferCharacteristics = videoTransferCharacteristics,
VideoHdrFormat = hdrFormat,
SchemaRevision = schemaRevision
};
}

View File

@@ -95,7 +95,7 @@ namespace NzbDrone.Core.Test.ParserTests
public void should_properly_parse_hashed_releases(string path, string title, Quality quality, string releaseGroup)
{
var result = Parser.Parser.ParseMoviePath(path);
result.MovieTitle.Should().Be(title);
result.PrimaryMovieTitle.Should().Be(title);
result.Quality.Quality.Should().Be(quality);
result.ReleaseGroup.Should().Be(releaseGroup);
}

View File

@@ -61,12 +61,9 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("World.Movie.Z.2.EXTENDED.2013.German.DL.1080p.BluRay.AVC-XANOR", "World Movie Z 2")]
[TestCase("G.I.Movie.Movie.2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "G.I. Movie Movie")]
[TestCase("www.Torrenting.org - Movie.2008.720p.X264-DIMENSION", "Movie")]
//Dont think this will ever apply but adding it to keep the regex in line with upstream
[TestCase("Movie name: 2013.THEATRiCAL.COMPLETE.BLURAY-GLiMMER", "Movie name")]
public void should_parse_movie_title(string postTitle, string title)
{
Parser.Parser.ParseMovieTitle(postTitle).MovieTitle.Should().Be(title);
Parser.Parser.ParseMovieTitle(postTitle).PrimaryMovieTitle.Should().Be(title);
}
[TestCase("Movie.Aufbruch.nach.Pandora.Extended.2009.German.DTS.720p.BluRay.x264-SoW", "Movie Aufbruch nach Pandora", "Extended", 2009)]
@@ -102,16 +99,85 @@ namespace NzbDrone.Core.Test.ParserTests
ParsedMovieInfo movie = Parser.Parser.ParseMovieTitle(postTitle);
using (new AssertionScope())
{
movie.MovieTitle.Should().Be(title);
movie.PrimaryMovieTitle.Should().Be(title);
movie.Edition.Should().Be(edition);
movie.Year.Should().Be(year);
}
}
[TestCase("L'hypothèse.du.tableau.volé.AKA.The.Hypothesis.of.the.Stolen.Painting.1978.1080p.CINET.WEB-DL.AAC2.0.x264-Cinefeel.mkv",
new string[]
{
"L'hypothèse du tableau volé AKA The Hypothesis of the Stolen Painting",
"L'hypothèse du tableau volé",
"The Hypothesis of the Stolen Painting"
})]
[TestCase("Akahige.AKA.Red.Beard.1965.CD1.CRiTERiON.DVDRip.XviD-KG.avi",
new string[]
{
"Akahige AKA Red Beard",
"Akahige",
"Red Beard"
})]
[TestCase("Akasen.chitai.AKA.Street.of.Shame.1956.1080p.BluRay.x264.FLAC.1.0.mkv",
new string[]
{
"Akasen chitai AKA Street of Shame",
"Akasen chitai",
"Street of Shame"
})]
[TestCase("Time.Under.Fire.(aka.Beneath.the.Bermuda.Triangle).1997.DVDRip.x264.CG-Grzechsin.mkv",
new string[]
{
"Time Under Fire (aka Beneath the Bermuda Triangle)",
"Time Under Fire",
"Beneath the Bermuda Triangle"
})]
[TestCase("Nochnoy.prodavet. AKA.Graveyard.Shift.2005.DVDRip.x264-HANDJOB.mkv",
new string[]
{
"Nochnoy prodavet AKA Graveyard Shift",
"Nochnoy prodavet",
"Graveyard Shift"
})]
[TestCase("AKA.2002.DVDRip.x264-HANDJOB.mkv",
new string[]
{
"AKA"
})]
[TestCase("Unbreakable.2000.BluRay.1080p.DTS.x264.dxva-EuReKA.mkv",
new string[]
{
"Unbreakable"
})]
[TestCase("Aka Ana (2008).avi",
new string[]
{
"Aka Ana"
})]
[TestCase("Return to Return to Nuke 'em High aka Volume 2 (2018) 1080p.mp4",
new string[]
{
"Return to Return to Nuke 'em High aka Volume 2",
"Return to Return to Nuke 'em High",
"Volume 2"
})]
public void should_parse_movie_alternative_titles(string postTitle, string[] parsedTitles)
{
var movieInfo = Parser.Parser.ParseMovieTitle(postTitle, true);
movieInfo.MovieTitles.Count.Should().Be(parsedTitles.Length);
for (var i = 0; i < movieInfo.MovieTitles.Count; i += 1)
{
movieInfo.MovieTitles[i].Should().Be(parsedTitles[i]);
}
}
[TestCase("(1995) Movie Name", "Movie Name")]
public void should_parse_movie_folder_name(string postTitle, string title)
{
Parser.Parser.ParseMovieTitle(postTitle, true).MovieTitle.Should().Be(title);
Parser.Parser.ParseMovieTitle(postTitle, true).PrimaryMovieTitle.Should().Be(title);
}
[TestCase("1776.1979.EXTENDED.720p.BluRay.X264-AMIABLE", 1979)]

View File

@@ -1,4 +1,5 @@
using NUnit.Framework;
using System.Collections.Generic;
using NUnit.Framework;
using NzbDrone.Core.Parser.Augmenters;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
@@ -17,7 +18,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests.AugmentersTests
{
MovieInfo = new ParsedMovieInfo
{
MovieTitle = "A Movie",
MovieTitles = new List<string> { "A Movie" },
Year = 1998,
SimpleReleaseTitle = "A Movie Title 1998 Bluray 1080p",
Quality = new QualityModel(Quality.Bluray1080p)

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Movies;
@@ -28,7 +29,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.GetMovie(title);
Mocker.GetMock<IMovieService>()
.Verify(s => s.FindByTitle(Parser.Parser.ParseMovieTitle(title, false).MovieTitle, It.IsAny<int>(), null, null, null), Times.Once());
.Verify(s => s.FindByTitle(Parser.Parser.ParseMovieTitle(title, false).MovieTitles, It.IsAny<int>(), It.IsAny<List<string>>(), null), Times.Once());
}
/*[Test]

View File

@@ -45,69 +45,69 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
_parsedMovieInfo = new ParsedMovieInfo
{
MovieTitle = _movie.Title,
MovieTitles = new List<string> { _movie.Title },
Languages = new List<Language> { Language.English },
Year = _movie.Year,
};
_wrongYearInfo = new ParsedMovieInfo
{
MovieTitle = _movie.Title,
MovieTitles = new List<string> { _movie.Title },
Languages = new List<Language> { Language.English },
Year = 1900,
};
_wrongTitleInfo = new ParsedMovieInfo
{
MovieTitle = "Other Title",
MovieTitles = new List<string> { "Other Title" },
Languages = new List<Language> { Language.English },
Year = 2015
};
_alternativeTitleInfo = new ParsedMovieInfo
{
MovieTitle = _movie.AlternativeTitles.First().Title,
MovieTitles = new List<string> { _movie.AlternativeTitles.First().Title },
Languages = new List<Language> { Language.English },
Year = _movie.Year,
};
_translationTitleInfo = new ParsedMovieInfo
{
MovieTitle = _movie.Translations.First().Title,
MovieTitles = new List<string> { _movie.Translations.First().Title },
Languages = new List<Language> { Language.English },
Year = _movie.Year,
};
_romanTitleInfo = new ParsedMovieInfo
{
MovieTitle = "Fack Ju Göthe II",
MovieTitles = new List<string> { "Fack Ju Göthe II" },
Languages = new List<Language> { Language.English },
Year = _movie.Year,
};
_umlautInfo = new ParsedMovieInfo
{
MovieTitle = "Fack Ju Goethe 2",
MovieTitles = new List<string> { "Fack Ju Goethe 2" },
Languages = new List<Language> { Language.English },
Year = _movie.Year
};
_umlautAltInfo = new ParsedMovieInfo
{
MovieTitle = "Fack Ju Goethe 2: Same same",
MovieTitles = new List<string> { "Fack Ju Goethe 2: Same same" },
Languages = new List<Language> { Language.English },
Year = _movie.Year
};
_multiLanguageInfo = new ParsedMovieInfo
{
MovieTitle = _movie.Title,
MovieTitles = { _movie.Title },
Languages = new List<Language> { Language.Original, Language.French }
};
_multiLanguageWithOriginalInfo = new ParsedMovieInfo
{
MovieTitle = _movie.Title,
MovieTitles = { _movie.Title },
Languages = new List<Language> { Language.Original, Language.French, Language.English }
};
@@ -132,7 +132,7 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Subject.Map(_parsedMovieInfo, "", null);
Mocker.GetMock<IMovieService>()
.Verify(v => v.FindByTitle(It.IsAny<string>(), It.IsAny<int>(), null, null, null), Times.Once());
.Verify(v => v.FindByTitle(It.IsAny<List<string>>(), It.IsAny<int>(), It.IsAny<List<string>>(), null), Times.Once());
}
[Test]

View File

@@ -87,6 +87,8 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Name.S06E11.The.Santa.Simulation.480p.WEB-DL.x264-mSD", false)]
[TestCase("Movie.Name.S02E04.480p.WEB.DL.nSD.x264-NhaNc3", false)]
[TestCase("[HorribleSubs] Movie Title! 2018 [Web][MKV][h264][480p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
[TestCase("[SubsPlease] Movie Title (540p) [AB649D32].mkv", false)]
[TestCase("[Erai-raws] Movie Title [540p][Multiple Subtitle].mkv", false)]
public void should_parse_webdl480p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Source.WEBDL, proper, Resolution.R480p);
@@ -148,6 +150,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie Name.S01E01.The.Insanity.Principle.720p.WEB-DL.DD5.1.H.264-BD", false)]
[TestCase("[HorribleSubs] Movie Title! 2018 [Web][MKV][h264][720p][AAC 2.0][Softsubs (HorribleSubs)]", false)]
[TestCase("[HorribleSubs] Movie Title! 2018 [Web][MKV][h264][AAC 2.0][Softsubs (HorribleSubs)]", false)]
[TestCase("Movie.Title.2013.960p.WEB-DL.AAC2.0.H.264-squalor", false)]
public void should_parse_webdl720p_quality(string title, bool proper)
{
ParseAndVerifyQuality(title, Source.WEBDL, proper, Resolution.R720p);
@@ -286,6 +289,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Movie.Name.2008.BDREMUX.1080p.Bluray.AVC.DTS-HR.MA.5.1-LEGi0N")]
[TestCase("Movie.Title.M.2008.USA.BluRay.Remux.1080p.MPEG-2.DD.5.1-TDD")]
[TestCase("Movie.Title.2018.1080p.BluRay.REMUX.MPEG-2.DTS-HD.MA.5.1-EPSiLON")]
[TestCase("Movie.Title.II.2003.4K.BluRay.Remux.1080p.AVC.DTS-HD.MA.5.1-BMF")]
public void should_parse_remux1080p_quality(string title)
{
ParseAndVerifyQuality(title, Source.BLURAY, false, Resolution.R1080p, Modifier.REMUX);

View File

@@ -118,6 +118,7 @@ namespace NzbDrone.Core.Test.ParserTests
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-EVO-4P", "EVO")]
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-EVO-4Planet", "EVO")]
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-DON-AlteZachen", "DON")]
[TestCase("Some.Movie.2019.1080p.BDRip.X264.AC3-HarrHD-RePACKPOST", "HarrHD")]
public void should_not_include_bad_suffix_in_release_group(string title, string expected)
{

View File

@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0</TargetFrameworks>
<TargetFrameworks>net6.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.90" />
<PackageReference Include="Dapper" Version="2.0.123" />
<PackageReference Include="NBuilder" Version="6.1.0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Test.Common\Radarr.Test.Common.csproj" />

View File

@@ -23,6 +23,7 @@ namespace NzbDrone.Core.Annotations
public string Section { get; set; }
public HiddenType Hidden { get; set; }
public PrivacyLevel Privacy { get; set; }
public string Placeholder { get; set; }
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]

View File

@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Blocklisting
bool Blocklisted(int movieId, ReleaseInfo release);
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
List<Blocklist> GetByMovieId(int movieId);
void Block(RemoteMovie remoteMovie, string message);
void Delete(int id);
void Delete(List<int> ids);
}
@@ -72,6 +73,30 @@ namespace NzbDrone.Core.Blocklisting
return _blocklistRepository.BlocklistedByMovie(movieId);
}
public void Block(RemoteMovie remoteMovie, string message)
{
var blocklist = new Blocklist
{
MovieId = remoteMovie.Movie.Id,
SourceTitle = remoteMovie.Release.Title,
Quality = remoteMovie.ParsedMovieInfo.Quality,
Date = DateTime.UtcNow,
PublishedDate = remoteMovie.Release.PublishDate,
Size = remoteMovie.Release.Size,
Indexer = remoteMovie.Release.Indexer,
Protocol = remoteMovie.Release.DownloadProtocol,
Message = message,
Languages = remoteMovie.ParsedMovieInfo.Languages
};
if (remoteMovie.Release is TorrentInfo torrentRelease)
{
blocklist.TorrentInfoHash = torrentRelease.InfoHash;
}
_blocklistRepository.Insert(blocklist);
}
public void Delete(int id)
{
_blocklistRepository.Delete(id);

View File

@@ -192,13 +192,6 @@ namespace NzbDrone.Core.Configuration
set { SetValue("WhitelistedHardcodedSubs", value); }
}
public bool RemoveCompletedDownloads
{
get { return GetValueBoolean("RemoveCompletedDownloads", false); }
set { SetValue("RemoveCompletedDownloads", value); }
}
public bool AutoRedownloadFailed
{
get { return GetValueBoolean("AutoRedownloadFailed", true); }
@@ -206,13 +199,6 @@ namespace NzbDrone.Core.Configuration
set { SetValue("AutoRedownloadFailed", value); }
}
public bool RemoveFailedDownloads
{
get { return GetValueBoolean("RemoveFailedDownloads", true); }
set { SetValue("RemoveFailedDownloads", value); }
}
public bool CreateEmptyMovieFolders
{
get { return GetValueBoolean("CreateEmptyMovieFolders", false); }

View File

@@ -21,10 +21,8 @@ namespace NzbDrone.Core.Configuration
//Completed/Failed Download Handling (Download client)
bool EnableCompletedDownloadHandling { get; set; }
bool RemoveCompletedDownloads { get; set; }
bool AutoRedownloadFailed { get; set; }
bool RemoveFailedDownloads { get; set; }
//Media Management
bool AutoUnmonitorPreviouslyDownloadedMovies { get; set; }

View File

@@ -76,7 +76,7 @@ namespace NzbDrone.Core.CustomFormats
var info = new ParsedMovieInfo
{
MovieTitle = movieFile.Movie.Title,
MovieTitles = new List<string>() { movieFile.Movie.Title },
SimpleReleaseTitle = sceneName.SimplifyReleaseTitle(),
Quality = movieFile.Quality,
Languages = movieFile.Languages,
@@ -112,7 +112,7 @@ namespace NzbDrone.Core.CustomFormats
var info = new ParsedMovieInfo
{
MovieTitle = movie.Title,
MovieTitles = new List<string>() { movie.Title },
SimpleReleaseTitle = parsed?.SimpleReleaseTitle ?? blocklist.SourceTitle.SimplifyReleaseTitle(),
Quality = blocklist.Quality,
Languages = blocklist.Languages,
@@ -140,7 +140,7 @@ namespace NzbDrone.Core.CustomFormats
var info = new ParsedMovieInfo
{
MovieTitle = movie.Title,
MovieTitles = new List<string>() { movie.Title },
SimpleReleaseTitle = parsed?.SimpleReleaseTitle ?? history.SourceTitle.SimplifyReleaseTitle(),
Quality = history.Quality,
Languages = history.Languages,

View File

@@ -0,0 +1,903 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Dapper;
using FluentMigrator;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(199)]
public class mediainfo_to_ffmpeg : NzbDroneMigrationBase
{
private readonly JsonSerializerOptions _serializerSettings;
public mediainfo_to_ffmpeg()
{
var serializerSettings = new JsonSerializerOptions
{
AllowTrailingCommas = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
serializerSettings.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true));
serializerSettings.Converters.Add(new STJTimeSpanConverter());
serializerSettings.Converters.Add(new STJUtcConverter());
_serializerSettings = serializerSettings;
}
protected override void MainDbUpgrade()
{
Execute.WithConnection(MigrateToFfprobe);
}
private void MigrateToFfprobe(IDbConnection conn, IDbTransaction tran)
{
var existing = conn.Query<MediaInfoRaw>("SELECT Id, MediaInfo, SceneName FROM MovieFiles");
var updated = new List<MediaInfoRaw>();
foreach (var row in existing)
{
if (row.MediaInfo.IsNullOrWhiteSpace())
{
continue;
}
// basic parse to check schema revision
// in case user already tested ffmpeg branch
var mediaInfoVersion = JsonSerializer.Deserialize<MediaInfoBase>(row.MediaInfo, _serializerSettings);
if (mediaInfoVersion.SchemaRevision >= 8)
{
continue;
}
// parse and migrate
var mediaInfo = JsonSerializer.Deserialize<MediaInfo198>(row.MediaInfo, _serializerSettings);
var ffprobe = MigrateMediaInfo(mediaInfo, row.SceneName);
updated.Add(new MediaInfoRaw
{
Id = row.Id,
MediaInfo = JsonSerializer.Serialize(ffprobe, _serializerSettings)
});
}
var updateSql = "UPDATE MovieFiles SET MediaInfo = @MediaInfo WHERE Id = @Id";
conn.Execute(updateSql, updated, transaction: tran);
}
public MediaInfo199 MigrateMediaInfo(MediaInfo198 old, string sceneName)
{
var m = new MediaInfo199
{
SchemaRevision = old.SchemaRevision,
ContainerFormat = old.ContainerFormat,
VideoProfile = old.VideoProfile,
VideoBitrate = old.VideoBitrate,
VideoBitDepth = old.VideoBitDepth,
VideoMultiViewCount = old.VideoMultiViewCount,
VideoColourPrimaries = MigratePrimaries(old.VideoColourPrimaries),
VideoTransferCharacteristics = MigrateTransferCharacteristics(old.VideoTransferCharacteristics),
Height = old.Height,
Width = old.Width,
AudioBitrate = old.AudioBitrate,
RunTime = old.RunTime,
AudioStreamCount = old.AudioStreamCount,
VideoFps = old.VideoFps,
ScanType = old.ScanType,
AudioLanguages = MigrateLanguages(old.AudioLanguages),
Subtitles = MigrateLanguages(old.Subtitles)
};
m.VideoHdrFormat = MigrateHdrFormat(old);
MigrateVideoCodec(old, m, sceneName);
MigrateAudioCodec(old, m);
MigrateAudioChannelPositions(old, m);
m.AudioChannels = old.AudioChannelsStream > 0 ? old.AudioChannelsStream : old.AudioChannelsContainer;
return m;
}
private void MigrateVideoCodec(MediaInfo198 mediaInfo, MediaInfo199 m, string sceneName)
{
if (mediaInfo.VideoFormat == null)
{
MigrateVideoCodecLegacy(mediaInfo, m, sceneName);
return;
}
var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty;
var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty;
var result = mediaInfo.VideoFormat.Trim();
m.VideoFormat = result;
m.VideoCodecID = null;
if (videoFormat.ContainsIgnoreCase("x264"))
{
m.VideoFormat = "h264";
m.VideoCodecID = "x264";
return;
}
if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC"))
{
m.VideoFormat = "h264";
if (videoCodecLibrary.StartsWithIgnoreCase("x264"))
{
m.VideoCodecID = "x264";
}
return;
}
if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC"))
{
m.VideoFormat = "hevc";
if (videoCodecLibrary.StartsWithIgnoreCase("x265"))
{
m.VideoCodecID = "x265";
}
return;
}
if (videoFormat.ContainsIgnoreCase("MPEG Video"))
{
if (videoCodecID == "2" || videoCodecID == "V_MPEG2")
{
m.VideoFormat = "mpeg2video";
}
if (videoCodecID.IsNullOrWhiteSpace())
{
m.VideoFormat = "MPEG";
}
}
if (videoFormat.ContainsIgnoreCase("MPEG-2 Video"))
{
m.VideoFormat = "mpeg2video";
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual"))
{
m.VideoFormat = "mpeg4";
if (videoCodecID.ContainsIgnoreCase("XVID") ||
videoCodecLibrary.StartsWithIgnoreCase("XviD"))
{
m.VideoCodecID = "XVID";
}
if (videoCodecID.ContainsIgnoreCase("DIV3") ||
videoCodecID.ContainsIgnoreCase("DIVX") ||
videoCodecID.ContainsIgnoreCase("DX50") ||
videoCodecLibrary.StartsWithIgnoreCase("DivX"))
{
m.VideoCodecID = "DIVX";
}
return;
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v"))
{
m.VideoFormat = "mpeg4";
result = GetSceneNameMatch(sceneName, "XviD", "DivX", "");
if (result == "XviD")
{
m.VideoCodecID = "XVID";
}
if (result == "DivX")
{
m.VideoCodecID = "DIVX";
}
return;
}
if (videoFormat.ContainsIgnoreCase("VC-1"))
{
m.VideoFormat = "vc1";
return;
}
if (videoFormat.ContainsIgnoreCase("AV1"))
{
m.VideoFormat = "av1";
return;
}
if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") ||
videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9"))
{
m.VideoFormat = videoFormat.First().ToLowerInvariant();
return;
}
if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2"))
{
m.VideoFormat = "WMV";
return;
}
if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "DIVX";
return;
}
if (videoFormat.ContainsIgnoreCase("XviD"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "XVID";
return;
}
if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") ||
videoFormat.ContainsIgnoreCase("RealVideo 4"))
{
m.VideoFormat = "qtrle";
return;
}
if (videoFormat.ContainsIgnoreCase("mp42") ||
videoFormat.ContainsIgnoreCase("mp43"))
{
m.VideoFormat = "mpeg4";
return;
}
}
private void MigrateVideoCodecLegacy(MediaInfo198 mediaInfo, MediaInfo199 m, string sceneName)
{
var videoCodec = mediaInfo.VideoCodec;
m.VideoFormat = videoCodec;
m.VideoCodecID = null;
if (videoCodec.IsNullOrWhiteSpace())
{
m.VideoFormat = null;
return;
}
if (videoCodec == "AVC")
{
m.VideoFormat = "h264";
}
if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC")
{
m.VideoFormat = "hevc";
}
if (videoCodec == "MPEG-2 Video")
{
m.VideoFormat = "mpeg2video";
}
if (videoCodec == "MPEG-4 Visual")
{
var result = GetSceneNameMatch(sceneName, "DivX", "XviD");
if (result == "DivX")
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "DIVX";
}
m.VideoFormat = "mpeg4";
m.VideoCodecID = "XVID";
}
if (videoCodec.StartsWithIgnoreCase("XviD"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "XVID";
}
if (videoCodec.StartsWithIgnoreCase("DivX"))
{
m.VideoFormat = "mpeg4";
m.VideoCodecID = "DIVX";
}
if (videoCodec.EqualsIgnoreCase("VC-1"))
{
m.VideoFormat = "vc1";
}
}
private HdrFormat MigrateHdrFormat(MediaInfo198 mediaInfo)
{
if (mediaInfo.VideoHdrFormatCompatibility.IsNotNullOrWhiteSpace())
{
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("HLG"))
{
return HdrFormat.Hlg10;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby"))
{
return HdrFormat.DolbyVision;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("dolby"))
{
return HdrFormat.DolbyVision;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10+"))
{
return HdrFormat.Hdr10Plus;
}
if (mediaInfo.VideoHdrFormatCompatibility.ContainsIgnoreCase("hdr10"))
{
return HdrFormat.Hdr10;
}
}
return VideoFileInfoReader.GetHdrFormat(mediaInfo.VideoBitDepth, mediaInfo.VideoColourPrimaries, mediaInfo.VideoTransferCharacteristics, new ());
}
private void MigrateAudioCodec(MediaInfo198 mediaInfo, MediaInfo199 m)
{
if (mediaInfo.AudioCodecID == null)
{
MigrateAudioCodecLegacy(mediaInfo, m);
return;
}
var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty;
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
m.AudioFormat = "";
if (audioFormat.Empty())
{
return;
}
if (audioFormat.ContainsIgnoreCase("Atmos"))
{
m.AudioFormat = "truehd";
m.AudioCodecID = "thd+";
return;
}
if (audioFormat.ContainsIgnoreCase("MLP FBA"))
{
m.AudioFormat = "truehd";
if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch"))
{
m.AudioCodecID = "thd+";
return;
}
return;
}
if (audioFormat.ContainsIgnoreCase("TrueHD"))
{
m.AudioFormat = "truehd";
return;
}
if (audioFormat.ContainsIgnoreCase("FLAC"))
{
m.AudioFormat = "flac";
return;
}
if (audioFormat.ContainsIgnoreCase("DTS"))
{
m.AudioFormat = "dts";
if (splitAdditionalFeatures.ContainsIgnoreCase("XLL"))
{
if (splitAdditionalFeatures.ContainsIgnoreCase("X"))
{
m.AudioProfile = "DTS:X";
return;
}
m.AudioProfile = "DTS-HD MA";
return;
}
if (splitAdditionalFeatures.ContainsIgnoreCase("ES"))
{
m.AudioProfile = "DTS-ES";
return;
}
if (splitAdditionalFeatures.ContainsIgnoreCase("XBR"))
{
m.AudioProfile = "DTS-HD HRA";
return;
}
return;
}
if (audioFormat.ContainsIgnoreCase("E-AC-3"))
{
m.AudioFormat = "eac3";
if (splitAdditionalFeatures.ContainsIgnoreCase("JOC"))
{
m.AudioCodecID = "ec+3";
}
return;
}
if (audioFormat.ContainsIgnoreCase("AC-3"))
{
m.AudioFormat = "ac3";
return;
}
if (audioFormat.ContainsIgnoreCase("AAC"))
{
m.AudioFormat = "aac";
if (audioCodecID == "A_AAC/MPEG4/LC/SBR")
{
m.AudioCodecID = audioCodecID;
}
return;
}
if (audioFormat.ContainsIgnoreCase("mp3"))
{
m.AudioFormat = "mp3";
return;
}
if (audioFormat.ContainsIgnoreCase("MPEG Audio"))
{
if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3")
{
m.AudioFormat = "mp3";
return;
}
if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2")
{
m.AudioFormat = "mp2";
}
}
if (audioFormat.ContainsIgnoreCase("Opus"))
{
m.AudioFormat = "opus";
return;
}
if (audioFormat.ContainsIgnoreCase("PCM"))
{
m.AudioFormat = "pcm_s16le";
return;
}
if (audioFormat.ContainsIgnoreCase("ADPCM"))
{
m.AudioFormat = "pcm_s16le";
return;
}
if (audioFormat.ContainsIgnoreCase("Vorbis"))
{
m.AudioFormat = "vorbis";
return;
}
if (audioFormat.ContainsIgnoreCase("WMA"))
{
m.AudioFormat = "wmav1";
return;
}
}
private void MigrateAudioCodecLegacy(MediaInfo198 mediaInfo, MediaInfo199 m)
{
var audioFormat = mediaInfo.AudioFormat;
m.AudioFormat = audioFormat;
if (audioFormat.IsNullOrWhiteSpace())
{
m.AudioFormat = null;
return;
}
if (audioFormat.EqualsIgnoreCase("AC-3"))
{
m.AudioFormat = "ac3";
return;
}
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
{
m.AudioFormat = "eac3";
return;
}
if (audioFormat.EqualsIgnoreCase("AAC"))
{
m.AudioFormat = "aac";
return;
}
if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3")
{
m.AudioFormat = "mp3";
return;
}
if (audioFormat.EqualsIgnoreCase("DTS"))
{
m.AudioFormat = "DTS";
return;
}
if (audioFormat.EqualsIgnoreCase("TrueHD"))
{
m.AudioFormat = "truehd";
return;
}
if (audioFormat.EqualsIgnoreCase("FLAC"))
{
m.AudioFormat = "flac";
return;
}
if (audioFormat.EqualsIgnoreCase("Vorbis"))
{
m.AudioFormat = "vorbis";
return;
}
if (audioFormat.EqualsIgnoreCase("Opus"))
{
m.AudioFormat = "opus";
return;
}
}
private void MigrateAudioChannelPositions(MediaInfo198 mediaInfo, MediaInfo199 m)
{
var audioChannels = FormatAudioChannelsFromAudioChannelPositions(mediaInfo);
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo);
}
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo);
}
audioChannels ??= 0;
m.AudioChannelPositions = audioChannels.ToString();
}
private decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfo198 mediaInfo)
{
var audioChannelPositions = mediaInfo.AudioChannelPositions;
if (audioChannelPositions.IsNullOrWhiteSpace())
{
return null;
}
try
{
if (audioChannelPositions.Contains("+"))
{
return audioChannelPositions.Split('+')
.Sum(s => decimal.Parse(s.Trim(), CultureInfo.InvariantCulture));
}
if (audioChannelPositions.Contains("/"))
{
var channelStringList = Regex.Replace(audioChannelPositions,
@"^\d+\sobjects",
"",
RegexOptions.Compiled | RegexOptions.IgnoreCase)
.Replace("Object Based / ", "")
.Split(new string[] { " / " }, StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault()
?.Split('/');
var positions = default(decimal);
if (channelStringList == null)
{
return 0;
}
foreach (var channel in channelStringList)
{
var channelSplit = channel.Split(new string[] { "." }, StringSplitOptions.None);
if (channelSplit.Length == 3)
{
positions += decimal.Parse(string.Format("{0}.{1}", channelSplit[1], channelSplit[2]), CultureInfo.InvariantCulture);
}
else
{
positions += decimal.Parse(channel, CultureInfo.InvariantCulture);
}
}
return positions;
}
}
catch
{
}
return null;
}
private decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfo198 mediaInfo)
{
var audioChannelPositionsTextContainer = mediaInfo.AudioChannelPositionsTextContainer;
var audioChannelPositionsTextStream = mediaInfo.AudioChannelPositionsTextStream;
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
var audioChannelsStream = mediaInfo.AudioChannelsStream;
//Skip if the positions texts give us nothing
if ((audioChannelPositionsTextContainer.IsNullOrWhiteSpace() || audioChannelPositionsTextContainer == "Object Based") &&
(audioChannelPositionsTextStream.IsNullOrWhiteSpace() || audioChannelPositionsTextStream == "Object Based"))
{
return null;
}
try
{
if (audioChannelsStream > 0)
{
return audioChannelPositionsTextStream.ContainsIgnoreCase("LFE") ? audioChannelsStream - 1 + 0.1m : audioChannelsStream;
}
return audioChannelPositionsTextContainer.ContainsIgnoreCase("LFE") ? audioChannelsContainer - 1 + 0.1m : audioChannelsContainer;
}
catch
{
}
return null;
}
private decimal? FormatAudioChannelsFromAudioChannels(MediaInfo198 mediaInfo)
{
var audioChannelsContainer = mediaInfo.AudioChannelsContainer;
var audioChannelsStream = mediaInfo.AudioChannelsStream;
var audioFormat = (mediaInfo.AudioFormat ?? string.Empty).Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// Workaround https://github.com/MediaArea/MediaInfo/issues/299 for DTS-X Audio
if (audioFormat.ContainsIgnoreCase("DTS") &&
splitAdditionalFeatures.ContainsIgnoreCase("XLL") &&
splitAdditionalFeatures.ContainsIgnoreCase("X") &&
audioChannelsContainer > 0)
{
return audioChannelsContainer - 1 + 0.1m;
}
// FLAC 6 channels is likely 5.1
if (audioFormat.ContainsIgnoreCase("FLAC") && audioChannelsContainer == 6)
{
return 5.1m;
}
if (mediaInfo.SchemaRevision > 5)
{
return audioChannelsStream > 0 ? audioChannelsStream : audioChannelsContainer;
}
if (mediaInfo.SchemaRevision >= 3)
{
return audioChannelsContainer;
}
return null;
}
private List<string> MigrateLanguages(string mediaInfoLanguages)
{
var languages = new List<string>();
var tokens = mediaInfoLanguages.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
var cultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
for (int i = 0; i < tokens.Count; i++)
{
if (tokens[i] == "Swedis")
{
// Probably typo in mediainfo (should be 'Swedish')
languages.Add("swe");
continue;
}
if (tokens[i] == "Chinese" && OsInfo.IsNotWindows)
{
// Mono only has 'Chinese (Simplified)' & 'Chinese (Traditional)'
languages.Add("zho");
continue;
}
if (tokens[i] == "Norwegian")
{
languages.Add("nor");
continue;
}
try
{
var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]);
if (cultureInfo != null)
{
languages.Add(cultureInfo.ThreeLetterISOLanguageName.ToLowerInvariant());
}
}
catch
{
}
}
return languages;
}
private string MigratePrimaries(string primary)
{
return primary.IsNotNullOrWhiteSpace() ? primary.Replace("BT.", "bt") : primary;
}
private string MigrateTransferCharacteristics(string transferCharacteristics)
{
if (transferCharacteristics == "PQ")
{
return "smpte2084";
}
if (transferCharacteristics == "HLG")
{
return "arib-std-b67";
}
return "bt709";
}
private static string GetSceneNameMatch(string sceneName, params string[] tokens)
{
sceneName = sceneName.IsNotNullOrWhiteSpace() ? Parser.Parser.RemoveFileExtension(sceneName) : string.Empty;
foreach (var token in tokens)
{
if (sceneName.ContainsIgnoreCase(token))
{
return token;
}
}
// Last token is the default.
return tokens.Last();
}
public class MediaInfoRaw : ModelBase
{
public string MediaInfo { get; set; }
public string SceneName { get; set; }
}
public class MediaInfoBase
{
public int SchemaRevision { get; set; }
}
public class MediaInfo198 : MediaInfoBase
{
public string ContainerFormat { get; set; }
// Deprecated according to MediaInfo
public string VideoCodec { get; set; }
public string VideoFormat { get; set; }
public string VideoCodecID { get; set; }
public string VideoProfile { get; set; }
public string VideoCodecLibrary { get; set; }
public int VideoBitrate { get; set; }
public int VideoBitDepth { get; set; }
public int VideoMultiViewCount { get; set; }
public string VideoColourPrimaries { get; set; }
public string VideoTransferCharacteristics { get; set; }
public string VideoHdrFormat { get; set; }
public string VideoHdrFormatCompatibility { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string AudioFormat { get; set; }
public string AudioCodecID { get; set; }
public string AudioCodecLibrary { get; set; }
public string AudioAdditionalFeatures { get; set; }
public int AudioBitrate { get; set; }
public TimeSpan RunTime { get; set; }
public int AudioStreamCount { get; set; }
public int AudioChannelsContainer { get; set; }
public int AudioChannelsStream { get; set; }
public string AudioChannelPositions { get; set; }
public string AudioChannelPositionsTextContainer { get; set; }
public string AudioChannelPositionsTextStream { get; set; }
public string AudioProfile { get; set; }
public decimal VideoFps { get; set; }
public string AudioLanguages { get; set; }
public string Subtitles { get; set; }
public string ScanType { get; set; }
}
public class MediaInfo199 : MediaInfoBase
{
public string ContainerFormat { get; set; }
public string VideoFormat { get; set; }
public string VideoCodecID { get; set; }
public string VideoProfile { get; set; }
public int VideoBitrate { get; set; }
public int VideoBitDepth { get; set; }
public int VideoMultiViewCount { get; set; }
public string VideoColourPrimaries { get; set; }
public string VideoTransferCharacteristics { get; set; }
public HdrFormat VideoHdrFormat { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public string AudioFormat { get; set; }
public string AudioCodecID { get; set; }
public string AudioProfile { get; set; }
public int AudioBitrate { get; set; }
public TimeSpan RunTime { get; set; }
public int AudioStreamCount { get; set; }
public int AudioChannels { get; set; }
public string AudioChannelPositions { get; set; }
public decimal VideoFps { get; set; }
public List<string> AudioLanguages { get; set; }
public List<string> Subtitles { get; set; }
public string ScanType { get; set; }
}
}
}

View File

@@ -0,0 +1,56 @@
using System.Data;
using System.Linq;
using FluentMigrator;
using Newtonsoft.Json.Linq;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(200)]
public class cdh_per_downloadclient : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("DownloadClients")
.AddColumn("RemoveCompletedDownloads").AsBoolean().NotNullable().WithDefaultValue(true)
.AddColumn("RemoveFailedDownloads").AsBoolean().NotNullable().WithDefaultValue(true);
Execute.WithConnection(MoveRemoveSettings);
}
private void MoveRemoveSettings(IDbConnection conn, IDbTransaction tran)
{
var removeCompletedDownloads = false;
var removeFailedDownloads = true;
using (var removeCompletedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removecompleteddownloads'"))
{
if ((removeCompletedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "true")
{
removeCompletedDownloads = true;
}
}
using (var removeFailedDownloadsCmd = conn.CreateCommand(tran, "SELECT Value FROM Config WHERE Key = 'removefaileddownloads'"))
{
if ((removeFailedDownloadsCmd.ExecuteScalar() as string)?.ToLower() == "false")
{
removeFailedDownloads = false;
}
}
using (var updateClientCmd = conn.CreateCommand(tran, $"UPDATE DownloadClients SET RemoveCompletedDownloads = (CASE WHEN Implementation IN (\"RTorrent\", \"Flood\") THEN 0 ELSE ? END), RemoveFailedDownloads = ?"))
{
updateClientCmd.AddParameter(removeCompletedDownloads ? 1 : 0);
updateClientCmd.AddParameter(removeFailedDownloads ? 1 : 0);
updateClientCmd.ExecuteNonQuery();
}
using (var removeConfigCmd = conn.CreateCommand(tran, $"DELETE FROM Config WHERE Key IN ('removecompleteddownloads', 'removefaileddownloads')"))
{
removeConfigCmd.ExecuteNonQuery();
}
}
}
}

View File

@@ -1,4 +1,5 @@
using FluentMigrator;
using System.Data;
using FluentMigrator;
using FluentMigrator.Builders.Create;
using FluentMigrator.Builders.Create.Table;
using FluentMigrator.Runner;
@@ -16,6 +17,15 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
return expressionRoot.Table(name).WithColumn("Id").AsInt32().PrimaryKey().Identity();
}
public static IDbCommand CreateCommand(this IDbConnection conn, IDbTransaction tran, string query)
{
var command = conn.CreateCommand();
command.Transaction = tran;
command.CommandText = query;
return command;
}
public static void AddParameter(this System.Data.IDbCommand command, object value)
{
var parameter = command.CreateParameter();

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@@ -20,8 +20,9 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
ILogger<NzbDroneSQLiteProcessor> logger,
IOptionsSnapshot<ProcessorOptions> options,
IConnectionStringAccessor connectionStringAccessor,
IServiceProvider serviceProvider)
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider)
IServiceProvider serviceProvider,
SQLiteQuoter quoter)
: base(factory, generator, logger, options, connectionStringAccessor, serviceProvider, quoter)
{
}

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