aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/redundancy/video-redundancy.ts2
-rw-r--r--server/models/utils.ts2
-rw-r--r--server/models/video/schedule-video-update.ts10
-rw-r--r--server/models/video/video-change-ownership.ts6
-rw-r--r--server/models/video/video-file.ts87
-rw-r--r--server/models/video/video-format-utils.ts127
-rw-r--r--server/models/video/video-streaming-playlist.ts40
-rw-r--r--server/models/video/video.ts204
8 files changed, 296 insertions, 182 deletions
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts
index 61d9a5612..77f83d8aa 100644
--- a/server/models/redundancy/video-redundancy.ts
+++ b/server/models/redundancy/video-redundancy.ts
@@ -497,7 +497,6 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
497 expires: this.expiresOn.toISOString(), 497 expires: this.expiresOn.toISOString(),
498 url: { 498 url: {
499 type: 'Link', 499 type: 'Link',
500 mimeType: 'application/x-mpegURL',
501 mediaType: 'application/x-mpegURL', 500 mediaType: 'application/x-mpegURL',
502 href: this.fileUrl 501 href: this.fileUrl
503 } 502 }
@@ -511,7 +510,6 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
511 expires: this.expiresOn.toISOString(), 510 expires: this.expiresOn.toISOString(),
512 url: { 511 url: {
513 type: 'Link', 512 type: 'Link',
514 mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
515 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any, 513 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ this.VideoFile.extname ] as any,
516 href: this.fileUrl, 514 href: this.fileUrl,
517 height: this.VideoFile.resolution, 515 height: this.VideoFile.resolution,
diff --git a/server/models/utils.ts b/server/models/utils.ts
index e7e6ddde1..ccdbcd1cf 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -1,7 +1,7 @@
1import { Model, Sequelize } from 'sequelize-typescript' 1import { Model, Sequelize } from 'sequelize-typescript'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { Col } from 'sequelize/types/lib/utils' 3import { Col } from 'sequelize/types/lib/utils'
4import { col, literal, OrderItem } from 'sequelize' 4import { literal, OrderItem } from 'sequelize'
5 5
6type SortType = { sortModel: string, sortValue: string } 6type SortType = { sortModel: string, sortValue: string }
7 7
diff --git a/server/models/video/schedule-video-update.ts b/server/models/video/schedule-video-update.ts
index fc2a424aa..eefc10f14 100644
--- a/server/models/video/schedule-video-update.ts
+++ b/server/models/video/schedule-video-update.ts
@@ -2,7 +2,7 @@ import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Ta
2import { ScopeNames as VideoScopeNames, VideoModel } from './video' 2import { ScopeNames as VideoScopeNames, VideoModel } from './video'
3import { VideoPrivacy } from '../../../shared/models/videos' 3import { VideoPrivacy } from '../../../shared/models/videos'
4import { Op, Transaction } from 'sequelize' 4import { Op, Transaction } from 'sequelize'
5import { MScheduleVideoUpdateFormattable } from '@server/typings/models' 5import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/typings/models'
6 6
7@Table({ 7@Table({
8 tableName: 'scheduleVideoUpdate', 8 tableName: 'scheduleVideoUpdate',
@@ -72,10 +72,12 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
72 { 72 {
73 model: VideoModel.scope( 73 model: VideoModel.scope(
74 [ 74 [
75 VideoScopeNames.WITH_FILES, 75 VideoScopeNames.WITH_WEBTORRENT_FILES,
76 VideoScopeNames.WITH_STREAMING_PLAYLISTS,
76 VideoScopeNames.WITH_ACCOUNT_DETAILS, 77 VideoScopeNames.WITH_ACCOUNT_DETAILS,
77 VideoScopeNames.WITH_BLACKLISTED, 78 VideoScopeNames.WITH_BLACKLISTED,
78 VideoScopeNames.WITH_THUMBNAILS 79 VideoScopeNames.WITH_THUMBNAILS,
80 VideoScopeNames.WITH_TAGS
79 ] 81 ]
80 ) 82 )
81 } 83 }
@@ -83,7 +85,7 @@ export class ScheduleVideoUpdateModel extends Model<ScheduleVideoUpdateModel> {
83 transaction: t 85 transaction: t
84 } 86 }
85 87
86 return ScheduleVideoUpdateModel.findAll(query) 88 return ScheduleVideoUpdateModel.findAll<MScheduleVideoUpdateVideoAll>(query)
87 } 89 }
88 90
89 static deleteByVideoId (videoId: number, t: Transaction) { 91 static deleteByVideoId (videoId: number, t: Transaction) {
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts
index f7a351329..3259b6c02 100644
--- a/server/models/video/video-change-ownership.ts
+++ b/server/models/video/video-change-ownership.ts
@@ -43,7 +43,11 @@ enum ScopeNames {
43 [ScopeNames.WITH_VIDEO]: { 43 [ScopeNames.WITH_VIDEO]: {
44 include: [ 44 include: [
45 { 45 {
46 model: VideoModel.scope([ VideoScopeNames.WITH_THUMBNAILS, VideoScopeNames.WITH_FILES ]), 46 model: VideoModel.scope([
47 VideoScopeNames.WITH_THUMBNAILS,
48 VideoScopeNames.WITH_WEBTORRENT_FILES,
49 VideoScopeNames.WITH_STREAMING_PLAYLISTS
50 ]),
47 required: true 51 required: true
48 } 52 }
49 ] 53 ]
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 68e2d562a..cacef0106 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -23,22 +23,52 @@ import { parseAggregateResult, throwIfNotValid } from '../utils'
23import { VideoModel } from './video' 23import { VideoModel } from './video'
24import { VideoRedundancyModel } from '../redundancy/video-redundancy' 24import { VideoRedundancyModel } from '../redundancy/video-redundancy'
25import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 25import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
26import { FindOptions, QueryTypes, Transaction } from 'sequelize' 26import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
27import { MIMETYPES } from '../../initializers/constants' 27import { MIMETYPES } from '../../initializers/constants'
28import { MVideoFile } from '@server/typings/models' 28import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
29import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
29 30
30@Table({ 31@Table({
31 tableName: 'videoFile', 32 tableName: 'videoFile',
32 indexes: [ 33 indexes: [
33 { 34 {
34 fields: [ 'videoId' ] 35 fields: [ 'videoId' ],
36 where: {
37 videoId: {
38 [Op.ne]: null
39 }
40 }
41 },
42 {
43 fields: [ 'videoStreamingPlaylistId' ],
44 where: {
45 videoStreamingPlaylistId: {
46 [Op.ne]: null
47 }
48 }
35 }, 49 },
50
36 { 51 {
37 fields: [ 'infoHash' ] 52 fields: [ 'infoHash' ]
38 }, 53 },
54
39 { 55 {
40 fields: [ 'videoId', 'resolution', 'fps' ], 56 fields: [ 'videoId', 'resolution', 'fps' ],
41 unique: true 57 unique: true,
58 where: {
59 videoId: {
60 [Op.ne]: null
61 }
62 }
63 },
64 {
65 fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
66 unique: true,
67 where: {
68 videoStreamingPlaylistId: {
69 [Op.ne]: null
70 }
71 }
42 } 72 }
43 ] 73 ]
44}) 74})
@@ -81,12 +111,24 @@ export class VideoFileModel extends Model<VideoFileModel> {
81 111
82 @BelongsTo(() => VideoModel, { 112 @BelongsTo(() => VideoModel, {
83 foreignKey: { 113 foreignKey: {
84 allowNull: false 114 allowNull: true
85 }, 115 },
86 onDelete: 'CASCADE' 116 onDelete: 'CASCADE'
87 }) 117 })
88 Video: VideoModel 118 Video: VideoModel
89 119
120 @ForeignKey(() => VideoStreamingPlaylistModel)
121 @Column
122 videoStreamingPlaylistId: number
123
124 @BelongsTo(() => VideoStreamingPlaylistModel, {
125 foreignKey: {
126 allowNull: true
127 },
128 onDelete: 'CASCADE'
129 })
130 VideoStreamingPlaylist: VideoStreamingPlaylistModel
131
90 @HasMany(() => VideoRedundancyModel, { 132 @HasMany(() => VideoRedundancyModel, {
91 foreignKey: { 133 foreignKey: {
92 allowNull: true 134 allowNull: true
@@ -163,6 +205,36 @@ export class VideoFileModel extends Model<VideoFileModel> {
163 })) 205 }))
164 } 206 }
165 207
208 // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
209 static async customUpsert (
210 videoFile: MVideoFile,
211 mode: 'streaming-playlist' | 'video',
212 transaction: Transaction
213 ) {
214 const baseWhere = {
215 fps: videoFile.fps,
216 resolution: videoFile.resolution
217 }
218
219 if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
220 else Object.assign(baseWhere, { videoId: videoFile.videoId })
221
222 const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
223 if (!element) return videoFile.save({ transaction })
224
225 for (const k of Object.keys(videoFile.toJSON())) {
226 element[k] = videoFile[k]
227 }
228
229 return element.save({ transaction })
230 }
231
232 getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
233 if (this.videoId) return (this as MVideoFileVideo).Video
234
235 return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
236 }
237
166 isAudio () { 238 isAudio () {
167 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname] 239 return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
168 } 240 }
@@ -170,6 +242,9 @@ export class VideoFileModel extends Model<VideoFileModel> {
170 hasSameUniqueKeysThan (other: MVideoFile) { 242 hasSameUniqueKeysThan (other: MVideoFile) {
171 return this.fps === other.fps && 243 return this.fps === other.fps &&
172 this.resolution === other.resolution && 244 this.resolution === other.resolution &&
173 this.videoId === other.videoId 245 (
246 (this.videoId !== null && this.videoId === other.videoId) ||
247 (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
248 )
174 } 249 }
175} 250}
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 2987aa780..9fed2d49d 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -1,11 +1,6 @@
1import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 1import { Video, VideoDetails } from '../../../shared/models/videos'
2import { VideoModel } from './video' 2import { VideoModel } from './video'
3import { 3import { ActivityTagObject, ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects'
4 ActivityPlaylistInfohashesObject,
5 ActivityPlaylistSegmentHashesObject,
6 ActivityUrlObject,
7 VideoTorrentObject
8} from '../../../shared/models/activitypub/objects'
9import { MIMETYPES, WEBSERVER } from '../../initializers/constants' 4import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
10import { VideoCaptionModel } from './video-caption' 5import { VideoCaptionModel } from './video-caption'
11import { 6import {
@@ -16,9 +11,18 @@ import {
16} from '../../lib/activitypub' 11} from '../../lib/activitypub'
17import { isArray } from '../../helpers/custom-validators/misc' 12import { isArray } from '../../helpers/custom-validators/misc'
18import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' 13import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model'
19import { MStreamingPlaylistRedundanciesOpt, MVideo, MVideoAP, MVideoFormattable, MVideoFormattableDetails } from '../../typings/models' 14import {
20import { MStreamingPlaylistRedundancies } from '../../typings/models/video/video-streaming-playlist' 15 MStreamingPlaylistRedundanciesOpt,
16 MStreamingPlaylistVideo,
17 MVideo,
18 MVideoAP,
19 MVideoFile,
20 MVideoFormattable,
21 MVideoFormattableDetails
22} from '../../typings/models'
21import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' 23import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
24import { VideoFile } from '@shared/models/videos/video-file.model'
25import { generateMagnetUri } from '@server/helpers/webtorrent'
22 26
23export type VideoFormattingJSONOptions = { 27export type VideoFormattingJSONOptions = {
24 completeDescription?: boolean 28 completeDescription?: boolean
@@ -115,7 +119,7 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
115 119
116 const tags = video.Tags ? video.Tags.map(t => t.name) : [] 120 const tags = video.Tags ? video.Tags.map(t => t.name) : []
117 121
118 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video.VideoStreamingPlaylists) 122 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
119 123
120 const detailsJson = { 124 const detailsJson = {
121 support: video.support, 125 support: video.support,
@@ -138,33 +142,43 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
138 } 142 }
139 143
140 // Format and sort video files 144 // Format and sort video files
141 detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles) 145 detailsJson.files = videoFilesModelToFormattedJSON(video, baseUrlHttp, baseUrlWs, video.VideoFiles)
142 146
143 return Object.assign(formattedJson, detailsJson) 147 return Object.assign(formattedJson, detailsJson)
144} 148}
145 149
146function streamingPlaylistsModelToFormattedJSON (playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] { 150function streamingPlaylistsModelToFormattedJSON (video: MVideo, playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] {
147 if (isArray(playlists) === false) return [] 151 if (isArray(playlists) === false) return []
148 152
153 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
154
149 return playlists 155 return playlists
150 .map(playlist => { 156 .map(playlist => {
157 const playlistWithVideo = Object.assign(playlist, { Video: video })
158
151 const redundancies = isArray(playlist.RedundancyVideos) 159 const redundancies = isArray(playlist.RedundancyVideos)
152 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl })) 160 ? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
153 : [] 161 : []
154 162
163 const files = videoFilesModelToFormattedJSON(playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
164
155 return { 165 return {
156 id: playlist.id, 166 id: playlist.id,
157 type: playlist.type, 167 type: playlist.type,
158 playlistUrl: playlist.playlistUrl, 168 playlistUrl: playlist.playlistUrl,
159 segmentsSha256Url: playlist.segmentsSha256Url, 169 segmentsSha256Url: playlist.segmentsSha256Url,
160 redundancies 170 redundancies,
161 } as VideoStreamingPlaylist 171 files
172 }
162 }) 173 })
163} 174}
164 175
165function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRedundanciesOpt[]): VideoFile[] { 176function videoFilesModelToFormattedJSON (
166 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 177 model: MVideo | MStreamingPlaylistVideo,
167 178 baseUrlHttp: string,
179 baseUrlWs: string,
180 videoFiles: MVideoFileRedundanciesOpt[]
181): VideoFile[] {
168 return videoFiles 182 return videoFiles
169 .map(videoFile => { 183 .map(videoFile => {
170 let resolutionLabel = videoFile.resolution + 'p' 184 let resolutionLabel = videoFile.resolution + 'p'
@@ -174,13 +188,13 @@ function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRe
174 id: videoFile.resolution, 188 id: videoFile.resolution,
175 label: resolutionLabel 189 label: resolutionLabel
176 }, 190 },
177 magnetUri: video.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), 191 magnetUri: generateMagnetUri(model, videoFile, baseUrlHttp, baseUrlWs),
178 size: videoFile.size, 192 size: videoFile.size,
179 fps: videoFile.fps, 193 fps: videoFile.fps,
180 torrentUrl: video.getTorrentUrl(videoFile, baseUrlHttp), 194 torrentUrl: model.getTorrentUrl(videoFile, baseUrlHttp),
181 torrentDownloadUrl: video.getTorrentDownloadUrl(videoFile, baseUrlHttp), 195 torrentDownloadUrl: model.getTorrentDownloadUrl(videoFile, baseUrlHttp),
182 fileUrl: video.getVideoFileUrl(videoFile, baseUrlHttp), 196 fileUrl: model.getVideoFileUrl(videoFile, baseUrlHttp),
183 fileDownloadUrl: video.getVideoFileDownloadUrl(videoFile, baseUrlHttp) 197 fileDownloadUrl: model.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
184 } as VideoFile 198 } as VideoFile
185 }) 199 })
186 .sort((a, b) => { 200 .sort((a, b) => {
@@ -190,6 +204,39 @@ function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRe
190 }) 204 })
191} 205}
192 206
207function addVideoFilesInAPAcc (
208 acc: ActivityUrlObject[] | ActivityTagObject[],
209 model: MVideoAP | MStreamingPlaylistVideo,
210 baseUrlHttp: string,
211 baseUrlWs: string,
212 files: MVideoFile[]
213) {
214 for (const file of files) {
215 acc.push({
216 type: 'Link',
217 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
218 href: model.getVideoFileUrl(file, baseUrlHttp),
219 height: file.resolution,
220 size: file.size,
221 fps: file.fps
222 })
223
224 acc.push({
225 type: 'Link',
226 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
227 href: model.getTorrentUrl(file, baseUrlHttp),
228 height: file.resolution
229 })
230
231 acc.push({
232 type: 'Link',
233 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
234 href: generateMagnetUri(model, file, baseUrlHttp, baseUrlWs),
235 height: file.resolution
236 })
237 }
238}
239
193function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { 240function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
194 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls() 241 const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
195 if (!video.Tags) video.Tags = [] 242 if (!video.Tags) video.Tags = []
@@ -224,50 +271,25 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
224 } 271 }
225 272
226 const url: ActivityUrlObject[] = [] 273 const url: ActivityUrlObject[] = []
227 for (const file of video.VideoFiles) { 274 addVideoFilesInAPAcc(url, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
228 url.push({
229 type: 'Link',
230 mimeType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
231 mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[ file.extname ] as any,
232 href: video.getVideoFileUrl(file, baseUrlHttp),
233 height: file.resolution,
234 size: file.size,
235 fps: file.fps
236 })
237
238 url.push({
239 type: 'Link',
240 mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
241 mediaType: 'application/x-bittorrent' as 'application/x-bittorrent',
242 href: video.getTorrentUrl(file, baseUrlHttp),
243 height: file.resolution
244 })
245
246 url.push({
247 type: 'Link',
248 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
249 mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
250 href: video.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
251 height: file.resolution
252 })
253 }
254 275
255 for (const playlist of (video.VideoStreamingPlaylists || [])) { 276 for (const playlist of (video.VideoStreamingPlaylists || [])) {
256 let tag: (ActivityPlaylistSegmentHashesObject | ActivityPlaylistInfohashesObject)[] 277 let tag: ActivityTagObject[]
257 278
258 tag = playlist.p2pMediaLoaderInfohashes 279 tag = playlist.p2pMediaLoaderInfohashes
259 .map(i => ({ type: 'Infohash' as 'Infohash', name: i })) 280 .map(i => ({ type: 'Infohash' as 'Infohash', name: i }))
260 tag.push({ 281 tag.push({
261 type: 'Link', 282 type: 'Link',
262 name: 'sha256', 283 name: 'sha256',
263 mimeType: 'application/json' as 'application/json',
264 mediaType: 'application/json' as 'application/json', 284 mediaType: 'application/json' as 'application/json',
265 href: playlist.segmentsSha256Url 285 href: playlist.segmentsSha256Url
266 }) 286 })
267 287
288 const playlistWithVideo = Object.assign(playlist, { Video: video })
289 addVideoFilesInAPAcc(tag, playlistWithVideo, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
290
268 url.push({ 291 url.push({
269 type: 'Link', 292 type: 'Link',
270 mimeType: 'application/x-mpegURL' as 'application/x-mpegURL',
271 mediaType: 'application/x-mpegURL' as 'application/x-mpegURL', 293 mediaType: 'application/x-mpegURL' as 'application/x-mpegURL',
272 href: playlist.playlistUrl, 294 href: playlist.playlistUrl,
273 tag 295 tag
@@ -277,7 +299,6 @@ function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject {
277 // Add video url too 299 // Add video url too
278 url.push({ 300 url.push({
279 type: 'Link', 301 type: 'Link',
280 mimeType: 'text/html',
281 mediaType: 'text/html', 302 mediaType: 'text/html',
282 href: WEBSERVER.URL + '/videos/watch/' + video.uuid 303 href: WEBSERVER.URL + '/videos/watch/' + video.uuid
283 }) 304 })
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index 0ea90d28c..faad4cc2d 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -5,12 +5,14 @@ import { VideoModel } from './video'
5import { VideoRedundancyModel } from '../redundancy/video-redundancy' 5import { VideoRedundancyModel } from '../redundancy/video-redundancy'
6import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 6import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 7import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
8import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' 8import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_DOWNLOAD_PATHS, STATIC_PATHS } from '../../initializers/constants'
9import { join } from 'path' 9import { join } from 'path'
10import { sha1 } from '../../helpers/core-utils' 10import { sha1 } from '../../helpers/core-utils'
11import { isArrayOf } from '../../helpers/custom-validators/misc' 11import { isArrayOf } from '../../helpers/custom-validators/misc'
12import { Op, QueryTypes } from 'sequelize' 12import { Op, QueryTypes } from 'sequelize'
13import { MStreamingPlaylist, MVideoFile } from '@server/typings/models' 13import { MStreamingPlaylist, MVideoFile } from '@server/typings/models'
14import { VideoFileModel } from '@server/models/video/video-file'
15import { getTorrentFileName, getVideoFilename } from '@server/lib/video-paths'
14 16
15@Table({ 17@Table({
16 tableName: 'videoStreamingPlaylist', 18 tableName: 'videoStreamingPlaylist',
@@ -70,6 +72,14 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
70 }) 72 })
71 Video: VideoModel 73 Video: VideoModel
72 74
75 @HasMany(() => VideoFileModel, {
76 foreignKey: {
77 allowNull: true
78 },
79 onDelete: 'CASCADE'
80 })
81 VideoFiles: VideoFileModel[]
82
73 @HasMany(() => VideoRedundancyModel, { 83 @HasMany(() => VideoRedundancyModel, {
74 foreignKey: { 84 foreignKey: {
75 allowNull: false 85 allowNull: false
@@ -91,11 +101,11 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
91 .then(results => results.length === 1) 101 .then(results => results.length === 1)
92 } 102 }
93 103
94 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, videoFiles: MVideoFile[]) { 104 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
95 const hashes: string[] = [] 105 const hashes: string[] = []
96 106
97 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115 107 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
98 for (let i = 0; i < videoFiles.length; i++) { 108 for (let i = 0; i < files.length; i++) {
99 hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`)) 109 hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
100 } 110 }
101 111
@@ -139,10 +149,6 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
139 return 'segments-sha256.json' 149 return 'segments-sha256.json'
140 } 150 }
141 151
142 static getHlsVideoName (uuid: string, resolution: number) {
143 return `${uuid}-${resolution}-fragmented.mp4`
144 }
145
146 static getHlsMasterPlaylistStaticPath (videoUUID: string) { 152 static getHlsMasterPlaylistStaticPath (videoUUID: string) {
147 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename()) 153 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
148 } 154 }
@@ -165,6 +171,26 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
165 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid 171 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
166 } 172 }
167 173
174 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
175 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
176 }
177
178 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
179 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile)
180 }
181
182 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
183 return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile))
184 }
185
186 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
187 return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile))
188 }
189
190 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
191 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
192 }
193
168 hasSameUniqueKeysThan (other: MStreamingPlaylist) { 194 hasSameUniqueKeysThan (other: MStreamingPlaylist) {
169 return this.type === other.type && 195 return this.type === other.type &&
170 this.videoId === other.videoId 196 this.videoId === other.videoId
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 0d1dbf106..f84a90992 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,7 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { maxBy } from 'lodash' 2import { maxBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri'
4import * as parseTorrent from 'parse-torrent'
5import { join } from 'path' 3import { join } from 'path'
6import { 4import {
7 CountOptions, 5 CountOptions,
@@ -38,11 +36,11 @@ import {
38} from 'sequelize-typescript' 36} from 'sequelize-typescript'
39import { UserRight, VideoPrivacy, VideoState } from '../../../shared' 37import { UserRight, VideoPrivacy, VideoState } from '../../../shared'
40import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 38import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
41import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 39import { Video, VideoDetails } from '../../../shared/models/videos'
42import { VideoFilter } from '../../../shared/models/videos/video-query.type' 40import { VideoFilter } from '../../../shared/models/videos/video-query.type'
43import { peertubeTruncate } from '../../helpers/core-utils' 41import { peertubeTruncate } from '../../helpers/core-utils'
44import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 42import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
45import { isArray, isBooleanValid } from '../../helpers/custom-validators/misc' 43import { isBooleanValid } from '../../helpers/custom-validators/misc'
46import { 44import {
47 isVideoCategoryValid, 45 isVideoCategoryValid,
48 isVideoDescriptionValid, 46 isVideoDescriptionValid,
@@ -100,7 +98,7 @@ import { VideoTagModel } from './video-tag'
100import { ScheduleVideoUpdateModel } from './schedule-video-update' 98import { ScheduleVideoUpdateModel } from './schedule-video-update'
101import { VideoCaptionModel } from './video-caption' 99import { VideoCaptionModel } from './video-caption'
102import { VideoBlacklistModel } from './video-blacklist' 100import { VideoBlacklistModel } from './video-blacklist'
103import { remove, writeFile } from 'fs-extra' 101import { remove } from 'fs-extra'
104import { VideoViewModel } from './video-views' 102import { VideoViewModel } from './video-views'
105import { VideoRedundancyModel } from '../redundancy/video-redundancy' 103import { VideoRedundancyModel } from '../redundancy/video-redundancy'
106import { 104import {
@@ -117,18 +115,20 @@ import { VideoPlaylistElementModel } from './video-playlist-element'
117import { CONFIG } from '../../initializers/config' 115import { CONFIG } from '../../initializers/config'
118import { ThumbnailModel } from './thumbnail' 116import { ThumbnailModel } from './thumbnail'
119import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 117import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
120import { createTorrentPromise } from '../../helpers/webtorrent'
121import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 118import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
122import { 119import {
123 MChannel, 120 MChannel,
124 MChannelAccountDefault, 121 MChannelAccountDefault,
125 MChannelId, 122 MChannelId,
123 MStreamingPlaylist,
124 MStreamingPlaylistFilesVideo,
126 MUserAccountId, 125 MUserAccountId,
127 MUserId, 126 MUserId,
128 MVideoAccountLight, 127 MVideoAccountLight,
129 MVideoAccountLightBlacklistAllFiles, 128 MVideoAccountLightBlacklistAllFiles,
130 MVideoAP, 129 MVideoAP,
131 MVideoDetails, 130 MVideoDetails,
131 MVideoFileVideo,
132 MVideoFormattable, 132 MVideoFormattable,
133 MVideoFormattableDetails, 133 MVideoFormattableDetails,
134 MVideoForUser, 134 MVideoForUser,
@@ -140,8 +140,10 @@ import {
140 MVideoWithFile, 140 MVideoWithFile,
141 MVideoWithRights 141 MVideoWithRights
142} from '../../typings/models' 142} from '../../typings/models'
143import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' 143import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file'
144import { MThumbnail } from '../../typings/models/video/thumbnail' 144import { MThumbnail } from '../../typings/models/video/thumbnail'
145import { VideoFile } from '@shared/models/videos/video-file.model'
146import { getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
145 147
146// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 148// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
147const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ 149const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
@@ -211,7 +213,7 @@ export enum ScopeNames {
211 FOR_API = 'FOR_API', 213 FOR_API = 'FOR_API',
212 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', 214 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
213 WITH_TAGS = 'WITH_TAGS', 215 WITH_TAGS = 'WITH_TAGS',
214 WITH_FILES = 'WITH_FILES', 216 WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
215 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 217 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
216 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 218 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
217 WITH_BLOCKLIST = 'WITH_BLOCKLIST', 219 WITH_BLOCKLIST = 'WITH_BLOCKLIST',
@@ -666,7 +668,7 @@ export type AvailableForListIDsOptions = {
666 } 668 }
667 ] 669 ]
668 }, 670 },
669 [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => { 671 [ ScopeNames.WITH_WEBTORRENT_FILES ]: (withRedundancies = false) => {
670 let subInclude: any[] = [] 672 let subInclude: any[] = []
671 673
672 if (withRedundancies === true) { 674 if (withRedundancies === true) {
@@ -691,16 +693,19 @@ export type AvailableForListIDsOptions = {
691 } 693 }
692 }, 694 },
693 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => { 695 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => {
694 let subInclude: any[] = [] 696 const subInclude: IncludeOptions[] = [
697 {
698 model: VideoFileModel.unscoped(),
699 required: false
700 }
701 ]
695 702
696 if (withRedundancies === true) { 703 if (withRedundancies === true) {
697 subInclude = [ 704 subInclude.push({
698 { 705 attributes: [ 'fileUrl' ],
699 attributes: [ 'fileUrl' ], 706 model: VideoRedundancyModel.unscoped(),
700 model: VideoRedundancyModel.unscoped(), 707 required: false
701 required: false 708 })
702 }
703 ]
704 } 709 }
705 710
706 return { 711 return {
@@ -913,7 +918,7 @@ export class VideoModel extends Model<VideoModel> {
913 @HasMany(() => VideoFileModel, { 918 @HasMany(() => VideoFileModel, {
914 foreignKey: { 919 foreignKey: {
915 name: 'videoId', 920 name: 'videoId',
916 allowNull: false 921 allowNull: true
917 }, 922 },
918 hooks: true, 923 hooks: true,
919 onDelete: 'cascade' 924 onDelete: 'cascade'
@@ -1071,7 +1076,7 @@ export class VideoModel extends Model<VideoModel> {
1071 } 1076 }
1072 1077
1073 return VideoModel.scope([ 1078 return VideoModel.scope([
1074 ScopeNames.WITH_FILES, 1079 ScopeNames.WITH_WEBTORRENT_FILES,
1075 ScopeNames.WITH_STREAMING_PLAYLISTS, 1080 ScopeNames.WITH_STREAMING_PLAYLISTS,
1076 ScopeNames.WITH_THUMBNAILS 1081 ScopeNames.WITH_THUMBNAILS
1077 ]).findAll(query) 1082 ]).findAll(query)
@@ -1463,7 +1468,7 @@ export class VideoModel extends Model<VideoModel> {
1463 } 1468 }
1464 1469
1465 return VideoModel.scope([ 1470 return VideoModel.scope([
1466 ScopeNames.WITH_FILES, 1471 ScopeNames.WITH_WEBTORRENT_FILES,
1467 ScopeNames.WITH_STREAMING_PLAYLISTS, 1472 ScopeNames.WITH_STREAMING_PLAYLISTS,
1468 ScopeNames.WITH_THUMBNAILS 1473 ScopeNames.WITH_THUMBNAILS
1469 ]).findOne(query) 1474 ]).findOne(query)
@@ -1500,7 +1505,7 @@ export class VideoModel extends Model<VideoModel> {
1500 1505
1501 return VideoModel.scope([ 1506 return VideoModel.scope([
1502 ScopeNames.WITH_ACCOUNT_DETAILS, 1507 ScopeNames.WITH_ACCOUNT_DETAILS,
1503 ScopeNames.WITH_FILES, 1508 ScopeNames.WITH_WEBTORRENT_FILES,
1504 ScopeNames.WITH_STREAMING_PLAYLISTS, 1509 ScopeNames.WITH_STREAMING_PLAYLISTS,
1505 ScopeNames.WITH_THUMBNAILS, 1510 ScopeNames.WITH_THUMBNAILS,
1506 ScopeNames.WITH_BLACKLISTED 1511 ScopeNames.WITH_BLACKLISTED
@@ -1521,7 +1526,7 @@ export class VideoModel extends Model<VideoModel> {
1521 ScopeNames.WITH_BLACKLISTED, 1526 ScopeNames.WITH_BLACKLISTED,
1522 ScopeNames.WITH_ACCOUNT_DETAILS, 1527 ScopeNames.WITH_ACCOUNT_DETAILS,
1523 ScopeNames.WITH_SCHEDULED_UPDATE, 1528 ScopeNames.WITH_SCHEDULED_UPDATE,
1524 ScopeNames.WITH_FILES, 1529 ScopeNames.WITH_WEBTORRENT_FILES,
1525 ScopeNames.WITH_STREAMING_PLAYLISTS, 1530 ScopeNames.WITH_STREAMING_PLAYLISTS,
1526 ScopeNames.WITH_THUMBNAILS 1531 ScopeNames.WITH_THUMBNAILS
1527 ] 1532 ]
@@ -1555,7 +1560,7 @@ export class VideoModel extends Model<VideoModel> {
1555 ScopeNames.WITH_ACCOUNT_DETAILS, 1560 ScopeNames.WITH_ACCOUNT_DETAILS,
1556 ScopeNames.WITH_SCHEDULED_UPDATE, 1561 ScopeNames.WITH_SCHEDULED_UPDATE,
1557 ScopeNames.WITH_THUMBNAILS, 1562 ScopeNames.WITH_THUMBNAILS,
1558 { method: [ ScopeNames.WITH_FILES, true ] }, 1563 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
1559 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } 1564 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
1560 ] 1565 ]
1561 1566
@@ -1787,17 +1792,31 @@ export class VideoModel extends Model<VideoModel> {
1787 this.VideoChannel.Account.isBlocked() 1792 this.VideoChannel.Account.isBlocked()
1788 } 1793 }
1789 1794
1790 getOriginalFile <T extends MVideoWithFile> (this: T) { 1795 getMaxQualityFile <T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
1791 if (Array.isArray(this.VideoFiles) === false) return undefined 1796 if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
1797 const file = maxBy(this.VideoFiles, file => file.resolution)
1798
1799 return Object.assign(file, { Video: this })
1800 }
1801
1802 // No webtorrent files, try with streaming playlist files
1803 if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
1804 const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
1805
1806 const file = maxBy(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
1807 return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
1808 }
1792 1809
1793 // The original file is the file that have the higher resolution 1810 return undefined
1794 return maxBy(this.VideoFiles, file => file.resolution)
1795 } 1811 }
1796 1812
1797 getFile <T extends MVideoWithFile> (this: T, resolution: number) { 1813 getWebTorrentFile <T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
1798 if (Array.isArray(this.VideoFiles) === false) return undefined 1814 if (Array.isArray(this.VideoFiles) === false) return undefined
1799 1815
1800 return this.VideoFiles.find(f => f.resolution === resolution) 1816 const file = this.VideoFiles.find(f => f.resolution === resolution)
1817 if (!file) return undefined
1818
1819 return Object.assign(file, { Video: this })
1801 } 1820 }
1802 1821
1803 async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) { 1822 async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
@@ -1813,10 +1832,6 @@ export class VideoModel extends Model<VideoModel> {
1813 this.Thumbnails.push(savedThumbnail) 1832 this.Thumbnails.push(savedThumbnail)
1814 } 1833 }
1815 1834
1816 getVideoFilename (videoFile: MVideoFile) {
1817 return this.uuid + '-' + videoFile.resolution + videoFile.extname
1818 }
1819
1820 generateThumbnailName () { 1835 generateThumbnailName () {
1821 return this.uuid + '.jpg' 1836 return this.uuid + '.jpg'
1822 } 1837 }
@@ -1837,46 +1852,10 @@ export class VideoModel extends Model<VideoModel> {
1837 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW) 1852 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
1838 } 1853 }
1839 1854
1840 getTorrentFileName (videoFile: MVideoFile) {
1841 const extension = '.torrent'
1842 return this.uuid + '-' + videoFile.resolution + extension
1843 }
1844
1845 isOwned () { 1855 isOwned () {
1846 return this.remote === false 1856 return this.remote === false
1847 } 1857 }
1848 1858
1849 getTorrentFilePath (videoFile: MVideoFile) {
1850 return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1851 }
1852
1853 getVideoFilePath (videoFile: MVideoFile) {
1854 return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
1855 }
1856
1857 async createTorrentAndSetInfoHash (videoFile: MVideoFile) {
1858 const options = {
1859 // Keep the extname, it's used by the client to stream the file inside a web browser
1860 name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
1861 createdBy: 'PeerTube',
1862 announceList: [
1863 [ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket' ],
1864 [ WEBSERVER.URL + '/tracker/announce' ]
1865 ],
1866 urlList: [ WEBSERVER.URL + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) ]
1867 }
1868
1869 const torrent = await createTorrentPromise(this.getVideoFilePath(videoFile), options)
1870
1871 const filePath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1872 logger.info('Creating torrent %s.', filePath)
1873
1874 await writeFile(filePath, torrent)
1875
1876 const parsedTorrent = parseTorrent(torrent)
1877 videoFile.infoHash = parsedTorrent.infoHash
1878 }
1879
1880 getWatchStaticPath () { 1859 getWatchStaticPath () {
1881 return '/videos/watch/' + this.uuid 1860 return '/videos/watch/' + this.uuid
1882 } 1861 }
@@ -1909,7 +1888,8 @@ export class VideoModel extends Model<VideoModel> {
1909 } 1888 }
1910 1889
1911 getFormattedVideoFilesJSON (): VideoFile[] { 1890 getFormattedVideoFilesJSON (): VideoFile[] {
1912 return videoFilesModelToFormattedJSON(this, this.VideoFiles) 1891 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
1892 return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
1913 } 1893 }
1914 1894
1915 toActivityPubObject (this: MVideoAP): VideoTorrentObject { 1895 toActivityPubObject (this: MVideoAP): VideoTorrentObject {
@@ -1923,8 +1903,10 @@ export class VideoModel extends Model<VideoModel> {
1923 return peertubeTruncate(this.description, { length: maxLength }) 1903 return peertubeTruncate(this.description, { length: maxLength })
1924 } 1904 }
1925 1905
1926 getOriginalFileResolution () { 1906 getMaxQualityResolution () {
1927 const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) 1907 const file = this.getMaxQualityFile()
1908 const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
1909 const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
1928 1910
1929 return getVideoFileResolution(originalFilePath) 1911 return getVideoFileResolution(originalFilePath)
1930 } 1912 }
@@ -1933,22 +1915,36 @@ export class VideoModel extends Model<VideoModel> {
1933 return `/api/${API_VERSION}/videos/${this.uuid}/description` 1915 return `/api/${API_VERSION}/videos/${this.uuid}/description`
1934 } 1916 }
1935 1917
1936 getHLSPlaylist () { 1918 getHLSPlaylist (): MStreamingPlaylistFilesVideo {
1937 if (!this.VideoStreamingPlaylists) return undefined 1919 if (!this.VideoStreamingPlaylists) return undefined
1938 1920
1939 return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) 1921 const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
1922 playlist.Video = this
1923
1924 return playlist
1940 } 1925 }
1941 1926
1942 removeFile (videoFile: MVideoFile, isRedundancy = false) { 1927 setHLSPlaylist (playlist: MStreamingPlaylist) {
1943 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR 1928 const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
1944 1929
1945 const filePath = join(baseDir, this.getVideoFilename(videoFile)) 1930 if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
1931 this.VideoStreamingPlaylists = toAdd
1932 return
1933 }
1934
1935 this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
1936 .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
1937 .concat(toAdd)
1938 }
1939
1940 removeFile (videoFile: MVideoFile, isRedundancy = false) {
1941 const filePath = getVideoFilePath(this, videoFile, isRedundancy)
1946 return remove(filePath) 1942 return remove(filePath)
1947 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) 1943 .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
1948 } 1944 }
1949 1945
1950 removeTorrent (videoFile: MVideoFile) { 1946 removeTorrent (videoFile: MVideoFile) {
1951 const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 1947 const torrentPath = getTorrentFilePath(this, videoFile)
1952 return remove(torrentPath) 1948 return remove(torrentPath)
1953 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 1949 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1954 } 1950 }
@@ -1973,38 +1969,30 @@ export class VideoModel extends Model<VideoModel> {
1973 return this.save() 1969 return this.save()
1974 } 1970 }
1975 1971
1976 getBaseUrls () { 1972 async publishIfNeededAndSave (t: Transaction) {
1977 let baseUrlHttp 1973 if (this.state !== VideoState.PUBLISHED) {
1978 let baseUrlWs 1974 this.state = VideoState.PUBLISHED
1975 this.publishedAt = new Date()
1976 await this.save({ transaction: t })
1979 1977
1980 if (this.isOwned()) { 1978 return true
1981 baseUrlHttp = WEBSERVER.URL
1982 baseUrlWs = WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
1983 } else {
1984 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
1985 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
1986 } 1979 }
1987 1980
1988 return { baseUrlHttp, baseUrlWs } 1981 return false
1989 } 1982 }
1990 1983
1991 generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) { 1984 getBaseUrls () {
1992 const xs = this.getTorrentUrl(videoFile, baseUrlHttp) 1985 if (this.isOwned()) {
1993 const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs) 1986 return {
1994 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] 1987 baseUrlHttp: WEBSERVER.URL,
1995 1988 baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
1996 const redundancies = videoFile.RedundancyVideos 1989 }
1997 if (isArray(redundancies)) urlList = urlList.concat(redundancies.map(r => r.fileUrl))
1998
1999 const magnetHash = {
2000 xs,
2001 announce,
2002 urlList,
2003 infoHash: videoFile.infoHash,
2004 name: this.name
2005 } 1990 }
2006 1991
2007 return magnetUtil.encode(magnetHash) 1992 return {
1993 baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
1994 baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
1995 }
2008 } 1996 }
2009 1997
2010 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) { 1998 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
@@ -2012,23 +2000,23 @@ export class VideoModel extends Model<VideoModel> {
2012 } 2000 }
2013 2001
2014 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) { 2002 getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2015 return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) 2003 return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
2016 } 2004 }
2017 2005
2018 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { 2006 getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2019 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile) 2007 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
2020 } 2008 }
2021 2009
2022 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) { 2010 getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2023 return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) 2011 return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
2024 } 2012 }
2025 2013
2026 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) { 2014 getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2027 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) 2015 return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
2028 } 2016 }
2029 2017
2030 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) { 2018 getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
2031 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 2019 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
2032 } 2020 }
2033 2021
2034 getBandwidthBits (videoFile: MVideoFile) { 2022 getBandwidthBits (videoFile: MVideoFile) {