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

Compare commits

..

15 Commits

Author SHA1 Message Date
nitsua
315c2e1b88 Add support for template switching using Theme Park templates 2021-11-12 07:58:12 -05: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
80 changed files with 3309 additions and 1713 deletions

View File

@@ -67,7 +67,7 @@ stages:
enableAnalysis: 'true'
Mac:
osName: 'Mac'
imageName: 'macos-10.14'
imageName: 'macos-10.15'
enableAnalysis: 'false'
Windows:
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'
@@ -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
@@ -383,7 +384,7 @@ stages:
osName: 'Mac'
testName: 'MacCore'
poolName: 'Azure Pipelines'
imageName: 'macos-10.14'
imageName: 'macos-10.15'
WindowsCore:
osName: 'Windows'
testName: 'WindowsCore'
@@ -417,16 +418,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 +472,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 +518,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 +693,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:

View File

@@ -4,6 +4,7 @@
],
"ignoreFiles": [
"frontend/src/Styles/scaffolding.css",
"**/Theme.Park/**/*.css",
"**/*.js"
],
"rules": {

View File

@@ -124,6 +124,20 @@ module.exports = (env) => {
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
},
// Theme.Park
{
source: 'frontend/src/Content/Theme.Park/*',
destination: path.join(distFolder, 'Content/Theme.Park')
},
{
source: 'frontend/src/Content/Theme.Park/Themes/*',
destination: path.join(distFolder, 'Content/Theme.Park/Themes')
},
{
source: 'frontend/src/Content/Theme.Park/Resources/*',
destination: path.join(distFolder, 'Content/Theme.Park/Resources')
}
]
}
@@ -243,6 +257,19 @@ module.exports = (env) => {
}
}
]
},
{
test: /\.(png)?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Theme.Park/Resources/[name].[ext]'
}
}
]
}
]
}

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

@@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react';
const theme = window.Radarr.theme;
function ThemeSelector({ children }) {
return (
<>
{
theme !== 'default' &&
<>
<link rel="stylesheet" type="text/css"
href={'/Content/Theme.Park/radarr-base.css'}
/>
<link rel="stylesheet" type="text/css"
href={`/Content/Theme.Park/Themes/${theme}.css`}
/>
</>
}
{children}
</>
);
}
ThemeSelector.propTypes = {
children: PropTypes.object.isRequired
};
export default ThemeSelector;

View File

