diff --git a/src/adapters/media-adapter.mjs b/src/adapters/media-adapter.mjs new file mode 100644 index 0000000..e51b617 --- /dev/null +++ b/src/adapters/media-adapter.mjs @@ -0,0 +1,52 @@ +/** + * Media Playback Context Adapter + * Normalizes raw playback histories into unified CCP music signals. + */ + +/** + * Parses a raw media history log string or a telemetry track frame object. + * @param {Object|string} rawPlayback - Input media playback detail data + * @returns {Object} Standardized CCP context metadata payload + */ +export function parseMediaPlayback(rawPlayback) { + let trackName = "Unknown Track"; + let artistName = "Unknown Artist"; + let contextUri = ""; + let durationMs = 0; + let timestamp = new Date().toISOString(); + + // Support parsing either a direct raw track title string or structured JSON telemetry + if (typeof rawPlayback === "string") { + const parts = rawPlayback.split(" - "); + if (parts.length >= 2) { + artistName = parts[0].trim(); + trackName = parts[1].trim(); + } else { + trackName = rawPlayback.trim(); + } + } else if (rawPlayback && typeof rawPlayback === "object") { + // Extract property variables matching standard media streaming webhook structures (e.g. Spotify) + trackName = rawPlayback.track_name || rawPlayback.track?.name || trackName; + artistName = rawPlayback.artist_name || rawPlayback.track?.artists?.[0]?.name || artistName; + contextUri = rawPlayback.context_uri || rawPlayback.context?.uri || ""; + durationMs = Number(rawPlayback.duration_ms || rawPlayback.track?.duration_ms || 0); + + if (rawPlayback.played_at || rawPlayback.timestamp) { + timestamp = new Date(rawPlayback.played_at || rawPlayback.timestamp).toISOString(); + } + } + + return { + schema_version: "memact.ccp_observation.v1", + event_type: "media_playback", + category: "music", + title: `Listened to ${trackName} by ${artistName}`, + evidence: { + track_title: trackName, + artist: artistName, + duration_ms: durationMs, + streaming_context_uri: contextUri + }, + occurred_at: timestamp + }; +} \ No newline at end of file diff --git a/src/index.mjs b/src/index.mjs index 5b0721f..cc35c73 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1,4 +1,5 @@ export { createMemactClient, MemactClient } from "./client.mjs" export { MemactSDKError } from "./errors.mjs" export { validateClientConfig } from "./config-validation.mjs" -export { MemactProvider, useContextClaim } from "./react.mjs" \ No newline at end of file +export { parseMediaPlayback } from "./adapters/media-adapter.mjs" +export { MemactProvider, useContextClaim } from "./react.mjs" diff --git a/test/media-adapter.test.mjs b/test/media-adapter.test.mjs new file mode 100644 index 0000000..2599343 --- /dev/null +++ b/test/media-adapter.test.mjs @@ -0,0 +1,40 @@ +import test from "node:test" +import assert from "node:assert/strict" +import { parseMediaPlayback } from "../src/adapters/media-adapter.mjs" + +test("parseMediaPlayback splits a raw text log string correctly into artist and track", () => { + const rawLog = "Daft Punk - One More Time"; + const result = parseMediaPlayback(rawLog); + + assert.equal(result.schema_version, "memact.ccp_observation.v1"); + assert.equal(result.category, "music"); + assert.equal(result.evidence.artist, "Daft Punk"); + assert.equal(result.evidence.track_title, "One More Time"); +}); + +test("parseMediaPlayback falls back cleanly if string format lacks standard delimiter", () => { + const rawLog = "Ambient White Noise Soundscape Loops"; + const result = parseMediaPlayback(rawLog); + + assert.equal(result.evidence.artist, "Unknown Artist"); + assert.equal(result.evidence.track_title, "Ambient White Noise Soundscape Loops"); +}); + +test("parseMediaPlayback normalizes structured Spotify webhook payload data structures", () => { + const payloadMock = { + played_at: "2026-07-12T07:30:00Z", + context_uri: "spotify:playlist:37i9dQZF1DX4sWSpwq3LiO", + track: { + name: "Starboy", + duration_ms: 230453, + artists: [{ name: "The Weeknd" }] + } + }; + + const result = parseMediaPlayback(payloadMock); + assert.equal(result.evidence.track_title, "Starboy"); + assert.equal(result.evidence.artist, "The Weeknd"); + assert.equal(result.evidence.duration_ms, 230453); + assert.equal(result.evidence.streaming_context_uri, "spotify:playlist:37i9dQZF1DX4sWSpwq3LiO"); + assert.equal(result.occurred_at, "2026-07-12T07:30:00.000Z"); +}); \ No newline at end of file