diff --git a/crates/ipx/src/main.rs b/crates/ipx/src/main.rs index 9be989f..9c8a8aa 100644 --- a/crates/ipx/src/main.rs +++ b/crates/ipx/src/main.rs @@ -2683,6 +2683,58 @@ mod tests { assert!(deadair::plan_for(&ctx, u, "f", 7).await.unwrap().is_none()); } + #[tokio::test] + async fn two_people_play_one_file_each_in_their_own_version_and_place() { + let ctx = test_ctx(config::Config::default()).await; + let db = &ctx.db; + for sql in [ + "INSERT INTO entries (feed_id, guid, first_seen, duration) VALUES ('f', 'g', 1, 600)", + "INSERT INTO enclosures (id, feed_id, guid, url, path, state, downloaded_at) + VALUES (7, 'f', 'g', 'http://x/a.mp3', '/x/a.mp3', 'done', 100)", + ] { + db.sql(sql).await.unwrap(); + } + db.store_plans(7, r#"{"300": {"cuts": [[60000,120000],[200000,260000]], "pieces": []}, + "500": {"cuts": [[60000,100000]], "pieces": []}}"#).await.unwrap(); + // One file on disk, three listeners at once: DeadAir at the usual pause, at a longer + // one, and the original. + let (usual, long, plain) = ( + db.create_user("usual", None, false).await.unwrap(), + db.create_user("long", None, false).await.unwrap(), + db.create_user("plain", None, false).await.unwrap(), + ); + for u in [usual, long, plain] { + db.subscribe(u, "f").await.unwrap(); + } + for u in [usual, long] { + db.set_deadair(u, "f", Some(true)).await.unwrap(); + } + db.set_deadair_pause(long, "f", 500).await.unwrap(); + // Each is handed their own playlist, as /media/7/deadair.m3u8 does. + let playing = deadair::Playing::default(); + for u in [usual, long] { + let plan = deadair::plan_for(&ctx, u, "f", 7).await.unwrap().unwrap(); + playing.lock().unwrap().insert((u, "f".into(), "g".into()), Arc::new(plan)); + } + assert!(deadair::plan_for(&ctx, plain, "f", 7).await.unwrap().is_none()); + // All three save 3:20 of what they hear, as the position route does: each is read in + // that person's own clock and kept as a place in the original, theirs alone. + let mut kept = vec![]; + for u in [usual, long, plain] { + let (secs, len) = deadair::on_save(&ctx, &playing, u, "f", "g", 200, None).await.unwrap(); + db.set_position(u, "f", "g", secs, len).await.unwrap(); + kept.push(secs); + } + assert_eq!(kept, [320, 240, 200], "two minutes cut before 3:20, forty seconds, none"); + // Each comes back to 3:20 of their own version, whatever the others saved. + let order = db::order_sql("published", "desc"); + for (u, len) in [(usual, 480), (long, 560), (plain, 600)] { + let mut rows = db.entries_in(u, Some("f"), db::Filter::All, None, 0, 10, &order).await.unwrap(); + deadair::for_reader(&ctx, u, &mut rows).await.unwrap(); + assert_eq!((rows[0].position, rows[0].duration), (200, Some(len)), "user {u}"); + } + } + #[tokio::test] async fn two_changes_to_the_catalogue_at_once_both_keep() { let ctx = Arc::new(test_ctx(config::Config::default()).await); diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 65bee55..0258aef 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -1936,6 +1936,36 @@ test('DeadAir is off until turned on for a show, then plays it shortened, with y await page.locator('#ssave').click(); await expect.poll(async () => (await entry()).enclosures[0].deadair).toBeLessThan(usual); + // Someone else on the same show at the usual pause, at the same address at the same time: one + // file on disk, a playlist each, and neither kept by a cache to be handed to the other. + { + const { execFileSync } = require('child_process'); + const setup = require('./global-setup'); + const env = { ...process.env, IPX_CONFIG: `${setup.root}/config/config.toml`, IPX_DATA_DIR: `${setup.root}/data` }; + try { execFileSync('./target/debug/ipx', ['user', 'add', 'quinn'], { input: 'quinnpassword', env }); } + catch (e) { if (!String(e.stderr || e.stdout).includes('already exists')) throw e; } + const ctx = await page.context().browser().newContext(); + const quinn = await ctx.newPage(); + await quinn.goto('/login'); + await quinn.locator('#name').fill('quinn'); + await quinn.locator('#pw').fill('quinnpassword'); + await quinn.locator('button[type=submit]').click(); + await expect(quinn.locator('#feedlist')).toBeVisible(); + await quinn.request.post('/api/feeds', { data: { url: 'http://127.0.0.1:8792/quiet.xml' } }); + await quinn.request.patch(`/api/feeds/${encodeURIComponent(id)}`, { data: { deadair: true } }); + const enc = (await entry()).enclosures[0].id; + const fetchList = async p => { const r = await p.request.get(`/media/${enc}/deadair.m3u8`); + return { status: r.status(), cache: r.headers()['cache-control'], body: await r.text() }; }; + const [mine, theirs] = await Promise.all([fetchList(page), fetchList(quinn)]); + const length = l => l.body.match(/#EXTINF:([\d.]+)/g).reduce((s, x) => s + Number(x.slice(8)), 0); + expect([mine.status, theirs.status]).toEqual([200, 200]); + expect([mine.cache, theirs.cache]).toEqual(['no-store', 'no-store']); + expect(mine.body).not.toBe(theirs.body); + expect(length(theirs), 'the usual pause cuts more than the longer one').toBeLessThan(length(mine)); + await quinn.request.delete(`/api/feeds/${encodeURIComponent(id)}`); + await ctx.close(); + } + // Saved without its dead air too (#186): smaller than the original, named to say so. const saved = await page.evaluate(async id => { const [d, o, p] = await Promise.all([fetch(`/media/${id}/deadair`), fetch(`/media/${id}`), fetch(`/media/${id}/deadair.mp3`, { headers: { Range: 'bytes=0-99' } })]);