@@ -113,6 +113,8 @@ function FormInputGroup(props) {
helpTexts,
helpTextWarning,
helpLink,
inlineLink,
tooltip,
pending,
errors,
warnings,
@@ -182,6 +184,9 @@ function FormInputGroup(props) {
!checkInput && helpText &&
<FormInputHelpText
text={helpText}
link={inlineLink}
tooltip={tooltip}
isWarning={!!inlineLink}
/>
}
@@ -263,6 +268,8 @@ FormInputGroup.propTypes = {
helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
inlineLink: PropTypes.string,
tooltip: PropTypes.string,
pending: PropTypes.bool,
errors: PropTypes.arrayOf(PropTypes.object),
warnings: PropTypes.arrayOf(PropTypes.object)

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: radial-gradient(ellipse at center, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--drop-down-menu-bg: radial-gradient(ellipse at top, #47918a 0%, #0b3161 100%) center center/cover no-repeat fixed;
--button-color: #009688;
--button-color-hover: #12afa0;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 18, 175, 160;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #0ed2bf;
--link-color-hover: #36e7d6;
--label-text-color: #fff;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #009688; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: rgb(21, 213, 194);
--petio-spinner: invert(39%) sepia(98%) saturate(527%) hue-rotate(129deg) brightness(94%) contrast(101%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 18, 175, 160;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: radial-gradient(circle, #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(circle , #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(circle , #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(circle , #3a3a3a, #2d2d2d, #202020, #141414, #000000) center center/cover no-repeat fixed;
--drop-down-menu-bg: #2d2d2d;
--button-color: #7a7a7a;
--button-color-hover: #9b9b9b;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 170, 170, 170;
--accent-color-hover: rgba(255, 255, 255, 0.45);
--link-color: #7a7a7a;
--link-color-hover: #fff;
--label-text-color: black;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #6b5; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #e5a00d;
--petio-spinner: invert(35%) sepia(12%) saturate(4%) hue-rotate(2deg) brightness(104%) contrast(86%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 255, 255, 255;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: #282a36;
--modal-bg-color: #1e2029;
--modal-header-color: #1e2029;
--modal-footer-color: #1e2029;
--drop-down-menu-bg: #1e2029;
--button-color: #bd93f9;
--button-color-hover: #ff79c6;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 80, 250, 123;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #ff79c6;
--link-color-hover: #8be9fd;
--label-text-color: #282a36;
--text:#6272a4;
--text-hover: #95adfa;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #50fa7b; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #bd93f9;
--petio-spinner: invert(79%) sepia(27%) saturate(1033%) hue-rotate(74deg) brightness(104%) contrast(96%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 80, 250, 123;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--modal-bg-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--modal-header-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--modal-footer-color: linear-gradient(0deg, rgba(247,101,184,1) 0%, rgb(21, 95, 165) 100%) center center/cover no-repeat fixed;
--drop-down-menu-bg: linear-gradient(90deg, rgba(247,101,184,1) 0%, rgba(21, 95, 165) 100%) center center/cover no-repeat fixed;
--button-color: #f98dc9;
--button-color-hover: #ff4cb1;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 249, 141, 201;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color:rgb(255, 179, 222);
--link-color-hover: #d7fffe;
--label-text-color: #fff;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #f98dc9; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #f765b8;
--petio-spinner: invert(78%) sepia(17%) saturate(4447%) hue-rotate(290deg) brightness(109%) contrast(95%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 215,255,254;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: linear-gradient(45deg, #fb3f62 0%, #204c80 37%, #004249 97%) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(circle, #204c80 0%, #000 100%) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(circle, #204c80 0%, #000 100%) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(circle, #204c80 0%, #000 100%) center center/cover no-repeat fixed;
--drop-down-menu-bg: #204c80;
--button-color: #fb3f62;
--button-color-hover: #cd4164;
--button-text: #eee;
--button-text-hover: #FFF;
--accent-color: 251, 63, 98;
--accent-color-hover: rgba(var(--accent-color), .8);
--link-color: rgb(0, 255, 157);
--link-color-hover: rgba(0, 255, 157, 0.8);
--label-text-color: #282a36;
--text:#eee;
--text-hover: #fff;
--text-muted: #999;
--arr-queue-color: rgb(0, 255, 157);
--plex-poster-unwatched: #fb3f62;
--petio-spinner: invert(29%) sepia(87%) saturate(2199%) hue-rotate(331deg) brightness(115%) contrast(97%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 251, 63, 98;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: #2E3440;
--modal-bg-color: #3B4252;
--modal-header-color: #434C5E;
--modal-footer-color: #434C5E;
--drop-down-menu-bg: #3B4252;
--button-color: #79b8ca;
--button-color-hover: #6a9daf;
--button-text: #2E3440;
--button-text-hover: #D8DEE9;
--accent-color: 121, 184, 202;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #81A1C1;
--link-color-hover: #88C0D0;
--label-text-color: #222730;
--text:#D8DEE9;
--text-hover: #ECEFF4;
--text-muted: #81A1C1;
/*Specials*/
--arr-queue-color: #A3BE8C; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #D08770;
--petio-spinner: invert(83%) sepia(9%) saturate(1787%) hue-rotate(156deg) brightness(85%) contrast(83%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 121, 184, 202;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: #1f1f1f;
--modal-bg-color: #333;
--modal-header-color: #232323;
--modal-footer-color: #232323;
--drop-down-menu-bg: #1b1b1b;
--button-color: #2cabe3;
--button-color-hover: #298fbc;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 44, 171, 227;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #2cabe3;
--link-color-hover: #3cc5ff;
--label-text-color: #fff;
--text:#96a2b4;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #2cabe3; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #2cabe3;
--petio-spinner: invert(65%) sepia(83%) saturate(2026%) hue-rotate(167deg) brightness(90%) contrast(97%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 44, 171, 227;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: linear-gradient(360deg, hsl(221, 39%, 11%) 65%, hsl(215, 28%, 17%) 100%);
--modal-bg-color: #1f2937;
--modal-header-color: #1f2937;
--modal-footer-color: #1f2937;
--drop-down-menu-bg: #374151;
--button-color: #4f46e5;
--button-color-hover: #6366f1;
--button-text: #e5e7eb;
--button-text-hover: #fff;
--accent-color: 167, 139, 250;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #6366f1;
--link-color-hover: #a78bfa;
--label-text-color: #000;
--text: #d1d5db;
--text-hover: #fff;
--text-muted: #9ca3af;
/*Specials*/
--arr-queue-color: #6366f1; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #6366f1;
--petio-spinner: invert(24%) sepia(59%) saturate(3411%) hue-rotate(237deg) brightness(91%) contrast(96%); /* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 98, 116, 145;
}

View File

@@ -0,0 +1,28 @@
:root {
--main-bg-color: url("Resources/blur-noise.png") repeat scroll 0% 0%, radial-gradient(circle at 0% 100%, rgba(54, 66, 84, 0.55) 0%, rgba(54, 66, 84, 0.043) 70%, rgba(54, 66, 84, 0) 80%), radial-gradient(circle at 100% 100%, rgba(113, 135, 153, 0.55) 0%, rgba(113, 135, 153, 0.043) 70%, rgba(113, 135, 153, 0) 80%), radial-gradient(circle at 100% 0%, rgba(54, 66, 84, 0.55) 0%, rgba(54, 66, 84, 0.043) 70%, rgba(54, 66, 84, 0) 80%), radial-gradient(circle at 0% 0%, rgba(91, 114, 135, 0.55) 0%, rgba(91, 114, 135, 0.043) 70%, rgba(91, 114, 135, 0) 80%), rgb(0, 0, 0) center center/cover no-repeat fixed;
--modal-bg-color: #1f2326;
--modal-header-color: #1f2326;
--modal-footer-color: #323232;
--drop-down-menu-bg: #191a1c;
--button-color: #cc7b19;
--button-color-hover: #e59029;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 229, 160, 13;
--accent-color-hover: #ffc107;
--link-color: #e5a00d;
--link-color-hover: #fff;
--label-text-color: #fff;
--text:#ddd;
--text-hover: #fff;
--text-muted: #999;
--arr-queue-color: #27c24c;
--petio-spinner: invert(0%) sepia(0%) saturate(100%) hue-rotate(0deg) brightness(100%) contrast(100%);
--gitea-color-primary-dark-4: 255, 193, 7;
}

View File

@@ -0,0 +1,124 @@
:root {
--main-bg-color: #454545;
--modal-bg-color: #595959;
--modal-header-color: #595959;
--modal-footer-color: #595959;
--drop-down-menu-bg: #606060;
--button-color: #5899eb;
--button-color-hover: #4b91ea;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 255, 194, 48;
--accent-color-hover: rgb(255, 194, 48, .8);
--link-color: rgb(255, 194, 48);
--link-color-hover: rgb(255, 194, 48, .8);
--label-text-color: #eee;
--text: #ccc;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #5d9cec;
--side-menu-active: #333333;
--scroller-hover: #606060;
--scroller: #707070;
--border-color: #606060;
--label-color: #707070;
--label-info: #5d9cec;
--header-color: #464b51;
--side-menu-color: #595959;
}
/* HEADER */
[class*="PageHeader-header-"] {
background-color: var(--header-color);
color: #fff;
}
[class*="PageToolbar-toolbar-"] {
background-color: #707070;
color: var(--text);
}
/* SIDE MENU */
[class*="PageSidebar-sidebar-"] {
background-color: var(--side-menu-color);
color: #fff;
}
[class*=PageSidebarItem-link-]:focus {
color: rgb(var(--accent-color)) !important;
}
[class*=PageSidebarItem-isActiveLink-] {
color: var(--link-color) !important;
}
[class*=PageSidebarItem-isActiveParentLink-] {
background-color: var(--side-menu-active);
}
/* SCROLLER */
[class*=ImportSeriesSelectSeries-results-]::-webkit-scrollbar-thumb:hover,
[class*=OverlayScroller-thumb-]:hover {
background-color: var(--scroller-hover) !important;
}
[class*="OverlayScroller-thumb-"],
[class*=Scroller-scroller-]::-webkit-scrollbar-thumb {
background-color: var(--scroller) !important;
}
/* MODALS */
[class*=ModalHeader-modalHeader-],
[class*=FieldSet-legend-] {
border-bottom: 1px solid var(--border-color);
}
[class*=ModalFooter-modalFooter-] {
border-top: 1px solid var(--border-color);
}
/* LABLES */
[class*="Label-default-"] {
border-color: var(--label-color);
background-color: var(--label-color);
color: white;
}
[class*="Label-info-"]:not([class*="PageSidebarItem-status-"] [class*="Label-info-"]) {
border-color: var(--label-info);
background-color: var(--label-info);
color: #fff;
}
/* SETTINGS */
[class*=Settings-link-] {
border-bottom: 1px solid var(--border-color);
}
/* SEARCH DROP DOWN */
[class*="MovieSearchInput-containerOpen-"] [class*="MovieSearchInput-movieContainer-"] {
border: 1px solid var(--drop-down-menu-bg);
background-color: var(--drop-down-menu-bg);
box-shadow: inset 0 1px 1px rgb(0 0 0 / 8%);
color: #e1e2e3;
}
/* SERIES PAGE */
[class*="MovieIndexPoster-controls-"] {
background-color: var(--label-color) !important;
color: #fff !important;
}

View File

@@ -0,0 +1,30 @@
:root {
--main-bg-color: radial-gradient(ellipse at center, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--modal-bg-color: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--modal-header-color: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--modal-footer-color: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--drop-down-menu-bg: radial-gradient(ellipse at top, rgba(87, 108, 117, 1) 0%, rgba(37, 50, 55, 1) 100.2%) center center/cover no-repeat fixed;
--button-color: #607D8B;
--button-color-hover: #81a6b7;
--button-text: #eee;
--button-text-hover: #fff;
--accent-color: 129, 166, 183;
--accent-color-hover: rgb(var(--accent-color),.8);
--link-color: #81a6b7;
--link-color-hover: #9adfff;
--label-text-color: #fff;
--text:#bbb;
--text-hover: #fff;
--text-muted: #999;
/*Specials*/
--arr-queue-color: #81a6b7; /* Servarr apps + Bazarr*/
--plex-poster-unwatched: #70aeca;
--petio-spinner: invert(50%) sepia(31%) saturate(341%) hue-rotate(155deg) brightness(88%) contrast(85%);/* Made with https://codepen.io/jsm91/embed/ZEEawyZ */
--gitea-color-primary-dark-4: 129, 166, 183;
}

File diff suppressed because it is too large Load Diff

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

@@ -48,6 +48,21 @@ export const movieRuntimeFormatOptions = [
{ key: 'minutes', value: '75 mins' }
];
export const themeOptions = [
{ key: 'default', value: 'Default' },
{ key: 'aquamarine', value: 'Aquamarine' },
{ key: 'dark', value: 'Dark' },
{ key: 'dracula', value: 'Dracula' },
{ key: 'hotline', value: 'Hotline' },
{ key: 'hotpink', value: 'Hotpink' },
{ key: 'nord', value: 'Nord' },
{ key: 'organizr', value: 'Organizr' },
{ key: 'overseerr', value: 'Overseerr' },
{ key: 'plex', value: 'Plex' },
{ key: 'radarr-darker', value: 'Radarr Darker' },
{ key: 'space-gray', value: 'Space Gray' }
];
class UISettings extends Component {
//
@@ -184,6 +199,21 @@ class UISettings extends Component {
</FieldSet>
<FieldSet legend={translate('Style')}>
<FormGroup>
<FormLabel>{translate('Theme')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="theme"
helpText={translate('ThemeHelpText', ['Theme.Park'])}
inlineLink="https://github.com/GilbN/theme.park"
tooltip="Theme.Park Github"
helpTextWarning={translate('ThemeHelpTextWarning')}
values={themeOptions}
onChange={onInputChange}
{...settings.theme}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SettingsEnableColorImpairedMode')}</FormLabel>
<FormInputGroup

View File

@@ -1,6 +1,7 @@
import { createBrowserHistory } from 'history';
import React from 'react';
import { render } from 'react-dom';
import ThemeSelector from 'App/ThemeSelector';
import createAppStore from 'Store/createAppStore';
import App from './App/App';
@@ -13,9 +14,11 @@ const history = createBrowserHistory();
const store = createAppStore(history);
render(
<App
store={store}
history={history}
/>,
<ThemeSelector>
<App
store={store}
history={history}
/>
</ThemeSelector>,
document.getElementById('root')
);

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

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

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

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

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

@@ -61,9 +61,6 @@ 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);

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

@@ -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,29 @@ 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
};
if (remoteMovie.Release is TorrentInfo torrentRelease)
{
blocklist.TorrentInfoHash = torrentRelease.InfoHash;
}
_blocklistRepository.Insert(blocklist);
}
public void Delete(int id)
{
_blocklistRepository.Delete(id);

View File

@@ -43,6 +43,7 @@ namespace NzbDrone.Core.Configuration
string SslCertPassword { get; }
string UrlBase { get; }
string UiFolder { get; }
string Theme { get; }
bool UpdateAutomatically { get; }
UpdateMechanism UpdateMechanism { get; }
string UpdateScriptPath { get; }
@@ -140,6 +141,8 @@ namespace NzbDrone.Core.Configuration
public int SslPort => GetValueInt("SslPort", 9898);
public string Theme => GetValue("Theme", "default");
public bool EnableSsl => GetValueBoolean("EnableSsl", false);
public bool LaunchBrowser => GetValueBoolean("LaunchBrowser", true);

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

@@ -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)
{
}

View File

@@ -32,7 +32,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
[FieldDefinition(1, Label = "Port", Type = FieldType.Number)]
public int Port { get; set; }
[FieldDefinition(2, Label = "RPC Path", Type = FieldType.Textbox)]
[FieldDefinition(2, Label = "XML RPC Path", Type = FieldType.Textbox)]
public string RpcPath { get; set; }
[FieldDefinition(3, Label = "Use SSL", Type = FieldType.Checkbox)]

View File

@@ -307,14 +307,14 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc
streamDetails.Add(video);
var audio = new XElement("audio");
var audioChannelCount = movieFile.MediaInfo.AudioChannelsStream > 0 ? movieFile.MediaInfo.AudioChannelsStream : movieFile.MediaInfo.AudioChannelsContainer;
var audioChannelCount = movieFile.MediaInfo.AudioChannels;
audio.Add(new XElement("bitrate", movieFile.MediaInfo.AudioBitrate));
audio.Add(new XElement("channels", audioChannelCount));
audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName)));
audio.Add(new XElement("language", movieFile.MediaInfo.AudioLanguages));
streamDetails.Add(audio);
if (movieFile.MediaInfo.Subtitles != null && movieFile.MediaInfo.Subtitles.Length > 0)
if (movieFile.MediaInfo.Subtitles != null && movieFile.MediaInfo.Subtitles.Count > 0)
{
var subtitle = new XElement("subtitle");
subtitle.Add(new XElement("language", movieFile.MediaInfo.Subtitles));

View File

@@ -1,30 +0,0 @@
using System;
using System.Runtime.CompilerServices;
using NzbDrone.Core.Localization;
using NzbDrone.Core.MediaFiles.MediaInfo;
namespace NzbDrone.Core.HealthCheck.Checks
{
public class MediaInfoDllCheck : HealthCheckBase
{
public MediaInfoDllCheck(ILocalizationService localizationService)
: base(localizationService)
{
}
[MethodImpl(MethodImplOptions.NoOptimization)]
public override HealthCheck Check()
{
try
{
var mediaInfo = new MediaInfo();
}
catch (Exception e)
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format(_localizationService.GetLocalizedString("MediaInfoDllCheckMessage"), e.Message), "#mediainfo-not-loaded");
}
return new HealthCheck(GetType());
}
}
}

View File

@@ -62,6 +62,7 @@
"AreYouSureYouWantToDeleteThisDelayProfile": "Are you sure you want to delete this delay profile?",
"AreYouSureYouWantToDeleteThisImportListExclusion": "Are you sure you want to delete this import list exclusion?",
"AreYouSureYouWantToDeleteThisRemotePathMapping": "Are you sure you want to delete this remote path mapping?",
"AreYouSureYouWantToRemoveSelectedItemFromQueue": "Are you sure you want to remove 1 item from the queue?",
"AreYouSureYouWantToRemoveSelectedItemsFromQueue": "Are you sure you want to remove {0} item{1} from the queue?",
"AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist": "Are you sure you want to remove the selected items from the blocklist?",
"AreYouSureYouWantToResetYourAPIKey": "Are you sure you want to reset your API Key?",
@@ -90,6 +91,7 @@
"Blocklisted": "Blocklisted",
"BlocklistHelpText": "Prevents Radarr from automatically grabbing this release again",
"BlocklistRelease": "Blocklist Release",
"BlocklistReleases": "Blocklist Releases",
"Branch": "Branch",
"BranchUpdate": "Branch to use to update Radarr",
"BranchUpdateMechanism": "Branch used by external update mechanism",
@@ -798,6 +800,8 @@
"RemoveMovieAndKeepFiles": "Remove Movie and Keep Files",
"RemoveRootFolder": "Remove root folder",
"RemoveSelected": "Remove Selected",
"RemoveSelectedItem": "Remove Selected Item",
"RemoveSelectedItems": "Remove Selected Items",
"RemovingTag": "Removing tag",
"Renamed": "Renamed",
"RenameFiles": "Rename Files",
@@ -972,6 +976,9 @@
"TestAllIndexers": "Test All Indexers",
"TestAllLists": "Test All Lists",
"TheLogLevelDefault": "The log level defaults to 'Info' and can be changed in",
"Theme": "Theme",
"ThemeHelpText": "Themes maintained by {0}!",
"ThemeHelpTextWarning": "Reload required after saving",
"ThisCannotBeCancelled": "This cannot be cancelled once started without restarting Radarr.",
"ThisConditionMatchesUsingRegularExpressions": "This condition matches using Regular Expressions. Note that the characters {0} have special meanings and need escaping with a {1}",
"Time": "Time",

View File

@@ -0,0 +1,12 @@
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public enum HdrFormat
{
None,
Pq10,
Hdr10,
Hdr10Plus,
Hlg10,
DolbyVision
}
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
@@ -12,9 +11,9 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public static class MediaInfoFormatter
{
private const string ValidHdrColourPrimaries = "BT.2020";
private const string VideoDynamicRangeHdr = "HDR";
private static readonly string[] ValidHdrTransferFunctions = { "PQ", "HLG" };
private static readonly Regex PositionRegex = new Regex(@"(?<position>^\d\.\d)", RegexOptions.Compiled);
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfoFormatter));
@@ -24,101 +23,95 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannelPositionsText(mediaInfo);
audioChannels = mediaInfo.AudioChannels;
}
if (audioChannels == null || audioChannels == 0.0m)
{
audioChannels = FormatAudioChannelsFromAudioChannels(mediaInfo);
}
return audioChannels ?? 0;
return audioChannels.Value;
}
public static string FormatAudioCodec(MediaInfoModel mediaInfo, string sceneName)
{
if (mediaInfo.AudioCodecID == null)
if (mediaInfo.AudioFormat == null)
{
return FormatAudioCodecLegacy(mediaInfo, sceneName);
return null;
}
var audioFormat = mediaInfo.AudioFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var audioFormat = mediaInfo.AudioFormat;
var audioCodecID = mediaInfo.AudioCodecID ?? string.Empty;
var audioProfile = mediaInfo.AudioProfile ?? string.Empty;
var audioCodecLibrary = mediaInfo.AudioCodecLibrary ?? string.Empty;
var splitAdditionalFeatures = (mediaInfo.AudioAdditionalFeatures ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (audioFormat.Empty())
{
return string.Empty;
}
if (audioFormat.ContainsIgnoreCase("Atmos"))
// see definitions here https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c
if (audioCodecID == "thd+")
{
return "TrueHD Atmos";
}
if (audioFormat.ContainsIgnoreCase("MLP FBA"))
{
if (splitAdditionalFeatures.ContainsIgnoreCase("16-ch"))
{
return "TrueHD Atmos";
}
return "TrueHD";
}
if (audioFormat.ContainsIgnoreCase("TrueHD"))
if (audioFormat == "truehd")
{
return "TrueHD";
}
if (audioFormat.ContainsIgnoreCase("FLAC"))
if (audioFormat == "flac")
{
return "FLAC";
}
if (audioFormat.ContainsIgnoreCase("DTS"))
if (audioFormat == "dts")
{
if (splitAdditionalFeatures.ContainsIgnoreCase("XLL"))
if (audioProfile == "DTS:X")
{
if (splitAdditionalFeatures.ContainsIgnoreCase("X"))
{
return "DTS-X";
}
return "DTS-X";
}
if (audioProfile == "DTS-HD MA")
{
return "DTS-HD MA";
}
if (splitAdditionalFeatures.ContainsIgnoreCase("ES"))
if (audioProfile == "DTS-ES")
{
return "DTS-ES";
}
if (splitAdditionalFeatures.ContainsIgnoreCase("XBR"))
if (audioProfile == "DTS-HD HRA")
{
return "DTS-HD HRA";
}
if (audioProfile == "DTS Express")
{
return "DTS Express";
}
if (audioProfile == "DTS 96/24")
{
return "DTS 96/24";
}
return "DTS";
}
if (audioFormat.ContainsIgnoreCase("E-AC-3"))
if (audioCodecID == "ec+3")
{
if (splitAdditionalFeatures.ContainsIgnoreCase("JOC"))
{
return "EAC3 Atmos";
}
return "EAC3 Atmos";
}
if (audioFormat == "eac3")
{
return "EAC3";
}
if (audioFormat.ContainsIgnoreCase("AC-3"))
if (audioFormat == "ac3")
{
return "AC3";
}
if (audioFormat.ContainsIgnoreCase("AAC"))
if (audioFormat == "aac")
{
if (audioCodecID == "A_AAC/MPEG4/LC/SBR")
{
@@ -128,461 +121,166 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
return "AAC";
}
if (audioFormat.ContainsIgnoreCase("mp3"))
if (audioFormat == "mp3")
{
return "MP3";
}
if (audioFormat.ContainsIgnoreCase("MPEG Audio"))
if (audioFormat == "mp2")
{
if (mediaInfo.AudioCodecID == "55" || mediaInfo.AudioCodecID == "A_MPEG/L3" || mediaInfo.AudioProfile == "Layer 3")
{
return "MP3";
}
if (mediaInfo.AudioCodecID == "A_MPEG/L2" || mediaInfo.AudioProfile == "Layer 2")
{
return "MP2";
}
return "MP2";
}
if (audioFormat.ContainsIgnoreCase("Opus"))
if (audioFormat == "opus")
{
return "Opus";
}
if (audioFormat.ContainsIgnoreCase("PCM"))
if (audioFormat.StartsWith("pcm_") || audioFormat.StartsWith("adpcm_"))
{
return "PCM";
}
if (audioFormat.ContainsIgnoreCase("ADPCM"))
{
return "PCM";
}
if (audioFormat.ContainsIgnoreCase("Vorbis"))
if (audioFormat == "vorbis")
{
return "Vorbis";
}
if (audioFormat.ContainsIgnoreCase("WMA"))
if (audioFormat == "wmav1" ||
audioFormat == "wmav2")
{
return "WMA";
}
if (audioFormat.ContainsIgnoreCase("A_QUICKTIME"))
{
return "";
}
Logger.Debug()
.Message("Unknown audio format: '{0}' in '{1}'.", string.Join(", ", mediaInfo.AudioFormat, audioCodecID, audioProfile, audioCodecLibrary, mediaInfo.AudioAdditionalFeatures), sceneName)
.WriteSentryWarn("UnknownAudioFormat", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID)
.Message("Unknown audio format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName)
.WriteSentryWarn("UnknownAudioFormatFFProbe", mediaInfo.ContainerFormat, mediaInfo.AudioFormat, audioCodecID)
.Write();
return mediaInfo.AudioFormat;
}
public static string FormatAudioCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
{
var audioFormat = mediaInfo.AudioFormat;
if (audioFormat.IsNullOrWhiteSpace())
{
return audioFormat;
}
if (audioFormat.EqualsIgnoreCase("AC-3"))
{
return "AC3";
}
if (audioFormat.EqualsIgnoreCase("E-AC-3"))
{
return "EAC3";
}
if (audioFormat.EqualsIgnoreCase("AAC"))
{
return "AAC";
}
if (audioFormat.EqualsIgnoreCase("MPEG Audio") && mediaInfo.AudioProfile == "Layer 3")
{
return "MP3";
}
if (audioFormat.EqualsIgnoreCase("DTS"))
{
return "DTS";
}
if (audioFormat.EqualsIgnoreCase("TrueHD"))
{
return "TrueHD";
}
if (audioFormat.EqualsIgnoreCase("FLAC"))
{
return "FLAC";
}
if (audioFormat.EqualsIgnoreCase("Vorbis"))
{
return "Vorbis";
}
if (audioFormat.EqualsIgnoreCase("Opus"))
{
return "Opus";
}
return audioFormat;
}
public static string FormatVideoCodec(MediaInfoModel mediaInfo, string sceneName)
{
if (mediaInfo.VideoFormat == null)
{
return FormatVideoCodecLegacy(mediaInfo, sceneName);
return null;
}
var videoFormat = mediaInfo.VideoFormat.Trim().Split(new[] { " / " }, StringSplitOptions.RemoveEmptyEntries);
var videoFormat = mediaInfo.VideoFormat;
var videoCodecID = mediaInfo.VideoCodecID ?? string.Empty;
var videoProfile = mediaInfo.VideoProfile ?? string.Empty;
var videoCodecLibrary = mediaInfo.VideoCodecLibrary ?? string.Empty;
var result = mediaInfo.VideoFormat.Trim();
var result = videoFormat.Trim();
if (videoFormat.Empty())
{
return result;
}
if (videoFormat.ContainsIgnoreCase("x264"))
// see definitions here: https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/codec_desc.c
if (videoCodecID == "x264")
{
return "x264";
}
if (videoFormat.ContainsIgnoreCase("AVC") || videoFormat.ContainsIgnoreCase("V.MPEG4/ISO/AVC"))
if (videoFormat == "h264")
{
if (videoCodecLibrary.StartsWithIgnoreCase("x264"))
{
return "x264";
}
return GetSceneNameMatch(sceneName, "AVC", "x264", "h264");
}
if (videoFormat.ContainsIgnoreCase("HEVC") || videoFormat.ContainsIgnoreCase("V_MPEGH/ISO/HEVC"))
if (videoCodecID == "x265")
{
if (videoCodecLibrary.StartsWithIgnoreCase("x265"))
{
return "x265";
}
return "x265";
}
if (videoFormat == "hevc")
{
return GetSceneNameMatch(sceneName, "HEVC", "x265", "h265");
}
if (videoFormat.ContainsIgnoreCase("MPEG Video"))
{
if (videoCodecID == "2" || videoCodecID == "V_MPEG2")
{
return "MPEG2";
}
if (videoCodecID.IsNullOrWhiteSpace())
{
return "MPEG";
}
}
if (videoFormat.ContainsIgnoreCase("MPEG-2 Video"))
if (videoFormat == "mpeg2video")
{
return "MPEG2";
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual"))
if (videoFormat == "mpeg1video")
{
if (videoCodecID.ContainsIgnoreCase("XVID") ||
videoCodecLibrary.StartsWithIgnoreCase("XviD"))
return "MPEG";
}
if (videoFormat == "mpeg4")
{
if (videoCodecID == "XVID")
{
return "XviD";
}
if (videoCodecID.ContainsIgnoreCase("DIV3") ||
videoCodecID.ContainsIgnoreCase("DIVX") ||
videoCodecID.ContainsIgnoreCase("DX50") ||
videoCodecLibrary.StartsWithIgnoreCase("DivX"))
if (videoCodecID == "DIV3" ||
videoCodecID == "DIVX" ||
videoCodecID == "DX50")
{
return "DivX";
}
return "";
}
if (videoFormat.ContainsIgnoreCase("MPEG-4 Visual") || videoFormat.ContainsIgnoreCase("mp4v"))
{
result = GetSceneNameMatch(sceneName, "XviD", "DivX", "");
if (result.IsNotNullOrWhiteSpace())
{
return result;
}
if (videoCodecLibrary.Contains("Lavc"))
{
return ""; // libavcodec mpeg-4
}
if (videoCodecLibrary.Contains("em4v"))
{
return ""; // NeroDigital
}
if (videoCodecLibrary.Contains("Intel(R) IPP"))
{
return ""; // Intel(R) IPP
}
if (videoCodecLibrary.Contains("ZJMedia") ||
videoCodecLibrary.Contains("DigiArty"))
{
return ""; // Other
}
if (string.IsNullOrEmpty(videoCodecLibrary))
{
return ""; // Unknown mp4v
}
}
if (videoFormat.ContainsIgnoreCase("VC-1"))
if (videoFormat == "vc1")
{
return "VC1";
}
if (videoFormat.ContainsIgnoreCase("AV1"))
if (videoFormat == "av1")
{
return "AV1";
}
if (videoFormat.ContainsIgnoreCase("VP6") || videoFormat.ContainsIgnoreCase("VP7") ||
videoFormat.ContainsIgnoreCase("VP8") || videoFormat.ContainsIgnoreCase("VP9"))
if (videoFormat == "vp6" ||
videoFormat == "vp7" ||
videoFormat == "vp8" ||
videoFormat == "vp9")
{
return videoFormat.First().ToUpperInvariant();
return videoFormat.ToUpperInvariant();
}
if (videoFormat.ContainsIgnoreCase("WMV1") || videoFormat.ContainsIgnoreCase("WMV2"))
if (videoFormat == "wmv1" ||
videoFormat == "wmv2")
{
return "WMV";
}
if (videoFormat.ContainsIgnoreCase("DivX") || videoFormat.ContainsIgnoreCase("div3"))
if (videoFormat == "qtrle" ||
videoFormat == "rpza" ||
videoFormat == "rv10" ||
videoFormat == "rv20" ||
videoFormat == "rv30" ||
videoFormat == "rv40")
{
return "DivX";
}
if (videoFormat.ContainsIgnoreCase("XviD"))
{
return "XviD";
}
if (videoFormat.ContainsIgnoreCase("V_QUICKTIME") ||
videoFormat.ContainsIgnoreCase("RealVideo 4"))
{
return "";
}
if (videoFormat.ContainsIgnoreCase("mp42") ||
videoFormat.ContainsIgnoreCase("mp43"))
{
// MS old DivX competitor
return "";
}
Logger.Debug()
.Message("Unknown video format: '{0}' in '{1}'.", string.Join(", ", mediaInfo.VideoFormat, videoCodecID, videoProfile, videoCodecLibrary), sceneName)
.WriteSentryWarn("UnknownVideoFormat", mediaInfo.ContainerFormat, mediaInfo.VideoFormat, videoCodecID)
.Message("Unknown video format: '{0}' in '{1}'.", mediaInfo.RawStreamData, sceneName)
.WriteSentryWarn("UnknownVideoFormatFFProbe", mediaInfo.ContainerFormat, videoFormat, videoCodecID)
.Write();
return result;
}
public static string FormatVideoCodecLegacy(MediaInfoModel mediaInfo, string sceneName)
{
var videoCodec = mediaInfo.VideoCodec;
if (videoCodec.IsNullOrWhiteSpace())
{
return videoCodec;
}
if (videoCodec == "AVC")
{
return GetSceneNameMatch(sceneName, "AVC", "h264", "x264");
}
if (videoCodec == "V_MPEGH/ISO/HEVC" || videoCodec == "HEVC")
{
return GetSceneNameMatch(sceneName, "HEVC", "h265", "x265");
}
if (videoCodec == "MPEG-2 Video")
{
return "MPEG2";
}
if (videoCodec == "MPEG-4 Visual")
{
return GetSceneNameMatch(sceneName, "DivX", "XviD");
}
if (videoCodec.StartsWithIgnoreCase("XviD"))
{
return "XviD";
}
if (videoCodec.StartsWithIgnoreCase("DivX"))
{
return "DivX";
}
if (videoCodec.EqualsIgnoreCase("VC-1"))
{
return "VC1";
}
return videoCodec;
}
private static decimal? FormatAudioChannelsFromAudioChannelPositions(MediaInfoModel mediaInfo)
{
var audioChannelPositions = mediaInfo.AudioChannelPositions;
var audioFormat = mediaInfo.AudioFormat;
if (audioChannelPositions.IsNullOrWhiteSpace())
if (mediaInfo.AudioChannelPositions == null)
{
return null;
return 0;
}
try
var match = PositionRegex.Match(mediaInfo.AudioChannelPositions);
if (match.Success)
{
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 (Exception ex)
{
Logger.Warn()
.Message("Unable to format audio channels using 'AudioChannelPositions', with a value of: '{0}' and '{1}'. Error {2}", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer, ex.Message)
.WriteSentryWarn("UnknownAudioChannelFormat", audioChannelPositions, mediaInfo.AudioChannelPositionsTextContainer)
.Write();
return decimal.Parse(match.Groups["position"].Value, NumberStyles.Number, CultureInfo.InvariantCulture);
}
return null;
}
private static decimal? FormatAudioChannelsFromAudioChannelPositionsText(MediaInfoModel 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 (Exception e)
{
Logger.Warn(e, "Unable to format audio channels using 'AudioChannelPositionsText' or 'AudioChannelPositionsTextStream', with value of: '{0}' and '{1}", audioChannelPositionsTextContainer, audioChannelPositionsTextStream);
}
return null;
}
private static decimal? FormatAudioChannelsFromAudioChannels(MediaInfoModel 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;
return 0;
}
private static string GetSceneNameMatch(string sceneName, params string[] tokens)
@@ -603,23 +301,7 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
public static string FormatVideoDynamicRange(MediaInfoModel mediaInfo)
{
if (mediaInfo.VideoHdrFormat.IsNotNullOrWhiteSpace())
{
return VideoDynamicRangeHdr;
}
if (mediaInfo.VideoBitDepth >= 10 &&
mediaInfo.VideoColourPrimaries.IsNotNullOrWhiteSpace() &&
mediaInfo.VideoTransferCharacteristics.IsNotNullOrWhiteSpace())
{
if (mediaInfo.VideoColourPrimaries.EqualsIgnoreCase(ValidHdrColourPrimaries) &&
ValidHdrTransferFunctions.Any(mediaInfo.VideoTransferCharacteristics.Contains))
{
return VideoDynamicRangeHdr;
}
}
return "";
return mediaInfo.VideoHdrFormat != HdrFormat.None ? VideoDynamicRangeHdr : "";
}
}
}

View File

@@ -1,370 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Instrumentation;
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
[Flags]
public enum BufferStatus
{
Accepted = 1,
Filled = 2,
Updated = 4,
Finalized = 8
}
public enum StreamKind
{
General,
Video,
Audio,
Text,
Other,
Image,
Menu
}
public enum InfoKind
{
Name,
Text,
Measure,
Options,
NameText,
MeasureText,
Info,
HowTo
}
public enum InfoOptions
{
ShowInInform,
Support,
ShowInSupported,
TypeOfValue
}
public enum InfoFileOptions
{
FileOption_Nothing = 0x00,
FileOption_NoRecursive = 0x01,
FileOption_CloseAll = 0x02,
FileOption_Max = 0x04
}
public class MediaInfo : IDisposable
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(MediaInfo));
private IntPtr _handle;
public bool MustUseAnsi { get; set; }
public Encoding Encoding { get; set; }
public MediaInfo()
{
_handle = MediaInfo_New();
InitializeEncoding();
}
~MediaInfo()
{
if (_handle != IntPtr.Zero)
{
MediaInfo_Delete(_handle);
}
}
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
MediaInfo_Delete(_handle);
}
GC.SuppressFinalize(this);
}
private void InitializeEncoding()
{
if (Environment.OSVersion.ToString().IndexOf("Windows") != -1)
{
// Windows guaranteed UCS-2
MustUseAnsi = false;
Encoding = Encoding.Unicode;
}
else
{
var responses = new List<string>();
// Linux normally UCS-4. As fallback we try UCS-2 and plain Ansi.
MustUseAnsi = false;
Encoding = Encoding.UTF32;
var version = Option("Info_Version", "");
responses.Add(version);
if (version.StartsWith("MediaInfoLib"))
{
return;
}
Encoding = Encoding.Unicode;
version = Option("Info_Version", "");
responses.Add(version);
if (version.StartsWith("MediaInfoLib"))
{
return;
}
MustUseAnsi = true;
Encoding = Encoding.Default;
version = Option("Info_Version", "");
responses.Add(version);
if (version.StartsWith("MediaInfoLib"))
{
return;
}
throw new NotSupportedException("Unsupported MediaInfoLib encoding, version check responses (may be gibberish, show it to the Radarr devs): " + responses.Join(", "));
}
}
private IntPtr MakeStringParameter(string value)
{
var buffer = Encoding.GetBytes(value);
Array.Resize(ref buffer, buffer.Length + 4);
var buf = Marshal.AllocHGlobal(buffer.Length);
Marshal.Copy(buffer, 0, buf, buffer.Length);
return buf;
}
private string MakeStringResult(IntPtr value)
{
if (Encoding == Encoding.Unicode)
{
return Marshal.PtrToStringUni(value);
}
else if (Encoding == Encoding.UTF32)
{
int i = 0;
for (; i < 1024; i += 4)
{
var data = Marshal.ReadInt32(value, i);
if (data == 0)
{
break;
}
}
var buffer = new byte[i];
Marshal.Copy(value, buffer, 0, i);
return Encoding.GetString(buffer, 0, i);
}
else
{
return Marshal.PtrToStringAnsi(value);
}
}
public int Open(string fileName)
{
var pFileName = MakeStringParameter(fileName);
try
{
if (MustUseAnsi)
{
return (int)MediaInfoA_Open(_handle, pFileName);
}
else
{
return (int)MediaInfo_Open(_handle, pFileName);
}
}
finally
{
Marshal.FreeHGlobal(pFileName);
}
}
public int Open(Stream stream)
{
if (stream.Length < 1024)
{
return 0;
}
var isValid = (int)MediaInfo_Open_Buffer_Init(_handle, stream.Length, 0);
if (isValid == 1)
{
var buffer = new byte[16 * 1024];
long seekStart = 0;
long totalRead = 0;
int bufferRead;
do
{
bufferRead = stream.Read(buffer, 0, buffer.Length);
totalRead += bufferRead;
var status = (BufferStatus)MediaInfo_Open_Buffer_Continue(_handle, buffer, (IntPtr)bufferRead);
if (status.HasFlag(BufferStatus.Finalized) || status <= 0 || bufferRead == 0)
{
Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart);
break;
}
var seekPos = MediaInfo_Open_Buffer_Continue_GoTo_Get(_handle);
if (seekPos != -1)
{
Logger.Trace("Read file offset {0}-{1} ({2} bytes)", seekStart, stream.Position, stream.Position - seekStart);
seekPos = stream.Seek(seekPos, SeekOrigin.Begin);
seekStart = seekPos;
MediaInfo_Open_Buffer_Init(_handle, stream.Length, seekPos);
}
}
while (bufferRead > 0);
MediaInfo_Open_Buffer_Finalize(_handle);
Logger.Trace("Read a total of {0} bytes ({1:0.0}%)", totalRead, totalRead * 100.0 / stream.Length);
}
return isValid;
}
public void Close()
{
MediaInfo_Close(_handle);
}
public string Get(StreamKind streamKind, int streamNumber, string parameter, InfoKind infoKind = InfoKind.Text, InfoKind searchKind = InfoKind.Name)
{
var pParameter = MakeStringParameter(parameter);
try
{
if (MustUseAnsi)
{
return MakeStringResult(MediaInfoA_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind));
}
else
{
return MakeStringResult(MediaInfo_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, pParameter, (IntPtr)infoKind, (IntPtr)searchKind));
}
}
finally
{
Marshal.FreeHGlobal(pParameter);
}
}
public string Get(StreamKind streamKind, int streamNumber, int parameter, InfoKind infoKind)
{
if (MustUseAnsi)
{
return MakeStringResult(MediaInfoA_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind));
}
else
{
return MakeStringResult(MediaInfo_GetI(_handle, (IntPtr)streamKind, (IntPtr)streamNumber, (IntPtr)parameter, (IntPtr)infoKind));
}
}
public string Option(string option, string value)
{
var pOption = MakeStringParameter(option.ToLowerInvariant());
var pValue = MakeStringParameter(value);
try
{
if (MustUseAnsi)
{
return MakeStringResult(MediaInfoA_Option(_handle, pOption, pValue));
}
else
{
return MakeStringResult(MediaInfo_Option(_handle, pOption, pValue));
}
}
finally
{
Marshal.FreeHGlobal(pOption);
Marshal.FreeHGlobal(pValue);
}
}
public int State_Get()
{
return (int)MediaInfo_State_Get(_handle);
}
public int Count_Get(StreamKind streamKind, int streamNumber = -1)
{
return (int)MediaInfo_Count_Get(_handle, (IntPtr)streamKind, (IntPtr)streamNumber);
}
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_New();
[DllImport("mediainfo")]
private static extern void MediaInfo_Delete(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open(IntPtr handle, IntPtr fileName);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize);
[DllImport("mediainfo")]
private static extern long MediaInfo_Open_Buffer_Continue_GoTo_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Open_Buffer_Finalize(IntPtr handle);
[DllImport("mediainfo")]
private static extern void MediaInfo_Close(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Option(IntPtr handle, IntPtr option, IntPtr value);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_State_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfo_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_New();
[DllImport("mediainfo")]
private static extern void MediaInfoA_Delete(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open(IntPtr handle, IntPtr fileName);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open_Buffer_Init(IntPtr handle, long fileSize, long fileOffset);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open_Buffer_Continue(IntPtr handle, byte[] buffer, IntPtr bufferSize);
[DllImport("mediainfo")]
private static extern long MediaInfoA_Open_Buffer_Continue_GoTo_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Open_Buffer_Finalize(IntPtr handle);
[DllImport("mediainfo")]
private static extern void MediaInfoA_Close(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_GetI(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber, IntPtr parameter, IntPtr infoKind, IntPtr searchKind);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Option(IntPtr handle, IntPtr option, IntPtr value);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_State_Get(IntPtr handle);
[DllImport("mediainfo")]
private static extern IntPtr MediaInfoA_Count_Get(IntPtr handle, IntPtr streamKind, IntPtr streamNumber);
}
}

View File

@@ -1,44 +1,70 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using FFMpegCore;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.MediaFiles.MediaInfo
{
public class MediaInfoModel : IEmbeddedDocument
{
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 string RawStreamData { get; set; }
public string RawFrameData { get; set; }
public int SchemaRevision { get; set; }
[JsonIgnore]
public IMediaAnalysis Analysis => FFProbe.Analyse(RawStreamData);
[JsonIgnore]
public IMediaAnalysis Frames => FFProbe.Analyse(RawFrameData);
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 DoviConfigurationRecordSideData DoviConfigurationRecord { 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

@@ -1,6 +1,8 @@
using System;
using System.Globalization;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using FFMpegCore;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
@@ -17,14 +19,33 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
{
private readonly IDiskProvider _diskProvider;
private readonly Logger _logger;
private readonly List<FFProbePixelFormat> _pixelFormats;
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 4;
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 7;
public const int MINIMUM_MEDIA_INFO_SCHEMA_REVISION = 8;
public const int CURRENT_MEDIA_INFO_SCHEMA_REVISION = 8;
private static readonly string[] ValidHdrColourPrimaries = { "bt2020" };
private static readonly string[] HlgTransferFunctions = { "bt2020-10", "arib-std-b67" };
private static readonly string[] PqTransferFunctions = { "smpte2084" };
private static readonly string[] ValidHdrTransferFunctions = HlgTransferFunctions.Concat(PqTransferFunctions).ToArray();
public VideoFileInfoReader(IDiskProvider diskProvider, Logger logger)
{
_diskProvider = diskProvider;
_logger = logger;
// We bundle ffprobe for all platforms
GlobalFFOptions.Configure(options => options.BinaryFolder = AppDomain.CurrentDomain.BaseDirectory);
try
{
_pixelFormats = FFProbe.GetPixelFormats();
}
catch (Exception e)
{
_logger.Error(e, "Failed to get supported pixel formats from ffprobe");
_pixelFormats = new List<FFProbePixelFormat>();
}
}
public MediaInfoModel GetMediaInfo(string filename)
@@ -34,181 +55,77 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
throw new FileNotFoundException("Media file does not exist: " + filename);
}
MediaInfo mediaInfo = null;
// TODO: Cache media info by path, mtime and length so we don't need to read files multiple times
try
{
mediaInfo = new MediaInfo();
_logger.Debug("Getting media info from {0}", filename);
var ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 50000000" });
if (filename.ToLower().EndsWith(".ts"))
var analysis = FFProbe.AnalyseStreamJson(ffprobeOutput);
if (analysis.PrimaryAudioStream.ChannelLayout.IsNullOrWhiteSpace())
{
// For .ts files we often have to scan more of the file to get all the info we need
mediaInfo.Option("ParseSpeed", "0.3");
}
else
{
mediaInfo.Option("ParseSpeed", "0.0");
ffprobeOutput = FFProbe.GetStreamJson(filename, ffOptions: new FFOptions { ExtraArguments = "-probesize 150000000 -analyzeduration 150000000" });
analysis = FFProbe.AnalyseStreamJson(ffprobeOutput);
}
int open;
using (var stream = _diskProvider.OpenReadStream(filename))
var mediaInfoModel = new MediaInfoModel
{
open = mediaInfo.Open(stream);
ContainerFormat = analysis.Format.FormatName,
VideoFormat = analysis.PrimaryVideoStream?.CodecName,
VideoCodecID = analysis.PrimaryVideoStream?.CodecTagString,
VideoProfile = analysis.PrimaryVideoStream?.Profile,
VideoBitrate = analysis.PrimaryVideoStream?.BitRate ?? 0,
VideoMultiViewCount = 1,
VideoBitDepth = GetPixelFormat(analysis.PrimaryVideoStream?.PixelFormat)?.Components.Min(x => x.BitDepth) ?? 8,
VideoColourPrimaries = analysis.PrimaryVideoStream?.ColorPrimaries,
VideoTransferCharacteristics = analysis.PrimaryVideoStream?.ColorTransfer,
DoviConfigurationRecord = analysis.PrimaryVideoStream?.SideDataList?.Find(x => x.GetType().Name == nameof(DoviConfigurationRecordSideData)) as DoviConfigurationRecordSideData,
Height = analysis.PrimaryVideoStream?.Height ?? 0,
Width = analysis.PrimaryVideoStream?.Width ?? 0,
AudioFormat = analysis.PrimaryAudioStream?.CodecName,
AudioCodecID = analysis.PrimaryAudioStream?.CodecTagString,
AudioProfile = analysis.PrimaryAudioStream?.Profile,
AudioBitrate = analysis.PrimaryAudioStream?.BitRate ?? 0,
RunTime = GetBestRuntime(analysis.PrimaryAudioStream?.Duration, analysis.PrimaryVideoStream?.Duration, analysis.Format.Duration),
AudioStreamCount = analysis.AudioStreams.Count,
AudioChannels = analysis.PrimaryAudioStream?.Channels ?? 0,
AudioChannelPositions = analysis.PrimaryAudioStream?.ChannelLayout,
VideoFps = analysis.PrimaryVideoStream?.FrameRate ?? 0,
AudioLanguages = analysis.AudioStreams?.Select(x => x.Language)
.Where(l => l.IsNotNullOrWhiteSpace())
.ToList(),
Subtitles = analysis.SubtitleStreams?.Select(x => x.Language)
.Where(l => l.IsNotNullOrWhiteSpace())
.ToList(),
ScanType = "Progressive",
RawStreamData = ffprobeOutput,
SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION
};
FFProbeFrames frames = null;
// if it looks like PQ10 or similar HDR, do a frame analysis to figure out which type it is
if (PqTransferFunctions.Contains(mediaInfoModel.VideoTransferCharacteristics))
{
var frameOutput = FFProbe.GetFrameJson(filename, ffOptions: new () { ExtraArguments = "-read_intervals \"%+#1\" -select_streams v" });
mediaInfoModel.RawFrameData = frameOutput;
frames = FFProbe.AnalyseFrameJson(frameOutput);
}
if (open != 0)
{
int audioRuntime;
int videoRuntime;
int generalRuntime;
var streamSideData = analysis.PrimaryVideoStream?.SideDataList ?? new ();
var framesSideData = frames?.Frames[0]?.SideDataList ?? new ();
// Runtime
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime);
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime);
int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime);
var sideData = streamSideData.Concat(framesSideData).ToList();
mediaInfoModel.VideoHdrFormat = GetHdrFormat(mediaInfoModel.VideoBitDepth, mediaInfoModel.VideoColourPrimaries, mediaInfoModel.VideoTransferCharacteristics, sideData);
// Audio Channels
var audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2");
int.TryParse(audioChannelsStr, out var audioChannels);
if (audioRuntime == 0 && videoRuntime == 0 && generalRuntime == 0)
{
// No runtime, ask mediainfo to scan the whole file
_logger.Trace("No runtime value found, rescanning at 1.0 scan depth");
mediaInfo.Option("ParseSpeed", "1.0");
using (var stream = _diskProvider.OpenReadStream(filename))
{
open = mediaInfo.Open(stream);
}
}
else if (audioChannels > 2 && audioChannelPositions.IsNullOrWhiteSpace())
{
// Some files with DTS don't have ChannelPositions unless more of the file is scanned
_logger.Trace("DTS audio without expected channel information, rescanning at 0.3 scan depth");
mediaInfo.Option("ParseSpeed", "0.3");
using (var stream = _diskProvider.OpenReadStream(filename))
{
open = mediaInfo.Open(stream);
}
}
}
if (open != 0)
{
int width;
int height;
int videoBitRate;
int audioBitRate;
int audioRuntime;
int videoRuntime;
int generalRuntime;
int streamCount;
int audioChannels;
int audioChannelsOrig;
int videoBitDepth;
decimal videoFrameRate;
int videoMultiViewCount;
string subtitles = mediaInfo.Get(StreamKind.General, 0, "Text_Language_List");
string scanType = mediaInfo.Get(StreamKind.Video, 0, "ScanType");
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Width"), out width);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "Height"), out height);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate"), out videoBitRate);
if (videoBitRate <= 0)
{
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitRate_Nominal"), out videoBitRate);
}
decimal.TryParse(mediaInfo.Get(StreamKind.Video, 0, "FrameRate"), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out videoFrameRate);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "BitDepth"), out videoBitDepth);
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "MultiView_Count"), out videoMultiViewCount);
//Runtime
int.TryParse(mediaInfo.Get(StreamKind.Video, 0, "PlayTime"), out videoRuntime);
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "PlayTime"), out audioRuntime);
int.TryParse(mediaInfo.Get(StreamKind.General, 0, "PlayTime"), out generalRuntime);
string aBitRate = mediaInfo.Get(StreamKind.Audio, 0, "BitRate").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(aBitRate, out audioBitRate);
int.TryParse(mediaInfo.Get(StreamKind.Audio, 0, "StreamCount"), out streamCount);
string audioChannelsStr = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(audioChannelsStr, out audioChannels);
string audioChannelsStrOrig = mediaInfo.Get(StreamKind.Audio, 0, "Channel(s)_Original").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
int.TryParse(audioChannelsStrOrig, out audioChannelsOrig);
var audioChannelPositionsText = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions");
var audioChannelPositionsTextOrig = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions_Original");
var audioChannelPositions = mediaInfo.Get(StreamKind.Audio, 0, "ChannelPositions/String2");
string audioLanguages = mediaInfo.Get(StreamKind.General, 0, "Audio_Language_List");
string videoProfile = mediaInfo.Get(StreamKind.Video, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
string audioProfile = mediaInfo.Get(StreamKind.Audio, 0, "Format_Profile").Split(new string[] { " /" }, StringSplitOptions.None)[0].Trim();
var mediaInfoModel = new MediaInfoModel
{
ContainerFormat = mediaInfo.Get(StreamKind.General, 0, "Format"),
VideoFormat = mediaInfo.Get(StreamKind.Video, 0, "Format"),
VideoCodecID = mediaInfo.Get(StreamKind.Video, 0, "CodecID"),
VideoProfile = videoProfile,
VideoCodecLibrary = mediaInfo.Get(StreamKind.Video, 0, "Encoded_Library"),
VideoBitrate = videoBitRate,
VideoBitDepth = videoBitDepth,
VideoMultiViewCount = videoMultiViewCount,
VideoColourPrimaries = mediaInfo.Get(StreamKind.Video, 0, "colour_primaries"),
VideoTransferCharacteristics = mediaInfo.Get(StreamKind.Video, 0, "transfer_characteristics"),
VideoHdrFormat = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format"),
VideoHdrFormatCompatibility = mediaInfo.Get(StreamKind.Video, 0, "HDR_Format_Compatibility"),
Height = height,
Width = width,
AudioFormat = mediaInfo.Get(StreamKind.Audio, 0, "Format"),
AudioCodecID = mediaInfo.Get(StreamKind.Audio, 0, "CodecID"),
AudioProfile = audioProfile,
AudioCodecLibrary = mediaInfo.Get(StreamKind.Audio, 0, "Encoded_Library"),
AudioAdditionalFeatures = mediaInfo.Get(StreamKind.Audio, 0, "Format_AdditionalFeatures"),
AudioBitrate = audioBitRate,
RunTime = GetBestRuntime(audioRuntime, videoRuntime, generalRuntime),
AudioStreamCount = streamCount,
AudioChannelsContainer = audioChannels,
AudioChannelsStream = audioChannelsOrig,
AudioChannelPositions = audioChannelPositions,
AudioChannelPositionsTextContainer = audioChannelPositionsText,
AudioChannelPositionsTextStream = audioChannelPositionsTextOrig,
VideoFps = videoFrameRate,
AudioLanguages = audioLanguages,
Subtitles = subtitles,
ScanType = scanType,
SchemaRevision = CURRENT_MEDIA_INFO_SCHEMA_REVISION
};
return mediaInfoModel;
}
else
{
_logger.Warn("Unable to open media info from file: " + filename);
}
}
catch (DllNotFoundException ex)
{
_logger.Error(ex, "mediainfo is required but was not found");
return mediaInfoModel;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to parse media info from file: {0}", filename);
}
finally
{
mediaInfo?.Close();
}
return null;
}
@@ -220,19 +137,70 @@ namespace NzbDrone.Core.MediaFiles.MediaInfo
return info?.RunTime;
}
private TimeSpan GetBestRuntime(int audio, int video, int general)
private static TimeSpan GetBestRuntime(TimeSpan? audio, TimeSpan? video, TimeSpan general)
{
if (video == 0)
if (!video.HasValue || video.Value.TotalMilliseconds == 0)
{
if (audio == 0)
if (!audio.HasValue || audio.Value.TotalMilliseconds == 0)
{
return TimeSpan.FromMilliseconds(general);
return general;
}
return TimeSpan.FromMilliseconds(audio);
return audio.Value;
}
return TimeSpan.FromMilliseconds(video);
return video.Value;
}
private FFProbePixelFormat GetPixelFormat(string format)
{
return _pixelFormats.Find(x => x.Name == format);
}
public static HdrFormat GetHdrFormat(int bitDepth, string colorPrimaries, string transferFunction, List<SideData> sideData)
{
if (bitDepth < 10)
{
return HdrFormat.None;
}
if (TryFindSideData(sideData, nameof(DoviConfigurationRecordSideData)))
{
return HdrFormat.DolbyVision;
}
if (!ValidHdrColourPrimaries.Contains(colorPrimaries) || !ValidHdrTransferFunctions.Contains(transferFunction))
{
return HdrFormat.None;
}
if (HlgTransferFunctions.Contains(transferFunction))
{
return HdrFormat.Hlg10;
}
if (PqTransferFunctions.Contains(transferFunction))
{
if (TryFindSideData(sideData, nameof(HdrDynamicMetadataSpmte2094)))
{
return HdrFormat.Hdr10Plus;
}
if (TryFindSideData(sideData, nameof(MasteringDisplayMetadata)) ||
TryFindSideData(sideData, nameof(ContentLightLevelMetadata)))
{
return HdrFormat.Hdr10;
}
return HdrFormat.Pq10;
}
return HdrFormat.None;
}
private static bool TryFindSideData(List<SideData> list, string typeName)
{
return list?.Find(x => x.GetType().Name == typeName) != null;
}
}
}

