video-file.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. import {
  2. AllowNull,
  3. BelongsTo,
  4. Column,
  5. CreatedAt,
  6. DataType,
  7. Default,
  8. ForeignKey,
  9. HasMany,
  10. Is,
  11. Model,
  12. Table,
  13. UpdatedAt
  14. } from 'sequelize-typescript'
  15. import {
  16. isVideoFileExtnameValid,
  17. isVideoFileInfoHashValid,
  18. isVideoFileResolutionValid,
  19. isVideoFileSizeValid,
  20. isVideoFPSResolutionValid
  21. } from '../../helpers/custom-validators/videos'
  22. import { parseAggregateResult, throwIfNotValid } from '../utils'
  23. import { VideoModel } from './video'
  24. import { VideoRedundancyModel } from '../redundancy/video-redundancy'
  25. import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
  26. import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
  27. import { MIMETYPES } from '../../initializers/constants'
  28. import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../typings/models/video/video-file'
  29. import { MStreamingPlaylist, MStreamingPlaylistVideo, MVideo } from '@server/typings/models'
  30. @Table({
  31. tableName: 'videoFile',
  32. indexes: [
  33. {
  34. fields: [ 'videoId' ],
  35. where: {
  36. videoId: {
  37. [Op.ne]: null
  38. }
  39. }
  40. },
  41. {
  42. fields: [ 'videoStreamingPlaylistId' ],
  43. where: {
  44. videoStreamingPlaylistId: {
  45. [Op.ne]: null
  46. }
  47. }
  48. },
  49. {
  50. fields: [ 'infoHash' ]
  51. },
  52. {
  53. fields: [ 'videoId', 'resolution', 'fps' ],
  54. unique: true,
  55. where: {
  56. videoId: {
  57. [Op.ne]: null
  58. }
  59. }
  60. },
  61. {
  62. fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
  63. unique: true,
  64. where: {
  65. videoStreamingPlaylistId: {
  66. [Op.ne]: null
  67. }
  68. }
  69. }
  70. ]
  71. })
  72. export class VideoFileModel extends Model<VideoFileModel> {
  73. @CreatedAt
  74. createdAt: Date
  75. @UpdatedAt
  76. updatedAt: Date
  77. @AllowNull(false)
  78. @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
  79. @Column
  80. resolution: number
  81. @AllowNull(false)
  82. @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
  83. @Column(DataType.BIGINT)
  84. size: number
  85. @AllowNull(false)
  86. @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
  87. @Column
  88. extname: string
  89. @AllowNull(false)
  90. @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash'))
  91. @Column
  92. infoHash: string
  93. @AllowNull(false)
  94. @Default(-1)
  95. @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
  96. @Column
  97. fps: number
  98. @ForeignKey(() => VideoModel)
  99. @Column
  100. videoId: number
  101. @BelongsTo(() => VideoModel, {
  102. foreignKey: {
  103. allowNull: true
  104. },
  105. onDelete: 'CASCADE'
  106. })
  107. Video: VideoModel
  108. @ForeignKey(() => VideoStreamingPlaylistModel)
  109. @Column
  110. videoStreamingPlaylistId: number
  111. @BelongsTo(() => VideoStreamingPlaylistModel, {
  112. foreignKey: {
  113. allowNull: true
  114. },
  115. onDelete: 'CASCADE'
  116. })
  117. VideoStreamingPlaylist: VideoStreamingPlaylistModel
  118. @HasMany(() => VideoRedundancyModel, {
  119. foreignKey: {
  120. allowNull: true
  121. },
  122. onDelete: 'CASCADE',
  123. hooks: true
  124. })
  125. RedundancyVideos: VideoRedundancyModel[]
  126. static doesInfohashExist (infoHash: string) {
  127. const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
  128. const options = {
  129. type: QueryTypes.SELECT as QueryTypes.SELECT,
  130. bind: { infoHash },
  131. raw: true
  132. }
  133. return VideoModel.sequelize.query(query, options)
  134. .then(results => results.length === 1)
  135. }
  136. static loadWithVideo (id: number) {
  137. const options = {
  138. include: [
  139. {
  140. model: VideoModel.unscoped(),
  141. required: true
  142. }
  143. ]
  144. }
  145. return VideoFileModel.findByPk(id, options)
  146. }
  147. static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
  148. const query = {
  149. include: [
  150. {
  151. model: VideoModel.unscoped(),
  152. required: true,
  153. include: [
  154. {
  155. model: VideoStreamingPlaylistModel.unscoped(),
  156. required: true,
  157. where: {
  158. id: streamingPlaylistId
  159. }
  160. }
  161. ]
  162. }
  163. ],
  164. transaction
  165. }
  166. return VideoFileModel.findAll(query)
  167. }
  168. static getStats () {
  169. const query: FindOptions = {
  170. include: [
  171. {
  172. attributes: [],
  173. model: VideoModel.unscoped(),
  174. where: {
  175. remote: false
  176. }
  177. }
  178. ]
  179. }
  180. return VideoFileModel.aggregate('size', 'SUM', query)
  181. .then(result => ({
  182. totalLocalVideoFilesSize: parseAggregateResult(result)
  183. }))
  184. }
  185. // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
  186. static async customUpsert (
  187. videoFile: MVideoFile,
  188. mode: 'streaming-playlist' | 'video',
  189. transaction: Transaction
  190. ) {
  191. const baseWhere = {
  192. fps: videoFile.fps,
  193. resolution: videoFile.resolution
  194. }
  195. if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
  196. else Object.assign(baseWhere, { videoId: videoFile.videoId })
  197. const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
  198. if (!element) return videoFile.save({ transaction })
  199. for (const k of Object.keys(videoFile.toJSON())) {
  200. element[k] = videoFile[k]
  201. }
  202. return element.save({ transaction })
  203. }
  204. getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
  205. if (this.videoId) return (this as MVideoFileVideo).Video
  206. return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
  207. }
  208. isAudio () {
  209. return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
  210. }
  211. hasSameUniqueKeysThan (other: MVideoFile) {
  212. return this.fps === other.fps &&
  213. this.resolution === other.resolution &&
  214. (
  215. (this.videoId !== null && this.videoId === other.videoId) ||
  216. (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
  217. )
  218. }
  219. }