video-streaming-playlist.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  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, MStreamingPlaylistVideo, MVideoFile } from '@server/typings/models'
  21. import { VideoFileModel } from '@server/models/video/video-file'
  22. import { getTorrentFileName, getTorrentFilePath, getVideoFilename } from '@server/lib/video-paths'
  23. import * as memoizee from 'memoizee'
  24. import { remove } from 'fs-extra'
  25. import { logger } from '@server/helpers/logger'
  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<VideoStreamingPlaylistModel> {
  43. @CreatedAt
  44. createdAt: Date
  45. @UpdatedAt
  46. updatedAt: Date
  47. @AllowNull(false)
  48. @Column
  49. type: VideoStreamingPlaylistType
  50. @AllowNull(false)
  51. @Is('PlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'playlist url'))
  52. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
  53. playlistUrl: string
  54. @AllowNull(false)
  55. @Is('VideoStreamingPlaylistInfoHashes', value => throwIfNotValid(value, v => isArrayOf(v, isVideoFileInfoHashValid), 'info hashes'))
  56. @Column(DataType.ARRAY(DataType.STRING))
  57. p2pMediaLoaderInfohashes: string[]
  58. @AllowNull(false)
  59. @Column
  60. p2pMediaLoaderPeerVersion: number
  61. @AllowNull(false)
  62. @Is('VideoStreamingSegmentsSha256Url', value => throwIfNotValid(value, isActivityPubUrlValid, 'segments sha256 url'))
  63. @Column
  64. segmentsSha256Url: string
  65. @ForeignKey(() => VideoModel)
  66. @Column
  67. videoId: number
  68. @BelongsTo(() => VideoModel, {
  69. foreignKey: {
  70. allowNull: false
  71. },
  72. onDelete: 'CASCADE'
  73. })
  74. Video: VideoModel
  75. @HasMany(() => VideoFileModel, {
  76. foreignKey: {
  77. allowNull: true
  78. },
  79. onDelete: 'CASCADE'
  80. })
  81. VideoFiles: VideoFileModel[]
  82. @HasMany(() => VideoRedundancyModel, {
  83. foreignKey: {
  84. allowNull: false
  85. },
  86. onDelete: 'CASCADE',
  87. hooks: true
  88. })
  89. RedundancyVideos: VideoRedundancyModel[]
  90. static doesInfohashExistCached = memoizee(VideoStreamingPlaylistModel.doesInfohashExist, {
  91. promise: true,
  92. max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
  93. maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
  94. })
  95. static doesInfohashExist (infoHash: string) {
  96. const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1'
  97. const options = {
  98. type: QueryTypes.SELECT as QueryTypes.SELECT,
  99. bind: { infoHash },
  100. raw: true
  101. }
  102. return VideoModel.sequelize.query<object>(query, options)
  103. .then(results => results.length === 1)
  104. }
  105. static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) {
  106. const hashes: string[] = []
  107. // https://github.com/Novage/p2p-media-loader/blob/master/p2p-media-loader-core/lib/p2p-media-manager.ts#L115
  108. for (let i = 0; i < files.length; i++) {
  109. hashes.push(sha1(`${P2P_MEDIA_LOADER_PEER_VERSION}${playlistUrl}+V${i}`))
  110. }
  111. return hashes
  112. }
  113. static listByIncorrectPeerVersion () {
  114. const query = {
  115. where: {
  116. p2pMediaLoaderPeerVersion: {
  117. [Op.ne]: P2P_MEDIA_LOADER_PEER_VERSION
  118. }
  119. }
  120. }
  121. return VideoStreamingPlaylistModel.findAll(query)
  122. }
  123. static loadWithVideo (id: number) {
  124. const options = {
  125. include: [
  126. {
  127. model: VideoModel.unscoped(),
  128. required: true
  129. }
  130. ]
  131. }
  132. return VideoStreamingPlaylistModel.findByPk(id, options)
  133. }
  134. static getHlsPlaylistFilename (resolution: number) {
  135. return resolution + '.m3u8'
  136. }
  137. static getMasterHlsPlaylistFilename () {
  138. return 'master.m3u8'
  139. }
  140. static getHlsSha256SegmentsFilename () {
  141. return 'segments-sha256.json'
  142. }
  143. static getHlsMasterPlaylistStaticPath (videoUUID: string) {
  144. return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
  145. }
  146. static getHlsPlaylistStaticPath (videoUUID: string, resolution: number) {
  147. return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsPlaylistFilename(resolution))
  148. }
  149. static getHlsSha256SegmentsStaticPath (videoUUID: string) {
  150. return join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
  151. }
  152. getStringType () {
  153. if (this.type === VideoStreamingPlaylistType.HLS) return 'hls'
  154. return 'unknown'
  155. }
  156. getVideoRedundancyUrl (baseUrlHttp: string) {
  157. return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getStringType() + '/' + this.Video.uuid
  158. }
  159. getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  160. return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
  161. }
  162. getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  163. return baseUrlHttp + STATIC_DOWNLOAD_PATHS.HLS_VIDEOS + getVideoFilename(this, videoFile)
  164. }
  165. getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  166. return baseUrlHttp + join(STATIC_PATHS.STREAMING_PLAYLISTS.HLS, this.Video.uuid, getVideoFilename(this, videoFile))
  167. }
  168. getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  169. return baseUrlHttp + join(STATIC_PATHS.TORRENTS, getTorrentFileName(this, videoFile))
  170. }
  171. getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
  172. return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
  173. }
  174. hasSameUniqueKeysThan (other: MStreamingPlaylist) {
  175. return this.type === other.type &&
  176. this.videoId === other.videoId
  177. }
  178. removeTorrent (this: MStreamingPlaylistVideo, videoFile: MVideoFile) {
  179. const torrentPath = getTorrentFilePath(this, videoFile)
  180. return remove(torrentPath)
  181. .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
  182. }
  183. }