View File

@@ -19,13 +19,13 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Aggregation.Aggregators.Augmenter
return null;
}
var audioLanguages = localMovie.MediaInfo.AudioLanguages.Split('/').Select(l => l.Trim()).Distinct().ToList();
var audioLanguages = localMovie.MediaInfo.AudioLanguages.Distinct().ToList();
var languages = new List<Languages.Language>();
foreach (var audioLanguage in audioLanguages)
{
var language = IsoLanguages.FindByName(audioLanguage)?.Language;
var language = IsoLanguages.Find(audioLanguage)?.Language;
languages.AddIfNotNull(language);
}

View File

@@ -53,7 +53,7 @@ namespace NzbDrone.Core.MediaFiles.MovieImport
if (!runTime.HasValue)
{
_logger.Error("Failed to get runtime from the file, make sure mediainfo is available");
_logger.Error("Failed to get runtime from the file, make sure ffprobe is available");
return DetectSampleResult.Indeterminate;
}

View File

@@ -187,11 +187,11 @@ namespace NzbDrone.Core.Notifications.Discord
break;
case DiscordImportFieldType.Languages:
discordField.Name = "Languages";
discordField.Value = message.MovieFile.MediaInfo.AudioLanguages;
discordField.Value = message.MovieFile.MediaInfo.AudioLanguages.ConcatToString("/");
break;
case DiscordImportFieldType.Subtitles:
discordField.Name = "Subtitles";
discordField.Value = message.MovieFile.MediaInfo.Subtitles;
discordField.Value = message.MovieFile.MediaInfo.Subtitles.ConcatToString("/");
break;
case DiscordImportFieldType.Release:
discordField.Name = "Release";

