feat(podcast): Add Podcast 2.0 support and remove external feed dependency; implement internal RSS/Atom/JSON generators and update tests/README
This commit is contained in:
@@ -195,6 +195,36 @@ export class PodcastFeed extends Feed {
|
||||
throw new Error('iTunes type must be either "episodic" or "serial"');
|
||||
}
|
||||
|
||||
// Validate Podcast 2.0 fields
|
||||
// Validate podcast GUID (required for Podcast 2.0 compatibility)
|
||||
validation.validateRequiredFields(
|
||||
optionsArg,
|
||||
['podcastGuid'],
|
||||
'Podcast feed options'
|
||||
);
|
||||
|
||||
if (!optionsArg.podcastGuid || typeof optionsArg.podcastGuid !== 'string' || optionsArg.podcastGuid.trim() === '') {
|
||||
throw new Error('Podcast GUID is required and must be a non-empty string');
|
||||
}
|
||||
|
||||
// Validate podcast medium if provided
|
||||
if (optionsArg.podcastMedium) {
|
||||
const validMediums = ['podcast', 'music', 'video', 'film', 'audiobook', 'newsletter', 'blog'];
|
||||
if (!validMediums.includes(optionsArg.podcastMedium)) {
|
||||
throw new Error(`Podcast medium must be one of: ${validMediums.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate podcast locked and owner
|
||||
if (optionsArg.podcastLocked && !optionsArg.podcastLockOwner) {
|
||||
throw new Error('Podcast lock owner (email or contact) is required when podcast is locked');
|
||||
}
|
||||
|
||||
if (optionsArg.podcastLockOwner) {
|
||||
// Validate it's a valid email
|
||||
validation.validateEmail(optionsArg.podcastLockOwner);
|
||||
}
|
||||
|
||||
this.podcastOptions = optionsArg;
|
||||
}
|
||||
|
||||
@@ -361,6 +391,25 @@ export class PodcastFeed extends Feed {
|
||||
rss += `<itunes:type>${this.podcastOptions.itunesType}</itunes:type>\n`;
|
||||
}
|
||||
|
||||
// Podcast 2.0 namespace tags
|
||||
rss += `<podcast:guid>${this.escapeXml(this.podcastOptions.podcastGuid)}</podcast:guid>\n`;
|
||||
|
||||
if (this.podcastOptions.podcastMedium) {
|
||||
rss += `<podcast:medium>${this.podcastOptions.podcastMedium}</podcast:medium>\n`;
|
||||
} else {
|
||||
// Default to 'podcast' if not specified
|
||||
rss += `<podcast:medium>podcast</podcast:medium>\n`;
|
||||
}
|
||||
|
||||
if (this.podcastOptions.podcastLocked !== undefined) {
|
||||
const lockedValue = this.podcastOptions.podcastLocked ? 'yes' : 'no';
|
||||
if (this.podcastOptions.podcastLockOwner) {
|
||||
rss += `<podcast:locked owner="${this.escapeXml(this.podcastOptions.podcastLockOwner)}">${lockedValue}</podcast:locked>\n`;
|
||||
} else {
|
||||
rss += `<podcast:locked>${lockedValue}</podcast:locked>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Episodes
|
||||
for (const episode of this.episodes) {
|
||||
rss += '<item>\n';
|
||||
@@ -434,18 +483,4 @@ export class PodcastFeed extends Feed {
|
||||
|
||||
return rss;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes XML special characters
|
||||
* @param str - String to escape
|
||||
* @returns Escaped string
|
||||
*/
|
||||
private escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user