To test the rss in a podcast app:
https://podcasts.puhcho.me/npub1u4x0m5grq47ahu42tmdtjq9pak7gsgz68uw2lagu44stn6rt2v7qnzgmhv(this will run temporary and can be plugged in most podcast apps, change in code to your domain feed url). The add of rss feeds does not require apps login (fountain can use some npub).
In general self-host
https://github.com/SnowCait/nostr-rss and it can use nostter client or you can change the code to nostrudel, nostr band, coracle etc to pull rss of notes.
If the notes have a direct media link in content or tags for certain events (for flarepub, wavlake and stemstr), that turns any pub to podcaster and will be played in the podcast apps if enclosure url is added like :
```
import 'websocket-polyfill'; import RSS from 'rss'; import { error, type RequestHandler } from '@sveltejs/kit'; import { nip19 } from 'nostr-tools'; import { type Content } from 'nostr-typedef'; import { NostrFetcher } from 'nostr-fetch'; import { defaultRelays } from '$lib/config'; import axios from 'axios'; import { setMaxListeners } from 'events'; // Set global max listeners for EventEmitters setMaxListeners(20); interface LNURLResponse { callback: string; minWithdrawable?: number; maxWithdrawable?: number; } interface Metadata { picture?: string; lud16?: string; display_name?: string; name?: string; } export const GET: RequestHandler = async ({ params }) => { const npub = params.slug; if (npub === undefined) { throw error(404, 'Not Found'); } const { type, data } = nip19.decode(npub); if (type !== 'npub' && type !== 'nprofile') { throw error(404, 'Not Found'); } const pubkey: string = type === 'npub' ? data : data.pubkey; const relays: string[] = type === 'npub' ? [] : data.relays ?? []; if (relays.length === 0) { relays.push(...defaultRelays); } const fetcher = NostrFetcher.init(); const event = await fetcher.fetchLastEvent(relays, { kinds: [0], authors: [pubkey] }); let metadata: Metadata | undefined; let pictureUrl: string | undefined; let lud16Address: string | undefined; if (event !== undefined) { try { metadata = JSON.parse(event.content) as Metadata; pictureUrl = metadata.picture; // Extract picture URL lud16Address = metadata.lud16; // Extract LUD-16 address console.log('LUD-16 Address:', lud16Address); // Debugging line } catch (error) { console.warn('[failed to parse metadata]', error, event); } } // Fetch a larger number of events to include older ones const abortController = new AbortController(); const events = await fetcher.fetchLatestEvents( relays, { kinds: [1, 34235, 32123, 1808], authors: [pubkey] }, 100, { abortSignal: abortController.signal } ); console.log('Fetched Events:', events); // Debugging line const podcastName = metadata?.display_name || metadata?.name || npub; const feed = new RSS({ title: podcastName, site_url: `
https://nostter.app/${npub}`, feed_url: `
https://podcasts.puhcho.me/${npub}`, custom_namespaces: { itunes: '
http://www.itunes.com/dtds/podcast-1.0.dtd';, podcast: '
https://podcastindex.org/namespace/1.0'; }, custom_elements: [ { 'itunes:image': { _attr: { href: pictureUrl || '
https://i.giphy.com/o1YuwnczQIcc3ZGlbq.webp'; } } } ] }); for (const event of events) { console.log(event); let enclosureUrl: string | null = null; let itemUrl = `
https://nostter.app/${nip19.neventEncode({ id: event.id })}`; let itemTitle = podcastName; if (event.kind === 34235) { enclosureUrl = event.tags.find(tag => tag[0] === "url")?.[1] || null; if (enclosureUrl) { itemUrl = enclosureUrl; } } else if (event.kind === 32123) { try { const contentData = JSON.parse(event.content); enclosureUrl = contentData.enclosure || null; itemTitle = contentData.title || extractTitleFromContent(event.content, podcastName); itemUrl = contentData.link || itemUrl; } catch (error) { console.warn( '[failed to parse content for kind 32123]', error, event ); } } else if (event.kind === 1808) { enclosureUrl = event.tags.find(tag => tag[0] === "stream_url")?.[1] || null; itemTitle = extractTitleFromContent(event.content, podcastName); } else { const urlPattern = /(https?:\/\/[^\s"']+\.(mp3|mp4|wav|ogg|m4a|flac|aac|m3u8|webm|mov))/i; const urlMatch = event.content.match(urlPattern); enclosureUrl = urlMatch ? urlMatch[0] : null; itemTitle = extractTitleFromContent(event.content, podcastName); } // Set payment description to include LUD-16 address as a link label
const paymentDescription = lud16Address ? `<br><br>Support us with Lightning payments: <a href="lightning:${lud16Address}">${lud16Address}</a>` : "Payment method not found"; const descriptionWithLNURL= `${event.content || "No Description"}\n\n${paymentDescription}`; feed.item({ title: itemTitle, description: descriptionWithLNURL, date: new Date(event.created_at * 1000), url: itemUrl, enclosure: enclosureUrl ? { url: enclosureUrl, type: 'audio/mpeg' } : undefined, guid: `
https://nostter.app/${nip19.neventEncode({ id:event.id })}`, custom_elements:[{'itunes:image':{_attr:{href: pictureUrl||'
https://i.giphy.com/o1YuwnczQIcc3ZGlbq.webp';}}},] }); } return new Response(feed.xml(),{ headers:{'Content-Type':'application/xml; charset=UTF-8'} }); }; // Helper function to extract a potential title from the content avoiding URLs function extractTitleFromContent(content:string,fallbackTitle:string):string{ // Remove URLs and trim the result to use as a potential title const cleanedContent=content.replace(/https?:\/\/\S+/g,'').trim(); // Use the first sentence or fallback to the provided fallbackTitle if cleanedContent is empty return cleanedContent.split('. ')[0]||fallbackTitle; }
```
Published at
2024-09-29 09:00:28Event JSON
{
"id": "6a8eed31d0b0707f0adc9929a6b280690cee007e77b6b1725d91f75bfec9a0ff",
"pubkey": "e54cfdd103057ddbf2aa5edab900a1edbc88205a3f1caff51cad60b9e86b533c",
"created_at": 1727600428,
"kind": 0,
"tags": [
[
"alt",
"User profile for Podcasts, why not"
]
],
"content": "{\"name\":\"Podcasts, why not\",\"display_name\":\"Podcasts, why not\",\"picture\":\"https://i.giphy.com/o1YuwnczQIcc3ZGlbq.webp\",\"banner\":\"https://static.vecteezy.com/system/resources/previews/011/000/296/non_2x/abstract-music-soundwave-banner-design-free-vector.jpg\",\"nip05\":\"podcasts@nostriches.club\",\"lud16\":\"podcasts@nostriches.club\",\"about\":\"To test the rss in a podcast app: \\n\\nhttps://podcasts.puhcho.me/npub1u4x0m5grq47ahu42tmdtjq9pak7gsgz68uw2lagu44stn6rt2v7qnzgmhv\\n(this will run temporary and can be plugged in most podcast apps, change in code to your domain feed url). The add of rss feeds does not require apps login (fountain can use some npub).\\n\\nIn general self-host https://github.com/SnowCait/nostr-rss and it can use nostter client or you can change the code to nostrudel, nostr band, coracle etc to pull rss of notes.\\n\\nIf the notes have a direct media link in content or tags for certain events (for flarepub, wavlake and stemstr), that turns any pub to podcaster and will be played in the podcast apps if enclosure url is added like :\\n```\\nimport 'websocket-polyfill'; import RSS from 'rss'; import { error, type RequestHandler } from '@sveltejs/kit'; import { nip19 } from 'nostr-tools'; import { type Content } from 'nostr-typedef'; import { NostrFetcher } from 'nostr-fetch'; import { defaultRelays } from '$lib/config'; import axios from 'axios'; import { setMaxListeners } from 'events'; // Set global max listeners for EventEmitters setMaxListeners(20); interface LNURLResponse { callback: string; minWithdrawable?: number; maxWithdrawable?: number; } interface Metadata { picture?: string; lud16?: string; display_name?: string; name?: string; } export const GET: RequestHandler = async ({ params }) =\u003e { const npub = params.slug; if (npub === undefined) { throw error(404, 'Not Found'); } const { type, data } = nip19.decode(npub); if (type !== 'npub' \u0026\u0026 type !== 'nprofile') { throw error(404, 'Not Found'); } const pubkey: string = type === 'npub' ? data : data.pubkey; const relays: string[] = type === 'npub' ? [] : data.relays ?? []; if (relays.length === 0) { relays.push(...defaultRelays); } const fetcher = NostrFetcher.init(); const event = await fetcher.fetchLastEvent(relays, { kinds: [0], authors: [pubkey] }); let metadata: Metadata | undefined; let pictureUrl: string | undefined; let lud16Address: string | undefined; if (event !== undefined) { try { metadata = JSON.parse(event.content) as Metadata; pictureUrl = metadata.picture; // Extract picture URL lud16Address = metadata.lud16; // Extract LUD-16 address console.log('LUD-16 Address:', lud16Address); // Debugging line } catch (error) { console.warn('[failed to parse metadata]', error, event); } } // Fetch a larger number of events to include older ones const abortController = new AbortController(); const events = await fetcher.fetchLatestEvents( relays, { kinds: [1, 34235, 32123, 1808], authors: [pubkey] }, 100, { abortSignal: abortController.signal } ); console.log('Fetched Events:', events); // Debugging line const podcastName = metadata?.display_name || metadata?.name || npub; const feed = new RSS({ title: podcastName, site_url: `https://nostter.app/${npub}`, feed_url: `https://podcasts.puhcho.me/${npub}`, custom_namespaces: { itunes: 'http://www.itunes.com/dtds/podcast-1.0.dtd', podcast: 'https://podcastindex.org/namespace/1.0' }, custom_elements: [ { 'itunes:image': { _attr: { href: pictureUrl || 'https://i.giphy.com/o1YuwnczQIcc3ZGlbq.webp' } } } ] }); for (const event of events) { console.log(event); let enclosureUrl: string | null = null; let itemUrl = `https://nostter.app/${nip19.neventEncode({ id: event.id })}`; let itemTitle = podcastName; if (event.kind === 34235) { enclosureUrl = event.tags.find(tag =\u003e tag[0] === \\\"url\\\")?.[1] || null; if (enclosureUrl) { itemUrl = enclosureUrl; } } else if (event.kind === 32123) { try { const contentData = JSON.parse(event.content); enclosureUrl = contentData.enclosure || null; itemTitle = contentData.title || extractTitleFromContent(event.content, podcastName); itemUrl = contentData.link || itemUrl; } catch (error) { console.warn( '[failed to parse content for kind 32123]', error, event ); } } else if (event.kind === 1808) { enclosureUrl = event.tags.find(tag =\u003e tag[0] === \\\"stream_url\\\")?.[1] || null; itemTitle = extractTitleFromContent(event.content, podcastName); } else { const urlPattern = /(https?:\\\\/\\\\/[^\\\\s\\\"']+\\\\.(mp3|mp4|wav|ogg|m4a|flac|aac|m3u8|webm|mov))/i; const urlMatch = event.content.match(urlPattern); enclosureUrl = urlMatch ? urlMatch[0] : null; itemTitle = extractTitleFromContent(event.content, podcastName); } // Set payment description to include LUD-16 address as a link label\\n const paymentDescription = lud16Address ? `\u003cbr\u003e\u003cbr\u003eSupport us with Lightning payments: \u003ca href=\\\"lightning:${lud16Address}\\\"\u003e${lud16Address}\u003c/a\u003e` : \\\"Payment method not found\\\"; const descriptionWithLNURL= `${event.content || \\\"No Description\\\"}\\\\n\\\\n${paymentDescription}`; feed.item({ title: itemTitle, description: descriptionWithLNURL, date: new Date(event.created_at * 1000), url: itemUrl, enclosure: enclosureUrl ? { url: enclosureUrl, type: 'audio/mpeg' } : undefined, guid: `https://nostter.app/${nip19.neventEncode({ id:event.id })}`, custom_elements:[{'itunes:image':{_attr:{href: pictureUrl||'https://i.giphy.com/o1YuwnczQIcc3ZGlbq.webp'}}},] }); } return new Response(feed.xml(),{ headers:{'Content-Type':'application/xml; charset=UTF-8'} }); }; // Helper function to extract a potential title from the content avoiding URLs function extractTitleFromContent(content:string,fallbackTitle:string):string{ // Remove URLs and trim the result to use as a potential title const cleanedContent=content.replace(/https?:\\\\/\\\\/\\\\S+/g,'').trim(); // Use the first sentence or fallback to the provided fallbackTitle if cleanedContent is empty return cleanedContent.split('. ')[0]||fallbackTitle; } \\n```\"}",
"sig": "7865fade0d759ce94d6e7910ae6282d5b55301be012303512952f4f3ef17ebe878b1e53b69b3eeef9a53e4e36bfdcb8ce070048fcd197cfa4dc35379ed3dc9d7"
}