mirror of https://github.com/lidarr/Lidarr
Compare commits
22 Commits
c13864ab57
...
464e236ee0
Author | SHA1 | Date |
---|---|---|
Hazard Sylvain | 464e236ee0 | |
Bogdan | 416d505316 | |
Bogdan | 4816f35256 | |
Josh McKinney | e42e0a72eb | |
Bogdan | db9e62f79d | |
Bogdan | bc69fa4842 | |
Bogdan | 86dad72c49 | |
Bogdan | 4a8d6c367d | |
Bogdan | c1926f8758 | |
Bogdan | 7820bcf91f | |
Servarr | 431ad0a028 | |
Bogdan | 59cf7a95c3 | |
Bogdan | e17e3633f8 | |
Bogdan | 46da2b49c6 | |
Josh McKinney | 3071977284 | |
Mark McDowall | b14e2bb618 | |
Mark McDowall | 8c09c0cb5c | |
Weblate | 8cebb21c2d | |
Bogdan | 74ac263b74 | |
HazardSy | 961484412a | |
HazardSy | 638940a035 | |
HazardSy | d3ee21b919 |
|
@ -0,0 +1,13 @@
|
|||
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||
// the frontend has vscode settings that are distinct from the backend
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "../frontend"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||
{
|
||||
"name": "Lidarr",
|
||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [8686],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for more information:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# https://containers.dev/guide/dependabot
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "devcontainers"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
|
@ -126,6 +126,7 @@ coverage*.xml
|
|||
coverage*.json
|
||||
setup/Output/
|
||||
*.~is
|
||||
.mono
|
||||
|
||||
# VS outout folders
|
||||
bin
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-vscode-remote.remote-containers"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||
"name": "Run Lidarr",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build dotnet",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/_output/net6.0/Lidarr",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}",
|
||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||
"console": "integratedTerminal",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Attach",
|
||||
"type": "coreclr",
|
||||
"request": "attach"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build dotnet",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"msbuild",
|
||||
"-restore",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-p:GenerateFullPaths=true",
|
||||
"-p:Configuration=Debug",
|
||||
"-p:Platform=Posix",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "publish",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"publish",
|
||||
"${workspaceFolder}/src/Lidarr.sln",
|
||||
"-property:GenerateFullPaths=true",
|
||||
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"watch",
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/src/Lidarr.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -9,13 +9,13 @@ variables:
|
|||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '2.2.5'
|
||||
majorVersion: '2.3.1'
|
||||
minorVersion: $[counter('minorVersion', 1076)]
|
||||
lidarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(lidarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.417'
|
||||
dotnetVersion: '6.0.421'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
|
|
|
@ -28,7 +28,8 @@ module.exports = {
|
|||
globals: {
|
||||
expect: false,
|
||||
chai: false,
|
||||
sinon: false
|
||||
sinon: false,
|
||||
JSX: true
|
||||
},
|
||||
|
||||
parserOptions: {
|
||||
|
|
|
@ -5,6 +5,7 @@ import CommandAppState from './CommandAppState';
|
|||
import HistoryAppState from './HistoryAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
import TrackFilesAppState from './TrackFilesAppState';
|
||||
import TracksAppState from './TracksAppState';
|
||||
|
@ -62,6 +63,7 @@ interface AppState {
|
|||
tags: TagsAppState;
|
||||
trackFiles: TrackFilesAppState;
|
||||
tracksSelection: TracksAppState;
|
||||
system: SystemAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
@ -46,7 +47,7 @@ export interface RootFolderAppState
|
|||
AppSectionSaveState {}
|
||||
|
||||
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
|
@ -57,7 +58,7 @@ interface SettingsAppState {
|
|||
notifications: NotificationAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import SystemStatus from 'typings/SystemStatus';
|
||||
import { AppSectionItemState } from './AppSectionState';
|
||||
|
||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||
|
||||
interface SystemAppState {
|
||||
status: SystemStatusAppState;
|
||||
}
|
||||
|
||||
export default SystemAppState;
|
|
@ -1,12 +1,32 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
export interface TagDetail extends ModelBase {
|
||||
label: string;
|
||||
autoTagIds: number[];
|
||||
delayProfileIds: number[];
|
||||
downloadClientIds: [];
|
||||
importListIds: number[];
|
||||
indexerIds: number[];
|
||||
notificationIds: number[];
|
||||
restrictionIds: number[];
|
||||
artistIds: number[];
|
||||
}
|
||||
|
||||
export interface TagDetailAppState
|
||||
extends AppSectionState<TagDetail>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
|
||||
details: TagDetailAppState;
|
||||
}
|
||||
|
||||
export default TagsAppState;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import TagInputConnector from './TagInputConnector';
|
||||
|
||||
interface ArtistTagInputProps {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
onChange: ({
|
||||
name,
|
||||
value,
|
||||
}: {
|
||||
name: string;
|
||||
value: number | number[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function ArtistTagInput(props: ArtistTagInputProps) {
|
||||
const { value, onChange, ...otherProps } = props;
|
||||
const isArray = Array.isArray(value);
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||
if (isArray) {
|
||||
onChange({ name, value: newValue });
|
||||
} else {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
[isArray, onChange]
|
||||
);
|
||||
|
||||
let finalValue: number[] = [];
|
||||
|
||||
if (isArray) {
|
||||
finalValue = value;
|
||||
} else if (value === 0) {
|
||||
finalValue = [];
|
||||
} else {
|
||||
finalValue = [value];
|
||||
}
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||
<TagInputConnector
|
||||
{...otherProps}
|
||||
value={finalValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,7 @@ import Link from 'Components/Link/Link';
|
|||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AlbumReleaseSelectInputConnector from './AlbumReleaseSelectInputConnector';
|
||||
import ArtistTagInput from './ArtistTagInput';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
|
@ -99,6 +100,9 @@ function getComponent(type) {
|
|||
case inputTypes.DYNAMIC_SELECT:
|
||||
return EnhancedSelectInputConnector;
|
||||
|
||||
case inputTypes.ARTIST_TAG:
|
||||
return ArtistTagInput;
|
||||
|
||||
case inputTypes.SERIES_TYPE_SELECT:
|
||||
return SeriesTypeSelectInput;
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||
return inputTypes.DYNAMIC_SELECT;
|
||||
}
|
||||
return inputTypes.SELECT;
|
||||
case 'artistTag':
|
||||
return inputTypes.ARTIST_TAG;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
case 'tagSelect':
|
||||
|
|
|
@ -20,6 +20,7 @@ export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
|||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const SERIES_TYPE_SELECT = 'artistTypeSelect';
|
||||
export const ARTIST_TAG = 'artistTag';
|
||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||
export const TAG = 'tag';
|
||||
export const TAG_SELECT = 'tagSelect';
|
||||
|
@ -49,6 +50,7 @@ export const all = [
|
|||
DOWNLOAD_CLIENT_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
ARTIST_TAG,
|
||||
DYNAMIC_SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
TAG,
|
||||
|
|
|
@ -12,7 +12,7 @@ export default function TagInUse(props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (count > 1 && labelPlural ) {
|
||||
if (count > 1 && labelPlural) {
|
||||
return (
|
||||
<div>
|
||||
{count} {labelPlural.toLowerCase()}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastTestData = null;
|
||||
|
||||
export function createCancelTestProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
|
@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
|||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isTesting: true }));
|
||||
|
||||
const testData = getProviderState(payload, getState, section);
|
||||
const {
|
||||
queryParams = {},
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const testData = getProviderState({ ...otherPayload }, getState, section);
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-testing the same provider without changes
|
||||
// force it to be tested.
|
||||
|
||||
if (_.isEqual(testData, lastTestData)) {
|
||||
params.forceTest = true;
|
||||
}
|
||||
|
||||
lastTestData = testData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/test`,
|
||||
url: `${url}/test?${$.param(params, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
|
@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
|||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastTestData = null;
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
isTesting: false,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createAllArtistSelector() {
|
||||
return createSelector(
|
||||
(state) => state.artist,
|
||||
(state: AppState) => state.artist,
|
||||
(artist) => {
|
||||
return artist.items;
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createArtistCountSelector() {
|
||||
return createSelector(
|
||||
createAllArtistSelector(),
|
||||
(state) => state.artist.error,
|
||||
(state) => state.artist.isFetching,
|
||||
(state) => state.artist.isPopulated,
|
||||
(state: AppState) => state.artist.error,
|
||||
(state: AppState) => state.artist.isFetching,
|
||||
(state: AppState) => state.artist.isPopulated,
|
||||
(artists, error, isFetching, isPopulated) => {
|
||||
return {
|
||||
count: artists.length,
|
||||
error,
|
||||
isFetching,
|
||||
isPopulated
|
||||
isPopulated,
|
||||
};
|
||||
}
|
||||
);
|
|
@ -2,13 +2,10 @@ import { createSelector } from 'reselect';
|
|||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import createCommandSelector from './createCommandSelector';
|
||||
|
||||
function createCommandExecutingSelector(name, contraints = {}) {
|
||||
return createSelector(
|
||||
createCommandSelector(name, contraints),
|
||||
(command) => {
|
||||
return isCommandExecuting(command);
|
||||
}
|
||||
);
|
||||
function createCommandExecutingSelector(name: string, contraints = {}) {
|
||||
return createSelector(createCommandSelector(name, contraints), (command) => {
|
||||
return isCommandExecuting(command);
|
||||
});
|
||||
}
|
||||
|
||||
export default createCommandExecutingSelector;
|
|
@ -1,14 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import createCommandsSelector from './createCommandsSelector';
|
||||
|
||||
function createCommandSelector(name, contraints = {}) {
|
||||
return createSelector(
|
||||
createCommandsSelector(),
|
||||
(commands) => {
|
||||
return findCommand(commands, { name, ...contraints });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createCommandSelector;
|
|
@ -0,0 +1,11 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { findCommand } from 'Utilities/Command';
|
||||
import createCommandsSelector from './createCommandsSelector';
|
||||
|
||||
function createCommandSelector(name: string, contraints = {}) {
|
||||
return createSelector(createCommandsSelector(), (commands) => {
|
||||
return findCommand(commands, { name, ...contraints });
|
||||
});
|
||||
}
|
||||
|
||||
export default createCommandSelector;
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createCommandsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.commands,
|
||||
(state: AppState) => state.commands,
|
||||
(commands) => {
|
||||
return commands.items;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
|
||||
const createDeepEqualSelector = createSelectorCreator(
|
||||
defaultMemoize,
|
||||
_.isEqual
|
||||
);
|
||||
|
||||
export default createDeepEqualSelector;
|
|
@ -0,0 +1,6 @@
|
|||
import { isEqual } from 'lodash';
|
||||
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||
|
||||
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
|
||||
|
||||
export default createDeepEqualSelector;
|
|
@ -1,9 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
|
||||
function createExecutingCommandsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.commands.items,
|
||||
(state: AppState) => state.commands.items,
|
||||
(commands) => {
|
||||
return commands.filter((command) => isCommandExecuting(command));
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
import _ from 'lodash';
|
||||
import { some } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createExistingArtistSelector() {
|
||||
return createSelector(
|
||||
(state, { foreignArtistId }) => foreignArtistId,
|
||||
(_: AppState, { foreignArtistId }: { foreignArtistId: string }) =>
|
||||
foreignArtistId,
|
||||
createAllArtistSelector(),
|
||||
(foreignArtistId, artist) => {
|
||||
return _.some(artist, { foreignArtistId });
|
||||
return some(artist, { foreignArtistId });
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
function createMetadataProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { metadataProfileId }) => metadataProfileId,
|
||||
(state) => state.settings.metadataProfiles.items,
|
||||
(metadataProfileId, metadataProfiles) => {
|
||||
return metadataProfiles.find((profile) => {
|
||||
return profile.id === metadataProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMetadataProfileSelector;
|
|
@ -0,0 +1,17 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createMetadataProfileSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { metadataProfileId }: { metadataProfileId: number }) =>
|
||||
metadataProfileId,
|
||||
(state: AppState) => state.settings.metadataProfiles.items,
|
||||
(metadataProfileId, metadataProfiles) => {
|
||||
return metadataProfiles.find(
|
||||
(profile) => profile.id === metadataProfileId
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMetadataProfileSelector;
|
|
@ -1,24 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createProfileInUseSelector(profileProp) {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
createAllArtistSelector(),
|
||||
(state) => state.settings.importLists.items,
|
||||
(id, artist, lists) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_.some(artist, { [profileProp]: id }) || _.some(lists, { [profileProp]: id })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createProfileInUseSelector;
|
|
@ -0,0 +1,25 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ImportList from 'typings/ImportList';
|
||||
import createAllArtistSelector from './createAllArtistSelector';
|
||||
|
||||
function createProfileInUseSelector(profileProp: string) {
|
||||
return createSelector(
|
||||
(_: AppState, { id }: { id: number }) => id,
|
||||
createAllArtistSelector(),
|
||||
(state: AppState) => state.settings.importLists.items,
|
||||
(id, artists, lists) => {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
artists.some((a) => a[profileProp as keyof Artist] === id) ||
|
||||
lists.some((list) => list[profileProp as keyof ImportList] === id)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createProfileInUseSelector;
|
|
@ -1,26 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
|
||||
export function createQualityProfileSelectorForHook(qualityProfileId) {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => {
|
||||
return profile.id === qualityProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createQualityProfileSelector() {
|
||||
return createSelector(
|
||||
(state, { qualityProfileId }) => qualityProfileId,
|
||||
(state) => state.settings.qualityProfiles.items,
|
||||
(qualityProfileId, qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => {
|
||||
return profile.id === qualityProfileId;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createQualityProfileSelector;
|
|
@ -0,0 +1,24 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export function createQualityProfileSelectorForHook(qualityProfileId: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createQualityProfileSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { qualityProfileId }: { qualityProfileId: number }) =>
|
||||
qualityProfileId,
|
||||
(state: AppState) => state.settings.qualityProfiles.items,
|
||||
(qualityProfileId, qualityProfiles) => {
|
||||
return qualityProfiles.find((profile) => profile.id === qualityProfileId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createQualityProfileSelector;
|
|
@ -1,21 +1,16 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createQueueItemSelector() {
|
||||
return createSelector(
|
||||
(state, { albumId }) => albumId,
|
||||
(state) => state.queue.details.items,
|
||||
(_: AppState, { albumId }: { albumId: number }) => albumId,
|
||||
(state: AppState) => state.queue.details.items,
|
||||
(albumId, details) => {
|
||||
if (!albumId || !details) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return details.find((item) => {
|
||||
if (item.album) {
|
||||
return item.album.id === albumId;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
return details.find((item) => item.albumId === albumId);
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createSystemStatusSelector() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
(state: AppState) => state.system.status,
|
||||
(status) => {
|
||||
return status.item;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createTagDetailsSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.tags.details.items,
|
||||
(_: AppState, { id }: { id: number }) => id,
|
||||
(state: AppState) => state.tags.details.items,
|
||||
(id, tagDetails) => {
|
||||
return tagDetails.find((t) => t.id === id);
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createTagsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.tags.items,
|
||||
(state: AppState) => state.tags.items,
|
||||
(tags) => {
|
||||
return tags;
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createTrackFileSelector() {
|
||||
return createSelector(
|
||||
(state, { trackFileId }) => trackFileId,
|
||||
(state) => state.trackFiles,
|
||||
(_: AppState, { trackFileId }: { trackFileId: number }) => trackFileId,
|
||||
(state: AppState) => state.trackFiles,
|
||||
(trackFileId, trackFiles) => {
|
||||
if (!trackFileId) {
|
||||
return;
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createUISettingsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.ui,
|
||||
(state: AppState) => state.settings.ui,
|
||||
(ui) => {
|
||||
return ui.item;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
interface SystemStatus {
|
||||
appData: string;
|
||||
appName: string;
|
||||
authentication: string;
|
||||
branch: string;
|
||||
buildTime: string;
|
||||
instanceName: string;
|
||||
isAdmin: boolean;
|
||||
isDebug: boolean;
|
||||
isDocker: boolean;
|
||||
isLinux: boolean;
|
||||
isNetCore: boolean;
|
||||
isOsx: boolean;
|
||||
isProduction: boolean;
|
||||
isUserInteractive: boolean;
|
||||
isWindows: boolean;
|
||||
migrationVersion: number;
|
||||
mode: string;
|
||||
osName: string;
|
||||
osVersion: string;
|
||||
packageUpdateMechanism: string;
|
||||
runtimeName: string;
|
||||
runtimeVersion: string;
|
||||
sqliteVersion: string;
|
||||
startTime: string;
|
||||
startupPath: string;
|
||||
urlBase: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export default SystemStatus;
|
42
package.json
42
package.json
|
@ -34,9 +34,9 @@
|
|||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.16.14",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@types/node": "18.19.31",
|
||||
"@types/react": "18.2.79",
|
||||
"@types/react-dom": "18.2.25",
|
||||
"ansi-colors": "4.1.3",
|
||||
"classnames": "2.3.2",
|
||||
"clipboard": "2.0.11",
|
||||
|
@ -87,47 +87,47 @@
|
|||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.1.8",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"typescript": "4.9.5"
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.11",
|
||||
"@babel/eslint-parser": "7.22.11",
|
||||
"@babel/plugin-proposal-export-default-from": "7.22.5",
|
||||
"@babel/core": "7.24.4",
|
||||
"@babel/eslint-parser": "7.24.1",
|
||||
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.22.14",
|
||||
"@babel/preset-react": "7.22.5",
|
||||
"@babel/preset-typescript": "7.22.11",
|
||||
"@types/lodash": "4.14.197",
|
||||
"@babel/preset-env": "7.24.4",
|
||||
"@babel/preset-react": "7.24.1",
|
||||
"@babel/preset-typescript": "7.24.1",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/react-lazyload": "3.2.0",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.14.1",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@types/redux-actions": "2.6.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.7",
|
||||
"@typescript-eslint/parser": "5.59.7",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@typescript-eslint/parser": "6.21.0",
|
||||
"autoprefixer": "10.4.14",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-inline-classnames": "2.0.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"core-js": "3.32.1",
|
||||
"core-js": "3.37.0",
|
||||
"css-loader": "6.7.3",
|
||||
"css-modules-typescript-loader": "4.0.1",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-prettier": "8.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-json": "3.1.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react": "7.34.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||
"file-loader": "6.2.0",
|
||||
"filemanager-webpack-plugin": "8.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||
"html-webpack-plugin": "5.5.1",
|
||||
"loader-utils": "^3.2.1",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
"postcss": "8.4.23",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"postcss": "8.4.38",
|
||||
"postcss-color-function": "4.1.0",
|
||||
"postcss-loader": "7.3.0",
|
||||
"postcss-mixins": "9.0.4",
|
||||
|
|
|
@ -101,8 +101,8 @@
|
|||
|
||||
<!-- Standard testing packages -->
|
||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||
</ItemGroup>
|
||||
|
@ -147,16 +147,46 @@
|
|||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<!--
|
||||
Set architecture to RuntimeInformation.ProcessArchitecture if not specified -->
|
||||
<Choose>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">
|
||||
<PropertyGroup>
|
||||
<Architecture>x64</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'">
|
||||
<PropertyGroup>
|
||||
<Architecture>x86</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">
|
||||
<PropertyGroup>
|
||||
<Architecture>arm64</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'">
|
||||
<PropertyGroup>
|
||||
<Architecture>arm</Architecture>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup>
|
||||
<Architecture></Architecture>
|
||||
</PropertyGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
||||
'$(RuntimeIdentifier)' == ''">
|
||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
||||
'$(RuntimeIdentifier)' == ''">
|
||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
||||
|
|
|
@ -204,10 +204,10 @@ namespace Lidarr.Api.V1
|
|||
[SkipValidation(true, false)]
|
||||
[HttpPost("test")]
|
||||
[Consumes("application/json")]
|
||||
public object Test([FromBody] TProviderResource providerResource)
|
||||
public object Test([FromBody] TProviderResource providerResource, [FromQuery] bool forceTest = false)
|
||||
{
|
||||
var existingDefinition = providerResource.Id > 0 ? _providerFactory.Find(providerResource.Id) : null;
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, true, true, true);
|
||||
var providerDefinition = GetDefinition(providerResource, existingDefinition, true, !forceTest, true);
|
||||
|
||||
Test(providerDefinition, true);
|
||||
|
||||
|
|
|
@ -2302,6 +2302,16 @@
|
|||
"tags": [
|
||||
"DownloadClient"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -3208,6 +3218,16 @@
|
|||
"tags": [
|
||||
"ImportList"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -3705,6 +3725,16 @@
|
|||
"tags": [
|
||||
"Indexer"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -4584,6 +4614,16 @@
|
|||
"tags": [
|
||||
"Metadata"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
@ -5516,6 +5556,16 @@
|
|||
"tags": [
|
||||
"Notification"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "forceTest",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Tags;
|
||||
|
@ -46,5 +49,35 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
|||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_delete_used_auto_tagging_tag_specification_tags()
|
||||
{
|
||||
var tags = Builder<Tag>
|
||||
.CreateListOfSize(2)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.BuildList();
|
||||
Db.InsertMany(tags);
|
||||
|
||||
var autoTags = Builder<AutoTag>.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(x => x.Id = 0)
|
||||
.With(x => x.Specifications = new List<IAutoTaggingSpecification>
|
||||
{
|
||||
new TagSpecification
|
||||
{
|
||||
Name = "Test",
|
||||
Value = tags[0].Id
|
||||
}
|
||||
})
|
||||
.BuildList();
|
||||
|
||||
Mocker.GetMock<IAutoTaggingRepository>().Setup(s => s.All())
|
||||
.Returns(autoTags);
|
||||
|
||||
Subject.Clean();
|
||||
AllStoredModels.Should().HaveCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-04-20 00:00:00Z")]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-06-20 00:00:00Z")]
|
||||
public void map_artist_should_work()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
@ -159,7 +159,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-04-20 00:00:00Z")]
|
||||
[Ignore("Pending mapping fixes", Until = "2024-06-20 00:00:00Z")]
|
||||
public void map_album_should_work()
|
||||
{
|
||||
UseRealHttp();
|
||||
|
|
|
@ -86,7 +86,8 @@ namespace NzbDrone.Core.Annotations
|
|||
TagSelect,
|
||||
RootFolder,
|
||||
QualityProfile,
|
||||
MetadataProfile
|
||||
MetadataProfile,
|
||||
ArtistTag
|
||||
}
|
||||
|
||||
public enum HiddenType
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.AutoTagging.Specifications
|
||||
{
|
||||
public class TagSpecificationValidator : AbstractValidator<TagSpecification>
|
||||
{
|
||||
public TagSpecificationValidator()
|
||||
{
|
||||
RuleFor(c => c.Value).GreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
public class TagSpecification : AutoTaggingSpecificationBase
|
||||
{
|
||||
private static readonly TagSpecificationValidator Validator = new ();
|
||||
|
||||
public override int Order => 1;
|
||||
public override string ImplementationName => "Tag";
|
||||
|
||||
[FieldDefinition(1, Label = "AutoTaggingSpecificationTag", Type = FieldType.ArtistTag)]
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override bool IsSatisfiedByWithoutNegate(Artist artist)
|
||||
{
|
||||
return artist.Tags.Contains(Value);
|
||||
}
|
||||
|
||||
public override NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ using System.Data;
|
|||
using System.Linq;
|
||||
using Dapper;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||
|
@ -10,17 +12,24 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
|||
public class CleanupUnusedTags : IHousekeepingTask
|
||||
{
|
||||
private readonly IMainDatabase _database;
|
||||
private readonly IAutoTaggingRepository _autoTaggingRepository;
|
||||
|
||||
public CleanupUnusedTags(IMainDatabase database)
|
||||
public CleanupUnusedTags(IMainDatabase database, IAutoTaggingRepository autoTaggingRepository)
|
||||
{
|
||||
_database = database;
|
||||
_autoTaggingRepository = autoTaggingRepository;
|
||||
}
|
||||
|
||||
public void Clean()
|
||||
{
|
||||
using var mapper = _database.OpenConnection();
|
||||
var usedTags = new[] { "Artists", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" }
|
||||
var usedTags = new[]
|
||||
{
|
||||
"Artists", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers",
|
||||
"AutoTagging", "DownloadClients"
|
||||
}
|
||||
.SelectMany(v => GetUsedTags(v, mapper))
|
||||
.Concat(GetAutoTaggingTagSpecificationTags(mapper))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
|
@ -45,10 +54,31 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
|
|||
|
||||
private int[] GetUsedTags(string table, IDbConnection mapper)
|
||||
{
|
||||
return mapper.Query<List<int>>($"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
|
||||
return mapper
|
||||
.Query<List<int>>(
|
||||
$"SELECT DISTINCT \"Tags\" FROM \"{table}\" WHERE NOT \"Tags\" = '[]' AND NOT \"Tags\" IS NULL")
|
||||
.SelectMany(x => x)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private List<int> GetAutoTaggingTagSpecificationTags(IDbConnection mapper)
|
||||
{
|
||||
var tags = new List<int>();
|
||||
var autoTags = _autoTaggingRepository.All();
|
||||
|
||||
foreach (var autoTag in autoTags)
|
||||
{
|
||||
foreach (var specification in autoTag.Specifications)
|
||||
{
|
||||
if (specification is TagSpecification tagSpec)
|
||||
{
|
||||
tags.Add(tagSpec.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using NLog;
|
|||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.MetadataSource;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
@ -22,13 +23,18 @@ namespace NzbDrone.Core.ImportLists.Spotify
|
|||
IConfigService configService,
|
||||
IParsingService parsingService,
|
||||
IHttpClient httpClient,
|
||||
ILocalizationService localizationService,
|
||||
Logger logger)
|
||||
: base(spotifyProxy, requestBuilder, importListStatusService, importListRepository, configService, parsingService, httpClient, logger)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
private const string LIKED_SONGS_ID = "LikedSongs";
|
||||
public override string Name => "Spotify Playlists";
|
||||
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public override IList<SpotifyImportListItemInfo> Fetch(SpotifyWebAPI api)
|
||||
{
|
||||
return Settings.PlaylistIds.SelectMany(x => Fetch(api, x)).ToList();
|
||||
|
@ -40,7 +46,27 @@ namespace NzbDrone.Core.ImportLists.Spotify
|
|||
|
||||
_logger.Trace($"Processing playlist {playlistId}");
|
||||
|
||||
var playlistTracks = _spotifyProxy.GetPlaylistTracks(this, api, playlistId, "next, items(track(name, artists(id, name), album(id, name, release_date, release_date_precision, artists(id, name))))");
|
||||
Paging<PlaylistTrack> playlistTracks;
|
||||
|
||||
if (playlistId.Equals(LIKED_SONGS_ID))
|
||||
{
|
||||
var savedTracks = _spotifyProxy.GetSavedTracks(this, api);
|
||||
playlistTracks = new Paging<PlaylistTrack>
|
||||
{
|
||||
Href = savedTracks.Href,
|
||||
Limit = savedTracks.Limit,
|
||||
Offset = savedTracks.Offset,
|
||||
Next = savedTracks.Next,
|
||||
Previous = savedTracks.Previous,
|
||||
Total = savedTracks.Total,
|
||||
Error = savedTracks.Error,
|
||||
Items = savedTracks.Items.Select(t => new PlaylistTrack { AddedAt = t.AddedAt, Track = t.Track }).ToList()
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
playlistTracks = _spotifyProxy.GetPlaylistTracks(this, api, playlistId, "next, items(track(name, artists(id, name), album(id, name, release_date, release_date_precision, artists(id, name))))");
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
|
@ -140,7 +166,7 @@ namespace NzbDrone.Core.ImportLists.Spotify
|
|||
{
|
||||
id = p.Id,
|
||||
name = p.Name
|
||||
})
|
||||
}).Prepend(new { id = LIKED_SONGS_ID, name = _localizationService.GetLocalizedString("LikedSongs") })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace NzbDrone.Core.ImportLists.Spotify
|
|||
PlaylistIds = System.Array.Empty<string>();
|
||||
}
|
||||
|
||||
public override string Scope => "playlist-read-private";
|
||||
public override string Scope => "playlist-read-private user-library-read";
|
||||
|
||||
[FieldDefinition(1, Label = "Playlists", Type = FieldType.Playlist)]
|
||||
public IEnumerable<string> PlaylistIds { get; set; }
|
||||
|
|
|
@ -18,6 +18,8 @@ namespace NzbDrone.Core.ImportLists.Spotify
|
|||
where TSettings : SpotifySettingsBase<TSettings>, new();
|
||||
Paging<PlaylistTrack> GetPlaylistTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, string id, string fields)
|
||||
where TSettings : SpotifySettingsBase<TSettings>, new();
|
||||
Paging<SavedTrack> GetSavedTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
|
||||
where TSettings : SpotifySettingsBase<TSettings>, new();
|
||||
Paging<T> GetNextPage<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Paging<T> item)
|
||||
where TSettings : SpotifySettingsBase<TSettings>, new();
|
||||
FollowedArtists GetNextPage<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, FollowedArtists item)
|
||||
|
@ -63,6 +65,12 @@ namespace NzbDrone.Core.ImportLists.Spotify
|
|||
return Execute(list, api, x => x.GetPlaylistTracks(id, fields: fields));
|
||||
}
|
||||
|
||||
public Paging<SavedTrack> GetSavedTracks<TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api)
|
||||
where TSettings : SpotifySettingsBase<TSettings>, new()
|
||||
{
|
||||
return Execute(list, api, x => x.GetSavedTracks(50));
|
||||
}
|
||||
|
||||
public Paging<T> GetNextPage<T, TSettings>(SpotifyImportListBase<TSettings> list, SpotifyWebAPI api, Paging<T> item)
|
||||
where TSettings : SpotifySettingsBase<TSettings>, new()
|
||||
{
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Dapper" Version="2.0.151" />
|
||||
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
|
||||
<PackageReference Include="Polly" Version="8.3.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||
<PackageReference Include="System.Memory" Version="4.5.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.25" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.29" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
|
||||
<PackageReference Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||
|
@ -23,10 +23,9 @@
|
|||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
|
||||
<PackageReference Include="TagLibSharp-Lidarr" Version="2.2.0.27" />
|
||||
<PackageReference Include="Kveer.XmlRPC" Version="1.2.0" />
|
||||
<PackageReference Include="Npgsql" Version="7.0.6" />
|
||||
<PackageReference Include="SpotifyAPI.Web" Version="5.1.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
|
||||
<PackageReference Include="Equ" Version="2.3.0" />
|
||||
<PackageReference Include="MonoTorrent" Version="2.0.7" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -724,5 +724,17 @@
|
|||
"ImportLists": "القوائم",
|
||||
"ArtistIndexFooterDownloading": "جارى التحميل",
|
||||
"ImportList": "القوائم",
|
||||
"AutomaticSearch": "البحث التلقائي"
|
||||
"AutomaticSearch": "البحث التلقائي",
|
||||
"FormatAgeMinutes": "الدقائق",
|
||||
"IndexerFlags": "أعلام المفهرس",
|
||||
"CustomFilter": "مرشحات مخصصة",
|
||||
"FormatAgeMinute": "الدقائق",
|
||||
"MonitorNoAlbums": "لا شيء",
|
||||
"Yesterday": "في الامس",
|
||||
"FormatAgeHours": "ساعات",
|
||||
"FormatAgeHour": "ساعات",
|
||||
"Tomorrow": "غدا",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "لم يتمكن Radarr من تحديد الفيلم الذي كان هذا الإصدار من أجله. قد يتعذر على Radarr استيراد هذا الإصدار تلقائيًا. هل تريد انتزاع \"{0}\"؟",
|
||||
"AddToDownloadQueue": "إضافة إلى قائمة انتظار التنزيل",
|
||||
"AddedToDownloadQueue": "تمت الإضافة إلى قائمة انتظار التنزيلات"
|
||||
}
|
||||
|
|
|
@ -726,5 +726,15 @@
|
|||
"MonitoredStatus": "Наблюдавано / Състояние",
|
||||
"ArtistIndexFooterDownloading": "Изтегляне",
|
||||
"ImportList": "Списъци",
|
||||
"AutomaticSearch": "Автоматично търсене"
|
||||
"AutomaticSearch": "Автоматично търсене",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "Radarr не успя да определи за кой филм е предназначено това издание. Radarr може да не може автоматично да импортира тази версия. Искате ли да вземете „{0}“?",
|
||||
"CustomFilter": "Персонализирани филтри",
|
||||
"FormatAgeHour": "Часа",
|
||||
"FormatAgeHours": "Часа",
|
||||
"FormatAgeMinute": "Минути",
|
||||
"FormatAgeMinutes": "Минути",
|
||||
"MonitorNoAlbums": "Нито един",
|
||||
"Tomorrow": "Утре",
|
||||
"Yesterday": "Вчера",
|
||||
"IndexerFlags": "Индексиращи знамена"
|
||||
}
|
||||
|
|
|
@ -915,5 +915,27 @@
|
|||
"DownloadClientDelugeSettingsDirectory": "Directori de baixada",
|
||||
"DownloadClientDelugeSettingsDirectoryCompleted": "Directori al qual es mou quan s'hagi completat",
|
||||
"DownloadClientDelugeSettingsDirectoryCompletedHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge"
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "Ubicació opcional de les baixades completades, deixeu-lo en blanc per utilitzar la ubicació predeterminada de Deluge",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "{appName} no ha pogut determinar per a quina pel·lícula era aquest llançament. És possible que {appName} no pugui importar automàticament aquesta versió. Voleu capturar \"{0}\"?",
|
||||
"IndexerFlags": "Indicadors de l'indexador",
|
||||
"MonitorNoAlbums": "Cap",
|
||||
"Rejections": "Rebutjats",
|
||||
"Yesterday": "Ahir",
|
||||
"FormatAgeDay": "dia",
|
||||
"FormatAgeDays": "dies",
|
||||
"FormatAgeHour": "hora",
|
||||
"FormatAgeHours": "hores",
|
||||
"FormatDateTimeRelative": "{relativeDay}, {formattedDate} {formattedTime}",
|
||||
"FormatDateTime": "{formattedDate} {formattedTime}",
|
||||
"FormatRuntimeHours": "{hours}h",
|
||||
"FormatRuntimeMinutes": "{minutes}m",
|
||||
"FormatShortTimeSpanHours": "{hours} hora(es)",
|
||||
"FormatShortTimeSpanMinutes": "{minutes} minut(s)",
|
||||
"FormatShortTimeSpanSeconds": "{seconds} segon(s)",
|
||||
"FormatTimeSpanDays": "{days}d {time}",
|
||||
"Tomorrow": "Demà",
|
||||
"AddToDownloadQueue": "Afegeix a la cua de baixades",
|
||||
"AddedToDownloadQueue": "Afegit a la cua de baixades",
|
||||
"FormatAgeMinute": "minut",
|
||||
"FormatAgeMinutes": "Minuts"
|
||||
}
|
||||
|
|
|
@ -842,5 +842,20 @@
|
|||
"UiSettingsSummary": "Možnosti kalendáře, data a barev",
|
||||
"CustomFormatsSettingsSummary": "Vlastní formáty a nastavení",
|
||||
"ArtistIndexFooterDownloading": "Stahování",
|
||||
"AutomaticSearch": "Vyhledat automaticky"
|
||||
"AutomaticSearch": "Vyhledat automaticky",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "{appName} nebyl schopen určit, pro který film je toto vydání určeno. {appName} nemusí být schopen toto vydání automaticky importovat. Chcete chytit „{0}“?",
|
||||
"IndexerFlags": "Příznaky indexeru",
|
||||
"CustomFilter": "Vlastní filtry",
|
||||
"FormatAgeHour": "hodina",
|
||||
"FormatAgeHours": "hodin",
|
||||
"FormatAgeMinute": "minuta",
|
||||
"FormatAgeMinutes": "minut",
|
||||
"MonitorNoAlbums": "Žádný",
|
||||
"Tomorrow": "Zítra",
|
||||
"FormatAgeDay": "den",
|
||||
"FormatAgeDays": "dnů",
|
||||
"FormatDateTime": "{formattedDate} {formattedTime}",
|
||||
"Yesterday": "Včera",
|
||||
"AddToDownloadQueue": "Přidat stahování do fronty",
|
||||
"AddedToDownloadQueue": "Stahování přidáno do fronty"
|
||||
}
|
||||
|
|
|
@ -743,5 +743,23 @@
|
|||
"ApplyChanges": "Anvend ændringer",
|
||||
"AddDownloadClientImplementation": "Tilføj downloadklient - {implementationName}",
|
||||
"AddImportList": "Tilføj importliste",
|
||||
"AddImportListImplementation": "Tilføj importliste - {implementationName}"
|
||||
"AddImportListImplementation": "Tilføj importliste - {implementationName}",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "{appName} var ikke i stand til at bestemme, hvilken film denne udgivelse var til. {appName} kan muligvis ikke automatisk importere denne udgivelse. Vil du hente '{0}'?",
|
||||
"Album": "album",
|
||||
"EditImportListImplementation": "Tilføj importliste - {implementationName}",
|
||||
"CatalogNumber": "katalognummer",
|
||||
"CustomFilter": "Bruger Tilpassede Filtere",
|
||||
"FormatAgeMinutes": "Protokoller",
|
||||
"PreferredProtocol": "Foretrukken protokol",
|
||||
"Theme": "Tema",
|
||||
"EditDownloadClientImplementation": "Tilføj downloadklient - {implementationName}",
|
||||
"Discography": "diskografi",
|
||||
"ExpandAlbumByDefaultHelpText": "album",
|
||||
"FormatAgeHours": "Timer",
|
||||
"FormatAgeMinute": "Protokoller",
|
||||
"MonitorNoAlbums": "Ingen",
|
||||
"Tomorrow": "I morgen",
|
||||
"Yesterday": "I går",
|
||||
"Albums": "album",
|
||||
"IndexerFlags": "Indexer Flag"
|
||||
}
|
||||
|
|
|
@ -1084,5 +1084,22 @@
|
|||
"UiSettingsSummary": "Ημερολόγιο, ημερομηνία και επιλογές με προβλήματα χρώματος",
|
||||
"ArtistIndexFooterDownloading": "Λήψη",
|
||||
"AutomaticSearch": "Αυτόματη αναζήτηση",
|
||||
"KeyboardShortcuts": "Συντομεύσεις πληκτρολογίου"
|
||||
"KeyboardShortcuts": "Συντομεύσεις πληκτρολογίου",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "Ο {appName} δεν μπόρεσε να προσδιορίσει ποια ταινία ήταν αυτή η κυκλοφορία. Το {appName} ενδέχεται να μην μπορεί να εισαγάγει αυτόματα αυτήν την κυκλοφορία. Θέλετε να τραβήξετε το \"{0}\";",
|
||||
"IndexerFlags": "Σημαίες ευρετηρίου",
|
||||
"CustomFilter": "Custom Φιλτρα",
|
||||
"Tomorrow": "Αύριο",
|
||||
"Yesterday": "Εχθές",
|
||||
"FormatAgeHours": "Ωρες",
|
||||
"FormatAgeMinute": "Λεπτά",
|
||||
"FormatAgeMinutes": "Λεπτά",
|
||||
"MonitorAllAlbums": "Όλα Τα Άλμπουμ",
|
||||
"MonitorFirstAlbum": "Πρώτο άλμπουμ",
|
||||
"MonitorFutureAlbums": "Μελλοντικά άλμπουμ",
|
||||
"MonitorLastestAlbum": "Τελευταίο άλμπουμ",
|
||||
"MonitorMissingAlbums": "Λείπουν άλμπουμ",
|
||||
"MonitorNoAlbums": "Κανένας",
|
||||
"AddToDownloadQueue": "Προστέθηκε για λήψη ουράς",
|
||||
"AddedToDownloadQueue": "Προστέθηκε στην ουρά λήψης",
|
||||
"UseSsl": "Χρησιμοποιήστε SSL"
|
||||
}
|
||||
|
|
|
@ -146,6 +146,7 @@
|
|||
"AutoTaggingLoadError": "Unable to load auto tagging",
|
||||
"AutoTaggingNegateHelpText": "If checked, the auto tagging rule will not apply if this {implementationName} condition matches.",
|
||||
"AutoTaggingRequiredHelpText": "This {implementationName} condition must match for the auto tagging rule to apply. Otherwise a single {implementationName} match is sufficient.",
|
||||
"AutoTaggingSpecificationTag": "Tag",
|
||||
"Automatic": "Automatic",
|
||||
"AutomaticAdd": "Automatic Add",
|
||||
"AutomaticSearch": "Automatic Search",
|
||||
|
@ -639,6 +640,7 @@
|
|||
"LidarrSupportsAnyIndexerThatUsesTheNewznabStandardAsWellAsOtherIndexersListedBelow": "{appName} supports any indexer that uses the Newznab standard, as well as other indexers listed below.",
|
||||
"LidarrSupportsMultipleListsForImportingAlbumsAndArtistsIntoTheDatabase": "{appName} supports multiple lists for importing Albums and Artists into the database.",
|
||||
"LidarrTags": "{appName} Tags",
|
||||
"LikedSongs": "Liked Songs",
|
||||
"Links": "Links",
|
||||
"ListRefreshInterval": "List Refresh Interval",
|
||||
"ListWillRefreshEveryInterp": "List will refresh every {0}",
|
||||
|
@ -798,6 +800,8 @@
|
|||
"NotificationsSettingsUpdateMapPathsTo": "Map Paths To",
|
||||
"NotificationsSettingsUpdateMapPathsToHelpText": "{serviceName} path, used to modify series paths when {serviceName} sees library path location differently from {appName} (Requires 'Update Library')",
|
||||
"NotificationsSettingsUseSslHelpText": "Connect to {serviceName} over HTTPS instead of HTTP",
|
||||
"NotificationsTelegramSettingsIncludeAppName": "Include {appName} in Title",
|
||||
"NotificationsTelegramSettingsIncludeAppNameHelpText": "Optionally prefix message title with {appName} to differentiate notifications from different applications",
|
||||
"Ok": "Ok",
|
||||
"OnAlbumDelete": "On Album Delete",
|
||||
"OnApplicationUpdate": "On Application Update",
|
||||
|
|
|
@ -1055,5 +1055,6 @@
|
|||
"NotificationsKodiSettingsUpdateLibraryHelpText": "¿Actualiza la biblioteca durante Importar y renombrar?",
|
||||
"NotificationsSettingsUpdateMapPathsFromHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
|
||||
"NotificationsSettingsUpdateMapPathsToHelpText": "Ruta de {appName}, usado para modificar rutas de series cuando {serviceName} ve la ubicación de ruta de biblioteca de forma distinta a {appName} (Requiere 'Actualizar biblioteca')",
|
||||
"Menu": "Menú"
|
||||
"Menu": "Menú",
|
||||
"CustomFormatsSettingsTriggerInfo": "Un formato personalizado será aplicado al lanzamiento o archivo cuando coincida con al menos uno de los diferentes tipos de condición elegidos."
|
||||
}
|
||||
|
|
|
@ -1210,5 +1210,7 @@
|
|||
"CustomFormatsSettingsSummary": "Formats et paramètres personnalisés",
|
||||
"DownloadClientsSettingsSummary": "Clients de téléchargement, gestion des téléchargements et mappages de chemins d'accès à distance",
|
||||
"AddToDownloadQueue": "Ajouter à la file d'attente de téléchargement",
|
||||
"AddedToDownloadQueue": "Ajouté à la file d'attente de téléchargement"
|
||||
"AddedToDownloadQueue": "Ajouté à la file d'attente de téléchargement",
|
||||
"IncludeHealthWarnings": "Inclure les avertissements de santé",
|
||||
"CustomFormatsSettingsTriggerInfo": "Un format personnalisé sera appliqué à une version ou à un fichier lorsqu'il correspond à au moins un de chacun des différents types de conditions choisis."
|
||||
}
|
||||
|
|
|
@ -786,5 +786,17 @@
|
|||
"UiSettingsSummary": "Opcje z osłabionym kalendarzem, datą i kolorem",
|
||||
"ArtistIndexFooterDownloading": "Ściąganie",
|
||||
"ImportLists": "Listy",
|
||||
"AutomaticSearch": "Automatyczne wyszukiwanie"
|
||||
"AutomaticSearch": "Automatyczne wyszukiwanie",
|
||||
"IndexerFlags": "Flagi indeksujące",
|
||||
"CustomFilter": "Filtry niestandardowe",
|
||||
"FormatAgeHour": "godziny",
|
||||
"FormatAgeHours": "godziny",
|
||||
"MonitorNoAlbums": "Żaden",
|
||||
"Yesterday": "Wczoraj",
|
||||
"AddToDownloadQueue": "Dodaj do kolejki pobierania",
|
||||
"AddedToDownloadQueue": "Dodano do kolejki pobierania",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "Radarr nie był w stanie określić, dla którego filmu jest to wydanie. Radarr może nie być w stanie automatycznie zaimportować tej wersji. Czy chcesz złapać „{0}”?",
|
||||
"Tomorrow": "Jutro",
|
||||
"FormatAgeMinute": "Minuty",
|
||||
"FormatAgeMinutes": "Minuty"
|
||||
}
|
||||
|
|
|
@ -939,5 +939,18 @@
|
|||
"AnchorTooltip": "Este arquivo já está na sua biblioteca para uma versão que você está importando no momento",
|
||||
"ArtistFolderFormat": "Formato da pasta do artista",
|
||||
"AlbumIsDownloading": "O álbum está sendo baixado",
|
||||
"AlbumStudioTruncated": "Apenas os últimos 20 álbuns são mostrados, acesse os detalhes para ver todos os álbuns"
|
||||
"AlbumStudioTruncated": "Apenas os últimos 20 álbuns são mostrados, acesse os detalhes para ver todos os álbuns",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "O {appName} não pode determinar a que filme pertence esta versão. O {appName} pode ser incapaz de importar automaticamente esta versão. Deseja capturar \"{0}\"?",
|
||||
"IndexerFlags": "Sinalizadores do indexador",
|
||||
"CustomFilter": "Filtros personalizados",
|
||||
"FormatAgeHours": "Horas",
|
||||
"FormatAgeMinute": "Minutos",
|
||||
"FormatAgeMinutes": "Minutos",
|
||||
"MonitorAllAlbums": "Todos os Álbuns",
|
||||
"Tomorrow": "Amanhã",
|
||||
"Yesterday": "Ontem",
|
||||
"AddToDownloadQueue": "Adicionar à fila de download",
|
||||
"AddedToDownloadQueue": "Adicionado à fila de download",
|
||||
"UseSsl": "Usar SSL",
|
||||
"MonitorNoAlbums": "Nenhum"
|
||||
}
|
||||
|
|
|
@ -1185,7 +1185,7 @@
|
|||
"RemoveQueueItemRemovalMethod": "Método de Remoção",
|
||||
"RemoveQueueItemRemovalMethodHelpTextWarning": "'Remover do cliente de download' removerá o download e os arquivos do cliente de download.",
|
||||
"ArtistIndexFooterDownloading": "Baixando",
|
||||
"IncludeHealthWarnings": "Incluir Advertências de Saúde",
|
||||
"IncludeHealthWarnings": "Incluir Alertas de Saúde",
|
||||
"OnArtistAdd": "Ao Adicionar Artista",
|
||||
"Donate": "Doar",
|
||||
"Menu": "Menu",
|
||||
|
@ -1278,5 +1278,7 @@
|
|||
"IndexerFlags": "Sinalizadores do Indexador",
|
||||
"Rejections": "Rejeições",
|
||||
"SelectIndexerFlags": "Selecionar Sinalizadores do Indexador",
|
||||
"SetIndexerFlags": "Definir Sinalizadores de Indexador"
|
||||
"SetIndexerFlags": "Definir Sinalizadores de Indexador",
|
||||
"CustomFormatsSettingsTriggerInfo": "Um formato personalizado será aplicado a um lançamento ou arquivo quando corresponder a pelo menos um de cada um dos diferentes tipos de condição escolhidos.",
|
||||
"IndexerPriorityHelpText": "Prioridade do indexador de 1 (mais alta) a 50 (mais baixa). Padrão: 25. Usado ao capturar lançamentos como desempate para lançamentos iguais, {appName} ainda usará todos os indexadores habilitados para sincronização e pesquisa de RSS"
|
||||
}
|
||||
|
|
|
@ -786,5 +786,27 @@
|
|||
"ClickToChangeReleaseGroup": "Sürüm grubunu değiştirmek için tıklayın",
|
||||
"Clone": "Klon",
|
||||
"CloneAutoTag": "Otomatik Etiketi Klonla",
|
||||
"CloneCondition": "Klon Durumu"
|
||||
"CloneCondition": "Klon Durumu",
|
||||
"ClickToChangeIndexerFlags": "Dizin oluşturucu bayraklarını değiştirmek için tıklayın",
|
||||
"IndexerFlags": "Dizin Oluşturucu Bayrakları",
|
||||
"ApiKeyValidationHealthCheckMessage": "Lütfen API anahtarınızı en az {length} karakter uzunluğunda olacak şekilde güncelleyin. Bunu ayarlar veya yapılandırma dosyası aracılığıyla yapabilirsiniz",
|
||||
"PreferredProtocol": "Tercih Edilen Protokol",
|
||||
"ChooseImportMethod": "İçe Aktarma Modunu Seçin",
|
||||
"MinimumCustomFormatScoreHelpText": "Tercih edilen protokolde gecikmeyi atlamak için gereken Minimum Özel Format Puanı",
|
||||
"CountDownloadClientsSelected": "{count} indirme istemcisi seçildi",
|
||||
"FormatAgeMinute": "Dakika",
|
||||
"FormatAgeMinutes": "Dakika",
|
||||
"MonitorNoAlbums": "Yok",
|
||||
"Tomorrow": "Yarın",
|
||||
"CountArtistsSelected": "{count} içe aktarma listesi seçildi",
|
||||
"AddToDownloadQueue": "İndirme kuyruğuna ekleyin",
|
||||
"AddedToDownloadQueue": "İndirme sırasına eklendi",
|
||||
"BypassIfAboveCustomFormatScore": "Özel Format Koşullarının Üstündeyse Baypas Et",
|
||||
"BypassIfAboveCustomFormatScoreHelpText": "Sürümün puanı, yapılandırılan minimum özel format puanından yüksek olduğunda bypass'ı etkinleştirin",
|
||||
"BypassIfHighestQuality": "En Yüksek Kalitedeyse Atla",
|
||||
"BypassIfHighestQualityHelpText": "Tercih edilen protokolle kalite profilinde en yüksek etkin kaliteye sahip sürüm olduğunda gecikmeyi atlayın",
|
||||
"CustomFormatsSpecificationFlag": "Bayrak",
|
||||
"FormatAgeHours": "Saatler",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "{appName}, bu sürümün hangi film için olduğunu belirleyemedi. {appName} bu sürümü otomatik olarak içe aktaramayabilir. '{0}' almak istiyor musunuz?",
|
||||
"Yesterday": "Dün"
|
||||
}
|
||||
|
|
|
@ -726,5 +726,16 @@
|
|||
"TagsSettingsSummary": "Xem tất cả các thẻ và cách chúng được sử dụng. Các thẻ không sử dụng có thể bị xóa",
|
||||
"UiSettingsSummary": "Lịch, ngày tháng và các tùy chọn bị suy giảm màu sắc",
|
||||
"ArtistIndexFooterDownloading": "Đang tải xuống",
|
||||
"AutomaticSearch": "Tìm kiếm tự động"
|
||||
"AutomaticSearch": "Tìm kiếm tự động",
|
||||
"IndexerFlags": "Cờ chỉ mục",
|
||||
"CustomFilter": "Bộ lọc tùy chỉnh",
|
||||
"FormatAgeHours": "Giờ",
|
||||
"FormatAgeMinute": "Phút",
|
||||
"MonitorNoAlbums": "không ai",
|
||||
"Tomorrow": "Ngày mai",
|
||||
"Yesterday": "Hôm qua",
|
||||
"AddToDownloadQueue": "Thêm vào hàng đợi tải xuống",
|
||||
"AddedToDownloadQueue": "Đã thêm vào hàng đợi tải xuống",
|
||||
"FormatAgeMinutes": "Phút",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "{appName} không thể xác định bộ phim này được phát hành. {appName} có thể không tự động nhập bản phát hành này. Bạn có muốn lấy '{0}' không?"
|
||||
}
|
||||
|
|
|
@ -1242,5 +1242,25 @@
|
|||
"BlocklistAndSearchMultipleHint": "列入黑名单后开始搜索替代版本",
|
||||
"BlocklistMultipleOnlyHint": "无需搜索替换的黑名单",
|
||||
"BlocklistOnly": "仅限黑名单",
|
||||
"BlocklistOnlyHint": "无需寻找替代版本的黑名单"
|
||||
"BlocklistOnlyHint": "无需寻找替代版本的黑名单",
|
||||
"GrabReleaseUnknownArtistOrAlbumMessageText": "{appName}无法确定这个发布版本是哪部剧集的哪一集,{appName}可能无法自动导入此版本,你想要获取“{title}”吗?",
|
||||
"NotificationsKodiSettingAlwaysUpdate": "总是更新",
|
||||
"NotificationsKodiSettingsCleanLibraryHelpText": "更新后清理资源库",
|
||||
"NotificationsKodiSettingsDisplayTime": "显示时间",
|
||||
"NotificationsKodiSettingsGuiNotification": "图形界面通知",
|
||||
"NotificationsPlexSettingsAuthToken": "验证令牌",
|
||||
"NotificationsPlexSettingsAuthenticateWithPlexTv": "使用 Plex.tv 验证身份",
|
||||
"Rejections": "拒绝",
|
||||
"CustomFilter": "自定义过滤器",
|
||||
"AddToDownloadQueue": "添加到下载队列",
|
||||
"AddedToDownloadQueue": "已加入下载队列",
|
||||
"IndexerFlags": "搜刮器标记",
|
||||
"NotificationsEmbySettingsSendNotifications": "发送通知",
|
||||
"NotificationsKodiSettingAlwaysUpdateHelpText": "即使有视频正在播放也更新资源库?",
|
||||
"NotificationsKodiSettingsCleanLibrary": "清理资源库",
|
||||
"NotificationsKodiSettingsDisplayTimeHelpText": "通知显示时长(秒)",
|
||||
"NotificationsKodiSettingsUpdateLibraryHelpText": "导入和重命名时更新资源库?",
|
||||
"ConnectionSettingsUrlBaseHelpText": "向 {clientName} url 添加前缀,例如 {url}",
|
||||
"DownloadClientDelugeSettingsDirectoryHelpText": "可选的下载位置,留空使用 Aria2 默认位置",
|
||||
"UseSsl": "使用 SSL"
|
||||
}
|
||||
|
|
|
@ -27,5 +27,6 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
|||
|
||||
public override bool SendUpdatesToClient => true;
|
||||
public override bool RequiresDiskAccess => true;
|
||||
public override bool IsLongRunning => true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.IO;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
@ -56,6 +57,12 @@ namespace NzbDrone.Core.Music
|
|||
{
|
||||
_logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath);
|
||||
}
|
||||
|
||||
if (sourcePath.PathEquals(destinationPath))
|
||||
{
|
||||
_logger.ProgressInfo("{0} is already in the specified location '{1}'.", artist, destinationPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
|
|
|
@ -18,52 +18,72 @@ namespace NzbDrone.Core.Notifications.Telegram
|
|||
|
||||
public override void OnGrab(GrabMessage grabMessage)
|
||||
{
|
||||
_proxy.SendNotification(ALBUM_GRABBED_TITLE, grabMessage.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? ALBUM_GRABBED_TITLE_BRANDED : ALBUM_GRABBED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, grabMessage.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnReleaseImport(AlbumDownloadMessage message)
|
||||
{
|
||||
_proxy.SendNotification(ALBUM_DOWNLOADED_TITLE, message.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? ALBUM_DOWNLOADED_TITLE_BRANDED : ALBUM_DOWNLOADED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnArtistAdd(ArtistAddMessage message)
|
||||
{
|
||||
_proxy.SendNotification(ARTIST_ADDED_TITLE, message.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? ARTIST_ADDED_TITLE_BRANDED : ARTIST_ADDED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnArtistDelete(ArtistDeleteMessage deleteMessage)
|
||||
{
|
||||
_proxy.SendNotification(ARTIST_DELETED_TITLE, deleteMessage.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? ARTIST_DELETED_TITLE_BRANDED : ARTIST_DELETED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, deleteMessage.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnAlbumDelete(AlbumDeleteMessage deleteMessage)
|
||||
{
|
||||
_proxy.SendNotification(ALBUM_DELETED_TITLE, deleteMessage.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? ALBUM_DELETED_TITLE_BRANDED : ALBUM_DELETED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, deleteMessage.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnHealthIssue(HealthCheck.HealthCheck healthCheck)
|
||||
{
|
||||
_proxy.SendNotification(HEALTH_ISSUE_TITLE, healthCheck.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? HEALTH_ISSUE_TITLE_BRANDED : HEALTH_ISSUE_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, healthCheck.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnHealthRestored(HealthCheck.HealthCheck previousCheck)
|
||||
{
|
||||
_proxy.SendNotification(HEALTH_RESTORED_TITLE, $"The following issue is now resolved: {previousCheck.Message}", Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? HEALTH_RESTORED_TITLE_BRANDED : HEALTH_RESTORED_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, $"The following issue is now resolved: {previousCheck.Message}", Settings);
|
||||
}
|
||||
|
||||
public override void OnDownloadFailure(DownloadFailedMessage message)
|
||||
{
|
||||
_proxy.SendNotification(DOWNLOAD_FAILURE_TITLE, message.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? DOWNLOAD_FAILURE_TITLE_BRANDED : DOWNLOAD_FAILURE_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnImportFailure(AlbumDownloadMessage message)
|
||||
{
|
||||
_proxy.SendNotification(IMPORT_FAILURE_TITLE, message.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? IMPORT_FAILURE_TITLE_BRANDED : IMPORT_FAILURE_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, message.Message, Settings);
|
||||
}
|
||||
|
||||
public override void OnApplicationUpdate(ApplicationUpdateMessage updateMessage)
|
||||
{
|
||||
_proxy.SendNotification(APPLICATION_UPDATE_TITLE, updateMessage.Message, Settings);
|
||||
var title = Settings.IncludeAppNameInTitle ? APPLICATION_UPDATE_TITLE_BRANDED : APPLICATION_UPDATE_TITLE;
|
||||
|
||||
_proxy.SendNotification(title, updateMessage.Message, Settings);
|
||||
}
|
||||
|
||||
public override ValidationResult Test()
|
||||
|
|
|
@ -49,10 +49,11 @@ namespace NzbDrone.Core.Notifications.Telegram
|
|||
{
|
||||
try
|
||||
{
|
||||
const string brandedTitle = "Lidarr - Test Notification";
|
||||
const string title = "Test Notification";
|
||||
const string body = "This is a test message from Lidarr";
|
||||
|
||||
SendNotification(title, body, settings);
|
||||
SendNotification(settings.IncludeAppNameInTitle ? brandedTitle : title, body, settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -32,6 +32,9 @@ namespace NzbDrone.Core.Notifications.Telegram
|
|||
[FieldDefinition(3, Label = "Send Silently", Type = FieldType.Checkbox, HelpText = "Sends the message silently. Users will receive a notification with no sound")]
|
||||
public bool SendSilently { get; set; }
|
||||
|
||||
[FieldDefinition(4, Label = "NotificationsTelegramSettingsIncludeAppName", Type = FieldType.Checkbox, HelpText = "NotificationsTelegramSettingsIncludeAppNameHelpText")]
|
||||
public bool IncludeAppNameInTitle { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
return new NzbDroneValidationResult(Validator.Validate(this));
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.AutoTagging;
|
||||
using NzbDrone.Core.AutoTagging.Specifications;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.ImportLists;
|
||||
|
@ -121,7 +122,7 @@ namespace NzbDrone.Core.Tags
|
|||
var artists = _artistService.GetAllArtistsTags();
|
||||
var rootFolders = _rootFolderService.All();
|
||||
var indexers = _indexerService.All();
|
||||
var autotags = _autoTaggingService.All();
|
||||
var autoTags = _autoTaggingService.All();
|
||||
var downloadClients = _downloadClientFactory.All();
|
||||
|
||||
var details = new List<TagDetails>();
|
||||
|
@ -139,7 +140,7 @@ namespace NzbDrone.Core.Tags
|
|||
ArtistIds = artists.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(),
|
||||
RootFolderIds = rootFolders.Where(c => c.DefaultTags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
AutoTagIds = GetAutoTagIds(tag, autoTags),
|
||||
DownloadClientIds = downloadClients.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||
});
|
||||
}
|
||||
|
@ -190,5 +191,23 @@ namespace NzbDrone.Core.Tags
|
|||
_repo.Delete(tagId);
|
||||
_eventAggregator.PublishEvent(new TagsUpdatedEvent());
|
||||
}
|
||||
|
||||
private List<int> GetAutoTagIds(Tag tag, List<AutoTag> autoTags)
|
||||
{
|
||||
var autoTagIds = autoTags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList();
|
||||
|
||||
foreach (var autoTag in autoTags)
|
||||
{
|
||||
foreach (var specification in autoTag.Specifications)
|
||||
{
|
||||
if (specification is TagSpecification)
|
||||
{
|
||||
autoTagIds.Add(autoTag.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return autoTagIds.Distinct().ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<OutputType>Library</OutputType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.25" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.29" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lidarr.Api.V1\Lidarr.Api.V1.csproj" />
|
||||
|
|
|
@ -6,15 +6,16 @@ namespace NzbDrone.Mono.Disk
|
|||
{
|
||||
public static class FindDriveType
|
||||
{
|
||||
private static readonly Dictionary<string, DriveType> DriveTypeMap = new Dictionary<string, DriveType>
|
||||
{
|
||||
{ "afpfs", DriveType.Network },
|
||||
{ "apfs", DriveType.Fixed },
|
||||
{ "fuse.mergerfs", DriveType.Fixed },
|
||||
{ "fuse.glusterfs", DriveType.Network },
|
||||
{ "nullfs", DriveType.Fixed },
|
||||
{ "zfs", DriveType.Fixed }
|
||||
};
|
||||
private static readonly Dictionary<string, DriveType> DriveTypeMap = new ()
|
||||
{
|
||||
{ "afpfs", DriveType.Network },
|
||||
{ "apfs", DriveType.Fixed },
|
||||
{ "fuse.mergerfs", DriveType.Fixed },
|
||||
{ "fuse.shfs", DriveType.Fixed },
|
||||
{ "fuse.glusterfs", DriveType.Network },
|
||||
{ "nullfs", DriveType.Fixed },
|
||||
{ "zfs", DriveType.Fixed }
|
||||
};
|
||||
|
||||
public static DriveType Find(string driveFormat)
|
||||
{
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<PackageReference Include="FluentValidation" Version="9.5.4" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NLog" Version="5.2.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||
<PackageReference Include="RestSharp" Version="106.15.0" />
|
||||
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||
<PackageReference Include="System.IO.Abstractions" Version="17.0.24" />
|
||||
|
|
Loading…
Reference in New Issue