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:
parent
888273a1d7
commit
cb91056514
10 changed files with 212 additions and 103 deletions
|
@ -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`
|
||||||
},
|
},
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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 () {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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 & Film',
|
2: 'TV & Film',
|
||||||
3: 'Leisure',
|
3: 'Leisure',
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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[]>
|
||||||
|
|
||||||
|
|
|
@ -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'>>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue