diff options
author | Chocobozzz <me@florianbigard.com> | 2021-02-18 10:15:11 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2021-02-18 13:38:09 +0100 |
commit | d9a2a03196275065c28f4a0b7d4d7bc9992d77a1 (patch) | |
tree | 14579db95cd07506bf3d8e5c0af3ef1630e8700c | |
parent | 2451916e45420fedf556913ce121f3964c4b57d6 (diff) | |
download | PeerTube-d9a2a03196275065c28f4a0b7d4d7bc9992d77a1.tar.gz PeerTube-d9a2a03196275065c28f4a0b7d4d7bc9992d77a1.tar.zst PeerTube-d9a2a03196275065c28f4a0b7d4d7bc9992d77a1.zip |
Don't guess remote tracker URL
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 | ||
204 | function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string) { | 204 | function 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 @@ | |||
1 | import validator from 'validator' | 1 | import validator from 'validator' |
2 | import { logger } from '@server/helpers/logger' | ||
3 | import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models' | ||
4 | import { VideoState } from '../../../../shared/models/videos' | ||
2 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' | 5 | import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants' |
3 | import { peertubeTruncate } from '../../core-utils' | 6 | import { peertubeTruncate } from '../../core-utils' |
4 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' | 7 | import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc' |
@@ -11,9 +14,6 @@ import { | |||
11 | isVideoViewsValid | 14 | isVideoViewsValid |
12 | } from '../videos' | 15 | } from '../videos' |
13 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' | 16 | import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc' |
14 | import { VideoState } from '../../../../shared/models/videos' | ||
15 | import { logger } from '@server/helpers/logger' | ||
16 | import { ActivityVideoFileMetadataObject } from '@shared/models' | ||
17 | 17 | ||
18 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { | 18 | function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) { |
19 | return isBaseActivityValid(activity, 'Update') && | 19 | return isBaseActivityValid(activity, 'Update') && |
@@ -84,6 +84,7 @@ function sanitizeAndCheckVideoTorrentObject (video: any) { | |||
84 | 84 | ||
85 | function isRemoteVideoUrlValid (url: any) { | 85 | function 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 | ||
112 | function isAPVideoFileMetadataObject (url: any): url is ActivityVideoFileMetadataObject { | 117 | function 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 | ||
124 | function 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 | ||
121 | export { | 132 | export { |
@@ -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 | ||
111 | function generateMagnetUri ( | 110 | function 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 | ||
27 | const LAST_MIGRATION_VERSION = 585 | 27 | const 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 @@ | |||
1 | import { TrackerModel } from '@server/models/server/tracker' | ||
2 | import { VideoTrackerModel } from '@server/models/server/video-tracker' | ||
1 | import { QueryTypes, Transaction } from 'sequelize' | 3 | import { QueryTypes, Transaction } from 'sequelize' |
2 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' | 4 | import { Sequelize as SequelizeTypescript } from 'sequelize-typescript' |
3 | import { isTestInstance } from '../helpers/core-utils' | 5 | import { 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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
37 | function down (options) { | ||
38 | throw new Error('Not implemented.') | ||
39 | } | ||
40 | |||
41 | export { | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async 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 | |||
123 | function down (options) { | ||
124 | throw new Error('Not implemented.') | ||
125 | } | ||
126 | |||
127 | export { | ||
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' | |||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import { basename, join } from 'path' | 4 | import { basename, join } from 'path' |
5 | import * as request from 'request' | 5 | import * as request from 'request' |
6 | import * as sequelize from 'sequelize' | 6 | import { Transaction } from 'sequelize/types' |
7 | import { TrackerModel } from '@server/models/server/tracker' | ||
7 | import { VideoLiveModel } from '@server/models/video/video-live' | 8 | import { VideoLiveModel } from '@server/models/video/video-live' |
8 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 9 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
9 | import { | 10 | import { |
@@ -16,12 +17,16 @@ import { | |||
16 | ActivityUrlObject, | 17 | ActivityUrlObject, |
17 | ActivityVideoUrlObject | 18 | ActivityVideoUrlObject |
18 | } from '../../../shared/index' | 19 | } from '../../../shared/index' |
19 | import { ActivityIconObject, VideoObject } from '../../../shared/models/activitypub/objects' | 20 | import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects' |
20 | import { VideoPrivacy } from '../../../shared/models/videos' | 21 | import { VideoPrivacy } from '../../../shared/models/videos' |
21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 22 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
22 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 23 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
23 | import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub' | 24 | import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub' |
24 | import { isAPVideoFileMetadataObject, sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos' | 25 | import { |
26 | isAPVideoFileUrlMetadataObject, | ||
27 | isAPVideoTrackerUrlObject, | ||
28 | sanitizeAndCheckVideoTorrentObject | ||
29 | } from '../../helpers/custom-validators/activitypub/videos' | ||
25 | import { isArray } from '../../helpers/custom-validators/misc' | 30 | import { isArray } from '../../helpers/custom-validators/misc' |
26 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 31 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
27 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 32 | import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
@@ -83,7 +88,7 @@ import { addVideoShares, shareVideoByServerAndChannel } from './share' | |||
83 | import { addVideoComments } from './video-comments' | 88 | import { addVideoComments } from './video-comments' |
84 | import { createRates } from './video-rates' | 89 | import { createRates } from './video-rates' |
85 | 90 | ||
86 | async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 91 | async 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 | ||
580 | function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject { | 591 | function 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 | |||
910 | function 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 | |||
928 | async 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 @@ | |||
1 | import { move } from 'fs-extra' | 1 | import { move } from 'fs-extra' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { getServerActor } from '@server/models/application/application' | 3 | import { getServerActor } from '@server/models/application/application' |
4 | import { TrackerModel } from '@server/models/server/tracker' | ||
4 | import { VideoModel } from '@server/models/video/video' | 5 | import { VideoModel } from '@server/models/video/video' |
5 | import { | 6 | import { |
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 @@ | |||
1 | import { copy } from 'fs-extra' | 1 | import { copy } from 'fs-extra' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { logger } from '@server/helpers/logger' | ||
3 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | 4 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' |
4 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | 5 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' |
5 | import { processImage } from '../helpers/image-utils' | 6 | import { 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 @@ | |||
1 | import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { Transaction } from 'sequelize/types' | ||
3 | import { MTracker } from '@server/types/models/server/tracker' | ||
4 | import { VideoModel } from '../video/video' | ||
5 | import { VideoTrackerModel } from './video-tracker' | ||
6 | |||
7 | @Table({ | ||
8 | tableName: 'tracker', | ||
9 | indexes: [ | ||
10 | { | ||
11 | fields: [ 'url' ], | ||
12 | unique: true | ||
13 | } | ||
14 | ] | ||
15 | }) | ||
16 | export 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 @@ | |||
1 | import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
2 | import { VideoModel } from '../video/video' | ||
3 | import { TrackerModel } from './tracker' | ||
4 | |||
5 | @Table({ | ||
6 | tableName: 'videoTracker', | ||
7 | indexes: [ | ||
8 | { | ||
9 | fields: [ 'videoId' ] | ||
10 | }, | ||
11 | { | ||
12 | fields: [ 'trackerId' ] | ||
13 | } | ||
14 | ] | ||
15 | }) | ||
16 | export 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' |
18 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | ||
19 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' | 18 | import { afterCommitIfTransaction } from '@server/helpers/database-utils' |
20 | import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models' | 19 | import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models' |
21 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 20 | import { 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' |
18 | import { v4 as uuidv4 } from 'uuid' | 18 | import { v4 as uuidv4 } from 'uuid' |
19 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | ||
20 | import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models' | 19 | import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models' |
21 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' | 20 | import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model' |
22 | import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions' | 21 | import { 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' |
15 | import { | 15 | import { |
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 | ||
198 | function videoFilesModelToFormattedJSON ( | 189 | function 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 | ||
233 | function addVideoFilesInAPAcc ( | 222 | function 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 | ||
280 | function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | 268 | function 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' | |||
107 | import { AvatarModel } from '../avatar/avatar' | 106 | import { AvatarModel } from '../avatar/avatar' |
108 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 107 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
109 | import { ServerModel } from '../server/server' | 108 | import { ServerModel } from '../server/server' |
109 | import { TrackerModel } from '../server/tracker' | ||
110 | import { VideoTrackerModel } from '../server/video-tracker' | ||
110 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | 111 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' |
111 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 112 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
112 | import { TagModel } from './tag' | 113 | import { 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 @@ | |||
1 | import { TrackerModel } from '../../../models/server/tracker' | ||
2 | |||
3 | export type MTracker = Omit<TrackerModel, 'Videos'> | ||
4 | |||
5 | // ############################################################################ | ||
6 | |||
7 | export 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 @@ | |||
1 | import { PickWith, PickWithOpt } from '@shared/core-utils' | 1 | import { PickWith, PickWithOpt } from '@shared/core-utils' |
2 | import { VideoModel } from '../../../models/video/video' | 2 | import { VideoModel } from '../../../models/video/video' |
3 | import { MTrackerUrl } from '../server/tracker' | ||
3 | import { MUserVideoHistoryTime } from '../user/user-video-history' | 4 | import { MUserVideoHistoryTime } from '../user/user-video-history' |
4 | import { MScheduleVideoUpdate } from './schedule-video-update' | 5 | import { MScheduleVideoUpdate } from './schedule-video-update' |
5 | import { MTag } from './tag' | 6 | import { 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 | ||
33 | export type ActivityVideoFileMetadataObject = { | 33 | export 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 | ||
42 | export type ActivityTrackerUrlObject = { | ||
43 | type: 'Link' | ||
44 | rel: [ 'tracker', 'websocket' | 'http' ] | ||
45 | name: string | ||
46 | href: string | ||
47 | } | ||
48 | |||
42 | export type ActivityPlaylistInfohashesObject = { | 49 | export 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 | ||
101 | export type ActivityUrlObject = | 108 | export 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 | ||
109 | export interface ActivityPubAttributedTo { | 117 | export 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 | } |