From 178beeca6ba1e2cf9e5349b0af1f7baa5b70a001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Wed, 13 Aug 2025 20:23:58 +0200 Subject: [PATCH 1/6] Add UUID namespace details --- astro.config.mjs | 4 + src/content/docs/specs/namespaces.mdx | 199 ++++++++++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 src/content/docs/specs/namespaces.mdx diff --git a/astro.config.mjs b/astro.config.mjs index d82197eb..44ff372d 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -45,6 +45,10 @@ export default defineConfig({ label: "Introduction", link: "specs", }, + { + label: "UUID namespaces", + link: "specs/namespaces", + }, { label: "Subscriptions", collapsed: true, diff --git a/src/content/docs/specs/namespaces.mdx b/src/content/docs/specs/namespaces.mdx new file mode 100644 index 00000000..d7cae395 --- /dev/null +++ b/src/content/docs/specs/namespaces.mdx @@ -0,0 +1,199 @@ +--- +title: UUID namespaces +description: Instructions for calculating UUIDs using namespaces +next: false +prev: false +tableOfContents: true +sidebar: + order: 2 +--- + +import { Tabs, TabItem } from '@astrojs/starlight/components'; + +The Open Podcast API uses [UUIDv5 values](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-5) as identifiers for entities. This value is known as a GUID (Globally Unique Identifier). Each GUID value MUST be calculated using the appropriate namespace and methodology to prevent duplication. + +Clients are responsible for parsing or calculating the GUID of feeds and episodes. The server stores this information, but does not calculate it. + +## Feed identifiers + +Feed GUID values MUST be created in accordance with the [Podcast Index's methodology](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/tags/guid.md). If a feed already has a valid UUIDv5 `guid` tag, the client MUST pass this value to the server when submitting the feed. If the feed doesn't have a valid `guid` tag, the client MUST: + +1. Generate a UUIDv5 `guid` value using the feed URL and the `podcast` namespace UUID: `ead4c236-bf58-58c6-a2c6-a6b28d128cb6`. +1. Pass the calculated value to the server when submitting a subscription. + +This process ensures that any feed not currently registered with the Podcast Index is identified by the exact same GUID it would receive if it were updated to the Podcasting 2.0 specification. + +### Example + +Here is a simple example of how to calculate the GUID for a feed from its feed URL. + + + + + ```java + import java.util.UUID; + import com.fasterxml.uuid.Generators; + import com.fasterxml.uuid.impl.NameBasedGenerator; + + public class UuidCalculator { + static final UUID podcastNamespace = UUID.fromString("ead4c236-bf58-58c6-a2c6-a6b28d128cb6"); + static final NameBasedGenerator generator = Generators.nameBasedGenerator(podcastNamespace); + + public static UUID calculateFeedId(String feedUrl) { + final UUID feedUuid = UUID.fromString(feedUrl); + return generator.generate(feedUrl); + } + } + ``` + + + + + ```py + import uuid + + def calculate_uuid(feed_url): + PODCAST_NAMESPACE = uuid.UUID("ead4c236-bf58-58c6-a2c6-a6b28d128cb6") + return uuid.uuid5(PODCAST_NAMESPACE, feed_url) + ``` + + + + + ```ts + import { v5 as uuidv5 } from "uuid"; + + const PODCAST_NAMESPACE = "ead4c236-bf58-58c6-a2c6-a6b28d128cb6"; + + function calculateUuid(feedUrl: string): string { + return uuidv5(feedUrl, PODCAST_NAMESPACE); + } + ``` + + + + +Running the above example with the feed URL `"podnews.net/rss"` will yield `9b024349-ccf0-5f69-a609-6b82873eab3c`. + +## Episode identifiers + +In the best-case scenario, podcast episodes are also identified by a case-sensitive string `guid`. This value MUST be unique on a per-feed basis. Some feeds do not supply `guid` values for episodes. In this case, the client must make use of other data points to create GUIDs for episodes. Each client MUST follow the same process. This ensures that clients calculate the same GUID for each episode. + +To calculate the GUID for a podcast episode: + +1. Calculate the feed UUID and store it as the namespace. +1. If a `guid` value is present in the episode section: + 1. Strip leading and trailing whitespace from the `guid` value. + 1. Convert the `guid` to lowercase. + 1. Create a new UUIDv5 value using the modified `guid` and the feed namespace. +1. If a `guid` value is NOT present in the episode section: + 1. Concatenate the `title`, `enclosure_url`, and `publish_date` as a single string. + 1. Strip leading and trailing whitespace from the resulting string and convert to lowercase. + 1. Create a new UUIDv5 value using the concatenated values and the feed namespace. + +### Example + +Here is a simple example of how to calculate the episode GUID using the `guid` tag. + + + + + ```java + import java.util.UUID; + import com.fasterxml.uuid.Generators; + import com.fasterxml.uuid.impl.NameBasedGenerator; + + public class UuidCalculator { + + public static UUID calculateEpisodeGuid(String guid, UUID feedUuid) { + final String sanitizedInput = guid.strip().toLowerCase(); + final NameBasedGenerator generator = Generators.nameBasedGenerator(feedUuid); + return generator.generate(sanitizedInput); + } + } + ``` + + + + + ```py + import uuid + + def calculate_episode_guid_from_guid(guid, feed_uuid): + normalized_input = guid.strip().lower() + namespace = uuid.UUID(feed_uuid) + return uuid.uuid5(namespace, normalized_input) + ``` + + + + + ```ts + import { v5 as uuidv5 } from "uuid"; + + function calculateEpisodeGuidFromGuid(guid: string, feedUuid: string): string { + const normalizedInput = guid.trim().toLowerCase(); + return uuidv5(normalizedInput, feedUuid); + } + ``` + + + + +Running the above example with the GUID `"https://example.com/episode_3.mp3"` and the feed UUID `9b024349-ccf0-5f69-a609-6b82873eab3c` will yield `66932137-05d2-5594-8b01-e84e025340ea`. + +Here is how to calculate the episode GUID using normalized metadata. + + + + + ```java + import java.util.UUID; + import com.fasterxml.uuid.Generators; + import com.fasterxml.uuid.impl.NameBasedGenerator; + + public class UuidCalculator { + + public static UUID calculateEpisodeGuid(String title, String enclosureUrl, String publishDate, UUID feedUuid) { + final String sanitizedInput = (title + enclosureUrl + publishDate).strip(); + final NameBasedGenerator generator = Generators.nameBasedGenerator(feedUuid); + return generator.generate(sanitizedInput); + } + } + ``` + + + + + ```py + import uuid + + def calculate_episode_guid_from_metadata(title, enclosure_url, publish_date, feed_uuid): + normalized_input = (title + enclosure_url + publish_date).strip().lower() + namespace = uuid.UUID(feed_uuid) + return uuid.uuid5(namespace, normalized_input) + ``` + + + + + ```ts + import { v5 as uuidv5 } from "uuid"; + + function calculateEpisodeGuidFromMetadata(title: string, enclosureUrl: string, publishDate: string, feedUuid: string): string { + const normalizedInput = (title + enclosureUrl + publishDate).trim().toLowerCase(); + return uuidv5(normalizedInput, feedUuid); + } + ``` + + + + +Running the above example with the feed UUID `9b024349-ccf0-5f69-a609-6b82873eab3c` and the following metadata: + +- `title`: `"Episode 3"` +- `enclosureUrl`: `"https://example.com/episode_3.mp3"` +- `publishDate`: `"Fri, 21 Apr 2023 18:56:30 -0500"` + +Will yield `09ee3d1e-8a74-5581-b692-c7136a6210b0`. + From 96719d99dc5681e175a20ac48e508da89f652cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Wed, 13 Aug 2025 20:32:00 +0200 Subject: [PATCH 2/6] Remove named import --- src/content/docs/specs/namespaces.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/content/docs/specs/namespaces.mdx b/src/content/docs/specs/namespaces.mdx index d7cae395..7a810d3d 100644 --- a/src/content/docs/specs/namespaces.mdx +++ b/src/content/docs/specs/namespaces.mdx @@ -8,8 +8,6 @@ sidebar: order: 2 --- -import { Tabs, TabItem } from '@astrojs/starlight/components'; - The Open Podcast API uses [UUIDv5 values](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-5) as identifiers for entities. This value is known as a GUID (Globally Unique Identifier). Each GUID value MUST be calculated using the appropriate namespace and methodology to prevent duplication. Clients are responsible for parsing or calculating the GUID of feeds and episodes. The server stores this information, but does not calculate it. From 4a09a2188cf07f13605e1f14c189b2d8a870a2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Wed, 13 Aug 2025 20:39:00 +0200 Subject: [PATCH 3/6] Remove lowercasing for guid since it's a case-sensitive value --- src/content/docs/specs/namespaces.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/content/docs/specs/namespaces.mdx b/src/content/docs/specs/namespaces.mdx index 7a810d3d..16b72489 100644 --- a/src/content/docs/specs/namespaces.mdx +++ b/src/content/docs/specs/namespaces.mdx @@ -8,6 +8,7 @@ sidebar: order: 2 --- + The Open Podcast API uses [UUIDv5 values](https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-5) as identifiers for entities. This value is known as a GUID (Globally Unique Identifier). Each GUID value MUST be calculated using the appropriate namespace and methodology to prevent duplication. Clients are responsible for parsing or calculating the GUID of feeds and episodes. The server stores this information, but does not calculate it. @@ -82,7 +83,6 @@ To calculate the GUID for a podcast episode: 1. Calculate the feed UUID and store it as the namespace. 1. If a `guid` value is present in the episode section: 1. Strip leading and trailing whitespace from the `guid` value. - 1. Convert the `guid` to lowercase. 1. Create a new UUIDv5 value using the modified `guid` and the feed namespace. 1. If a `guid` value is NOT present in the episode section: 1. Concatenate the `title`, `enclosure_url`, and `publish_date` as a single string. @@ -104,7 +104,7 @@ Here is a simple example of how to calculate the episode GUID using the `guid` t public class UuidCalculator { public static UUID calculateEpisodeGuid(String guid, UUID feedUuid) { - final String sanitizedInput = guid.strip().toLowerCase(); + final String sanitizedInput = guid.strip(); final NameBasedGenerator generator = Generators.nameBasedGenerator(feedUuid); return generator.generate(sanitizedInput); } @@ -118,7 +118,7 @@ Here is a simple example of how to calculate the episode GUID using the `guid` t import uuid def calculate_episode_guid_from_guid(guid, feed_uuid): - normalized_input = guid.strip().lower() + normalized_input = guid.strip() namespace = uuid.UUID(feed_uuid) return uuid.uuid5(namespace, normalized_input) ``` @@ -130,7 +130,7 @@ Here is a simple example of how to calculate the episode GUID using the `guid` t import { v5 as uuidv5 } from "uuid"; function calculateEpisodeGuidFromGuid(guid: string, feedUuid: string): string { - const normalizedInput = guid.trim().toLowerCase(); + const normalizedInput = guid.trim(); return uuidv5(normalizedInput, feedUuid); } ``` From 837acac4437384cd52ffcae6a924cd27ec2646bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Wed, 13 Aug 2025 21:00:25 +0200 Subject: [PATCH 4/6] Add instructions for stripping the feed URL --- src/content/docs/specs/namespaces.mdx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/content/docs/specs/namespaces.mdx b/src/content/docs/specs/namespaces.mdx index 16b72489..55a89344 100644 --- a/src/content/docs/specs/namespaces.mdx +++ b/src/content/docs/specs/namespaces.mdx @@ -17,7 +17,9 @@ Clients are responsible for parsing or calculating the GUID of feeds and episode Feed GUID values MUST be created in accordance with the [Podcast Index's methodology](https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/tags/guid.md). If a feed already has a valid UUIDv5 `guid` tag, the client MUST pass this value to the server when submitting the feed. If the feed doesn't have a valid `guid` tag, the client MUST: -1. Generate a UUIDv5 `guid` value using the feed URL and the `podcast` namespace UUID: `ead4c236-bf58-58c6-a2c6-a6b28d128cb6`. +1. Generate a UUIDv5 `guid` value using: + - The feed URL with the protocol scheme and trailing slashes stripped off. + - The `podcast` namespace UUID: `ead4c236-bf58-58c6-a2c6-a6b28d128cb6`. 1. Pass the calculated value to the server when submitting a subscription. This process ensures that any feed not currently registered with the Podcast Index is identified by the exact same GUID it would receive if it were updated to the Podcasting 2.0 specification. @@ -39,8 +41,9 @@ Here is a simple example of how to calculate the GUID for a feed from its feed U static final NameBasedGenerator generator = Generators.nameBasedGenerator(podcastNamespace); public static UUID calculateFeedId(String feedUrl) { - final UUID feedUuid = UUID.fromString(feedUrl); - return generator.generate(feedUrl); + final String sanitizedFeedUrl = feedUrl.replaceFirst("^[a-zA-Z]+://", "").replaceAll("/+$", ""); + final UUID feedUuid = UUID.fromString(sanitizedFeedUrl); + return generator.generate(feedUuid); } } ``` @@ -50,10 +53,12 @@ Here is a simple example of how to calculate the GUID for a feed from its feed U ```py import uuid + import re def calculate_uuid(feed_url): PODCAST_NAMESPACE = uuid.UUID("ead4c236-bf58-58c6-a2c6-a6b28d128cb6") - return uuid.uuid5(PODCAST_NAMESPACE, feed_url) + sanitized_feed_url = re.sub(r'^[a-zA-Z]+://', '', url).rstrip('/') + return uuid.uuid5(PODCAST_NAMESPACE, sanitized_feed_url) ``` @@ -65,6 +70,7 @@ Here is a simple example of how to calculate the GUID for a feed from its feed U const PODCAST_NAMESPACE = "ead4c236-bf58-58c6-a2c6-a6b28d128cb6"; function calculateUuid(feedUrl: string): string { + const sanitizedFeedUrl = feedUrl.replace(/^[a-zA-Z]+:\/\//, "").replace(/\/+$/, ""); return uuidv5(feedUrl, PODCAST_NAMESPACE); } ``` From aef65ac5d59806749d54bd65f5ab91312cd611ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Wed, 13 Aug 2025 21:31:46 +0200 Subject: [PATCH 5/6] Add syncKey to tabs --- src/content/docs/specs/namespaces.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/docs/specs/namespaces.mdx b/src/content/docs/specs/namespaces.mdx index 55a89344..8093f2bd 100644 --- a/src/content/docs/specs/namespaces.mdx +++ b/src/content/docs/specs/namespaces.mdx @@ -28,7 +28,7 @@ This process ensures that any feed not currently registered with the Podcast Ind Here is a simple example of how to calculate the GUID for a feed from its feed URL. - + ```java @@ -99,7 +99,7 @@ To calculate the GUID for a podcast episode: Here is a simple example of how to calculate the episode GUID using the `guid` tag. - + ```java @@ -148,7 +148,7 @@ Running the above example with the GUID `"https://example.com/episode_3.mp3"` an Here is how to calculate the episode GUID using normalized metadata. - + ```java From f6397b568d1ed7978f40df0becb93051cc0c3e0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ciar=C3=A1n=20Ainsworth?= Date: Wed, 13 Aug 2025 21:34:05 +0200 Subject: [PATCH 6/6] Add icons --- src/content/docs/specs/namespaces.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/content/docs/specs/namespaces.mdx b/src/content/docs/specs/namespaces.mdx index 8093f2bd..6b4a85b6 100644 --- a/src/content/docs/specs/namespaces.mdx +++ b/src/content/docs/specs/namespaces.mdx @@ -29,7 +29,7 @@ This process ensures that any feed not currently registered with the Podcast Ind Here is a simple example of how to calculate the GUID for a feed from its feed URL. - + ```java import java.util.UUID; @@ -49,7 +49,7 @@ Here is a simple example of how to calculate the GUID for a feed from its feed U ``` - + ```py import uuid @@ -62,7 +62,7 @@ Here is a simple example of how to calculate the GUID for a feed from its feed U ``` - + ```ts import { v5 as uuidv5 } from "uuid"; @@ -100,7 +100,7 @@ To calculate the GUID for a podcast episode: Here is a simple example of how to calculate the episode GUID using the `guid` tag. - + ```java import java.util.UUID; @@ -118,7 +118,7 @@ Here is a simple example of how to calculate the episode GUID using the `guid` t ``` - + ```py import uuid @@ -130,7 +130,7 @@ Here is a simple example of how to calculate the episode GUID using the `guid` t ``` - + ```ts import { v5 as uuidv5 } from "uuid"; @@ -149,7 +149,7 @@ Running the above example with the GUID `"https://example.com/episode_3.mp3"` an Here is how to calculate the episode GUID using normalized metadata. - + ```java import java.util.UUID; @@ -167,7 +167,7 @@ Here is how to calculate the episode GUID using normalized metadata. ``` - + ```py import uuid @@ -179,7 +179,7 @@ Here is how to calculate the episode GUID using normalized metadata. ``` - + ```ts import { v5 as uuidv5 } from "uuid";