Compare commits

...

22 Commits

Author SHA1 Message Date
Hazard Sylvain 464e236ee0
Merge 961484412a into 416d505316 2024-04-22 16:07:58 +00:00
Bogdan 416d505316 Bump dotnet to 6.0.29 2024-04-22 07:58:44 +03:00
Bogdan 4816f35256 Bump typescript eslint plugin and parser 2024-04-21 12:45:16 +03:00
Josh McKinney e42e0a72eb Add dev container workspace
Allows the linting and style settings for the frontend to be applied even when you load the main repo as a workspace

(cherry picked from commit d6278fced49b26be975c3a6039b38a94f700864b)

Closes #4756
2024-04-21 12:05:07 +03:00
Bogdan db9e62f79d Convert store selectors to Typescript
Closes #3937
2024-04-21 12:04:13 +03:00
Bogdan bc69fa4842 Bump frontend dependencies 2024-04-21 11:11:55 +03:00
Bogdan 86dad72c49 Bump version to 2.3.1 2024-04-21 09:15:31 +03:00
Bogdan 4a8d6c367d Bump skipping spotify tests 2024-04-20 18:37:21 +03:00
Bogdan c1926f8758 Fixed: Skip move when source and destination are the same
Co-authored-by: Qstick <qstick@gmail.com>
2024-04-20 17:56:04 +03:00
Bogdan 7820bcf91f Bump SixLabors.ImageSharp to 3.1.4 2024-04-19 08:00:59 +03:00
Servarr 431ad0a028 Automated API Docs update 2024-04-18 15:33:13 +03:00
Bogdan 59cf7a95c3 Fixed: Re-testing edited providers will forcibly test them
(cherry picked from commit e9662544621b2d1fb133ff9d96d0eb20b8198725)
2024-04-16 10:54:12 +03:00
Bogdan e17e3633f8 Don't block task queue for queued update task for Rescan Folders
Towards #4551
2024-04-14 15:45:35 +03:00
Bogdan 46da2b49c6 Bump version to 2.3.0 2024-04-13 07:10:44 +03:00
Josh McKinney 3071977284 Add DevContainer, VSCode config and extensions.json
(cherry picked from commit 5061dc4b5e5ea9925740496a5939a1762788b793)

Closes #4740
2024-04-11 23:16:31 +03:00
Mark McDowall b14e2bb618 New: Auto tag artists based on tags present/absent on artists
(cherry picked from commit f4c19a384bd9bb4e35c9fa0ca5d9a448c04e409e)

Closes #4742
2024-04-11 23:02:04 +03:00
Mark McDowall 8c09c0cb5c New: Option to prefix app name on Telegram notification titles
(cherry picked from commit 37863a8deb339ef730b2dd5be61e1da1311fdd23)

Closes #4739
2024-04-11 22:43:54 +03:00
Weblate 8cebb21c2d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: myrad2267 <myrad2267@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/lidarr/zh_CN/
Translation: Servarr/Lidarr
2024-04-10 02:21:56 +03:00
Bogdan 74ac263b74 New: Detect shfs mounts in disk space
(cherry picked from commit 1aef91041e404f76f278f430e4e53140fb125792)
2024-04-10 02:21:35 +03:00
HazardSy 961484412a Rename constant 2024-02-21 08:40:20 +01:00
HazardSy 638940a035 Feat: add localization for Liked Songs 2024-02-18 14:33:11 +01:00
HazardSy d3ee21b919 Feat : add Liked Songs in Spotify Playlist import list 2024-02-18 14:25:06 +01:00
80 changed files with 2736 additions and 1709 deletions

View File

@ -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": {}
}

View File

@ -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"]
}
}
}

12
.github/dependabot.yml vendored Normal file
View File

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

1
.gitignore vendored
View File

@ -126,6 +126,7 @@ coverage*.xml
coverage*.json
setup/Output/
*.~is
.mono
# VS outout folders
bin

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"ms-dotnettools.csdevkit",
"ms-vscode-remote.remote-containers"
]
}

