1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-03 09:49:20 +02:00

Support <podcast:txt purpose="p20url"> element

This commit is contained in:
Chocobozzz 2025-03-04 13:49:01 +01:00
parent 888273a1d7
commit cb91056514
No known key found for this signature in database
GPG key ID: 583A612D890159BE
10 changed files with 212 additions and 103 deletions

View file

@ -10,10 +10,14 @@ export function getDefaultRSSFeeds (url: string, instanceName: string) {
] ]
} }
export function getChannelPodcastFeed (url: string, channel: { id: number }) {
return `${url}/feeds/podcast/videos.xml?videoChannelId=${channel.id}`
}
export function getChannelRSSFeeds (url: string, instanceName: string, channel: { name: string, id: number }) { export function getChannelRSSFeeds (url: string, instanceName: string, channel: { name: string, id: number }) {
return [ return [
{ {
url: `${url}/feeds/podcast/videos.xml?videoChannelId=${channel.id}`, url: getChannelPodcastFeed(url, channel),
// TODO: translate // TODO: translate
title: `${channel.name} podcast feed` title: `${channel.name} podcast feed`
}, },

View file

@ -5,35 +5,39 @@ import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
type FeedType = 'videos' | 'video-comments' | 'subscriptions' type FeedType = 'videos' | 'video-comments' | 'subscriptions'
export class FeedCommand extends AbstractCommand { export class FeedCommand extends AbstractCommand {
getXML (
getXML (options: OverrideCommandOptions & { options: OverrideCommandOptions & {
feed: FeedType feed: FeedType
ignoreCache: boolean ignoreCache: boolean
format?: string format?: string
}) { query?: { [id: string]: any }
const { feed, format, ignoreCache } = options }
) {
const { feed, format, ignoreCache, query = {} } = options
const path = '/feeds/' + feed + '.xml' const path = '/feeds/' + feed + '.xml'
const query: { [id: string]: string } = {} const internalQuery: { [id: string]: string } = {}
if (ignoreCache) query.v = buildUUID() if (ignoreCache) internalQuery.v = buildUUID()
if (format) query.format = format if (format) internalQuery.format = format
return this.getRequestText({ return this.getRequestText({
...options, ...options,
path, path,
query, query: { ...internalQuery, ...query },
accept: 'application/xml', accept: 'application/xml',
implicitToken: false, implicitToken: false,
defaultExpectedStatus: HttpStatusCode.OK_200 defaultExpectedStatus: HttpStatusCode.OK_200
}) })
} }
getPodcastXML (options: OverrideCommandOptions & { getPodcastXML (
ignoreCache: boolean options: OverrideCommandOptions & {
channelId: number ignoreCache: boolean
}) { channelId: number
}
) {
const { ignoreCache, channelId } = options const { ignoreCache, channelId } = options
const path = `/feeds/podcast/videos.xml` const path = `/feeds/podcast/videos.xml`
@ -53,11 +57,13 @@ export class FeedCommand extends AbstractCommand {
}) })
} }
getJSON (options: OverrideCommandOptions & { getJSON (
feed: FeedType options: OverrideCommandOptions & {
ignoreCache: boolean feed: FeedType
query?: { [ id: string ]: any } ignoreCache: boolean
}) { query?: { [id: string]: any }
}
) {
const { feed, query = {}, ignoreCache } = options const { feed, query = {}, ignoreCache } = options
const path = '/feeds/' + feed + '.json' const path = '/feeds/' + feed + '.json'

View file

@ -13,7 +13,6 @@ import { unwrapBody } from '../requests/index.js'
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js' import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
export class ChannelsCommand extends AbstractCommand { export class ChannelsCommand extends AbstractCommand {
list (options: OverrideCommandOptions & { list (options: OverrideCommandOptions & {
start?: number start?: number
count?: number count?: number
@ -32,14 +31,16 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
listByAccount (options: OverrideCommandOptions & { listByAccount (
accountName: string options: OverrideCommandOptions & {
start?: number accountName: string
count?: number start?: number
sort?: string count?: number
withStats?: boolean sort?: string
search?: string withStats?: boolean
}) { search?: string
}
) {
const { accountName, sort = 'createdAt' } = options const { accountName, sort = 'createdAt' } = options
const path = '/api/v1/accounts/' + accountName + '/video-channels' const path = '/api/v1/accounts/' + accountName + '/video-channels'
@ -53,9 +54,11 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
async create (options: OverrideCommandOptions & { async create (
attributes: Partial<VideoChannelCreate> options: OverrideCommandOptions & {
}) { attributes: Partial<VideoChannelCreate>
}
) {
const path = '/api/v1/video-channels/' const path = '/api/v1/video-channels/'
// Default attributes // Default attributes
@ -78,10 +81,12 @@ export class ChannelsCommand extends AbstractCommand {
return body.videoChannel return body.videoChannel
} }
update (options: OverrideCommandOptions & { update (
channelName: string options: OverrideCommandOptions & {
attributes: VideoChannelUpdate channelName: string
}) { attributes: VideoChannelUpdate
}
) {
const { channelName, attributes } = options const { channelName, attributes } = options
const path = '/api/v1/video-channels/' + channelName const path = '/api/v1/video-channels/' + channelName
@ -95,9 +100,11 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
delete (options: OverrideCommandOptions & { delete (
channelName: string options: OverrideCommandOptions & {
}) { channelName: string
}
) {
const path = '/api/v1/video-channels/' + options.channelName const path = '/api/v1/video-channels/' + options.channelName
return this.deleteRequest({ return this.deleteRequest({
@ -109,9 +116,13 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
get (options: OverrideCommandOptions & { // ---------------------------------------------------------------------------
channelName: string
}) { get (
options: OverrideCommandOptions & {
channelName: string
}
) {
const path = '/api/v1/video-channels/' + options.channelName const path = '/api/v1/video-channels/' + options.channelName
return this.getRequestBody<VideoChannel>({ return this.getRequestBody<VideoChannel>({
@ -123,11 +134,25 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
updateImage (options: OverrideCommandOptions & { async getIdOf (
fixture: string options: OverrideCommandOptions & {
channelName: string | number channelName: string
type: 'avatar' | 'banner' }
}) { ) {
const { id } = await this.get(options)
return id
}
// ---------------------------------------------------------------------------
updateImage (
options: OverrideCommandOptions & {
fixture: string
channelName: string | number
type: 'avatar' | 'banner'
}
) {
const { channelName, fixture, type } = options const { channelName, fixture, type } = options
const path = `/api/v1/video-channels/${channelName}/${type}/pick` const path = `/api/v1/video-channels/${channelName}/${type}/pick`
@ -144,10 +169,12 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
deleteImage (options: OverrideCommandOptions & { deleteImage (
channelName: string | number options: OverrideCommandOptions & {
type: 'avatar' | 'banner' channelName: string | number
}) { type: 'avatar' | 'banner'
}
) {
const { channelName, type } = options const { channelName, type } = options
const path = `/api/v1/video-channels/${channelName}/${type}` const path = `/api/v1/video-channels/${channelName}/${type}`
@ -161,13 +188,15 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
listFollowers (options: OverrideCommandOptions & { listFollowers (
channelName: string options: OverrideCommandOptions & {
start?: number channelName: string
count?: number start?: number
sort?: string count?: number
search?: string sort?: string
}) { search?: string
}
) {
const { channelName, start, count, sort, search } = options const { channelName, start, count, sort, search } = options
const path = '/api/v1/video-channels/' + channelName + '/followers' const path = '/api/v1/video-channels/' + channelName + '/followers'
@ -183,9 +212,11 @@ export class ChannelsCommand extends AbstractCommand {
}) })
} }
importVideos (options: OverrideCommandOptions & VideosImportInChannelCreate & { importVideos (
channelName: string options: OverrideCommandOptions & VideosImportInChannelCreate & {
}) { channelName: string
}
) {
const { channelName, externalChannelUrl, videoChannelSyncId } = options const { channelName, externalChannelUrl, videoChannelSyncId } = options
const path = `/api/v1/video-channels/${channelName}/import-videos` const path = `/api/v1/video-channels/${channelName}/import-videos`

View file

@ -34,7 +34,7 @@ describe('Test syndication feeds', () => {
let userAccessToken: string let userAccessToken: string
let rootAccountId: number let rootAccountId: number
let rootChannelId: number let rootChannelIdServer1: number
let userAccountId: number let userAccountId: number
let userChannelId: number let userChannelId: number
@ -53,7 +53,7 @@ describe('Test syndication feeds', () => {
await setAccessTokensToServers([ ...servers, serverHLSOnly ]) await setAccessTokensToServers([ ...servers, serverHLSOnly ])
await setDefaultChannelAvatar([ servers[0], serverHLSOnly ]) await setDefaultChannelAvatar([ servers[0], serverHLSOnly ])
await setDefaultVideoChannel(servers) await setDefaultVideoChannel([ ...servers, serverHLSOnly ])
await doubleFollow(servers[0], servers[1]) await doubleFollow(servers[0], servers[1])
await servers[0].config.enableLive({ allowReplay: false, transcoding: false }) await servers[0].config.enableLive({ allowReplay: false, transcoding: false })
@ -62,7 +62,7 @@ describe('Test syndication feeds', () => {
{ {
const user = await servers[0].users.getMyInfo() const user = await servers[0].users.getMyInfo()
rootAccountId = user.account.id rootAccountId = user.account.id
rootChannelId = user.videoChannels[0].id rootChannelIdServer1 = user.videoChannels[0].id
} }
{ {
@ -116,7 +116,6 @@ describe('Test syndication feeds', () => {
}) })
describe('All feed', function () { describe('All feed', function () {
it('Should be well formed XML (covers RSS 2.0 and ATOM 1.0 endpoints)', async function () { it('Should be well formed XML (covers RSS 2.0 and ATOM 1.0 endpoints)', async function () {
for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) { for (const feed of [ 'video-comments' as 'video-comments', 'videos' as 'videos' ]) {
const rss = await servers[0].feed.getXML({ feed, ignoreCache: true }) const rss = await servers[0].feed.getXML({ feed, ignoreCache: true })
@ -128,7 +127,7 @@ describe('Test syndication feeds', () => {
}) })
it('Should be well formed XML (covers Podcast endpoint)', async function () { it('Should be well formed XML (covers Podcast endpoint)', async function () {
const podcast = await servers[0].feed.getPodcastXML({ ignoreCache: true, channelId: rootChannelId }) const podcast = await servers[0].feed.getPodcastXML({ ignoreCache: true, channelId: rootChannelIdServer1 })
expect(podcast).xml.to.be.valid() expect(podcast).xml.to.be.valid()
}) })
@ -154,13 +153,11 @@ describe('Test syndication feeds', () => {
}) })
describe('Videos feed', function () { describe('Videos feed', function () {
describe('Podcast feed', function () { describe('Podcast feed', function () {
it('Should contain a valid podcast enclosures', async function () { it('Should contain a valid podcast enclosures', async function () {
// Since podcast feeds should only work on the server they originate on, // Since podcast feeds should only work on the server they originate on,
// only test the first server where the videos reside // only test the first server where the videos reside
const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: servers[0].store.channel.id })
expect(XMLValidator.validate(rss)).to.be.true expect(XMLValidator.validate(rss)).to.be.true
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
@ -192,7 +189,7 @@ describe('Test syndication feeds', () => {
}) })
it('Should contain a valid podcast enclosures with HLS only', async function () { it('Should contain a valid podcast enclosures with HLS only', async function () {
const rss = await serverHLSOnly.feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) const rss = await serverHLSOnly.feed.getPodcastXML({ ignoreCache: false, channelId: serverHLSOnly.store.channel.id })
expect(XMLValidator.validate(rss)).to.be.true expect(XMLValidator.validate(rss)).to.be.true
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
@ -230,7 +227,7 @@ describe('Test syndication feeds', () => {
}) })
it('Should contain a valid podcast:socialInteract', async function () { it('Should contain a valid podcast:socialInteract', async function () {
const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: servers[0].store.channel.id })
expect(XMLValidator.validate(rss)).to.be.true expect(XMLValidator.validate(rss)).to.be.true
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
@ -284,7 +281,7 @@ describe('Test syndication feeds', () => {
fields: { fields: {
name: 'live-0', name: 'live-0',
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
channelId: rootChannelId, channelId: rootChannelIdServer1,
permanentLive: false permanentLive: false
} }
}) })
@ -293,7 +290,7 @@ describe('Test syndication feeds', () => {
const ffmpeg = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveId, copyCodecs: true, fixtureName: 'video_short.mp4' }) const ffmpeg = await servers[0].live.sendRTMPStreamInVideo({ videoId: liveId, copyCodecs: true, fixtureName: 'video_short.mp4' })
await servers[0].live.waitUntilPublished({ videoId: liveId }) await servers[0].live.waitUntilPublished({ videoId: liveId })
const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: servers[0].store.channel.id })
expect(XMLValidator.validate(rss)).to.be.true expect(XMLValidator.validate(rss)).to.be.true
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
@ -321,7 +318,7 @@ describe('Test syndication feeds', () => {
}) })
it('Should have valid itunes metadata', async function () { it('Should have valid itunes metadata', async function () {
const rss = await serverHLSOnly.feed.getPodcastXML({ ignoreCache: false, channelId: rootChannelId }) const rss = await serverHLSOnly.feed.getPodcastXML({ ignoreCache: false, channelId: serverHLSOnly.store.channel.id })
expect(XMLValidator.validate(rss)).to.be.true expect(XMLValidator.validate(rss)).to.be.true
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false }) const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
@ -345,10 +342,49 @@ describe('Test syndication feeds', () => {
expect(item['itunes:duration']).to.equal(5) expect(item['itunes:duration']).to.equal(5)
}) })
it('Should have p20url podcast txt attribute with local podcast feed', async function () {
const rss = await servers[0].feed.getPodcastXML({ ignoreCache: false, channelId: servers[0].store.channel.id })
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
const xmlDoc = parser.parse(rss)
const podcastUrlEl = xmlDoc.rss.channel['podcast:txt']
expect(podcastUrlEl).to.exist
expect(podcastUrlEl['@_purpose']).to.equal('p20url')
expect(podcastUrlEl['#text']).to.equal(
servers[0].url + '/feeds/podcast/videos.xml?videoChannelId=' + servers[0].store.channel.id
)
})
it('Should have p20url podcast txt attribute with remote classic RSS feed with channel', async function () {
const videoChannelId = await servers[1].channels.getIdOf({ channelName: 'root_channel@' + servers[0].host })
const rss = await servers[1].feed.getXML({
feed: 'videos',
ignoreCache: true,
query: { videoChannelId }
})
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
const xmlDoc = parser.parse(rss)
const podcastUrlEl = xmlDoc.rss.channel['podcast:txt']
expect(podcastUrlEl).to.exist
expect(podcastUrlEl['@_purpose']).to.equal('p20url')
expect(podcastUrlEl['#text']).to.equal(servers[0].url + '/feeds/podcast/videos.xml?videoChannelId=' + videoChannelId)
})
it('Should not have p20url podcast txt attribute with classic RSS feed without channel', async function () {
const rss = await serverHLSOnly.feed.getXML({ feed: 'videos', ignoreCache: true })
const parser = new XMLParser({ parseAttributeValue: true, ignoreAttributes: false })
const xmlDoc = parser.parse(rss)
const podcastUrlEl = xmlDoc.rss.channel['podcast:txt']
expect(podcastUrlEl).to.not.exist
})
}) })
describe('JSON feed', function () { describe('JSON feed', function () {
it('Should contain a valid \'attachments\' object', async function () { it('Should contain a valid \'attachments\' object', async function () {
for (const server of servers) { for (const server of servers) {
const json = await server.feed.getJSON({ feed: 'videos', ignoreCache: true }) const json = await server.feed.getJSON({ feed: 'videos', ignoreCache: true })
@ -398,7 +434,7 @@ describe('Test syndication feeds', () => {
it('Should filter by video channel', async function () { it('Should filter by video channel', async function () {
{ {
const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: rootChannelId }, ignoreCache: true }) const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: rootChannelIdServer1 }, ignoreCache: true })
const jsonObj = JSON.parse(json) const jsonObj = JSON.parse(json)
expect(jsonObj.items.length).to.be.equal(1) expect(jsonObj.items.length).to.be.equal(1)
expect(jsonObj.items[0].title).to.equal('my super name for server 1') expect(jsonObj.items[0].title).to.equal('my super name for server 1')
@ -453,7 +489,7 @@ describe('Test syndication feeds', () => {
fields: { fields: {
name: 'live', name: 'live',
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
channelId: rootChannelId channelId: rootChannelIdServer1
} }
}) })
liveId = uuid liveId = uuid
@ -484,7 +520,7 @@ describe('Test syndication feeds', () => {
}) })
it('Should have the channel avatar as feed icon', async function () { it('Should have the channel avatar as feed icon', async function () {
const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: rootChannelId }, ignoreCache: true }) const json = await servers[0].feed.getJSON({ feed: 'videos', query: { videoChannelId: rootChannelIdServer1 }, ignoreCache: true })
const jsonObj = JSON.parse(json) const jsonObj = JSON.parse(json)
const imageUrl = jsonObj.icon const imageUrl = jsonObj.icon
@ -494,7 +530,6 @@ describe('Test syndication feeds', () => {
}) })
describe('XML feed', function () { describe('XML feed', function () {
it('Should correctly have video mime types feed with HLS only', async function () { it('Should correctly have video mime types feed with HLS only', async function () {
this.timeout(120000) this.timeout(120000)
@ -514,7 +549,6 @@ describe('Test syndication feeds', () => {
}) })
describe('Video comments feed', function () { describe('Video comments feed', function () {
it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted/password protected videos', async function () { it('Should contain valid comments (covers JSON feed 1.0 endpoint) and not from unlisted/password protected videos', async function () {
for (const server of servers) { for (const server of servers) {
const json = await server.feed.getJSON({ feed: 'video-comments', ignoreCache: true }) const json = await server.feed.getJSON({ feed: 'video-comments', ignoreCache: true })
@ -544,7 +578,11 @@ describe('Test syndication feeds', () => {
it('Should filter by videoChannelId/videoChannelName', async function () { it('Should filter by videoChannelId/videoChannelName', async function () {
{ {
const json = await servers[0].feed.getJSON({ feed: 'video-comments', query: { videoChannelId: rootChannelId }, ignoreCache: true }) const json = await servers[0].feed.getJSON({
feed: 'video-comments',
query: { videoChannelId: rootChannelIdServer1 },
ignoreCache: true
})
expect(JSON.parse(json).items.length).to.be.equal(2) expect(JSON.parse(json).items.length).to.be.equal(2)
} }
@ -744,7 +782,6 @@ describe('Test syndication feeds', () => {
const query = { accountId: userAccountId, token: userFeedToken } const query = { accountId: userAccountId, token: userFeedToken }
await servers[0].feed.getJSON({ feed: 'subscriptions', query, ignoreCache: true }) await servers[0].feed.getJSON({ feed: 'subscriptions', query, ignoreCache: true })
}) })
}) })
describe('Cache', function () { describe('Cache', function () {
@ -830,7 +867,6 @@ describe('Test syndication feeds', () => {
const res = await doPodcastRequest() const res = await doPodcastRequest()
expect(res.headers['x-api-cache-cached']).to.not.exist expect(res.headers['x-api-cache-cached']).to.not.exist
}) })
}) })
after(async function () { after(async function () {

View file

@ -1,12 +1,13 @@
import { getChannelPodcastFeed } from '@peertube/peertube-core-utils'
import { VideoIncludeType } from '@peertube/peertube-models' import { VideoIncludeType } from '@peertube/peertube-models'
import { mdToPlainText, toSafeHtml } from '@server/helpers/markdown.js' import { mdToPlainText, toSafeHtml } from '@server/helpers/markdown.js'
import { CONFIG } from '@server/initializers/config.js' import { CONFIG } from '@server/initializers/config.js'
import { WEBSERVER } from '@server/initializers/constants.js' import { REMOTE_SCHEME, WEBSERVER } from '@server/initializers/constants.js'
import { getServerActor } from '@server/models/application/application.js' import { getServerActor } from '@server/models/application/application.js'
import { getCategoryLabel } from '@server/models/video/formatter/index.js' import { getCategoryLabel } from '@server/models/video/formatter/index.js'
import { DisplayOnlyForFollowerOptions } from '@server/models/video/sql/video/index.js' import { DisplayOnlyForFollowerOptions } from '@server/models/video/sql/video/index.js'
import { VideoModel } from '@server/models/video/video.js' import { VideoModel } from '@server/models/video/video.js'
import { MThumbnail, MUserDefault } from '@server/types/models/index.js' import { MChannelHostOnly, MThumbnail, MUserDefault } from '@server/types/models/index.js'
export async function getVideosForFeeds (options: { export async function getVideosForFeeds (options: {
sort: string sort: string
@ -64,3 +65,16 @@ export function getCommonVideoFeedAttributes (video: VideoModel) {
})) }))
} }
} }
export function getPodcastFeedUrlCustomTag (videoChannel: MChannelHostOnly) {
const rootHost = videoChannel.Actor.getHost()
const originUrl = `${REMOTE_SCHEME.HTTP}://${rootHost}`
return {
name: 'podcast:txt',
attributes: {
purpose: 'p20url'
},
value: getChannelPodcastFeed(originUrl, videoChannel)
}
}

View file

@ -18,7 +18,14 @@ import {
videosSortValidator, videosSortValidator,
videoSubscriptionFeedsValidator videoSubscriptionFeedsValidator
} from '../../middlewares/index.js' } from '../../middlewares/index.js'
import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed, sendFeed } from './shared/index.js' import {
buildFeedMetadata,
getCommonVideoFeedAttributes,
getPodcastFeedUrlCustomTag,
getVideosForFeeds,
initFeed,
sendFeed
} from './shared/index.js'
const videoFeedsRouter = express.Router() const videoFeedsRouter = express.Router()
@ -28,7 +35,8 @@ const { middleware: cacheRouteMiddleware } = cacheRouteFactory({
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
videoFeedsRouter.get('/videos.:format', videoFeedsRouter.get(
'/videos.:format',
videosSortValidator, videosSortValidator,
setDefaultVideosSort, setDefaultVideosSort,
feedsFormatValidator, feedsFormatValidator,
@ -39,7 +47,8 @@ videoFeedsRouter.get('/videos.:format',
asyncMiddleware(generateVideoFeed) asyncMiddleware(generateVideoFeed)
) )
videoFeedsRouter.get('/subscriptions.:format', videoFeedsRouter.get(
'/subscriptions.:format',
videosSortValidator, videosSortValidator,
setDefaultVideosSort, setDefaultVideosSort,
feedsFormatValidator, feedsFormatValidator,
@ -72,7 +81,10 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
imageUrl: ownerImageUrl || imageUrl, imageUrl: ownerImageUrl || imageUrl,
author: { name, link: ownerLink }, author: { name, link: ownerLink },
resourceType: 'videos', resourceType: 'videos',
queryString: new URL(WEBSERVER.URL + req.url).search queryString: new URL(WEBSERVER.URL + req.url).search,
customTags: videoChannel
? [ getPodcastFeedUrlCustomTag(videoChannel) ]
: []
}) })
const data = await getVideosForFeeds({ const data = await getVideosForFeeds({

View file

@ -16,7 +16,7 @@ import { MIMETYPES, ROUTE_CACHE_LIFETIME, VIDEO_CATEGORIES, WEBSERVER } from '..
import { asyncMiddleware, setFeedPodcastContentType, videoFeedsPodcastValidator } from '../../middlewares/index.js' import { asyncMiddleware, setFeedPodcastContentType, videoFeedsPodcastValidator } from '../../middlewares/index.js'
import { VideoCaptionModel } from '../../models/video/video-caption.js' import { VideoCaptionModel } from '../../models/video/video-caption.js'
import { VideoModel } from '../../models/video/video.js' import { VideoModel } from '../../models/video/video.js'
import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared/index.js' import { buildFeedMetadata, getCommonVideoFeedAttributes, getPodcastFeedUrlCustomTag, getVideosForFeeds, initFeed } from './shared/index.js'
const videoPodcastFeedsRouter = express.Router() const videoPodcastFeedsRouter = express.Router()
@ -42,7 +42,8 @@ for (const event of ([ 'channel-updated', 'channel-deleted' ] as const)) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
videoPodcastFeedsRouter.get('/podcast/videos.xml', videoPodcastFeedsRouter.get(
'/podcast/videos.xml',
setFeedPodcastContentType, setFeedPodcastContentType,
videoFeedsPodcastSetCacheKey, videoFeedsPodcastSetCacheKey,
podcastCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS), podcastCacheRouteMiddleware(ROUTE_CACHE_LIFETIME.FEEDS),
@ -85,7 +86,7 @@ async function generateVideoPodcastFeed (req: express.Request, res: express.Resp
: false : false
const customTags: CustomTag[] = await Hooks.wrapObject( const customTags: CustomTag[] = await Hooks.wrapObject(
[], [ getPodcastFeedUrlCustomTag(videoChannel) ],
'filter:feed.podcast.channel.create-custom-tags.result', 'filter:feed.podcast.channel.create-custom-tags.result',
{ videoChannel } { videoChannel }
) )
@ -128,15 +129,15 @@ async function generateVideoPodcastFeed (req: express.Request, res: express.Resp
} }
type PodcastMedia = type PodcastMedia =
{ | {
type: string type: string
length: number length: number
bitrate: number bitrate: number
sources: { uri: string, contentType?: string }[] sources: { uri: string, contentType?: string }[]
title: string title: string
language?: string language?: string
} | }
{ | {
sources: { uri: string }[] sources: { uri: string }[]
type: string type: string
title: string title: string
@ -209,7 +210,7 @@ async function addVideosToPodcastFeed (feed: Feed, videos: VideoModel[]) {
async function addVODPodcastItem (options: { async function addVODPodcastItem (options: {
feed: Feed feed: Feed
video: VideoModel video: VideoModel
captionsGroup: { [ id: number ]: MVideoCaptionVideo[] } captionsGroup: { [id: number]: MVideoCaptionVideo[] }
}) { }) {
const { feed, video, captionsGroup } = options const { feed, video, captionsGroup } = options
@ -350,7 +351,7 @@ function buildVODCaptions (video: MVideo, videoCaptions: MVideoCaptionVideo[]) {
} }
function categoryToItunes (category: number) { function categoryToItunes (category: number) {
const itunesMap: { [ id in keyof typeof VIDEO_CATEGORIES ]: string } = { const itunesMap: { [id in keyof typeof VIDEO_CATEGORIES]: string } = {
1: 'Music', 1: 'Music',
2: 'TV &amp; Film', 2: 'TV &amp; Film',
3: 'Leisure', 3: 'Leisure',

View file

@ -664,6 +664,8 @@ export class ActorModel extends SequelizeModel<ActorModel> {
} }
getHost (this: MActorHostOnly) { getHost (this: MActorHostOnly) {
if (this.serverId && !this.Server) throw new Error('Server is not loaded in the object')
return this.Server ? this.Server.host : WEBSERVER.HOST return this.Server ? this.Server.host : WEBSERVER.HOST
} }

View file

@ -29,7 +29,10 @@ export type MActorLight = Omit<MActor, 'privateKey' | 'privateKey'>
// Some association attributes // Some association attributes
export type MActorHostOnly = Use<'Server', MServerHost> export type MActorHostOnly =
& Pick<ActorModel, 'serverId' | 'getHost'>
& Use<'Server', MServerHost>
export type MActorHost = export type MActorHost =
& MActorLight & MActorLight
& Use<'Server', MServerHost> & Use<'Server', MServerHost>
@ -157,7 +160,7 @@ export type MActorAPI = Omit<
export type MActorSummaryFormattable = export type MActorSummaryFormattable =
& FunctionProperties<MActor> & FunctionProperties<MActor>
& Pick<MActor, 'url' | 'preferredUsername'> & Pick<MActor, 'url' | 'preferredUsername' | 'serverId'>
& Use<'Server', MServerHost> & Use<'Server', MServerHost>
& Use<'Avatars', MActorImageFormattable[]> & Use<'Avatars', MActorImageFormattable[]>

View file

@ -31,7 +31,7 @@ export module UserNotificationIncludes {
& PickWith<VideoModel, 'VideoChannel', VideoChannelIncludeActor> & PickWith<VideoModel, 'VideoChannel', VideoChannelIncludeActor>
export type ActorInclude = export type ActorInclude =
& Pick<ActorModel, 'preferredUsername' | 'getHost'> & Pick<ActorModel, 'preferredUsername' | 'getHost' | 'serverId'>
& PickWith<ActorModel, 'Avatars', ActorImageInclude[]> & PickWith<ActorModel, 'Avatars', ActorImageInclude[]>
& PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
@ -78,13 +78,13 @@ export module UserNotificationIncludes {
& PickWith<VideoImportModel, 'Video', VideoInclude> & PickWith<VideoImportModel, 'Video', VideoInclude>
export type ActorFollower = export type ActorFollower =
& Pick<ActorModel, 'preferredUsername' | 'getHost'> & Pick<ActorModel, 'preferredUsername' | 'getHost' | 'serverId'>
& PickWith<ActorModel, 'Account', AccountInclude> & PickWith<ActorModel, 'Account', AccountInclude>
& PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>
& PickWithOpt<ActorModel, 'Avatars', ActorImageInclude[]> & PickWithOpt<ActorModel, 'Avatars', ActorImageInclude[]>
export type ActorFollowing = export type ActorFollowing =
& Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost'> & Pick<ActorModel, 'preferredUsername' | 'type' | 'getHost' | 'serverId'>
& PickWith<ActorModel, 'VideoChannel', VideoChannelInclude> & PickWith<ActorModel, 'VideoChannel', VideoChannelInclude>
& PickWith<ActorModel, 'Account', AccountInclude> & PickWith<ActorModel, 'Account', AccountInclude>
& PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>> & PickWith<ActorModel, 'Server', Pick<ServerModel, 'host'>>