457 lines
17 KiB
Diff
457 lines
17 KiB
Diff
diff --git a/server/core/controllers/feeds/comment-feeds.ts b/server/core/controllers/feeds/comment-feeds.ts
|
|
index 105ae27..d4ae72d 100644
|
|
--- a/server/core/controllers/feeds/comment-feeds.ts
|
|
+++ b/server/core/controllers/feeds/comment-feeds.ts
|
|
@@ -56,7 +56,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
|
|
|
|
const { name, description, imageUrl, link } = await buildFeedMetadata({ video, account, videoChannel })
|
|
|
|
- const feed = initFeed({
|
|
+ const feed = await initFeed({
|
|
name,
|
|
description,
|
|
imageUrl,
|
|
diff --git a/server/core/controllers/feeds/shared/common-feed-utils.ts b/server/core/controllers/feeds/shared/common-feed-utils.ts
|
|
index 7196364..2076317 100644
|
|
--- a/server/core/controllers/feeds/shared/common-feed-utils.ts
|
|
+++ b/server/core/controllers/feeds/shared/common-feed-utils.ts
|
|
@@ -8,8 +8,9 @@ import { WEBSERVER } from '@server/initializers/constants.js'
|
|
import { UserModel } from '@server/models/user/user.js'
|
|
import { MAccountDefault, MChannelBannerAccountDefault, MUser, MVideoFullLight } from '@server/types/models/index.js'
|
|
import express from 'express'
|
|
+import { PluginModel } from '@server/models/server/plugin.js'
|
|
|
|
-export function initFeed (parameters: {
|
|
+export async function initFeed (parameters: {
|
|
name: string
|
|
description: string
|
|
imageUrl: string
|
|
@@ -33,6 +34,13 @@ export function initFeed (parameters: {
|
|
const webserverUrl = WEBSERVER.URL
|
|
const { name, description, link, imageUrl, isPodcast, resourceType, queryString, medium } = parameters
|
|
|
|
+ let generator = `Toraifōsu`; // ^.~
|
|
+
|
|
+ const peerhubPlugin = await PluginModel.loadByNpmName('peertube-plugin-peerhub');
|
|
+ if (peerhubPlugin && peerhubPlugin.enabled && !peerhubPlugin.uninstalled) {
|
|
+ generator = `Peerhub v${peerhubPlugin.version}`
|
|
+ }
|
|
+
|
|
return new Feed({
|
|
title: name,
|
|
description: mdToOneLinePlainText(description),
|
|
@@ -43,7 +51,7 @@ export function initFeed (parameters: {
|
|
favicon: webserverUrl + '/client/assets/images/favicon.png',
|
|
copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` +
|
|
` and potential licenses granted by each content's rightholder.`,
|
|
- generator: `PeerTube - ${webserverUrl}`,
|
|
+ generator,
|
|
medium: medium || 'video',
|
|
feedLinks: {
|
|
json: `${webserverUrl}/feeds/${resourceType}.json${queryString}`,
|
|
diff --git a/server/core/controllers/feeds/shared/video-feed-utils.ts b/server/core/controllers/feeds/shared/video-feed-utils.ts
|
|
index 260dac3..75902c9 100644
|
|
--- a/server/core/controllers/feeds/shared/video-feed-utils.ts
|
|
+++ b/server/core/controllers/feeds/shared/video-feed-utils.ts
|
|
@@ -1,5 +1,5 @@
|
|
import { VideoIncludeType } from '@peertube/peertube-models'
|
|
-import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown.js'
|
|
+import { toSafeHtml } from '@server/helpers/markdown.js'
|
|
import { CONFIG } from '@server/initializers/config.js'
|
|
import { WEBSERVER } from '@server/initializers/constants.js'
|
|
import { getServerActor } from '@server/models/application/application.js'
|
|
@@ -47,7 +47,9 @@ export function getCommonVideoFeedAttributes (video: VideoModel) {
|
|
return {
|
|
title: video.name,
|
|
link: localLink,
|
|
- description: mdToOneLinePlainText(video.getTruncatedDescription()),
|
|
+ description: toSafeHtml(video.description), // peerhub >= 9.7.22
|
|
+ // description: mdToOneLinePlainText(video.getTruncatedDescription()), // original peertube
|
|
+
|
|
content: toSafeHtml(video.description),
|
|
|
|
date: video.publishedAt,
|
|
diff --git a/server/core/controllers/feeds/video-feeds.ts b/server/core/controllers/feeds/video-feeds.ts
|
|
index 679d73f..ae7133a 100644
|
|
--- a/server/core/controllers/feeds/video-feeds.ts
|
|
+++ b/server/core/controllers/feeds/video-feeds.ts
|
|
@@ -19,6 +19,12 @@ import {
|
|
import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed, sendFeed } from './shared/index.js'
|
|
import { getVideoFileMimeType } from '@server/lib/video-file.js'
|
|
|
|
+import { PluginModel } from '@server/models/server/plugin.js'
|
|
+import { PluginType } from '@peertube/peertube-models'
|
|
+
|
|
+const rssDisabledStorageFieldName = 'htrssdis'; // localStorage
|
|
+
|
|
+
|
|
const videoFeedsRouter = express.Router()
|
|
|
|
const { middleware: cacheRouteMiddleware } = cacheRouteFactory({
|
|
@@ -63,7 +69,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
|
|
|
|
const { name, description, imageUrl, accountImageUrl, link, accountLink } = await buildFeedMetadata({ videoChannel, account })
|
|
|
|
- const feed = initFeed({
|
|
+ const feed = await initFeed({
|
|
name,
|
|
description,
|
|
link,
|
|
@@ -85,15 +91,32 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
|
|
|
|
addVideosToFeed(feed, data)
|
|
|
|
- // Now the feed generation is done, let's send it!
|
|
- return sendFeed(feed, req, res)
|
|
+ // Now the feed generation is done, let's send it, if not disabled (peerhub only)!
|
|
+
|
|
+ const peerhubPlugin = await PluginModel.loadByNpmName('peertube-plugin-peerhub');
|
|
+
|
|
+ if (videoChannel && peerhubPlugin && peerhubPlugin.enabled && !peerhubPlugin.uninstalled) {
|
|
+ const rssDisabled = await PluginModel.getData(
|
|
+ 'hive-tube', // use retro comp storage
|
|
+ PluginType.PLUGIN,
|
|
+ rssDisabledStorageFieldName + '-' + videoChannel.id
|
|
+ )
|
|
+
|
|
+ if (rssDisabled == 1) return res.redirect('/404');
|
|
+ else return res.redirect('/feeds/podcast/videos.xml?videoChannelId='+videoChannel.id); // redirect to richer podcast feed
|
|
+
|
|
+ } else {
|
|
+ // no hive tube, standard peertube or it is not channel rss but account one
|
|
+
|
|
+ return sendFeed(feed, req, res) // peertube native
|
|
+ }
|
|
}
|
|
|
|
async function generateVideoFeedForSubscriptions (req: express.Request, res: express.Response) {
|
|
const account = res.locals.account
|
|
const { name, description, imageUrl, link } = await buildFeedMetadata({ account })
|
|
|
|
- const feed = initFeed({
|
|
+ const feed = await initFeed({
|
|
name,
|
|
description,
|
|
link,
|
|
diff --git a/server/core/controllers/feeds/video-podcast-feeds.ts b/server/core/controllers/feeds/video-podcast-feeds.ts
|
|
index 7e0dca2..ab4c7ec 100644
|
|
--- a/server/core/controllers/feeds/video-podcast-feeds.ts
|
|
+++ b/server/core/controllers/feeds/video-podcast-feeds.ts
|
|
@@ -16,8 +16,17 @@ import { VideoCaptionModel } from '../../models/video/video-caption.js'
|
|
import { VideoModel } from '../../models/video/video.js'
|
|
import { buildFeedMetadata, getCommonVideoFeedAttributes, getVideosForFeeds, initFeed } from './shared/index.js'
|
|
|
|
+import { PluginModel } from '@server/models/server/plugin.js'
|
|
+import { PluginType } from '@peertube/peertube-models'
|
|
+
|
|
+import { RegisterServerSettingOptions} from '@peertube/peertube-models'
|
|
+
|
|
const videoPodcastFeedsRouter = express.Router()
|
|
|
|
+const sriFieldName = 'htsri'; // localStorage
|
|
+const rssDisabledStorageFieldName = 'htrssdis'; // localStorage
|
|
+
|
|
+
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const { middleware: podcastCacheRouteMiddleware, instance: podcastApiCache } = cacheRouteFactory({
|
|
@@ -82,7 +91,7 @@ async function generateVideoPodcastFeed (req: express.Request, res: express.Resp
|
|
'filter:feed.podcast.rss.create-custom-xmlns.result'
|
|
)
|
|
|
|
- const feed = initFeed({
|
|
+ const feed = await initFeed({
|
|
name,
|
|
description,
|
|
link,
|
|
@@ -103,8 +112,28 @@ async function generateVideoPodcastFeed (req: express.Request, res: express.Resp
|
|
|
|
await addVideosToPodcastFeed(feed, data)
|
|
|
|
- // Now the feed generation is done, let's send it!
|
|
- return res.send(feed.podcast()).end()
|
|
+ let rssDisabled = 0; // default
|
|
+ const peerhubPlugin = await PluginModel.loadByNpmName('peertube-plugin-peerhub');
|
|
+ if (peerhubPlugin && peerhubPlugin.enabled && !peerhubPlugin.uninstalled) {
|
|
+ // only if hive tube installed
|
|
+ rssDisabled = await PluginModel.getData(
|
|
+ 'hive-tube', // use retro comp storage
|
|
+ PluginType.PLUGIN,
|
|
+ rssDisabledStorageFieldName + '-' + videoChannel.id
|
|
+ )
|
|
+
|
|
+ }
|
|
+
|
|
+ let resultPodcast = feed.podcast();
|
|
+
|
|
+ // fix itunes explicit field format directly into string, cause it's simpler than changing imported lib
|
|
+ // https://github.com/Podcast-Standards-Project/PSP-1-Podcast-RSS-Specification?tab=readme-ov-file#channel-itunes-explicit
|
|
+ let fixedResult = resultPodcast.replace(/itunes:explicit>no</g, "itunes:explicit>false<").replace(/itunes:explicit>yes</g, "itunes:explicit>true<");
|
|
+
|
|
+
|
|
+ // Now the feed generation is done, let's send it, if not disabled (hive tube only)!
|
|
+ if (rssDisabled == 1) return res.redirect('/404');
|
|
+ else return res.send(fixedResult).end()
|
|
}
|
|
|
|
type PodcastMedia =
|
|
@@ -153,7 +182,36 @@ async function generatePodcastItem (options: {
|
|
const avatar = maxBy(account.Actor.Avatars, 'width')
|
|
personImage = WEBSERVER.URL + avatar.getStaticPath()
|
|
}
|
|
-
|
|
+
|
|
+ // default peertube
|
|
+ let accountUrl = account.getClientUrl();
|
|
+ let socialInteract: any[] = [
|
|
+ {
|
|
+ uri: video.url,
|
|
+ protocol: 'activitypub',
|
|
+ accountUrl
|
|
+ }
|
|
+ ]
|
|
+ try {
|
|
+ const peerhubResponse = await fetch(`${WEBSERVER.URL}/plugins/peerhub/router/jsci?v=${video.id}`);
|
|
+ const comments = await peerhubResponse.json();
|
|
+ if (comments && (comments.length > 0)) {
|
|
+ socialInteract = []; // let's us our own correct format
|
|
+ comments.forEach((comment) => {
|
|
+ socialInteract.push({
|
|
+ priority: comment.priority,
|
|
+ protocol: 'activitypub',
|
|
+ uri: `${video.url}#${comment.commentId}`,
|
|
+ accountId: `@${comment.accountId}`,
|
|
+ accountUrl: comment.accountUrl
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+
|
|
+ } catch(err) {
|
|
+ console.log(err); // safe
|
|
+ }
|
|
+
|
|
return {
|
|
guid,
|
|
...commonAttributes,
|
|
@@ -171,13 +229,7 @@ async function generatePodcastItem (options: {
|
|
|
|
media,
|
|
|
|
- socialInteract: [
|
|
- {
|
|
- uri: video.url,
|
|
- protocol: 'activitypub',
|
|
- accountUrl: account.getClientUrl()
|
|
- }
|
|
- ],
|
|
+ socialInteract,
|
|
|
|
customTags
|
|
}
|
|
@@ -206,13 +258,22 @@ async function addVODPodcastItem (options: {
|
|
.map(f => buildVODWebVideoFile(video, f))
|
|
.sort(sortObjectComparator('bitrate', 'desc'))
|
|
|
|
- const streamingPlaylistFiles = buildVODStreamingPlaylists(video)
|
|
+ const streamingPlaylistFiles = await buildVODStreamingPlaylists(video)
|
|
|
|
// Order matters here, the first media URI will be the "default"
|
|
// So web videos are default if enabled
|
|
const media = [ ...webVideos, ...streamingPlaylistFiles ]
|
|
|
|
- const videoCaptions = buildVODCaptions(video, captionsGroup[video.id])
|
|
+ let videoCaptions = buildVODCaptions(video, captionsGroup[video.id]) // original peertube
|
|
+ if (!videoCaptions || videoCaptions.length == 0) {
|
|
+ // always generate videoCaptions field even if not present, using video description
|
|
+ videoCaptions = [{
|
|
+ url: `${WEBSERVER.URL}/api/v1/videos/${video.id}/description`,
|
|
+ type: 'application/json',
|
|
+ language: video.language || 'en',
|
|
+ rel: 'captions'
|
|
+ }];
|
|
+ }
|
|
const item = await generatePodcastItem({ video, liveItem: false, media })
|
|
|
|
feed.addPodcastItem({ ...item, subTitle: videoCaptions })
|
|
@@ -262,20 +323,139 @@ function buildVODWebVideoFile (video: MVideo, videoFile: VideoFile) {
|
|
}
|
|
}
|
|
|
|
-function buildVODStreamingPlaylists (video: MVideoFullLight) {
|
|
+async function buildVODStreamingPlaylists (video: MVideoFullLight) {
|
|
const hls = video.getHLSPlaylist()
|
|
if (!hls) return []
|
|
|
|
- return [
|
|
- {
|
|
- type: 'application/x-mpegURL',
|
|
- title: 'HLS',
|
|
- sources: [
|
|
- { uri: hls.getMasterPlaylistUrl(video) }
|
|
- ],
|
|
- language: video.language
|
|
+ let calculatedUri = hls.getMasterPlaylistUrl(video); // original peertube one, safe default
|
|
+ let torrentUri = '';
|
|
+ let integrity = '';
|
|
+
|
|
+ let magnetHash = '';
|
|
+ let torrentFileName = '';
|
|
+ let size = 0;
|
|
+ let resolution = 0;
|
|
+ let extname = '';
|
|
+
|
|
+ let calculatedType = 'application/x-mpegURL'; // original peertube one, safe default
|
|
+ let length = 0; // safe default
|
|
+ if (hls.VideoFiles && hls.VideoFiles.length > 0) {
|
|
+ let selected = 0; // choose highest resolution one, default first one
|
|
+ for (let i=0; i<hls.VideoFiles.length; i++) {
|
|
+ if (hls.VideoFiles[i].resolution > resolution) {
|
|
+ resolution = hls.VideoFiles[i].resolution;
|
|
+ selected = i;
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (hls.VideoFiles[selected].resolution > 0) {
|
|
+ // video
|
|
+ calculatedType = 'video/mp4';
|
|
+ } else {
|
|
+ // audio
|
|
+ calculatedType = 'audio/mpeg';
|
|
+ }
|
|
+ length = hls.VideoFiles[selected].size;
|
|
+ calculatedUri = WEBSERVER.URL + `/static/streaming-playlists/hls/${video.uuid}/${hls.VideoFiles[selected].filename}`;
|
|
+ torrentUri = WEBSERVER.URL + `/download/torrents/${hls.VideoFiles[selected].torrentFilename}`;
|
|
+
|
|
+ integrity = await PluginModel.getData(
|
|
+ 'hive-tube', // use retro comp storage
|
|
+ PluginType.PLUGIN,
|
|
+ sriFieldName + '-' + video.id
|
|
+ )
|
|
+
|
|
+ magnetHash = hls.VideoFiles[selected].infoHash;
|
|
+ torrentFileName = hls.VideoFiles[selected].torrentFilename;
|
|
+ size = hls.VideoFiles[selected].size;
|
|
+ extname = hls.VideoFiles[selected].extname;
|
|
+
|
|
+ }
|
|
+
|
|
+ let sourcesUri = [
|
|
+ { uri: calculatedUri } // default source, standard peertube
|
|
+ ];
|
|
+
|
|
+ if (torrentUri) { // torrent source, if found
|
|
+ sourcesUri.push({
|
|
+ uri: torrentUri
|
|
+ })
|
|
+ }
|
|
+
|
|
+ // this will work even if peerhub is not installed, in that case properties will be empty
|
|
+ const additionalUriSettings = await PluginModel.getSettings(
|
|
+ 'peerhub', PluginType.PLUGIN, ['podcast-onion', 'podcast-loki', 'podcast-i2p'], [<RegisterServerSettingOptions><unknown>'podcast-onion', <RegisterServerSettingOptions><unknown>'podcast-loki', <RegisterServerSettingOptions><unknown>'podcast-i2p']
|
|
+ )
|
|
+
|
|
+ // 3 optional additional sources if peerhub installed and fields have values
|
|
+ if (additionalUriSettings) {
|
|
+
|
|
+ if (additionalUriSettings['podcast-onion']) {
|
|
+ let onionUri = (additionalUriSettings['podcast-onion']).toString().trim();
|
|
+ if (onionUri.substr(-1) != '/') onionUri += '/'; // auto add trailing slash if missing
|
|
+ sourcesUri.push({
|
|
+ uri: onionUri+`static/streaming-playlists/hls/${video.uuid}/${hls.VideoFiles[0].filename}`
|
|
+ })
|
|
}
|
|
- ]
|
|
+
|
|
+ if (additionalUriSettings['podcast-loki']) {
|
|
+ let lokiUri = (additionalUriSettings['podcast-loki']).toString().trim();
|
|
+ if (lokiUri.substr(-1) != '/') lokiUri += '/'; // auto add trailing slash if missing
|
|
+ sourcesUri.push({
|
|
+ uri: lokiUri+`static/streaming-playlists/hls/${video.uuid}/${hls.VideoFiles[0].filename}`
|
|
+ })
|
|
+ }
|
|
+
|
|
+ if (additionalUriSettings['podcast-i2p']) {
|
|
+ let itwopUri = (additionalUriSettings['podcast-i2p']).toString().trim();
|
|
+ if (itwopUri.substr(-1) != '/') itwopUri += '/'; // auto add trailing slash if missing
|
|
+ sourcesUri.push({
|
|
+ uri: itwopUri+`static/streaming-playlists/hls/${video.uuid}/${hls.VideoFiles[0].filename}`
|
|
+ })
|
|
+ }
|
|
+
|
|
+
|
|
+ }
|
|
+
|
|
+ const result: any = {
|
|
+ type: calculatedType,
|
|
+ title: 'HLS',
|
|
+ sources: sourcesUri,
|
|
+ language: video.language,
|
|
+ length
|
|
+ }
|
|
+
|
|
+ if (integrity) result.integrity = [{type:"sri", value:integrity}]; // add only if found
|
|
+
|
|
+ const results = [];
|
|
+
|
|
+ results.push(result);
|
|
+
|
|
+ // now let's add optional magnet link
|
|
+ if (magnetHash) {
|
|
+
|
|
+ const xs = `${WEBSERVER.URL}/lazy-static/torrents/${torrentFileName}`;
|
|
+ const name = video.name;
|
|
+ const announce = `${WEBSERVER.WS}://${WEBSERVER.HOSTNAME}:${WEBSERVER.PORT}/tracker/socket`;
|
|
+ const videoName = name.replace(/[/\\?%*:|"<>]/g, '-');
|
|
+
|
|
+ const infoName = `${videoName} ${resolution}p${extname}`;
|
|
+
|
|
+ let magnet = `magnet:?xs=${encodeURIComponent(xs)}&xt=urn:btih:${magnetHash}&dn=${encodeURIComponent(infoName)}&xl=${size}&tr=${encodeURIComponent(announce)}`;
|
|
+
|
|
+ const magnetResult = {
|
|
+ type:"application/x-bittorrent",
|
|
+ title: "MAGNET",
|
|
+ sources: [
|
|
+ { uri: magnet }
|
|
+ ]
|
|
+ // length: 1024 // we don't have it easily at this step, this is the torrent size, not file one, it is optional
|
|
+ }
|
|
+ results.push(magnetResult);
|
|
+
|
|
+ }
|
|
+
|
|
+ return results
|
|
}
|
|
|
|
function buildLiveStreamingPlaylists (video: MVideoFullLight) {
|
|
diff --git a/server/server.ts b/server/server.ts
|
|
index 603e35e..65a044f 100644
|
|
--- a/server/server.ts
|
|
+++ b/server/server.ts
|
|
@@ -11,6 +11,10 @@ import { CONFIG } from './core/initializers/config.js'
|
|
import { API_VERSION, WEBSERVER, loadLanguages } from './core/initializers/constants.js'
|
|
import { logger } from './core/helpers/logger.js'
|
|
|
|
+import { PluginModel } from '@server/models/server/plugin.js'
|
|
+import { PluginType } from '@peertube/peertube-models'
|
|
+
|
|
+
|
|
const missed = checkMissedConfig()
|
|
if (missed.length !== 0) {
|
|
logger.error('Your configuration files miss keys: ' + missed)
|
|
@@ -342,6 +346,16 @@ async function startApplication () {
|
|
// Before PeerTubeSocket init
|
|
PluginManager.Instance.registerWebSocketRouter()
|
|
|
|
+ // save patch version, to use from plugin, silent errors, failsafe
|
|
+ try {
|
|
+ await PluginModel.storeData(
|
|
+ 'hive-tube', // use retro comp storage
|
|
+ PluginType.PLUGIN,
|
|
+ 'peerhub-patch-version',
|
|
+ '6.3.0_peerhub-v3.patch'
|
|
+ )
|
|
+ } catch(e) {}
|
|
+
|
|
PeerTubeSocket.Instance.init(server)
|
|
VideoViewsManager.Instance.init()
|
|
|