video-redundancy.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. import {
  2. AllowNull,
  3. BeforeDestroy,
  4. BelongsTo,
  5. Column,
  6. CreatedAt,
  7. DataType,
  8. ForeignKey,
  9. Is,
  10. Model,
  11. Scopes,
  12. Table,
  13. UpdatedAt
  14. } from 'sequelize-typescript'
  15. import { ActorModel } from '../activitypub/actor'
  16. import { getVideoSort, throwIfNotValid } from '../utils'
  17. import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
  18. import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers'
  19. import { VideoFileModel } from '../video/video-file'
  20. import { getServerActor } from '../../helpers/utils'
  21. import { VideoModel } from '../video/video'
  22. import { VideoRedundancyStrategy } from '../../../shared/models/redundancy'
  23. import { logger } from '../../helpers/logger'
  24. import { CacheFileObject, VideoPrivacy } from '../../../shared'
  25. import { VideoChannelModel } from '../video/video-channel'
  26. import { ServerModel } from '../server/server'
  27. import { sample } from 'lodash'
  28. import { isTestInstance } from '../../helpers/core-utils'
  29. import * as Bluebird from 'bluebird'
  30. import * as Sequelize from 'sequelize'
  31. export enum ScopeNames {
  32. WITH_VIDEO = 'WITH_VIDEO'
  33. }
  34. @Scopes({
  35. [ ScopeNames.WITH_VIDEO ]: {
  36. include: [
  37. {
  38. model: () => VideoFileModel,
  39. required: true,
  40. include: [
  41. {
  42. model: () => VideoModel,
  43. required: true
  44. }
  45. ]
  46. }
  47. ]
  48. }
  49. })
  50. @Table({
  51. tableName: 'videoRedundancy',
  52. indexes: [
  53. {
  54. fields: [ 'videoFileId' ]
  55. },
  56. {
  57. fields: [ 'actorId' ]
  58. },
  59. {
  60. fields: [ 'url' ],
  61. unique: true
  62. }
  63. ]
  64. })
  65. export class VideoRedundancyModel extends Model<VideoRedundancyModel> {
  66. @CreatedAt
  67. createdAt: Date
  68. @UpdatedAt
  69. updatedAt: Date
  70. @AllowNull(false)
  71. @Column
  72. expiresOn: Date
  73. @AllowNull(false)
  74. @Is('VideoRedundancyFileUrl', value => throwIfNotValid(value, isUrlValid, 'fileUrl'))
  75. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
  76. fileUrl: string
  77. @AllowNull(false)
  78. @Is('VideoRedundancyUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  79. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
  80. url: string
  81. @AllowNull(true)
  82. @Column
  83. strategy: string // Only used by us
  84. @ForeignKey(() => VideoFileModel)
  85. @Column
  86. videoFileId: number
  87. @BelongsTo(() => VideoFileModel, {
  88. foreignKey: {
  89. allowNull: false
  90. },
  91. onDelete: 'cascade'
  92. })
  93. VideoFile: VideoFileModel
  94. @ForeignKey(() => ActorModel)
  95. @Column
  96. actorId: number
  97. @BelongsTo(() => ActorModel, {
  98. foreignKey: {
  99. allowNull: false
  100. },
  101. onDelete: 'cascade'
  102. })
  103. Actor: ActorModel
  104. @BeforeDestroy
  105. static async removeFile (instance: VideoRedundancyModel) {
  106. // Not us
  107. if (!instance.strategy) return
  108. const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
  109. const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
  110. logger.info('Removing duplicated video file %s.', logIdentifier)
  111. videoFile.Video.removeFile(videoFile)
  112. .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
  113. return undefined
  114. }
  115. static async loadLocalByFileId (videoFileId: number) {
  116. const actor = await getServerActor()
  117. const query = {
  118. where: {
  119. actorId: actor.id,
  120. videoFileId
  121. }
  122. }
  123. return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
  124. }
  125. static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
  126. const query = {
  127. where: {
  128. url
  129. },
  130. transaction
  131. }
  132. return VideoRedundancyModel.findOne(query)
  133. }
  134. static async isLocalByVideoUUIDExists (uuid: string) {
  135. const actor = await getServerActor()
  136. const query = {
  137. raw: true,
  138. attributes: [ 'id' ],
  139. where: {
  140. actorId: actor.id
  141. },
  142. include: [
  143. {
  144. attributes: [ ],
  145. model: VideoFileModel,
  146. required: true,
  147. include: [
  148. {
  149. attributes: [ ],
  150. model: VideoModel,
  151. required: true,
  152. where: {
  153. uuid
  154. }
  155. }
  156. ]
  157. }
  158. ]
  159. }
  160. return VideoRedundancyModel.findOne(query)
  161. .then(r => !!r)
  162. }
  163. static async getVideoSample (p: Bluebird<VideoModel[]>) {
  164. const rows = await p
  165. const ids = rows.map(r => r.id)
  166. const id = sample(ids)
  167. return VideoModel.loadWithFile(id, undefined, !isTestInstance())
  168. }
  169. static async findMostViewToDuplicate (randomizedFactor: number) {
  170. // On VideoModel!
  171. const query = {
  172. attributes: [ 'id', 'views' ],
  173. limit: randomizedFactor,
  174. order: getVideoSort('-views'),
  175. where: {
  176. privacy: VideoPrivacy.PUBLIC
  177. },
  178. include: [
  179. await VideoRedundancyModel.buildVideoFileForDuplication(),
  180. VideoRedundancyModel.buildServerRedundancyInclude()
  181. ]
  182. }
  183. return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
  184. }
  185. static async findTrendingToDuplicate (randomizedFactor: number) {
  186. // On VideoModel!
  187. const query = {
  188. attributes: [ 'id', 'views' ],
  189. subQuery: false,
  190. group: 'VideoModel.id',
  191. limit: randomizedFactor,
  192. order: getVideoSort('-trending'),
  193. where: {
  194. privacy: VideoPrivacy.PUBLIC
  195. },
  196. include: [
  197. await VideoRedundancyModel.buildVideoFileForDuplication(),
  198. VideoRedundancyModel.buildServerRedundancyInclude(),
  199. VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
  200. ]
  201. }
  202. return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
  203. }
  204. static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
  205. // On VideoModel!
  206. const query = {
  207. attributes: [ 'id', 'publishedAt' ],
  208. limit: randomizedFactor,
  209. order: getVideoSort('-publishedAt'),
  210. where: {
  211. privacy: VideoPrivacy.PUBLIC,
  212. views: {
  213. [ Sequelize.Op.gte ]: minViews
  214. }
  215. },
  216. include: [
  217. await VideoRedundancyModel.buildVideoFileForDuplication(),
  218. VideoRedundancyModel.buildServerRedundancyInclude()
  219. ]
  220. }
  221. return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
  222. }
  223. static async loadOldestLocalThatAlreadyExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number) {
  224. const expiredDate = new Date()
  225. expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
  226. const actor = await getServerActor()
  227. const query = {
  228. where: {
  229. actorId: actor.id,
  230. strategy,
  231. createdAt: {
  232. [ Sequelize.Op.lt ]: expiredDate
  233. }
  234. }
  235. }
  236. return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
  237. }
  238. static async getTotalDuplicated (strategy: VideoRedundancyStrategy) {
  239. const actor = await getServerActor()
  240. const options = {
  241. include: [
  242. {
  243. attributes: [],
  244. model: VideoRedundancyModel,
  245. required: true,
  246. where: {
  247. actorId: actor.id,
  248. strategy
  249. }
  250. }
  251. ]
  252. }
  253. return VideoFileModel.sum('size', options as any) // FIXME: typings
  254. }
  255. static async listLocalExpired () {
  256. const actor = await getServerActor()
  257. const query = {
  258. where: {
  259. actorId: actor.id,
  260. expiresOn: {
  261. [ Sequelize.Op.lt ]: new Date()
  262. }
  263. }
  264. }
  265. return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
  266. }
  267. static async listRemoteExpired () {
  268. const actor = await getServerActor()
  269. const query = {
  270. where: {
  271. actorId: {
  272. [Sequelize.Op.ne]: actor.id
  273. },
  274. expiresOn: {
  275. [ Sequelize.Op.lt ]: new Date()
  276. }
  277. }
  278. }
  279. return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
  280. }
  281. static async listLocalOfServer (serverId: number) {
  282. const actor = await getServerActor()
  283. const query = {
  284. where: {
  285. actorId: actor.id
  286. },
  287. include: [
  288. {
  289. model: VideoFileModel,
  290. required: true,
  291. include: [
  292. {
  293. model: VideoModel,
  294. required: true,
  295. include: [
  296. {
  297. attributes: [],
  298. model: VideoChannelModel.unscoped(),
  299. required: true,
  300. include: [
  301. {
  302. attributes: [],
  303. model: ActorModel.unscoped(),
  304. required: true,
  305. where: {
  306. serverId
  307. }
  308. }
  309. ]
  310. }
  311. ]
  312. }
  313. ]
  314. }
  315. ]
  316. }
  317. return VideoRedundancyModel.findAll(query)
  318. }
  319. static async getStats (strategy: VideoRedundancyStrategy) {
  320. const actor = await getServerActor()
  321. const query = {
  322. raw: true,
  323. attributes: [
  324. [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoFile.size')), '0'), 'totalUsed' ],
  325. [ Sequelize.fn('COUNT', Sequelize.fn('DISTINCT', Sequelize.col('videoId'))), 'totalVideos' ],
  326. [ Sequelize.fn('COUNT', Sequelize.col('videoFileId')), 'totalVideoFiles' ]
  327. ],
  328. where: {
  329. strategy,
  330. actorId: actor.id
  331. },
  332. include: [
  333. {
  334. attributes: [],
  335. model: VideoFileModel,
  336. required: true
  337. }
  338. ]
  339. }
  340. return VideoRedundancyModel.find(query as any) // FIXME: typings
  341. .then((r: any) => ({
  342. totalUsed: parseInt(r.totalUsed.toString(), 10),
  343. totalVideos: r.totalVideos,
  344. totalVideoFiles: r.totalVideoFiles
  345. }))
  346. }
  347. toActivityPubObject (): CacheFileObject {
  348. return {
  349. id: this.url,
  350. type: 'CacheFile' as 'CacheFile',
  351. object: this.VideoFile.Video.url,
  352. expires: this.expiresOn.toISOString(),
  353. url: {
  354. type: 'Link',
  355. mimeType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
  356. mediaType: VIDEO_EXT_MIMETYPE[ this.VideoFile.extname ] as any,
  357. href: this.fileUrl,
  358. height: this.VideoFile.resolution,
  359. size: this.VideoFile.size,
  360. fps: this.VideoFile.fps
  361. }
  362. }
  363. }
  364. // Don't include video files we already duplicated
  365. private static async buildVideoFileForDuplication () {
  366. const actor = await getServerActor()
  367. const notIn = Sequelize.literal(
  368. '(' +
  369. `SELECT "videoFileId" FROM "videoRedundancy" WHERE "actorId" = ${actor.id}` +
  370. ')'
  371. )
  372. return {
  373. attributes: [],
  374. model: VideoFileModel.unscoped(),
  375. required: true,
  376. where: {
  377. id: {
  378. [ Sequelize.Op.notIn ]: notIn
  379. }
  380. }
  381. }
  382. }
  383. private static buildServerRedundancyInclude () {
  384. return {
  385. attributes: [],
  386. model: VideoChannelModel.unscoped(),
  387. required: true,
  388. include: [
  389. {
  390. attributes: [],
  391. model: ActorModel.unscoped(),
  392. required: true,
  393. include: [
  394. {
  395. attributes: [],
  396. model: ServerModel.unscoped(),
  397. required: true,
  398. where: {
  399. redundancyAllowed: true
  400. }
  401. }
  402. ]
  403. }
  404. ]
  405. }
  406. }
  407. }