26
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

44
.vscode/tasks.json vendored Normal file
View File

@ -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"
}
]
}

View File

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

View File

@ -28,7 +28,8 @@ module.exports = {
globals: {
expect: false,
chai: false,
sinon: false
sinon: false,
JSX: true
},
parserOptions: {

View File

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

View File

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

View File

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

View File

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

View File

@ -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}
/>
);
}

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ export default function TagInUse(props) {
return null;
}
if (count > 1 && labelPlural ) {
if (count > 1 && labelPlural) {
return (
<div>
{count} {labelPlural.toLowerCase()}

View File

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

View File

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

View File

@ -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,
};
}
);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +0,0 @@
import _ from 'lodash';
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
_.isEqual
);
export default createDeepEqualSelector;

View File

@ -0,0 +1,6 @@
import { isEqual } from 'lodash';
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
export default createDeepEqualSelector;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": {

View File

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

View File

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

View File

@ -86,7 +86,8 @@ namespace NzbDrone.Core.Annotations
TagSelect,
RootFolder,
QualityProfile,
MetadataProfile
MetadataProfile,
ArtistTag
}
public enum HiddenType

View File

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

View File

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

View File

@ -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") })
}
};
}

View File

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

View File

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

View File

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

View File

@ -724,5 +724,17 @@
"ImportLists": "القوائم",
"ArtistIndexFooterDownloading": "جارى التحميل",
"ImportList": "القوائم",
"AutomaticSearch": "البحث التلقائي"
"AutomaticSearch": "البحث التلقائي",
"FormatAgeMinutes": "الدقائق",
"IndexerFlags": "أعلام المفهرس",
"CustomFilter": "مرشحات مخصصة",
"FormatAgeMinute": "الدقائق",
"MonitorNoAlbums": "لا شيء",
"Yesterday": "في الامس",
"FormatAgeHours": "ساعات",
"FormatAgeHour": "ساعات",
"Tomorrow": "غدا",
"GrabReleaseUnknownArtistOrAlbumMessageText": "لم يتمكن Radarr من تحديد الفيلم الذي كان هذا الإصدار من أجله. قد يتعذر على Radarr استيراد هذا الإصدار تلقائيًا. هل تريد انتزاع \"{0}\"؟",
"AddToDownloadQueue": "إضافة إلى قائمة انتظار التنزيل",
"AddedToDownloadQueue": "تمت الإضافة إلى قائمة انتظار التنزيلات"
}

View File

@ -726,5 +726,15 @@
"MonitoredStatus": "Наблюдавано / Състояние",
"ArtistIndexFooterDownloading": "Изтегляне",
"ImportList": "Списъци",
"AutomaticSearch": "Автоматично търсене"
"AutomaticSearch": "Автоматично търсене",
"GrabReleaseUnknownArtistOrAlbumMessageText": "Radarr не успя да определи за кой филм е предназначено това издание. Radarr може да не може автоматично да импортира тази версия. Искате ли да вземете „{0}“?",
"CustomFilter": "Персонализирани филтри",
"FormatAgeHour": "Часа",
"FormatAgeHours": "Часа",
"FormatAgeMinute": "Минути",
"FormatAgeMinutes": "Минути",
"MonitorNoAlbums": "Нито един",
"Tomorrow": "Утре",
"Yesterday": "Вчера",
"IndexerFlags": "Индексиращи знамена"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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."
}

View File

@ -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."
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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?"
}

View File

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

View File

@ -27,5 +27,6 @@ namespace NzbDrone.Core.MediaFiles.Commands
public override bool SendUpdatesToClient => true;
public override bool RequiresDiskAccess => true;
public override bool IsLongRunning => true;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
}
}
}

View File

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

View File

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

View File

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

3315
yarn.lock

File diff suppressed because it is too large Load Diff