+ static listForApi (options: {
+ start: number
+ count: number
+ sort: string
+ target: VideoRedundanciesTarget
+ strategy?: string
+ }) {
+ const { start, count, sort, target, strategy } = options
+ const redundancyWhere: WhereOptions = {}
+ const videosWhere: WhereOptions = {}
+ let redundancySqlSuffix = ''
+
+ if (target === 'my-videos') {
+ Object.assign(videosWhere, { remote: false })
+ } else if (target === 'remote-videos') {
+ Object.assign(videosWhere, { remote: true })
+ Object.assign(redundancyWhere, { strategy: { [Op.ne]: null } })
+ redundancySqlSuffix = ' AND "videoRedundancy"."strategy" IS NOT NULL'
+ }
+
+ if (strategy) {
+ Object.assign(redundancyWhere, { strategy: strategy })
+ }
+
+ const videoFilterWhere = {
+ [Op.and]: [
+ {
+ [Op.or]: [
+ {
+ id: {
+ [Op.in]: literal(
+ '(' +
+ 'SELECT "videoId" FROM "videoFile" ' +
+ 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoFileId" = "videoFile".id' +
+ redundancySqlSuffix +
+ ')'
+ )
+ }
+ },
+ {
+ id: {
+ [Op.in]: literal(
+ '(' +
+ 'select "videoId" FROM "videoStreamingPlaylist" ' +
+ 'INNER JOIN "videoRedundancy" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id' +
+ redundancySqlSuffix +
+ ')'
+ )
+ }
+ }
+ ]
+ },
+
+ videosWhere
+ ]
+ }
+
+ // /!\ On video model /!\
+ const findOptions = {
+ offset: start,
+ limit: count,
+ order: getSort(sort),
+ include: [
+ {
+ required: false,
+ model: VideoFileModel,
+ include: [
+ {
+ model: VideoRedundancyModel.unscoped(),
+ required: false,
+ where: redundancyWhere
+ }
+ ]
+ },
+ {
+ required: false,
+ model: VideoStreamingPlaylistModel.unscoped(),
+ include: [
+ {
+ model: VideoRedundancyModel.unscoped(),
+ required: false,
+ where: redundancyWhere
+ },
+ {
+ model: VideoFileModel,
+ required: false
+ }
+ ]
+ }
+ ],
+ where: videoFilterWhere
+ }
+
+ // /!\ On video model /!\
+ const countOptions = {
+ where: videoFilterWhere
+ }
+
+ return Promise.all([
+ VideoModel.findAll(findOptions),
+
+ VideoModel.count(countOptions)
+ ]).then(([ data, total ]) => ({ total, data }))
+ }
+
+ static async getStats (strategy: VideoRedundancyStrategyWithManual) {
+ const actor = await getServerActor()
+
+ const sql = `WITH "tmp" AS ` +
+ `(` +
+ `SELECT "videoFile"."size" AS "videoFileSize", "videoStreamingFile"."size" AS "videoStreamingFileSize", ` +
+ `"videoFile"."videoId" AS "videoFileVideoId", "videoStreamingPlaylist"."videoId" AS "videoStreamingVideoId"` +
+ `FROM "videoRedundancy" AS "videoRedundancy" ` +
+ `LEFT JOIN "videoFile" AS "videoFile" ON "videoRedundancy"."videoFileId" = "videoFile"."id" ` +
+ `LEFT JOIN "videoStreamingPlaylist" ON "videoRedundancy"."videoStreamingPlaylistId" = "videoStreamingPlaylist"."id" ` +
+ `LEFT JOIN "videoFile" AS "videoStreamingFile" ` +
+ `ON "videoStreamingPlaylist"."id" = "videoStreamingFile"."videoStreamingPlaylistId" ` +
+ `WHERE "videoRedundancy"."strategy" = :strategy AND "videoRedundancy"."actorId" = :actorId` +
+ `), ` +
+ `"videoIds" AS (` +
+ `SELECT "videoFileVideoId" AS "videoId" FROM "tmp" ` +
+ `UNION SELECT "videoStreamingVideoId" AS "videoId" FROM "tmp" ` +
+ `) ` +
+ `SELECT ` +
+ `COALESCE(SUM("videoFileSize"), '0') + COALESCE(SUM("videoStreamingFileSize"), '0') AS "totalUsed", ` +
+ `(SELECT COUNT("videoIds"."videoId") FROM "videoIds") AS "totalVideos", ` +
+ `COUNT(*) AS "totalVideoFiles" ` +
+ `FROM "tmp"`
+
+ return VideoRedundancyModel.sequelize.query<any>(sql, {
+ replacements: { strategy, actorId: actor.id },
+ type: QueryTypes.SELECT
+ }).then(([ row ]) => ({
+ totalUsed: parseAggregateResult(row.totalUsed),
+ totalVideos: row.totalVideos,
+ totalVideoFiles: row.totalVideoFiles
+ }))
+ }
+
+ static toFormattedJSONStatic (video: MVideoForRedundancyAPI): VideoRedundancy {
+ const filesRedundancies: FileRedundancyInformation[] = []
+ const streamingPlaylistsRedundancies: StreamingPlaylistRedundancyInformation[] = []
+
+ for (const file of video.VideoFiles) {
+ for (const redundancy of file.RedundancyVideos) {
+ filesRedundancies.push({
+ id: redundancy.id,
+ fileUrl: redundancy.fileUrl,
+ strategy: redundancy.strategy,
+ createdAt: redundancy.createdAt,
+ updatedAt: redundancy.updatedAt,
+ expiresOn: redundancy.expiresOn,
+ size: file.size
+ })
+ }
+ }
+
+ for (const playlist of video.VideoStreamingPlaylists) {
+ const size = playlist.VideoFiles.reduce((a, b) => a + b.size, 0)
+
+ for (const redundancy of playlist.RedundancyVideos) {
+ streamingPlaylistsRedundancies.push({
+ id: redundancy.id,
+ fileUrl: redundancy.fileUrl,
+ strategy: redundancy.strategy,
+ createdAt: redundancy.createdAt,
+ updatedAt: redundancy.updatedAt,
+ expiresOn: redundancy.expiresOn,
+ size
+ })
+ }
+ }
+
+ return {
+ id: video.id,
+ name: video.name,
+ url: video.url,
+ uuid: video.uuid,
+
+ redundancies: {
+ files: filesRedundancies,
+ streamingPlaylists: streamingPlaylistsRedundancies
+ }
+ }
+ }
+
+ getVideo () {
+ if (this.VideoFile?.Video) return this.VideoFile.Video
+
+ if (this.VideoStreamingPlaylist?.Video) return this.VideoStreamingPlaylist.Video
+
+ return undefined
+ }
+
+ getVideoUUID () {
+ const video = this.getVideo()
+ if (!video) return undefined
+
+ return video.uuid
+ }
+
+ isOwned () {
+ return !!this.strategy
+ }
+
+ toActivityPubObject (this: MVideoRedundancyAP): CacheFileObject {
+ if (this.VideoStreamingPlaylist) {
+ return {
+ id: this.url,
+ type: 'CacheFile' as 'CacheFile',
+ object: this.VideoStreamingPlaylist.Video.url,
+ expires: this.expiresOn ? this.expiresOn.toISOString() : null,
+ url: {
+ type: 'Link',
+ mediaType: 'application/x-mpegURL',
+ href: this.fileUrl
+ }
+ }
+ }
+