Include unrated Navidrome tracks in the low-star queue.
CI / Gitleaks (pull_request) Successful in 7s
CI / Unit tests (pull_request) Successful in 10s

Sort by rating ASC so null ratings load with 0–2★, and stop once stars exceed maxRating.
This commit is contained in:
2026-09-06 15:38:48 -04:00
parent bee93e95cc
commit 295f790a8f
2 changed files with 34 additions and 25 deletions
+23 -12
View File
@@ -37,8 +37,8 @@ class NavidromeAdapter extends Adapter {
default: '2',
options: [
{ value: '0', label: 'Unrated / 0★ only' },
{ value: '1', label: '0–1★' },
{ value: '2', label: '0–2★' },
{ value: '1', label: 'Unrated–1★' },
{ value: '2', label: 'Unrated–2★' },
],
},
{
@@ -193,17 +193,32 @@ class NavidromeAdapter extends Adapter {
return new Set((tracks || []).map((t) => t.id));
}
async _fetchRating(rating) {
/**
* Songs with no annotation sort first when ordered by rating ASC (rating=null),
* then 0, 1, 2… Stop once we pass maxRating.
*/
async _fetchLowRated() {
const out = [];
const page = 200;
let start = 0;
for (;;) {
const res = await this._api(`/api/song?_start=${start}&_end=${start + page}&_sort=title&_order=ASC&rating=${rating}`);
const res = await this._api(
`/api/song?_start=${start}&_end=${start + page}&_sort=rating&_order=ASC&missing=false`
);
const total = Number(res.headers.get('X-Total-Count') || 0);
const batch = await res.json();
out.push(...(batch || []));
if (!batch || batch.length === 0) break;
let pastMax = false;
for (const song of batch) {
const rating = song.rating == null ? 0 : Number(song.rating);
if (rating > this.maxRating) {
pastMax = true;
break;
}
out.push(song);
}
start += page;
if (!batch || batch.length === 0 || start >= total || out.length >= this.take * 3) break;
if (pastMax || start >= total || out.length >= this.take * 4) break;
}
return out;
}
@@ -222,11 +237,7 @@ class NavidromeAdapter extends Adapter {
async list() {
const rejected = await this._rejectedIds();
const songs = [];
for (let r = 0; r <= this.maxRating; r += 1) {
const batch = await this._fetchRating(r);
songs.push(...batch);
}
const songs = await this._fetchLowRated();
const items = [];
for (const song of songs) {
if (rejected.has(song.id)) continue;
@@ -240,7 +251,7 @@ class NavidromeAdapter extends Adapter {
subtitle: [song.artist, song.album].filter(Boolean).join(' — ') || undefined,
previewType: 'audio',
meta: {
rating: `${rating}★`,
rating: song.rating == null ? 'unrated' : `${rating}★`,
duration: dur,
year: song.year || undefined,
path: song.path || undefined,
+11 -13
View File
@@ -75,23 +75,19 @@ test('list() loads ≤maxRating songs and skips reject-playlist tracks', async (
if (url.includes('/api/playlist/pl-reject/tracks')) {
return jsonResponse([{ id: 'skip-me' }]);
}
if (url.includes('/api/song') && url.includes('rating=1')) {
if (url.includes('/api/song') && url.includes('_sort=rating')) {
return jsonResponse(
[
{ id: 'unrated', title: 'Bare', artist: 'X', album: 'Y', rating: null, duration: 30 },
{ id: 'skip-me', title: 'Nope', artist: 'A', album: 'B', rating: 1, duration: 10 },
{ id: 'keep-me', title: 'Song', artist: 'Artist', album: 'Album', rating: 1, duration: 120 },
{ id: 'too-high', title: 'Good', artist: 'Z', album: 'Z', rating: 3, duration: 90 },
],
true,
200,
{ total: 2 }
{ total: 4 }
);
}
if (url.includes('/api/song') && url.includes('rating=0')) {
return jsonResponse([], true, 200, { total: 0 });
}
if (url.includes('/api/song') && url.includes('rating=2')) {
return jsonResponse([], true, 200, { total: 0 });
}
return jsonResponse({}, false, 404);
},
async () => {
@@ -104,11 +100,13 @@ test('list() loads ≤maxRating songs and skips reject-playlist tracks', async (
});
await adapter.init();
const items = await adapter.list();
assert.equal(items.length, 1);
assert.equal(items[0].id, 'keep-me');
assert.equal(items[0].previewType, 'audio');
assert.equal(items[0].meta.rating, '1★');
assert.match(items[0].subtitle, /Artist/);
assert.equal(items.length, 2);
assert.equal(items[0].id, 'unrated');
assert.equal(items[0].meta.rating, 'unrated');
assert.equal(items[1].id, 'keep-me');
assert.equal(items[1].previewType, 'audio');
assert.equal(items[1].meta.rating, '1★');
assert.match(items[1].subtitle, /Artist/);
}
);
});