diff options
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/actor/actor-follow.ts | 36 | ||||
-rw-r--r-- | server/models/redundancy/video-redundancy.ts | 4 | ||||
-rw-r--r-- | server/models/video/formatter/video-format-utils.ts | 8 | ||||
-rw-r--r-- | server/models/video/sql/shared/video-tables.ts | 3 | ||||
-rw-r--r-- | server/models/video/sql/videos-id-list-query-builder.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 45 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 8 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 85 | ||||
-rw-r--r-- | server/models/video/video.ts | 19 |
9 files changed, 128 insertions, 86 deletions
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 3a09e51d6..3080e02a6 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -19,8 +19,8 @@ import { | |||
19 | UpdatedAt | 19 | UpdatedAt |
20 | } from 'sequelize-typescript' | 20 | } from 'sequelize-typescript' |
21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' | 21 | import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' |
22 | import { doesExist } from '@server/helpers/database-utils' | ||
22 | import { getServerActor } from '@server/models/application/application' | 23 | import { getServerActor } from '@server/models/application/application' |
23 | import { VideoModel } from '@server/models/video/video' | ||
24 | import { | 24 | import { |
25 | MActorFollowActorsDefault, | 25 | MActorFollowActorsDefault, |
26 | MActorFollowActorsDefaultSubscription, | 26 | MActorFollowActorsDefaultSubscription, |
@@ -166,14 +166,8 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
166 | 166 | ||
167 | static isFollowedBy (actorId: number, followerActorId: number) { | 167 | static isFollowedBy (actorId: number, followerActorId: number) { |
168 | const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' | 168 | const query = 'SELECT 1 FROM "actorFollow" WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId LIMIT 1' |
169 | const options = { | ||
170 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
171 | bind: { actorId, followerActorId }, | ||
172 | raw: true | ||
173 | } | ||
174 | 169 | ||
175 | return VideoModel.sequelize.query(query, options) | 170 | return doesExist(query, { actorId, followerActorId }) |
176 | .then(results => results.length === 1) | ||
177 | } | 171 | } |
178 | 172 | ||
179 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { | 173 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { |
@@ -324,13 +318,13 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
324 | 318 | ||
325 | const followWhere = state ? { state } : {} | 319 | const followWhere = state ? { state } : {} |
326 | const followingWhere: WhereOptions = {} | 320 | const followingWhere: WhereOptions = {} |
327 | const followingServerWhere: WhereOptions = {} | ||
328 | 321 | ||
329 | if (search) { | 322 | if (search) { |
330 | Object.assign(followingServerWhere, { | 323 | Object.assign(followWhere, { |
331 | host: { | 324 | [Op.or]: [ |
332 | [Op.iLike]: '%' + search + '%' | 325 | searchAttribute(options.search, '$ActorFollowing.preferredUsername$'), |
333 | } | 326 | searchAttribute(options.search, '$ActorFollowing.Server.host$') |
327 | ] | ||
334 | }) | 328 | }) |
335 | } | 329 | } |
336 | 330 | ||
@@ -361,8 +355,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
361 | include: [ | 355 | include: [ |
362 | { | 356 | { |
363 | model: ServerModel, | 357 | model: ServerModel, |
364 | required: true, | 358 | required: true |
365 | where: followingServerWhere | ||
366 | } | 359 | } |
367 | ] | 360 | ] |
368 | } | 361 | } |
@@ -391,13 +384,13 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
391 | 384 | ||
392 | const followWhere = state ? { state } : {} | 385 | const followWhere = state ? { state } : {} |
393 | const followerWhere: WhereOptions = {} | 386 | const followerWhere: WhereOptions = {} |
394 | const followerServerWhere: WhereOptions = {} | ||
395 | 387 | ||
396 | if (search) { | 388 | if (search) { |
397 | Object.assign(followerServerWhere, { | 389 | Object.assign(followWhere, { |
398 | host: { | 390 | [Op.or]: [ |
399 | [Op.iLike]: '%' + search + '%' | 391 | searchAttribute(search, '$ActorFollower.preferredUsername$'), |
400 | } | 392 | searchAttribute(search, '$ActorFollower.Server.host$') |
393 | ] | ||
401 | }) | 394 | }) |
402 | } | 395 | } |
403 | 396 | ||
@@ -420,8 +413,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
420 | include: [ | 413 | include: [ |
421 | { | 414 | { |
422 | model: ServerModel, | 415 | model: ServerModel, |
423 | required: true, | 416 | required: true |
424 | where: followerServerWhere | ||
425 | } | 417 | } |
426 | ] | 418 | ] |
427 | }, | 419 | }, |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index ccda023e0..d645be248 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -160,8 +160,8 @@ export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedu | |||
160 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 160 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
161 | logger.info('Removing duplicated video file %s.', logIdentifier) | 161 | logger.info('Removing duplicated video file %s.', logIdentifier) |
162 | 162 | ||
163 | videoFile.Video.removeFile(videoFile, true) | 163 | videoFile.Video.removeFileAndTorrent(videoFile, true) |
164 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 164 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
165 | } | 165 | } |
166 | 166 | ||
167 | if (instance.videoStreamingPlaylistId) { | 167 | if (instance.videoStreamingPlaylistId) { |
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts index 6b1e59063..3310b3b46 100644 --- a/server/models/video/formatter/video-format-utils.ts +++ b/server/models/video/formatter/video-format-utils.ts | |||
@@ -182,8 +182,8 @@ function streamingPlaylistsModelToFormattedJSON ( | |||
182 | return { | 182 | return { |
183 | id: playlist.id, | 183 | id: playlist.id, |
184 | type: playlist.type, | 184 | type: playlist.type, |
185 | playlistUrl: playlist.playlistUrl, | 185 | playlistUrl: playlist.getMasterPlaylistUrl(video), |
186 | segmentsSha256Url: playlist.segmentsSha256Url, | 186 | segmentsSha256Url: playlist.getSha256SegmentsUrl(video), |
187 | redundancies, | 187 | redundancies, |
188 | files | 188 | files |
189 | } | 189 | } |
@@ -331,7 +331,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
331 | type: 'Link', | 331 | type: 'Link', |
332 | name: 'sha256', | 332 | name: 'sha256', |
333 | mediaType: 'application/json' as 'application/json', | 333 | mediaType: 'application/json' as 'application/json', |
334 | href: playlist.segmentsSha256Url | 334 | href: playlist.getSha256SegmentsUrl(video) |
335 | }) | 335 | }) |
336 | 336 | ||
337 | addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) | 337 | addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || []) |
@@ -339,7 +339,7 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoObject { | |||
339 | url.push({ | 339 | url.push({ |
340 | type: 'Link', | 340 | type: 'Link', |
341 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', | 341 | mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', |
342 | href: playlist.playlistUrl, | 342 | href: playlist.getMasterPlaylistUrl(video), |
343 | tag | 343 | tag |
344 | }) | 344 | }) |
345 | } | 345 | } |
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts index abdd22188..742d19099 100644 --- a/server/models/video/sql/shared/video-tables.ts +++ b/server/models/video/sql/shared/video-tables.ts | |||
@@ -92,12 +92,13 @@ export class VideoTables { | |||
92 | } | 92 | } |
93 | 93 | ||
94 | getStreamingPlaylistAttributes () { | 94 | getStreamingPlaylistAttributes () { |
95 | let playlistKeys = [ 'id', 'playlistUrl', 'type' ] | 95 | let playlistKeys = [ 'id', 'playlistUrl', 'playlistFilename', 'type' ] |
96 | 96 | ||
97 | if (this.mode === 'get') { | 97 | if (this.mode === 'get') { |
98 | playlistKeys = playlistKeys.concat([ | 98 | playlistKeys = playlistKeys.concat([ |
99 | 'p2pMediaLoaderInfohashes', | 99 | 'p2pMediaLoaderInfohashes', |
100 | 'p2pMediaLoaderPeerVersion', | 100 | 'p2pMediaLoaderPeerVersion', |
101 | 'segmentsSha256Filename', | ||
101 | 'segmentsSha256Url', | 102 | 'segmentsSha256Url', |
102 | 'videoId', | 103 | 'videoId', |
103 | 'createdAt', | 104 | 'createdAt', |
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts index 30b251f0f..054f71c8c 100644 --- a/server/models/video/sql/videos-id-list-query-builder.ts +++ b/server/models/video/sql/videos-id-list-query-builder.ts | |||
@@ -304,16 +304,16 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | |||
304 | private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { | 304 | private whereFollowerActorId (followerActorId: number, includeLocalVideos: boolean) { |
305 | let query = | 305 | let query = |
306 | '(' + | 306 | '(' + |
307 | ' EXISTS (' + | 307 | ' EXISTS (' + // Videos shared by actors we follow |
308 | ' SELECT 1 FROM "videoShare" ' + | 308 | ' SELECT 1 FROM "videoShare" ' + |
309 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + | 309 | ' INNER JOIN "actorFollow" "actorFollowShare" ON "actorFollowShare"."targetActorId" = "videoShare"."actorId" ' + |
310 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + | 310 | ' AND "actorFollowShare"."actorId" = :followerActorId AND "actorFollowShare"."state" = \'accepted\' ' + |
311 | ' WHERE "videoShare"."videoId" = "video"."id"' + | 311 | ' WHERE "videoShare"."videoId" = "video"."id"' + |
312 | ' )' + | 312 | ' )' + |
313 | ' OR' + | 313 | ' OR' + |
314 | ' EXISTS (' + | 314 | ' EXISTS (' + // Videos published by accounts we follow |
315 | ' SELECT 1 from "actorFollow" ' + | 315 | ' SELECT 1 from "actorFollow" ' + |
316 | ' WHERE "actorFollow"."targetActorId" = "videoChannel"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + | 316 | ' WHERE "actorFollow"."targetActorId" = "account"."actorId" AND "actorFollow"."actorId" = :followerActorId ' + |
317 | ' AND "actorFollow"."state" = \'accepted\'' + | 317 | ' AND "actorFollow"."state" = \'accepted\'' + |
318 | ' )' | 318 | ' )' |
319 | 319 | ||
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 22cf63804..797a85a4e 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { remove } from 'fs-extra' | 1 | import { remove } from 'fs-extra' |
2 | import * as memoizee from 'memoizee' | 2 | import * as memoizee from 'memoizee' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' | 4 | import { FindOptions, Op, Transaction } from 'sequelize' |
5 | import { | 5 | import { |
6 | AllowNull, | 6 | AllowNull, |
7 | BelongsTo, | 7 | BelongsTo, |
@@ -21,6 +21,7 @@ import { | |||
21 | import { Where } from 'sequelize/types/lib/utils' | 21 | import { Where } from 'sequelize/types/lib/utils' |
22 | import validator from 'validator' | 22 | import validator from 'validator' |
23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' | 23 | import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' |
24 | import { doesExist } from '@server/helpers/database-utils' | ||
24 | import { logger } from '@server/helpers/logger' | 25 | import { logger } from '@server/helpers/logger' |
25 | import { extractVideo } from '@server/helpers/video' | 26 | import { extractVideo } from '@server/helpers/video' |
26 | import { getTorrentFilePath } from '@server/lib/video-paths' | 27 | import { getTorrentFilePath } from '@server/lib/video-paths' |
@@ -250,14 +251,8 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
250 | 251 | ||
251 | static doesInfohashExist (infoHash: string) { | 252 | static doesInfohashExist (infoHash: string) { |
252 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 253 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
253 | const options = { | ||
254 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
255 | bind: { infoHash }, | ||
256 | raw: true | ||
257 | } | ||
258 | 254 | ||
259 | return VideoModel.sequelize.query(query, options) | 255 | return doesExist(query, { infoHash }) |
260 | .then(results => results.length === 1) | ||
261 | } | 256 | } |
262 | 257 | ||
263 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { | 258 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { |
@@ -266,6 +261,33 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
266 | return !!videoFile | 261 | return !!videoFile |
267 | } | 262 | } |
268 | 263 | ||
264 | static async doesOwnedTorrentFileExist (filename: string) { | ||
265 | const query = 'SELECT 1 FROM "videoFile" ' + | ||
266 | 'LEFT JOIN "video" "webtorrent" ON "webtorrent"."id" = "videoFile"."videoId" AND "webtorrent"."remote" IS FALSE ' + | ||
267 | 'LEFT JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoFile"."videoStreamingPlaylistId" ' + | ||
268 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | ||
269 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | ||
270 | |||
271 | return doesExist(query, { filename }) | ||
272 | } | ||
273 | |||
274 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | ||
275 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | ||
276 | 'WHERE "filename" = $filename LIMIT 1' | ||
277 | |||
278 | return doesExist(query, { filename }) | ||
279 | } | ||
280 | |||
281 | static loadByFilename (filename: string) { | ||
282 | const query = { | ||
283 | where: { | ||
284 | filename | ||
285 | } | ||
286 | } | ||
287 | |||
288 | return VideoFileModel.findOne(query) | ||
289 | } | ||
290 | |||
269 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { | 291 | static loadWithVideoOrPlaylistByTorrentFilename (filename: string) { |
270 | const query = { | 292 | const query = { |
271 | where: { | 293 | where: { |
@@ -443,10 +465,9 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
443 | } | 465 | } |
444 | 466 | ||
445 | getFileDownloadUrl (video: MVideoWithHost) { | 467 | getFileDownloadUrl (video: MVideoWithHost) { |
446 | const basePath = this.isHLS() | 468 | const path = this.isHLS() |
447 | ? STATIC_DOWNLOAD_PATHS.HLS_VIDEOS | 469 | ? join(STATIC_DOWNLOAD_PATHS.HLS_VIDEOS, `${video.uuid}-${this.resolution}-fragmented${this.extname}`) |
448 | : STATIC_DOWNLOAD_PATHS.VIDEOS | 470 | : join(STATIC_DOWNLOAD_PATHS.VIDEOS, `${video.uuid}-${this.resolution}${this.extname}`) |
449 | const path = join(basePath, this.filename) | ||
450 | 471 | ||
451 | if (video.isOwned()) return WEBSERVER.URL + path | 472 | if (video.isOwned()) return WEBSERVER.URL + path |
452 | 473 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index af81c9906..245475f94 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -20,7 +20,7 @@ import { | |||
20 | import { setAsUpdated } from '@server/helpers/database-utils' | 20 | import { setAsUpdated } from '@server/helpers/database-utils' |
21 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' | 21 | import { buildUUID, uuidToShort } from '@server/helpers/uuid' |
22 | import { MAccountId, MChannelId } from '@server/types/models' | 22 | import { MAccountId, MChannelId } from '@server/types/models' |
23 | import { AttributesOnly } from '@shared/core-utils' | 23 | import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistLink, buildPlaylistWatchPath } from '@shared/core-utils' |
24 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | 24 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' |
25 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 25 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
26 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 26 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
@@ -560,12 +560,12 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
560 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) | 560 | return join(STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename) |
561 | } | 561 | } |
562 | 562 | ||
563 | getWatchUrl () { | 563 | getWatchStaticPath () { |
564 | return WEBSERVER.URL + '/w/p/' + this.uuid | 564 | return buildPlaylistWatchPath({ shortUUID: uuidToShort(this.uuid) }) |
565 | } | 565 | } |
566 | 566 | ||
567 | getEmbedStaticPath () { | 567 | getEmbedStaticPath () { |
568 | return '/video-playlists/embed/' + this.uuid | 568 | return buildPlaylistEmbedPath(this) |
569 | } | 569 | } |
570 | 570 | ||
571 | static async getStats () { | 571 | static async getStats () { |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index d627e8c9d..b15d20cf9 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -1,19 +1,27 @@ | |||
1 | import * as memoizee from 'memoizee' | 1 | import * as memoizee from 'memoizee' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { Op, QueryTypes } from 'sequelize' | 3 | import { Op } from 'sequelize' |
4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 4 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
5 | import { doesExist } from '@server/helpers/database-utils' | ||
5 | import { VideoFileModel } from '@server/models/video/video-file' | 6 | import { VideoFileModel } from '@server/models/video/video-file' |
6 | import { MStreamingPlaylist } from '@server/types/models' | 7 | import { MStreamingPlaylist, MVideo } from '@server/types/models' |
8 | import { AttributesOnly } from '@shared/core-utils' | ||
7 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 9 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
8 | import { sha1 } from '../../helpers/core-utils' | 10 | import { sha1 } from '../../helpers/core-utils' |
9 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
10 | import { isArrayOf } from '../../helpers/custom-validators/misc' | 12 | import { isArrayOf } from '../../helpers/custom-validators/misc' |
11 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 13 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
12 | import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' | 14 | import { |
15 | CONSTRAINTS_FIELDS, | ||
16 | MEMOIZE_LENGTH, | ||
17 | MEMOIZE_TTL, | ||
18 | P2P_MEDIA_LOADER_PEER_VERSION, | ||
19 | STATIC_PATHS, | ||
20 | WEBSERVER | ||
21 | } from '../../initializers/constants' | ||
13 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 22 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
14 | import { throwIfNotValid } from '../utils' | 23 | import { throwIfNotValid } from '../utils' |
15 | import { VideoModel } from './video' | 24 | import { VideoModel } from './video' |
16 | import { AttributesOnly } from '@shared/core-utils' | ||
17 | 25 | ||
18 | @Table({ | 26 | @Table({ |
19 | tableName: 'videoStreamingPlaylist', | 27 | tableName: 'videoStreamingPlaylist', |
@@ -43,7 +51,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
43 | type: VideoStreamingPlaylistType | 51 | type: VideoStreamingPlaylistType |
44 | 52 | ||
45 | @AllowNull(false) | 53 | @AllowNull(false) |
46 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url')) | 54 | @Column |
55 | playlistFilename: string | ||
56 | |||
57 | @AllowNull(true) | ||
58 | @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true)) | ||
47 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) | 59 | @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) |
48 | playlistUrl: string | 60 | playlistUrl: string |
49 | 61 | ||
@@ -57,7 +69,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
57 | p2pMediaLoaderPeerVersion: number | 69 | p2pMediaLoaderPeerVersion: number |
58 | 70 | ||
59 | @AllowNull(false) | 71 | @AllowNull(false) |
60 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url')) | 72 | @Column |
73 | segmentsSha256Filename: string | ||
74 | |||
75 | @AllowNull(true) | ||
76 | @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true)) | ||
61 | @Column | 77 | @Column |
62 | segmentsSha256Url: string | 78 | segmentsSha256Url: string |
63 | 79 | ||
@@ -98,14 +114,8 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
98 | 114 | ||
99 | static doesInfohashExist (infoHash: string) { | 115 | static doesInfohashExist (infoHash: string) { |
100 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | 116 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' |
101 | const options = { | ||
102 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
103 | bind: { infoHash }, | ||
104 | raw: true | ||
105 | } | ||
106 | 117 | ||
107 | return VideoModel.sequelize.query<object>(query, options) | 118 | return doesExist(query, { infoHash }) |
108 | .then(results => results.length === 1) | ||
109 | } | 119 | } |
110 | 120 | ||
111 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { | 121 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { |
@@ -125,7 +135,13 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
125 | p2pMediaLoaderPeerVersion: { | 135 | p2pMediaLoaderPeerVersion: { |
126 | [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION | 136 | [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION |
127 | } | 137 | } |
128 | } | 138 | }, |
139 | include: [ | ||
140 | { | ||
141 | model: VideoModel.unscoped(), | ||
142 | required: true | ||
143 | } | ||
144 | ] | ||
129 | } | 145 | } |
130 | 146 | ||
131 | return VideoStreamingPlaylistModel.findAll(query) | 147 | return VideoStreamingPlaylistModel.findAll(query) |
@@ -144,7 +160,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
144 | return VideoStreamingPlaylistModel.findByPk(id, options) | 160 | return VideoStreamingPlaylistModel.findByPk(id, options) |
145 | } | 161 | } |
146 | 162 | ||
147 | static loadHLSPlaylistByVideo (videoId: number) { | 163 | static loadHLSPlaylistByVideo (videoId: number): Promise<MStreamingPlaylist> { |
148 | const options = { | 164 | const options = { |
149 | where: { | 165 | where: { |
150 | type: VideoStreamingPlaylistType.HLS, | 166 | type: VideoStreamingPlaylistType.HLS, |
@@ -155,30 +171,29 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
155 | return VideoStreamingPlaylistModel.findOne(options) | 171 | return VideoStreamingPlaylistModel.findOne(options) |
156 | } | 172 | } |
157 | 173 | ||
158 | static getHlsPlaylistFilename (resolution: number) { | 174 | static async loadOrGenerate (video: MVideo) { |
159 | return resolution + '.m3u8' | 175 | let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id) |
160 | } | 176 | if (!playlist) playlist = new VideoStreamingPlaylistModel() |
161 | 177 | ||
162 | static getMasterHlsPlaylistFilename () { | 178 | return Object.assign(playlist, { videoId: video.id, Video: video }) |
163 | return 'master.m3u8' | ||
164 | } | 179 | } |
165 | 180 | ||
166 | static getHlsSha256SegmentsFilename () { | 181 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { |
167 | return 'segments-sha256.json' | 182 | const masterPlaylistUrl = this.getMasterPlaylistUrl(video) |
168 | } | ||
169 | 183 | ||
170 | static getHlsMasterPlaylistStaticPath (videoUUID: string) { | 184 | this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files) |
171 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) | ||
172 | } | 185 | } |
173 | 186 | ||
174 | static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) { | 187 | getMasterPlaylistUrl (video: MVideo) { |
175 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution)) | 188 | if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid) |
189 | |||
190 | return this.playlistUrl | ||
176 | } | 191 | } |
177 | 192 | ||
178 | static getHlsSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { | 193 | getSha256SegmentsUrl (video: MVideo) { |
179 | if (isLive) return join('/live', 'segments-sha256', videoUUID) | 194 | if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive) |
180 | 195 | ||
181 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename()) | 196 | return this.segmentsSha256Url |
182 | } | 197 | } |
183 | 198 | ||
184 | getStringType () { | 199 | getStringType () { |
@@ -195,4 +210,14 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
195 | return this.type === other.type && | 210 | return this.type === other.type && |
196 | this.videoId === other.videoId | 211 | this.videoId === other.videoId |
197 | } | 212 | } |
213 | |||
214 | private getMasterPlaylistStaticPath (videoUUID: string) { | ||
215 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename) | ||
216 | } | ||
217 | |||
218 | private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) { | ||
219 | if (isLive) return join('/live', 'segments-sha256', videoUUID) | ||
220 | |||
221 | return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename) | ||
222 | } | ||
198 | } | 223 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 1e5648a36..543e604bb 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -26,12 +26,13 @@ import { | |||
26 | } from 'sequelize-typescript' | 26 | } from 'sequelize-typescript' |
27 | import { setAsUpdated } from '@server/helpers/database-utils' | 27 | import { setAsUpdated } from '@server/helpers/database-utils' |
28 | import { buildNSFWFilter } from '@server/helpers/express-utils' | 28 | import { buildNSFWFilter } from '@server/helpers/express-utils' |
29 | import { shortToUUID } from '@server/helpers/uuid' | ||
29 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' | 30 | import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' |
30 | import { LiveManager } from '@server/lib/live/live-manager' | 31 | import { LiveManager } from '@server/lib/live/live-manager' |
31 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' | 32 | import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' |
32 | import { getServerActor } from '@server/models/application/application' | 33 | import { getServerActor } from '@server/models/application/application' |
33 | import { ModelCache } from '@server/models/model-cache' | 34 | import { ModelCache } from '@server/models/model-cache' |
34 | import { AttributesOnly } from '@shared/core-utils' | 35 | import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath } from '@shared/core-utils' |
35 | import { VideoFile } from '@shared/models/videos/video-file.model' | 36 | import { VideoFile } from '@shared/models/videos/video-file.model' |
36 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' | 37 | import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' |
37 | import { VideoObject } from '../../../shared/models/activitypub/objects' | 38 | import { VideoObject } from '../../../shared/models/activitypub/objects' |
@@ -762,8 +763,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
762 | 763 | ||
763 | // Remove physical files and torrents | 764 | // Remove physical files and torrents |
764 | instance.VideoFiles.forEach(file => { | 765 | instance.VideoFiles.forEach(file => { |
765 | tasks.push(instance.removeFile(file)) | 766 | tasks.push(instance.removeFileAndTorrent(file)) |
766 | tasks.push(file.removeTorrent()) | ||
767 | }) | 767 | }) |
768 | 768 | ||
769 | // Remove playlists file | 769 | // Remove playlists file |
@@ -1579,11 +1579,11 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1579 | } | 1579 | } |
1580 | 1580 | ||
1581 | getWatchStaticPath () { | 1581 | getWatchStaticPath () { |
1582 | return '/w/' + this.uuid | 1582 | return buildVideoWatchPath({ shortUUID: shortToUUID(this.uuid) }) |
1583 | } | 1583 | } |
1584 | 1584 | ||
1585 | getEmbedStaticPath () { | 1585 | getEmbedStaticPath () { |
1586 | return '/videos/embed/' + this.uuid | 1586 | return buildVideoEmbedPath(this) |
1587 | } | 1587 | } |
1588 | 1588 | ||
1589 | getMiniatureStaticPath () { | 1589 | getMiniatureStaticPath () { |
@@ -1670,10 +1670,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1670 | .concat(toAdd) | 1670 | .concat(toAdd) |
1671 | } | 1671 | } |
1672 | 1672 | ||
1673 | removeFile (videoFile: MVideoFile, isRedundancy = false) { | 1673 | removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) { |
1674 | const filePath = getVideoFilePath(this, videoFile, isRedundancy) | 1674 | const filePath = getVideoFilePath(this, videoFile, isRedundancy) |
1675 | return remove(filePath) | 1675 | |
1676 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1676 | const promises: Promise<any>[] = [ remove(filePath) ] |
1677 | if (!isRedundancy) promises.push(videoFile.removeTorrent()) | ||
1678 | |||
1679 | return Promise.all(promises) | ||
1677 | } | 1680 | } |
1678 | 1681 | ||
1679 | async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { | 1682 | async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { |