ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

thumbnail.ts 5.7 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import { ActivityIconObject, ThumbnailType, type ThumbnailType_Type } from '@peertube/peertube-models'
  2. import { afterCommitIfTransaction } from '@server/helpers/database-utils.js'
  3. import { MThumbnail, MThumbnailVideo, MVideo, MVideoPlaylist } from '@server/types/models/index.js'
  4. import { remove } from 'fs-extra/esm'
  5. import { join } from 'path'
  6. import {
  7. AfterDestroy,
  8. AllowNull,
  9. BeforeCreate,
  10. BeforeUpdate,
  11. BelongsTo,
  12. Column,
  13. CreatedAt,
  14. DataType,
  15. Default,
  16. ForeignKey, Table,
  17. UpdatedAt
  18. } from 'sequelize-typescript'
  19. import { logger } from '../../helpers/logger.js'
  20. import { CONFIG } from '../../initializers/config.js'
  21. import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, WEBSERVER } from '../../initializers/constants.js'
  22. import { VideoPlaylistModel } from './video-playlist.js'
  23. import { VideoModel } from './video.js'
  24. import { SequelizeModel } from '../shared/sequelize-type.js'
  25. @Table({
  26. tableName: 'thumbnail',
  27. indexes: [
  28. {
  29. fields: [ 'videoId' ]
  30. },
  31. {
  32. fields: [ 'videoPlaylistId' ],
  33. unique: true
  34. },
  35. {
  36. fields: [ 'filename', 'type' ],
  37. unique: true
  38. }
  39. ]
  40. })
  41. export class ThumbnailModel extends SequelizeModel<ThumbnailModel> {
  42. @AllowNull(false)
  43. @Column
  44. filename: string
  45. @AllowNull(true)
  46. @Default(null)
  47. @Column
  48. height: number
  49. @AllowNull(true)
  50. @Default(null)
  51. @Column
  52. width: number
  53. @AllowNull(false)
  54. @Column
  55. type: ThumbnailType_Type
  56. @AllowNull(true)
  57. @Column(DataType.STRING(CONSTRAINTS_FIELDS.COMMONS.URL.max))
  58. fileUrl: string
  59. @AllowNull(true)
  60. @Column
  61. automaticallyGenerated: boolean
  62. @AllowNull(false)
  63. @Column
  64. onDisk: boolean
  65. @ForeignKey(() => VideoModel)
  66. @Column
  67. videoId: number
  68. @BelongsTo(() => VideoModel, {
  69. foreignKey: {
  70. allowNull: true
  71. },
  72. onDelete: 'CASCADE'
  73. })
  74. Video: Awaited<VideoModel>
  75. @ForeignKey(() => VideoPlaylistModel)
  76. @Column
  77. videoPlaylistId: number
  78. @BelongsTo(() => VideoPlaylistModel, {
  79. foreignKey: {
  80. allowNull: true
  81. },
  82. onDelete: 'CASCADE'
  83. })
  84. VideoPlaylist: Awaited<VideoPlaylistModel>
  85. @CreatedAt
  86. createdAt: Date
  87. @UpdatedAt
  88. updatedAt: Date
  89. // If this thumbnail replaced existing one, track the old name
  90. previousThumbnailFilename: string
  91. private static readonly types: { [ id in ThumbnailType_Type ]: { label: string, directory: string, staticPath: string } } = {
  92. [ThumbnailType.MINIATURE]: {
  93. label: 'miniature',
  94. directory: CONFIG.STORAGE.THUMBNAILS_DIR,
  95. staticPath: LAZY_STATIC_PATHS.THUMBNAILS
  96. },
  97. [ThumbnailType.PREVIEW]: {
  98. label: 'preview',
  99. directory: CONFIG.STORAGE.PREVIEWS_DIR,
  100. staticPath: LAZY_STATIC_PATHS.PREVIEWS
  101. }
  102. }
  103. @BeforeCreate
  104. @BeforeUpdate
  105. static removeOldFile (instance: ThumbnailModel, options) {
  106. return afterCommitIfTransaction(options.transaction, () => instance.removePreviousFilenameIfNeeded())
  107. }
  108. @AfterDestroy
  109. static removeFiles (instance: ThumbnailModel) {
  110. logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename)
  111. // Don't block the transaction
  112. instance.removeThumbnail()
  113. .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, { err }))
  114. }
  115. static loadByFilename (filename: string, thumbnailType: ThumbnailType_Type): Promise<MThumbnail> {
  116. const query = {
  117. where: {
  118. filename,
  119. type: thumbnailType
  120. }
  121. }
  122. return ThumbnailModel.findOne(query)
  123. }
  124. static loadWithVideoByFilename (filename: string, thumbnailType: ThumbnailType_Type): Promise<MThumbnailVideo> {
  125. const query = {
  126. where: {
  127. filename,
  128. type: thumbnailType
  129. },
  130. include: [
  131. {
  132. model: VideoModel.unscoped(),
  133. required: true
  134. }
  135. ]
  136. }
  137. return ThumbnailModel.findOne(query)
  138. }
  139. static listRemoteOnDisk () {
  140. return this.findAll<MThumbnail>({
  141. where: {
  142. onDisk: true
  143. },
  144. include: [
  145. {
  146. attributes: [ 'id' ],
  147. model: VideoModel.unscoped(),
  148. required: true,
  149. where: {
  150. remote: true
  151. }
  152. }
  153. ]
  154. })
  155. }
  156. // ---------------------------------------------------------------------------
  157. static buildPath (type: ThumbnailType_Type, filename: string) {
  158. const directory = ThumbnailModel.types[type].directory
  159. return join(directory, filename)
  160. }
  161. // ---------------------------------------------------------------------------
  162. getOriginFileUrl (videoOrPlaylist: MVideo | MVideoPlaylist) {
  163. const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
  164. if (videoOrPlaylist.isOwned()) return WEBSERVER.URL + staticPath
  165. return this.fileUrl
  166. }
  167. getLocalStaticPath () {
  168. return ThumbnailModel.types[this.type].staticPath + this.filename
  169. }
  170. getPath () {
  171. return ThumbnailModel.buildPath(this.type, this.filename)
  172. }
  173. getPreviousPath () {
  174. return ThumbnailModel.buildPath(this.type, this.previousThumbnailFilename)
  175. }
  176. removeThumbnail () {
  177. return remove(this.getPath())
  178. }
  179. removePreviousFilenameIfNeeded () {
  180. if (!this.previousThumbnailFilename) return
  181. const previousPath = this.getPreviousPath()
  182. remove(previousPath)
  183. .catch(err => logger.error('Cannot remove previous thumbnail file %s.', previousPath, { err }))
  184. this.previousThumbnailFilename = undefined
  185. }
  186. isOwned () {
  187. return !this.fileUrl
  188. }
  189. // ---------------------------------------------------------------------------
  190. toActivityPubObject (this: MThumbnail, video: MVideo): ActivityIconObject {
  191. return {
  192. type: 'Image',
  193. url: this.getOriginFileUrl(video),
  194. mediaType: 'image/jpeg',
  195. width: this.width,
  196. height: this.height
  197. }
  198. }
  199. }