aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--server/helpers/activitypub.ts6
-rw-r--r--server/helpers/custom-validators/activitypub/videos.ts24
-rw-r--r--server/helpers/webtorrent.ts7
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/database.ts4
-rw-r--r--server/initializers/migrations/0590-trackers.ts44
-rw-r--r--server/initializers/migrations/0595-remote-url.ts130
-rw-r--r--server/lib/activitypub/videos.ts59
-rw-r--r--server/lib/schedulers/videos-redundancy-scheduler.ts5
-rw-r--r--server/lib/thumbnail.ts16
-rw-r--r--server/models/server/tracker.ts73
-rw-r--r--server/models/server/video-tracker.ts30
-rw-r--r--server/models/video/thumbnail.ts5
-rw-r--r--server/models/video/video-caption.ts5
-rw-r--r--server/models/video/video-file.ts9
-rw-r--r--server/models/video/video-format-utils.ts49
-rw-r--r--server/models/video/video.ts54
-rw-r--r--server/types/models/server/tracker.ts7
-rw-r--r--server/types/models/video/video.ts4
-rw-r--r--shared/models/activitypub/objects/common-objects.ts14
-rw-r--r--shared/models/activitypub/objects/video-torrent-object.ts3
21 files changed, 457 insertions, 93 deletions
diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts
index 02a9d4026..08aef2908 100644
--- a/server/helpers/activitypub.ts
+++ b/server/helpers/activitypub.ts
@@ -201,10 +201,12 @@ function checkUrlsSameHost (url1: string, url2: string) {
201 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase() 201 return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
202} 202}
203 203
204function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string) { 204function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string, scheme?: string) {
205 if (!scheme) scheme = REMOTE_SCHEME.HTTP
206
205 const host = video.VideoChannel.Actor.Server.host 207 const host = video.VideoChannel.Actor.Server.host
206 208
207 return REMOTE_SCHEME.HTTP + '://' + host + path 209 return scheme + '://' + host + path
208} 210}
209 211
210// --------------------------------------------------------------------------- 212// ---------------------------------------------------------------------------
diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts
index a01429c83..a41d37810 100644
--- a/server/helpers/custom-validators/activitypub/videos.ts
+++ b/server/helpers/custom-validators/activitypub/videos.ts
@@ -1,4 +1,7 @@
1import validator from 'validator' 1import validator from 'validator'
2import { logger } from '@server/helpers/logger'
3import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
4import { VideoState } from '../../../../shared/models/videos'
2import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' 5import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
3import { peertubeTruncate } from '../../core-utils' 6import { peertubeTruncate } from '../../core-utils'
4import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' 7import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
@@ -11,9 +14,6 @@ import {
11 isVideoViewsValid 14 isVideoViewsValid
12} from '../videos' 15} from '../videos'
13import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' 16import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
14import { VideoState } from '../../../../shared/models/videos'
15import { logger } from '@server/helpers/logger'
16import { ActivityVideoFileMetadataObject } from '@shared/models'
17 17
18function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { 18function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
19 return isBaseActivityValid(activity, 'Update') && 19 return isBaseActivityValid(activity, 'Update') &&
@@ -84,6 +84,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) {
84 84
85function isRemoteVideoUrlValid (url: any) { 85function isRemoteVideoUrlValid (url: any) {
86 return url.type === 'Link' && 86 return url.type === 'Link' &&
87 // Video file link
87 ( 88 (
88 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) && 89 ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) &&
89 isActivityPubUrlValid(url.href) && 90 isActivityPubUrlValid(url.href) &&
@@ -91,31 +92,41 @@ function isRemoteVideoUrlValid (url: any) {
91 validator.isInt(url.size + '', { min: 0 }) && 92 validator.isInt(url.size + '', { min: 0 }) &&
92 (!url.fps || validator.isInt(url.fps + '', { min: -1 })) 93 (!url.fps || validator.isInt(url.fps + '', { min: -1 }))
93 ) || 94 ) ||
95 // Torrent link
94 ( 96 (
95 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) && 97 ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) &&
96 isActivityPubUrlValid(url.href) && 98 isActivityPubUrlValid(url.href) &&
97 validator.isInt(url.height + '', { min: 0 }) 99 validator.isInt(url.height + '', { min: 0 })
98 ) || 100 ) ||
101 // Magnet link
99 ( 102 (
100 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) && 103 ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) &&
101 validator.isLength(url.href, { min: 5 }) && 104 validator.isLength(url.href, { min: 5 }) &&
102 validator.isInt(url.height + '', { min: 0 }) 105 validator.isInt(url.height + '', { min: 0 })
103 ) || 106 ) ||
107 // HLS playlist link
104 ( 108 (
105 (url.mediaType || url.mimeType) === 'application/x-mpegURL' && 109 (url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
106 isActivityPubUrlValid(url.href) && 110 isActivityPubUrlValid(url.href) &&
107 isArray(url.tag) 111 isArray(url.tag)
108 ) || 112 ) ||
109 isAPVideoFileMetadataObject(url) 113 isAPVideoTrackerUrlObject(url) ||
114 isAPVideoFileUrlMetadataObject(url)
110} 115}
111 116
112function isAPVideoFileMetadataObject (url: any): url is ActivityVideoFileMetadataObject { 117function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
113 return url && 118 return url &&
114 url.type === 'Link' && 119 url.type === 'Link' &&
115 url.mediaType === 'application/json' && 120 url.mediaType === 'application/json' &&
116 isArray(url.rel) && url.rel.includes('metadata') 121 isArray(url.rel) && url.rel.includes('metadata')
117} 122}
118 123
124function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
125 return isArray(url.rel) &&
126 url.rel.includes('tracker') &&
127 isActivityPubUrlValid(url.href)
128}
129
119// --------------------------------------------------------------------------- 130// ---------------------------------------------------------------------------
120 131
121export { 132export {
@@ -123,7 +134,8 @@ export {
123 isRemoteStringIdentifierValid, 134 isRemoteStringIdentifierValid,
124 sanitizeAndCheckVideoTorrentObject, 135 sanitizeAndCheckVideoTorrentObject,
125 isRemoteVideoUrlValid, 136 isRemoteVideoUrlValid,
126 isAPVideoFileMetadataObject 137 isAPVideoFileUrlMetadataObject,
138 isAPVideoTrackerUrlObject
127} 139}
128 140
129// --------------------------------------------------------------------------- 141// ---------------------------------------------------------------------------
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts
index 73418aa0a..4e08c27c6 100644
--- a/server/helpers/webtorrent.ts
+++ b/server/helpers/webtorrent.ts
@@ -107,16 +107,13 @@ async function createTorrentAndSetInfoHash (
107 videoFile.torrentFilename = torrentFilename 107 videoFile.torrentFilename = torrentFilename
108} 108}
109 109
110// FIXME: merge/refactor videoOrPlaylist and video arguments
111function generateMagnetUri ( 110function generateMagnetUri (
112 videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
113 video: MVideoWithHost, 111 video: MVideoWithHost,
114 videoFile: MVideoFileRedundanciesOpt, 112 videoFile: MVideoFileRedundanciesOpt,
115 baseUrlHttp: string, 113 trackerUrls: string[]
116 baseUrlWs: string
117) { 114) {
118 const xs = videoFile.getTorrentUrl() 115 const xs = videoFile.getTorrentUrl()
119 const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs) 116 const announce = trackerUrls
120 let urlList = [ videoFile.getFileUrl(video) ] 117 let urlList = [ videoFile.getFileUrl(video) ]
121 118
122 const redundancies = videoFile.RedundancyVideos 119 const redundancies = videoFile.RedundancyVideos
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index c4c7ffdac..fbedc2164 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 585 27const LAST_MIGRATION_VERSION = 595
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/database.ts b/server/initializers/database.ts
index 61768234f..1f2b6d521 100644
--- a/server/initializers/database.ts
+++ b/server/initializers/database.ts
@@ -1,3 +1,5 @@
1import { TrackerModel } from '@server/models/server/tracker'
2import { VideoTrackerModel } from '@server/models/server/video-tracker'
1import { QueryTypes, Transaction } from 'sequelize' 3import { QueryTypes, Transaction } from 'sequelize'
2import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' 4import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
3import { isTestInstance } from '../helpers/core-utils' 5import { isTestInstance } from '../helpers/core-utils'
@@ -128,6 +130,8 @@ async function initDatabaseModels (silent: boolean) {
128 VideoPlaylistModel, 130 VideoPlaylistModel,
129 VideoPlaylistElementModel, 131 VideoPlaylistElementModel,
130 ThumbnailModel, 132 ThumbnailModel,
133 TrackerModel,
134 VideoTrackerModel,
131 PluginModel 135 PluginModel
132 ]) 136 ])
133 137
diff --git a/server/initializers/migrations/0590-trackers.ts b/server/initializers/migrations/0590-trackers.ts
new file mode 100644
index 000000000..47b9022a3
--- /dev/null
+++ b/server/initializers/migrations/0590-trackers.ts
@@ -0,0 +1,44 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = `CREATE TABLE IF NOT EXISTS "tracker" (
11 "id" serial,
12 "url" varchar(255) NOT NULL,
13 "createdAt" timestamp WITH time zone NOT NULL,
14 "updatedAt" timestamp WITH time zone NOT NULL,
15 PRIMARY KEY ("id")
16 );`
17
18 await utils.sequelize.query(query)
19 }
20
21 {
22 const query = `CREATE TABLE IF NOT EXISTS "videoTracker" (
23 "videoId" integer REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
24 "trackerId" integer REFERENCES "tracker" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
25 "createdAt" timestamp WITH time zone NOT NULL,
26 "updatedAt" timestamp WITH time zone NOT NULL,
27 UNIQUE ("videoId", "trackerId"),
28 PRIMARY KEY ("videoId", "trackerId")
29 );`
30
31 await utils.sequelize.query(query)
32 }
33
34 await utils.sequelize.query(`CREATE UNIQUE INDEX "tracker_url" ON "tracker" ("url")`)
35}
36
37function down (options) {
38 throw new Error('Not implemented.')
39}
40
41export {
42 up,
43 down
44}
diff --git a/server/initializers/migrations/0595-remote-url.ts b/server/initializers/migrations/0595-remote-url.ts
new file mode 100644
index 000000000..85b367555
--- /dev/null
+++ b/server/initializers/migrations/0595-remote-url.ts
@@ -0,0 +1,130 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9
10 // Torrent and file URLs
11 {
12 const fromQueryWebtorrent = `SELECT 'https://' || server.host AS "serverUrl", '/static/webseed/' AS "filePath", "videoFile".id ` +
13 `FROM video ` +
14 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
15 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
16 `INNER JOIN server ON server.id = actor."serverId" ` +
17 `INNER JOIN "videoFile" ON "videoFile"."videoId" = video.id ` +
18 `WHERE video.remote IS TRUE`
19
20 const fromQueryHLS = `SELECT 'https://' || server.host AS "serverUrl", ` +
21 `'/static/streaming-playlists/hls/' || video.uuid || '/' AS "filePath", "videoFile".id ` +
22 `FROM video ` +
23 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
24 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
25 `INNER JOIN server ON server.id = actor."serverId" ` +
26 `INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."videoId" = video.id ` +
27 `INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ` +
28 `WHERE video.remote IS TRUE`
29
30 for (const fromQuery of [ fromQueryWebtorrent, fromQueryHLS ]) {
31 const query = `UPDATE "videoFile" ` +
32 `SET "torrentUrl" = t."serverUrl" || '/static/torrents/' || "videoFile"."torrentFilename", ` +
33 `"fileUrl" = t."serverUrl" || t."filePath" || "videoFile"."filename" ` +
34 `FROM (${fromQuery}) AS t WHERE t.id = "videoFile"."id" AND "videoFile"."fileUrl" IS NULL`
35
36 await utils.sequelize.query(query)
37 }
38 }
39
40 // Caption URLs
41 {
42 const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "videoCaption".id ` +
43 `FROM video ` +
44 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
45 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
46 `INNER JOIN server ON server.id = actor."serverId" ` +
47 `INNER JOIN "videoCaption" ON "videoCaption"."videoId" = video.id ` +
48 `WHERE video.remote IS TRUE`
49
50 const query = `UPDATE "videoCaption" ` +
51 `SET "fileUrl" = t."serverUrl" || '/lazy-static/video-captions/' || t.uuid || '-' || "videoCaption"."language" || '.vtt' ` +
52 `FROM (${fromQuery}) AS t WHERE t.id = "videoCaption"."id" AND "videoCaption"."fileUrl" IS NULL`
53
54 await utils.sequelize.query(query)
55 }
56
57 // Thumbnail URLs
58 {
59 const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "thumbnail".id ` +
60 `FROM video ` +
61 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
62 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
63 `INNER JOIN server ON server.id = actor."serverId" ` +
64 `INNER JOIN "thumbnail" ON "thumbnail"."videoId" = video.id ` +
65 `WHERE video.remote IS TRUE`
66
67 // Thumbnails
68 {
69 const query = `UPDATE "thumbnail" ` +
70 `SET "fileUrl" = t."serverUrl" || '/static/thumbnails/' || t.uuid || '.jpg' ` +
71 `FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 1`
72
73 await utils.sequelize.query(query)
74 }
75
76 {
77 // Previews
78 const query = `UPDATE "thumbnail" ` +
79 `SET "fileUrl" = t."serverUrl" || '/lazy-static/previews/' || t.uuid || '.jpg' ` +
80 `FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 2`
81
82 await utils.sequelize.query(query)
83 }
84 }
85
86 // Trackers
87 {
88 const trackerUrls = [
89 `'https://' || server.host || '/tracker/announce'`,
90 `'wss://' || server.host || '/tracker/socket'`
91 ]
92
93 for (const trackerUrl of trackerUrls) {
94 {
95 const query = `INSERT INTO "tracker" ("url", "createdAt", "updatedAt") ` +
96 `SELECT ${trackerUrl} AS "url", NOW(), NOW() ` +
97 `FROM video ` +
98 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
99 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
100 `INNER JOIN server ON server.id = actor."serverId" ` +
101 `WHERE video.remote IS TRUE ` +
102 `ON CONFLICT DO NOTHING`
103
104 await utils.sequelize.query(query)
105 }
106
107 {
108 const query = `INSERT INTO "videoTracker" ("videoId", "trackerId", "createdAt", "updatedAt") ` +
109 `SELECT video.id, (SELECT tracker.id FROM tracker WHERE url = ${trackerUrl}) AS "trackerId", NOW(), NOW()` +
110 `FROM video ` +
111 `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
112 `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
113 `INNER JOIN server ON server.id = actor."serverId" ` +
114 `WHERE video.remote IS TRUE`
115
116 await utils.sequelize.query(query)
117 }
118 }
119 }
120
121}
122
123function down (options) {
124 throw new Error('Not implemented.')
125}
126
127export {
128 up,
129 down
130}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index a5f6537eb..66330a964 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -3,7 +3,8 @@ import { maxBy, minBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import { basename, join } from 'path' 4import { basename, join } from 'path'
5import * as request from 'request' 5import * as request from 'request'
6import * as sequelize from 'sequelize' 6import { Transaction } from 'sequelize/types'
7import { TrackerModel } from '@server/models/server/tracker'
7import { VideoLiveModel } from '@server/models/video/video-live' 8import { VideoLiveModel } from '@server/models/video/video-live'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
9import { 10import {
@@ -16,12 +17,16 @@ import {
16 ActivityUrlObject, 17 ActivityUrlObject,
17 ActivityVideoUrlObject 18 ActivityVideoUrlObject
18} from '../../../shared/index' 19} from '../../../shared/index'
19import { ActivityIconObject, VideoObject } from '../../../shared/models/activitypub/objects' 20import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
20import { VideoPrivacy } from '../../../shared/models/videos' 21import { VideoPrivacy } from '../../../shared/models/videos'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 22import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 23import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
23import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub' 24import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
24import { isAPVideoFileMetadataObject, sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' 25import {
26 isAPVideoFileUrlMetadataObject,
27 isAPVideoTrackerUrlObject,
28 sanitizeAndCheckVideoTorrentObject
29} from '../../helpers/custom-validators/activitypub/videos'
25import { isArray } from '../../helpers/custom-validators/misc' 30import { isArray } from '../../helpers/custom-validators/misc'
26import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 31import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
27import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' 32import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
@@ -83,7 +88,7 @@ import { addVideoShares, shareVideoByServerAndChannel } from './share'
83import { addVideoComments } from './video-comments' 88import { addVideoComments } from './video-comments'
84import { createRates } from './video-rates' 89import { createRates } from './video-rates'
85 90
86async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { 91async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: Transaction) {
87 const video = videoArg as MVideoAP 92 const video = videoArg as MVideoAP
88 93
89 if ( 94 if (
@@ -433,6 +438,12 @@ async function updateVideoFromAP (options: {
433 await setVideoTags({ video: videoUpdated, tags, transaction: t, defaultValue: videoUpdated.Tags }) 438 await setVideoTags({ video: videoUpdated, tags, transaction: t, defaultValue: videoUpdated.Tags })
434 } 439 }
435 440
441 // Update trackers
442 {
443 const trackers = getTrackerUrls(videoObject, videoUpdated)
444 await setVideoTrackers({ video: videoUpdated, trackers, transaction: t })
445 }
446
436 { 447 {
437 // Update captions 448 // Update captions
438 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t) 449 await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
@@ -577,7 +588,7 @@ function isAPVideoUrlObject (url: any): url is ActivityVideoUrlObject {
577 return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/') 588 return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/')
578} 589}
579 590
580function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { 591function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject {
581 return url && url.mediaType === 'application/x-mpegURL' 592 return url && url.mediaType === 'application/x-mpegURL'
582} 593}
583 594
@@ -671,6 +682,12 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
671 }) 682 })
672 await Promise.all(videoCaptionsPromises) 683 await Promise.all(videoCaptionsPromises)
673 684
685 // Process trackers
686 {
687 const trackers = getTrackerUrls(videoObject, videoCreated)
688 await setVideoTrackers({ video: videoCreated, trackers, transaction: t })
689 }
690
674 videoCreated.VideoFiles = videoFiles 691 videoCreated.VideoFiles = videoFiles
675 692
676 if (videoCreated.isLive) { 693 if (videoCreated.isLive) {
@@ -797,7 +814,7 @@ function videoFileActivityUrlToDBAttributes (
797 : parsed.xs 814 : parsed.xs
798 815
799 // Fetch associated metadata url, if any 816 // Fetch associated metadata url, if any
800 const metadata = urls.filter(isAPVideoFileMetadataObject) 817 const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
801 .find(u => { 818 .find(u => {
802 return u.height === fileUrl.height && 819 return u.height === fileUrl.height &&
803 u.fps === fileUrl.fps && 820 u.fps === fileUrl.fps &&
@@ -889,3 +906,33 @@ function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoWithHost)
889 ? previewIcon.url 906 ? previewIcon.url
890 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName())) 907 : buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
891} 908}
909
910function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
911 let wsFound = false
912
913 const trackers = object.url.filter(u => isAPVideoTrackerUrlObject(u))
914 .map((u: ActivityTrackerUrlObject) => {
915 if (u.rel.includes('websocket')) wsFound = true
916
917 return u.href
918 })
919
920 if (wsFound) return trackers
921
922 return [
923 buildRemoteVideoBaseUrl(video, '/tracker/socket', REMOTE_SCHEME.WS),
924 buildRemoteVideoBaseUrl(video, '/tracker/announce')
925 ]
926}
927
928async function setVideoTrackers (options: {
929 video: MVideo
930 trackers: string[]
931 transaction?: Transaction
932}) {
933 const { video, trackers, transaction } = options
934
935 const trackerInstances = await TrackerModel.findOrCreateTrackers(trackers, transaction)
936
937 await video.$set('Trackers', trackerInstances, { transaction })
938}
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts
index 60008e695..9e2667416 100644
--- a/server/lib/schedulers/videos-redundancy-scheduler.ts
+++ b/server/lib/schedulers/videos-redundancy-scheduler.ts
@@ -1,6 +1,7 @@
1import { move } from 'fs-extra' 1import { move } from 'fs-extra'
2import { join } from 'path' 2import { join } from 'path'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { TrackerModel } from '@server/models/server/tracker'
4import { VideoModel } from '@server/models/video/video' 5import { VideoModel } from '@server/models/video/video'
5import { 6import {
6 MStreamingPlaylist, 7 MStreamingPlaylist,
@@ -221,8 +222,8 @@ export class VideosRedundancyScheduler extends AbstractScheduler {
221 222
222 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy) 223 logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy)
223 224
224 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 225 const trackerUrls = await TrackerModel.listUrlsByVideoId(video.id)
225 const magnetUri = generateMagnetUri(video, video, file, baseUrlHttp, baseUrlWs) 226 const magnetUri = generateMagnetUri(video, file, trackerUrls)
226 227
227 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) 228 const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
228 229
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index 4bad8d6ca..49317df28 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -1,5 +1,6 @@
1import { copy } from 'fs-extra' 1import { copy } from 'fs-extra'
2import { join } from 'path' 2import { join } from 'path'
3import { logger } from '@server/helpers/logger'
3import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' 4import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
4import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' 5import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
5import { processImage } from '../helpers/image-utils' 6import { processImage } from '../helpers/image-utils'
@@ -62,7 +63,7 @@ function createVideoMiniatureFromUrl (options: {
62 size?: ImageSize 63 size?: ImageSize
63}) { 64}) {
64 const { downloadUrl, video, type, size } = options 65 const { downloadUrl, video, type, size } = options
65 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 66 const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
66 67
67 // Only save the file URL if it is a remote video 68 // Only save the file URL if it is a remote video
68 const fileUrl = video.isOwned() 69 const fileUrl = video.isOwned()
@@ -76,10 +77,16 @@ function createVideoMiniatureFromUrl (options: {
76 77
77 // If the thumbnail URL did not change and has a unique filename (introduced in 3.2), avoid thumbnail processing 78 // If the thumbnail URL did not change and has a unique filename (introduced in 3.2), avoid thumbnail processing
78 const thumbnailUrlChanged = !existingUrl || existingUrl !== downloadUrl || downloadUrl.endsWith(`${video.uuid}.jpg`) 79 const thumbnailUrlChanged = !existingUrl || existingUrl !== downloadUrl || downloadUrl.endsWith(`${video.uuid}.jpg`)
80
81 // Do not change the thumbnail filename if the file did not change
82 const filename = thumbnailUrlChanged
83 ? updatedFilename
84 : existingThumbnail.filename
85
79 const thumbnailCreator = () => { 86 const thumbnailCreator = () => {
80 if (thumbnailUrlChanged) return downloadImage(downloadUrl, basePath, filename, { width, height }) 87 if (thumbnailUrlChanged) return downloadImage(downloadUrl, basePath, filename, { width, height })
81 88
82 return copy(existingThumbnail.getPath(), ThumbnailModel.buildPath(type, filename)) 89 return Promise.resolve()
83 } 90 }
84 91
85 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) 92 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
@@ -236,7 +243,7 @@ async function createThumbnailFromFunction (parameters: {
236 fileUrl = null 243 fileUrl = null
237 } = parameters 244 } = parameters
238 245
239 const oldFilename = existingThumbnail 246 const oldFilename = existingThumbnail && existingThumbnail.filename !== filename
240 ? existingThumbnail.filename 247 ? existingThumbnail.filename
241 : undefined 248 : undefined
242 249
@@ -248,7 +255,8 @@ async function createThumbnailFromFunction (parameters: {
248 thumbnail.type = type 255 thumbnail.type = type
249 thumbnail.fileUrl = fileUrl 256 thumbnail.fileUrl = fileUrl
250 thumbnail.automaticallyGenerated = automaticallyGenerated 257 thumbnail.automaticallyGenerated = automaticallyGenerated
251 thumbnail.previousThumbnailFilename = oldFilename 258
259 if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename
252 260
253 await thumbnailCreator() 261 await thumbnailCreator()
254 262
diff --git a/server/models/server/tracker.ts b/server/models/server/tracker.ts
new file mode 100644
index 000000000..d7c91faad
--- /dev/null
+++ b/server/models/server/tracker.ts
@@ -0,0 +1,73 @@
1import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { Transaction } from 'sequelize/types'
3import { MTracker } from '@server/types/models/server/tracker'
4import { VideoModel } from '../video/video'
5import { VideoTrackerModel } from './video-tracker'
6
7@Table({
8 tableName: 'tracker',
9 indexes: [
10 {
11 fields: [ 'url' ],
12 unique: true
13 }
14 ]
15})
16export class TrackerModel extends Model {
17
18 @AllowNull(false)
19 @Column
20 url: string
21
22 @CreatedAt
23 createdAt: Date
24
25 @UpdatedAt
26 updatedAt: Date
27
28 @BelongsToMany(() => VideoModel, {
29 foreignKey: 'trackerId',
30 through: () => VideoTrackerModel,
31 onDelete: 'CASCADE'
32 })
33 Videos: VideoModel[]
34
35 static listUrlsByVideoId (videoId: number) {
36 const query = {
37 include: [
38 {
39 attributes: [ 'id', 'trackerId' ],
40 model: VideoModel.unscoped(),
41 required: true,
42 where: { id: videoId }
43 }
44 ]
45 }
46
47 return TrackerModel.findAll(query)
48 .then(rows => rows.map(rows => rows.url))
49 }
50
51 static findOrCreateTrackers (trackers: string[], transaction: Transaction): Promise<MTracker[]> {
52 if (trackers === null) return Promise.resolve([])
53
54 const tasks: Promise<MTracker>[] = []
55 trackers.forEach(tracker => {
56 const query = {
57 where: {
58 url: tracker
59 },
60 defaults: {
61 url: tracker
62 },
63 transaction
64 }
65
66 const promise = TrackerModel.findOrCreate<MTracker>(query)
67 .then(([ trackerInstance ]) => trackerInstance)
68 tasks.push(promise)
69 })
70
71 return Promise.all(tasks)
72 }
73}
diff --git a/server/models/server/video-tracker.ts b/server/models/server/video-tracker.ts
new file mode 100644
index 000000000..367bf0117
--- /dev/null
+++ b/server/models/server/video-tracker.ts
@@ -0,0 +1,30 @@
1import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { VideoModel } from '../video/video'
3import { TrackerModel } from './tracker'
4
5@Table({
6 tableName: 'videoTracker',
7 indexes: [
8 {
9 fields: [ 'videoId' ]
10 },
11 {
12 fields: [ 'trackerId' ]
13 }
14 ]
15})
16export class VideoTrackerModel extends Model {
17 @CreatedAt
18 createdAt: Date
19
20 @UpdatedAt
21 updatedAt: Date
22
23 @ForeignKey(() => VideoModel)
24 @Column
25 videoId: number
26
27 @ForeignKey(() => TrackerModel)
28 @Column
29 trackerId: number
30}
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 9533c8d19..319e1175a 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -15,7 +15,6 @@ import {
15 Table, 15 Table,
16 UpdatedAt 16 UpdatedAt
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
19import { afterCommitIfTransaction } from '@server/helpers/database-utils' 18import { afterCommitIfTransaction } from '@server/helpers/database-utils'
20import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models' 19import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 20import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
@@ -168,10 +167,8 @@ export class ThumbnailModel extends Model {
168 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename 167 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
169 168
170 if (video.isOwned()) return WEBSERVER.URL + staticPath 169 if (video.isOwned()) return WEBSERVER.URL + staticPath
171 if (this.fileUrl) return this.fileUrl
172 170
173 // Fallback if we don't have a file URL 171 return this.fileUrl
174 return buildRemoteVideoBaseUrl(video, staticPath)
175 } 172 }
176 173
177 getPath () { 174 getPath () {
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index 71b067335..0bbe9b752 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -16,7 +16,6 @@ import {
16 UpdatedAt 16 UpdatedAt
17} from 'sequelize-typescript' 17} from 'sequelize-typescript'
18import { v4 as uuidv4 } from 'uuid' 18import { v4 as uuidv4 } from 'uuid'
19import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
20import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models' 19import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models'
21import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' 20import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
22import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' 21import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
@@ -208,9 +207,7 @@ export class VideoCaptionModel extends Model {
208 if (!this.Video) this.Video = video as VideoModel 207 if (!this.Video) this.Video = video as VideoModel
209 208
210 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath() 209 if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
211 if (this.fileUrl) return this.fileUrl
212 210
213 // Fallback if we don't have a file URL 211 return this.fileUrl
214 return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
215 } 212 }
216} 213}
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 57807cbfd..5a3706259 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -427,10 +427,8 @@ export class VideoFileModel extends Model {
427 if (!this.Video) this.Video = video as VideoModel 427 if (!this.Video) this.Video = video as VideoModel
428 428
429 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video) 429 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
430 if (this.fileUrl) return this.fileUrl
431 430
432 // Fallback if we don't have a file URL 431 return this.fileUrl
433 return buildRemoteVideoBaseUrl(video, this.getFileStaticPath(video))
434 } 432 }
435 433
436 getFileStaticPath (video: MVideo) { 434 getFileStaticPath (video: MVideo) {
@@ -454,10 +452,7 @@ export class VideoFileModel extends Model {
454 getRemoteTorrentUrl (video: MVideoWithHost) { 452 getRemoteTorrentUrl (video: MVideoWithHost) {
455 if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`) 453 if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
456 454
457 if (this.torrentUrl) return this.torrentUrl 455 return this.torrentUrl
458
459 // Fallback if we don't have a torrent URL
460 return buildRemoteVideoBaseUrl(video, this.getTorrentStaticPath())
461 } 456 }
462 457
463 // We proxify torrent requests so use a local URL 458 // We proxify torrent requests so use a local URL
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index adf460734..9dc3e7722 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -14,8 +14,6 @@ import {
14} from '../../lib/activitypub/url' 14} from '../../lib/activitypub/url'
15import { 15import {
16 MStreamingPlaylistRedundanciesOpt, 16 MStreamingPlaylistRedundanciesOpt,
17 MStreamingPlaylistVideo,
18 MVideo,
19 MVideoAP, 17 MVideoAP,
20 MVideoFile, 18 MVideoFile,
21 MVideoFormattable, 19 MVideoFormattable,
@@ -127,8 +125,6 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
127 } 125 }
128 }) 126 })
129 127
130 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
131
132 const tags = video.Tags ? video.Tags.map(t => t.name) : [] 128 const tags = video.Tags ? video.Tags.map(t => t.name) : []
133 129
134 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) 130 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
@@ -147,14 +143,14 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
147 label: VideoModel.getStateLabel(video.state) 143 label: VideoModel.getStateLabel(video.state)
148 }, 144 },
149 145
150 trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs), 146 trackerUrls: video.getTrackerUrls(),
151 147
152 files: [], 148 files: [],
153 streamingPlaylists 149 streamingPlaylists
154 } 150 }
155 151
156 // Format and sort video files 152 // Format and sort video files
157 detailsJson.files = videoFilesModelToFormattedJSON(video, video, baseUrlHttp, baseUrlWs, video.VideoFiles) 153 detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
158 154
159 return Object.assign(formattedJson, detailsJson) 155 return Object.assign(formattedJson, detailsJson)
160} 156}
@@ -165,17 +161,13 @@ function streamingPlaylistsModelToFormattedJSON (
165): VideoStreamingPlaylist[] { 161): VideoStreamingPlaylist[] {
166 if (isArray(playlists) === false) return [] 162 if (isArray(playlists) === false) return []
167 163
168 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
169
170 return playlists 164 return playlists
171 .map(playlist => { 165 .map(playlist => {
172 const playlistWithVideo = Object.assign(playlist, { Video: video })
173
174 const redundancies = isArray(playlist.RedundancyVideos) 166 const redundancies = isArray(playlist.RedundancyVideos)
175 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) 167 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
176 : [] 168 : []
177 169
178 const files = videoFilesModelToFormattedJSON(playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles) 170 const files = videoFilesModelToFormattedJSON(video, playlist.VideoFiles)
179 171
180 return { 172 return {
181 id: playlist.id, 173 id: playlist.id,
@@ -194,14 +186,12 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
194 return -1 186 return -1
195} 187}
196 188
197// FIXME: refactor/merge model and video arguments
198function videoFilesModelToFormattedJSON ( 189function videoFilesModelToFormattedJSON (
199 model: MVideo | MStreamingPlaylistVideo,
200 video: MVideoFormattableDetails, 190 video: MVideoFormattableDetails,
201 baseUrlHttp: string,
202 baseUrlWs: string,
203 videoFiles: MVideoFileRedundanciesOpt[] 191 videoFiles: MVideoFileRedundanciesOpt[]
204): VideoFile[] { 192): VideoFile[] {
193 const trackerUrls = video.getTrackerUrls()
194
205 return [ ...videoFiles ] 195 return [ ...videoFiles ]
206 .filter(f => !f.isLive()) 196 .filter(f => !f.isLive())
207 .sort(sortByResolutionDesc) 197 .sort(sortByResolutionDesc)
@@ -213,7 +203,7 @@ function videoFilesModelToFormattedJSON (
213 }, 203 },
214 204
215 // FIXME: deprecated in 3.2 205 // FIXME: deprecated in 3.2
216 magnetUri: generateMagnetUri(model, video, videoFile, baseUrlHttp, baseUrlWs), 206 magnetUri: generateMagnetUri(video, videoFile, trackerUrls),
217 207
218 size: videoFile.size, 208 size: videoFile.size,
219 fps: videoFile.fps, 209 fps: videoFile.fps,
@@ -229,15 +219,13 @@ function videoFilesModelToFormattedJSON (
229 }) 219 })
230} 220}
231 221
232// FIXME: refactor/merge model and video arguments
233function addVideoFilesInAPAcc ( 222function addVideoFilesInAPAcc (
234 acc: ActivityUrlObject[] | ActivityTagObject[], 223 acc: ActivityUrlObject[] | ActivityTagObject[],
235 model: MVideoAP | MStreamingPlaylistVideo,
236 video: MVideoWithHost, 224 video: MVideoWithHost,
237 baseUrlHttp: string,
238 baseUrlWs: string,
239 files: MVideoFile[] 225 files: MVideoFile[]
240) { 226) {
227 const trackerUrls = video.getTrackerUrls()
228
241 const sortedFiles = [ ...files ] 229 const sortedFiles = [ ...files ]
242 .filter(f => !f.isLive()) 230 .filter(f => !f.isLive())
243 .sort(sortByResolutionDesc) 231 .sort(sortByResolutionDesc)
@@ -271,14 +259,13 @@ function addVideoFilesInAPAcc (
271 acc.push({ 259 acc.push({
272 type: 'Link', 260 type: 'Link',
273 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet', 261 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
274 href: generateMagnetUri(model, video, file, baseUrlHttp, baseUrlWs), 262 href: generateMagnetUri(video, file, trackerUrls),
275 height: file.resolution 263 height: file.resolution
276 }) 264 })
277 } 265 }
278} 266}
279 267
280function videoModelToActivityPubObject (video: MVideoAP): VideoObject { 268function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
281 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
282 if (!video.Tags) video.Tags = [] 269 if (!video.Tags) video.Tags = []
283 270
284 const tag = video.Tags.map(t => ({ 271 const tag = video.Tags.map(t => ({
@@ -319,7 +306,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
319 } 306 }
320 ] 307 ]
321 308
322 addVideoFilesInAPAcc(url, video, video, baseUrlHttp, baseUrlWs, video.VideoFiles || []) 309 addVideoFilesInAPAcc(url, video, video.VideoFiles || [])
323 310
324 for (const playlist of (video.VideoStreamingPlaylists || [])) { 311 for (const playlist of (video.VideoStreamingPlaylists || [])) {
325 const tag = playlist.p2pMediaLoaderInfohashes 312 const tag = playlist.p2pMediaLoaderInfohashes
@@ -331,8 +318,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
331 href: playlist.segmentsSha256Url 318 href: playlist.segmentsSha256Url
332 }) 319 })
333 320
334 const playlistWithVideo = Object.assign(playlist, { Video: video }) 321 addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || [])
335 addVideoFilesInAPAcc(tag, playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
336 322
337 url.push({ 323 url.push({
338 type: 'Link', 324 type: 'Link',
@@ -342,6 +328,19 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
342 }) 328 })
343 } 329 }
344 330
331 for (const trackerUrl of video.getTrackerUrls()) {
332 const rel2 = trackerUrl.startsWith('http')
333 ? 'http'
334 : 'websocket'
335
336 url.push({
337 type: 'Link',
338 name: `tracker-${rel2}`,
339 rel: [ 'tracker', rel2 ],
340 href: trackerUrl
341 })
342 }
343
345 const subtitleLanguage = [] 344 const subtitleLanguage = []
346 for (const caption of video.VideoCaptions) { 345 for (const caption of video.VideoCaptions) {
347 subtitleLanguage.push({ 346 subtitleLanguage.push({
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 2e6b6aeec..9e67ca0f4 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -60,7 +60,6 @@ import {
60 API_VERSION, 60 API_VERSION,
61 CONSTRAINTS_FIELDS, 61 CONSTRAINTS_FIELDS,
62 LAZY_STATIC_PATHS, 62 LAZY_STATIC_PATHS,
63 REMOTE_SCHEME,
64 STATIC_PATHS, 63 STATIC_PATHS,
65 VIDEO_CATEGORIES, 64 VIDEO_CATEGORIES,
66 VIDEO_LANGUAGES, 65 VIDEO_LANGUAGES,
@@ -107,6 +106,8 @@ import { ActorModel } from '../activitypub/actor'
107import { AvatarModel } from '../avatar/avatar' 106import { AvatarModel } from '../avatar/avatar'
108import { VideoRedundancyModel } from '../redundancy/video-redundancy' 107import { VideoRedundancyModel } from '../redundancy/video-redundancy'
109import { ServerModel } from '../server/server' 108import { ServerModel } from '../server/server'
109import { TrackerModel } from '../server/tracker'
110import { VideoTrackerModel } from '../server/video-tracker'
110import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' 111import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
111import { ScheduleVideoUpdateModel } from './schedule-video-update' 112import { ScheduleVideoUpdateModel } from './schedule-video-update'
112import { TagModel } from './tag' 113import { TagModel } from './tag'
@@ -137,6 +138,7 @@ export enum ScopeNames {
137 FOR_API = 'FOR_API', 138 FOR_API = 'FOR_API',
138 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', 139 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
139 WITH_TAGS = 'WITH_TAGS', 140 WITH_TAGS = 'WITH_TAGS',
141 WITH_TRACKERS = 'WITH_TRACKERS',
140 WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES', 142 WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
141 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 143 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
142 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 144 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
@@ -320,6 +322,14 @@ export type AvailableForListIDsOptions = {
320 [ScopeNames.WITH_TAGS]: { 322 [ScopeNames.WITH_TAGS]: {
321 include: [ TagModel ] 323 include: [ TagModel ]
322 }, 324 },
325 [ScopeNames.WITH_TRACKERS]: {
326 include: [
327 {
328 attributes: [ 'id', 'url' ],
329 model: TrackerModel
330 }
331 ]
332 },
323 [ScopeNames.WITH_BLACKLISTED]: { 333 [ScopeNames.WITH_BLACKLISTED]: {
324 include: [ 334 include: [
325 { 335 {
@@ -616,6 +626,13 @@ export class VideoModel extends Model {
616 }) 626 })
617 Tags: TagModel[] 627 Tags: TagModel[]
618 628
629 @BelongsToMany(() => TrackerModel, {
630 foreignKey: 'videoId',
631 through: () => VideoTrackerModel,
632 onDelete: 'CASCADE'
633 })
634 Trackers: TrackerModel[]
635
619 @HasMany(() => ThumbnailModel, { 636 @HasMany(() => ThumbnailModel, {
620 foreignKey: { 637 foreignKey: {
621 name: 'videoId', 638 name: 'videoId',
@@ -1436,6 +1453,7 @@ export class VideoModel extends Model {
1436 ScopeNames.WITH_SCHEDULED_UPDATE, 1453 ScopeNames.WITH_SCHEDULED_UPDATE,
1437 ScopeNames.WITH_THUMBNAILS, 1454 ScopeNames.WITH_THUMBNAILS,
1438 ScopeNames.WITH_LIVE, 1455 ScopeNames.WITH_LIVE,
1456 ScopeNames.WITH_TRACKERS,
1439 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] }, 1457 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
1440 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } 1458 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
1441 ] 1459 ]
@@ -1887,18 +1905,15 @@ export class VideoModel extends Model {
1887 } 1905 }
1888 1906
1889 getFormattedVideoFilesJSON (): VideoFile[] { 1907 getFormattedVideoFilesJSON (): VideoFile[] {
1890 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
1891 let files: VideoFile[] = [] 1908 let files: VideoFile[] = []
1892 1909
1893 if (Array.isArray(this.VideoFiles)) { 1910 if (Array.isArray(this.VideoFiles)) {
1894 const result = videoFilesModelToFormattedJSON(this, this, baseUrlHttp, baseUrlWs, this.VideoFiles) 1911 const result = videoFilesModelToFormattedJSON(this, this.VideoFiles)
1895 files = files.concat(result) 1912 files = files.concat(result)
1896 } 1913 }
1897 1914
1898 for (const p of (this.VideoStreamingPlaylists || [])) { 1915 for (const p of (this.VideoStreamingPlaylists || [])) {
1899 p.Video = this 1916 const result = videoFilesModelToFormattedJSON(this, p.VideoFiles)
1900
1901 const result = videoFilesModelToFormattedJSON(p, this, baseUrlHttp, baseUrlWs, p.VideoFiles)
1902 files = files.concat(result) 1917 files = files.concat(result)
1903 } 1918 }
1904 1919
@@ -2030,25 +2045,18 @@ export class VideoModel extends Model {
2030 return false 2045 return false
2031 } 2046 }
2032 2047
2033 getBaseUrls () { 2048 getBandwidthBits (videoFile: MVideoFile) {
2034 if (this.isOwned()) { 2049 return Math.ceil((videoFile.size * 8) / this.duration)
2035 return {
2036 baseUrlHttp: WEBSERVER.URL,
2037 baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
2038 }
2039 }
2040
2041 return {
2042 baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
2043 baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
2044 }
2045 } 2050 }
2046 2051
2047 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { 2052 getTrackerUrls () {
2048 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 2053 if (this.isOwned()) {
2049 } 2054 return [
2055 WEBSERVER.URL + '/tracker/announce',
2056 WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket'
2057 ]
2058 }
2050 2059
2051 getBandwidthBits (videoFile: MVideoFile) { 2060 return this.Trackers.map(t => t.url)
2052 return Math.ceil((videoFile.size * 8) / this.duration)
2053 } 2061 }
2054} 2062}
diff --git a/server/types/models/server/tracker.ts b/server/types/models/server/tracker.ts
new file mode 100644
index 000000000..5fe03f8c0
--- /dev/null
+++ b/server/types/models/server/tracker.ts
@@ -0,0 +1,7 @@
1import { TrackerModel } from '../../../models/server/tracker'
2
3export type MTracker = Omit<TrackerModel, 'Videos'>
4
5// ############################################################################
6
7export type MTrackerUrl = Pick<MTracker, 'url'>
diff --git a/server/types/models/video/video.ts b/server/types/models/video/video.ts
index 92dcbaf59..692490230 100644
--- a/server/types/models/video/video.ts
+++ b/server/types/models/video/video.ts
@@ -1,5 +1,6 @@
1import { PickWith, PickWithOpt } from '@shared/core-utils' 1import { PickWith, PickWithOpt } from '@shared/core-utils'
2import { VideoModel } from '../../../models/video/video' 2import { VideoModel } from '../../../models/video/video'
3import { MTrackerUrl } from '../server/tracker'
3import { MUserVideoHistoryTime } from '../user/user-video-history' 4import { MUserVideoHistoryTime } from '../user/user-video-history'
4import { MScheduleVideoUpdate } from './schedule-video-update' 5import { MScheduleVideoUpdate } from './schedule-video-update'
5import { MTag } from './tag' 6import { MTag } from './tag'
@@ -216,4 +217,5 @@ export type MVideoFormattableDetails =
216 Use<'VideoChannel', MChannelFormattable> & 217 Use<'VideoChannel', MChannelFormattable> &
217 Use<'Tags', MTag[]> & 218 Use<'Tags', MTag[]> &
218 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> & 219 Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> &
219 Use<'VideoFiles', MVideoFileRedundanciesOpt[]> 220 Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
221 Use<'Trackers', MTrackerUrl[]>
diff --git a/shared/models/activitypub/objects/common-objects.ts b/shared/models/activitypub/objects/common-objects.ts
index 43e8ea067..76f0e3bcf 100644
--- a/shared/models/activitypub/objects/common-objects.ts
+++ b/shared/models/activitypub/objects/common-objects.ts
@@ -30,7 +30,7 @@ export type ActivityPlaylistSegmentHashesObject = {
30 href: string 30 href: string
31} 31}
32 32
33export type ActivityVideoFileMetadataObject = { 33export type ActivityVideoFileMetadataUrlObject = {
34 type: 'Link' 34 type: 'Link'
35 rel: [ 'metadata', any ] 35 rel: [ 'metadata', any ]
36 mediaType: 'application/json' 36 mediaType: 'application/json'
@@ -39,6 +39,13 @@ export type ActivityVideoFileMetadataObject = {
39 fps: number 39 fps: number
40} 40}
41 41
42export type ActivityTrackerUrlObject = {
43 type: 'Link'
44 rel: [ 'tracker', 'websocket' | 'http' ]
45 name: string
46 href: string
47}
48
42export type ActivityPlaylistInfohashesObject = { 49export type ActivityPlaylistInfohashesObject = {
43 type: 'Infohash' 50 type: 'Infohash'
44 name: string 51 name: string
@@ -96,7 +103,7 @@ export type ActivityTagObject =
96 | ActivityMentionObject 103 | ActivityMentionObject
97 | ActivityBitTorrentUrlObject 104 | ActivityBitTorrentUrlObject
98 | ActivityMagnetUrlObject 105 | ActivityMagnetUrlObject
99 | ActivityVideoFileMetadataObject 106 | ActivityVideoFileMetadataUrlObject
100 107
101export type ActivityUrlObject = 108export type ActivityUrlObject =
102 ActivityVideoUrlObject 109 ActivityVideoUrlObject
@@ -104,7 +111,8 @@ export type ActivityUrlObject =
104 | ActivityBitTorrentUrlObject 111 | ActivityBitTorrentUrlObject
105 | ActivityMagnetUrlObject 112 | ActivityMagnetUrlObject
106 | ActivityHtmlUrlObject 113 | ActivityHtmlUrlObject
107 | ActivityVideoFileMetadataObject 114 | ActivityVideoFileMetadataUrlObject
115 | ActivityTrackerUrlObject
108 116
109export interface ActivityPubAttributedTo { 117export interface ActivityPubAttributedTo {
110 type: 'Group' | 'Person' 118 type: 'Group' | 'Person'
diff --git a/shared/models/activitypub/objects/video-torrent-object.ts b/shared/models/activitypub/objects/video-torrent-object.ts
index 6d18e93d5..bfbcfb1a5 100644
--- a/shared/models/activitypub/objects/video-torrent-object.ts
+++ b/shared/models/activitypub/objects/video-torrent-object.ts
@@ -40,11 +40,14 @@ export interface VideoObject {
40 icon: ActivityIconObject[] 40 icon: ActivityIconObject[]
41 41
42 url: ActivityUrlObject[] 42 url: ActivityUrlObject[]
43
43 likes: string 44 likes: string
44 dislikes: string 45 dislikes: string
45 shares: string 46 shares: string
46 comments: string 47 comments: string
48
47 attributedTo: ActivityPubAttributedTo[] 49 attributedTo: ActivityPubAttributedTo[]
50
48 to?: string[] 51 to?: string[]
49 cc?: string[] 52 cc?: string[]
50} 53}