Skip to content

Commit e8a2c05

Browse files
authored
Merge pull request #347 from lklynet/feature/lidarr-album-wait
Wait for new Lidarr albums before searching
2 parents 1d15369 + 48d9d98 commit e8a2c05

2 files changed

Lines changed: 237 additions & 9 deletions

File tree

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

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ test("requestAlbumFromSearch resolves artist add settings and triggers search",
113113
const originalResolveArtistAddOptions = libraryManager.resolveArtistAddOptions;
114114
const originalAddArtistWithResolvedOptions =
115115
libraryManager.addArtistWithResolvedOptions;
116+
const originalWaitForAlbumByMbidForArtist =
117+
libraryManager.waitForAlbumByMbidForArtist;
116118
const originalAddAlbum = libraryManager.addAlbum;
117119

118120
lidarrClient.isConfigured = () => true;
@@ -135,6 +137,7 @@ test("requestAlbumFromSearch resolves artist add settings and triggers search",
135137
artistName: "Various Artists",
136138
monitorOption: options.monitorOption,
137139
});
140+
libraryManager.waitForAlbumByMbidForArtist = async () => null;
138141
libraryManager.addAlbum = async () => ({
139142
id: "42",
140143
artistId: "7",
@@ -176,6 +179,100 @@ test("requestAlbumFromSearch resolves artist add settings and triggers search",
176179
libraryManager.resolveArtistAddOptions = originalResolveArtistAddOptions;
177180
libraryManager.addArtistWithResolvedOptions =
178181
originalAddArtistWithResolvedOptions;
182+
libraryManager.waitForAlbumByMbidForArtist =
183+
originalWaitForAlbumByMbidForArtist;
184+
libraryManager.addAlbum = originalAddAlbum;
185+
}
186+
});
187+
188+
test("requestAlbumFromSearch waits for Lidarr to populate a new artist album", async () => {
189+
const originalIsConfigured = lidarrClient.isConfigured;
190+
const originalGetAlbumByMbid = lidarrClient.getAlbumByMbid;
191+
const originalGetArtist = libraryManager.getArtist;
192+
const originalResolveArtistAddOptions = libraryManager.resolveArtistAddOptions;
193+
const originalAddArtistWithResolvedOptions =
194+
libraryManager.addArtistWithResolvedOptions;
195+
const originalWaitForAlbumByMbidForArtist =
196+
libraryManager.waitForAlbumByMbidForArtist;
197+
const originalAddAlbum = libraryManager.addAlbum;
198+
199+
let waitCall = null;
200+
201+
lidarrClient.isConfigured = () => true;
202+
lidarrClient.getAlbumByMbid = async () => null;
203+
libraryManager.getArtist = async () => null;
204+
libraryManager.resolveArtistAddOptions = async () => ({
205+
quality: "standard",
206+
monitorOption: "none",
207+
rootFolderPath: "/music/main",
208+
qualityProfileId: 7,
209+
});
210+
libraryManager.addArtistWithResolvedOptions = async () => ({
211+
id: "7",
212+
mbid: "artist-mbid",
213+
foreignArtistId: "artist-mbid",
214+
artistName: "Boards of Canada",
215+
monitorOption: "none",
216+
});
217+
libraryManager.waitForAlbumByMbidForArtist = async (albumMbid, artistId) => {
218+
waitCall = { albumMbid, artistId };
219+
return {
220+
id: 42,
221+
artistId: 7,
222+
foreignAlbumId: "album-mbid",
223+
title: "Geogaddi",
224+
};
225+
};
226+
libraryManager.addAlbum = async (artistId, albumMbid, albumName, options) => {
227+
assert.deepEqual(waitCall, {
228+
albumMbid: "album-mbid",
229+
artistId: "7",
230+
});
231+
assert.equal(artistId, "7");
232+
assert.equal(albumMbid, "album-mbid");
233+
assert.equal(albumName, "Geogaddi");
234+
assert.equal(options.triggerSearch, true);
235+
return {
236+
id: "42",
237+
artistId: "7",
238+
mbid: "album-mbid",
239+
foreignAlbumId: "album-mbid",
240+
albumName: "Geogaddi",
241+
monitored: true,
242+
statistics: {
243+
percentOfTracks: 0,
244+
sizeOnDisk: 0,
245+
},
246+
};
247+
};
248+
249+
try {
250+
const result = await libraryManager.requestAlbumFromSearch({
251+
albumMbid: "album-mbid",
252+
albumName: "Geogaddi",
253+
artistMbid: "artist-mbid",
254+
artistName: "Boards of Canada",
255+
triggerSearch: true,
256+
user: {
257+
role: "user",
258+
permissions: { addAlbum: true, addArtist: true },
259+
},
260+
});
261+
262+
assert.equal(result.success, true);
263+
assert.equal(result.createdArtist, true);
264+
assert.equal(result.createdAlbum, false);
265+
assert.equal(result.triggeredSearch, true);
266+
assert.equal(result.album.id, "42");
267+
} finally {
268+
lidarrClient.isConfigured = originalIsConfigured;
269+
lidarrClient.getAlbumByMbid = originalGetAlbumByMbid;
270+
libraryManager.getArtist = originalGetArtist;
271+
libraryManager.resolveArtistAddOptions = originalResolveArtistAddOptions;
272+
libraryManager.addArtistWithResolvedOptions =
273+
originalAddArtistWithResolvedOptions;
274+
libraryManager.waitForAlbumByMbidForArtist =
275+
originalWaitForAlbumByMbidForArtist;
179276
libraryManager.addAlbum = originalAddAlbum;
180277
}
181278
});
@@ -257,3 +354,83 @@ test("addAlbum preserves artist monitoring state while monitoring only the reque
257354
lidarrClient.updateArtistMonitoring = originalUpdateArtistMonitoring;
258355
}
259356
});
357+
358+
test("addAlbum monitors and searches the requested album after Lidarr conflict lag", async () => {
359+
const originalIsConfigured = lidarrClient.isConfigured;
360+
const originalGetArtist = lidarrClient.getArtist;
361+
const originalGetAlbumByMbid = lidarrClient.getAlbumByMbid;
362+
const originalAddAlbum = lidarrClient.addAlbum;
363+
const originalGetAlbum = lidarrClient.getAlbum;
364+
const originalMonitorAlbum = lidarrClient.monitorAlbum;
365+
const originalTriggerAlbumSearch = lidarrClient.triggerAlbumSearch;
366+
const originalWaitForAlbumByMbidForArtist =
367+
libraryManager.waitForAlbumByMbidForArtist;
368+
369+
let monitorCalls = 0;
370+
let searchCalls = 0;
371+
372+
lidarrClient.isConfigured = () => true;
373+
lidarrClient.getArtist = async () => ({
374+
id: 7,
375+
artistName: "Boards of Canada",
376+
foreignArtistId: "artist-mbid",
377+
monitored: false,
378+
monitor: "none",
379+
});
380+
lidarrClient.getAlbumByMbid = async () => null;
381+
lidarrClient.addAlbum = async () => {
382+
throw new Error("AlbumExistsValidator: This album has already been added");
383+
};
384+
libraryManager.waitForAlbumByMbidForArtist = async () => ({
385+
id: 42,
386+
artistId: "7",
387+
foreignAlbumId: "album-mbid",
388+
title: "Geogaddi",
389+
monitored: false,
390+
statistics: {
391+
percentOfTracks: 0,
392+
sizeOnDisk: 0,
393+
},
394+
});
395+
lidarrClient.monitorAlbum = async (albumId, monitored) => {
396+
monitorCalls += 1;
397+
assert.equal(albumId, 42);
398+
assert.equal(monitored, true);
399+
};
400+
lidarrClient.triggerAlbumSearch = async (albumId) => {
401+
searchCalls += 1;
402+
assert.equal(albumId, 42);
403+
};
404+
lidarrClient.getAlbum = async () => ({
405+
id: 42,
406+
artistId: 7,
407+
foreignAlbumId: "album-mbid",
408+
title: "Geogaddi",
409+
monitored: true,
410+
statistics: {
411+
percentOfTracks: 0,
412+
sizeOnDisk: 0,
413+
},
414+
});
415+
416+
try {
417+
const album = await libraryManager.addAlbum(7, "album-mbid", "Geogaddi", {
418+
triggerSearch: true,
419+
});
420+
421+
assert.equal(album.id, "42");
422+
assert.equal(album.monitored, true);
423+
assert.equal(monitorCalls, 1);
424+
assert.equal(searchCalls, 1);
425+
} finally {
426+
lidarrClient.isConfigured = originalIsConfigured;
427+
lidarrClient.getArtist = originalGetArtist;
428+
lidarrClient.getAlbumByMbid = originalGetAlbumByMbid;
429+
lidarrClient.addAlbum = originalAddAlbum;
430+
lidarrClient.getAlbum = originalGetAlbum;
431+
lidarrClient.monitorAlbum = originalMonitorAlbum;
432+
lidarrClient.triggerAlbumSearch = originalTriggerAlbumSearch;
433+
libraryManager.waitForAlbumByMbidForArtist =
434+
originalWaitForAlbumByMbidForArtist;
435+
}
436+
});

