From d90ee51da6ca8ea899d559e788af4ed0f8d30d6c Mon Sep 17 00:00:00 2001
From: Beneditk Hermann <Benedikt.Hermann@br.de>
Date: Thu, 13 Mar 2025 13:50:38 +0100
Subject: [PATCH] added episode list

---
 .gitignore      |   2 +
 src/mappings.ts |  69 ++++++++++++
 src/server.ts   | 271 +++++++++++++++++++++++++-----------------------
 src/types.ts    |  19 +++-
 4 files changed, 231 insertions(+), 130 deletions(-)
 create mode 100644 src/mappings.ts

diff --git a/.gitignore b/.gitignore
index 07e6e47..a62970c 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 0000000..db90fd8
--- /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 69a977f..47adeca 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 9dfa73d..d667e56 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
-- 
GitLab