import { ThumbnailModel } from '../server/models/video/thumbnail'
import { AvatarModel } from '../server/models/avatar/avatar'
import { uniq, values } from 'lodash'
+import { ThumbnailType } from '@shared/models'
run()
.then(() => process.exit(0))
await pruneDirectory(CONFIG.STORAGE.REDUNDANCY_DIR, doesRedundancyExist),
- await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true)),
- await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false)),
+ await pruneDirectory(CONFIG.STORAGE.PREVIEWS_DIR, doesThumbnailExist(true, ThumbnailType.PREVIEW)),
+ await pruneDirectory(CONFIG.STORAGE.THUMBNAILS_DIR, doesThumbnailExist(false, ThumbnailType.MINIATURE)),
await pruneDirectory(CONFIG.STORAGE.AVATARS_DIR, doesAvatarExist)
)
}
}
-function doesThumbnailExist (keepOnlyOwned: boolean) {
+function doesThumbnailExist (keepOnlyOwned: boolean, type: ThumbnailType) {
return async (file: string) => {
- const thumbnail = await ThumbnailModel.loadByName(file)
+ const thumbnail = await ThumbnailModel.loadWithVideoByName(file, type)
if (!thumbnail) return false
if (keepOnlyOwned) {
)
lazyStaticRouter.use(
- LAZY_STATIC_PATHS.PREVIEWS + ':uuid.jpg',
+ LAZY_STATIC_PATHS.PREVIEWS + ':filename',
asyncMiddleware(getPreview)
)
}
async function getPreview (req: express.Request, res: express.Response) {
- const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid)
+ const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename)
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
import * as cors from 'cors'
import * as express from 'express'
+import { join } from 'path'
+import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
+import { serveIndexHTML } from '@server/lib/client-html'
+import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
+import { MVideoFile, MVideoFullLight } from '@server/types/models'
+import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
+import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
+import { root } from '../helpers/core-utils'
+import { CONFIG, isEmailEnabled } from '../initializers/config'
import {
CONSTRAINTS_FIELDS,
DEFAULT_THEME_NAME,
STATIC_PATHS,
WEBSERVER
} from '../initializers/constants'
-import { cacheRoute } from '../middlewares/cache'
+import { getThemeOrDefault } from '../lib/plugins/theme-utils'
+import { getEnabledResolutions } from '../lib/video-transcoding'
import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
-import { VideoModel } from '../models/video/video'
+import { cacheRoute } from '../middlewares/cache'
import { UserModel } from '../models/account/user'
+import { VideoModel } from '../models/video/video'
import { VideoCommentModel } from '../models/video/video-comment'
-import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
-import { join } from 'path'
-import { root } from '../helpers/core-utils'
-import { getEnabledResolutions } from '../lib/video-transcoding'
-import { CONFIG, isEmailEnabled } from '../initializers/config'
-import { getPreview, getVideoCaption } from './lazy-static'
-import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
-import { MVideoFile, MVideoFullLight } from '@server/types/models'
-import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
-import { getThemeOrDefault } from '../lib/plugins/theme-utils'
-import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
-import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { serveIndexHTML } from '@server/lib/client-html'
const staticRouter = express.Router()
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 570
+const LAST_MIGRATION_VERSION = 575
// ---------------------------------------------------------------------------
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+ db: any
+}): Promise<void> {
+ {
+ const query = 'DELETE FROM "thumbnail" s1 ' +
+ 'USING (SELECT MIN(id) as id, "filename", "type" FROM "thumbnail" GROUP BY "filename", "type" HAVING COUNT(*) > 1) s2 ' +
+ 'WHERE s1."filename" = s2."filename" AND s1."type" = s2."type" AND s1.id <> s2.id'
+ await utils.sequelize.query(query)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
import * as request from 'request'
import * as sequelize from 'sequelize'
import { VideoLiveModel } from '@server/models/video/video-live'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import {
ActivityHashTagObject,
ActivityMagnetUrlObject,
ActivityUrlObject,
ActivityVideoUrlObject
} from '../../../shared/index'
-import { VideoObject } from '../../../shared/models/activitypub/objects'
+import { ActivityIconObject, VideoObject } from '../../../shared/models/activitypub/objects'
import { VideoPrivacy } from '../../../shared/models/videos'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import { addVideoShares, shareVideoByServerAndChannel } from './share'
import { addVideoComments } from './video-comments'
import { createRates } from './video-rates'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
const video = videoArg as MVideoAP
if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
if (videoUpdated.getPreview()) {
- const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated)
+ const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video)
const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
await videoUpdated.addAndSaveThumbnail(previewModel, t)
}
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
const previewIcon = getPreviewFromIcons(videoObject)
- const previewUrl = previewIcon
- ? previewIcon.url
- : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
+ const previewUrl = getPreviewUrl(previewIcon, videoCreated)
const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
function getPreviewFromIcons (videoObject: VideoObject) {
const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
- // FIXME: don't put a fallback here for compatibility with PeerTube <2.2
-
return maxBy(validIcons, 'width')
}
+
+function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoAccountLight) {
+ return previewIcon
+ ? previewIcon.url
+ : buildRemoteVideoBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
+}
import { VideoModel } from '../../models/video/video'
import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
import { doRequestAndSaveToFile } from '@server/helpers/requests'
+import { ThumbnailModel } from '@server/models/video/thumbnail'
+import { ThumbnailType } from '@shared/models'
+import { logger } from '@server/helpers/logger'
class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
return this.instance || (this.instance = new this())
}
- async getFilePathImpl (videoUUID: string) {
- const video = await VideoModel.loadByUUID(videoUUID)
- if (!video) return undefined
+ async getFilePathImpl (filename: string) {
+ const thumbnail = await ThumbnailModel.loadWithVideoByName(filename, ThumbnailType.PREVIEW)
+ if (!thumbnail) return undefined
- if (video.isOwned()) return { isOwned: true, path: video.getPreview().getPath() }
+ if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() }
- return this.loadRemoteFile(videoUUID)
+ return this.loadRemoteFile(thumbnail.Video.uuid)
}
protected async loadRemoteFile (key: string) {
const remoteUrl = preview.getFileUrl(video)
await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
+ logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
+
return { isOwned: false, path: destPath }
}
}
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
}
-function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) {
+function createPlaylistMiniatureFromUrl (downloadUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) {
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
const type = ThumbnailType.MINIATURE
- const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
+ // Only save the file URL if it is a remote playlist
+ const fileUrl = playlist.isOwned()
+ ? null
+ : downloadUrl
+
+ const thumbnailCreator = () => downloadImage(downloadUrl, basePath, filename, { width, height })
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
}
-function createVideoMiniatureFromUrl (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
+function createVideoMiniatureFromUrl (downloadUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
- const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
+ // Only save the file URL if it is a remote video
+ const fileUrl = video.isOwned()
+ ? null
+ : downloadUrl
+
+ const thumbnailCreator = () => downloadImage(downloadUrl, basePath, filename, { width, height })
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
}
+import { remove } from 'fs-extra'
import { join } from 'path'
import {
AfterDestroy,
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
+import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
+import { MThumbnailVideo, MVideoAccountLight } from '@server/types/models'
+import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { logger } from '../../helpers/logger'
-import { remove } from 'fs-extra'
import { CONFIG } from '../../initializers/config'
+import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
import { VideoModel } from './video'
import { VideoPlaylistModel } from './video-playlist'
-import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
-import { MVideoAccountLight } from '@server/types/models'
-import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
@Table({
tableName: 'thumbnail',
{
fields: [ 'videoPlaylistId' ],
unique: true
+ },
+ {
+ fields: [ 'filename', 'type' ],
+ unique: true
}
]
})
.catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err))
}
- static loadByName (filename: string) {
+ static loadWithVideoByName (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnailVideo> {
const query = {
where: {
- filename
- }
+ filename,
+ type: thumbnailType
+ },
+ include: [
+ {
+ model: VideoModel.unscoped(),
+ required: true
+ }
+ ]
}
return ThumbnailModel.findOne(query)
}
- static generateDefaultPreviewName (videoUUID: string) {
- return videoUUID + '.jpg'
- }
-
getFileUrl (video: MVideoAccountLight) {
const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
import { VideoTagModel } from './video-tag'
import { VideoViewModel } from './video-view'
+import { v4 as uuidv4 } from 'uuid'
export enum ScopeNames {
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
}
generateThumbnailName () {
- return this.uuid + '.jpg'
+ return uuidv4() + '.jpg'
}
getMiniature () {
}
generatePreviewName () {
- return this.uuid + '.jpg'
+ return uuidv4() + '.jpg'
}
hasPreview () {
+import { PickWith } from '@shared/core-utils'
import { ThumbnailModel } from '../../../models/video/thumbnail'
+import { MVideo } from './video'
+
+type Use<K extends keyof ThumbnailModel, M> = PickWith<ThumbnailModel, K, M>
+
+// ############################################################################
export type MThumbnail = Omit<ThumbnailModel, 'Video' | 'VideoPlaylist'>
+
+// ############################################################################
+
+export type MThumbnailVideo =
+ MThumbnail &
+ Use<'Video', MVideo>