]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/models/video/video-streaming-playlist.ts
Use random names for VOD HLS playlists
[github/Chocobozzz/PeerTube.git] / server / models / video / video-streaming-playlist.ts
1 import * as memoizee from 'memoizee'
2 import { join } from 'path'
3 import { Op } from 'sequelize'
4 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
5 import { doesExist } from '@server/helpers/database-utils'
6 import { VideoFileModel } from '@server/models/video/video-file'
7 import { MStreamingPlaylist, MVideo } from '@server/types/models'
8 import { AttributesOnly } from '@shared/core-utils'
9 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
10 import { sha1 } from '../../helpers/core-utils'
11 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
12 import { isArrayOf } from '../../helpers/custom-validators/misc'
13 import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
14 import {
15 CONSTRAINTS_FIELDS,
16 MEMOIZE_LENGTH,
17 MEMOIZE_TTL,
18 P2P_MEDIA_LOADER_PEER_VERSION,
19 STATIC_PATHS,
20 WEBSERVER
21 } from '../../initializers/constants'
22 import { VideoRedundancyModel } from '../redundancy/video-redundancy'
23 import { throwIfNotValid } from '../utils'
24 import { VideoModel } from './video'
25
26 @Table({
27 tableName: 'videoStreamingPlaylist',
28 indexes: [
29 {
30 fields: [ 'videoId' ]
31 },
32 {
33 fields: [ 'videoId', 'type' ],
34 unique: true
35 },
36 {
37 fields: [ 'p2pMediaLoaderInfohashes' ],
38 using: 'gin'
39 }
40 ]
41 })
42 export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<VideoStreamingPlaylistModel>>> {
43 @CreatedAt
44 createdAt: Date
45
46 @UpdatedAt
47 updatedAt: Date
48
49 @AllowNull(false)
50 @Column
51 type: VideoStreamingPlaylistType
52
53 @AllowNull(false)
54 @Column
55 playlistFilename: string
56
57 @AllowNull(true)
58 @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url', true))
59 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
60 playlistUrl: string
61
62 @AllowNull(false)
63 @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
64 @Column(DataType.ARRAY(DataType.STRING))
65 p2pMediaLoaderInfohashes: string[]
66
67 @AllowNull(false)
68 @Column
69 p2pMediaLoaderPeerVersion: number
70
71 @AllowNull(false)
72 @Column
73 segmentsSha256Filename: string
74
75 @AllowNull(true)
76 @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url', true))
77 @Column
78 segmentsSha256Url: string
79
80 @ForeignKey(() => VideoModel)
81 @Column
82 videoId: number
83
84 @BelongsTo(() => VideoModel, {
85 foreignKey: {
86 allowNull: false
87 },
88 onDelete: 'CASCADE'
89 })
90 Video: VideoModel
91
92 @HasMany(() => VideoFileModel, {
93 foreignKey: {
94 allowNull: true
95 },
96 onDelete: 'CASCADE'
97 })
98 VideoFiles: VideoFileModel[]
99
100 @HasMany(() => VideoRedundancyModel, {
101 foreignKey: {
102 allowNull: false
103 },
104 onDelete: 'CASCADE',
105 hooks: true
106 })
107 RedundancyVideos: VideoRedundancyModel[]
108
109 static doesInfohashExistCached = memoizee(VideoStreamingPlaylistModel.doesInfohashExist, {
110 promise: true,
111 max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
112 maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
113 })
114
115 static doesInfohashExist (infoHash: string) {
116 const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
117
118 return doesExist(query, { infoHash })
119 }
120
121 static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
122 const hashes: string[] = []
123
124 // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
125 for (let i = 0; i < files.length; i++) {
126 hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
127 }
128
129 return hashes
130 }
131
132 static listByIncorrectPeerVersion () {
133 const query = {
134 where: {
135 p2pMediaLoaderPeerVersion: {
136 [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
137 }
138 },
139 include: [
140 {
141 model: VideoModel.unscoped(),
142 required: true
143 }
144 ]
145 }
146
147 return VideoStreamingPlaylistModel.findAll(query)
148 }
149
150 static loadWithVideo (id: number) {
151 const options = {
152 include: [
153 {
154 model: VideoModel.unscoped(),
155 required: true
156 }
157 ]
158 }
159
160 return VideoStreamingPlaylistModel.findByPk(id, options)
161 }
162
163 static loadHLSPlaylistByVideo (videoId: number): Promise<MStreamingPlaylist> {
164 const options = {
165 where: {
166 type: VideoStreamingPlaylistType.HLS,
167 videoId
168 }
169 }
170
171 return VideoStreamingPlaylistModel.findOne(options)
172 }
173
174 static async loadOrGenerate (video: MVideo) {
175 let playlist = await VideoStreamingPlaylistModel.loadHLSPlaylistByVideo(video.id)
176 if (!playlist) playlist = new VideoStreamingPlaylistModel()
177
178 return Object.assign(playlist, { videoId: video.id, Video: video })
179 }
180
181 assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) {
182 const masterPlaylistUrl = this.getMasterPlaylistUrl(video)
183
184 this.p2pMediaLoaderInfohashes = VideoStreamingPlaylistModel.buildP2PMediaLoaderInfoHashes(masterPlaylistUrl, files)
185 }
186
187 getMasterPlaylistUrl (video: MVideo) {
188 if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid)
189
190 return this.playlistUrl
191 }
192
193 getSha256SegmentsUrl (video: MVideo) {
194 if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive)
195
196 return this.segmentsSha256Url
197 }
198
199 getStringType () {
200 if (this.type === VideoStreamingPlaylistType.HLS) return 'hls'
201
202 return 'unknown'
203 }
204
205 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
206 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
207 }
208
209 hasSameUniqueKeysThan (other: MStreamingPlaylist) {
210 return this.type === other.type &&
211 this.videoId === other.videoId
212 }
213
214 private getMasterPlaylistStaticPath (videoUUID: string) {
215 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.playlistFilename)
216 }
217
218 private getSha256SegmentsStaticPath (videoUUID: string, isLive: boolean) {
219 if (isLive) return join('/live', 'segments-sha256', videoUUID)
220
221 return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, this.segmentsSha256Filename)
222 }
223 }