import { Client } from "@atcute/client"; import { PasswordSession } from "@atcute/password-session"; const AGENT_STRING = "pipris/0.1.1 (https://tangled.org/clay.rip/pipris)"; const PLAY_FINISH_TOLERANCE_MS = 5000; let rpc; let did; let prevTrack = null; // --- TID generation (AT Protocol timestamp-based ID) --- const B32_CHARS = "234567abcdefghijklmnopqrstuvwxyz"; let lastTimestamp = 0; let clockId = Math.floor(Math.random() * 1024); function generateTid() { let timestamp = Date.now() * 1000; // microseconds if (timestamp <= lastTimestamp) { timestamp = lastTimestamp + 1; } lastTimestamp = timestamp; const id = BigInt(timestamp) << 10n | BigInt(clockId); let encoded = ""; let val = id; for (let i = 0; i < 13; i++) { encoded = B32_CHARS[Number(val & 31n)] + encoded; val >>= 5n; } return encoded; } // --- Initialisation --- export async function init(config) { if (!config) { throw new Error("Missing config/teal.json"); } const session = await PasswordSession.login({ service: config.service, identifier: config.handle, password: config.password, }); did = session.did; rpc = new Client({ handler: session }); console.log("[teal] Logged in as", did); } // --- Status update --- async function updateStatus(data) { await rpc.post("com.atproto.repo.putRecord", { input: { repo: did, collection: "fm.teal.alpha.actor.status", rkey: "self", record: { $type: "fm.teal.alpha.actor.status", time: data.playedTime, expiry: data.expiry, item: { trackName: data.trackName, artists: data.artists, releaseName: data.releaseName || undefined, duration: data.duration || undefined, playedTime: data.playedTime, musicServiceBaseDomain: "local", submissionClientAgent: AGENT_STRING, }, }, }, }); } async function clearStatus() { const now = new Date(); const expired = new Date(now.getTime() - 60000); await rpc.post("com.atproto.repo.putRecord", { input: { repo: did, collection: "fm.teal.alpha.actor.status", rkey: "self", record: { $type: "fm.teal.alpha.actor.status", time: expired.toISOString(), expiry: expired.toISOString(), item: { trackName: "", artists: [], }, }, }, }); } // --- Play submission --- async function submitPlay(track) { await rpc.post("com.atproto.repo.createRecord", { input: { repo: did, collection: "fm.teal.alpha.feed.play", rkey: generateTid(), record: { $type: "fm.teal.alpha.feed.play", trackName: track.trackName, artists: track.artists, releaseName: track.releaseName || undefined, duration: track.duration || undefined, playedTime: track.playedTime, musicServiceBaseDomain: "local", submissionClientAgent: AGENT_STRING, }, }, }); console.log(`[teal] Submitted play: ${track.trackName}`); } function shouldSubmitPlay(prev) { if (!prev) return false; const expiryMs = new Date(prev.expiry).getTime(); return Date.now() >= expiryMs - PLAY_FINISH_TOLERANCE_MS; } // --- Module entry point --- export async function onData(data) { if (!rpc) return; const trackKey = `${data.trackName}|${data.artists.map((a) => a.artistName).join(",")}`; const prevKey = prevTrack ? `${prevTrack.trackName}|${prevTrack.artists.map((a) => a.artistName).join(",")}` : null; if (trackKey !== prevKey) { if (shouldSubmitPlay(prevTrack)) { try { await submitPlay(prevTrack); } catch (err) { console.error(`[teal] Failed to submit play: ${err.message}`); } } prevTrack = { ...data }; } else { prevTrack = { ...data }; } if (data.playbackStatus !== "Playing") { try { await clearStatus(); } catch (err) { console.error(`[teal] Failed to clear status: ${err.message}`); } return; } try { await updateStatus(data); } catch (err) { console.error(`[teal] Failed to update status: ${err.message}`); } } export async function onClear() { if (!rpc) return; try { await clearStatus(); console.log("[teal] Cleared status (player gone)"); } catch (err) { console.error(`[teal] Failed to clear status: ${err.message}`); } }