Improve podcast feed
This commit is contained in:
parent
29f55e9115
commit
1579d8ce1e
|
@ -116,7 +116,7 @@
|
|||
"@opentelemetry/sdk-trace-node": "^1.15.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.15.1",
|
||||
"@peertube/bittorrent-tracker-server": "^11.1.2",
|
||||
"@peertube/feed": "^5.2.0",
|
||||
"@peertube/feed": "^5.3.0",
|
||||
"@peertube/http-signature": "^1.7.0",
|
||||
"@smithy/node-http-handler": "^4.0.2",
|
||||
"@uploadx/core": "^6.0.0",
|
||||
|
|
|
@ -36,10 +36,11 @@ function buildDownloadFilesUrl (options: {
|
|||
videoUUID: string
|
||||
videoFiles: number[]
|
||||
videoFileToken?: string
|
||||
extension?: string
|
||||
}) {
|
||||
const { baseUrl, videoFiles, videoUUID, videoFileToken } = options
|
||||
const { baseUrl, videoFiles, videoUUID, videoFileToken, extension = '' } = options
|
||||
|
||||
let url = `${baseUrl}/download/videos/generate/${videoUUID}?`
|
||||
let url = `${baseUrl}/download/videos/generate/${videoUUID}${extension}?`
|
||||
url += videoFiles.map(f => 'videoFileIds=' + f).join('&')
|
||||
|
||||
if (videoFileToken) url += `&videoFileToken=${videoFileToken}`
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
stopFfmpeg,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expectStartWith } from '@tests/shared/checks.js'
|
||||
import * as chai from 'chai'
|
||||
import chaiJSONSChema from 'chai-json-schema'
|
||||
import chaiXML from 'chai-xml'
|
||||
|
@ -203,13 +204,17 @@ describe('Test syndication feeds', () => {
|
|||
|
||||
const enclosure = xmlDoc.rss.channel.item.enclosure
|
||||
expect(enclosure).to.exist
|
||||
expect(enclosure['@_url']).to.contain(`${serverHLSOnly.url}/download/videos/generate/`)
|
||||
expect(enclosure['@_type']).to.equal('audio/mp4')
|
||||
expectStartWith(enclosure['@_url'], `${serverHLSOnly.url}/download/videos/generate/`)
|
||||
expect(enclosure['@_url']).to.contain('.m4a')
|
||||
expect(enclosure['@_type']).to.equal('audio/x-m4a')
|
||||
|
||||
const res = await makeRawRequest({ url: enclosure['@_url'], expectedStatus: HttpStatusCode.OK_200 })
|
||||
expect(res.headers['content-type']).to.equal('audio/mp4')
|
||||
|
||||
const alternateEnclosures = xmlDoc.rss.channel.item['podcast:alternateEnclosure']
|
||||
expect(alternateEnclosures).to.be.an('array')
|
||||
|
||||
const audioEnclosure = alternateEnclosures.find(e => e['@_type'] === 'audio/mp4')
|
||||
const audioEnclosure = alternateEnclosures.find(e => e['@_type'] === 'audio/x-m4a')
|
||||
expect(audioEnclosure).to.exist
|
||||
expect(audioEnclosure['@_default']).to.equal(true)
|
||||
expect(audioEnclosure['podcast:source']['@_uri']).to.equal(enclosure['@_url'])
|
||||
|
@ -330,8 +335,14 @@ describe('Test syndication feeds', () => {
|
|||
|
||||
expect(channel['itunes:explicit']).to.equal(true)
|
||||
|
||||
expect(channel['itunes:author']).to.equal('PeerTube')
|
||||
|
||||
expect(channel['itunes:image']['@_href']).to.exist
|
||||
await makeRawRequest({ url: channel['itunes:image']['@_href'], expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
const item = xmlDoc.rss.channel.item
|
||||
|
||||
expect(item['itunes:duration']).to.equal(5)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ const downloadGenerateRateLimiter = buildRateLimiter({
|
|||
})
|
||||
|
||||
downloadRouter.use(
|
||||
DOWNLOAD_PATHS.GENERATE_VIDEO + ':id',
|
||||
[ DOWNLOAD_PATHS.GENERATE_VIDEO + ':id.m4a', DOWNLOAD_PATHS.GENERATE_VIDEO + ':id.mp4', DOWNLOAD_PATHS.GENERATE_VIDEO + ':id' ],
|
||||
downloadGenerateRateLimiter,
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(videosDownloadValidator),
|
||||
|
@ -251,8 +251,13 @@ async function downloadGeneratedVideoFile (req: express.Request, res: express.Re
|
|||
? '.m4a'
|
||||
: maxResolutionFile.extname
|
||||
|
||||
const downloadFilename = buildDownloadFilename({ video, extname })
|
||||
res.setHeader('Content-disposition', `attachment; filename="${encodeURI(downloadFilename)}`)
|
||||
// If there is the extension, we want to simulate a "raw file" and so not send the content disposition header
|
||||
if (!req.path.endsWith('.mp4') && !req.path.endsWith('.m4a')) {
|
||||
const downloadFilename = buildDownloadFilename({ video, extname })
|
||||
res.setHeader('Content-disposition', `attachment; filename="${encodeURI(downloadFilename)}`)
|
||||
}
|
||||
|
||||
res.type(extname)
|
||||
|
||||
await muxToMergeVideoFiles({ video, videoFiles, output: res })
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Feed } from '@peertube/feed'
|
||||
import { CustomTag, CustomXMLNS, LiveItemStatus } from '@peertube/feed/lib/typings/index.js'
|
||||
import { buildDownloadFilesUrl, getResolutionLabel, maxBy, sortObjectComparator } from '@peertube/peertube-core-utils'
|
||||
import { buildDownloadFilesUrl, getResolutionLabel, sortObjectComparator } from '@peertube/peertube-core-utils'
|
||||
import { ActorImageType, VideoFile, VideoInclude, VideoResolution, VideoState } from '@peertube/peertube-models'
|
||||
import { buildUUIDv5FromURL } from '@peertube/peertube-node-utils'
|
||||
import { buildNSFWFilter } from '@server/helpers/express-utils.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { InternalEventEmitter } from '@server/lib/internal-event-emitter.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { getVideoFileMimeType } from '@server/lib/video-file.js'
|
||||
|
@ -112,6 +113,7 @@ async function generateVideoPodcastFeed (req: express.Request, res: express.Resp
|
|||
: undefined,
|
||||
|
||||
person: [ { name, href: ownerLink, img: ownerImageUrl } ],
|
||||
author: { name: CONFIG.INSTANCE.NAME, link: WEBSERVER.URL },
|
||||
resourceType: 'videos',
|
||||
queryString: new URL(WEBSERVER.URL + req.url).search,
|
||||
medium: 'video',
|
||||
|
@ -153,23 +155,19 @@ async function generatePodcastItem (options: {
|
|||
{ video, liveItem }
|
||||
)
|
||||
|
||||
const account = video.VideoChannel.Account
|
||||
|
||||
const author = {
|
||||
name: account.getDisplayName(),
|
||||
href: account.getClientUrl()
|
||||
}
|
||||
|
||||
const commonAttributes = getCommonVideoFeedAttributes(video)
|
||||
const guid = liveItem
|
||||
? `${video.url}?publishedAt=${video.publishedAt.toISOString()}`
|
||||
: video.url
|
||||
|
||||
let personImage: string
|
||||
const account = video.VideoChannel.Account
|
||||
const person = {
|
||||
name: account.getDisplayName(),
|
||||
href: account.getClientUrl(),
|
||||
|
||||
if (account.Actor.hasImage(ActorImageType.AVATAR)) {
|
||||
const avatar = maxBy(account.Actor.Avatars, 'width')
|
||||
personImage = WEBSERVER.URL + avatar.getStaticPath()
|
||||
img: account.Actor.hasImage(ActorImageType.AVATAR)
|
||||
? WEBSERVER.URL + account.Actor.getMaxQualityImage(ActorImageType.AVATAR).getStaticPath()
|
||||
: undefined
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -178,14 +176,7 @@ async function generatePodcastItem (options: {
|
|||
|
||||
trackers: video.getTrackerUrls(),
|
||||
|
||||
author: [ author ],
|
||||
person: [
|
||||
{
|
||||
...author,
|
||||
|
||||
img: personImage
|
||||
}
|
||||
],
|
||||
person: [ person ],
|
||||
|
||||
media,
|
||||
|
||||
|
@ -197,6 +188,8 @@ async function generatePodcastItem (options: {
|
|||
}
|
||||
],
|
||||
|
||||
duration: video.duration,
|
||||
|
||||
customTags
|
||||
}
|
||||
}
|
||||
|
@ -271,7 +264,7 @@ function buildVODWebVideoFile (video: MVideo, videoFile: VideoFile) {
|
|||
}
|
||||
|
||||
return {
|
||||
type: getVideoFileMimeType(extname(videoFile.fileUrl), videoFile.resolution.id === VideoResolution.H_NOVIDEO),
|
||||
type: getAppleMimeType(extname(videoFile.fileUrl), videoFile.resolution.id === VideoResolution.H_NOVIDEO),
|
||||
title: videoFile.resolution.label,
|
||||
length: videoFile.size,
|
||||
bitrate: videoFile.size / video.duration * 8,
|
||||
|
@ -297,13 +290,20 @@ function buildVODStreamingPlaylists (video: MVideoFullLight) {
|
|||
}
|
||||
|
||||
return {
|
||||
type: getVideoFileMimeType(videoFile.extname, videoFile.resolution === VideoResolution.H_NOVIDEO),
|
||||
type: getAppleMimeType(videoFile.extname, videoFile.resolution === VideoResolution.H_NOVIDEO),
|
||||
title: getResolutionLabel(videoFile),
|
||||
length: files.reduce((p, f) => p + f.size, 0),
|
||||
language: video.language,
|
||||
sources: [
|
||||
{
|
||||
uri: buildDownloadFilesUrl({ baseUrl: WEBSERVER.URL, videoFiles: files.map(f => f.id), videoUUID: video.uuid })
|
||||
uri: buildDownloadFilesUrl({
|
||||
baseUrl: WEBSERVER.URL,
|
||||
videoFiles: files.map(f => f.id),
|
||||
videoUUID: video.uuid,
|
||||
extension: videoFile.hasVideo() && videoFile.hasAudio()
|
||||
? '.mp4'
|
||||
: '.m4a'
|
||||
})
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -373,3 +373,12 @@ function categoryToItunes (category: number) {
|
|||
|
||||
return itunesMap[category]
|
||||
}
|
||||
|
||||
// Guidelines: https://help.apple.com/itc/podcasts_connect/#/itcb54353390
|
||||
// "The type values for the supported file formats are: audio/x-m4a, audio/mpeg, video/quicktime, video/mp4, video/x-m4v, ..."
|
||||
function getAppleMimeType (extname: string, isAudio: boolean) {
|
||||
if (extname === '.mp4' && isAudio) return 'audio/x-m4a'
|
||||
if (extname === '.mp3') return 'audio/mpeg'
|
||||
|
||||
return getVideoFileMimeType(extname, isAudio)
|
||||
}
|
||||
|
|
|
@ -1854,10 +1854,10 @@
|
|||
bufferutil "^4.0.8"
|
||||
utf-8-validate "^6.0.4"
|
||||
|
||||
"@peertube/feed@^5.2.0":
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@peertube/feed/-/feed-5.2.0.tgz#67b355f33e06f0217ef42a8a27e3804deafc2a96"
|
||||
integrity sha512-YyF1Ud3kW23eQ+N7RSbHM9w629o0+P2E0A8oLRmBUVpJa5O0osmW4KAh+Z/YbfJslH7dJrHHLnIFMgzg3rYKEQ==
|
||||
"@peertube/feed@^5.3.0":
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@peertube/feed/-/feed-5.3.0.tgz#133f35aee89bec3af5505ef923f4271e1a75d283"
|
||||
integrity sha512-NyAf+bBcDhgmxxLHGDSvjedPUE6nEWP2fsntKM8dWCwLCii/niMVrft/fYy1EE4sW1ERLBsuj/jgn4SihAYErw==
|
||||
dependencies:
|
||||
xml-js "^1.6.11"
|
||||
|
||||
|
|
Loading…
Reference in New Issue