diff --git a/.gitignore b/.gitignore index 07e6e472cc75fafa944e2a6d4b0f101bc476c060..a62970cd320860fd3557df4e83d103c53c9502a5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /node_modules +package-lock.json +.env diff --git a/src/mappings.ts b/src/mappings.ts new file mode 100644 index 0000000000000000000000000000000000000000..db90fd8d3d2de73274b9f960291f74c6e6fd5f2b --- /dev/null +++ b/src/mappings.ts @@ -0,0 +1,69 @@ +import { ActivityPubNote, ActivityPubOutbox, EpisodeResponse } from "./types"; + +export function transformEpisode(response: EpisodeResponse) { + const item = response.item; + return { + "@context": "https://www.w3.org/ns/activitystreams", + "id": item.assetId, + "type": "PodcastEpisode", + "published": item.published, + "updated": item.updated, + "attributedTo": item.show.coreId, + "externalId": item.url, + "title": item.title, + "description": { + "type": "Note", + "mediaType": "text/markdown", + "content": item.description + }, + "image": item.imagesList.length > 0 ? { + "type": "Image", + "url": item.imagesList[0].url + } : undefined, + "audio": item.audioList.map(audio => ({ + "type": "Audio", + "name": audio.title, + "mediaType": "audio/mpeg", + "url": audio.href + })), + "partOf": { + "type": "PodcastSeries", + "id": item.show.coreId, + "externalId": item.show.externalId, + "name": item.show.title, + "attributedTo": item.show.publisher.coreId, + "publisher": { + "type": "BroadcastService", + "name": item.show.publisher.title, + "url": item.show.publisher.url, + "organization": { + "type": "Organization", + "name": item.show.publisher.organization.name, + "url": item.show.publisher.organization.url + } + } + } + }; +} + +export function transformEpisodeList(input: any): ActivityPubOutbox { + const baseUrl = "https://example.com/outbox"; + const actorUrl = "https://example.com/actor"; + + const orderedItems: ActivityPubNote[] = input.show.items.nodes.map((item: any) => ({ + id: item.assetId, + type: "Note", + actor: actorUrl, + published: new Date().toISOString(), + to: ["https://www.w3.org/ns/activitystreams#Public"], + content: item.title, + url: item.url, + })); + + return { + "@context": "https://www.w3.org/ns/activitystreams", + id: baseUrl, + type: "OrderedCollection", + orderedItems, + }; + } \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 69a977f434e033b4e4f93452cba6abc34e02a5f5..47adeca61176f5b34778d0a8f1f06bdd90418f98 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,120 +1,77 @@ import express from 'express'; import { Client, cacheExchange, fetchExchange, gql } from '@urql/core'; -import { Response } from './types'; +import { EpisodeResponse } from './types'; +import { transformEpisode, transformEpisodeList } from './mappings'; const app = express(); const PORT = 3000; const audiothekApi = new Client({ - url: 'https://api.ardaudiothek.de/graphql', - exchanges: [cacheExchange, fetchExchange], + url: 'https://api.ardaudiothek.de/graphql', + exchanges: [cacheExchange, fetchExchange], }); const query = gql` - query Item { - item(id: "14278953") { - show { - coreId - externalId - title - url - publisher: publicationService { - coreId - dvbServiceId - title - url - organizationName - organization { - name - url - } - } - coreDocument - } - assetId - url: sharingUrl - title - description - published: publishDate - updated: core(key: "modified") - imagesList { - url - title - } - coreDocument - audioList { - title - audioCodec - href - } - } - } + query Item { + item(id: "14278953") { + show { + coreId + externalId + title + url + publisher: publicationService { + coreId + dvbServiceId + title + url + organizationName + organization { + name + url + } + } + coreDocument + } + assetId + url: sharingUrl + title + description + published: publishDate + updated: core(key: "modified") + imagesList { + url + title + } + coreDocument + audioList { + title + audioCodec + href + } + } + } `; -async function fetchData(query: any, variables: any) { - const result = await audiothekApi.query<Response>(query, variables).toPromise(); - if (result.error) { - throw result.error; - } - // console.log(JSON.stringify(result.data, null, 2)); - return result.data; +async function fetchData<R>(query: any, variables: any) { + const result = await audiothekApi.query<R>(query, variables).toPromise(); + if (result.error) { + throw result.error; + } + // console.log(JSON.stringify(result.data, null, 2)); + return result.data; } -function transformEpisodeToActivityPub(response: Response) { - const item = response.item; - return { - "@context": "https://www.w3.org/ns/activitystreams", - "id": item.assetId, - "type": "PodcastEpisode", - "published": item.published, - "updated": item.updated, - "attributedTo": item.show.coreId, - "externalId": item.url, - "title": item.title, - "description": { - "type": "Note", - "mediaType": "text/markdown", - "content": item.description - }, - "image": item.imagesList.length > 0 ? { - "type": "Image", - "url": item.imagesList[0].url - } : undefined, - "audio": item.audioList.map(audio => ({ - "type": "Audio", - "name": audio.title, - "mediaType": "audio/mpeg", - "url": audio.href - })), - "partOf": { - "type": "PodcastSeries", - "id": item.show.coreId, - "externalId": item.show.externalId, - "name": item.show.title, - "attributedTo": item.show.publisher.coreId, - "publisher": { - "type": "BroadcastService", - "name": item.show.publisher.title, - "url": item.show.publisher.url, - "organization": { - "type": "Organization", - "name": item.show.publisher.organization.name, - "url": item.show.publisher.organization.url - } - } - } - }; -} + async function main() { - const response = await fetchData(query, { id: '14278953' }); + const response = await fetchData(query, { id: '14278953' }); - if (response) { - const activityPubEpisode = transformEpisodeToActivityPub(response); - console.log(JSON.stringify(activityPubEpisode, null, 2)); - } + if (response) { + const activityPubEpisode = transformEpisode(response); + console.log(JSON.stringify(activityPubEpisode, null, 2)); + } } // main(); @@ -122,43 +79,99 @@ async function main() { app.get('/:actor/episodes/:id', async (req, res) => { - try { - const data = await fetchData(query, { id: req.params.id }); - if (!data) { - res.status(404).json({ error: 'Episode not found' }); - return; - } + try { + const data = await fetchData<EpisodeResponse>(query, { id: req.params.id }); + if (!data) { + res.status(404).json({ error: 'Episode not found' }); + return; + } + + // TODO: verifiy that the actor is the same as the attributedTo field - // TODO: verifiy that the actor is the same as the attributedTo field + const activityPubDocument = transformEpisode(data); + res.json(activityPubDocument); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch episode data' }); + } +}); - const activityPubDocument = transformEpisodeToActivityPub(data); - res.json(activityPubDocument); - } catch (error) { - res.status(500).json({ error: 'Failed to fetch episode data' }); - } +app.get('/:actor/episodes', async (req, res) => { + const query = gql` + query ShowWithEpisodes($showId: ID!, $first: Int = 10, $offset: Int) { + show(id: $showId) { + coreId + externalId + title + url + + items(first: $first, orderBy: PUBLISH_DATE_DESC, offset: $offset, condition: { + isPublished: true + }) { + nodes { + assetId + title + url: sharingUrl + } + } + publisher: publicationService { + coreId + dvbServiceId + title + url + organizationName + organization { + name + url + } + } + } + } + `; + + + try { + + // TODO: verifiy that the actor is the same as the attributedTo field + + + const data = await fetchData<any>(query, { showId: "urn:ard:show:a42d1ea0b4a07053" }); + if (!data) { + res.status(404).json({ error: 'Episode not found' }); + return; + } + + + const activityPubDocument = transformEpisodeList(data); + res.json(activityPubDocument); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch episode data' }); + } }); + app.get('/@blaue-couch', async (req, res) => { - res.json({ - "@context": "https://www.w3.org/ns/activitystreams", - "id": "urn:ard:show:a42d1ea0b4a07053", - "type": "Service", - "name": "Blaue Couch", - "externalId": "https://feeds.br.de/blaue-couch/feed.xml", - "attributedTo": "urn:ard:publisher:c4a9cee041835529", - "publisher": { - "type": "BroadcastService", - "name": "BAYERN 1", - "url": "https://www.ardaudiothek.de/radio/br/bayern-1/", - "organization": { - "type": "Organization", - "name": "BR", - "url": "https://www.ardaudiothek.de/radio/br/" - } - } - }); + res.json({ + "@context": "https://www.w3.org/ns/activitystreams", + "id": "urn:ard:show:a42d1ea0b4a07053", + "type": "Service", + "name": "Blaue Couch", + "externalId": "https://feeds.br.de/blaue-couch/feed.xml", + "attributedTo": "urn:ard:publisher:c4a9cee041835529", + "publisher": { + "type": "BroadcastService", + "name": "BAYERN 1", + "url": "https://www.ardaudiothek.de/radio/br/bayern-1/", + "organization": { + "type": "Organization", + "name": "BR", + "url": "https://www.ardaudiothek.de/radio/br/" + } + } + }); }); +app.get('/', (req, res) => res.redirect('/@blaue-couch')); + app.listen(PORT, () => { - console.log(`Server running at http://localhost:${PORT}`); + console.log(`Server running at http://localhost:${PORT}`); }); diff --git a/src/types.ts b/src/types.ts index 9dfa73d2b947c9f1510610855f3a622ef1be4671..d667e56ec27141a91f1fa775be5b0478ff2ff080 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,23 @@ export type Item = { show: Show; }; -export type Response = { +export type EpisodeResponse = { item: Item; +}; + +export type ActivityPubOutbox = { + "@context": string; + id: string; + type: string; + orderedItems: ActivityPubNote[]; +}; + +export type ActivityPubNote = { + id: string; + type: string; + actor: string; + published: string; + to: string[]; + content: string; + url: string; }; \ No newline at end of file