]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-streaming-playlist.ts
Add ability to list imports of a channel sync
[github/Chocobozzz/PeerTube.git] / server / models / video / video-streaming-playlist.ts
1 import memoizee from 'memoizee'
2 import { join } from 'path'
3 import { Op, Transaction } from 'sequelize'
4 import {
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 Default,
11 ForeignKey,
12 HasMany,
13 Is,
14 Model,
15 Table,
16 UpdatedAt
17 } from 'sequelize-typescript'
18 import { getHLSPublicFileUrl } from '@server/lib/object-storage'
19 import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename } from '@server/lib/paths'
20 import { VideoFileModel } from '@server/models/video/video-file'
21 import { MStreamingPlaylist, MStreamingPlaylistFilesVideo, MVideo } from '@server/types/models'
22 import { sha1 } from '@shared/extra-utils'
23 import { VideoStorage } from '@shared/models'
24 import { AttributesOnly } from '@shared/typescript-utils'
25 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
26 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
27 import { isArrayOf } from '../../helpers/custom-validators/misc'
28 import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
29 import {
30 CONSTRAINTS_FIELDS,
31 MEMOIZE_LENGTH,
32 MEMOIZE_TTL,
33 P2P_MEDIA_LOADER_PEER_VERSION,
34 STATIC_PATHS,
35 WEBSERVER
36 } from '../../initializers/constants'
37 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
38 import { doesExist } from '../shared'
39 import { throwIfNotValid } from '../utils'
40 import { VideoModel } from './video'
41
42 @Table({
43 tableName: 'videoStreamingPlaylist',
44 indexes: [
45 {
46 fields: [ 'videoId' ]
47 },
48 {
49 fields: [ 'videoId', 'type' ],
50 unique: true
51 },
52 {
53 fields: [ 'p2pMediaLoaderInfohashes' ],
54 using: 'gin'
55 }
56 ]
57 })
58 export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<VideoStreamingPlaylistModel>>> {
59 @CreatedAt
60 createdAt: Date
61
62 @UpdatedAt
63 updatedAt: Date
64
65 @AllowNull(false)
66 @Column
67 type: VideoStreamingPlaylistType
68
69 @AllowNull(false)
70 @Column
71 playlistFilename: string
72
73 @AllowNull(true)
74 @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true))
75 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
76 playlistUrl: string
77
78 @AllowNull(false)
79 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
80 @Column(DataType.ARRAY(DataType.STRING))
81 p2pMediaLoaderInfohashes: string[]
82
83 @AllowNull(false)
84 @Column
85 p2pMediaLoaderPeerVersion: number
86
87 @AllowNull(false)
88 @Column
89 segmentsSha256Filename: string
90
91 @AllowNull(true)
92 @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true))
93 @Column
94 segmentsSha256Url: string
95
96 @ForeignKey(() => VideoModel)
97 @Column
98 videoId: number
99
100 @AllowNull(false)
101 @Default(VideoStorage.FILE_SYSTEM)
102 @Column
103 storage: VideoStorage
104
105 @BelongsTo(() => VideoModel, {
106 foreignKey: {
107 allowNull: false
108 },
109 onDelete: 'CASCADE'
110 })
111 Video: VideoModel
112
113 @HasMany(() => VideoFileModel, {
114 foreignKey: {
115 allowNull: true
116 },
117 onDelete: 'CASCADE'
118 })
119 VideoFiles: VideoFileModel[]
120
121 @HasMany(() => VideoRedundancyModel, {
122 foreignKey: {
123 allowNull: false
124 },
125 onDelete: 'CASCADE',
126 hooks: true
127 })
128 RedundancyVideos: VideoRedundancyModel[]
129
130 static doesInfohashExistCached = memoizee(VideoStreamingPlaylistModel.doesInfohashExist, {
131 promise: true,
132 max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
133 maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
134 })
135
136 static doesInfohashExist (infoHash: string) {
137 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
138
139 return doesExist(query, { infoHash })
140 }
141
142 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
143 const hashes: string[] = []
144
145 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
146 for (let i = 0; i < files.length; i++) {
147 hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
148 }
149
150 return hashes
151 }
152
153 static listByIncorrectPeerVersion () {
154 const query = {
155 where: {
156 p2pMediaLoaderPeerVersion: {
157 [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
158 }
159 },
160 include: [
161 {
162 model: VideoModel.unscoped(),
163 required: true
164 }
165 ]
166 }
167
168 return VideoStreamingPlaylistModel.findAll(query)
169 }
170
171 static loadWithVideoAndFiles (id: number) {
172 const options = {
173 include: [
174 {
175 model: VideoModel.unscoped(),
176 required: true
177 },
178 {
179 model: VideoFileModel.unscoped()
180 }
181 ]
182 }
183
184 return VideoStreamingPlaylistModel.findByPk<MStreamingPlaylistFilesVideo>(id, options)
185 }
186
187 static loadWithVideo (id: number) {
188 const options = {
189 include: [
190 {
191 model: VideoModel.unscoped(),
192 required: true
193 }
194 ]
195 }
196
197 return VideoStreamingPlaylistModel.findByPk(id, options)
198 }
199
200 static loadHLSPlaylistByVideo (videoId: number, transaction?: Transaction): Promise<MStreamingPlaylist> {
201 const options = {
202 where: {
203 type: VideoStreamingPlaylistType.HLS,
204 videoId
205 },
206 transaction
207 }
208
209 return VideoStreamingPlaylistModel.findOne(options)
210 }
211
212 static async loadOrGenerate (video: MVideo, transaction?: Transaction) {
213 let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id, transaction)
214
215 if (!playlist) {
216 playlist = new VideoStreamingPlaylistModel({
217 p2pMediaLoaderPeerVersion: P2P_MEDIA_LOADER_PEER_VERSION,
218 type: VideoStreamingPlaylistType.HLS,
219 storage: VideoStorage.FILE_SYSTEM,
220 p2pMediaLoaderInfohashes: [],
221 playlistFilename: generateHLSMasterPlaylistFilename(video.isLive),
222 segmentsSha256Filename: generateHlsSha256SegmentsFilename(video.isLive),
223 videoId: video.id
224 })
225
226 await playlist.save({ transaction })
227 }
228
229 return Object.assign(playlist, { Video: video })
230 }
231
232 static doesOwnedHLSPlaylistExist (videoUUID: string) {
233 const query = `SELECT 1 FROM "videoStreamingPlaylist" ` +
234 `INNER JOIN "video" ON "video"."id" = "videoStreamingPlaylist"."videoId" ` +
235 `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` +
236 `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1`
237
238 return doesExist(query, { videoUUID })
239 }
240
241 assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) {
242 const masterPlaylistUrl = this.getMasterPlaylistUrl(video)
243
244 this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files)
245 }
246
247 getMasterPlaylistUrl (video: MVideo) {
248 if (this.storage === VideoStorage.OBJECT_STORAGE) {
249 return getHLSPublicFileUrl(this.playlistUrl)
250 }
251
252 if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid)
253
254 return this.playlistUrl
255 }
256
257 getSha256SegmentsUrl (video: MVideo) {
258 if (this.storage === VideoStorage.OBJECT_STORAGE) {
259 return getHLSPublicFileUrl(this.segmentsSha256Url)
260 }
261
262 if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive)
263
264 return this.segmentsSha256Url
265 }
266
267 getStringType () {
268 if (this.type === VideoStreamingPlaylistType.HLS) return 'hls'
269
270 return 'unknown'
271 }
272
273 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
274 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
275 }
276
277 hasSameUniqueKeysThan (other: MStreamingPlaylist) {
278 return this.type === other.type &&
279 this.videoId === other.videoId
280 }
281
282 withVideo (video: MVideo) {
283 return Object.assign(this, { Video: video })
284 }
285
286 private getMasterPlaylistStaticPath (videoUUID: string) {
287 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename)
288 }
289
290 private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) {
291 if (isLive) return join('/live', 'segments-sha256', videoUUID)
292
293 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename)
294 }
295 }