backend/services/libraryManager.js

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,38 @@ export class LibraryManager {
180180
return [];
181181
}
182182

183+
async waitForAlbumByMbidForArtist(
184+
albumMbid,
185+
artistId,
186+
{ attempts = 20, delayMs = 1500 } = {},
187+
) {
188+
const lidarr = await getLidarrClient();
189+
if (!lidarr || !lidarr.isConfigured()) {
190+
return null;
191+
}
192+
193+
const normalizedAlbumMbid = String(albumMbid || "").trim();
194+
const normalizedArtistId = String(artistId || "").trim();
195+
if (!normalizedAlbumMbid || !normalizedArtistId) {
196+
return null;
197+
}
198+
199+
for (let attempt = 1; attempt <= attempts; attempt++) {
200+
try {
201+
const album = await lidarr.getAlbumByMbid(normalizedAlbumMbid);
202+
if (album && String(album.artistId) === normalizedArtistId) {
203+
return album;
204+
}
205+
} catch {}
206+
207+
if (attempt < attempts) {
208+
await new Promise((resolve) => setTimeout(resolve, delayMs));
209+
}
210+
}
211+
212+
return null;
213+
}
214+
183215
async applyArtistMonitoringDefaults(artist, albums = null) {
184216
if (
185217
!artist?.monitored ||
@@ -818,7 +850,9 @@ export class LibraryManager {
818850
const existing = await lidarr.getAlbumByMbid(releaseGroupMbid);
819851
const artistNumericId = parseInt(artistId, 10);
820852
const sameArtistExisting =
821-
existing && existing.artistId === artistNumericId ? existing : null;
853+
existing && String(existing.artistId) === String(artistNumericId)
854+
? existing
855+
: null;
822856
if (sameArtistExisting) {
823857
const mappedExisting = await mapExistingAlbum(
824858
sameArtistExisting,
@@ -846,12 +880,18 @@ export class LibraryManager {
846880
break;
847881
} catch (error) {
848882
if (isAlbumAlreadyAddedError(error)) {
849-
const existingAfterConflict = await lidarr
850-
.getAlbumByMbid(releaseGroupMbid)
851-
.catch(() => null);
883+
const existingAfterConflict =
884+
(await this.waitForAlbumByMbidForArtist(
885+
releaseGroupMbid,
886+
artistNumericId,
887+
{ attempts: 8, delayMs: 1500 },
888+
)) ||
889+
(await lidarr
890+
.getAlbumByMbid(releaseGroupMbid)
891+
.catch(() => null));
852892
const sameArtistAfterConflict =
853893
existingAfterConflict &&
854-
existingAfterConflict.artistId === artistNumericId
894+
String(existingAfterConflict.artistId) === String(artistNumericId)
855895
? existingAfterConflict
856896
: null;
857897
if (sameArtistAfterConflict) {
@@ -953,7 +993,7 @@ export class LibraryManager {
953993
throw error;
954994
}
955995

956-
const existingAlbum = await lidarr.getAlbumByMbid(normalizedAlbumMbid);
996+
let existingAlbum = await lidarr.getAlbumByMbid(normalizedAlbumMbid);
957997
if (
958998
existingAlbum &&
959999
existingAlbum.artistId != null &&
@@ -969,9 +1009,20 @@ export class LibraryManager {
9691009
const settings = getSettings();
9701010
const searchOnAdd = settings.integrations?.lidarr?.searchOnAdd ?? false;
9711011
const shouldTriggerSearch = triggerSearch === true || searchOnAdd;
972-
const album = await this.addAlbum(artist.id, normalizedAlbumMbid, normalizedAlbumName, {
973-
triggerSearch: shouldTriggerSearch,
974-
});
1012+
if (!existingAlbum && createdArtist) {
1013+
existingAlbum = await this.waitForAlbumByMbidForArtist(
1014+
normalizedAlbumMbid,
1015+
artist.id,
1016+
);
1017+
}
1018+
const album = await this.addAlbum(
1019+
artist.id,
1020+
normalizedAlbumMbid,
1021+
normalizedAlbumName,
1022+
{
1023+
triggerSearch: shouldTriggerSearch,
1024+
},
1025+
);
9751026

9761027
if (album?.error) {
9771028
const error = new Error(album.error);

0 commit comments

Comments
 (0)