{
- split(required).map((item) => {
+ required.map((item) => {
if (!item) {
return null;
}
return (
);
})
@@ -101,34 +105,22 @@ class ReleaseProfile extends Component {
{
- preferred.map((item) => {
- const isPreferred = item.value >= 0;
-
- return (
-
- );
- })
- }
-
-
-
- {
- split(ignored).map((item) => {
+ ignored.map((item) => {
if (!item) {
return null;
}
return (
);
})
@@ -186,9 +178,8 @@ class ReleaseProfile extends Component {
ReleaseProfile.propTypes = {
id: PropTypes.number.isRequired,
enabled: PropTypes.bool.isRequired,
- required: PropTypes.string.isRequired,
- ignored: PropTypes.string.isRequired,
- preferred: PropTypes.arrayOf(PropTypes.object).isRequired,
+ required: PropTypes.arrayOf(PropTypes.string).isRequired,
+ ignored: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerId: PropTypes.number.isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -198,9 +189,8 @@ ReleaseProfile.propTypes = {
ReleaseProfile.defaultProps = {
enabled: true,
- required: '',
- ignored: '',
- preferred: [],
+ required: [],
+ ignored: [],
indexerId: 0
};
diff --git a/frontend/src/Settings/Settings.js b/frontend/src/Settings/Settings.js
index 085acbd64..36f47d82e 100644
--- a/frontend/src/Settings/Settings.js
+++ b/frontend/src/Settings/Settings.js
@@ -47,6 +47,17 @@ function Settings() {
Quality sizes and naming
+
+ Custom Formats
+
+
+
+ Custom Formats and Settings
+
+
{
+ return {
+ section,
+ ...payload
+ };
+});
+
+export const setCustomFormatSpecificationFieldValue = createAction(SET_CUSTOM_FORMAT_SPECIFICATION_FIELD_VALUE, (payload) => {
+ return {
+ section,
+ ...payload
+ };
+});
+
+export const cloneCustomFormatSpecification = createAction(CLONE_CUSTOM_FORMAT_SPECIFICATION);
+
+export const clearCustomFormatSpecification = createAction(CLEAR_CUSTOM_FORMAT_SPECIFICATIONS);
+
+export const clearCustomFormatSpecificationPending = createThunk(CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING);
+
+//
+// Details
+
+export default {
+
+ //
+ // State
+
+ defaultState: {
+ isPopulated: false,
+ error: null,
+ isSchemaFetching: false,
+ isSchemaPopulated: false,
+ schemaError: null,
+ schema: [],
+ selectedSchema: {},
+ isSaving: false,
+ saveError: null,
+ items: [],
+ pendingChanges: {}
+ },
+
+ //
+ // Action Handlers
+
+ actionHandlers: {
+ [FETCH_CUSTOM_FORMAT_SPECIFICATION_SCHEMA]: createFetchSchemaHandler(section, '/customformat/schema'),
+
+ [FETCH_CUSTOM_FORMAT_SPECIFICATIONS]: (getState, payload, dispatch) => {
+ let tags = [];
+ if (payload.id) {
+ const cfState = getSectionState(getState(), 'settings.customFormats', true);
+ const cf = cfState.items[cfState.itemMap[payload.id]];
+ tags = cf.specifications.map((tag, i) => {
+ return {
+ id: i + 1,
+ ...tag
+ };
+ });
+ }
+
+ dispatch(batchActions([
+ update({ section, data: tags }),
+ set({
+ section,
+ isPopulated: true
+ })
+ ]));
+ },
+
+ [SAVE_CUSTOM_FORMAT_SPECIFICATION]: (getState, payload, dispatch) => {
+ const {
+ id,
+ ...otherPayload
+ } = payload;
+
+ const saveData = getProviderState({ id, ...otherPayload }, getState, section, false);
+
+ // we have to set id since not actually posting to server yet
+ if (!saveData.id) {
+ saveData.id = getNextId(getState().settings.customFormatSpecifications.items);
+ }
+
+ dispatch(batchActions([
+ updateItem({ section, ...saveData }),
+ set({
+ section,
+ pendingChanges: {}
+ })
+ ]));
+ },
+
+ [DELETE_CUSTOM_FORMAT_SPECIFICATION]: (getState, payload, dispatch) => {
+ const id = payload.id;
+ return dispatch(removeItem({ section, id }));
+ },
+
+ [DELETE_ALL_CUSTOM_FORMAT_SPECIFICATION]: (getState, payload, dispatch) => {
+ return dispatch(set({
+ section,
+ items: []
+ }));
+ },
+
+ [CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING]: (getState, payload, dispatch) => {
+ return dispatch(set({
+ section,
+ pendingChanges: {}
+ }));
+ }
+ },
+
+ //
+ // Reducers
+
+ reducers: {
+ [SET_CUSTOM_FORMAT_SPECIFICATION_VALUE]: createSetSettingValueReducer(section),
+ [SET_CUSTOM_FORMAT_SPECIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
+
+ [SELECT_CUSTOM_FORMAT_SPECIFICATION_SCHEMA]: (state, { payload }) => {
+ return selectProviderSchema(state, section, payload, (selectedSchema) => {
+ return selectedSchema;
+ });
+ },
+
+ [CLONE_CUSTOM_FORMAT_SPECIFICATION]: function(state, { payload }) {
+ const id = payload.id;
+ const newState = getSectionState(state, section);
+ const items = newState.items;
+ const item = items.find((i) => i.id === id);
+ const newId = getNextId(newState.items);
+ const newItem = {
+ ...item,
+ id: newId,
+ name: `${item.name} - Copy`
+ };
+ newState.items = [...items, newItem];
+ newState.itemMap[newId] = newState.items.length - 1;
+
+ return updateSectionState(state, section, newState);
+ },
+
+ [CLEAR_CUSTOM_FORMAT_SPECIFICATIONS]: createClearReducer(section, {
+ isPopulated: false,
+ error: null,
+ items: []
+ })
+ }
+};
diff --git a/frontend/src/Store/Actions/Settings/customFormats.js b/frontend/src/Store/Actions/Settings/customFormats.js
new file mode 100644
index 000000000..4a175abea
--- /dev/null
+++ b/frontend/src/Store/Actions/Settings/customFormats.js
@@ -0,0 +1,108 @@
+import { createAction } from 'redux-actions';
+import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
+import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
+import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
+import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
+import { createThunk } from 'Store/thunks';
+import getSectionState from 'Utilities/State/getSectionState';
+import updateSectionState from 'Utilities/State/updateSectionState';
+import { set } from '../baseActions';
+
+//
+// Variables
+
+const section = 'settings.customFormats';
+
+//
+// Actions Types
+
+export const FETCH_CUSTOM_FORMATS = 'settings/customFormats/fetchCustomFormats';
+export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
+export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
+export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
+export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
+
+//
+// Action Creators
+
+export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
+export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
+export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
+
+export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
+ return {
+ section,
+ ...payload
+ };
+});
+
+export const cloneCustomFormat = createAction(CLONE_CUSTOM_FORMAT);
+
+//
+// Details
+
+export default {
+
+ //
+ // State
+
+ defaultState: {
+ isSchemaFetching: false,
+ isSchemaPopulated: false,
+ isFetching: false,
+ isPopulated: false,
+ schema: {
+ includeCustomFormatWhenRenaming: false
+ },
+ error: null,
+ isDeleting: false,
+ deleteError: null,
+ isSaving: false,
+ saveError: null,
+ items: [],
+ pendingChanges: {}
+ },
+
+ //
+ // Action Handlers
+
+ actionHandlers: {
+ [FETCH_CUSTOM_FORMATS]: createFetchHandler(section, '/customformat'),
+
+ [DELETE_CUSTOM_FORMAT]: createRemoveItemHandler(section, '/customformat'),
+
+ [SAVE_CUSTOM_FORMAT]: (getState, payload, dispatch) => {
+ // move the format tags in as a pending change
+ const state = getState();
+ const pendingChanges = state.settings.customFormats.pendingChanges;
+ pendingChanges.specifications = state.settings.customFormatSpecifications.items;
+ dispatch(set({
+ section,
+ pendingChanges
+ }));
+
+ createSaveProviderHandler(section, '/customformat')(getState, payload, dispatch);
+ }
+ },
+
+ //
+ // Reducers
+
+ reducers: {
+ [SET_CUSTOM_FORMAT_VALUE]: createSetSettingValueReducer(section),
+
+ [CLONE_CUSTOM_FORMAT]: function(state, { payload }) {
+ const id = payload.id;
+ const newState = getSectionState(state, section);
+ const item = newState.items.find((i) => i.id === id);
+ const pendingChanges = { ...item, id: 0 };
+ delete pendingChanges.id;
+
+ pendingChanges.name = `${pendingChanges.name} - Copy`;
+ newState.pendingChanges = pendingChanges;
+
+ return updateSectionState(state, section, newState);
+ }
+ }
+
+};
diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js
index b529f10c6..06d870983 100644
--- a/frontend/src/Store/Actions/blocklistActions.js
+++ b/frontend/src/Store/Actions/blocklistActions.js
@@ -48,6 +48,12 @@ export const defaultState = {
label: translate('Quality'),
isVisible: true
},
+ {
+ name: 'customFormats',
+ label: 'Formats',
+ isSortable: false,
+ isVisible: true
+ },
{
name: 'date',
label: translate('Date'),
diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js
index 7bfdb5843..86e4c984e 100644
--- a/frontend/src/Store/Actions/historyActions.js
+++ b/frontend/src/Store/Actions/historyActions.js
@@ -1,5 +1,7 @@
+import React from 'react';
import { createAction } from 'redux-actions';
-import { filterTypes, sortDirections } from 'Helpers/Props';
+import Icon from 'Components/Icon';
+import { filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@@ -56,6 +58,12 @@ export const defaultState = {
label: translate('Quality'),
isVisible: true
},
+ {
+ name: 'customFormats',
+ label: 'Formats',
+ isSortable: false,
+ isVisible: true
+ },
{
name: 'date',
label: translate('Date'),
@@ -82,6 +90,20 @@ export const defaultState = {
label: translate('SourceTitle'),
isVisible: false
},
+ {
+ name: 'sourceTitle',
+ label: 'Source Title',
+ isVisible: false
+ },
+ {
+ name: 'customFormatScore',
+ columnLabel: 'Custom Format Score',
+ label: React.createElement(Icon, {
+ name: icons.SCORE,
+ title: 'Custom format score'
+ }),
+ isVisible: false
+ },
{
name: 'details',
columnLabel: translate('Details'),
diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js
index 4449f24d4..8328ea079 100644
--- a/frontend/src/Store/Actions/queueActions.js
+++ b/frontend/src/Store/Actions/queueActions.js
@@ -87,6 +87,12 @@ export const defaultState = {
isSortable: true,
isVisible: true
},
+ {
+ name: 'customFormats',
+ label: 'Formats',
+ isSortable: false,
+ isVisible: true
+ },
{
name: 'protocol',
label: translate('Protocol'),
diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js
index 582f6e361..c4729368a 100644
--- a/frontend/src/Store/Actions/releaseActions.js
+++ b/frontend/src/Store/Actions/releaseActions.js
@@ -197,6 +197,11 @@ export const defaultState = {
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.QUALITY
},
+ {
+ name: 'customFormatScore',
+ label: translate('CustomFormatScore'),
+ type: filterBuilderTypes.NUMBER
+ },
{
name: 'rejectionCount',
label: translate('RejectionCount'),
diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js
index 3e28acaff..a8af15174 100644
--- a/frontend/src/Store/Actions/settingsActions.js
+++ b/frontend/src/Store/Actions/settingsActions.js
@@ -1,6 +1,8 @@
import { createAction } from 'redux-actions';
import { handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
+import customFormats from './Settings/customFormats';
+import customFormatSpecifications from './Settings/customFormatSpecifications';
import delayProfiles from './Settings/delayProfiles';
import downloadClientOptions from './Settings/downloadClientOptions';
import downloadClients from './Settings/downloadClients';
@@ -24,6 +26,8 @@ import remotePathMappings from './Settings/remotePathMappings';
import rootFolders from './Settings/rootFolders';
import ui from './Settings/ui';
+export * from './Settings/customFormatSpecifications.js';
+export * from './Settings/customFormats';
export * from './Settings/delayProfiles';
export * from './Settings/downloadClients';
export * from './Settings/downloadClientOptions';
@@ -58,6 +62,8 @@ export const section = 'settings';
export const defaultState = {
advancedSettings: false,
+ customFormatSpecifications: customFormatSpecifications.defaultState,
+ customFormats: customFormats.defaultState,
delayProfiles: delayProfiles.defaultState,
downloadClients: downloadClients.defaultState,
downloadClientOptions: downloadClientOptions.defaultState,
@@ -100,6 +106,8 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
// Action Handlers
export const actionHandlers = handleThunks({
+ ...customFormatSpecifications.actionHandlers,
+ ...customFormats.actionHandlers,
...delayProfiles.actionHandlers,
...downloadClients.actionHandlers,
...downloadClientOptions.actionHandlers,
@@ -133,6 +141,8 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
},
+ ...customFormatSpecifications.reducers,
+ ...customFormats.reducers,
...delayProfiles.reducers,
...downloadClients.reducers,
...downloadClientOptions.reducers,
diff --git a/frontend/src/Utilities/Number/formatPreferredWordScore.js b/frontend/src/Utilities/Number/formatPreferredWordScore.js
new file mode 100644
index 000000000..5869f0052
--- /dev/null
+++ b/frontend/src/Utilities/Number/formatPreferredWordScore.js
@@ -0,0 +1,16 @@
+
+function formatPreferredWordScore(input, customFormatsLength = 0) {
+ const score = Number(input);
+
+ if (score > 0) {
+ return `+${score}`;
+ }
+
+ if (score < 0) {
+ return score;
+ }
+
+ return customFormatsLength > 0 ? '+0' : '';
+}
+
+export default formatPreferredWordScore;
diff --git a/frontend/src/Utilities/State/getNextId.js b/frontend/src/Utilities/State/getNextId.js
new file mode 100644
index 000000000..204aac95a
--- /dev/null
+++ b/frontend/src/Utilities/State/getNextId.js
@@ -0,0 +1,5 @@
+function getNextId(items) {
+ return items.reduce((id, x) => Math.max(id, x.id), 1) + 1;
+}
+
+export default getNextId;
diff --git a/frontend/src/Utilities/State/getProviderState.js b/frontend/src/Utilities/State/getProviderState.js
index 60923a646..4159905b8 100644
--- a/frontend/src/Utilities/State/getProviderState.js
+++ b/frontend/src/Utilities/State/getProviderState.js
@@ -1,7 +1,7 @@
import _ from 'lodash';
import getSectionState from 'Utilities/State/getSectionState';
-function getProviderState(payload, getState, section) {
+function getProviderState(payload, getState, section, keyValueOnly=true) {
const {
id,
...otherPayload
@@ -23,10 +23,17 @@ function getProviderState(payload, getState, section) {
field.value;
// Only send the name and value to the server
- result.push({
- name,
- value
- });
+ if (keyValueOnly) {
+ result.push({
+ name,
+ value
+ });
+ } else {
+ result.push({
+ ...field,
+ value
+ });
+ }
return result;
}, []);
diff --git a/package.json b/package.json
index d1ebbe695..9d9b8dcea 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"react-google-recaptcha": "2.1.0",
"react-lazyload": "3.2.0",
"react-measure": "1.4.7",
+ "react-middle-truncate": "1.0.3",
"react-popper": "1.3.7",
"react-redux": "7.2.4",
"react-router": "5.2.0",
diff --git a/src/Lidarr.Api.V1/Blocklist/BlocklistController.cs b/src/Lidarr.Api.V1/Blocklist/BlocklistController.cs
index a924dba33..181645735 100644
--- a/src/Lidarr.Api.V1/Blocklist/BlocklistController.cs
+++ b/src/Lidarr.Api.V1/Blocklist/BlocklistController.cs
@@ -3,6 +3,7 @@ using Lidarr.Http.Extensions;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Core.Blocklisting;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
namespace Lidarr.Api.V1.Blocklist
@@ -11,10 +12,13 @@ namespace Lidarr.Api.V1.Blocklist
public class BlocklistController : Controller
{
private readonly IBlocklistService _blocklistService;
+ private readonly ICustomFormatCalculationService _formatCalculator;
- public BlocklistController(IBlocklistService blocklistService)
+ public BlocklistController(IBlocklistService blocklistService,
+ ICustomFormatCalculationService formatCalculator)
{
_blocklistService = blocklistService;
+ _formatCalculator = formatCalculator;
}
[HttpGet]
@@ -23,7 +27,7 @@ namespace Lidarr.Api.V1.Blocklist
var pagingResource = Request.ReadPagingResourceFromRequest
();
var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending);
- return pagingSpec.ApplyToPage(_blocklistService.Paged, BlocklistResourceMapper.MapToResource);
+ return pagingSpec.ApplyToPage(_blocklistService.Paged, model => BlocklistResourceMapper.MapToResource(model, _formatCalculator));
}
[RestDeleteById]
diff --git a/src/Lidarr.Api.V1/Blocklist/BlocklistResource.cs b/src/Lidarr.Api.V1/Blocklist/BlocklistResource.cs
index c81eb0075..ac906b701 100644
--- a/src/Lidarr.Api.V1/Blocklist/BlocklistResource.cs
+++ b/src/Lidarr.Api.V1/Blocklist/BlocklistResource.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using Lidarr.Api.V1.Artist;
+using Lidarr.Api.V1.CustomFormats;
using Lidarr.Http.REST;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Qualities;
@@ -13,6 +15,7 @@ namespace Lidarr.Api.V1.Blocklist
public List AlbumIds { get; set; }
public string SourceTitle { get; set; }
public QualityModel Quality { get; set; }
+ public List CustomFormats { get; set; }
public DateTime Date { get; set; }
public DownloadProtocol Protocol { get; set; }
public string Indexer { get; set; }
@@ -23,7 +26,7 @@ namespace Lidarr.Api.V1.Blocklist
public static class BlocklistResourceMapper
{
- public static BlocklistResource MapToResource(this NzbDrone.Core.Blocklisting.Blocklist model)
+ public static BlocklistResource MapToResource(this NzbDrone.Core.Blocklisting.Blocklist model, ICustomFormatCalculationService formatCalculator)
{
if (model == null)
{
@@ -38,6 +41,7 @@ namespace Lidarr.Api.V1.Blocklist
AlbumIds = model.AlbumIds,
SourceTitle = model.SourceTitle,
Quality = model.Quality,
+ CustomFormats = formatCalculator.ParseCustomFormat(model, model.Artist).ToResource(false),
Date = model.Date,
Protocol = model.Protocol,
Indexer = model.Indexer,
diff --git a/src/Lidarr.Api.V1/CustomFormats/CustomFormatController.cs b/src/Lidarr.Api.V1/CustomFormats/CustomFormatController.cs
new file mode 100644
index 000000000..c152ca115
--- /dev/null
+++ b/src/Lidarr.Api.V1/CustomFormats/CustomFormatController.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using System.Linq;
+using FluentValidation;
+using Lidarr.Api.V1.CustomFormats;
+using Lidarr.Http;
+using Lidarr.Http.REST;
+using Lidarr.Http.REST.Attributes;
+using Microsoft.AspNetCore.Mvc;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Core.CustomFormats;
+
+namespace Sonarr.Api.V3.CustomFormats
+{
+ [V1ApiController]
+ public class CustomFormatController : RestController
+ {
+ private readonly ICustomFormatService _formatService;
+ private readonly List _specifications;
+
+ public CustomFormatController(ICustomFormatService formatService,
+ List specifications)
+ {
+ _formatService = formatService;
+ _specifications = specifications;
+
+ SharedValidator.RuleFor(c => c.Name).NotEmpty();
+ SharedValidator.RuleFor(c => c.Name)
+ .Must((v, c) => !_formatService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique.");
+ SharedValidator.RuleFor(c => c.Specifications).NotEmpty();
+ SharedValidator.RuleFor(c => c).Custom((customFormat, context) =>
+ {
+ if (!customFormat.Specifications.Any())
+ {
+ context.AddFailure("Must contain at least one Condition");
+ }
+
+ if (customFormat.Specifications.Any(s => s.Name.IsNullOrWhiteSpace()))
+ {
+ context.AddFailure("Condition name(s) cannot be empty or consist of only spaces");
+ }
+ });
+ }
+
+ public override CustomFormatResource GetResourceById(int id)
+ {
+ return _formatService.GetById(id).ToResource(true);
+ }
+
+ [RestPostById]
+ [Consumes("application/json")]
+ public ActionResult Create(CustomFormatResource customFormatResource)
+ {
+ var model = customFormatResource.ToModel(_specifications);
+ return Created(_formatService.Insert(model).Id);
+ }
+
+ [RestPutById]
+ [Consumes("application/json")]
+ public ActionResult Update(CustomFormatResource resource)
+ {
+ var model = resource.ToModel(_specifications);
+ _formatService.Update(model);
+
+ return Accepted(model.Id);
+ }
+
+ [HttpGet]
+ [Produces("application/json")]
+ public List GetAll()
+ {
+ return _formatService.All().ToResource(true);
+ }
+
+ [RestDeleteById]
+ public void DeleteFormat(int id)
+ {
+ _formatService.Delete(id);
+ }
+
+ [HttpGet("schema")]
+ public object GetTemplates()
+ {
+ var schema = _specifications.OrderBy(x => x.Order).Select(x => x.ToSchema()).ToList();
+
+ var presets = GetPresets();
+
+ foreach (var item in schema)
+ {
+ item.Presets = presets.Where(x => x.GetType().Name == item.Implementation).Select(x => x.ToSchema()).ToList();
+ }
+
+ return schema;
+ }
+
+ private IEnumerable GetPresets()
+ {
+ yield return new ReleaseTitleSpecification
+ {
+ Name = "Preferred Words",
+ Value = @"\b(SPARKS|Framestor)\b"
+ };
+
+ var formats = _formatService.All();
+ foreach (var format in formats)
+ {
+ foreach (var condition in format.Specifications)
+ {
+ var preset = condition.Clone();
+ preset.Name = $"{format.Name}: {preset.Name}";
+ yield return preset;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Lidarr.Api.V1/CustomFormats/CustomFormatResource.cs b/src/Lidarr.Api.V1/CustomFormats/CustomFormatResource.cs
new file mode 100644
index 000000000..c5f5dbee8
--- /dev/null
+++ b/src/Lidarr.Api.V1/CustomFormats/CustomFormatResource.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json.Serialization;
+using Lidarr.Http.ClientSchema;
+using Lidarr.Http.REST;
+using NzbDrone.Core.CustomFormats;
+
+namespace Lidarr.Api.V1.CustomFormats
+{
+ public class CustomFormatResource : RestResource
+ {
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public override int Id { get; set; }
+ public string Name { get; set; }
+ public bool? IncludeCustomFormatWhenRenaming { get; set; }
+ public List Specifications { get; set; }
+ }
+
+ public static class CustomFormatResourceMapper
+ {
+ public static CustomFormatResource ToResource(this CustomFormat model, bool includeDetails)
+ {
+ var resource = new CustomFormatResource
+ {
+ Id = model.Id,
+ Name = model.Name
+ };
+
+ if (includeDetails)
+ {
+ resource.IncludeCustomFormatWhenRenaming = model.IncludeCustomFormatWhenRenaming;
+ resource.Specifications = model.Specifications.Select(x => x.ToSchema()).ToList();
+ }
+
+ return resource;
+ }
+
+ public static List ToResource(this IEnumerable models, bool includeDetails)
+ {
+ return models.Select(m => m.ToResource(includeDetails)).ToList();
+ }
+
+ public static CustomFormat ToModel(this CustomFormatResource resource, List specifications)
+ {
+ return new CustomFormat
+ {
+ Id = resource.Id,
+ Name = resource.Name,
+ IncludeCustomFormatWhenRenaming = resource.IncludeCustomFormatWhenRenaming ?? false,
+ Specifications = resource.Specifications?.Select(x => MapSpecification(x, specifications)).ToList() ?? new List()
+ };
+ }
+
+ private static ICustomFormatSpecification MapSpecification(CustomFormatSpecificationSchema resource, List specifications)
+ {
+ var matchingSpec =
+ specifications.SingleOrDefault(x => x.GetType().Name == resource.Implementation);
+
+ if (matchingSpec is null)
+ {
+ throw new ArgumentException(
+ $"{resource.Implementation} is not a valid specification implementation");
+ }
+
+ var type = matchingSpec.GetType();
+
+ var spec = (ICustomFormatSpecification)SchemaBuilder.ReadFromSchema(resource.Fields, type);
+ spec.Name = resource.Name;
+ spec.Negate = resource.Negate;
+ spec.Required = resource.Required;
+ return spec;
+ }
+ }
+}
diff --git a/src/Lidarr.Api.V1/CustomFormats/CustomFormatSpecificationSchema.cs b/src/Lidarr.Api.V1/CustomFormats/CustomFormatSpecificationSchema.cs
new file mode 100644
index 000000000..8400f34c1
--- /dev/null
+++ b/src/Lidarr.Api.V1/CustomFormats/CustomFormatSpecificationSchema.cs
@@ -0,0 +1,36 @@
+using System.Collections.Generic;
+using Lidarr.Http.ClientSchema;
+using Lidarr.Http.REST;
+using NzbDrone.Core.CustomFormats;
+
+namespace Lidarr.Api.V1.CustomFormats
+{
+ public class CustomFormatSpecificationSchema : RestResource
+ {
+ public string Name { get; set; }
+ public string Implementation { get; set; }
+ public string ImplementationName { get; set; }
+ public string InfoLink { get; set; }
+ public bool Negate { get; set; }
+ public bool Required { get; set; }
+ public List Fields { get; set; }
+ public List Presets { get; set; }
+ }
+
+ public static class CustomFormatSpecificationSchemaMapper
+ {
+ public static CustomFormatSpecificationSchema ToSchema(this ICustomFormatSpecification model)
+ {
+ return new CustomFormatSpecificationSchema
+ {
+ Name = model.Name,
+ Implementation = model.GetType().Name,
+ ImplementationName = model.ImplementationName,
+ InfoLink = model.InfoLink,
+ Negate = model.Negate,
+ Required = model.Required,
+ Fields = SchemaBuilder.ToSchema(model)
+ };
+ }
+ }
+}
diff --git a/src/Lidarr.Api.V1/History/HistoryController.cs b/src/Lidarr.Api.V1/History/HistoryController.cs
index a87d1d8d7..ecc41b7d7 100644
--- a/src/Lidarr.Api.V1/History/HistoryController.cs
+++ b/src/Lidarr.Api.V1/History/HistoryController.cs
@@ -7,6 +7,7 @@ using Lidarr.Api.V1.Tracks;
using Lidarr.Http;
using Lidarr.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download;
@@ -18,21 +19,24 @@ namespace Lidarr.Api.V1.History
public class HistoryController : Controller
{
private readonly IHistoryService _historyService;
+ private readonly ICustomFormatCalculationService _formatCalculator;
private readonly IUpgradableSpecification _upgradableSpecification;
private readonly IFailedDownloadService _failedDownloadService;
public HistoryController(IHistoryService historyService,
+ ICustomFormatCalculationService formatCalculator,
IUpgradableSpecification upgradableSpecification,
IFailedDownloadService failedDownloadService)
{
_historyService = historyService;
+ _formatCalculator = formatCalculator;
_upgradableSpecification = upgradableSpecification;
_failedDownloadService = failedDownloadService;
}
protected HistoryResource MapToResource(EntityHistory model, bool includeArtist, bool includeAlbum, bool includeTrack)
{
- var resource = model.ToResource();
+ var resource = model.ToResource(_formatCalculator);
if (includeArtist)
{
diff --git a/src/Lidarr.Api.V1/History/HistoryResource.cs b/src/Lidarr.Api.V1/History/HistoryResource.cs
index c34ad98e7..2b59e9b9f 100644
--- a/src/Lidarr.Api.V1/History/HistoryResource.cs
+++ b/src/Lidarr.Api.V1/History/HistoryResource.cs
@@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using Lidarr.Api.V1.Albums;
using Lidarr.Api.V1.Artist;
+using Lidarr.Api.V1.CustomFormats;
using Lidarr.Api.V1.Tracks;
using Lidarr.Http.REST;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.History;
using NzbDrone.Core.Qualities;
@@ -16,6 +18,7 @@ namespace Lidarr.Api.V1.History
public int TrackId { get; set; }
public string SourceTitle { get; set; }
public QualityModel Quality { get; set; }
+ public List CustomFormats { get; set; }
public bool QualityCutoffNotMet { get; set; }
public DateTime Date { get; set; }
public string DownloadId { get; set; }
@@ -31,7 +34,7 @@ namespace Lidarr.Api.V1.History
public static class HistoryResourceMapper
{
- public static HistoryResource ToResource(this EntityHistory model)
+ public static HistoryResource ToResource(this EntityHistory model, ICustomFormatCalculationService formatCalculator)
{
if (model == null)
{
@@ -47,6 +50,7 @@ namespace Lidarr.Api.V1.History
TrackId = model.TrackId,
SourceTitle = model.SourceTitle,
Quality = model.Quality,
+ CustomFormats = formatCalculator.ParseCustomFormat(model, model.Artist).ToResource(false),
// QualityCutoffNotMet
Date = model.Date,
diff --git a/src/Lidarr.Api.V1/Indexers/ReleaseResource.cs b/src/Lidarr.Api.V1/Indexers/ReleaseResource.cs
index d9a17c4b1..ca9acd4d1 100644
--- a/src/Lidarr.Api.V1/Indexers/ReleaseResource.cs
+++ b/src/Lidarr.Api.V1/Indexers/ReleaseResource.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
+using Lidarr.Api.V1.CustomFormats;
using Lidarr.Http.REST;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.Indexers;
@@ -40,7 +41,8 @@ namespace Lidarr.Api.V1.Indexers
public string InfoUrl { get; set; }
public bool DownloadAllowed { get; set; }
public int ReleaseWeight { get; set; }
- public int PreferredWordScore { get; set; }
+ public List CustomFormats { get; set; }
+ public int CustomFormatScore { get; set; }
public string MagnetUrl { get; set; }
public string InfoHash { get; set; }
@@ -99,7 +101,8 @@ namespace Lidarr.Api.V1.Indexers
DownloadAllowed = remoteAlbum.DownloadAllowed,
// ReleaseWeight
- PreferredWordScore = remoteAlbum.PreferredWordScore,
+ CustomFormatScore = remoteAlbum.CustomFormatScore,
+ CustomFormats = remoteAlbum.CustomFormats.ToResource(false),
MagnetUrl = torrentInfo.MagnetUrl,
InfoHash = torrentInfo.InfoHash,
diff --git a/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileResource.cs b/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileResource.cs
index 23c483c21..1e5c7f1da 100644
--- a/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileResource.cs
+++ b/src/Lidarr.Api.V1/Profiles/Delay/DelayProfileResource.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using Lidarr.Http.REST;
using NzbDrone.Core.Indexers;
@@ -13,6 +13,9 @@ namespace Lidarr.Api.V1.Profiles.Delay
public DownloadProtocol PreferredProtocol { get; set; }
public int UsenetDelay { get; set; }
public int TorrentDelay { get; set; }
+ public bool BypassIfHighestQuality { get; set; }
+ public bool BypassIfAboveCustomFormatScore { get; set; }
+ public int MinimumCustomFormatScore { get; set; }
public int Order { get; set; }
public HashSet Tags { get; set; }
}
@@ -35,6 +38,9 @@ namespace Lidarr.Api.V1.Profiles.Delay
PreferredProtocol = model.PreferredProtocol,
UsenetDelay = model.UsenetDelay,
TorrentDelay = model.TorrentDelay,
+ BypassIfHighestQuality = model.BypassIfHighestQuality,
+ BypassIfAboveCustomFormatScore = model.BypassIfAboveCustomFormatScore,
+ MinimumCustomFormatScore = model.MinimumCustomFormatScore,
Order = model.Order,
Tags = new HashSet(model.Tags)
};
@@ -56,6 +62,9 @@ namespace Lidarr.Api.V1.Profiles.Delay
PreferredProtocol = resource.PreferredProtocol,
UsenetDelay = resource.UsenetDelay,
TorrentDelay = resource.TorrentDelay,
+ BypassIfHighestQuality = resource.BypassIfHighestQuality,
+ BypassIfAboveCustomFormatScore = resource.BypassIfAboveCustomFormatScore,
+ MinimumCustomFormatScore = resource.MinimumCustomFormatScore,
Order = resource.Order,
Tags = new HashSet(resource.Tags)
};
diff --git a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileController.cs b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileController.cs
index 3f2bcb016..c9e936112 100644
--- a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileController.cs
+++ b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileController.cs
@@ -1,9 +1,12 @@
using System.Collections.Generic;
+using System.Linq;
using FluentValidation;
using Lidarr.Http;
using Lidarr.Http.REST;
using Lidarr.Http.REST.Attributes;
using Microsoft.AspNetCore.Mvc;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.Profiles.Qualities;
namespace Lidarr.Api.V1.Profiles.Quality
@@ -12,13 +15,30 @@ namespace Lidarr.Api.V1.Profiles.Quality
public class QualityProfileController : RestController
{
private readonly IQualityProfileService _qualityProfileService;
+ private readonly ICustomFormatService _formatService;
- public QualityProfileController(IQualityProfileService qualityProfileService)
+ public QualityProfileController(IQualityProfileService qualityProfileService, ICustomFormatService formatService)
{
_qualityProfileService = qualityProfileService;
+ _formatService = formatService;
SharedValidator.RuleFor(c => c.Name).NotEmpty();
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
SharedValidator.RuleFor(c => c.Items).ValidItems();
+ SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
+ {
+ var all = _formatService.All().Select(f => f.Id).ToList();
+ var ids = items.Select(i => i.Format);
+
+ return all.Except(ids).Empty();
+ }).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
+ SharedValidator.RuleFor(c => c).Custom((profile, context) =>
+ {
+ if (profile.FormatItems.Where(x => x.Score > 0).Sum(x => x.Score) < profile.MinFormatScore &&
+ profile.FormatItems.Max(x => x.Score) < profile.MinFormatScore)
+ {
+ context.AddFailure("Minimum Custom Format Score can never be satisfied");
+ }
+ });
}
[RestPostById]
diff --git a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs
index 381bb17c7..5962278bd 100644
--- a/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs
+++ b/src/Lidarr.Api.V1/Profiles/Quality/QualityProfileResource.cs
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Lidarr.Http.REST;
+using NzbDrone.Core.CustomFormats;
+using NzbDrone.Core.Profiles;
using NzbDrone.Core.Profiles.Qualities;
namespace Lidarr.Api.V1.Profiles.Quality
@@ -11,6 +13,9 @@ namespace Lidarr.Api.V1.Profiles.Quality
public bool UpgradeAllowed { get; set; }
public int Cutoff { get; set; }
public List Items { get; set; }
+ public int MinFormatScore { get; set; }
+ public int CutoffFormatScore { get; set; }
+ public List FormatItems { get; set; }
}
public class QualityProfileQualityItemResource : RestResource
@@ -26,6 +31,13 @@ namespace Lidarr.Api.V1.Profiles.Quality
}
}
+ public class ProfileFormatItemResource : RestResource
+ {
+ public int Format { get; set; }
+ public string Name { get; set; }
+ public int Score { get; set; }
+ }
+
public static class ProfileResourceMapper
{
public static QualityProfileResource ToResource(this QualityProfile model)
@@ -41,7 +53,10 @@ namespace Lidarr.Api.V1.Profiles.Quality
Name = model.Name,
UpgradeAllowed = model.UpgradeAllowed,
Cutoff = model.Cutoff,
- Items = model.Items.ConvertAll(ToResource)
+ Items = model.Items.ConvertAll(ToResource),
+ MinFormatScore = model.MinFormatScore,
+ CutoffFormatScore = model.CutoffFormatScore,
+ FormatItems = model.FormatItems.ConvertAll(ToResource)
};
}
@@ -62,6 +77,16 @@ namespace Lidarr.Api.V1.Profiles.Quality
};
}
+ public static ProfileFormatItemResource ToResource(this ProfileFormatItem model)
+ {
+ return new ProfileFormatItemResource
+ {
+ Format = model.Format.Id,
+ Name = model.Format.Name,
+ Score = model.Score
+ };
+ }
+
public static QualityProfile ToModel(this QualityProfileResource resource)
{
if (resource == null)
@@ -75,7 +100,10 @@ namespace Lidarr.Api.V1.Profiles.Quality
Name = resource.Name,
UpgradeAllowed = resource.UpgradeAllowed,
Cutoff = resource.Cutoff,
- Items = resource.Items.ConvertAll(ToModel)
+ Items = resource.Items.ConvertAll(ToModel),
+ MinFormatScore = resource.MinFormatScore,
+ CutoffFormatScore = resource.CutoffFormatScore,
+ FormatItems = resource.FormatItems.ConvertAll(ToModel)
};
}
@@ -96,6 +124,15 @@ namespace Lidarr.Api.V1.Profiles.Quality
};
}
+ public static ProfileFormatItem ToModel(this ProfileFormatItemResource resource)
+ {
+ return new ProfileFormatItem
+ {
+ Format = new CustomFormat { Id = resource.Format },
+ Score = resource.Score
+ };
+ }
+
public static List ToResource(this IEnumerable models)
{
return models.Select(ToResource).ToList();
diff --git a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileController.cs b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileController.cs
index 5cc4810d3..192b51d49 100644
--- a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileController.cs
+++ b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileController.cs
@@ -24,7 +24,7 @@ namespace Lidarr.Api.V1.Profiles.Release
SharedValidator.RuleFor(r => r).Custom((restriction, context) =>
{
- if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace() && restriction.Preferred.Empty())
+ if (restriction.Ignored.Empty() && restriction.Required.Empty())
{
context.AddFailure("Either 'Must contain' or 'Must not contain' is required");
}
@@ -33,11 +33,6 @@ namespace Lidarr.Api.V1.Profiles.Release
{
context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist");
}
-
- if (restriction.Preferred.Any(p => p.Key.IsNullOrWhiteSpace()))
- {
- context.AddFailure("Preferred", "Term cannot be empty or consist of only spaces");
- }
});
}
diff --git a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs
index 203186fa5..b2c7d68cc 100644
--- a/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs
+++ b/src/Lidarr.Api.V1/Profiles/Release/ReleaseProfileResource.cs
@@ -8,10 +8,8 @@ namespace Lidarr.Api.V1.Profiles.Release
public class ReleaseProfileResource : RestResource
{
public bool Enabled { get; set; }
- public string Required { get; set; }
- public string Ignored { get; set; }
- public List> Preferred { get; set; }
- public bool IncludePreferredWhenRenaming { get; set; }
+ public List Required { get; set; }
+ public List Ignored { get; set; }
public int IndexerId { get; set; }
public HashSet Tags { get; set; }
@@ -37,8 +35,6 @@ namespace Lidarr.Api.V1.Profiles.Release
Enabled = model.Enabled,
Required = model.Required,
Ignored = model.Ignored,
- Preferred = model.Preferred,
- IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming,
IndexerId = model.IndexerId,
Tags = new HashSet(model.Tags)
};
@@ -58,8 +54,6 @@ namespace Lidarr.Api.V1.Profiles.Release
Enabled = resource.Enabled,
Required = resource.Required,
Ignored = resource.Ignored,
- Preferred = resource.Preferred,
- IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming,
IndexerId = resource.IndexerId,
Tags = new HashSet(resource.Tags)
};
diff --git a/src/Lidarr.Api.V1/Queue/QueueResource.cs b/src/Lidarr.Api.V1/Queue/QueueResource.cs
index c34403642..6748cf474 100644
--- a/src/Lidarr.Api.V1/Queue/QueueResource.cs
+++ b/src/Lidarr.Api.V1/Queue/QueueResource.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Lidarr.Api.V1.Albums;
using Lidarr.Api.V1.Artist;
+using Lidarr.Api.V1.CustomFormats;
using Lidarr.Http.REST;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Download.TrackedDownloads;
@@ -18,6 +19,7 @@ namespace Lidarr.Api.V1.Queue
public ArtistResource Artist { get; set; }
public AlbumResource Album { get; set; }
public QualityModel Quality { get; set; }
+ public List CustomFormats { get; set; }
public decimal Size { get; set; }
public string Title { get; set; }
public decimal Sizeleft { get; set; }
@@ -53,6 +55,7 @@ namespace Lidarr.Api.V1.Queue
Artist = includeArtist && model.Artist != null ? model.Artist.ToResource() : null,
Album = includeAlbum && model.Album != null ? model.Album.ToResource() : null,
Quality = model.Quality,
+ CustomFormats = model.RemoteAlbum?.CustomFormats?.ToResource(false),
Size = model.Size,
Title = model.Title,
Sizeleft = model.Sizeleft,
diff --git a/src/Lidarr.Http/ClientSchema/Field.cs b/src/Lidarr.Http/ClientSchema/Field.cs
index 601aa8b3e..1028c8eff 100644
--- a/src/Lidarr.Http/ClientSchema/Field.cs
+++ b/src/Lidarr.Http/ClientSchema/Field.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using NzbDrone.Core.Annotations;
namespace Lidarr.Http.ClientSchema
{
diff --git a/src/Lidarr.Http/REST/RestResource.cs b/src/Lidarr.Http/REST/RestResource.cs
index 4774d10a5..827cd2bc5 100644
--- a/src/Lidarr.Http/REST/RestResource.cs
+++ b/src/Lidarr.Http/REST/RestResource.cs
@@ -5,7 +5,7 @@ namespace Lidarr.Http.REST
public abstract class RestResource
{
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
- public int Id { get; set; }
+ public virtual int Id { get; set; }
[JsonIgnore]
public virtual string ResourceName => GetType().Name.ToLowerInvariant().Replace("resource", "");
diff --git a/src/NzbDrone.Core.Test/CustomFormats/CustomFormatsTestHelpers.cs b/src/NzbDrone.Core.Test/CustomFormats/CustomFormatsTestHelpers.cs
new file mode 100644
index 000000000..a384a2b8e
--- /dev/null
+++ b/src/NzbDrone.Core.Test/CustomFormats/CustomFormatsTestHelpers.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NzbDrone.Core.CustomFormats;
+using NzbDrone.Core.Profiles;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.CustomFormats
+{
+ public class CustomFormatsTestHelpers : CoreTest
+ {
+ private static List _customFormats { get; set; }
+
+ public static void GivenCustomFormats(params CustomFormat[] formats)
+ {
+ _customFormats = formats.ToList();
+ }
+
+ public static List GetSampleFormatItems(params string[] allowed)
+ {
+ var allowedItems = _customFormats.Where(x => allowed.Contains(x.Name)).Select((f, index) => new ProfileFormatItem { Format = f, Score = (int)Math.Pow(2, index) }).ToList();
+ var disallowedItems = _customFormats.Where(x => !allowed.Contains(x.Name)).Select(f => new ProfileFormatItem { Format = f, Score = -1 * (int)Math.Pow(2, allowedItems.Count) });
+
+ return disallowedItems.Concat(allowedItems).ToList();
+ }
+
+ public static List GetDefaultFormatItems()
+ {
+ return new List();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/063_add_custom_formatsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/063_add_custom_formatsFixture.cs
new file mode 100644
index 000000000..5aa64f766
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Migration/063_add_custom_formatsFixture.cs
@@ -0,0 +1,431 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Common.Serializer;
+using NzbDrone.Core.Datastore.Migration;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Datastore.Migration
+{
+ [TestFixture]
+ public class add_custom_formatsFixture : MigrationTest
+ {
+ [Test]
+ public void should_add_cf_from_named_release_profile()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = false,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
+
+ customFormats.Should().HaveCount(1);
+ customFormats.First().Name.Should().Be("Unnamed_1");
+ customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
+ customFormats.First().Specifications.Should().HaveCount(1);
+ }
+
+ [Test]
+ public void should_not_migrate_if_bad_regex_in_release_profile()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "[somestring[",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = true,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
+
+ customFormats.Should().HaveCount(0);
+ }
+
+ [Test]
+ public void should_set_cf_naming_token_if_set_in_release_profile()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = true,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
+
+ customFormats.Should().HaveCount(1);
+ customFormats.First().Name.Should().Be("Unnamed_1");
+ customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeTrue();
+ customFormats.First().Specifications.Should().HaveCount(1);
+ }
+
+ [Test]
+ public void should_not_remove_release_profile_if_ignored_or_required()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "some",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = true,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var releaseProfiles = db.Query("SELECT \"Id\" FROM \"ReleaseProfiles\"");
+
+ releaseProfiles.Should().HaveCount(1);
+ }
+
+ [Test]
+ public void should_remove_release_profile_if_no_ignored_or_required()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = true,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var releaseProfiles = db.Query("SELECT \"Id\" FROM \"ReleaseProfiles\"");
+
+ releaseProfiles.Should().HaveCount(0);
+ }
+
+ [Test]
+ public void should_add_cf_from_unnamed_release_profile()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = false,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
+
+ customFormats.Should().HaveCount(1);
+ customFormats.First().Name.Should().Be("Unnamed_1");
+ customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
+ customFormats.First().Specifications.Should().HaveCount(1);
+ }
+
+ [Test]
+ public void should_add_cfs_from_multiple_unnamed_release_profile()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = false,
+ Enabled = true,
+ IndexerId = 0
+ });
+
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x265",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = false,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
+
+ customFormats.Should().HaveCount(2);
+ customFormats.First().Name.Should().Be("Unnamed_1");
+ customFormats.Last().Name.Should().Be("Unnamed_2");
+ customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
+ customFormats.First().Specifications.Should().HaveCount(1);
+ }
+
+ [Test]
+ public void should_add_two_cfs_if_release_profile_has_multiple_terms()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ },
+ new
+ {
+ Key = "x265",
+ Value = 5
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = false,
+ Enabled = true,
+ IndexerId = 0
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"IncludeCustomFormatWhenRenaming\", \"Specifications\" FROM \"CustomFormats\"");
+
+ customFormats.Should().HaveCount(2);
+ customFormats.First().Name.Should().Be("Unnamed_1_0");
+ customFormats.Last().Name.Should().Be("Unnamed_1_1");
+ customFormats.First().IncludeCustomFormatWhenRenaming.Should().BeFalse();
+ customFormats.First().Specifications.Should().HaveCount(1);
+ }
+
+ [Test]
+ public void should_set_scores_for_enabled_release_profiles()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = false,
+ Enabled = true,
+ IndexerId = 0
+ });
+
+ c.Insert.IntoTable("QualityProfiles").Row(new
+ {
+ Name = "SDTV",
+ Cutoff = 1,
+ Items = "[ { \"quality\": 1, \"allowed\": true } ]"
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"FormatItems\" FROM \"QualityProfiles\"");
+
+ customFormats.Should().HaveCount(1);
+ customFormats.First().FormatItems.Should().HaveCount(1);
+ customFormats.First().FormatItems.First().Score.Should().Be(2);
+ }
+
+ [Test]
+ public void should_set_zero_scores_for_disabled_release_profiles()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("ReleaseProfiles").Row(new
+ {
+ Preferred = new[]
+ {
+ new
+ {
+ Key = "x264",
+ Value = 2
+ }
+ }.ToJson(),
+ Required = "",
+ Ignored = "",
+ Tags = "[]",
+ IncludePreferredWhenRenaming = false,
+ Enabled = false,
+ IndexerId = 0
+ });
+
+ c.Insert.IntoTable("QualityProfiles").Row(new
+ {
+ Name = "SDTV",
+ Cutoff = 1,
+ Items = "[ { \"quality\": 1, \"allowed\": true } ]"
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"Id\", \"Name\", \"FormatItems\" FROM \"QualityProfiles\"");
+
+ customFormats.Should().HaveCount(1);
+ customFormats.First().FormatItems.Should().HaveCount(1);
+ customFormats.First().FormatItems.First().Score.Should().Be(0);
+ }
+
+ [Test]
+ public void should_migrate_naming_configs()
+ {
+ var db = WithMigrationTestDb(c =>
+ {
+ c.Insert.IntoTable("NamingConfig").Row(new
+ {
+ ReplaceIllegalCharacters = false,
+ StandardTrackFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Preferred Words } {Quality Full}",
+ MultiDiscTrackFormat = "{Series Title} - {Air-Date} - {Episode Title} {Preferred.Words } {Quality Full}",
+ });
+ });
+
+ var customFormats = db.Query("SELECT \"StandardTrackFormat\", \"MultiDiscTrackFormat\" FROM \"NamingConfig\"");
+
+ customFormats.Should().HaveCount(1);
+ customFormats.First().StandardTrackFormat.Should().Be("{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Custom Formats } {Quality Full}");
+ customFormats.First().MultiDiscTrackFormat.Should().Be("{Series Title} - {Air-Date} - {Episode Title} {Custom.Formats } {Quality Full}");
+ }
+
+ private class NamingConfig171
+ {
+ public string StandardTrackFormat { get; set; }
+ public string MultiDiscTrackFormat { get; set; }
+ }
+
+ private class ReleaseProfile171
+ {
+ public int Id { get; set; }
+ }
+
+ private class QualityProfile171
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public List FormatItems { get; set; }
+ }
+
+ private class FormatItem171
+ {
+ public int Format { get; set; }
+ public int Score { get; set; }
+ }
+
+ private class CustomFormat171
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public bool IncludeCustomFormatWhenRenaming { get; set; }
+ public List Specifications { get; set; }
+ }
+
+ private class CustomFormatSpec171
+ {
+ public string Type { get; set; }
+ public CustomFormatReleaseTitleSpec171 Body { get; set; }
+ }
+
+ private class CustomFormatReleaseTitleSpec171
+ {
+ public int Order { get; set; }
+ public string ImplementationName { get; set; }
+ public string Name { get; set; }
+ public string Value { get; set; }
+ public bool Required { get; set; }
+ public bool Negate { get; set; }
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs
new file mode 100644
index 000000000..0d692bc20
--- /dev/null
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CustomFormatAllowedByProfileSpecificationFixture.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
+using NzbDrone.Core.DecisionEngine.Specifications;
+using NzbDrone.Core.Music;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Profiles.Qualities;
+using NzbDrone.Core.Qualities;
+using NzbDrone.Core.Test.CustomFormats;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.DecisionEngineTests
+{
+ [TestFixture]
+
+ public class CustomFormatAllowedByProfileSpecificationFixture : CoreTest
+ {
+ private RemoteAlbum _remoteAlbum;
+
+ private CustomFormat _format1;
+ private CustomFormat _format2;
+
+ [SetUp]
+ public void Setup()
+ {
+ _format1 = new CustomFormat("Awesome Format");
+ _format1.Id = 1;
+
+ _format2 = new CustomFormat("Cool Format");
+ _format2.Id = 2;
+
+ var fakeArtist = Builder.CreateNew()
+ .With(c => c.QualityProfile = new QualityProfile
+ {
+ Cutoff = Quality.MP3_320.Id,
+ MinFormatScore = 1
+ })
+ .Build();
+
+ _remoteAlbum = new RemoteAlbum
+ {
+ Artist = fakeArtist,
+ ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_016, new Revision(version: 2)) },
+ };
+
+ CustomFormatsTestHelpers.GivenCustomFormats(_format1, _format2);
+ }
+
+ [Test]
+ public void should_allow_if_format_score_greater_than_min()
+ {
+ _remoteAlbum.CustomFormats = new List { _format1 };
+ _remoteAlbum.Artist.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name);
+ _remoteAlbum.CustomFormatScore = _remoteAlbum.Artist.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_deny_if_format_score_not_greater_than_min()
+ {
+ _remoteAlbum.CustomFormats = new List { _format2 };
+ _remoteAlbum.Artist.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name);
+ _remoteAlbum.CustomFormatScore = _remoteAlbum.Artist.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
+
+ Console.WriteLine(_remoteAlbum.CustomFormatScore);
+ Console.WriteLine(_remoteAlbum.Artist.QualityProfile.Value.MinFormatScore);
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_deny_if_format_score_not_greater_than_min_2()
+ {
+ _remoteAlbum.CustomFormats = new List { _format2, _format1 };
+ _remoteAlbum.Artist.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name);
+ _remoteAlbum.CustomFormatScore = _remoteAlbum.Artist.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_allow_if_all_format_is_defined_in_profile()
+ {
+ _remoteAlbum.CustomFormats = new List { _format2, _format1 };
+ _remoteAlbum.Artist.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name);
+ _remoteAlbum.CustomFormatScore = _remoteAlbum.Artist.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
+ }
+
+ [Test]
+ public void should_deny_if_no_format_was_parsed_and_min_score_positive()
+ {
+ _remoteAlbum.CustomFormats = new List { };
+ _remoteAlbum.Artist.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name);
+ _remoteAlbum.CustomFormatScore = _remoteAlbum.Artist.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_allow_if_no_format_was_parsed_min_score_is_zero()
+ {
+ _remoteAlbum.CustomFormats = new List { };
+ _remoteAlbum.Artist.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(_format1.Name, _format2.Name);
+ _remoteAlbum.Artist.QualityProfile.Value.MinFormatScore = 0;
+ _remoteAlbum.CustomFormatScore = _remoteAlbum.Artist.QualityProfile.Value.CalculateCustomFormatScore(_remoteAlbum.CustomFormats);
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs
index a616b2d97..23761abc3 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
@@ -11,8 +12,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestFixture]
public class CutoffSpecificationFixture : CoreTest
{
- private static readonly int NoPreferredWordScore = 0;
-
[Test]
public void should_return_true_if_current_album_is_less_than_cutoff()
{
@@ -23,7 +22,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Items = Qualities.QualityFixture.GetDefaultQualities()
},
new List { new QualityModel(Quality.MP3_192, new Revision(version: 2)) },
- NoPreferredWordScore).Should().BeTrue();
+ new List()).Should().BeTrue();
}
[Test]
@@ -36,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Items = Qualities.QualityFixture.GetDefaultQualities()
},
new List { new QualityModel(Quality.MP3_256, new Revision(version: 2)) },
- NoPreferredWordScore).Should().BeFalse();
+ new List()).Should().BeFalse();
}
[Test]
@@ -49,7 +48,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Items = Qualities.QualityFixture.GetDefaultQualities()
},
new List { new QualityModel(Quality.MP3_320, new Revision(version: 2)) },
- NoPreferredWordScore).Should().BeFalse();
+ new List()).Should().BeFalse();
}
[Test]
@@ -62,7 +61,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Items = Qualities.QualityFixture.GetDefaultQualities()
},
new List { new QualityModel(Quality.MP3_320, new Revision(version: 1)) },
- NoPreferredWordScore,
+ new List(),
new QualityModel(Quality.MP3_320, new Revision(version: 2))).Should().BeTrue();
}
@@ -76,12 +75,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Items = Qualities.QualityFixture.GetDefaultQualities()
},
new List { new QualityModel(Quality.MP3_320, new Revision(version: 2)) },
- NoPreferredWordScore,
+ new List(),
new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse();
}
[Test]
- public void should_return_true_if_cutoffs_are_met_and_score_is_higher()
+ public void should_return_true_if_cutoffs_are_met_but_is_a_revision_upgrade()
{
QualityProfile profile = new QualityProfile
{
@@ -91,27 +90,26 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.CutoffNotMet(
profile,
- new List { new QualityModel(Quality.MP3_320, new Revision(version: 2)) },
- NoPreferredWordScore,
- new QualityModel(Quality.FLAC, new Revision(version: 2)),
- 10).Should().BeTrue();
+ new List { new QualityModel(Quality.FLAC, new Revision(version: 1)) },
+ new List(),
+ new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeTrue();
}
[Test]
- public void should_return_true_if_cutoffs_are_met_but_is_a_revision_upgrade()
+ public void should_return_false_if_quality_profile_does_not_allow_upgrades_but_cutoff_is_set_to_highest_quality()
{
QualityProfile profile = new QualityProfile
{
- Cutoff = Quality.MP3_320.Id,
+ Cutoff = Quality.FLAC_24.Id,
Items = Qualities.QualityFixture.GetDefaultQualities(),
+ UpgradeAllowed = false
};
Subject.CutoffNotMet(
profile,
- new List { new QualityModel(Quality.FLAC, new Revision(version: 1)) },
- NoPreferredWordScore,
- new QualityModel(Quality.FLAC, new Revision(version: 2)),
- NoPreferredWordScore).Should().BeTrue();
+ new List { new QualityModel(Quality.MP3_320, new Revision(version: 1)) },
+ new List(),
+ new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse();
}
}
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs
index 3cec1e913..4a2b062fb 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs
@@ -416,15 +416,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC));
var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC));
- remoteAlbum1.PreferredWordScore = 10;
- remoteAlbum2.PreferredWordScore = 0;
+ remoteAlbum1.CustomFormatScore = 10;
+ remoteAlbum2.CustomFormatScore = 0;
var decisions = new List();
decisions.Add(new DownloadDecision(remoteAlbum1));
decisions.Add(new DownloadDecision(remoteAlbum2));
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
- qualifiedReports.First().RemoteAlbum.PreferredWordScore.Should().Be(10);
+ qualifiedReports.First().RemoteAlbum.CustomFormatScore.Should().Be(10);
}
[Test]
@@ -437,8 +437,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1)));
var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2)));
- remoteAlbum1.PreferredWordScore = 10;
- remoteAlbum2.PreferredWordScore = 0;
+ remoteAlbum1.CustomFormatScore = 10;
+ remoteAlbum2.CustomFormatScore = 0;
var decisions = new List();
decisions.Add(new DownloadDecision(remoteAlbum1));
@@ -458,8 +458,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1)));
var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2)));
- remoteAlbum1.PreferredWordScore = 10;
- remoteAlbum2.PreferredWordScore = 0;
+ remoteAlbum1.CustomFormatScore = 10;
+ remoteAlbum2.CustomFormatScore = 0;
var decisions = new List();
decisions.Add(new DownloadDecision(remoteAlbum1));
@@ -479,8 +479,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1)));
var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(2)));
- remoteAlbum1.PreferredWordScore = 10;
- remoteAlbum2.PreferredWordScore = 0;
+ remoteAlbum1.CustomFormatScore = 10;
+ remoteAlbum2.CustomFormatScore = 0;
var decisions = new List();
decisions.Add(new DownloadDecision(remoteAlbum1));
@@ -489,7 +489,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.FLAC);
qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.Should().Be(1);
- qualifiedReports.First().RemoteAlbum.PreferredWordScore.Should().Be(10);
+ qualifiedReports.First().RemoteAlbum.CustomFormatScore.Should().Be(10);
}
[Test]
@@ -502,8 +502,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1, 0)));
var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.FLAC, new Revision(1, 1)));
- remoteAlbum1.PreferredWordScore = 10;
- remoteAlbum2.PreferredWordScore = 0;
+ remoteAlbum1.CustomFormatScore = 10;
+ remoteAlbum2.CustomFormatScore = 0;
var decisions = new List();
decisions.Add(new DownloadDecision(remoteAlbum1));
@@ -513,7 +513,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.FLAC);
qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Revision.Version.Should().Be(1);
qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Revision.Real.Should().Be(0);
- qualifiedReports.First().RemoteAlbum.PreferredWordScore.Should().Be(10);
+ qualifiedReports.First().RemoteAlbum.CustomFormatScore.Should().Be(10);
}
[Test]
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs
index ca1bc44a7..4add10856 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs
@@ -2,7 +2,9 @@ using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
+using Moq;
using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download.TrackedDownloads;
using NzbDrone.Core.Music;
@@ -10,6 +12,7 @@ using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Queue;
+using NzbDrone.Core.Test.CustomFormats;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests
@@ -31,11 +34,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Mocker.Resolve();
+ CustomFormatsTestHelpers.GivenCustomFormats();
+
_artist = Builder.CreateNew()
.With(e => e.QualityProfile = new QualityProfile
{
UpgradeAllowed = true,
Items = Qualities.QualityFixture.GetDefaultQualities(),
+ FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems(),
+ MinFormatScore = 0
})
.Build();
@@ -59,8 +66,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.With(r => r.Artist = _artist)
.With(r => r.Albums = new List { _album })
.With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256) })
- .With(r => r.PreferredWordScore = 0)
+ .With(r => r.CustomFormats = new List())
.Build();
+
+ Mocker.GetMock()
+ .Setup(x => x.ParseCustomFormat(It.IsAny(), It.IsAny()))
+ .Returns(new List());
}
private void GivenEmptyQueue()
@@ -70,6 +81,13 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Returns(new List());
}
+ private void GivenQueueFormats(List formats)
+ {
+ Mocker.GetMock()
+ .Setup(x => x.ParseCustomFormat(It.IsAny(), It.IsAny()))
+ .Returns(formats);
+ }
+
private void GivenQueue(IEnumerable remoteAlbums, TrackedDownloadState trackedDownloadState = TrackedDownloadState.Downloading)
{
var queue = remoteAlbums.Select(remoteAlbum => new Queue.Queue
@@ -97,6 +115,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.With(r => r.Artist = _otherArtist)
.With(r => r.Albums = new List { _album })
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum });
@@ -115,6 +134,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Quality = new QualityModel(Quality.MP3_256)
})
+ .With(r => r.CustomFormats = new List())
.With(r => r.Release = _releaseInfo)
.Build();
@@ -136,6 +156,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_192)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum });
@@ -153,6 +174,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_192)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum });
@@ -160,9 +182,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
}
[Test]
- public void should_return_true_when_qualities_are_the_same_with_higher_preferred_word_score()
+ public void should_return_true_when_qualities_are_the_same_with_higher_custom_format_score()
{
- _remoteAlbum.PreferredWordScore = 1;
+ _remoteAlbum.CustomFormats = new List { new CustomFormat("My Format", new ReleaseTitleSpecification { Value = "MP3" }) { Id = 1 } };
+
+ var lowFormat = new List { new CustomFormat("Bad Format", new ReleaseTitleSpecification { Value = "MP3" }) { Id = 2 } };
+
+ CustomFormatsTestHelpers.GivenCustomFormats(_remoteAlbum.CustomFormats.First(), lowFormat.First());
+
+ _artist.QualityProfile.Value.FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("My Format");
+
+ GivenQueueFormats(lowFormat);
var remoteAlbum = Builder.CreateNew()
.With(r => r.Artist = _artist)
@@ -172,6 +202,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_256)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = lowFormat)
.Build();
GivenQueue(new List { remoteAlbum });
@@ -189,6 +220,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_192)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum });
@@ -208,6 +240,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_320)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum });
@@ -225,6 +258,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_320)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum });
@@ -242,6 +276,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_320)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
_remoteAlbum.Albums.Add(_otherAlbum);
@@ -261,6 +296,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_320)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
_remoteAlbum.Albums.Add(_otherAlbum);
@@ -275,6 +311,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
var remoteAlbums = Builder.CreateListOfSize(2)
.All()
.With(r => r.Artist = _artist)
+ .With(r => r.CustomFormats = new List())
.With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo
{
Quality = new QualityModel(Quality.MP3_320)
@@ -305,6 +342,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.FLAC)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum });
@@ -324,6 +362,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Quality = new QualityModel(Quality.MP3_008)
})
.With(r => r.Release = _releaseInfo)
+ .With(r => r.CustomFormats = new List())
.Build();
GivenQueue(new List { remoteAlbum }, TrackedDownloadState.DownloadFailedPending);
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
index f54fafa08..c54ab3c3b 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Linq;
using FluentAssertions;
using Moq;
using NUnit.Framework;
@@ -33,7 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Mocker.SetConstant(Mocker.Resolve());
}
- private void GivenRestictions(string required, string ignored)
+ private void GivenRestictions(List required, List ignored)
{
Mocker.GetMock()
.Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny()))
@@ -60,7 +61,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test]
public void should_be_true_when_title_contains_one_required_term()
{
- GivenRestictions("WEBRip", null);
+ GivenRestictions(new List { "WEBRip" }, new List());
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
@@ -68,7 +69,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test]
public void should_be_false_when_title_does_not_contain_any_required_terms()
{
- GivenRestictions("doesnt,exist", null);
+ GivenRestictions(new List { "doesnt", "exist" }, new List());
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
}
@@ -76,7 +77,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test]
public void should_be_true_when_title_does_not_contain_any_ignored_terms()
{
- GivenRestictions(null, "ignored");
+ GivenRestictions(new List(), new List { "ignored" });
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
@@ -84,7 +85,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test]
public void should_be_false_when_title_contains_one_anded_ignored_terms()
{
- GivenRestictions(null, "edited");
+ GivenRestictions(new List(), new List { "edited" });
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
}
@@ -95,7 +96,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestCase("X264,NOTTHERE")]
public void should_ignore_case_when_matching_required(string required)
{
- GivenRestictions(required, null);
+ GivenRestictions(required.Split(',').ToList(), new List());
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
@@ -106,7 +107,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestCase("X264,NOTTHERE")]
public void should_ignore_case_when_matching_ignored(string ignored)
{
- GivenRestictions(null, ignored);
+ GivenRestictions(new List(), ignored.Split(',').ToList());
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
}
@@ -120,7 +121,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Setup(s => s.EnabledForTags(It.IsAny>(), It.IsAny()))
.Returns(new List
{
- new ReleaseProfile { Required = "320", Ignored = "www.Speed.cd" }
+ new ReleaseProfile { Required = new List { "320" }, Ignored = new List { "www.Speed.cd" } }
});
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
@@ -132,7 +133,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestCase(@"/\.WEB/", true)]
public void should_match_perl_regex(string pattern, bool expected)
{
- GivenRestictions(pattern, null);
+ GivenRestictions(pattern.Split(',').ToList(), new List());
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().Be(expected);
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs
index da4432241..7a8639cf6 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs
@@ -5,6 +5,7 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.Download.Pending;
@@ -87,7 +88,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
private void GivenUpgradeForExistingFile()
{
Mocker.GetMock()
- .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny>()))
.Returns(true);
}
@@ -117,10 +118,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
}
[Test]
- public void should_be_true_when_quality_is_last_allowed_in_profile()
+ public void should_be_false_when_quality_is_last_allowed_in_profile_and_bypass_disabled()
{
+ _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
_remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320);
+ _delayProfile.UsenetDelay = 720;
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_be_true_when_quality_is_last_allowed_in_profile_and_bypass_enabled()
+ {
+ _delayProfile.UsenetDelay = 720;
+ _delayProfile.BypassIfHighestQuality = true;
+
+ _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
+ _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.FLAC);
+
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
}
@@ -194,5 +210,43 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
}
+
+ [Test]
+ public void should_be_false_when_custom_format_score_is_above_minimum_but_bypass_disabled()
+ {
+ _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
+ _remoteAlbum.CustomFormatScore = 100;
+
+ _delayProfile.UsenetDelay = 720;
+ _delayProfile.MinimumCustomFormatScore = 50;
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_be_false_when_custom_format_score_is_above_minimum_and_bypass_enabled_but_under_minimum()
+ {
+ _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
+ _remoteAlbum.CustomFormatScore = 5;
+
+ _delayProfile.UsenetDelay = 720;
+ _delayProfile.BypassIfAboveCustomFormatScore = true;
+ _delayProfile.MinimumCustomFormatScore = 50;
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_be_true_when_custom_format_score_is_above_minimum_and_bypass_enabled()
+ {
+ _remoteAlbum.Release.PublishDate = DateTime.UtcNow;
+ _remoteAlbum.CustomFormatScore = 100;
+
+ _delayProfile.UsenetDelay = 720;
+ _delayProfile.BypassIfAboveCustomFormatScore = true;
+ _delayProfile.MinimumCustomFormatScore = 50;
+
+ Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue();
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs
similarity index 93%
rename from src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs
rename to src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs
index 514b0b178..43cf0122f 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs
@@ -5,6 +5,7 @@ using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.History;
@@ -13,9 +14,10 @@ using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
+using NzbDrone.Core.Test.CustomFormats;
using NzbDrone.Core.Test.Framework;
-namespace NzbDrone.Core.Test.DecisionEngineTests
+namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
{
[TestFixture]
public class HistorySpecificationFixture : CoreTest
@@ -37,6 +39,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Mocker.Resolve();
_upgradeHistory = Mocker.Resolve();
+ CustomFormatsTestHelpers.GivenCustomFormats();
+
var singleAlbumList = new List { new Album { Id = FIRST_ALBUM_ID } };
var doubleAlbumList = new List
{
@@ -50,6 +54,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
UpgradeAllowed = true,
Cutoff = Quality.MP3_320.Id,
+ FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("None"),
+ MinFormatScore = 0,
Items = Qualities.QualityFixture.GetDefaultQualities()
})
.Build();
@@ -58,14 +64,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Artist = _fakeArtist,
ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) },
- Albums = doubleAlbumList
+ Albums = doubleAlbumList,
+ CustomFormats = new List()
};
_parseResultSingle = new RemoteAlbum
{
Artist = _fakeArtist,
ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) },
- Albums = singleAlbumList
+ Albums = singleAlbumList,
+ CustomFormats = new List()
};
_upgradableQuality = new QualityModel(Quality.MP3_192, new Revision(version: 1));
@@ -74,6 +82,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Mocker.GetMock()
.SetupGet(s => s.EnableCompletedDownloadHandling)
.Returns(true);
+
+ Mocker.GetMock()
+ .Setup(x => x.ParseCustomFormat(It.IsAny(), It.IsAny()))
+ .Returns(new List());
}
private void GivenMostRecentForAlbum(int albumId, string downloadId, QualityModel quality, DateTime date, EntityHistoryEventType eventType)
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs
index 43e431245..12d05989b 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeAllowedSpecificationFixture.cs
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
+using NzbDrone.Core.Profiles;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
@@ -11,93 +13,197 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestFixture]
public class UpgradeAllowedSpecificationFixture : CoreTest
{
+ private CustomFormat _customFormatOne;
+ private CustomFormat _customFormatTwo;
+ private QualityProfile _qualityProfile;
+
+ [SetUp]
+ public void Setup()
+ {
+ _customFormatOne = new CustomFormat
+ {
+ Id = 1,
+ Name = "One"
+ };
+ _customFormatTwo = new CustomFormat
+ {
+ Id = 2,
+ Name = "Two"
+ };
+
+ _qualityProfile = new QualityProfile
+ {
+ Cutoff = Quality.FLAC.Id,
+ Items = Qualities.QualityFixture.GetDefaultQualities(),
+ UpgradeAllowed = false,
+ CutoffFormatScore = 100,
+ FormatItems = new List
+ {
+ new ProfileFormatItem
+ {
+ Format = _customFormatOne,
+ Score = 50
+ },
+ new ProfileFormatItem
+ {
+ Format = _customFormatTwo,
+ Score = 100
+ }
+ }
+ };
+ }
+
[Test]
- public void should_return_false_when_quality_is_better_and_upgrade_allowed_is_false_for_quality_profile()
+ public void should_return_false_when_quality_is_better_custom_formats_are_the_same_and_upgrading_is_not_allowed()
{
+ _qualityProfile.UpgradeAllowed = false;
+
Subject.IsUpgradeAllowed(
- new QualityProfile
- {
- Cutoff = Quality.FLAC.Id,
- Items = Qualities.QualityFixture.GetDefaultQualities(),
- UpgradeAllowed = false
- },
+ _qualityProfile,
new List { new QualityModel(Quality.MP3_320) },
- new QualityModel(Quality.FLAC))
+ new List(),
+ new QualityModel(Quality.FLAC),
+ new List())
.Should().BeFalse();
}
+ [Test]
+ public void should_return_false_when_quality_is_same_and_custom_format_is_upgrade_and_upgrading_is_not_allowed()
+ {
+ _qualityProfile.UpgradeAllowed = false;
+
+ Subject.IsUpgradeAllowed(
+ _qualityProfile,
+ new List { new QualityModel(Quality.MP3_320) },
+ new List { _customFormatOne },
+ new QualityModel(Quality.MP3_320),
+ new List { _customFormatTwo })
+ .Should().BeFalse();
+ }
+
+ [Test]
+ public void should_return_true_for_custom_format_upgrade_when_upgrading_is_allowed()
+ {
+ _qualityProfile.UpgradeAllowed = true;
+
+ Subject.IsUpgradeAllowed(
+ _qualityProfile,
+ new List { new QualityModel(Quality.MP3_320) },
+ new List { _customFormatOne },
+ new QualityModel(Quality.MP3_320),
+ new List { _customFormatTwo })
+ .Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_for_same_custom_format_score_when_upgrading_is_not_allowed()
+ {
+ _qualityProfile.UpgradeAllowed = false;
+
+ Subject.IsUpgradeAllowed(
+ _qualityProfile,
+ new List { new QualityModel(Quality.MP3_320) },
+ new List { _customFormatOne },
+ new QualityModel(Quality.MP3_320),
+ new List { _customFormatOne })
+ .Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_for_lower_custom_format_score_when_upgrading_is_allowed()
+ {
+ _qualityProfile.UpgradeAllowed = true;
+
+ Subject.IsUpgradeAllowed(
+ _qualityProfile,
+ new List { new QualityModel(Quality.MP3_320) },
+ new List { _customFormatTwo },
+ new QualityModel(Quality.MP3_320),
+ new List { _customFormatOne })
+ .Should().BeTrue();
+ }
+
+ [Test]
+ public void should_return_true_for_lower_language_when_upgrading_is_not_allowed()
+ {
+ _qualityProfile.UpgradeAllowed = false;
+
+ Subject.IsUpgradeAllowed(
+ _qualityProfile,
+ new List { new QualityModel(Quality.MP3_320) },
+ new List { _customFormatTwo },
+ new QualityModel(Quality.MP3_320),
+ new List { _customFormatOne })
+ .Should().BeTrue();
+ }
+
[Test]
public void should_return_true_for_quality_upgrade_when_upgrading_is_allowed()
{
+ _qualityProfile.UpgradeAllowed = true;
+
Subject.IsUpgradeAllowed(
- new QualityProfile
- {
- Cutoff = Quality.FLAC.Id,
- Items = Qualities.QualityFixture.GetDefaultQualities(),
- UpgradeAllowed = true
- },
+ _qualityProfile,
new List { new QualityModel(Quality.MP3_320) },
- new QualityModel(Quality.FLAC))
+ new List(),
+ new QualityModel(Quality.FLAC),
+ new List())
.Should().BeTrue();
}
[Test]
public void should_return_true_for_same_quality_when_upgrading_is_allowed()
{
+ _qualityProfile.UpgradeAllowed = true;
+
Subject.IsUpgradeAllowed(
- new QualityProfile
- {
- Cutoff = Quality.FLAC.Id,
- Items = Qualities.QualityFixture.GetDefaultQualities(),
- UpgradeAllowed = true
- },
+ _qualityProfile,
new List { new QualityModel(Quality.MP3_320) },
- new QualityModel(Quality.MP3_320))
+ new List(),
+ new QualityModel(Quality.MP3_320),
+ new List())
.Should().BeTrue();
}
[Test]
public void should_return_true_for_same_quality_when_upgrading_is_not_allowed()
{
+ _qualityProfile.UpgradeAllowed = false;
+
Subject.IsUpgradeAllowed(
- new QualityProfile
- {
- Cutoff = Quality.FLAC.Id,
- Items = Qualities.QualityFixture.GetDefaultQualities(),
- UpgradeAllowed = false
- },
+ _qualityProfile,
new List { new QualityModel(Quality.MP3_320) },
- new QualityModel(Quality.MP3_320))
+ new List(),
+ new QualityModel(Quality.MP3_320),
+ new List())
.Should().BeTrue();
}
[Test]
public void should_return_true_for_lower_quality_when_upgrading_is_allowed()
{
+ _qualityProfile.UpgradeAllowed = true;
+
Subject.IsUpgradeAllowed(
- new QualityProfile
- {
- Cutoff = Quality.FLAC.Id,
- Items = Qualities.QualityFixture.GetDefaultQualities(),
- UpgradeAllowed = true
- },
+ _qualityProfile,
new List { new QualityModel(Quality.MP3_320) },
- new QualityModel(Quality.MP3_256))
+ new List(),
+ new QualityModel(Quality.MP3_192),
+ new List())
.Should().BeTrue();
}
[Test]
public void should_return_true_for_lower_quality_when_upgrading_is_not_allowed()
{
+ _qualityProfile.UpgradeAllowed = false;
+
Subject.IsUpgradeAllowed(
- new QualityProfile
- {
- Cutoff = Quality.FLAC.Id,
- Items = Qualities.QualityFixture.GetDefaultQualities(),
- UpgradeAllowed = false
- },
+ _qualityProfile,
new List { new QualityModel(Quality.MP3_320) },
- new QualityModel(Quality.MP3_256))
+ new List(),
+ new QualityModel(Quality.MP3_192),
+ new List())
.Should().BeTrue();
}
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs
index 7fa65e375..d6f144668 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs
@@ -4,12 +4,15 @@ using FizzWare.NBuilder;
using FluentAssertions;
using Moq;
using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
+using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Music;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
+using NzbDrone.Core.Test.CustomFormats;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests
@@ -28,6 +31,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Mocker.Resolve();
+ CustomFormatsTestHelpers.GivenCustomFormats();
+
_firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now };
_secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now };
@@ -39,7 +44,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
UpgradeAllowed = true,
Cutoff = Quality.MP3_320.Id,
- Items = Qualities.QualityFixture.GetDefaultQualities()
+ Items = Qualities.QualityFixture.GetDefaultQualities(),
+ FormatItems = CustomFormatsTestHelpers.GetSampleFormatItems("None"),
+ MinFormatScore = 0,
})
.Build();
@@ -55,15 +62,21 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
Artist = fakeArtist,
ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) },
- Albums = doubleAlbumList
+ Albums = doubleAlbumList,
+ CustomFormats = new List()
};
_parseResultSingle = new RemoteAlbum
{
Artist = fakeArtist,
ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) },
- Albums = singleAlbumList
+ Albums = singleAlbumList,
+ CustomFormats = new List()
};
+
+ Mocker.GetMock()
+ .Setup(x => x.ParseCustomFormat(It.IsAny()))
+ .Returns(new List());
}
private void WithFirstFileUpgradable()
@@ -148,6 +161,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[Test]
public void should_be_false_if_some_tracks_are_upgradable_and_some_are_downgrades()
{
+ Mocker.GetMock()
+ .Setup(s => s.ParseCustomFormat(It.IsAny()))
+ .Returns(new List());
+
WithFirstFileUpgradable();
_parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320);
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse();
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs
index f284f9688..c23dfd375 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Configuration;
+using NzbDrone.Core.CustomFormats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Profiles.Qualities;
using NzbDrone.Core.Qualities;
@@ -23,8 +24,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
new object[] { Quality.MP3_320, 1, Quality.MP3_320, 1, Quality.MP3_320, false }
};
- private static readonly int NoPreferredWordScore = 0;
-
private void GivenAutoDownloadPropers(ProperDownloadTypes type)
{
Mocker.GetMock()
@@ -47,9 +46,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsUpgradable(
profile,
new List { new QualityModel(current, new Revision(version: currentVersion)) },
- NoPreferredWordScore,
+ new List(),
new QualityModel(newQuality, new Revision(version: newVersion)),
- NoPreferredWordScore)
+ new List())
.Should().Be(expected);
}
@@ -66,9 +65,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsUpgradable(
profile,
new List { new QualityModel(Quality.MP3_256, new Revision(version: 1)) },
- NoPreferredWordScore,
+ new List(),
new QualityModel(Quality.MP3_256, new Revision(version: 2)),
- NoPreferredWordScore)
+ new List())
.Should().BeTrue();
}
@@ -85,9 +84,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.IsUpgradable(
profile,
new List { new QualityModel(Quality.MP3_256, new Revision(version: 1)) },
- NoPreferredWordScore,
+ new List(),
new QualityModel(Quality.MP3_256, new Revision(version: 2)),
- NoPreferredWordScore)
+ new List())
.Should().BeFalse();
}
}
diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
index f628c86ac..601609193 100644
--- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
@@ -104,6 +104,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
.With(h => h.Title = title)
.With(h => h.Release = release)
.With(h => h.Reason = reason)
+ .With(h => h.ParsedAlbumInfo = _parsedAlbumInfo)
.Build();
_heldReleases.AddRange(heldReleases);
diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs
index 0784006ac..095ee8c78 100644
--- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemovePendingFixture.cs
@@ -52,7 +52,9 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
_pending.Add(new PendingRelease
{
Id = id,
- ParsedAlbumInfo = new ParsedAlbumInfo { AlbumTitle = album }
+ Title = "Artist.Title-Album.Title.abc-Lidarr",
+ ParsedAlbumInfo = new ParsedAlbumInfo { AlbumTitle = album },
+ Release = Builder.CreateNew().Build()
});
}
diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs
new file mode 100644
index 000000000..14bb29f3a
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupQualityProfileFormatItemsFixture.cs
@@ -0,0 +1,134 @@
+using System.Collections.Generic;
+using System.Linq;
+using FizzWare.NBuilder;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.CustomFormats;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Housekeeping.Housekeepers;
+using NzbDrone.Core.Messaging.Events;
+using NzbDrone.Core.Profiles;
+using NzbDrone.Core.Profiles.Qualities;
+using NzbDrone.Core.Test.Framework;
+
+namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
+{
+ [TestFixture]
+ public class CleanupQualityProfileFormatItemsFixture : DbTest
+ {
+ [SetUp]
+ public void Setup()
+ {
+ Mocker.SetConstant(
+ new QualityProfileFormatItemsCleanupRepository(Mocker.Resolve(), Mocker.Resolve()));
+
+ Mocker.SetConstant(
+ new CustomFormatRepository(Mocker.Resolve(), Mocker.Resolve()));
+ }
+
+ [Test]
+ public void should_remove_orphaned_custom_formats()
+ {
+ var qualityProfile = Builder.CreateNew()
+ .With(h => h.Items = Qualities.QualityFixture.GetDefaultQualities())
+ .With(h => h.MinFormatScore = 50)
+ .With(h => h.CutoffFormatScore = 100)
+ .With(h => h.FormatItems = new List
+ {
+ Builder