aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/formatter/video-format-utils.ts2
-rw-r--r--server/models/video/sql/shared/video-tables.ts9
-rw-r--r--server/models/video/video-file.ts26
-rw-r--r--server/models/video/video-job-info.ts100
-rw-r--r--server/models/video/video-streaming-playlist.ts30
-rw-r--r--server/models/video/video.ts51
6 files changed, 196 insertions, 22 deletions
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index 8a54de3b0..b3c4f390d 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -1,6 +1,6 @@
1import { uuidToShort } from '@server/helpers/uuid' 1import { uuidToShort } from '@server/helpers/uuid'
2import { generateMagnetUri } from '@server/helpers/webtorrent' 2import { generateMagnetUri } from '@server/helpers/webtorrent'
3import { getLocalVideoFileMetadataUrl } from '@server/lib/video-paths' 3import { getLocalVideoFileMetadataUrl } from '@server/lib/video-urls'
4import { VideoFile } from '@shared/models/videos/video-file.model' 4import { VideoFile } from '@shared/models/videos/video-file.model'
5import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects' 5import { ActivityTagObject, ActivityUrlObject, VideoObject } from '../../../../shared/models/activitypub/objects'
6import { Video, VideoDetails } from '../../../../shared/models/videos' 6import { Video, VideoDetails } from '../../../../shared/models/videos'
diff --git a/server/models/video/sql/shared/video-tables.ts b/server/models/video/sql/shared/video-tables.ts
index 742d19099..75823864d 100644
--- a/server/models/video/sql/shared/video-tables.ts
+++ b/server/models/video/sql/shared/video-tables.ts
@@ -87,7 +87,8 @@ export class VideoTables {
87 'fps', 87 'fps',
88 'metadataUrl', 88 'metadataUrl',
89 'videoStreamingPlaylistId', 89 'videoStreamingPlaylistId',
90 'videoId' 90 'videoId',
91 'storage'
91 ] 92 ]
92 } 93 }
93 94
@@ -102,7 +103,8 @@ export class VideoTables {
102 'segmentsSha256Url', 103 'segmentsSha256Url',
103 'videoId', 104 'videoId',
104 'createdAt', 105 'createdAt',
105 'updatedAt' 106 'updatedAt',
107 'storage'
106 ]) 108 ])
107 } 109 }
108 110
@@ -258,7 +260,8 @@ export class VideoTables {
258 'originallyPublishedAt', 260 'originallyPublishedAt',
259 'channelId', 261 'channelId',
260 'createdAt', 262 'createdAt',
261 'updatedAt' 263 'updatedAt',
264 'moveJobsRunning'
262 ] 265 ]
263 } 266 }
264} 267}
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 09fc5288b..627c95763 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -23,9 +23,11 @@ import validator from 'validator'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub' 23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24import { logger } from '@server/helpers/logger' 24import { logger } from '@server/helpers/logger'
25import { extractVideo } from '@server/helpers/video' 25import { extractVideo } from '@server/helpers/video'
26import { getTorrentFilePath } from '@server/lib/video-paths' 26import { getHLSPublicFileUrl, getWebTorrentPublicFileUrl } from '@server/lib/object-storage'
27import { getFSTorrentFilePath } from '@server/lib/paths'
27import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models' 28import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
28import { AttributesOnly } from '@shared/core-utils' 29import { AttributesOnly } from '@shared/core-utils'
30import { VideoStorage } from '@shared/models'
29import { 31import {
30 isVideoFileExtnameValid, 32 isVideoFileExtnameValid,
31 isVideoFileInfoHashValid, 33 isVideoFileInfoHashValid,
@@ -214,6 +216,11 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
214 @Column 216 @Column
215 videoId: number 217 videoId: number
216 218
219 @AllowNull(false)
220 @Default(VideoStorage.FILE_SYSTEM)
221 @Column
222 storage: VideoStorage
223
217 @BelongsTo(() => VideoModel, { 224 @BelongsTo(() => VideoModel, {
218 foreignKey: { 225 foreignKey: {
219 allowNull: true 226 allowNull: true
@@ -273,7 +280,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
273 280
274 static async doesOwnedWebTorrentVideoFileExist (filename: string) { 281 static async doesOwnedWebTorrentVideoFileExist (filename: string) {
275 const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + 282 const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' +
276 'WHERE "filename" = $filename LIMIT 1' 283 `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1`
277 284
278 return doesExist(query, { filename }) 285 return doesExist(query, { filename })
279 } 286 }
@@ -450,9 +457,20 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
450 return !!this.videoStreamingPlaylistId 457 return !!this.videoStreamingPlaylistId
451 } 458 }
452 459
460 getObjectStorageUrl () {
461 if (this.isHLS()) {
462 return getHLSPublicFileUrl(this.fileUrl)
463 }
464
465 return getWebTorrentPublicFileUrl(this.fileUrl)
466 }
467
453 getFileUrl (video: MVideo) { 468 getFileUrl (video: MVideo) {
454 if (!this.Video) this.Video = video as VideoModel 469 if (this.storage === VideoStorage.OBJECT_STORAGE) {
470 return this.getObjectStorageUrl()
471 }
455 472
473 if (!this.Video) this.Video = video as VideoModel
456 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video) 474 if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
457 475
458 return this.fileUrl 476 return this.fileUrl
@@ -503,7 +521,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
503 removeTorrent () { 521 removeTorrent () {
504 if (!this.torrentFilename) return null 522 if (!this.torrentFilename) return null
505 523
506 const torrentPath = getTorrentFilePath(this) 524 const torrentPath = getFSTorrentFilePath(this)
507 return remove(torrentPath) 525 return remove(torrentPath)
508 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 526 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
509 } 527 }
diff --git a/server/models/video/video-job-info.ts b/server/models/video/video-job-info.ts
new file mode 100644
index 000000000..7c1fe6734
--- /dev/null
+++ b/server/models/video/video-job-info.ts
@@ -0,0 +1,100 @@
1import { Op, QueryTypes, Transaction } from 'sequelize'
2import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, IsInt, Model, Table, Unique, UpdatedAt } from 'sequelize-typescript'
3import { AttributesOnly } from '@shared/core-utils'
4import { VideoModel } from './video'
5
6@Table({
7 tableName: 'videoJobInfo',
8 indexes: [
9 {
10 fields: [ 'videoId' ],
11 where: {
12 videoId: {
13 [Op.ne]: null
14 }
15 }
16 }
17 ]
18})
19
20export class VideoJobInfoModel extends Model<Partial<AttributesOnly<VideoJobInfoModel>>> {
21 @CreatedAt
22 createdAt: Date
23
24 @UpdatedAt
25 updatedAt: Date
26
27 @AllowNull(false)
28 @Default(0)
29 @IsInt
30 @Column
31 pendingMove: number
32
33 @AllowNull(false)
34 @Default(0)
35 @IsInt
36 @Column
37 pendingTranscode: number
38
39 @ForeignKey(() => VideoModel)
40 @Unique
41 @Column
42 videoId: number
43
44 @BelongsTo(() => VideoModel, {
45 foreignKey: {
46 allowNull: false
47 },
48 onDelete: 'cascade'
49 })
50 Video: VideoModel
51
52 static load (videoId: number, transaction: Transaction) {
53 const where = {
54 videoId
55 }
56
57 return VideoJobInfoModel.findOne({ where, transaction })
58 }
59
60 static async increaseOrCreate (videoUUID: string, column: 'pendingMove' | 'pendingTranscode'): Promise<number> {
61 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } }
62
63 const [ { pendingMove } ] = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(`
64 INSERT INTO "videoJobInfo" ("videoId", "${column}", "createdAt", "updatedAt")
65 SELECT
66 "video"."id" AS "videoId", 1, NOW(), NOW()
67 FROM
68 "video"
69 WHERE
70 "video"."uuid" = $videoUUID
71 ON CONFLICT ("videoId") DO UPDATE
72 SET
73 "${column}" = "videoJobInfo"."${column}" + 1,
74 "updatedAt" = NOW()
75 RETURNING
76 "${column}"
77 `, options)
78
79 return pendingMove
80 }
81
82 static async decrease (videoUUID: string, column: 'pendingMove' | 'pendingTranscode'): Promise<number> {
83 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT, bind: { videoUUID } }
84
85 const [ { pendingMove } ] = await VideoJobInfoModel.sequelize.query<{ pendingMove: number }>(`
86 UPDATE
87 "videoJobInfo"
88 SET
89 "${column}" = "videoJobInfo"."${column}" - 1,
90 "updatedAt" = NOW()
91 FROM "video"
92 WHERE
93 "video"."id" = "videoJobInfo"."videoId" AND "video"."uuid" = $videoUUID
94 RETURNING
95 "${column}";
96 `, options)
97
98 return pendingMove
99 }
100}
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts
index d591a3134..3e9fd97c7 100644
--- a/server/models/video/video-streaming-playlist.ts
+++ b/server/models/video/video-streaming-playlist.ts
@@ -1,10 +1,25 @@
1import * as memoizee from 'memoizee' 1import * as memoizee from 'memoizee'
2import { join } from 'path' 2import { join } from 'path'
3import { Op } from 'sequelize' 3import { Op } from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' 4import {
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 Default,
11 ForeignKey,
12 HasMany,
13 Is,
14 Model,
15 Table,
16 UpdatedAt
17} from 'sequelize-typescript'
18import { getHLSPublicFileUrl } from '@server/lib/object-storage'
5import { VideoFileModel } from '@server/models/video/video-file' 19import { VideoFileModel } from '@server/models/video/video-file'
6import { MStreamingPlaylist, MVideo } from '@server/types/models' 20import { MStreamingPlaylist, MVideo } from '@server/types/models'
7import { AttributesOnly } from '@shared/core-utils' 21import { AttributesOnly } from '@shared/core-utils'
22import { VideoStorage } from '@shared/models'
8import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 23import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
9import { sha1 } from '../../helpers/core-utils' 24import { sha1 } from '../../helpers/core-utils'
10import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 25import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -81,6 +96,11 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
81 @Column 96 @Column
82 videoId: number 97 videoId: number
83 98
99 @AllowNull(false)
100 @Default(VideoStorage.FILE_SYSTEM)
101 @Column
102 storage: VideoStorage
103
84 @BelongsTo(() => VideoModel, { 104 @BelongsTo(() => VideoModel, {
85 foreignKey: { 105 foreignKey: {
86 allowNull: false 106 allowNull: false
@@ -185,12 +205,20 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi
185 } 205 }
186 206
187 getMasterPlaylistUrl (video: MVideo) { 207 getMasterPlaylistUrl (video: MVideo) {
208 if (this.storage === VideoStorage.OBJECT_STORAGE) {
209 return getHLSPublicFileUrl(this.playlistUrl)
210 }
211
188 if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid) 212 if (video.isOwned()) return WEBSERVER.URL + this.getMasterPlaylistStaticPath(video.uuid)
189 213
190 return this.playlistUrl 214 return this.playlistUrl
191 } 215 }
192 216
193 getSha256SegmentsUrl (video: MVideo) { 217 getSha256SegmentsUrl (video: MVideo) {
218 if (this.storage === VideoStorage.OBJECT_STORAGE) {
219 return getHLSPublicFileUrl(this.segmentsSha256Url)
220 }
221
194 if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive) 222 if (video.isOwned()) return WEBSERVER.URL + this.getSha256SegmentsStaticPath(video.uuid, video.isLive)
195 223
196 return this.segmentsSha256Url 224 return this.segmentsSha256Url
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 56a5b0e18..874ad168a 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -28,14 +28,16 @@ import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { uuidToShort } from '@server/helpers/uuid' 28import { uuidToShort } from '@server/helpers/uuid'
29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
30import { LiveManager } from '@server/lib/live/live-manager' 30import { LiveManager } from '@server/lib/live/live-manager'
31import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths' 31import { removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage'
32import { getHLSDirectory, getHLSRedundancyDirectory } from '@server/lib/paths'
33import { VideoPathManager } from '@server/lib/video-path-manager'
32import { getServerActor } from '@server/models/application/application' 34import { getServerActor } from '@server/models/application/application'
33import { ModelCache } from '@server/models/model-cache' 35import { ModelCache } from '@server/models/model-cache'
34import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' 36import { AttributesOnly, buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils'
35import { VideoFile } from '@shared/models/videos/video-file.model' 37import { VideoFile } from '@shared/models/videos/video-file.model'
36import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared' 38import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
37import { VideoObject } from '../../../shared/models/activitypub/objects' 39import { VideoObject } from '../../../shared/models/activitypub/objects'
38import { Video, VideoDetails, VideoRateType } from '../../../shared/models/videos' 40import { Video, VideoDetails, VideoRateType, VideoStorage } from '../../../shared/models/videos'
39import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 41import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
40import { VideoFilter } from '../../../shared/models/videos/video-query.type' 42import { VideoFilter } from '../../../shared/models/videos/video-query.type'
41import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 43import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -114,6 +116,7 @@ import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel
114import { VideoCommentModel } from './video-comment' 116import { VideoCommentModel } from './video-comment'
115import { VideoFileModel } from './video-file' 117import { VideoFileModel } from './video-file'
116import { VideoImportModel } from './video-import' 118import { VideoImportModel } from './video-import'
119import { VideoJobInfoModel } from './video-job-info'
117import { VideoLiveModel } from './video-live' 120import { VideoLiveModel } from './video-live'
118import { VideoPlaylistElementModel } from './video-playlist-element' 121import { VideoPlaylistElementModel } from './video-playlist-element'
119import { VideoShareModel } from './video-share' 122import { VideoShareModel } from './video-share'
@@ -732,6 +735,15 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
732 }) 735 })
733 VideoCaptions: VideoCaptionModel[] 736 VideoCaptions: VideoCaptionModel[]
734 737
738 @HasOne(() => VideoJobInfoModel, {
739 foreignKey: {
740 name: 'videoId',
741 allowNull: false
742 },
743 onDelete: 'cascade'
744 })
745 VideoJobInfo: VideoJobInfoModel
746
735 @BeforeDestroy 747 @BeforeDestroy
736 static async sendDelete (instance: MVideoAccountLight, options) { 748 static async sendDelete (instance: MVideoAccountLight, options) {
737 if (!instance.isOwned()) return undefined 749 if (!instance.isOwned()) return undefined
@@ -1641,9 +1653,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1641 getMaxQualityResolution () { 1653 getMaxQualityResolution () {
1642 const file = this.getMaxQualityFile() 1654 const file = this.getMaxQualityFile()
1643 const videoOrPlaylist = file.getVideoOrStreamingPlaylist() 1655 const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
1644 const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
1645 1656
1646 return getVideoFileResolution(originalFilePath) 1657 return VideoPathManager.Instance.makeAvailableVideoFile(videoOrPlaylist, file, originalFilePath => {
1658 return getVideoFileResolution(originalFilePath)
1659 })
1647 } 1660 }
1648 1661
1649 getDescriptionAPIPath () { 1662 getDescriptionAPIPath () {
@@ -1673,16 +1686,24 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1673 } 1686 }
1674 1687
1675 removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) { 1688 removeFileAndTorrent (videoFile: MVideoFile, isRedundancy = false) {
1676 const filePath = getVideoFilePath(this, videoFile, isRedundancy) 1689 const filePath = isRedundancy
1690 ? VideoPathManager.Instance.getFSRedundancyVideoFilePath(this, videoFile)
1691 : VideoPathManager.Instance.getFSVideoFileOutputPath(this, videoFile)
1677 1692
1678 const promises: Promise<any>[] = [ remove(filePath) ] 1693 const promises: Promise<any>[] = [ remove(filePath) ]
1679 if (!isRedundancy) promises.push(videoFile.removeTorrent()) 1694 if (!isRedundancy) promises.push(videoFile.removeTorrent())
1680 1695
1696 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
1697 promises.push(removeWebTorrentObjectStorage(videoFile))
1698 }
1699
1681 return Promise.all(promises) 1700 return Promise.all(promises)
1682 } 1701 }
1683 1702
1684 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) { 1703 async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
1685 const directoryPath = getHLSDirectory(this, isRedundancy) 1704 const directoryPath = isRedundancy
1705 ? getHLSRedundancyDirectory(this)
1706 : getHLSDirectory(this)
1686 1707
1687 await remove(directoryPath) 1708 await remove(directoryPath)
1688 1709
@@ -1698,6 +1719,10 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1698 await Promise.all( 1719 await Promise.all(
1699 streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent()) 1720 streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
1700 ) 1721 )
1722
1723 if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) {
1724 await removeHLSObjectStorage(streamingPlaylist, this)
1725 }
1701 } 1726 }
1702 } 1727 }
1703 1728
@@ -1741,16 +1766,16 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1741 this.privacy === VideoPrivacy.INTERNAL 1766 this.privacy === VideoPrivacy.INTERNAL
1742 } 1767 }
1743 1768
1744 async publishIfNeededAndSave (t: Transaction) { 1769 async setNewState (newState: VideoState, transaction: Transaction) {
1745 if (this.state !== VideoState.PUBLISHED) { 1770 if (this.state === newState) throw new Error('Cannot use same state ' + newState)
1746 this.state = VideoState.PUBLISHED 1771
1747 this.publishedAt = new Date() 1772 this.state = newState
1748 await this.save({ transaction: t })
1749 1773
1750 return true 1774 if (this.state === VideoState.PUBLISHED) {
1775 this.publishedAt = new Date()
1751 } 1776 }
1752 1777
1753 return false 1778 await this.save({ transaction })
1754 } 1779 }
1755 1780
1756 getBandwidthBits (videoFile: MVideoFile) { 1781 getBandwidthBits (videoFile: MVideoFile) {