diff --git a/static/js/podcast-article.js b/static/js/podcast-article.js index b235943..831d0e7 100644 --- a/static/js/podcast-article.js +++ b/static/js/podcast-article.js @@ -260,6 +260,20 @@ const send = (url, method, body) => fetch(url, { body: body === undefined ? undefined : JSON.stringify(body), }); +// This device's name for reserving a wish (/article/claim/), kept per browser or app. +let memDevice = ''; +export function deviceId() { + try { + let id = localStorage.getItem('diora-device-id'); + if (!id) { id = 'd-' + Math.random().toString(36).slice(2, 12); localStorage.setItem('diora-device-id', id); } + return id; + } catch (e) { + return memDevice || (memDevice = 'd-' + Math.random().toString(36).slice(2, 12)); + } +} +export const claimWish = id => send('/podcasts/episodes/' + id + '/article/claim/', 'POST', {device: deviceId()}); +export const release = id => send('/podcasts/episodes/' + id + '/article/claim/', 'DELETE', {device: deviceId()}); + async function getJson(url) { const r = await fetch(url, {cache: 'no-store'}); return {status: r.status, ok: r.ok, data: r.ok ? await r.json() : null}; @@ -330,7 +344,7 @@ export async function pickUp({onState} = {}) { for (const wish of pending) { if (gaveUp.has(wish.episode_id)) { result.skipped++; continue; } let claim; - try { claim = await send('/podcasts/episodes/' + wish.episode_id + '/article/claim/', 'POST', {}); } catch (e) { return result; } + try { claim = await claimWish(wish.episode_id); } catch (e) { return result; } if (!claim.ok) { result.skipped++; continue; } // 409: another device has it try { const saved = await write(wish.episode_id, {onState}); @@ -339,7 +353,7 @@ export async function pickUp({onState} = {}) { } catch (e) { result.failed++; gaveUp.add(wish.episode_id); - try { await send('/podcasts/episodes/' + wish.episode_id + '/article/claim/', 'DELETE'); } catch (e2) { /* it runs out in 15 minutes */ } + try { await release(wish.episode_id); } catch (e2) { /* it runs out in 15 minutes */ } } } } finally { @@ -359,7 +373,7 @@ export function watch(on) { export const _reset = () => { running = false; gaveUp.clear(); }; if (typeof window !== 'undefined' && typeof document !== 'undefined') { - window.DioraPodcastArticle = {write, pickUp, watch, notifyDone, markdownToHtml, transcriptToText}; + window.DioraPodcastArticle = {write, pickUp, watch, notifyDone, claim: claimWish, release, deviceId, markdownToHtml, transcriptToText}; window.dispatchEvent(new Event('diora-podcast-article')); // At the start of diora. setTimeout(() => { pickUp(); }, 4000); diff --git a/tools/tests/podcast-article.test.mjs b/tools/tests/podcast-article.test.mjs index 2b974ae..f1904c8 100644 --- a/tools/tests/podcast-article.test.mjs +++ b/tools/tests/podcast-article.test.mjs @@ -135,10 +135,10 @@ test('pickUp: claim, 409 skipped, failure releases the claim, notifier per succe }); const r = await pa.pickUp(); assert.deepEqual(r, {done: 1, failed: 1, skipped: 1}); - assert.ok(w.calls.includes('POST /podcasts/episodes/2/article/claim/ {}')); + assert.ok(w.calls.some(c => /^POST \/podcasts\/episodes\/2\/article\/claim\/ \{"device":"d-/.test(c))); assert.ok(!w.calls.some(c => c.startsWith('GET /podcasts/episodes/2/'))); // 409: not written - assert.ok(w.calls.includes('DELETE /podcasts/episodes/4/article/claim/')); // failed: released - assert.ok(!w.calls.includes('DELETE /podcasts/episodes/1/article/claim/')); + assert.ok(w.calls.some(c => c.startsWith('DELETE /podcasts/episodes/4/article/claim/'))); // failed: released + assert.ok(!w.calls.some(c => c.startsWith('DELETE /podcasts/episodes/1/article/claim/'))); assert.deepEqual(w.notices, ['Artikel fertig: A']); // The failed one is not tried again in this visit. const w2 = stubWorld({pending: [{episode_id: 4, title: 'Vier'}]}); @@ -168,5 +168,5 @@ test('pickUp: no key does nothing, not even a request; no second run at the same const b = await pa.pickUp(); assert.deepEqual(b, {busy: true}); assert.equal((await a).done, 1); - assert.equal(w2.calls.filter(c => c.startsWith('POST') && c.endsWith('/claim/ {}')).length, 1); + assert.equal(w2.calls.filter(c => c.startsWith('POST') && c.includes('/claim/ {"device":')).length, 1); });