View File

@@ -55,6 +55,35 @@ namespace NzbDrone.Core.Notifications.Emby
}
}
public override void OnMovieDelete(MovieDeleteMessage deleteMessage)
{
if (deleteMessage.DeletedFiles)
{
if (Settings.Notify)
{
_mediaBrowserService.Notify(Settings, MOVIE_DELETED_TITLE_BRANDED, deleteMessage.Message);
}
if (Settings.UpdateLibrary)
{
_mediaBrowserService.UpdateMovies(Settings, deleteMessage.Movie, "Deleted");
}
}
}
public override void OnMovieFileDelete(MovieFileDeleteMessage deleteMessage)
{
if (Settings.Notify)
{
_mediaBrowserService.Notify(Settings, MOVIE_FILE_DELETED_TITLE_BRANDED, deleteMessage.Message);
}
if (Settings.UpdateLibrary)
{
_mediaBrowserService.UpdateMovies(Settings, deleteMessage.Movie, "Deleted");
}
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();

View File

@@ -39,7 +39,7 @@ namespace NzbDrone.Core.Notifications.Emby
[FieldDefinition(4, Label = "Send Notifications", HelpText = "Have MediaBrowser send notfications to configured providers", Type = FieldType.Checkbox)]
public bool Notify { get; set; }
[FieldDefinition(5, Label = "Update Library", HelpText = "Update Library on Import & Rename?", Type = FieldType.Checkbox)]
[FieldDefinition(5, Label = "Update Library", HelpText = "Update Library on Import, Rename or Delete?", Type = FieldType.Checkbox)]
public bool UpdateLibrary { get; set; }
[JsonIgnore]

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -69,6 +70,31 @@ namespace NzbDrone.Core.Organizer
private static readonly Regex ReservedDeviceNamesRegex = new Regex(@"^(?:aux|com[1-9]|con|lpt[1-9]|nul|prn)\.", RegexOptions.Compiled | RegexOptions.IgnoreCase);
// generated from https://www.loc.gov/standards/iso639-2/ISO-639-2_utf-8.txt
public static readonly ImmutableDictionary<string, string> Iso639BTMap = new Dictionary<string, string>
{
{ "alb", "sqi" },
{ "arm", "hye" },
{ "baq", "eus" },
{ "bur", "mya" },
{ "chi", "zho" },
{ "cze", "ces" },
{ "dut", "nld" },
{ "fre", "fra" },
{ "geo", "kat" },
{ "ger", "deu" },
{ "gre", "ell" },
{ "ice", "isl" },
{ "mac", "mkd" },
{ "mao", "mri" },
{ "may", "msa" },
{ "per", "fas" },
{ "rum", "ron" },
{ "slo", "slk" },
{ "tib", "bod" },
{ "wel", "cym" }
}.ToImmutableDictionary();
public FileNameBuilder(INamingConfigService namingConfigService,
IQualityDefinitionService qualityDefinitionService,
IUpdateMediaInfo mediaInfoUpdater,
@@ -344,8 +370,8 @@ namespace NzbDrone.Core.Organizer
var videoCodec = MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName);
var audioCodec = MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName);
var audioChannels = MediaInfoFormatter.FormatAudioChannels(movieFile.MediaInfo);
var audioLanguages = movieFile.MediaInfo.AudioLanguages ?? string.Empty;
var subtitles = movieFile.MediaInfo.Subtitles ?? string.Empty;
var audioLanguages = movieFile.MediaInfo.AudioLanguages ?? new List<string>();
var subtitles = movieFile.MediaInfo.Subtitles ?? new List<string>();
var mediaInfoAudioLanguages = GetLanguagesToken(audioLanguages);
if (!mediaInfoAudioLanguages.IsNullOrWhiteSpace())
@@ -365,7 +391,7 @@ namespace NzbDrone.Core.Organizer
mediaInfoSubtitleLanguages = $"[{mediaInfoSubtitleLanguages}]";
}
var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : string.Empty;
var videoBitDepth = movieFile.MediaInfo.VideoBitDepth > 0 ? movieFile.MediaInfo.VideoBitDepth.ToString() : 8.ToString();
var audioChannelsFormatted = audioChannels > 0 ?
audioChannels.ToString("F1", CultureInfo.InvariantCulture) :
string.Empty;
@@ -405,42 +431,29 @@ namespace NzbDrone.Core.Organizer
tokenHandlers["{Custom Formats}"] = m => string.Join(" ", customFormats.Where(x => x.IncludeCustomFormatWhenRenaming));
}
private string GetLanguagesToken(string mediaInfoLanguages)
private string GetLanguagesToken(List<string> mediaInfoLanguages)
{
List<string> tokens = new List<string>();
foreach (var item in mediaInfoLanguages.Split('/'))
var tokens = new List<string>();
foreach (var item in mediaInfoLanguages)
{
if (!string.IsNullOrWhiteSpace(item))
if (!string.IsNullOrWhiteSpace(item) && item != "und")
{
tokens.Add(item.Trim());
}
}
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')
tokens[i] = "SV";
continue;
}
if (tokens[i] == "Chinese" && OsInfo.IsNotWindows)
{
// Mono only has 'Chinese (Simplified)' & 'Chinese (Traditional)'
tokens[i] = "ZH";
continue;
}
try
{
var cultureInfo = cultures.FirstOrDefault(p => p.EnglishName.RemoveAccent() == tokens[i]);
if (cultureInfo != null)
var token = tokens[i].ToLowerInvariant();
if (Iso639BTMap.TryGetValue(token, out var mapped))
{
tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper();
token = mapped;
}
var cultureInfo = new CultureInfo(token);
tokens[i] = cultureInfo.TwoLetterISOLanguageName.ToUpper();
}
catch
{

View File

@@ -30,13 +30,13 @@ namespace NzbDrone.Core.Organizer
VideoFormat = "AVC",
VideoBitDepth = 10,
VideoMultiViewCount = 2,
VideoColourPrimaries = "BT.2020",
VideoColourPrimaries = "bt2020",
VideoTransferCharacteristics = "HLG",
AudioFormat = "DTS",
AudioChannelsContainer = 6,
AudioChannelPositions = "3/2/0.1",
AudioLanguages = "German",
Subtitles = "English/German"
AudioChannels = 6,
AudioChannelPositions = "5.1",
AudioLanguages = new List<string> { "ger" },
Subtitles = new List<string> { "eng", "ger" }
};
_movieFile = new MovieFile

