arr add: a root folder must be named when several exist; lidarr's lookup shows the artist's name

Sonarr's first root folder is /anime, so an unnamed add landed a series
there. A profile is a preference and the first stands in; a root folder
decides where data lands, so with several it is named or the refusal
lists them. Lidarr's lookup used title where the field is artistName.
This commit is contained in:
2026-10-01 14:57:49 +02:00
parent da815c0238
commit 702e9f8419
6 changed files with 26 additions and 17 deletions
+8 -5
View File
@@ -106,7 +106,7 @@ export class LidarrClient {
async lookup(term: string): Promise<Record<string, unknown>[]> {
const found = (await this.get("artist/lookup", { term })) as Record<string, unknown>[];
return (found ?? []).map((x) => ({
title: x.title, year: x.year, foreignArtistId: x.foreignArtistId,
title: x.artistName ?? x.title, year: x.year, foreignArtistId: x.foreignArtistId,
overview: typeof x.overview === "string" ? x.overview.slice(0, 200) : undefined,
inLibrary: typeof x.id === "number" && x.id > 0,
}));
@@ -139,15 +139,18 @@ export class LidarrClient {
if (typeof record.id === "number" && record.id > 0) {
return { added: false, alreadyInLibrary: true, id: record.id, title: record.title };
}
const pick = <T extends { id: number; name?: string; path?: string }>(list: T[], want: string | undefined, what: string): T => {
// A profile is a preference, so the first stands in when none is named; a root folder decides
// where the data lands, so with several it must be named — the refusal lists them.
const pick = <T extends { id: number; name?: string; path?: string }>(list: T[], want: string | undefined, what: string, firstWillDo = true): T => {
if (!list.length) throw new Error(`Lidarr has no ${what} configured`);
if (!want) return list[0];
if (!want && (firstWillDo || list.length === 1)) return list[0];
if (!want) throw new Error(`Lidarr has ${list.length} ${what}s — name one: ${list.map((x) => x.name ?? x.path).join(", ")}`);
const hit = list.find((x) => x.name === want || x.path === want);
if (!hit) throw new Error(`Lidarr has no ${what} ${JSON.stringify(want)}; it has ${list.map((x) => x.name ?? x.path).join(", ")}`);
return hit;
};
const quality = pick(await this.qualityProfiles(), opts.qualityProfile, "quality profile");
const root = pick(await this.rootFolders(), opts.rootFolder, "root folder");
const root = pick(await this.rootFolders(), opts.rootFolder, "root folder", false);
const metadata = pick(await this.metadataProfiles(), opts.metadataProfile, "metadata profile");
const search = opts.search ?? false;
const body = {
@@ -159,7 +162,7 @@ export class LidarrClient {
addOptions: { monitor: "all", searchForMissingAlbums: search },
};
const made = (await this.send("POST", "artist", body)) as Record<string, unknown>;
return { added: true, id: made.id, title: made.title, foreignArtistId: made.foreignArtistId, rootFolder: root.path, qualityProfile: quality.name, metadataProfile: metadata.name, monitored: body.monitored, searchRequested: search };
return { added: true, id: made.id, title: made.artistName ?? made.title, foreignArtistId: made.foreignArtistId, rootFolder: root.path, qualityProfile: quality.name, metadataProfile: metadata.name, monitored: body.monitored, searchRequested: search };
}
/** Remove a artist from the library by its Lidarr id. Files stay on disk unless `deleteFiles`. */
+2 -2
View File
@@ -75,12 +75,12 @@ export function getLidarrTools(lidarr: LidarrClient): ToolDefinition[] {
},
{
name: "lidarr_add",
description: "Add a artist to Lidarr by its MusicBrainz artist id (from lidarr_lookup). Uses the first quality and metadata profile and root folder unless named. Does not search the indexers unless `search` is true.",
description: "Add a artist to Lidarr by its MusicBrainz artist id (from lidarr_lookup). The first quality and metadata profiles stand in unless named; the root folder must be named when several exist. Does not search the indexers unless `search` is true.",
input: {
id: { type: "string", description: "the MusicBrainz artist id" },
qualityProfile: { type: "string", description: "a quality profile's name (default: the first)" },
metadataProfile: { type: "string", description: "a metadata profile's name (default: the first)" },
rootFolder: { type: "string", description: "a root folder path (default: the first)" },
rootFolder: { type: "string", description: "a root folder path (required when the app has several)" },
monitored: { type: "boolean", description: "monitor it (default true)" },
search: { type: "boolean", description: "search the indexers right away (default false)" },
},
+6 -3
View File
@@ -135,15 +135,18 @@ export class RadarrClient {
if (typeof record.id === "number" && record.id > 0) {
return { added: false, alreadyInLibrary: true, id: record.id, title: record.title };
}
const pick = <T extends { id: number; name?: string; path?: string }>(list: T[], want: string | undefined, what: string): T => {
// A profile is a preference, so the first stands in when none is named; a root folder decides
// where the data lands, so with several it must be named — the refusal lists them.
const pick = <T extends { id: number; name?: string; path?: string }>(list: T[], want: string | undefined, what: string, firstWillDo = true): T => {
if (!list.length) throw new Error(`Radarr has no ${what} configured`);
if (!want) return list[0];
if (!want && (firstWillDo || list.length === 1)) return list[0];
if (!want) throw new Error(`Radarr has ${list.length} ${what}s — name one: ${list.map((x) => x.name ?? x.path).join(", ")}`);
const hit = list.find((x) => x.name === want || x.path === want);
if (!hit) throw new Error(`Radarr has no ${what} ${JSON.stringify(want)}; it has ${list.map((x) => x.name ?? x.path).join(", ")}`);
return hit;
};
const quality = pick(await this.qualityProfiles(), opts.qualityProfile, "quality profile");
const root = pick(await this.rootFolders(), opts.rootFolder, "root folder");
const root = pick(await this.rootFolders(), opts.rootFolder, "root folder", false);
const search = opts.search ?? false;
const body = {
+2 -2
View File
@@ -75,12 +75,12 @@ export function getRadarrTools(radarr: RadarrClient): ToolDefinition[] {
},
{
name: "radarr_add",
description: "Add a movie to Radarr by its TMDb id (from radarr_lookup). Uses the first quality profile and root folder unless named. Does not search the indexers unless `search` is true.",
description: "Add a movie to Radarr by its TMDb id (from radarr_lookup). The first quality profile stands in unless named; the root folder must be named when several exist. Does not search the indexers unless `search` is true.",
input: {
id: { type: "string", description: "the TMDb id" },
qualityProfile: { type: "string", description: "a quality profile's name (default: the first)" },
rootFolder: { type: "string", description: "a root folder path (default: the first)" },
rootFolder: { type: "string", description: "a root folder path (required when the app has several)" },
monitored: { type: "boolean", description: "monitor it (default true)" },
search: { type: "boolean", description: "search the indexers right away (default false)" },
},
+6 -3
View File
@@ -135,15 +135,18 @@ export class SonarrClient {
if (typeof record.id === "number" && record.id > 0) {
return { added: false, alreadyInLibrary: true, id: record.id, title: record.title };
}
const pick = <T extends { id: number; name?: string; path?: string }>(list: T[], want: string | undefined, what: string): T => {
// A profile is a preference, so the first stands in when none is named; a root folder decides
// where the data lands, so with several it must be named — the refusal lists them.
const pick = <T extends { id: number; name?: string; path?: string }>(list: T[], want: string | undefined, what: string, firstWillDo = true): T => {
if (!list.length) throw new Error(`Sonarr has no ${what} configured`);
if (!want) return list[0];
if (!want && (firstWillDo || list.length === 1)) return list[0];
if (!want) throw new Error(`Sonarr has ${list.length} ${what}s — name one: ${list.map((x) => x.name ?? x.path).join(", ")}`);
const hit = list.find((x) => x.name === want || x.path === want);
if (!hit) throw new Error(`Sonarr has no ${what} ${JSON.stringify(want)}; it has ${list.map((x) => x.name ?? x.path).join(", ")}`);
return hit;
};
const quality = pick(await this.qualityProfiles(), opts.qualityProfile, "quality profile");
const root = pick(await this.rootFolders(), opts.rootFolder, "root folder");
const root = pick(await this.rootFolders(), opts.rootFolder, "root folder", false);
const search = opts.search ?? false;
const body = {
+2 -2
View File
@@ -75,12 +75,12 @@ export function getSonarrTools(sonarr: SonarrClient): ToolDefinition[] {
},
{
name: "sonarr_add",
description: "Add a series to Sonarr by its TVDb id (from sonarr_lookup). Uses the first quality profile and root folder unless named. Does not search the indexers unless `search` is true.",
description: "Add a series to Sonarr by its TVDb id (from sonarr_lookup). The first quality profile stands in unless named; the root folder must be named when several exist. Does not search the indexers unless `search` is true.",
input: {
id: { type: "string", description: "the TVDb id" },
qualityProfile: { type: "string", description: "a quality profile's name (default: the first)" },
rootFolder: { type: "string", description: "a root folder path (default: the first)" },
rootFolder: { type: "string", description: "a root folder path (required when the app has several)" },
monitored: { type: "boolean", description: "monitor it (default true)" },
search: { type: "boolean", description: "search the indexers right away (default false)" },
},