Music: Mixes, Favorites and Stats; smart and Family playlists; library radio; offline albums; more
build-container / build (push) Successful in 6m21s
build-container / build (push) Successful in 6m21s
- Mixes tab: daily mix, most played, forgotten favorites, moods, on this day, decades, genres, smart playlists (rules), library radio - Favorites (hearts, kept by the hub) and Stats (plays, time, top artists/songs/albums, streak, recently played) - Family playlists shared by everyone with Music; queue reorder; More like this; album/artist About notes - Save albums offline; player takes colors from the cover; Share a lyric line as a picture; verse of the day on Featured - musicdata.js keeps plays, favorites, smart rules and Family playlists, with tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46e686ce22
commit
2d5aaad41a
14 files changed
+572
-24
No files matched your search
@@ -0,0 +1,37 @@
|
||||
// Music data: plays, history and stats, favorites, smart playlist rules, and Family playlists shared by everyone with Music.
|
||||
const http = require('http'), assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path');
|
||||
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-md-'));
|
||||
const users = require('../users'), md = require('../musicdata');
|
||||
const people = { a: users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b: users.create({ name: 'Bob', role: 'user', tabs: ['music'], handle: 'Yg' }), c: users.create({ name: 'Cy', role: 'user', tabs: ['music'], handle: 'Yw' }) };
|
||||
const hub = http.createServer((req, res) => users.runAs(people[req.headers['x-user'] || 'a'], () => md.handle(req, res))).listen(0, async () => {
|
||||
const B = 'http://127.0.0.1:' + hub.address().port + '/api/music', call = (p, o = {}, who) => fetch(B + p, { ...o, headers: { ...(o.headers || {}), ...(who ? { 'x-user': who } : {}) } }), post = (p, b, who, m = 'POST') => call(p, { method: m, body: JSON.stringify(b) }, who), j = async r => r.json();
|
||||
const s = (n, o = {}) => ({ key: String(100 + n), title: 'Song ' + n, artist: 'Artist ' + (n % 2), album: 'Album ' + n, thumb: '/library/metadata/1/thumb/1', artistKey: String(10 + n % 2), albumKey: String(50 + n), dur: 180000, ...o });
|
||||
try {
|
||||
// plays
|
||||
assert.equal((await post('/played', { key: 'abc', title: 'x' })).status, 400, 'only Plex songs');
|
||||
for (const n of [1, 2, 1, 3, 1]) assert.equal((await post('/played', s(n))).status, 200);
|
||||
const h = await j(await call('/history?limit=3')); assert.equal(h.length, 3); assert.equal(h[0].title, 'Song 1', 'newest first');
|
||||
assert.equal((await j(await post('/played', s(1)))).duplicate, true, 'the same song twice in a moment counts once'); assert.equal((await j(await call('/history'))).length, 5);
|
||||
const st = await j(await call('/stats?range=week'));
|
||||
assert.ok(st.plays >= 4 && st.minutes >= 9); assert.equal(st.songs[0].title, 'Song 1'); assert.equal(st.songs[0].plays, 3, 'the most played song is first'); assert.equal(st.artists[0].name, 'Artist 1'); assert.equal(st.series.length, 7, 'a week has seven days'); assert.equal(st.series.at(-1).minutes, st.minutes, 'all of it was today'); assert.equal(st.streak, 1);
|
||||
assert.equal((await j(await call('/stats?range=year'))).series.length, 1, 'a year is shown per month'); assert.deepEqual((await j(await call('/stats', {}, 'b'))).plays, 0, "each person's own history");
|
||||
assert.equal((await j(await call('/top?days=30&limit=2'))).length, 2);
|
||||
// favorites
|
||||
await post('/favs', s(7)); await post('/favs', s(8)); await post('/favs', s(7)); let f = await j(await call('/favs')); assert.equal(f.length, 2, 'saving twice keeps one'); assert.equal(f[0].key, '107', 'the one just saved again is newest');
|
||||
await call('/favs/107', { method: 'DELETE' }); assert.deepEqual((await j(await call('/favs'))).map(x => x.key), ['108']); assert.deepEqual(await j(await call('/favs', {}, 'b')), []);
|
||||
// smart playlist rules are cleaned
|
||||
let sp = await j(await post('/smart', { name: 'Old gospel', rules: { genres: ['Gospel', '<script>', 'Soul'], yearFrom: 1950, yearTo: 1989, addedDays: -5, order: 'weird', limit: 9999, onlyFavorites: 1 } })); assert.deepEqual(sp.rules.genres, ['Gospel', 'Soul']); assert.equal(sp.rules.addedDays, 0); assert.equal(sp.rules.order, 'random'); assert.equal(sp.rules.limit, 50, 'a silly limit falls back'); assert.equal(sp.rules.onlyFavorites, true);
|
||||
sp = await j(await post('/smart', { id: sp.id, name: 'Renamed', rules: { order: 'newest', limit: 20 } })); assert.equal((await j(await call('/smart'))).length, 1); assert.equal(sp.rules.order, 'newest'); assert.equal((await post('/smart', { name: '' })).status, 400);
|
||||
await call('/smart/' + sp.id, { method: 'DELETE' }); assert.deepEqual(await j(await call('/smart')), []);
|
||||
// family playlists
|
||||
let fam = await j(await post('/family', { name: 'Road trip', tracks: [s(1), s(2)] }, 'b')); assert.equal(fam.count, 2); assert.equal(fam.ownerName, 'Bob'); assert.equal(fam.tracks[0].by, 'Bob');
|
||||
assert.equal((await j(await call('/family', {}, 'c'))).length, 1, 'everyone sees it');
|
||||
fam = await j(await post('/family/' + fam.id + '/add', { tracks: [s(2), s(3)] }, 'c')); assert.equal(fam.added, 1, 'a repeat is not added again'); assert.equal(fam.count, 3);
|
||||
fam = await j(await call('/family/' + fam.id + '/tracks/101', { method: 'DELETE' }, 'c')); assert.equal(fam.count, 2, 'anyone can take a song off');
|
||||
assert.equal((await post('/family/' + fam.id, { name: 'Mine now' }, 'c')).status, 403, 'only the owner or an admin renames'); assert.equal((await post('/family/' + fam.id, { name: 'Trip' }, 'b')).status, 200); assert.equal((await post('/family/' + fam.id, { name: 'Admin said' }, 'a')).status, 200);
|
||||
assert.equal((await call('/family/' + fam.id, { method: 'DELETE' }, 'c')).status, 403); assert.equal((await call('/family/' + fam.id, { method: 'DELETE' }, 'a')).status, 200); assert.deepEqual(await j(await call('/family')), []);
|
||||
assert.equal((await call('/history', { method: 'DELETE' })).status, 200); assert.deepEqual(await j(await call('/history')), []);
|
||||
console.log('musicdata ok');
|
||||
} catch (e) { console.error(e); process.exitCode = 1; }
|
||||
hub.close(); setTimeout(() => process.exit(process.exitCode || 0), 50);
|
||||
});
|
||||
Reference in new issue
Block a user