View File

@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices.ComTypes;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Organizer;
namespace NzbDrone.Core.Parser
{
@@ -62,6 +64,11 @@ namespace NzbDrone.Core.Parser
else if (langCode.Length == 3)
{
//Lookup ISO639-2T code
if (FileNameBuilder.Iso639BTMap.TryGetValue(langCode, out var mapped))
{
langCode = mapped;
}
return All.FirstOrDefault(l => l.ThreeLetterCode == langCode);
}

View File

@@ -91,7 +91,7 @@ namespace NzbDrone.Core.Parser
private static readonly Regex ReportImdbId = new Regex(@"(?<imdbid>tt\d{7,8})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex ReportTmdbId = new Regex(@"tmdb(id)?-(?<tmdbid>\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"\s*(?:480[ip]|576[ip]|720[ip]|1080[ip]|2160[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*|]|848x480|1280x720|1920x1080|(8|10)b(it)?)",
private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"\s*(?:480[ip]|576[ip]|720[ip]|1080[ip]|2160[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -105,7 +105,7 @@ namespace NzbDrone.Core.Parser
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly RegexReplace CleanReleaseGroupRegex = new RegexReplace(@"(-(RP|1|NZBGeek|Obfuscated|Obfuscation|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen))+$",
private static readonly RegexReplace CleanReleaseGroupRegex = new RegexReplace(@"(-(RP|1|NZBGeek|Obfuscated|Obfuscation|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen|RePACKPOST))+$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.Compiled);

View File

@@ -5,12 +5,13 @@
<ItemGroup>
<PackageReference Include="Dapper" Version="2.0.90" />
<PackageReference Include="MailKit" Version="2.15.0" />
<PackageReference Include="Servarr.FFprobe" Version="4.4.1.41" />
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
<PackageReference Include="FluentMigrator.Runner" Version="4.0.0-alpha.268" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="4.0.0-alpha.268" />
<PackageReference Include="FluentMigrator.Runner" Version="3.3.1" />
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.1" />
<PackageReference Include="FluentValidation" Version="8.6.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
@@ -19,6 +20,7 @@
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.113.0-0" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="MonoTorrent" Version="1.0.29" />
<PackageReference Include="FFMpegCore" Version="4.6.16" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NzbDrone.Common\Radarr.Common.csproj" />

View File

@@ -8,8 +8,10 @@ using System.Security.Cryptography.X509Certificates;
using System.Text;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.WindowsServices;
using NLog;
@@ -131,6 +133,7 @@ namespace Radarr.Host
})
.ConfigureWebHost(builder =>
{
builder.UseConfiguration(config);
builder.UseUrls(urls.ToArray());
builder.UseKestrel(options =>
{
@@ -186,6 +189,7 @@ namespace Radarr.Host
var appFolder = new AppFolderInfo(context);
return new ConfigurationBuilder()
.AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false)
.AddInMemoryCollection(new List<KeyValuePair<string, string>> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) })
.Build();
}

View File

@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
@@ -46,6 +48,7 @@ namespace NzbDrone.Host
b.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
b.AddFilter("Microsoft.AspNetCore", Microsoft.Extensions.Logging.LogLevel.Warning);
b.AddFilter("Radarr.Http.Authentication", LogLevel.Information);
b.AddFilter("Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager", LogLevel.Error);
b.AddNLog();
});
@@ -95,6 +98,9 @@ namespace NzbDrone.Host
options.PayloadSerializerOptions = STJson.GetSerializerSettings();
});
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"]));
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
services.AddAuthorization(options =>
{

View File

@@ -128,10 +128,11 @@ namespace NzbDrone.Update.UpdateEngine
_logger.Info("Copying new files to target folder");
_diskTransferService.MirrorFolder(_appFolderInfo.GetUpdatePackageFolder(), installationFolder);
// Set executable flag on app
// Set executable flag on app and ffprobe
if (OsInfo.IsOsx || (OsInfo.IsLinux && PlatformInfo.IsNetCore))
{
_diskProvider.SetFilePermissions(Path.Combine(installationFolder, "Radarr"), "755", null);
_diskProvider.SetFilePermissions(Path.Combine(installationFolder, "ffprobe"), "755", null);
}
}
catch (Exception e)

View File

@@ -10,7 +10,7 @@ namespace Radarr.Api.V3.Config
public abstract class ConfigController<TResource> : RestController<TResource>
where TResource : RestResource, new()
{
private readonly IConfigService _configService;
protected readonly IConfigService _configService;
protected ConfigController(IConfigService configService)
{
@@ -32,7 +32,7 @@ namespace Radarr.Api.V3.Config
}
[RestPutById]
public ActionResult<TResource> SaveConfig(TResource resource)
public virtual ActionResult<TResource> SaveConfig(TResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)

View File

@@ -86,7 +86,7 @@ namespace Radarr.Api.V3.Config
[HttpGet]
public HostConfigResource GetHostConfig()
{
var resource = _configFileProvider.ToResource(_configService);
var resource = HostConfigResourceMapper.ToResource(_configFileProvider, _configService);
resource.Id = 1;
var user = _userService.FindUser();

View File

@@ -1,19 +1,39 @@
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Configuration;
using Radarr.Http;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V3.Config
{
[V3ApiController("config/ui")]
public class UiConfigController : ConfigController<UiConfigResource>
{
public UiConfigController(IConfigService configService)
private readonly IConfigFileProvider _configFileProvider;
public UiConfigController(IConfigService configService, IConfigFileProvider configFileProvider)
: base(configService)
{
_configFileProvider = configFileProvider;
}
protected override UiConfigResource ToResource(IConfigService model)
{
return UiConfigResourceMapper.ToResource(model);
return UiConfigResourceMapper.ToResource(_configFileProvider, model);
}
[RestPutById]
public override ActionResult<UiConfigResource> SaveConfig(UiConfigResource resource)
{
var dictionary = resource.GetType()
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null));
_configFileProvider.SaveConfigDictionary(dictionary);
_configService.SaveConfigDictionary(dictionary);
return Accepted(resource.Id);
}
}
}

View File

@@ -21,27 +21,29 @@ namespace Radarr.Api.V3.Config
public bool EnableColorImpairedMode { get; set; }
public int MovieInfoLanguage { get; set; }
public int UILanguage { get; set; }
public string Theme { get; set; }
}
public static class UiConfigResourceMapper
{
public static UiConfigResource ToResource(IConfigService model)
public static UiConfigResource ToResource(this IConfigFileProvider model, IConfigService configService)
{
return new UiConfigResource
{
FirstDayOfWeek = model.FirstDayOfWeek,
CalendarWeekColumnHeader = model.CalendarWeekColumnHeader,
FirstDayOfWeek = configService.FirstDayOfWeek,
CalendarWeekColumnHeader = configService.CalendarWeekColumnHeader,
MovieRuntimeFormat = model.MovieRuntimeFormat,
MovieRuntimeFormat = configService.MovieRuntimeFormat,
ShortDateFormat = model.ShortDateFormat,
LongDateFormat = model.LongDateFormat,
TimeFormat = model.TimeFormat,
ShowRelativeDates = model.ShowRelativeDates,
ShortDateFormat = configService.ShortDateFormat,
LongDateFormat = configService.LongDateFormat,
TimeFormat = configService.TimeFormat,
ShowRelativeDates = configService.ShowRelativeDates,
EnableColorImpairedMode = model.EnableColorImpairedMode,
MovieInfoLanguage = model.MovieInfoLanguage,
UILanguage = model.UILanguage
EnableColorImpairedMode = configService.EnableColorImpairedMode,
MovieInfoLanguage = configService.MovieInfoLanguage,
UILanguage = configService.UILanguage,
Theme = model.Theme
};
}
}

