aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/video-abuse.ts19
-rw-r--r--server/models/video/video-blacklist.ts21
-rw-r--r--server/models/video/video-channel.ts4
-rw-r--r--server/models/video/video-file.ts26
-rw-r--r--server/models/video/video-format-utils.ts63
-rw-r--r--server/models/video/video-streaming-playlist.ts158
-rw-r--r--server/models/video/video.ts183
7 files changed, 407 insertions, 67 deletions
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 4c9e2d05e..cc47644f2 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -1,17 +1,4 @@
1import { 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2 AfterCreate,
3 AllowNull,
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 Default,
9 ForeignKey,
10 Is,
11 Model,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' 2import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
16import { VideoAbuse } from '../../../shared/models/videos' 3import { VideoAbuse } from '../../../shared/models/videos'
17import { 4import {
@@ -19,7 +6,6 @@ import {
19 isVideoAbuseReasonValid, 6 isVideoAbuseReasonValid,
20 isVideoAbuseStateValid 7 isVideoAbuseStateValid
21} from '../../helpers/custom-validators/video-abuses' 8} from '../../helpers/custom-validators/video-abuses'
22import { Emailer } from '../../lib/emailer'
23import { AccountModel } from '../account/account' 9import { AccountModel } from '../account/account'
24import { getSort, throwIfNotValid } from '../utils' 10import { getSort, throwIfNotValid } from '../utils'
25import { VideoModel } from './video' 11import { VideoModel } from './video'
@@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers'
40export class VideoAbuseModel extends Model<VideoAbuseModel> { 26export class VideoAbuseModel extends Model<VideoAbuseModel> {
41 27
42 @AllowNull(false) 28 @AllowNull(false)
29 @Default(null)
43 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) 30 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
44 @Column 31 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
45 reason: string 32 reason: string
46 33
47 @AllowNull(false) 34 @AllowNull(false)
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 23e992685..3b567e488 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -1,21 +1,7 @@
1import { 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2 AfterCreate,
3 AfterDestroy,
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 ForeignKey,
10 Is,
11 Model,
12 Table,
13 UpdatedAt
14} from 'sequelize-typescript'
15import { getSortOnModel, SortType, throwIfNotValid } from '../utils' 2import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 3import { VideoModel } from './video'
17import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 4import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
18import { Emailer } from '../../lib/emailer'
19import { VideoBlacklist } from '../../../shared/models/videos' 5import { VideoBlacklist } from '../../../shared/models/videos'
20import { CONSTRAINTS_FIELDS } from '../../initializers' 6import { CONSTRAINTS_FIELDS } from '../../initializers'
21 7
@@ -35,6 +21,10 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
35 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max)) 21 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_BLACKLIST.REASON.max))
36 reason: string 22 reason: string
37 23
24 @AllowNull(false)
25 @Column
26 unfederated: boolean
27
38 @CreatedAt 28 @CreatedAt
39 createdAt: Date 29 createdAt: Date
40 30
@@ -93,6 +83,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
93 createdAt: this.createdAt, 83 createdAt: this.createdAt,
94 updatedAt: this.updatedAt, 84 updatedAt: this.updatedAt,
95 reason: this.reason, 85 reason: this.reason,
86 unfederated: this.unfederated,
96 87
97 video: { 88 video: {
98 id: video.id, 89 id: video.id,
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 86bf0461a..5598d80f6 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -470,4 +470,8 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
470 getDisplayName () { 470 getDisplayName () {
471 return this.name 471 return this.name
472 } 472 }
473
474 isOutdated () {
475 return this.Actor.isOutdated()
476 }
473} 477}
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 0fd868cd6..7d1e371b9 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -62,7 +62,7 @@ export class VideoFileModel extends Model<VideoFileModel> {
62 extname: string 62 extname: string
63 63
64 @AllowNull(false) 64 @AllowNull(false)
65 @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash')) 65 @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash'))
66 @Column 66 @Column
67 infoHash: string 67 infoHash: string
68 68
@@ -86,14 +86,14 @@ export class VideoFileModel extends Model<VideoFileModel> {
86 86
87 @HasMany(() => VideoRedundancyModel, { 87 @HasMany(() => VideoRedundancyModel, {
88 foreignKey: { 88 foreignKey: {
89 allowNull: false 89 allowNull: true
90 }, 90 },
91 onDelete: 'CASCADE', 91 onDelete: 'CASCADE',
92 hooks: true 92 hooks: true
93 }) 93 })
94 RedundancyVideos: VideoRedundancyModel[] 94 RedundancyVideos: VideoRedundancyModel[]
95 95
96 static isInfohashExists (infoHash: string) { 96 static doesInfohashExist (infoHash: string) {
97 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' 97 const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
98 const options = { 98 const options = {
99 type: Sequelize.QueryTypes.SELECT, 99 type: Sequelize.QueryTypes.SELECT,
@@ -120,6 +120,26 @@ export class VideoFileModel extends Model<VideoFileModel> {
120 return VideoFileModel.findById(id, options) 120 return VideoFileModel.findById(id, options)
121 } 121 }
122 122
123 static async getStats () {
124 let totalLocalVideoFilesSize = await VideoFileModel.sum('size', {
125 include: [
126 {
127 attributes: [],
128 model: VideoModel.unscoped(),
129 where: {
130 remote: false
131 }
132 }
133 ]
134 } as any)
135 // Sequelize could return null...
136 if (!totalLocalVideoFilesSize) totalLocalVideoFilesSize = 0
137
138 return {
139 totalLocalVideoFilesSize
140 }
141 }
142
123 hasSameUniqueKeysThan (other: VideoFileModel) { 143 hasSameUniqueKeysThan (other: VideoFileModel) {
124 return this.fps === other.fps && 144 return this.fps === other.fps &&
125 this.resolution === other.resolution && 145 this.resolution === other.resolution &&
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 7a9513cbe..c63285e25 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -1,7 +1,12 @@
1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { VideoFileModel } from './video-file' 3import { VideoFileModel } from './video-file'
4import { ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' 4import {
5 ActivityPlaylistInfohashesObject,
6 ActivityPlaylistSegmentHashesObject,
7 ActivityUrlObject,
8 VideoTorrentObject
9} from '../../../shared/models/activitypub/objects'
5import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers' 10import { CONFIG, MIMETYPES, THUMBNAILS_SIZE } from '../../initializers'
6import { VideoCaptionModel } from './video-caption' 11import { VideoCaptionModel } from './video-caption'
7import { 12import {
@@ -11,6 +16,8 @@ import {
11 getVideoSharesActivityPubUrl 16 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 17} from '../../lib/activitypub'
13import { isArray } from '../../helpers/custom-validators/misc' 18import { isArray } from '../../helpers/custom-validators/misc'
19import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
20import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
14 21
15export type VideoFormattingJSONOptions = { 22export type VideoFormattingJSONOptions = {
16 completeDescription?: boolean 23 completeDescription?: boolean
@@ -121,7 +128,12 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
121 } 128 }
122 }) 129 })
123 130
131 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
132
124 const tags = video.Tags ? video.Tags.map(t => t.name) : [] 133 const tags = video.Tags ? video.Tags.map(t => t.name) : []
134
135 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
136
125 const detailsJson = { 137 const detailsJson = {
126 support: video.support, 138 support: video.support,
127 descriptionPath: video.getDescriptionAPIPath(), 139 descriptionPath: video.getDescriptionAPIPath(),
@@ -129,12 +141,17 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
129 account: video.VideoChannel.Account.toFormattedJSON(), 141 account: video.VideoChannel.Account.toFormattedJSON(),
130 tags, 142 tags,
131 commentsEnabled: video.commentsEnabled, 143 commentsEnabled: video.commentsEnabled,
144 downloadEnabled: video.downloadEnabled,
132 waitTranscoding: video.waitTranscoding, 145 waitTranscoding: video.waitTranscoding,
133 state: { 146 state: {
134 id: video.state, 147 id: video.state,
135 label: VideoModel.getStateLabel(video.state) 148 label: VideoModel.getStateLabel(video.state)
136 }, 149 },
137 files: [] 150
151 trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs),
152
153 files: [],
154 streamingPlaylists
138 } 155 }
139 156
140 // Format and sort video files 157 // Format and sort video files
@@ -143,6 +160,25 @@ function videoModelToFormattedDetailsJSON (video: VideoModel): VideoDetails {
143 return Object.assign(formattedJson, detailsJson) 160 return Object.assign(formattedJson, detailsJson)
144} 161}
145 162
163function streamingPlaylistsModelToFormattedJSON (video: VideoModel, playlists: VideoStreamingPlaylistModel[]): VideoStreamingPlaylist[] {
164 if (isArray(playlists) === false) return []
165
166 return playlists
167 .map(playlist => {
168 const redundancies = isArray(playlist.RedundancyVideos)
169 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
170 : []
171
172 return {
173 id: playlist.id,
174 type: playlist.type,
175 playlistUrl: playlist.playlistUrl,
176 segmentsSha256Url: playlist.segmentsSha256Url,
177 redundancies
178 } as VideoStreamingPlaylist
179 })
180}
181
146function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] { 182function videoFilesModelToFormattedJSON (video: VideoModel, videoFiles: VideoFileModel[]): VideoFile[] {
147 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 183 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
148 184
@@ -233,6 +269,28 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
233 }) 269 })
234 } 270 }
235 271
272 for (const playlist of (video.VideoStreamingPlaylists || [])) {
273 let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[]
274
275 tag = playlist.p2pMediaLoaderInfohashes
276 .map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
277 tag.push({
278 type: 'Link',
279 name: 'sha256',
280 mimeType: 'application/json' as 'application/json',
281 mediaType: 'application/json' as 'application/json',
282 href: playlist.segmentsSha256Url
283 })
284
285 url.push({
286 type: 'Link',
287 mimeType: 'application/x-mpegURL' as 'application/x-mpegURL',
288 mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
289 href: playlist.playlistUrl,
290 tag
291 })
292 }
293
236 // Add video url too 294 // Add video url too
237 url.push({ 295 url.push({
238 type: 'Link', 296 type: 'Link',
@@ -264,6 +322,7 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject {
264 waitTranscoding: video.waitTranscoding, 322 waitTranscoding: video.waitTranscoding,
265 state: video.state, 323 state: video.state,
266 commentsEnabled: video.commentsEnabled, 324 commentsEnabled: video.commentsEnabled,
325 downloadEnabled: video.downloadEnabled,
267 published: video.publishedAt.toISOString(), 326 published: video.publishedAt.toISOString(),
268 originallyPublishedAt: video.originallyPublishedAt ? 327 originallyPublishedAt: video.originallyPublishedAt ?
269 video.originallyPublishedAt.toISOString() : 328 video.originallyPublishedAt.toISOString() :
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
new file mode 100644
index 000000000..bf6f7b0c4
--- /dev/null
+++ b/server/models/video/video-streaming-playlist.ts
@@ -0,0 +1,158 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
2import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
3import { throwIfNotValid } from '../utils'
4import { VideoModel } from './video'
5import * as Sequelize from 'sequelize'
6import { VideoRedundancyModel } from '../redundancy/video-redundancy'
7import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
8import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
9import { CONSTRAINTS_FIELDS, STATIC_PATHS } from '../../initializers'
10import { VideoFileModel } from './video-file'
11import { join } from 'path'
12import { sha1 } from '../../helpers/core-utils'
13import { isArrayOf } from '../../helpers/custom-validators/misc'
14
15@Table({
16 tableName: 'videoStreamingPlaylist',
17 indexes: [
18 {
19 fields: [ 'videoId' ]
20 },
21 {
22 fields: [ 'videoId', 'type' ],
23 unique: true
24 },
25 {
26 fields: [ 'p2pMediaLoaderInfohashes' ],
27 using: 'gin'
28 }
29 ]
30})
31export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> {
32 @CreatedAt
33 createdAt: Date
34
35 @UpdatedAt
36 updatedAt: Date
37
38 @AllowNull(false)
39 @Column
40 type: VideoStreamingPlaylistType
41
42 @AllowNull(false)
43 @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url'))
44 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
45 playlistUrl: string
46
47 @AllowNull(false)
48 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
49 @Column(DataType.ARRAY(DataType.STRING))
50 p2pMediaLoaderInfohashes: string[]
51
52 @AllowNull(false)
53 @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url'))
54 @Column
55 segmentsSha256Url: string
56
57 @ForeignKey(() => VideoModel)
58 @Column
59 videoId: number
60
61 @BelongsTo(() => VideoModel, {
62 foreignKey: {
63 allowNull: false
64 },
65 onDelete: 'CASCADE'
66 })
67 Video: VideoModel
68
69 @HasMany(() => VideoRedundancyModel, {
70 foreignKey: {
71 allowNull: false
72 },
73 onDelete: 'CASCADE',
74 hooks: true
75 })
76 RedundancyVideos: VideoRedundancyModel[]
77
78 static doesInfohashExist (infoHash: string) {
79 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
80 const options = {
81 type: Sequelize.QueryTypes.SELECT,
82 bind: { infoHash },
83 raw: true
84 }
85
86 return VideoModel.sequelize.query(query, options)
87 .then(results => {
88 return results.length === 1
89 })
90 }
91
92 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: VideoFileModel[]) {
93 const hashes: string[] = []
94
95 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L97
96 for (let i = 0; i < videoFiles.length; i++) {
97 hashes.push(sha1(`1${playlistUrl}+V${i}`))
98 }
99
100 return hashes
101 }
102
103 static loadWithVideo (id: number) {
104 const options = {
105 include: [
106 {
107 model: VideoModel.unscoped(),
108 required: true
109 }
110 ]
111 }
112
113 return VideoStreamingPlaylistModel.findById(id, options)
114 }
115
116 static getHlsPlaylistFilename (resolution: number) {
117 return resolution + '.m3u8'
118 }
119
120 static getMasterHlsPlaylistFilename () {
121 return 'master.m3u8'
122 }
123
124 static getHlsSha256SegmentsFilename () {
125 return 'segments-sha256.json'
126 }
127
128 static getHlsVideoName (uuid: string, resolution: number) {
129 return `${uuid}-${resolution}-fragmented.mp4`
130 }
131
132 static getHlsMasterPlaylistStaticPath (videoUUID: string) {
133 return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
134 }
135
136 static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) {
137 return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
138 }
139
140 static getHlsSha256SegmentsStaticPath (videoUUID: string) {
141 return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
142 }
143
144 getStringType () {
145 if (this.type === VideoStreamingPlaylistType.HLS) return 'hls'
146
147 return 'unknown'
148 }
149
150 getVideoRedundancyUrl (baseUrlHttp: string) {
151 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
152 }
153
154 hasSameUniqueKeysThan (other: VideoStreamingPlaylistModel) {
155 return this.type === other.type &&
156 this.videoId === other.videoId
157 }
158}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 806b6e046..73626b6a0 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -52,7 +52,7 @@ import {
52 ACTIVITY_PUB, 52 ACTIVITY_PUB,
53 API_VERSION, 53 API_VERSION,
54 CONFIG, 54 CONFIG,
55 CONSTRAINTS_FIELDS, 55 CONSTRAINTS_FIELDS, HLS_PLAYLIST_DIRECTORY, HLS_REDUNDANCY_DIRECTORY,
56 PREVIEWS_SIZE, 56 PREVIEWS_SIZE,
57 REMOTE_SCHEME, 57 REMOTE_SCHEME,
58 STATIC_DOWNLOAD_PATHS, 58 STATIC_DOWNLOAD_PATHS,
@@ -95,6 +95,7 @@ import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 96import { UserModel } from '../account/user'
97import { VideoImportModel } from './video-import' 97import { VideoImportModel } from './video-import'
98import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
98 99
99// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 100// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
100const indexes: Sequelize.DefineIndexesOptions[] = [ 101const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -160,7 +161,9 @@ export enum ScopeNames {
160 WITH_FILES = 'WITH_FILES', 161 WITH_FILES = 'WITH_FILES',
161 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 162 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
162 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 163 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
163 WITH_USER_HISTORY = 'WITH_USER_HISTORY' 164 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
165 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
166 WITH_USER_ID = 'WITH_USER_ID'
164} 167}
165 168
166type ForAPIOptions = { 169type ForAPIOptions = {
@@ -464,6 +467,22 @@ type AvailableForListIDsOptions = {
464 467
465 return query 468 return query
466 }, 469 },
470 [ ScopeNames.WITH_USER_ID ]: {
471 include: [
472 {
473 attributes: [ 'accountId' ],
474 model: () => VideoChannelModel.unscoped(),
475 required: true,
476 include: [
477 {
478 attributes: [ 'userId' ],
479 model: () => AccountModel.unscoped(),
480 required: true
481 }
482 ]
483 }
484 ]
485 },
467 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 486 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
468 include: [ 487 include: [
469 { 488 {
@@ -528,22 +547,55 @@ type AvailableForListIDsOptions = {
528 } 547 }
529 ] 548 ]
530 }, 549 },
531 [ ScopeNames.WITH_FILES ]: { 550 [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => {
532 include: [ 551 let subInclude: any[] = []
533 { 552
534 model: () => VideoFileModel.unscoped(), 553 if (withRedundancies === true) {
535 // FIXME: typings 554 subInclude = [
536 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join 555 {
537 required: false, 556 attributes: [ 'fileUrl' ],
538 include: [ 557 model: VideoRedundancyModel.unscoped(),
539 { 558 required: false
540 attributes: [ 'fileUrl' ], 559 }
541 model: () => VideoRedundancyModel.unscoped(), 560 ]
542 required: false 561 }
543 } 562
544 ] 563 return {
545 } 564 include: [
546 ] 565 {
566 model: VideoFileModel.unscoped(),
567 // FIXME: typings
568 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
569 required: false,
570 include: subInclude
571 }
572 ]
573 }
574 },
575 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => {
576 let subInclude: any[] = []
577
578 if (withRedundancies === true) {
579 subInclude = [
580 {
581 attributes: [ 'fileUrl' ],
582 model: VideoRedundancyModel.unscoped(),
583 required: false
584 }
585 ]
586 }
587
588 return {
589 include: [
590 {
591 model: VideoStreamingPlaylistModel.unscoped(),
592 // FIXME: typings
593 [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
594 required: false,
595 include: subInclude
596 }
597 ]
598 }
547 }, 599 },
548 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 600 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
549 include: [ 601 include: [
@@ -666,6 +718,10 @@ export class VideoModel extends Model<VideoModel> {
666 718
667 @AllowNull(false) 719 @AllowNull(false)
668 @Column 720 @Column
721 downloadEnabled: boolean
722
723 @AllowNull(false)
724 @Column
669 waitTranscoding: boolean 725 waitTranscoding: boolean
670 726
671 @AllowNull(false) 727 @AllowNull(false)
@@ -726,6 +782,16 @@ export class VideoModel extends Model<VideoModel> {
726 }) 782 })
727 VideoFiles: VideoFileModel[] 783 VideoFiles: VideoFileModel[]
728 784
785 @HasMany(() => VideoStreamingPlaylistModel, {
786 foreignKey: {
787 name: 'videoId',
788 allowNull: false
789 },
790 hooks: true,
791 onDelete: 'cascade'
792 })
793 VideoStreamingPlaylists: VideoStreamingPlaylistModel[]
794
729 @HasMany(() => VideoShareModel, { 795 @HasMany(() => VideoShareModel, {
730 foreignKey: { 796 foreignKey: {
731 name: 'videoId', 797 name: 'videoId',
@@ -851,6 +917,9 @@ export class VideoModel extends Model<VideoModel> {
851 tasks.push(instance.removeFile(file)) 917 tasks.push(instance.removeFile(file))
852 tasks.push(instance.removeTorrent(file)) 918 tasks.push(instance.removeTorrent(file))
853 }) 919 })
920
921 // Remove playlists file
922 tasks.push(instance.removeStreamingPlaylist())
854 } 923 }
855 924
856 // Do not wait video deletion because we could be in a transaction 925 // Do not wait video deletion because we could be in a transaction
@@ -862,10 +931,6 @@ export class VideoModel extends Model<VideoModel> {
862 return undefined 931 return undefined
863 } 932 }
864 933
865 static list () {
866 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
867 }
868
869 static listLocal () { 934 static listLocal () {
870 const query = { 935 const query = {
871 where: { 936 where: {
@@ -873,7 +938,7 @@ export class VideoModel extends Model<VideoModel> {
873 } 938 }
874 } 939 }
875 940
876 return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query) 941 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query)
877 } 942 }
878 943
879 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 944 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
@@ -1204,6 +1269,16 @@ export class VideoModel extends Model<VideoModel> {
1204 return VideoModel.findOne(options) 1269 return VideoModel.findOne(options)
1205 } 1270 }
1206 1271
1272 static loadWithRights (id: number | string, t?: Sequelize.Transaction) {
1273 const where = VideoModel.buildWhereIdOrUUID(id)
1274 const options = {
1275 where,
1276 transaction: t
1277 }
1278
1279 return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options)
1280 }
1281
1207 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { 1282 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) {
1208 const where = VideoModel.buildWhereIdOrUUID(id) 1283 const where = VideoModel.buildWhereIdOrUUID(id)
1209 1284
@@ -1216,8 +1291,8 @@ export class VideoModel extends Model<VideoModel> {
1216 return VideoModel.findOne(options) 1291 return VideoModel.findOne(options)
1217 } 1292 }
1218 1293
1219 static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { 1294 static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) {
1220 return VideoModel.scope(ScopeNames.WITH_FILES) 1295 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ])
1221 .findById(id, { transaction: t, logging }) 1296 .findById(id, { transaction: t, logging })
1222 } 1297 }
1223 1298
@@ -1228,9 +1303,7 @@ export class VideoModel extends Model<VideoModel> {
1228 } 1303 }
1229 } 1304 }
1230 1305
1231 return VideoModel 1306 return VideoModel.findOne(options)
1232 .scope([ ScopeNames.WITH_FILES ])
1233 .findOne(options)
1234 } 1307 }
1235 1308
1236 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 1309 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
@@ -1252,7 +1325,11 @@ export class VideoModel extends Model<VideoModel> {
1252 transaction 1325 transaction
1253 } 1326 }
1254 1327
1255 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1328 return VideoModel.scope([
1329 ScopeNames.WITH_ACCOUNT_DETAILS,
1330 ScopeNames.WITH_FILES,
1331 ScopeNames.WITH_STREAMING_PLAYLISTS
1332 ]).findOne(query)
1256 } 1333 }
1257 1334
1258 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { 1335 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
@@ -1267,9 +1344,37 @@ export class VideoModel extends Model<VideoModel> {
1267 const scopes = [ 1344 const scopes = [
1268 ScopeNames.WITH_TAGS, 1345 ScopeNames.WITH_TAGS,
1269 ScopeNames.WITH_BLACKLISTED, 1346 ScopeNames.WITH_BLACKLISTED,
1347 ScopeNames.WITH_ACCOUNT_DETAILS,
1348 ScopeNames.WITH_SCHEDULED_UPDATE,
1270 ScopeNames.WITH_FILES, 1349 ScopeNames.WITH_FILES,
1350 ScopeNames.WITH_STREAMING_PLAYLISTS
1351 ]
1352
1353 if (userId) {
1354 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1355 }
1356
1357 return VideoModel
1358 .scope(scopes)
1359 .findOne(options)
1360 }
1361
1362 static loadForGetAPI (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1363 const where = VideoModel.buildWhereIdOrUUID(id)
1364
1365 const options = {
1366 order: [ [ 'Tags', 'name', 'ASC' ] ],
1367 where,
1368 transaction: t
1369 }
1370
1371 const scopes = [
1372 ScopeNames.WITH_TAGS,
1373 ScopeNames.WITH_BLACKLISTED,
1271 ScopeNames.WITH_ACCOUNT_DETAILS, 1374 ScopeNames.WITH_ACCOUNT_DETAILS,
1272 ScopeNames.WITH_SCHEDULED_UPDATE 1375 ScopeNames.WITH_SCHEDULED_UPDATE,
1376 { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
1377 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
1273 ] 1378 ]
1274 1379
1275 if (userId) { 1380 if (userId) {
@@ -1616,6 +1721,14 @@ export class VideoModel extends Model<VideoModel> {
1616 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 1721 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1617 } 1722 }
1618 1723
1724 removeStreamingPlaylist (isRedundancy = false) {
1725 const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_PLAYLIST_DIRECTORY
1726
1727 const filePath = join(baseDir, this.uuid)
1728 return remove(filePath)
1729 .catch(err => logger.warn('Cannot delete playlist directory %s.', filePath, { err }))
1730 }
1731
1619 isOutdated () { 1732 isOutdated () {
1620 if (this.isOwned()) return false 1733 if (this.isOwned()) return false
1621 1734
@@ -1650,7 +1763,7 @@ export class VideoModel extends Model<VideoModel> {
1650 1763
1651 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { 1764 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
1652 const xs = this.getTorrentUrl(videoFile, baseUrlHttp) 1765 const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
1653 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 1766 const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
1654 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] 1767 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
1655 1768
1656 const redundancies = videoFile.RedundancyVideos 1769 const redundancies = videoFile.RedundancyVideos
@@ -1667,6 +1780,10 @@ export class VideoModel extends Model<VideoModel> {
1667 return magnetUtil.encode(magnetHash) 1780 return magnetUtil.encode(magnetHash)
1668 } 1781 }
1669 1782
1783 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1784 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1785 }
1786
1670 getThumbnailUrl (baseUrlHttp: string) { 1787 getThumbnailUrl (baseUrlHttp: string) {
1671 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() 1788 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
1672 } 1789 }
@@ -1690,4 +1807,8 @@ export class VideoModel extends Model<VideoModel> {
1690 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1807 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1691 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 1808 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
1692 } 1809 }
1810
1811 getBandwidthBits (videoFile: VideoFileModel) {
1812 return Math.ceil((videoFile.size * 8) / this.duration)
1813 }
1693} 1814}