video-redundancy.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793
  1. import { sample } from 'lodash'
  2. import { literal, Op, QueryTypes, Transaction, WhereOptions } from 'sequelize'
  3. import {
  4. AllowNull,
  5. BeforeDestroy,
  6. BelongsTo,
  7. Column,
  8. CreatedAt,
  9. DataType,
  10. ForeignKey,
  11. Is,
  12. Model,
  13. Scopes,
  14. Table,
  15. UpdatedAt
  16. } from 'sequelize-typescript'
  17. import { getServerActor } from '@server/models/application/application'
  18. import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models'
  19. import {
  20. CacheFileObject,
  21. FileRedundancyInformation,
  22. StreamingPlaylistRedundancyInformation,
  23. VideoPrivacy,
  24. VideoRedundanciesTarget,
  25. VideoRedundancy,
  26. VideoRedundancyStrategy,
  27. VideoRedundancyStrategyWithManual
  28. } from '@shared/models'
  29. import { AttributesOnly } from '@shared/typescript-utils'
  30. import { isTestInstance } from '../../helpers/core-utils'
  31. import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc'
  32. import { logger } from '../../helpers/logger'
  33. import { CONFIG } from '../../initializers/config'
  34. import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
  35. import { ActorModel } from '../actor/actor'
  36. import { ServerModel } from '../server/server'
  37. import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared'
  38. import { ScheduleVideoUpdateModel } from '../video/schedule-video-update'
  39. import { VideoModel } from '../video/video'
  40. import { VideoChannelModel } from '../video/video-channel'
  41. import { VideoFileModel } from '../video/video-file'
  42. import { VideoStreamingPlaylistModel } from '../video/video-streaming-playlist'
  43. export enum ScopeNames {
  44. WITH_VIDEO = 'WITH_VIDEO'
  45. }
  46. @Scopes(() => ({
  47. [ScopeNames.WITH_VIDEO]: {
  48. include: [
  49. {
  50. model: VideoFileModel,
  51. required: false,
  52. include: [
  53. {
  54. model: VideoModel,
  55. required: true
  56. }
  57. ]
  58. },
  59. {
  60. model: VideoStreamingPlaylistModel,
  61. required: false,
  62. include: [
  63. {
  64. model: VideoModel,
  65. required: true
  66. }
  67. ]
  68. }
  69. ]
  70. }
  71. }))
  72. @Table({
  73. tableName: 'videoRedundancy',
  74. indexes: [
  75. {
  76. fields: [ 'videoFileId' ]
  77. },
  78. {
  79. fields: [ 'actorId' ]
  80. },
  81. {
  82. fields: [ 'expiresOn' ]
  83. },
  84. {
  85. fields: [ 'url' ],
  86. unique: true
  87. }
  88. ]
  89. })
  90. export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedundancyModel>>> {
  91. @CreatedAt
  92. createdAt: Date
  93. @UpdatedAt
  94. updatedAt: Date
  95. @AllowNull(true)
  96. @Column
  97. expiresOn: Date
  98. @AllowNull(false)
  99. @Is('VideoRedundancyFileUrl', value => throwIfNotValid(value, isUrlValid, 'fileUrl'))
  100. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
  101. fileUrl: string
  102. @AllowNull(false)
  103. @Is('VideoRedundancyUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  104. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS_REDUNDANCY.URL.max))
  105. url: string
  106. @AllowNull(true)
  107. @Column
  108. strategy: string // Only used by us
  109. @ForeignKey(() => VideoFileModel)
  110. @Column
  111. videoFileId: number
  112. @BelongsTo(() => VideoFileModel, {
  113. foreignKey: {
  114. allowNull: true
  115. },
  116. onDelete: 'cascade'
  117. })
  118. VideoFile: VideoFileModel
  119. @ForeignKey(() => VideoStreamingPlaylistModel)
  120. @Column
  121. videoStreamingPlaylistId: number
  122. @BelongsTo(() => VideoStreamingPlaylistModel, {
  123. foreignKey: {
  124. allowNull: true
  125. },
  126. onDelete: 'cascade'
  127. })
  128. VideoStreamingPlaylist: VideoStreamingPlaylistModel
  129. @ForeignKey(() => ActorModel)
  130. @Column
  131. actorId: number
  132. @BelongsTo(() => ActorModel, {
  133. foreignKey: {
  134. allowNull: false
  135. },
  136. onDelete: 'cascade'
  137. })
  138. Actor: ActorModel
  139. @BeforeDestroy
  140. static async removeFile (instance: VideoRedundancyModel) {
  141. if (!instance.isOwned()) return
  142. if (instance.videoFileId) {
  143. const videoFile = await VideoFileModel.loadWithVideo(instance.videoFileId)
  144. const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}`
  145. logger.info('Removing duplicated video file %s.', logIdentifier)
  146. videoFile.Video.removeWebTorrentFile(videoFile, true)
  147. .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err }))
  148. }
  149. if (instance.videoStreamingPlaylistId) {
  150. const videoStreamingPlaylist = await VideoStreamingPlaylistModel.loadWithVideo(instance.videoStreamingPlaylistId)
  151. const videoUUID = videoStreamingPlaylist.Video.uuid
  152. logger.info('Removing duplicated video streaming playlist %s.', videoUUID)
  153. videoStreamingPlaylist.Video.removeStreamingPlaylistFiles(videoStreamingPlaylist, true)
  154. .catch(err => logger.error('Cannot delete video streaming playlist files of %s.', videoUUID, { err }))
  155. }
  156. return undefined
  157. }
  158. static async loadLocalByFileId (videoFileId: number): Promise<MVideoRedundancyVideo> {
  159. const actor = await getServerActor()
  160. const query = {
  161. where: {
  162. actorId: actor.id,
  163. videoFileId
  164. }
  165. }
  166. return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
  167. }
  168. static async listLocalByVideoId (videoId: number): Promise<MVideoRedundancyVideo[]> {
  169. const actor = await getServerActor()
  170. const queryStreamingPlaylist = {
  171. where: {
  172. actorId: actor.id
  173. },
  174. include: [
  175. {
  176. model: VideoStreamingPlaylistModel.unscoped(),
  177. required: true,
  178. include: [
  179. {
  180. model: VideoModel.unscoped(),
  181. required: true,
  182. where: {
  183. id: videoId
  184. }
  185. }
  186. ]
  187. }
  188. ]
  189. }
  190. const queryFiles = {
  191. where: {
  192. actorId: actor.id
  193. },
  194. include: [
  195. {
  196. model: VideoFileModel,
  197. required: true,
  198. include: [
  199. {
  200. model: VideoModel,
  201. required: true,
  202. where: {
  203. id: videoId
  204. }
  205. }
  206. ]
  207. }
  208. ]
  209. }
  210. return Promise.all([
  211. VideoRedundancyModel.findAll(queryStreamingPlaylist),
  212. VideoRedundancyModel.findAll(queryFiles)
  213. ]).then(([ r1, r2 ]) => r1.concat(r2))
  214. }
  215. static async loadLocalByStreamingPlaylistId (videoStreamingPlaylistId: number): Promise<MVideoRedundancyVideo> {
  216. const actor = await getServerActor()
  217. const query = {
  218. where: {
  219. actorId: actor.id,
  220. videoStreamingPlaylistId
  221. }
  222. }
  223. return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
  224. }
  225. static loadByIdWithVideo (id: number, transaction?: Transaction): Promise<MVideoRedundancyVideo> {
  226. const query = {
  227. where: { id },
  228. transaction
  229. }
  230. return VideoRedundancyModel.scope(ScopeNames.WITH_VIDEO).findOne(query)
  231. }
  232. static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoRedundancy> {
  233. const query = {
  234. where: {
  235. url
  236. },
  237. transaction
  238. }
  239. return VideoRedundancyModel.findOne(query)
  240. }
  241. static async isLocalByVideoUUIDExists (uuid: string) {
  242. const actor = await getServerActor()
  243. const query = {
  244. raw: true,
  245. attributes: [ 'id' ],
  246. where: {
  247. actorId: actor.id
  248. },
  249. include: [
  250. {
  251. attributes: [],
  252. model: VideoFileModel,
  253. required: true,
  254. include: [
  255. {
  256. attributes: [],
  257. model: VideoModel,
  258. required: true,
  259. where: {
  260. uuid
  261. }
  262. }
  263. ]
  264. }
  265. ]
  266. }
  267. return VideoRedundancyModel.findOne(query)
  268. .then(r => !!r)
  269. }
  270. static async getVideoSample (p: Promise<VideoModel[]>) {
  271. const rows = await p
  272. if (rows.length === 0) return undefined
  273. const ids = rows.map(r => r.id)
  274. const id = sample(ids)
  275. return VideoModel.loadWithFiles(id, undefined, !isTestInstance())
  276. }
  277. static async findMostViewToDuplicate (randomizedFactor: number) {
  278. const peertubeActor = await getServerActor()
  279. // On VideoModel!
  280. const query = {
  281. attributes: [ 'id', 'views' ],
  282. limit: randomizedFactor,
  283. order: getVideoSort('-views'),
  284. where: {
  285. privacy: VideoPrivacy.PUBLIC,
  286. isLive: false,
  287. ...this.buildVideoIdsForDuplication(peertubeActor)
  288. },
  289. include: [
  290. VideoRedundancyModel.buildServerRedundancyInclude()
  291. ]
  292. }
  293. return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
  294. }
  295. static async findTrendingToDuplicate (randomizedFactor: number) {
  296. const peertubeActor = await getServerActor()
  297. // On VideoModel!
  298. const query = {
  299. attributes: [ 'id', 'views' ],
  300. subQuery: false,
  301. group: 'VideoModel.id',
  302. limit: randomizedFactor,
  303. order: getVideoSort('-trending'),
  304. where: {
  305. privacy: VideoPrivacy.PUBLIC,
  306. isLive: false,
  307. ...this.buildVideoIdsForDuplication(peertubeActor)
  308. },
  309. include: [
  310. VideoRedundancyModel.buildServerRedundancyInclude(),
  311. VideoModel.buildTrendingQuery(CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS)
  312. ]
  313. }
  314. return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
  315. }
  316. static async findRecentlyAddedToDuplicate (randomizedFactor: number, minViews: number) {
  317. const peertubeActor = await getServerActor()
  318. // On VideoModel!
  319. const query = {
  320. attributes: [ 'id', 'publishedAt' ],
  321. limit: randomizedFactor,
  322. order: getVideoSort('-publishedAt'),
  323. where: {
  324. privacy: VideoPrivacy.PUBLIC,
  325. isLive: false,
  326. views: {
  327. [Op.gte]: minViews
  328. },
  329. ...this.buildVideoIdsForDuplication(peertubeActor)
  330. },
  331. include: [
  332. VideoRedundancyModel.buildServerRedundancyInclude(),
  333. // Required by publishedAt sort
  334. {
  335. model: ScheduleVideoUpdateModel.unscoped(),
  336. required: false
  337. }
  338. ]
  339. }
  340. return VideoRedundancyModel.getVideoSample(VideoModel.unscoped().findAll(query))
  341. }
  342. static async loadOldestLocalExpired (strategy: VideoRedundancyStrategy, expiresAfterMs: number): Promise<MVideoRedundancyVideo> {
  343. const expiredDate = new Date()
  344. expiredDate.setMilliseconds(expiredDate.getMilliseconds() - expiresAfterMs)
  345. const actor = await getServerActor()
  346. const query = {
  347. where: {
  348. actorId: actor.id,
  349. strategy,
  350. createdAt: {
  351. [Op.lt]: expiredDate
  352. }
  353. }
  354. }
  355. return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findOne(query)
  356. }
  357. static async listLocalExpired (): Promise<MVideoRedundancyVideo[]> {
  358. const actor = await getServerActor()
  359. const query = {
  360. where: {
  361. actorId: actor.id,
  362. expiresOn: {
  363. [Op.lt]: new Date()
  364. }
  365. }
  366. }
  367. return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
  368. }
  369. static async listRemoteExpired () {
  370. const actor = await getServerActor()
  371. const query = {
  372. where: {
  373. actorId: {
  374. [Op.ne]: actor.id
  375. },
  376. expiresOn: {
  377. [Op.lt]: new Date(),
  378. [Op.ne]: null
  379. }
  380. }
  381. }
  382. return VideoRedundancyModel.scope([ ScopeNames.WITH_VIDEO ]).findAll(query)
  383. }
  384. static async listLocalOfServer (serverId: number) {
  385. const actor = await getServerActor()
  386. const buildVideoInclude = () => ({
  387. model: VideoModel,
  388. required: true,
  389. include: [
  390. {
  391. attributes: [],
  392. model: VideoChannelModel.unscoped(),
  393. required: true,
  394. include: [
  395. {
  396. attributes: [],
  397. model: ActorModel.unscoped(),
  398. required: true,
  399. where: {
  400. serverId
  401. }
  402. }
  403. ]
  404. }
  405. ]
  406. })
  407. const query = {
  408. where: {
  409. [Op.and]: [
  410. {
  411. actorId: actor.id
  412. },
  413. {
  414. [Op.or]: [
  415. {
  416. '$VideoStreamingPlaylist.id$': {
  417. [Op.ne]: null
  418. }
  419. },
  420. {
  421. '$VideoFile.id$': {
  422. [Op.ne]: null
  423. }
  424. }
  425. ]
  426. }
  427. ]
  428. },
  429. include: [
  430. {
  431. model: VideoFileModel.unscoped(),
  432. required: false,
  433. include: [ buildVideoInclude() ]
  434. },
  435. {
  436. model: VideoStreamingPlaylistModel.unscoped(),
  437. required: false,
  438. include: [ buildVideoInclude() ]
  439. }
  440. ]
  441. }
  442. return VideoRedundancyModel.findAll(query)
  443. }
  444. static listForApi (options: {
  445. start: number
  446. count: number
  447. sort: string
  448. target: VideoRedundanciesTarget
  449. strategy?: string
  450. }) {
  451. const { start, count, sort, target, strategy } = options
  452. const redundancyWhere: WhereOptions = {}
  453. const videosWhere: WhereOptions = {}
  454. let redundancySqlSuffix = ''
  455. if (target === 'my-videos') {
  456. Object.assign(videosWhere, { remote: false })
  457. } else if (target === 'remote-videos') {
  458. Object.assign(videosWhere, { remote: true })
  459. Object.assign(redundancyWhere, { strategy: { [Op.ne]: null } })
  460. redundancySqlSuffix = ' AND "videoRedundancy"."strategy" IS NOT NULL'
  461. }
  462. if (strategy) {
  463. Object.assign(redundancyWhere, { strategy })
  464. }
  465. const videoFilterWhere = {
  466. [Op.and]: [
  467. {
  468. [Op.or]: [
  469. {
  470. id: {
  471. [Op.in]: literal(
  472. '(' +
  473. 'SELECT "videoId" FROM "videoFile" ' +
  474. 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoFileId" = "videoFile".id' +
  475. redundancySqlSuffix +
  476. ')'
  477. )
  478. }
  479. },
  480. {
  481. id: {
  482. [Op.in]: literal(
  483. '(' +
  484. 'select "videoId" FROM "videoStreamingPlaylist" ' +
  485. 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' +
  486. redundancySqlSuffix +
  487. ')'
  488. )
  489. }
  490. }
  491. ]
  492. },
  493. videosWhere
  494. ]
  495. }
  496. // /!\ On video model /!\
  497. const findOptions = {
  498. offset: start,
  499. limit: count,
  500. order: getSort(sort),
  501. include: [
  502. {
  503. required: false,
  504. model: VideoFileModel,
  505. include: [
  506. {
  507. model: VideoRedundancyModel.unscoped(),
  508. required: false,
  509. where: redundancyWhere
  510. }
  511. ]
  512. },
  513. {
  514. required: false,
  515. model: VideoStreamingPlaylistModel.unscoped(),
  516. include: [
  517. {
  518. model: VideoRedundancyModel.unscoped(),
  519. required: false,
  520. where: redundancyWhere
  521. },
  522. {
  523. model: VideoFileModel,
  524. required: false
  525. }
  526. ]
  527. }
  528. ],
  529. where: videoFilterWhere
  530. }
  531. // /!\ On video model /!\
  532. const countOptions = {
  533. where: videoFilterWhere
  534. }
  535. return Promise.all([
  536. VideoModel.findAll(findOptions),
  537. VideoModel.count(countOptions)
  538. ]).then(([ data, total ]) => ({ total, data }))
  539. }
  540. static async getStats (strategy: VideoRedundancyStrategyWithManual) {
  541. const actor = await getServerActor()
  542. const sql = `WITH "tmp" AS ` +
  543. `(` +
  544. `SELECT "videoFile"."size" AS "videoFileSize", "videoStreamingFile"."size" AS "videoStreamingFileSize", ` +
  545. `"videoFile"."videoId" AS "videoFileVideoId", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
  546. `FROM "videoRedundancy" AS "videoRedundancy" ` +
  547. `LEFT JOIN "videoFile" AS "videoFile" ON "videoRedundancy"."videoFileId" = "videoFile"."id" ` +
  548. `LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
  549. `LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
  550. `ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
  551. `WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
  552. `), ` +
  553. `"videoIds" AS (` +
  554. `SELECT "videoFileVideoId" AS "videoId" FROM "tmp" ` +
  555. `UNION SELECT "videoStreamingVideoId" AS "videoId" FROM "tmp" ` +
  556. `) ` +
  557. `SELECT ` +
  558. `COALESCE(SUM("videoFileSize"), '0') + COALESCE(SUM("videoStreamingFileSize"), '0') AS "totalUsed", ` +
  559. `(SELECT COUNT("videoIds"."videoId") FROM "videoIds") AS "totalVideos", ` +
  560. `COUNT(*) AS "totalVideoFiles" ` +
  561. `FROM "tmp"`
  562. return VideoRedundancyModel.sequelize.query<any>(sql, {
  563. replacements: { strategy, actorId: actor.id },
  564. type: QueryTypes.SELECT
  565. }).then(([ row ]) => ({
  566. totalUsed: parseAggregateResult(row.totalUsed),
  567. totalVideos: row.totalVideos,
  568. totalVideoFiles: row.totalVideoFiles
  569. }))
  570. }
  571. static toFormattedJSONStatic (video: MVideoForRedundancyAPI): VideoRedundancy {
  572. const filesRedundancies: FileRedundancyInformation[] = []
  573. const streamingPlaylistsRedundancies: StreamingPlaylistRedundancyInformation[] = []
  574. for (const file of video.VideoFiles) {
  575. for (const redundancy of file.RedundancyVideos) {
  576. filesRedundancies.push({
  577. id: redundancy.id,
  578. fileUrl: redundancy.fileUrl,
  579. strategy: redundancy.strategy,
  580. createdAt: redundancy.createdAt,
  581. updatedAt: redundancy.updatedAt,
  582. expiresOn: redundancy.expiresOn,
  583. size: file.size
  584. })
  585. }
  586. }
  587. for (const playlist of video.VideoStreamingPlaylists) {
  588. const size = playlist.VideoFiles.reduce((a, b) => a + b.size, 0)
  589. for (const redundancy of playlist.RedundancyVideos) {
  590. streamingPlaylistsRedundancies.push({
  591. id: redundancy.id,
  592. fileUrl: redundancy.fileUrl,
  593. strategy: redundancy.strategy,
  594. createdAt: redundancy.createdAt,
  595. updatedAt: redundancy.updatedAt,
  596. expiresOn: redundancy.expiresOn,
  597. size
  598. })
  599. }
  600. }
  601. return {
  602. id: video.id,
  603. name: video.name,
  604. url: video.url,
  605. uuid: video.uuid,
  606. redundancies: {
  607. files: filesRedundancies,
  608. streamingPlaylists: streamingPlaylistsRedundancies
  609. }
  610. }
  611. }
  612. getVideo () {
  613. if (this.VideoFile?.Video) return this.VideoFile.Video
  614. if (this.VideoStreamingPlaylist?.Video) return this.VideoStreamingPlaylist.Video
  615. return undefined
  616. }
  617. getVideoUUID () {
  618. const video = this.getVideo()
  619. if (!video) return undefined
  620. return video.uuid
  621. }
  622. isOwned () {
  623. return !!this.strategy
  624. }
  625. toActivityPubObject (this: MVideoRedundancyAP): CacheFileObject {
  626. if (this.VideoStreamingPlaylist) {
  627. return {
  628. id: this.url,
  629. type: 'CacheFile' as 'CacheFile',
  630. object: this.VideoStreamingPlaylist.Video.url,
  631. expires: this.expiresOn ? this.expiresOn.toISOString() : null,
  632. url: {
  633. type: 'Link',
  634. mediaType: 'application/x-mpegURL',
  635. href: this.fileUrl
  636. }
  637. }
  638. }
  639. return {
  640. id: this.url,
  641. type: 'CacheFile' as 'CacheFile',
  642. object: this.VideoFile.Video.url,
  643. expires: this.expiresOn ? this.expiresOn.toISOString() : null,
  644. url: {
  645. type: 'Link',
  646. mediaType: MIMETYPES.VIDEO.EXT_MIMETYPE[this.VideoFile.extname] as any,
  647. href: this.fileUrl,
  648. height: this.VideoFile.resolution,
  649. size: this.VideoFile.size,
  650. fps: this.VideoFile.fps
  651. }
  652. }
  653. }
  654. // Don't include video files we already duplicated
  655. private static buildVideoIdsForDuplication (peertubeActor: MActor) {
  656. const notIn = literal(
  657. '(' +
  658. `SELECT "videoFile"."videoId" AS "videoId" FROM "videoRedundancy" ` +
  659. `INNER JOIN "videoFile" ON "videoFile"."id" = "videoRedundancy"."videoFileId" ` +
  660. `WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
  661. `UNION ` +
  662. `SELECT "videoStreamingPlaylist"."videoId" AS "videoId" FROM "videoRedundancy" ` +
  663. `INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."id" = "videoRedundancy"."videoStreamingPlaylistId" ` +
  664. `WHERE "videoRedundancy"."actorId" = ${peertubeActor.id} ` +
  665. ')'
  666. )
  667. return {
  668. id: {
  669. [Op.notIn]: notIn
  670. }
  671. }
  672. }
  673. private static buildServerRedundancyInclude () {
  674. return {
  675. attributes: [],
  676. model: VideoChannelModel.unscoped(),
  677. required: true,
  678. include: [
  679. {
  680. attributes: [],
  681. model: ActorModel.unscoped(),
  682. required: true,
  683. include: [
  684. {
  685. attributes: [],
  686. model: ServerModel.unscoped(),
  687. required: true,
  688. where: {
  689. redundancyAllowed: true
  690. }
  691. }
  692. ]
  693. }
  694. ]
  695. }
  696. }
  697. }