aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.scss7
-rw-r--r--client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts1
-rw-r--r--server/lib/object-storage/videos.ts5
-rw-r--r--server/lib/transcoding/transcoding.ts28
-rw-r--r--server/models/video/video-caption.ts3
-rw-r--r--server/models/video/video-file.ts41
-rw-r--r--server/models/video/video.ts21
-rw-r--r--server/tests/api/transcoding/create-transcoding.ts14
-rw-r--r--shared/models/videos/caption/video-caption.model.ts1
10 files changed, 102 insertions, 21 deletions
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index 650448a74..2892d603d 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -183,7 +183,7 @@
183 [href]="videoCaption.captionPath" 183 [href]="videoCaption.captionPath"
184 >{{ videoCaption.language.label }}</a> 184 >{{ videoCaption.language.label }}</a>
185 185
186 <div i18n class="caption-entry-state">Already uploaded &#10004;</div> 186 <div i18n class="caption-entry-state">Already uploaded on {{ videoCaption.updatedAt | date }} &#10004;</div>
187 187
188 <span i18n class="caption-entry-edit" (click)="videoCaptionEditModal.show()">Edit</span> 188 <span i18n class="caption-entry-edit" (click)="videoCaptionEditModal.show()">Edit</span>
189 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span> 189 <span i18n class="caption-entry-delete" (click)="deleteCaption(videoCaption)">Delete</span>
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
index e8a6c6e42..a8075cc6d 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.scss
@@ -41,7 +41,6 @@ my-peertube-checkbox {
41 a.caption-entry-label { 41 a.caption-entry-label {
42 @include disable-default-a-behaviour; 42 @include disable-default-a-behaviour;
43 43
44 flex-grow: 1;
45 color: #000; 44 color: #000;
46 45
47 &:hover { 46 &:hover {
@@ -53,11 +52,13 @@ my-peertube-checkbox {
53 @include margin-right(20px); 52 @include margin-right(20px);
54 53
55 font-weight: bold; 54 font-weight: bold;
56 width: 150px; 55 min-width: 100px;
57 } 56 }
58 57
59 .caption-entry-state { 58 .caption-entry-state {
60 width: 200px; 59 @include margin-right(15px);
60
61 min-width: 250px;
61 62
62 &.caption-entry-state-create { 63 &.caption-entry-state-create {
63 color: #39CC0B; 64 color: #39CC0B;
diff --git a/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts b/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts
index 129e80bc0..8d578cae6 100644
--- a/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts
+++ b/client/src/app/shared/shared-main/video-caption/video-caption-edit.model.ts
@@ -6,6 +6,7 @@ export interface VideoCaptionEdit {
6 6
7 action?: 'CREATE' | 'REMOVE' | 'UPDATE' 7 action?: 'CREATE' | 'REMOVE' | 'UPDATE'
8 captionfile?: any 8 captionfile?: any
9 updatedAt?: string
9} 10}
10 11
11export type VideoCaptionWithPathEdit = VideoCaptionEdit & { captionPath?: string } 12export type VideoCaptionWithPathEdit = VideoCaptionEdit & { captionPath?: string }
diff --git a/server/lib/object-storage/videos.ts b/server/lib/object-storage/videos.ts
index 066b48ab0..66e738200 100644
--- a/server/lib/object-storage/videos.ts
+++ b/server/lib/object-storage/videos.ts
@@ -26,6 +26,10 @@ function removeHLSObjectStorage (playlist: MStreamingPlaylistVideo) {
26 return removePrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS) 26 return removePrefix(generateHLSObjectBaseStorageKey(playlist), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
27} 27}
28 28
29function removeHLSFileObjectStorage (playlist: MStreamingPlaylistVideo, filename: string) {
30 return removeObject(generateHLSObjectStorageKey(playlist, filename), CONFIG.OBJECT_STORAGE.STREAMING_PLAYLISTS)
31}
32
29function removeWebTorrentObjectStorage (videoFile: MVideoFile) { 33function removeWebTorrentObjectStorage (videoFile: MVideoFile) {
30 return removeObject(generateWebTorrentObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.VIDEOS) 34 return removeObject(generateWebTorrentObjectStorageKey(videoFile.filename), CONFIG.OBJECT_STORAGE.VIDEOS)
31} 35}
@@ -63,6 +67,7 @@ export {
63 storeHLSFile, 67 storeHLSFile,
64 68
65 removeHLSObjectStorage, 69 removeHLSObjectStorage,
70 removeHLSFileObjectStorage,
66 removeWebTorrentObjectStorage, 71 removeWebTorrentObjectStorage,
67 72
68 makeWebTorrentFileAvailable, 73 makeWebTorrentFileAvailable,
diff --git a/server/lib/transcoding/transcoding.ts b/server/lib/transcoding/transcoding.ts
index b64ce6e1f..69a973fbd 100644
--- a/server/lib/transcoding/transcoding.ts
+++ b/server/lib/transcoding/transcoding.ts
@@ -2,7 +2,9 @@ import { Job } from 'bull'
2import { copyFile, ensureDir, move, remove, stat } from 'fs-extra' 2import { copyFile, ensureDir, move, remove, stat } from 'fs-extra'
3import { basename, extname as extnameUtil, join } from 'path' 3import { basename, extname as extnameUtil, join } from 'path'
4import { toEven } from '@server/helpers/core-utils' 4import { toEven } from '@server/helpers/core-utils'
5import { retryTransactionWrapper } from '@server/helpers/database-utils'
5import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 6import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
7import { sequelizeTypescript } from '@server/initializers/database'
6import { MStreamingPlaylistFilesVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models' 8import { MStreamingPlaylistFilesVideo, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
7import { VideoResolution, VideoStorage } from '../../../shared/models/videos' 9import { VideoResolution, VideoStorage } from '../../../shared/models/videos'
8import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 10import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -29,8 +31,6 @@ import {
29} from '../paths' 31} from '../paths'
30import { VideoPathManager } from '../video-path-manager' 32import { VideoPathManager } from '../video-path-manager'
31import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' 33import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
32import { retryTransactionWrapper } from '@server/helpers/database-utils'
33import { sequelizeTypescript } from '@server/initializers/database'
34 34
35/** 35/**
36 * 36 *
@@ -259,6 +259,9 @@ async function onWebTorrentVideoFileTranscoding (
259 259
260 await createTorrentAndSetInfoHash(video, videoFile) 260 await createTorrentAndSetInfoHash(video, videoFile)
261 261
262 const oldFile = await VideoFileModel.loadWebTorrentFile({ videoId: video.id, fps: videoFile.fps, resolution: videoFile.resolution })
263 if (oldFile) await video.removeWebTorrentFileAndTorrent(oldFile)
264
262 await VideoFileModel.customUpsert(videoFile, 'video', undefined) 265 await VideoFileModel.customUpsert(videoFile, 'video', undefined)
263 video.VideoFiles = await video.$get('VideoFiles') 266 video.VideoFiles = await video.$get('VideoFiles')
264 267
@@ -311,17 +314,15 @@ async function generateHlsPlaylistCommon (options: {
311 await transcodeVOD(transcodeOptions) 314 await transcodeVOD(transcodeOptions)
312 315
313 // Create or update the playlist 316 // Create or update the playlist
314 const playlist = await retryTransactionWrapper(() => { 317 const { playlist, oldPlaylistFilename, oldSegmentsSha256Filename } = await retryTransactionWrapper(() => {
315 return sequelizeTypescript.transaction(async transaction => { 318 return sequelizeTypescript.transaction(async transaction => {
316 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video, transaction) 319 const playlist = await VideoStreamingPlaylistModel.loadOrGenerate(video, transaction)
317 320
318 if (!playlist.playlistFilename) { 321 const oldPlaylistFilename = playlist.playlistFilename
319 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive) 322 const oldSegmentsSha256Filename = playlist.segmentsSha256Filename
320 }
321 323
322 if (!playlist.segmentsSha256Filename) { 324 playlist.playlistFilename = generateHLSMasterPlaylistFilename(video.isLive)
323 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive) 325 playlist.segmentsSha256Filename = generateHlsSha256SegmentsFilename(video.isLive)
324 }
325 326
326 playlist.p2pMediaLoaderInfohashes = [] 327 playlist.p2pMediaLoaderInfohashes = []
327 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION 328 playlist.p2pMediaLoaderPeerVersion = P2P_MEDIA_LOADER_PEER_VERSION
@@ -330,10 +331,13 @@ async function generateHlsPlaylistCommon (options: {
330 331
331 await playlist.save({ transaction }) 332 await playlist.save({ transaction })
332 333
333 return playlist 334 return { playlist, oldPlaylistFilename, oldSegmentsSha256Filename }
334 }) 335 })
335 }) 336 })
336 337
338 if (oldPlaylistFilename) await video.removeStreamingPlaylistFile(playlist, oldPlaylistFilename)
339 if (oldSegmentsSha256Filename) await video.removeStreamingPlaylistFile(playlist, oldSegmentsSha256Filename)
340
337 // Build the new playlist file 341 // Build the new playlist file
338 const extname = extnameUtil(videoFilename) 342 const extname = extnameUtil(videoFilename)
339 const newVideoFile = new VideoFileModel({ 343 const newVideoFile = new VideoFileModel({
@@ -364,11 +368,15 @@ async function generateHlsPlaylistCommon (options: {
364 368
365 await createTorrentAndSetInfoHash(playlist, newVideoFile) 369 await createTorrentAndSetInfoHash(playlist, newVideoFile)
366 370
371 const oldFile = await VideoFileModel.loadHLSFile({ playlistId: playlist.id, fps: newVideoFile.fps, resolution: newVideoFile.resolution })
372 if (oldFile) await video.removeStreamingPlaylistVideoFile(playlist, oldFile)
373
367 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined) 374 const savedVideoFile = await VideoFileModel.customUpsert(newVideoFile, 'streaming-playlist', undefined)
368 375
369 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo 376 const playlistWithFiles = playlist as MStreamingPlaylistFilesVideo
370 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles') 377 playlistWithFiles.VideoFiles = await playlist.$get('VideoFiles')
371 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles) 378 playlist.assignP2PMediaLoaderInfoHashes(video, playlistWithFiles.VideoFiles)
379 playlist.storage = VideoStorage.FILE_SYSTEM
372 380
373 await playlist.save() 381 await playlist.save()
374 382
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts
index 6b240f116..5fbcd6e3b 100644
--- a/server/models/video/video-caption.ts
+++ b/server/models/video/video-caption.ts
@@ -195,7 +195,8 @@ export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaption
195 id: this.language, 195 id: this.language,
196 label: VideoCaptionModel.getLanguageLabel(this.language) 196 label: VideoCaptionModel.getLanguageLabel(this.language)
197 }, 197 },
198 captionPath: this.getCaptionStaticPath() 198 captionPath: this.getCaptionStaticPath(),
199 updatedAt: this.updatedAt.toISOString()
199 } 200 }
200 } 201 }
201 202
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts
index 4aaee1ffa..d4f07f85f 100644
--- a/server/models/video/video-file.ts
+++ b/server/models/video/video-file.ts
@@ -405,15 +405,16 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
405 mode: 'streaming-playlist' | 'video', 405 mode: 'streaming-playlist' | 'video',
406 transaction: Transaction 406 transaction: Transaction
407 ) { 407 ) {
408 const baseWhere = { 408 const baseFind = {
409 fps: videoFile.fps, 409 fps: videoFile.fps,
410 resolution: videoFile.resolution 410 resolution: videoFile.resolution,
411 transaction
411 } 412 }
412 413
413 if (mode === 'streaming-playlist') Object.assign(baseWhere, { videoStreamingPlaylistId: videoFile.videoStreamingPlaylistId }) 414 const element = mode === 'streaming-playlist'
414 else Object.assign(baseWhere, { videoId: videoFile.videoId }) 415 ? await VideoFileModel.loadHLSFile({ ...baseFind, playlistId: videoFile.videoStreamingPlaylistId })
416 : await VideoFileModel.loadWebTorrentFile({ ...baseFind, videoId: videoFile.videoId })
415 417
416 const element = await VideoFileModel.findOne({ where: baseWhere, transaction })
417 if (!element) return videoFile.save({ transaction }) 418 if (!element) return videoFile.save({ transaction })
418 419
419 for (const k of Object.keys(videoFile.toJSON())) { 420 for (const k of Object.keys(videoFile.toJSON())) {
@@ -423,6 +424,36 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>
423 return element.save({ transaction }) 424 return element.save({ transaction })
424 } 425 }
425 426
427 static async loadWebTorrentFile (options: {
428 videoId: number
429 fps: number
430 resolution: number
431 transaction?: Transaction
432 }) {
433 const where = {
434 fps: options.fps,
435 resolution: options.resolution,
436 videoId: options.videoId
437 }
438
439 return VideoFileModel.findOne({ where, transaction: options.transaction })
440 }
441
442 static async loadHLSFile (options: {
443 playlistId: number
444 fps: number
445 resolution: number
446 transaction?: Transaction
447 }) {
448 const where = {
449 fps: options.fps,
450 resolution: options.resolution,
451 videoStreamingPlaylistId: options.playlistId
452 }
453
454 return VideoFileModel.findOne({ where, transaction: options.transaction })
455 }
456
426 static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) { 457 static removeHLSFilesOfVideoId (videoStreamingPlaylistId: number) {
427 const options = { 458 const options = {
428 where: { videoStreamingPlaylistId } 459 where: { videoStreamingPlaylistId }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 55da53058..27e605be6 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -26,7 +26,7 @@ import {
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 27import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
28import { LiveManager } from '@server/lib/live/live-manager' 28import { LiveManager } from '@server/lib/live/live-manager'
29import { removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage' 29import { removeHLSFileObjectStorage, removeHLSObjectStorage, removeWebTorrentObjectStorage } from '@server/lib/object-storage'
30import { getHLSDirectory, getHLSRedundancyDirectory } from '@server/lib/paths' 30import { getHLSDirectory, getHLSRedundancyDirectory } from '@server/lib/paths'
31import { VideoPathManager } from '@server/lib/video-path-manager' 31import { VideoPathManager } from '@server/lib/video-path-manager'
32import { getServerActor } from '@server/models/application/application' 32import { getServerActor } from '@server/models/application/application'
@@ -1816,6 +1816,25 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1816 } 1816 }
1817 } 1817 }
1818 1818
1819 async removeStreamingPlaylistVideoFile (streamingPlaylist: MStreamingPlaylist, videoFile: MVideoFile) {
1820 const filePath = VideoPathManager.Instance.getFSHLSOutputPath(this, videoFile.filename)
1821 await videoFile.removeTorrent()
1822 await remove(filePath)
1823
1824 if (videoFile.storage === VideoStorage.OBJECT_STORAGE) {
1825 await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), videoFile.filename)
1826 }
1827 }
1828
1829 async removeStreamingPlaylistFile (streamingPlaylist: MStreamingPlaylist, filename: string) {
1830 const filePath = VideoPathManager.Instance.getFSHLSOutputPath(this, filename)
1831 await remove(filePath)
1832
1833 if (streamingPlaylist.storage === VideoStorage.OBJECT_STORAGE) {
1834 await removeHLSFileObjectStorage(streamingPlaylist.withVideo(this), filename)
1835 }
1836 }
1837
1819 isOutdated () { 1838 isOutdated () {
1820 if (this.isOwned()) return false 1839 if (this.isOwned()) return false
1821 1840
diff --git a/server/tests/api/transcoding/create-transcoding.ts b/server/tests/api/transcoding/create-transcoding.ts
index a4defdf51..e3867fdad 100644
--- a/server/tests/api/transcoding/create-transcoding.ts
+++ b/server/tests/api/transcoding/create-transcoding.ts
@@ -46,6 +46,8 @@ function runTests (objectStorage: boolean) {
46 let videoUUID: string 46 let videoUUID: string
47 let publishedAt: string 47 let publishedAt: string
48 48
49 let shouldBeDeleted: string[]
50
49 before(async function () { 51 before(async function () {
50 this.timeout(120000) 52 this.timeout(120000)
51 53
@@ -187,6 +189,12 @@ function runTests (objectStorage: boolean) {
187 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1) 189 expect(videoDetails.streamingPlaylists[0].files).to.have.lengthOf(1)
188 190
189 if (objectStorage) await checkFilesInObjectStorage(videoDetails) 191 if (objectStorage) await checkFilesInObjectStorage(videoDetails)
192
193 shouldBeDeleted = [
194 videoDetails.streamingPlaylists[0].files[0].fileUrl,
195 videoDetails.streamingPlaylists[0].playlistUrl,
196 videoDetails.streamingPlaylists[0].segmentsSha256Url
197 ]
190 } 198 }
191 199
192 await servers[0].config.updateExistingSubConfig({ 200 await servers[0].config.updateExistingSubConfig({
@@ -227,6 +235,12 @@ function runTests (objectStorage: boolean) {
227 } 235 }
228 }) 236 })
229 237
238 it('Should have correctly deleted previous files', async function () {
239 for (const fileUrl of shouldBeDeleted) {
240 await makeRawRequest(fileUrl, HttpStatusCode.NOT_FOUND_404)
241 }
242 })
243
230 it('Should not have updated published at attributes', async function () { 244 it('Should not have updated published at attributes', async function () {
231 const video = await servers[0].videos.get({ id: videoUUID }) 245 const video = await servers[0].videos.get({ id: videoUUID })
232 246
diff --git a/shared/models/videos/caption/video-caption.model.ts b/shared/models/videos/caption/video-caption.model.ts
index d3c73e1a6..6d5665006 100644
--- a/shared/models/videos/caption/video-caption.model.ts
+++ b/shared/models/videos/caption/video-caption.model.ts
@@ -3,4 +3,5 @@ import { VideoConstant } from '../video-constant.model'
3export interface VideoCaption { 3export interface VideoCaption {
4 language: VideoConstant<string> 4 language: VideoConstant<string>
5 captionPath: string 5 captionPath: string
6 updatedAt: string
6} 7}