video-streaming-playlist.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
  2. import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
  3. import { throwIfNotValid } from '../utils'
  4. import { VideoModel } from './video'
  5. import { VideoRedundancyModel } from '../redundancy/video-redundancy'
  6. import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
  7. import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
  8. import {
  9. CONSTRAINTS_FIELDS,
  10. MEMOIZE_LENGTH,
  11. MEMOIZE_TTL,
  12. P2P_MEDIA_LOADER_PEER_VERSION,
  13. STATIC_DOWNLOAD_PATHS,
  14. STATIC_PATHS
  15. } from '../../initializers/constants'
  16. import { join } from 'path'
  17. import { sha1 } from '../../helpers/core-utils'
  18. import { isArrayOf } from '../../helpers/custom-validators/misc'
  19. import { Op, QueryTypes } from 'sequelize'
  20. import { MStreamingPlaylist, MVideoFile } from '@server/typings/models'
  21. import { VideoFileModel } from '@server/models/video/video-file'
  22. import { getTorrentFileName, getVideoFilename } from '@server/lib/video-paths'
  23. import * as memoizee from 'memoizee'
  24. @Table({
  25. tableName: 'videoStreamingPlaylist',
  26. indexes: [
  27. {
  28. fields: [ 'videoId' ]
  29. },
  30. {
  31. fields: [ 'videoId', 'type' ],
  32. unique: true
  33. },
  34. {
  35. fields: [ 'p2pMediaLoaderInfohashes' ],
  36. using: 'gin'
  37. }
  38. ]
  39. })
  40. export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistModel> {
  41. @CreatedAt
  42. createdAt: Date
  43. @UpdatedAt
  44. updatedAt: Date
  45. @AllowNull(false)
  46. @Column
  47. type: VideoStreamingPlaylistType
  48. @AllowNull(false)
  49. @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url'))
  50. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
  51. playlistUrl: string
  52. @AllowNull(false)
  53. @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
  54. @Column(DataType.ARRAY(DataType.STRING))
  55. p2pMediaLoaderInfohashes: string[]
  56. @AllowNull(false)
  57. @Column
  58. p2pMediaLoaderPeerVersion: number
  59. @AllowNull(false)
  60. @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url'))
  61. @Column
  62. segmentsSha256Url: string
  63. @ForeignKey(() => VideoModel)
  64. @Column
  65. videoId: number
  66. @BelongsTo(() => VideoModel, {
  67. foreignKey: {
  68. allowNull: false
  69. },
  70. onDelete: 'CASCADE'
  71. })
  72. Video: VideoModel
  73. @HasMany(() => VideoFileModel, {
  74. foreignKey: {
  75. allowNull: true
  76. },
  77. onDelete: 'CASCADE'
  78. })
  79. VideoFiles: VideoFileModel[]
  80. @HasMany(() => VideoRedundancyModel, {
  81. foreignKey: {
  82. allowNull: false
  83. },
  84. onDelete: 'CASCADE',
  85. hooks: true
  86. })
  87. RedundancyVideos: VideoRedundancyModel[]
  88. static doesInfohashExistCached = memoizee(VideoStreamingPlaylistModel.doesInfohashExist, {
  89. promise: true,
  90. max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
  91. maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
  92. })
  93. static doesInfohashExist (infoHash: string) {
  94. const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
  95. const options = {
  96. type: QueryTypes.SELECT as QueryTypes.SELECT,
  97. bind: { infoHash },
  98. raw: true
  99. }
  100. return VideoModel.sequelize.query<object>(query, options)
  101. .then(results => results.length === 1)
  102. }
  103. static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
  104. const hashes: string[] = []
  105. // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
  106. for (let i = 0; i < files.length; i++) {
  107. hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
  108. }
  109. return hashes
  110. }
  111. static listByIncorrectPeerVersion () {
  112. const query = {
  113. where: {
  114. p2pMediaLoaderPeerVersion: {
  115. [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
  116. }
  117. }
  118. }
  119. return VideoStreamingPlaylistModel.findAll(query)
  120. }
  121. static loadWithVideo (id: number) {
  122. const options = {
  123. include: [
  124. {
  125. model: VideoModel.unscoped(),
  126. required: true
  127. }
  128. ]
  129. }
  130. return VideoStreamingPlaylistModel.findByPk(id, options)
  131. }
  132. static getHlsPlaylistFilename (resolution: number) {
  133. return resolution + '.m3u8'
  134. }
  135. static getMasterHlsPlaylistFilename () {
  136. return 'master.m3u8'
  137. }
  138. static getHlsSha256SegmentsFilename () {
  139. return 'segments-sha256.json'
  140. }
  141. static getHlsMasterPlaylistStaticPath (videoUUID: string) {
  142. return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
  143. }
  144. static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) {
  145. return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
  146. }
  147. static getHlsSha256SegmentsStaticPath (videoUUID: string) {
  148. return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
  149. }
  150. getStringType () {
  151. if (this.type === VideoStreamingPlaylistType.HLS) return 'hls'
  152. return 'unknown'
  153. }
  154. getVideoRedundancyUrl (baseUrlHttp: string) {
  155. return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
  156. }
  157. getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  158. return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
  159. }
  160. getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  161. return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile)
  162. }
  163. getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  164. return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile))
  165. }
  166. getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  167. return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile))
  168. }
  169. getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
  170. return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
  171. }
  172. hasSameUniqueKeysThan (other: MStreamingPlaylist) {
  173. return this.type === other.type &&
  174. this.videoId === other.videoId
  175. }
  176. }