video-state.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { Transaction } from 'sequelize'
  2. import { VideoState, VideoStateType } from '@peertube/peertube-models'
  3. import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
  4. import { logger } from '@server/helpers/logger.js'
  5. import { CONFIG } from '@server/initializers/config.js'
  6. import { sequelizeTypescript } from '@server/initializers/database.js'
  7. import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
  8. import { VideoModel } from '@server/models/video/video.js'
  9. import { MVideo, MVideoFullLight, MVideoUUID } from '@server/types/models/index.js'
  10. import { federateVideoIfNeeded } from './activitypub/videos/index.js'
  11. import { JobQueue } from './job-queue/index.js'
  12. import { Notifier } from './notifier/index.js'
  13. import { buildMoveJob } from './video-jobs.js'
  14. function buildNextVideoState (currentState?: VideoStateType) {
  15. if (currentState === VideoState.PUBLISHED) {
  16. throw new Error('Video is already in its final state')
  17. }
  18. if (
  19. currentState !== VideoState.TO_EDIT &&
  20. currentState !== VideoState.TO_TRANSCODE &&
  21. currentState !== VideoState.TO_MOVE_TO_EXTERNAL_STORAGE &&
  22. currentState !== VideoState.TO_MOVE_TO_FILE_SYSTEM &&
  23. CONFIG.TRANSCODING.ENABLED
  24. ) {
  25. return VideoState.TO_TRANSCODE
  26. }
  27. if (
  28. currentState !== VideoState.TO_MOVE_TO_EXTERNAL_STORAGE &&
  29. currentState !== VideoState.TO_MOVE_TO_FILE_SYSTEM &&
  30. CONFIG.OBJECT_STORAGE.ENABLED
  31. ) {
  32. return VideoState.TO_MOVE_TO_EXTERNAL_STORAGE
  33. }
  34. return VideoState.PUBLISHED
  35. }
  36. function moveToNextState (options: {
  37. video: MVideoUUID
  38. previousVideoState?: VideoStateType
  39. isNewVideo?: boolean // Default true
  40. }) {
  41. const { video, previousVideoState, isNewVideo = true } = options
  42. return retryTransactionWrapper(() => {
  43. return sequelizeTypescript.transaction(async t => {
  44. // Maybe the video changed in database, refresh it
  45. const videoDatabase = await VideoModel.loadFull(video.uuid, t)
  46. // Video does not exist anymore
  47. if (!videoDatabase) return undefined
  48. // Already in its final state
  49. if (videoDatabase.state === VideoState.PUBLISHED) {
  50. return federateVideoIfNeeded(videoDatabase, false, t)
  51. }
  52. const newState = buildNextVideoState(videoDatabase.state)
  53. if (newState === VideoState.PUBLISHED) {
  54. return moveToPublishedState({ video: videoDatabase, previousVideoState, isNewVideo, transaction: t })
  55. }
  56. if (newState === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
  57. return moveToExternalStorageState({ video: videoDatabase, isNewVideo, transaction: t })
  58. }
  59. })
  60. })
  61. }
  62. // ---------------------------------------------------------------------------
  63. async function moveToExternalStorageState (options: {
  64. video: MVideoFullLight
  65. isNewVideo: boolean
  66. transaction: Transaction
  67. }) {
  68. const { video, isNewVideo, transaction } = options
  69. const videoJobInfo = await VideoJobInfoModel.load(video.id, transaction)
  70. const pendingTranscode = videoJobInfo?.pendingTranscode || 0
  71. // We want to wait all transcoding jobs before moving the video on an external storage
  72. if (pendingTranscode !== 0) return false
  73. const previousVideoState = video.state
  74. if (video.state !== VideoState.TO_MOVE_TO_EXTERNAL_STORAGE) {
  75. await video.setNewState(VideoState.TO_MOVE_TO_EXTERNAL_STORAGE, isNewVideo, transaction)
  76. }
  77. logger.info('Creating external storage move job for video %s.', video.uuid, { tags: [ video.uuid ] })
  78. try {
  79. await JobQueue.Instance.createJob(await buildMoveJob({ video, previousVideoState, isNewVideo, type: 'move-to-object-storage' }))
  80. return true
  81. } catch (err) {
  82. logger.error('Cannot add move to object storage job', { err })
  83. return false
  84. }
  85. }
  86. async function moveToFileSystemState (options: {
  87. video: MVideoFullLight
  88. isNewVideo: boolean
  89. transaction: Transaction
  90. }) {
  91. const { video, isNewVideo, transaction } = options
  92. const previousVideoState = video.state
  93. if (video.state !== VideoState.TO_MOVE_TO_FILE_SYSTEM) {
  94. await video.setNewState(VideoState.TO_MOVE_TO_FILE_SYSTEM, false, transaction)
  95. }
  96. logger.info('Creating move to file system job for video %s.', video.uuid, { tags: [ video.uuid ] })
  97. try {
  98. await JobQueue.Instance.createJob(await buildMoveJob({ video, previousVideoState, isNewVideo, type: 'move-to-file-system' }))
  99. return true
  100. } catch (err) {
  101. logger.error('Cannot add move to file system job', { err })
  102. return false
  103. }
  104. }
  105. // ---------------------------------------------------------------------------
  106. function moveToFailedTranscodingState (video: MVideo) {
  107. if (video.state === VideoState.TRANSCODING_FAILED) return
  108. return video.setNewState(VideoState.TRANSCODING_FAILED, false, undefined)
  109. }
  110. function moveToFailedMoveToObjectStorageState (video: MVideo) {
  111. if (video.state === VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED) return
  112. return video.setNewState(VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED, false, undefined)
  113. }
  114. function moveToFailedMoveToFileSystemState (video: MVideo) {
  115. if (video.state === VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED) return
  116. return video.setNewState(VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED, false, undefined)
  117. }
  118. // ---------------------------------------------------------------------------
  119. export {
  120. buildNextVideoState,
  121. moveToFailedMoveToFileSystemState,
  122. moveToExternalStorageState,
  123. moveToFileSystemState,
  124. moveToFailedTranscodingState,
  125. moveToFailedMoveToObjectStorageState,
  126. moveToNextState
  127. }
  128. // ---------------------------------------------------------------------------
  129. async function moveToPublishedState (options: {
  130. video: MVideoFullLight
  131. isNewVideo: boolean
  132. transaction: Transaction
  133. previousVideoState?: VideoStateType
  134. }) {
  135. const { video, isNewVideo, transaction, previousVideoState } = options
  136. const previousState = previousVideoState ?? video.state
  137. logger.info('Publishing video %s.', video.uuid, { isNewVideo, previousState, tags: [ video.uuid ] })
  138. await video.setNewState(VideoState.PUBLISHED, isNewVideo, transaction)
  139. await federateVideoIfNeeded(video, isNewVideo, transaction)
  140. if (previousState === VideoState.TO_EDIT) {
  141. Notifier.Instance.notifyOfFinishedVideoStudioEdition(video)
  142. return
  143. }
  144. if (isNewVideo) {
  145. Notifier.Instance.notifyOnNewVideoOrLiveIfNeeded(video)
  146. if (previousState === VideoState.TO_TRANSCODE) {
  147. Notifier.Instance.notifyOnVideoPublishedAfterTranscoding(video)
  148. }
  149. }
  150. }