diff options
Diffstat (limited to 'server/models/video')
-rw-r--r-- | server/models/video/schedule-video-update.ts | 10 | ||||
-rw-r--r-- | server/models/video/video-change-ownership.ts | 6 | ||||
-rw-r--r-- | server/models/video/video-file.ts | 87 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 127 | ||||
-rw-r--r-- | server/models/video/video-streaming-playlist.ts | 40 | ||||
-rw-r--r-- | server/models/video/video.ts | 204 |
6 files changed, 295 insertions, 179 deletions
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 | |||
2 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' | 2 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' |
3 | import { VideoPrivacy } from '../../../shared/models/videos' | 3 | import { VideoPrivacy } from '../../../shared/models/videos' |
4 | import { Op, Transaction } from 'sequelize' | 4 | import { Op, Transaction } from 'sequelize' |
5 | import { MScheduleVideoUpdateFormattable } from '@server/typings/models' | 5 | import { 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' | |||
23 | import { VideoModel } from './video' | 23 | import { VideoModel } from './video' |
24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 24 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 25 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
26 | import { FindOptions, QueryTypes, Transaction } from 'sequelize' | 26 | import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize' |
27 | import { MIMETYPES } from '../../initializers/constants' | 27 | import { MIMETYPES } from '../../initializers/constants' |
28 | import { MVideoFile } from '@server/typings/models' | 28 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file' |
29 | import { 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 @@ | |||
1 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 1 | import { Video, VideoDetails } from '../../../shared/models/videos' |
2 | import { VideoModel } from './video' | 2 | import { VideoModel } from './video' |
3 | import { | 3 | import { ActivityTagObject, ActivityUrlObject, VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
4 | ActivityPlaylistInfohashesObject, | ||
5 | ActivityPlaylistSegmentHashesObject, | ||
6 | ActivityUrlObject, | ||
7 | VideoTorrentObject | ||
8 | } from '../../../shared/models/activitypub/objects' | ||
9 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' | 4 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' |
10 | import { VideoCaptionModel } from './video-caption' | 5 | import { VideoCaptionModel } from './video-caption' |
11 | import { | 6 | import { |
@@ -16,9 +11,18 @@ import { | |||
16 | } from '../../lib/activitypub' | 11 | } from '../../lib/activitypub' |
17 | import { isArray } from '../../helpers/custom-validators/misc' | 12 | import { isArray } from '../../helpers/custom-validators/misc' |
18 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' | 13 | import { VideoStreamingPlaylist } from '../../../shared/models/videos/video-streaming-playlist.model' |
19 | import { MStreamingPlaylistRedundanciesOpt, MVideo, MVideoAP, MVideoFormattable, MVideoFormattableDetails } from '../../typings/models' | 14 | import { |
20 | import { 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' | ||
21 | import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' | 23 | import { MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' |
24 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
25 | import { generateMagnetUri } from '@server/helpers/webtorrent' | ||
22 | 26 | ||
23 | export type VideoFormattingJSONOptions = { | 27 | export 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 | ||
146 | function streamingPlaylistsModelToFormattedJSON (playlists: MStreamingPlaylistRedundanciesOpt[]): VideoStreamingPlaylist[] { | 150 | function 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 | ||
165 | function videoFilesModelToFormattedJSON (video: MVideo, videoFiles: MVideoFileRedundanciesOpt[]): VideoFile[] { | 176 | function 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 | ||
207 | function 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 | |||
193 | function videoModelToActivityPubObject (video: MVideoAP): VideoTorrentObject { | 240 | function 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' | |||
5 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 5 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
6 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 6 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 7 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
8 | import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_PATHS } from '../../initializers/constants' | 8 | import { CONSTRAINTS_FIELDS, P2P_MEDIA_LOADER_PEER_VERSION, STATIC_DOWNLOAD_PATHS, STATIC_PATHS } from '../../initializers/constants' |
9 | import { join } from 'path' | 9 | import { join } from 'path' |
10 | import { sha1 } from '../../helpers/core-utils' | 10 | import { sha1 } from '../../helpers/core-utils' |
11 | import { isArrayOf } from '../../helpers/custom-validators/misc' | 11 | import { isArrayOf } from '../../helpers/custom-validators/misc' |
12 | import { Op, QueryTypes } from 'sequelize' | 12 | import { Op, QueryTypes } from 'sequelize' |
13 | import { MStreamingPlaylist, MVideoFile } from '@server/typings/models' | 13 | import { MStreamingPlaylist, MVideoFile } from '@server/typings/models' |
14 | import { VideoFileModel } from '@server/models/video/video-file' | ||
15 | import { 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 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy } from 'lodash' | 2 | import { maxBy } from 'lodash' |
3 | import * as magnetUtil from 'magnet-uri' | ||
4 | import * as parseTorrent from 'parse-torrent' | ||
5 | import { join } from 'path' | 3 | import { join } from 'path' |
6 | import { | 4 | import { |
7 | CountOptions, | 5 | CountOptions, |
@@ -38,11 +36,11 @@ import { | |||
38 | } from 'sequelize-typescript' | 36 | } from 'sequelize-typescript' |
39 | import { UserRight, VideoPrivacy, VideoState } from '../../../shared' | 37 | import { UserRight, VideoPrivacy, VideoState } from '../../../shared' |
40 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 38 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
41 | import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' | 39 | import { Video, VideoDetails } from '../../../shared/models/videos' |
42 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' | 40 | import { VideoFilter } from '../../../shared/models/videos/video-query.type' |
43 | import { peertubeTruncate } from '../../helpers/core-utils' | 41 | import { peertubeTruncate } from '../../helpers/core-utils' |
44 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 42 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
45 | import { isArray, isBooleanValid } from '../../helpers/custom-validators/misc' | 43 | import { isBooleanValid } from '../../helpers/custom-validators/misc' |
46 | import { | 44 | import { |
47 | isVideoCategoryValid, | 45 | isVideoCategoryValid, |
48 | isVideoDescriptionValid, | 46 | isVideoDescriptionValid, |
@@ -100,7 +98,7 @@ import { VideoTagModel } from './video-tag' | |||
100 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 98 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
101 | import { VideoCaptionModel } from './video-caption' | 99 | import { VideoCaptionModel } from './video-caption' |
102 | import { VideoBlacklistModel } from './video-blacklist' | 100 | import { VideoBlacklistModel } from './video-blacklist' |
103 | import { remove, writeFile } from 'fs-extra' | 101 | import { remove } from 'fs-extra' |
104 | import { VideoViewModel } from './video-views' | 102 | import { VideoViewModel } from './video-views' |
105 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 103 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
106 | import { | 104 | import { |
@@ -117,18 +115,20 @@ import { VideoPlaylistElementModel } from './video-playlist-element' | |||
117 | import { CONFIG } from '../../initializers/config' | 115 | import { CONFIG } from '../../initializers/config' |
118 | import { ThumbnailModel } from './thumbnail' | 116 | import { ThumbnailModel } from './thumbnail' |
119 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | 117 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' |
120 | import { createTorrentPromise } from '../../helpers/webtorrent' | ||
121 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' | 118 | import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' |
122 | import { | 119 | import { |
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' |
143 | import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file' | 143 | import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../typings/models/video/video-file' |
144 | import { MThumbnail } from '../../typings/models/video/thumbnail' | 144 | import { MThumbnail } from '../../typings/models/video/thumbnail' |
145 | import { VideoFile } from '@shared/models/videos/video-file.model' | ||
146 | import { 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 |
147 | const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [ | 149 | const 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) { |