aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/actor/actor-follow.ts36
-rw-r--r--server/models/redundancy/video-redundancy.ts4
-rw-r--r--server/models/video/formatter/video-format-utils.ts8
-rw-r--r--server/models/video/sql/shared/video-tables.ts3
-rw-r--r--server/models/video/sql/videos-id-list-query-builder.ts6
-rw-r--r--server/models/video/video-file.ts45
-rw-r--r--server/models/video/video-playlist.ts8
-rw-r--r--server/models/video/video-streaming-playlist.ts85
-rw-r--r--server/models/video/video.ts19
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'
21import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc' 21import { isActivityPubUrlValid } from '@server/helpers/custom-validators/activitypub/misc'
22import { doesExist } from '@server/helpers/database-utils'
22import { getServerActor } from '@server/models/application/application' 23import { getServerActor } from '@server/models/application/application'
23import { VideoModel } from '@server/models/video/video'
24import { 24import {
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 @@
1import { remove } from 'fs-extra' 1import { remove } from 'fs-extra'
2import * as memoizee from 'memoizee' 2import * as memoizee from 'memoizee'
3import { join } from 'path' 3import { join } from 'path'
4import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' 4import { FindOptions, Op, Transaction } from 'sequelize'
5import { 5import {
6 AllowNull, 6 AllowNull,
7 BelongsTo, 7 BelongsTo,
@@ -21,6 +21,7 @@ import {
21import { Where } from 'sequelize/types/lib/utils' 21import { Where } from 'sequelize/types/lib/utils'
22import validator from 'validator' 22import validator from 'validator'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24import { doesExist } from '@server/helpers/database-utils'
24import { logger } from '@server/helpers/logger' 25import { logger } from '@server/helpers/logger'
25import { extractVideo } from '@server/helpers/video' 26import { extractVideo } from '@server/helpers/video'
26import { getTorrentFilePath } from '@server/lib/video-paths' 27import { 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 {
20import { setAsUpdated } from '@server/helpers/database-utils' 20import { setAsUpdated } from '@server/helpers/database-utils'
21import { buildUUID, uuidToShort } from '@server/helpers/uuid' 21import { buildUUID, uuidToShort } from '@server/helpers/uuid'
22import { MAccountId, MChannelId } from '@server/types/models' 22import { MAccountId, MChannelId } from '@server/types/models'
23import { AttributesOnly } from '@shared/core-utils' 23import { AttributesOnly, buildPlaylistEmbedPath, buildPlaylistLink, buildPlaylistWatchPath } from '@shared/core-utils'
24import { ActivityIconObject } from '../../../shared/models/activitypub/objects' 24import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
25import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' 25import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
26import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' 26import { 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 @@
1import * as memoizee from 'memoizee' 1import * as memoizee from 'memoizee'
2import { join } from 'path' 2import { join } from 'path'
3import { Op, QueryTypes } from 'sequelize' 3import { Op } from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
5import { doesExist } from '@server/helpers/database-utils'
5import { VideoFileModel } from '@server/models/video/video-file' 6import { VideoFileModel } from '@server/models/video/video-file'
6import { MStreamingPlaylist } from '@server/types/models' 7import { MStreamingPlaylist, MVideo } from '@server/types/models'
8import { AttributesOnly } from '@shared/core-utils'
7import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 9import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
8import { sha1 } from '../../helpers/core-utils' 10import { sha1 } from '../../helpers/core-utils'
9import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 11import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
10import { isArrayOf } from '../../helpers/custom-validators/misc' 12import { isArrayOf } from '../../helpers/custom-validators/misc'
11import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' 13import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
12import { CONSTRAINTS_FIELDS, MEMOIZE_LENGTH, MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' 14import {
15 CONSTRAINTS_FIELDS,
16 MEMOIZE_LENGTH,
17 MEMOIZE_TTL,
18 P2P_MEDIA_LOADER_PEER_VERSION,
19 STATIC_PATHS,
20 WEBSERVER
21} from '../../initializers/constants'
13import { VideoRedundancyModel } from '../redundancy/video-redundancy' 22import { VideoRedundancyModel } from '../redundancy/video-redundancy'
14import { throwIfNotValid } from '../utils' 23import { throwIfNotValid } from '../utils'
15import { VideoModel } from './video' 24import { VideoModel } from './video'
16import { 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'
27import { setAsUpdated } from '@server/helpers/database-utils' 27import { setAsUpdated } from '@server/helpers/database-utils'
28import { buildNSFWFilter } from '@server/helpers/express-utils' 28import { buildNSFWFilter } from '@server/helpers/express-utils'
29import { shortToUUID } from '@server/helpers/uuid'
29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 30import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
30import { LiveManager } from '@server/lib/live/live-manager' 31import { LiveManager } from '@server/lib/live/live-manager'
31import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' 32import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths'
32import { getServerActor } from '@server/models/application/application' 33import { getServerActor } from '@server/models/application/application'
33import { ModelCache } from '@server/models/model-cache' 34import { ModelCache } from '@server/models/model-cache'
34import { AttributesOnly } from '@shared/core-utils' 35import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath } from '@shared/core-utils'
35import { VideoFile } from '@shared/models/videos/video-file.model' 36import { VideoFile } from '@shared/models/videos/video-file.model'
36import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' 37import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
37import { VideoObject } from '../../../shared/models/activitypub/objects' 38import { 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) {