aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorJelle Besseling <jelle@pingiun.com>2021-08-17 08:26:20 +0200
committerGitHub <noreply@github.com>2021-08-17 08:26:20 +0200
commit0305db28c98fd6cf43a3c50ba92c76215e99d512 (patch)
tree33b753a19728d9f453c1aa4f19b36ac797e5fe80 /server/models
parentf88ae8f5bc223579313b28582de9101944a4a814 (diff)
downloadPeerTube-0305db28c98fd6cf43a3c50ba92c76215e99d512.tar.gz
PeerTube-0305db28c98fd6cf43a3c50ba92c76215e99d512.tar.zst
PeerTube-0305db28c98fd6cf43a3c50ba92c76215e99d512.zip
Add support for saving video files to object storage (#4290)
* Add support for saving video files to object storage * Add support for custom url generation on s3 stored files Uses two config keys to support url generation that doesn't directly go to (compatible s3). Can be used to generate urls to any cache server or CDN. * Upload files to s3 concurrently and delete originals afterwards * Only publish after move to object storage is complete * Use base url instead of url template * Fix mistyped config field * Add rudenmentary way to download before transcode * Implement Chocobozzz suggestions https://github.com/Chocobozzz/PeerTube/pull/4290#issuecomment-891670478 The remarks in question: Try to use objectStorage prefix instead of s3 prefix for your function/variables/config names Prefer to use a tree for the config: s3.streaming_playlists_bucket -> object_storage.streaming_playlists.bucket Use uppercase for config: S3.STREAMING_PLAYLISTS_BUCKETINFO.bucket -> OBJECT_STORAGE.STREAMING_PLAYLISTS.BUCKET (maybe BUCKET_NAME instead of BUCKET) I suggest to rename moveJobsRunning to pendingMovingJobs (or better, create a dedicated videoJobInfo table with a pendingMove & videoId columns so we could also use this table to track pending transcoding jobs) https://github.com/Chocobozzz/PeerTube/pull/4290/files#diff-3e26d41ca4bda1de8e1747af70ca2af642abcc1e9e0bfb94239ff2165acfbde5R19 uses a string instead of an integer I think we should store the origin object storage URL in fileUrl, without base_url injection. Instead, inject the base_url at "runtime" so admins can easily change this configuration without running a script to update DB URLs * Import correct function * Support multipart upload * Remove import of node 15.0 module stream/promises * Extend maximum upload job length Using the same value as for redundancy downloading seems logical * Use dynamic part size for really large uploads Also adds very small part size for local testing * Fix decreasePendingMove query * Resolve various PR comments * Move to object storage after optimize * Make upload size configurable and increase default * Prune webtorrent files that are stored in object storage * Move files after transcoding jobs * Fix federation * Add video path manager * Support move to external storage job in client * Fix live object storage tests Co-authored-by: Chocobozzz <me@florianbigard.com>
Diffstat (limited to 'server/models')
-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) {