View File

@@ -94,9 +94,8 @@ namespace Radarr.Api.V3.History
return _historyService.GetByMovieId(movieId, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
}
// v4 TODO: Getting the ID from the form is atypical, consider removing.
[HttpPost("failed")]
public object MarkAsFailed([FromBody] int id)
[HttpPost("failed/{id}")]
public object MarkAsFailed([FromRoute] int id)
{
_failedDownloadService.MarkAsFailed(id);
return new object();

View File

@@ -1,4 +1,5 @@
using System;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.MediaFiles.MediaInfo;
using Radarr.Http.REST;
@@ -6,7 +7,6 @@ namespace Radarr.Api.V3.MovieFiles
{
public class MediaInfoResource : RestResource
{
public string AudioAdditionalFeatures { get; set; }
public int AudioBitrate { get; set; }
public decimal AudioChannels { get; set; }
public string AudioCodec { get; set; }
@@ -33,20 +33,19 @@ namespace Radarr.Api.V3.MovieFiles
return new MediaInfoResource
{
AudioAdditionalFeatures = model.AudioAdditionalFeatures,
AudioBitrate = model.AudioBitrate,
AudioChannels = MediaInfoFormatter.FormatAudioChannels(model),
AudioLanguages = model.AudioLanguages,
AudioLanguages = model.AudioLanguages.ConcatToString("/"),
AudioStreamCount = model.AudioStreamCount,
AudioCodec = MediaInfoFormatter.FormatAudioCodec(model, sceneName),
VideoBitDepth = model.VideoBitDepth,
VideoBitrate = model.VideoBitrate,
VideoCodec = MediaInfoFormatter.FormatVideoCodec(model, sceneName),
VideoFps = model.VideoFps,
VideoFps = Math.Round(model.VideoFps, 3),
Resolution = $"{model.Width}x{model.Height}",
RunTime = FormatRuntime(model.RunTime),
ScanType = model.ScanType,
Subtitles = model.Subtitles
Subtitles = model.Subtitles.ConcatToString("/")
};
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Download;
@@ -33,6 +34,7 @@ namespace Radarr.Api.V3.Queue
private readonly IFailedDownloadService _failedDownloadService;
private readonly IIgnoredDownloadService _ignoredDownloadService;
private readonly IProvideDownloadClient _downloadClientProvider;
private readonly IBlocklistService _blocklistService;
public QueueController(IBroadcastSignalRMessage broadcastSignalRMessage,
IQueueService queueService,
@@ -41,7 +43,8 @@ namespace Radarr.Api.V3.Queue
ITrackedDownloadService trackedDownloadService,
IFailedDownloadService failedDownloadService,
IIgnoredDownloadService ignoredDownloadService,
IProvideDownloadClient downloadClientProvider)
IProvideDownloadClient downloadClientProvider,
IBlocklistService blocklistService)
: base(broadcastSignalRMessage)
{
_queueService = queueService;
@@ -50,6 +53,7 @@ namespace Radarr.Api.V3.Queue
_failedDownloadService = failedDownloadService;
_ignoredDownloadService = ignoredDownloadService;
_downloadClientProvider = downloadClientProvider;
_blocklistService = blocklistService;
_qualityComparer = new QualityModelComparer(qualityProfileService.GetDefaultProfile(string.Empty));
}
@@ -200,6 +204,7 @@ namespace Radarr.Api.V3.Queue
if (pendingRelease != null)
{
_blocklistService.Block(pendingRelease.RemoteMovie, "Pending release manually blocklisted");
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
return null;

View File

@@ -46,6 +46,7 @@ namespace Radarr.Http.Frontend
builder.AppendLine("window.Radarr = {");
builder.AppendLine($" apiRoot: '{_urlBase}/api/v3',");
builder.AppendLine($" apiKey: '{_apiKey}',");
builder.AppendLine($" theme: '{_configFileProvider.Theme}',");
builder.AppendLine($" release: '{BuildInfo.Release}',");
builder.AppendLine($" version: '{BuildInfo.Version.ToString()}',");
builder.AppendLine($" branch: '{_configFileProvider.Branch.ToLower()}',");

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<dllmap os="osx" dll="mediainfo" target="libmediainfo.dylib"/>
<dllmap os="linux" dll="mediainfo" target="libmediainfo.so.0" />
<dllmap os="freebsd" dll="mediainfo" target="libmediainfo.so.0" />
<dllmap os="solaris" dll="mediainfo" target="libmediainfo.so.0.0.0" />
</configuration>

Binary file not shown.

Binary file not shown.