Skip to content

Commit d85b66c

Browse files
authored
Merge pull request #370 from lklynet/feature/lidarr-existing-monitoring
Add Lidarr existing-albums monitoring support
2 parents 8d94472 + c75fd41 commit d85b66c

8 files changed

Lines changed: 647 additions & 49 deletions

File tree

.tests/auth/lidarr-preferences.integration.test.js

Lines changed: 399 additions & 2 deletions
Large diffs are not rendered by default.

.tests/search/search-service.test.js

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ test("requestAlbumFromSearch rejects when artist must be created without addArti
309309
}
310310
});
311311

312-
test("addAlbum preserves artist monitoring state while monitoring only the requested album", async () => {
312+
test("addAlbum checks artist monitoring while monitoring only the requested album", async () => {
313313
const originalIsConfigured = lidarrClient.isConfigured;
314314
const originalGetArtist = lidarrClient.getArtist;
315315
const originalGetAlbumByMbid = lidarrClient.getAlbumByMbid;
@@ -338,14 +338,26 @@ test("addAlbum preserves artist monitoring state while monitoring only the reque
338338
sizeOnDisk: 0,
339339
},
340340
});
341-
lidarrClient.updateArtistMonitoring = async () => {
341+
lidarrClient.updateArtistMonitoring = async (_artistId, monitorOption) => {
342342
updateArtistMonitoringCalls += 1;
343+
assert.equal(monitorOption, "none");
344+
return {
345+
id: 7,
346+
artistName: "Boards of Canada",
347+
foreignArtistId: "artist-mbid",
348+
monitored: true,
349+
monitor: "none",
350+
monitorNewItems: "none",
351+
addOptions: {
352+
monitor: "none",
353+
},
354+
};
343355
};
344356

345357
try {
346358
const album = await libraryManager.addAlbum(7, "album-mbid", "Geogaddi");
347359
assert.equal(album.id, "42");
348-
assert.equal(updateArtistMonitoringCalls, 0);
360+
assert.equal(updateArtistMonitoringCalls, 1);
349361
} finally {
350362
lidarrClient.isConfigured = originalIsConfigured;
351363
lidarrClient.getArtist = originalGetArtist;
@@ -363,11 +375,13 @@ test("addAlbum monitors and searches the requested album after Lidarr conflict l
363375
const originalGetAlbum = lidarrClient.getAlbum;
364376
const originalMonitorAlbum = lidarrClient.monitorAlbum;
365377
const originalTriggerAlbumSearch = lidarrClient.triggerAlbumSearch;
378+
const originalUpdateArtistMonitoring = lidarrClient.updateArtistMonitoring;
366379
const originalWaitForAlbumByMbidForArtist =
367380
libraryManager.waitForAlbumByMbidForArtist;
368381

369382
let monitorCalls = 0;
370383
let searchCalls = 0;
384+
let updateArtistMonitoringCalls = 0;
371385

372386
lidarrClient.isConfigured = () => true;
373387
lidarrClient.getArtist = async () => ({
@@ -378,6 +392,17 @@ test("addAlbum monitors and searches the requested album after Lidarr conflict l
378392
monitor: "none",
379393
});
380394
lidarrClient.getAlbumByMbid = async () => null;
395+
lidarrClient.updateArtistMonitoring = async (_artistId, monitorOption) => {
396+
updateArtistMonitoringCalls += 1;
397+
assert.equal(monitorOption, "none");
398+
return {
399+
id: 7,
400+
artistName: "Boards of Canada",
401+
foreignArtistId: "artist-mbid",
402+
monitored: true,
403+
monitor: "none",
404+
};
405+
};
381406
lidarrClient.addAlbum = async () => {
382407
throw new Error("AlbumExistsValidator: This album has already been added");
383408
};
@@ -420,6 +445,7 @@ test("addAlbum monitors and searches the requested album after Lidarr conflict l
420445

421446
assert.equal(album.id, "42");
422447
assert.equal(album.monitored, true);
448+
assert.equal(updateArtistMonitoringCalls, 1);
423449
assert.equal(monitorCalls, 1);
424450
assert.equal(searchCalls, 1);
425451
} finally {
@@ -430,6 +456,7 @@ test("addAlbum monitors and searches the requested album after Lidarr conflict l
430456
lidarrClient.getAlbum = originalGetAlbum;
431457
lidarrClient.monitorAlbum = originalMonitorAlbum;
432458
lidarrClient.triggerAlbumSearch = originalTriggerAlbumSearch;
459+
lidarrClient.updateArtistMonitoring = originalUpdateArtistMonitoring;
433460
libraryManager.waitForAlbumByMbidForArtist =
434461
originalWaitForAlbumByMbidForArtist;
435462
}

backend/routes/library/handlers/downloads.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -524,18 +524,7 @@ export default function registerDownloads(router) {
524524
}
525525

526526
try {
527-
if (!artist.monitored || artist.monitorOption === "none") {
528-
const artistMbidToUpdate = artist.mbid || artist.foreignArtistId;
529-
if (artistMbidToUpdate) {
530-
try {
531-
await libraryManager.updateArtist(artistMbidToUpdate, {
532-
monitored: true,
533-
monitorOption: "missing",
534-
});
535-
artist = await libraryManager.getArtist(artistMbidToUpdate);
536-
} catch {}
537-
}
538-
}
527+
artist = await libraryManager.ensureArtistMonitored(artist);
539528
if (!album.monitored) {
540529
await libraryManager.updateAlbum(albumId, { monitored: true });
541530
}
@@ -549,6 +538,14 @@ export default function registerDownloads(router) {
549538
name: "AlbumSearch",
550539
albumIds: [parseInt(albumId, 10)],
551540
});
541+
await libraryManager.ensureRequestedAlbumMonitoring(
542+
artist.id,
543+
albumId,
544+
);
545+
libraryManager.scheduleRequestedAlbumMonitoringRepair(
546+
artist.id,
547+
albumId,
548+
);
552549
}
553550
invalidateAllDownloadStatusesCache();
554551

@@ -601,6 +598,13 @@ export default function registerDownloads(router) {
601598
return res.status(404).json({ error: "Album not found" });
602599
}
603600

601+
const artist = album.artistId
602+
? await libraryManager.getArtistById(album.artistId)
603+
: null;
604+
if (artist) {
605+
await libraryManager.ensureArtistMonitored(artist);
606+
}
607+
604608
if (!album.monitored) {
605609
await libraryManager.updateAlbum(albumId, { monitored: true });
606610
}
@@ -609,6 +613,16 @@ export default function registerDownloads(router) {
609613
name: "AlbumSearch",
610614
albumIds: [parseInt(albumId, 10)],
611615
});
616+
if (album.artistId) {
617+
await libraryManager.ensureRequestedAlbumMonitoring(
618+
album.artistId,
619+
albumId,
620+
);
621+
libraryManager.scheduleRequestedAlbumMonitoringRepair(
622+
album.artistId,
623+
albumId,
624+
);
625+
}
612626
invalidateAllDownloadStatusesCache();
613627

614628
res.json({

backend/services/libraryManager.js

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ function upsertCachedArtist(mappedArtist) {
4646
_cachedArtists.unshift(mappedArtist);
4747
}
4848

49+
function removeCachedArtistByMbid(mbid) {
50+
if (!mbid || !Array.isArray(_cachedArtists) || _cachedArtists.length === 0) {
51+
return;
52+
}
53+
_cachedArtists = _cachedArtists.filter(
54+
(artist) => artist?.mbid !== mbid && artist?.foreignArtistId !== mbid,
55+
);
56+
}
57+
58+
function sleep(ms) {
59+
return new Promise((resolve) => setTimeout(resolve, ms));
60+
}
61+
4962
async function getLidarrClient() {
5063
if (!lidarrClient) {
5164
try {
@@ -292,8 +305,8 @@ export class LibraryManager {
292305
});
293306

294307
switch (artist.monitorOption) {
295-
case "all":
296308
case "existing":
309+
case "all":
297310
albumsToMonitor.push(...eligibleAlbums.filter((album) => !album.monitored));
298311
break;
299312
case "latest":
@@ -552,6 +565,69 @@ export class LibraryManager {
552565
}
553566
}
554567

568+
async ensureArtistMonitored(artist, monitorOption = null) {
569+
if (!artist || artist.monitored !== false) {
570+
return artist;
571+
}
572+
573+
const mbid = artist.mbid || artist.foreignArtistId;
574+
if (!mbid) {
575+
return artist;
576+
}
577+
578+
const nextMonitorOption =
579+
monitorOption ||
580+
artist.monitorOption ||
581+
artist.addOptions?.monitor ||
582+
"none";
583+
const updated = await this.updateArtist(mbid, {
584+
monitored: true,
585+
monitorOption: nextMonitorOption,
586+
});
587+
return updated?.error ? artist : updated;
588+
}
589+
590+
async ensureRequestedAlbumMonitoring(artistId, albumId, options = {}) {
591+
const normalizedArtistId = String(artistId || "").trim();
592+
const normalizedAlbumId = String(albumId || "").trim();
593+
if (!normalizedArtistId || !normalizedAlbumId) {
594+
return { artist: null, album: null };
595+
}
596+
597+
let artist = await this.getArtistById(normalizedArtistId);
598+
if (artist?.monitored === false) {
599+
artist = await this.ensureArtistMonitored(artist, options.monitorOption);
600+
}
601+
602+
let album = await this.getAlbumById(normalizedAlbumId);
603+
if (album?.monitored === false) {
604+
album = await this.updateAlbum(normalizedAlbumId, { monitored: true });
605+
}
606+
607+
return { artist, album };
608+
}
609+
610+
scheduleRequestedAlbumMonitoringRepair(artistId, albumId, options = {}) {
611+
const normalizedArtistId = String(artistId || "").trim();
612+
const normalizedAlbumId = String(albumId || "").trim();
613+
if (!normalizedArtistId || !normalizedAlbumId) return;
614+
615+
for (const delayMs of [1000, 3000, 8000, 15000]) {
616+
const timeout = setTimeout(() => {
617+
this.ensureRequestedAlbumMonitoring(
618+
normalizedArtistId,
619+
normalizedAlbumId,
620+
options,
621+
).catch((error) => {
622+
console.error(
623+
`[LibraryManager] Failed to stabilize requested album monitoring: ${error.message}`,
624+
);
625+
});
626+
}, delayMs);
627+
timeout.unref?.();
628+
}
629+
}
630+
555631
async getAllArtists() {
556632
try {
557633
const lidarr = await getLidarrClient();
@@ -686,8 +762,7 @@ export class LibraryManager {
686762
const artistPath = lidarrArtist.path ?? null;
687763
const monitorOption =
688764
lidarrArtist.monitor || lidarrArtist.addOptions?.monitor || "none";
689-
const normalizedMonitorOption =
690-
monitorOption === "existing" ? "all" : monitorOption;
765+
const normalizedMonitorOption = monitorOption || "none";
691766
return {
692767
id: lidarrArtist.id?.toString() || lidarrArtist.foreignArtistId,
693768
mbid: lidarrArtist.foreignArtistId,
@@ -725,8 +800,7 @@ export class LibraryManager {
725800
) {
726801
const monitorOption =
727802
updates.monitorOption || lidarrArtist.monitor || "none";
728-
const normalizedMonitorOption =
729-
monitorOption === "existing" ? "all" : monitorOption;
803+
const normalizedMonitorOption = monitorOption || "none";
730804
await lidarr.updateArtistMonitoring(lidarrArtist.id, monitorOption);
731805
console.log(
732806
`[LibraryManager] Updated Lidarr monitoring for "${lidarrArtist.artistName}" to "${monitorOption}"`,
@@ -771,6 +845,7 @@ export class LibraryManager {
771845
if (!lidarrArtist)
772846
return { success: false, error: "Artist not found in Lidarr" };
773847
await lidarr.deleteArtist(lidarrArtist.id, deleteFiles);
848+
removeCachedArtistByMbid(mbid);
774849
console.log(
775850
`[LibraryManager] Deleted artist "${lidarrArtist.artistName}" from Lidarr`,
776851
);
@@ -805,7 +880,6 @@ export class LibraryManager {
805880
msg.includes("foreignalbumid")
806881
);
807882
};
808-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
809883
const settings = getSettings();
810884
const searchOnAdd = settings.integrations?.lidarr?.searchOnAdd ?? false;
811885
const shouldTriggerSearch =
@@ -818,6 +892,11 @@ export class LibraryManager {
818892
}
819893
if (shouldTriggerSearch) {
820894
await lidarr.triggerAlbumSearch(existingAlbum.id);
895+
await this.ensureRequestedAlbumMonitoring(artistId, existingAlbum.id);
896+
this.scheduleRequestedAlbumMonitoringRepair(
897+
artistId,
898+
existingAlbum.id,
899+
);
821900
}
822901
const refreshedExisting = await lidarr
823902
.getAlbum(existingAlbum.id)
@@ -847,6 +926,12 @@ export class LibraryManager {
847926
}
848927
}
849928
if (!lidarrArtist) return { error: "Artist not found in Lidarr" };
929+
if (lidarrArtist.monitored === false) {
930+
lidarrArtist = await lidarr.updateArtistMonitoring(
931+
artistId,
932+
lidarrArtist.monitor || lidarrArtist.addOptions?.monitor || "none",
933+
);
934+
}
850935
const existing = await lidarr.getAlbumByMbid(releaseGroupMbid);
851936
const artistNumericId = parseInt(artistId, 10);
852937
const sameArtistExisting =
@@ -912,6 +997,13 @@ export class LibraryManager {
912997
if (!lidarrAlbum) {
913998
return { error: "Failed to add album to Lidarr" };
914999
}
1000+
if (shouldTriggerSearch) {
1001+
await this.ensureRequestedAlbumMonitoring(artistId, lidarrAlbum.id);
1002+
this.scheduleRequestedAlbumMonitoringRepair(artistId, lidarrAlbum.id);
1003+
lidarrAlbum = await lidarr
1004+
.getAlbum(lidarrAlbum.id)
1005+
.catch(() => lidarrAlbum);
1006+
}
9151007
const updatedArtist = await lidarr.getArtist(artistId);
9161008
return this.mapLidarrAlbum(lidarrAlbum, updatedArtist);
9171009
} catch (error) {
@@ -993,6 +1085,8 @@ export class LibraryManager {
9931085
throw error;
9941086
}
9951087

1088+
artist = await this.ensureArtistMonitored(artist);
1089+
9961090
let existingAlbum = await lidarr.getAlbumByMbid(normalizedAlbumMbid);
9971091
if (
9981092
existingAlbum &&

0 commit comments

Comments
 (0)