video-file.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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. Scopes,
  15. DefaultScope
  16. } from 'sequelize-typescript'
  17. import {
  18. isVideoFileExtnameValid,
  19. isVideoFileInfoHashValid,
  20. isVideoFileResolutionValid,
  21. isVideoFileSizeValid,
  22. isVideoFPSResolutionValid
  23. } from '../../helpers/custom-validators/videos'
  24. import { parseAggregateResult, throwIfNotValid } from '../utils'
  25. import { VideoModel } from './video'
  26. import { VideoRedundancyModel } from '../redundancy/video-redundancy'
  27. import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
  28. import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
  29. import { MIMETYPES, MEMOIZE_LENGTH, MEMOIZE_TTL } from '../../initializers/constants'
  30. import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file'
  31. import { MStreamingPlaylistVideo, MVideo } from '@server/types/models'
  32. import * as memoizee from 'memoizee'
  33. import validator from 'validator'
  34. export enum ScopeNames {
  35. WITH_VIDEO = 'WITH_VIDEO',
  36. WITH_METADATA = 'WITH_METADATA'
  37. }
  38. @DefaultScope(() => ({
  39. attributes: {
  40. exclude: [ 'metadata' ]
  41. }
  42. }))
  43. @Scopes(() => ({
  44. [ScopeNames.WITH_VIDEO]: {
  45. include: [
  46. {
  47. model: VideoModel.unscoped(),
  48. required: true
  49. }
  50. ]
  51. },
  52. [ScopeNames.WITH_METADATA]: {
  53. attributes: {
  54. include: [ 'metadata' ]
  55. }
  56. }
  57. }))
  58. @Table({
  59. tableName: 'videoFile',
  60. indexes: [
  61. {
  62. fields: [ 'videoId' ],
  63. where: {
  64. videoId: {
  65. [Op.ne]: null
  66. }
  67. }
  68. },
  69. {
  70. fields: [ 'videoStreamingPlaylistId' ],
  71. where: {
  72. videoStreamingPlaylistId: {
  73. [Op.ne]: null
  74. }
  75. }
  76. },
  77. {
  78. fields: [ 'infoHash' ]
  79. },
  80. {
  81. fields: [ 'videoId', 'resolution', 'fps' ],
  82. unique: true,
  83. where: {
  84. videoId: {
  85. [Op.ne]: null
  86. }
  87. }
  88. },
  89. {
  90. fields: [ 'videoStreamingPlaylistId', 'resolution', 'fps' ],
  91. unique: true,
  92. where: {
  93. videoStreamingPlaylistId: {
  94. [Op.ne]: null
  95. }
  96. }
  97. }
  98. ]
  99. })
  100. export class VideoFileModel extends Model {
  101. @CreatedAt
  102. createdAt: Date
  103. @UpdatedAt
  104. updatedAt: Date
  105. @AllowNull(false)
  106. @Is('VideoFileResolution', value => throwIfNotValid(value, isVideoFileResolutionValid, 'resolution'))
  107. @Column
  108. resolution: number
  109. @AllowNull(false)
  110. @Is('VideoFileSize', value => throwIfNotValid(value, isVideoFileSizeValid, 'size'))
  111. @Column(DataType.BIGINT)
  112. size: number
  113. @AllowNull(false)
  114. @Is('VideoFileExtname', value => throwIfNotValid(value, isVideoFileExtnameValid, 'extname'))
  115. @Column
  116. extname: string
  117. @AllowNull(true)
  118. @Is('VideoFileInfohash', value => throwIfNotValid(value, isVideoFileInfoHashValid, 'info hash', true))
  119. @Column
  120. infoHash: string
  121. @AllowNull(false)
  122. @Default(-1)
  123. @Is('VideoFileFPS', value => throwIfNotValid(value, isVideoFPSResolutionValid, 'fps'))
  124. @Column
  125. fps: number
  126. @AllowNull(true)
  127. @Column(DataType.JSONB)
  128. metadata: any
  129. @AllowNull(true)
  130. @Column
  131. metadataUrl: string
  132. @ForeignKey(() => VideoModel)
  133. @Column
  134. videoId: number
  135. @BelongsTo(() => VideoModel, {
  136. foreignKey: {
  137. allowNull: true
  138. },
  139. onDelete: 'CASCADE'
  140. })
  141. Video: VideoModel
  142. @ForeignKey(() => VideoStreamingPlaylistModel)
  143. @Column
  144. videoStreamingPlaylistId: number
  145. @BelongsTo(() => VideoStreamingPlaylistModel, {
  146. foreignKey: {
  147. allowNull: true
  148. },
  149. onDelete: 'CASCADE'
  150. })
  151. VideoStreamingPlaylist: VideoStreamingPlaylistModel
  152. @HasMany(() => VideoRedundancyModel, {
  153. foreignKey: {
  154. allowNull: true
  155. },
  156. onDelete: 'CASCADE',
  157. hooks: true
  158. })
  159. RedundancyVideos: VideoRedundancyModel[]
  160. static doesInfohashExistCached = memoizee(VideoFileModel.doesInfohashExist, {
  161. promise: true,
  162. max: MEMOIZE_LENGTH.INFO_HASH_EXISTS,
  163. maxAge: MEMOIZE_TTL.INFO_HASH_EXISTS
  164. })
  165. static doesInfohashExist (infoHash: string) {
  166. const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1'
  167. const options = {
  168. type: QueryTypes.SELECT as QueryTypes.SELECT,
  169. bind: { infoHash },
  170. raw: true
  171. }
  172. return VideoModel.sequelize.query(query, options)
  173. .then(results => results.length === 1)
  174. }
  175. static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) {
  176. const videoFile = await VideoFileModel.loadWithVideoOrPlaylist(id, videoIdOrUUID)
  177. return !!videoFile
  178. }
  179. static loadWithMetadata (id: number) {
  180. return VideoFileModel.scope(ScopeNames.WITH_METADATA).findByPk(id)
  181. }
  182. static loadWithVideo (id: number) {
  183. return VideoFileModel.scope(ScopeNames.WITH_VIDEO).findByPk(id)
  184. }
  185. static loadWithVideoOrPlaylist (id: number, videoIdOrUUID: number | string) {
  186. const whereVideo = validator.isUUID(videoIdOrUUID + '')
  187. ? { uuid: videoIdOrUUID }
  188. : { id: videoIdOrUUID }
  189. const options = {
  190. where: {
  191. id
  192. },
  193. include: [
  194. {
  195. model: VideoModel.unscoped(),
  196. required: false,
  197. where: whereVideo
  198. },
  199. {
  200. model: VideoStreamingPlaylistModel.unscoped(),
  201. required: false,
  202. include: [
  203. {
  204. model: VideoModel.unscoped(),
  205. required: true,
  206. where: whereVideo
  207. }
  208. ]
  209. }
  210. ]
  211. }
  212. return VideoFileModel.findOne(options)
  213. .then(file => {
  214. // We used `required: false` so check we have at least a video or a streaming playlist
  215. if (!file.Video && !file.VideoStreamingPlaylist) return null
  216. return file
  217. })
  218. }
  219. static listByStreamingPlaylist (streamingPlaylistId: number, transaction: Transaction) {
  220. const query = {
  221. include: [
  222. {
  223. model: VideoModel.unscoped(),
  224. required: true,
  225. include: [
  226. {
  227. model: VideoStreamingPlaylistModel.unscoped(),
  228. required: true,
  229. where: {
  230. id: streamingPlaylistId
  231. }
  232. }
  233. ]
  234. }
  235. ],
  236. transaction
  237. }
  238. return VideoFileModel.findAll(query)
  239. }
  240. static getStats () {
  241. const webtorrentFilesQuery: FindOptions = {
  242. include: [
  243. {
  244. attributes: [],
  245. required: true,
  246. model: VideoModel.unscoped(),
  247. where: {
  248. remote: false
  249. }
  250. }
  251. ]
  252. }
  253. const hlsFilesQuery: FindOptions = {
  254. include: [
  255. {
  256. attributes: [],
  257. required: true,
  258. model: VideoStreamingPlaylistModel.unscoped(),
  259. include: [
  260. {
  261. attributes: [],
  262. model: VideoModel.unscoped(),
  263. required: true,
  264. where: {
  265. remote: false
  266. }
  267. }
  268. ]
  269. }
  270. ]
  271. }
  272. return Promise.all([
  273. VideoFileModel.aggregate('size', 'SUM', webtorrentFilesQuery),
  274. VideoFileModel.aggregate('size', 'SUM', hlsFilesQuery)
  275. ]).then(([ webtorrentResult, hlsResult ]) => ({
  276. totalLocalVideoFilesSize: parseAggregateResult(webtorrentResult) + parseAggregateResult(hlsResult)
  277. }))
  278. }
  279. // Redefine upsert because sequelize does not use an appropriate where clause in the update query with 2 unique indexes
  280. static async customUpsert (
  281. videoFile: MVideoFile,
  282. mode: 'streaming-playlist' | 'video',
  283. transaction: Transaction
  284. ) {
  285. const baseWhere = {
  286. fps: videoFile.fps,
  287. resolution: videoFile.resolution
  288. }
  289. if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId })
  290. else Object.assign(baseWhere, { videoId: videoFile.videoId })
  291. const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
  292. if (!element) return videoFile.save({ transaction })
  293. for (const k of Object.keys(videoFile.toJSON())) {
  294. element[k] = videoFile[k]
  295. }
  296. return element.save({ transaction })
  297. }
  298. static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) {
  299. const options = {
  300. where: { videoStreamingPlaylistId }
  301. }
  302. return VideoFileModel.destroy(options)
  303. }
  304. getVideoOrStreamingPlaylist (this: MVideoFileVideo | MVideoFileStreamingPlaylistVideo): MVideo | MStreamingPlaylistVideo {
  305. if (this.videoId) return (this as MVideoFileVideo).Video
  306. return (this as MVideoFileStreamingPlaylistVideo).VideoStreamingPlaylist
  307. }
  308. isAudio () {
  309. return !!MIMETYPES.AUDIO.EXT_MIMETYPE[this.extname]
  310. }
  311. isLive () {
  312. return this.size === -1
  313. }
  314. isHLS () {
  315. return !!this.videoStreamingPlaylistId
  316. }
  317. hasSameUniqueKeysThan (other: MVideoFile) {
  318. return this.fps === other.fps &&
  319. this.resolution === other.resolution &&
  320. (
  321. (this.videoId !== null && this.videoId === other.videoId) ||
  322. (this.videoStreamingPlaylistId !== null && this.videoStreamingPlaylistId === other.videoStreamingPlaylistId)
  323. )
  324. }
  325. }