import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as smartfeed from '../ts/index.js'; let testSmartFeed: smartfeed.Smartfeed; let advancedPodcast: smartfeed.PodcastFeed; tap.test('setup', async () => { testSmartFeed = new smartfeed.Smartfeed(); advancedPodcast = testSmartFeed.createPodcastFeed({ domain: 'advanced.example.com', title: 'Advanced Podcast Features', description: 'Testing advanced podcast features', category: 'Technology', company: 'Advanced Inc', companyEmail: 'advanced@example.com', companyDomain: 'https://example.com', itunesCategory: 'Technology', itunesAuthor: 'Tech Host', itunesOwner: { name: 'Tech Host', email: 'host@example.com' }, itunesImage: 'https://example.com/podcast.jpg', itunesExplicit: false, podcastGuid: 'test-guid-auto', }); }); tap.test('should add episode with persons (hosts and guests)', async () => { advancedPodcast.addEpisode({ title: 'Episode with Guests', authorName: 'Main Host', imageUrl: 'https://example.com/episode.jpg', timestamp: Date.now(), url: 'https://example.com/episode/guests', content: 'Episode featuring special guests', audioUrl: 'https://example.com/audio/guests.mp3', audioType: 'audio/mpeg', audioLength: 50000000, itunesDuration: 3600, persons: [ { name: 'Main Host', role: 'host', href: 'https://example.com/host', img: 'https://example.com/host.jpg', }, { name: 'Special Guest 1', role: 'guest', href: 'https://example.com/guest1', }, { name: 'Special Guest 2', role: 'guest', }, ], }); expect(advancedPodcast.episodes[0].persons).toBeArray(); expect(advancedPodcast.episodes[0].persons?.length).toEqual(3); expect(advancedPodcast.episodes[0].persons?.[0].role).toEqual('host'); expect(advancedPodcast.episodes[0].persons?.[1].role).toEqual('guest'); }); tap.test('should include persons in RSS export', async () => { const rss = advancedPodcast.exportPodcastRss(); expect(rss).toInclude('xmlns:podcast="https://podcastindex.org/namespace/1.0"'); expect(rss).toInclude(''); expect(rss).toInclude(''); expect(rss).toInclude('href="https://example.com/host"'); }); tap.test('should add episode with transcripts', async () => { const podcast = testSmartFeed.createPodcastFeed({ domain: 'transcript.example.com', title: 'Podcast with Transcripts', description: 'Testing transcript features', category: 'Education', company: 'Edu Inc', companyEmail: 'edu@example.com', companyDomain: 'https://example.com', itunesCategory: 'Education', itunesAuthor: 'Teacher', itunesOwner: { name: 'Teacher', email: 'teacher@example.com' }, itunesImage: 'https://example.com/edu.jpg', itunesExplicit: false, podcastGuid: 'test-guid-auto', }); podcast.addEpisode({ title: 'Episode with Transcript', authorName: 'Teacher', imageUrl: 'https://example.com/episode.jpg', timestamp: Date.now(), url: 'https://example.com/episode/transcript', content: 'Episode with multiple transcript formats', audioUrl: 'https://example.com/audio/episode.mp3', audioType: 'audio/mpeg', audioLength: 40000000, itunesDuration: 2400, transcripts: [ { url: 'https://example.com/transcripts/episode.txt', type: 'text/plain', language: 'en', }, { url: 'https://example.com/transcripts/episode.srt', type: 'application/srt', language: 'en', rel: 'captions', }, { url: 'https://example.com/transcripts/episode.vtt', type: 'text/vtt', language: 'en', }, ], }); expect(podcast.episodes[0].transcripts).toBeArray(); expect(podcast.episodes[0].transcripts?.length).toEqual(3); const rss = podcast.exportPodcastRss(); expect(rss).toInclude(' { const podcast = testSmartFeed.createPodcastFeed({ domain: 'funding.example.com', title: 'Podcast with Funding', description: 'Testing funding features', category: 'Arts', company: 'Arts Inc', companyEmail: 'arts@example.com', companyDomain: 'https://example.com', itunesCategory: 'Arts', itunesAuthor: 'Artist', itunesOwner: { name: 'Artist', email: 'artist@example.com' }, itunesImage: 'https://example.com/arts.jpg', itunesExplicit: false, podcastGuid: 'test-guid-auto', }); podcast.addEpisode({ title: 'Episode with Funding', authorName: 'Artist', imageUrl: 'https://example.com/episode.jpg', timestamp: Date.now(), url: 'https://example.com/episode/funding', content: 'Support this podcast', audioUrl: 'https://example.com/audio/episode.mp3', audioType: 'audio/mpeg', audioLength: 35000000, itunesDuration: 2100, funding: [ { url: 'https://patreon.com/example', message: 'Support us on Patreon', }, { url: 'https://buymeacoffee.com/example', message: 'Buy me a coffee', }, ], }); expect(podcast.episodes[0].funding).toBeArray(); expect(podcast.episodes[0].funding?.length).toEqual(2); const rss = podcast.exportPodcastRss(); expect(rss).toInclude('Support us on Patreon'); expect(rss).toInclude('Buy me a coffee'); }); tap.test('should add episode with all advanced features', async () => { const podcast = testSmartFeed.createPodcastFeed({ domain: 'complete.example.com', title: 'Complete Podcast', description: 'All features combined', category: 'Society & Culture', company: 'Complete Inc', companyEmail: 'complete@example.com', companyDomain: 'https://example.com', itunesCategory: 'Society & Culture', itunesAuthor: 'Host Name', itunesOwner: { name: 'Host Name', email: 'host@example.com' }, itunesImage: 'https://example.com/complete.jpg', itunesExplicit: false, podcastGuid: 'test-guid-auto', }); podcast.addEpisode({ title: 'Complete Feature Episode', authorName: 'Host Name', imageUrl: 'https://example.com/complete-episode.jpg', timestamp: Date.now(), url: 'https://example.com/episode/complete', content: 'An episode with all advanced features enabled', audioUrl: 'https://example.com/audio/complete.mp3', audioType: 'audio/mpeg', audioLength: 60000000, itunesDuration: 4500, itunesEpisode: 42, itunesSeason: 2, itunesEpisodeType: 'full', itunesSubtitle: 'A subtitle for this episode', itunesSummary: 'A longer summary describing this amazing episode in detail', persons: [ { name: 'Host Name', role: 'host', href: 'https://example.com/host' }, { name: 'Co-Host', role: 'co-host' }, { name: 'Guest Expert', role: 'guest' }, ], transcripts: [ { url: 'https://example.com/transcript.txt', type: 'text/plain', language: 'en' }, ], funding: [ { url: 'https://support.example.com', message: 'Support the show' }, ], }); expect(podcast.episodes.length).toEqual(1); const rss = podcast.exportPodcastRss(); // Verify iTunes tags expect(rss).toInclude('42'); expect(rss).toInclude('2'); expect(rss).toInclude('full'); expect(rss).toInclude('A subtitle for this episode'); expect(rss).toInclude('A longer summary describing this amazing episode in detail'); // Verify podcast namespace tags expect(rss).toInclude(' { const podcast = testSmartFeed.createPodcastFeed({ domain: 'explicit.example.com', title: 'Explicit Podcast', description: 'Testing explicit flag', category: 'Comedy', company: 'Comedy Inc', companyEmail: 'comedy@example.com', companyDomain: 'https://example.com', itunesCategory: 'Comedy', itunesAuthor: 'Comedian', itunesOwner: { name: 'Comedian', email: 'comedian@example.com' }, itunesImage: 'https://example.com/comedy.jpg', itunesExplicit: false, // Podcast is not explicit by default podcastGuid: 'test-guid-auto', }); podcast.addEpisode({ title: 'Clean Episode', authorName: 'Comedian', imageUrl: 'https://example.com/clean.jpg', timestamp: Date.now(), url: 'https://example.com/episode/clean', content: 'A clean episode', audioUrl: 'https://example.com/audio/clean.mp3', audioType: 'audio/mpeg', audioLength: 30000000, itunesDuration: 1800, itunesExplicit: false, podcastGuid: 'test-guid-auto', }); podcast.addEpisode({ title: 'Explicit Episode', authorName: 'Comedian', imageUrl: 'https://example.com/explicit.jpg', timestamp: Date.now() + 1, url: 'https://example.com/episode/explicit', content: 'An explicit episode', audioUrl: 'https://example.com/audio/explicit.mp3', audioType: 'audio/mpeg', audioLength: 30000000, itunesDuration: 1800, itunesExplicit: true, // This episode is explicit podcastGuid: 'test-guid-auto', }); const rss = podcast.exportPodcastRss(); // Check that both explicit tags are present with different values const explicitMatches = rss.match(/(true|false)<\/itunes:explicit>/g); expect(explicitMatches).toBeArray(); expect(rss).toInclude('false'); // Clean episode expect(rss).toInclude('true'); // Explicit episode }); tap.test('should validate transcript URL', async () => { const podcast = testSmartFeed.createPodcastFeed({ domain: 'test.com', title: 'Test', description: 'Test', category: 'Test', company: 'Test Inc', companyEmail: 'test@example.com', companyDomain: 'https://example.com', itunesCategory: 'Technology', itunesAuthor: 'Author', itunesOwner: { name: 'Owner', email: 'owner@example.com' }, itunesImage: 'https://example.com/image.jpg', itunesExplicit: false, podcastGuid: 'test-guid-auto', }); let errorThrown = false; try { podcast.addEpisode({ title: 'Episode 1', authorName: 'Author', imageUrl: 'https://example.com/episode.jpg', timestamp: Date.now(), url: 'https://example.com/episode/1', content: 'Content', audioUrl: 'https://example.com/audio.mp3', audioType: 'audio/mpeg', audioLength: 1000000, itunesDuration: 600, transcripts: [ { url: 'not-a-url', // Invalid! type: 'text/plain', }, ], }); } catch (error) { errorThrown = true; expect(error.message).toInclude('Invalid or relative URL'); } expect(errorThrown).toEqual(true); }); tap.test('should validate funding URL', async () => { const podcast = testSmartFeed.createPodcastFeed({ domain: 'test.com', title: 'Test', description: 'Test', category: 'Test', company: 'Test Inc', companyEmail: 'test@example.com', companyDomain: 'https://example.com', itunesCategory: 'Technology', itunesAuthor: 'Author', itunesOwner: { name: 'Owner', email: 'owner@example.com' }, itunesImage: 'https://example.com/image.jpg', itunesExplicit: false, podcastGuid: 'test-guid-auto', }); let errorThrown = false; try { podcast.addEpisode({ title: 'Episode 1', authorName: 'Author', imageUrl: 'https://example.com/episode.jpg', timestamp: Date.now(), url: 'https://example.com/episode/1', content: 'Content', audioUrl: 'https://example.com/audio.mp3', audioType: 'audio/mpeg', audioLength: 1000000, itunesDuration: 600, funding: [ { url: 'relative/path', // Invalid! message: 'Support us', }, ], }); } catch (error) { errorThrown = true; expect(error.message).toInclude('Invalid or relative URL'); } expect(errorThrown).toEqual(true); }); export default tap.start();