aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/lazy-static.ts4
-rw-r--r--server/controllers/static.ts29
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/initializers/migrations/0575-duplicate-thumbnail.ts24
-rw-r--r--server/lib/activitypub/videos.ts18
-rw-r--r--server/lib/files-cache/videos-preview-cache.ts15
-rw-r--r--server/lib/thumbnail.ts18
-rw-r--r--server/models/video/thumbnail.ts31
-rw-r--r--server/models/video/video.ts5
-rw-r--r--server/types/models/video/thumbnail.ts12
10 files changed, 109 insertions, 49 deletions
diff --git a/server/controllers/lazy-static.ts b/server/controllers/lazy-static.ts
index 5c6369c9e..847d24fd4 100644
--- a/server/controllers/lazy-static.ts
+++ b/server/controllers/lazy-static.ts
@@ -18,7 +18,7 @@ lazyStaticRouter.use(
18) 18)
19 19
20lazyStaticRouter.use( 20lazyStaticRouter.use(
21 LAZY_STATIC_PATHS.PREVIEWS + ':uuid.jpg', 21 LAZY_STATIC_PATHS.PREVIEWS + ':filename',
22 asyncMiddleware(getPreview) 22 asyncMiddleware(getPreview)
23) 23)
24 24
@@ -71,7 +71,7 @@ async function getAvatar (req: express.Request, res: express.Response) {
71} 71}
72 72
73async function getPreview (req: express.Request, res: express.Response) { 73async function getPreview (req: express.Request, res: express.Response) {
74 const result = await VideosPreviewCache.Instance.getFilePath(req.params.uuid) 74 const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename)
75 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) 75 if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
76 76
77 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) 77 return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })
diff --git a/server/controllers/static.ts b/server/controllers/static.ts
index a645c496b..2064857eb 100644
--- a/server/controllers/static.ts
+++ b/server/controllers/static.ts
@@ -1,5 +1,15 @@
1import * as cors from 'cors' 1import * as cors from 'cors'
2import * as express from 'express' 2import * as express from 'express'
3import { join } from 'path'
4import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
5import { serveIndexHTML } from '@server/lib/client-html'
6import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
7import { MVideoFile, MVideoFullLight } from '@server/types/models'
8import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
9import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
10import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
11import { root } from '../helpers/core-utils'
12import { CONFIG, isEmailEnabled } from '../initializers/config'
3import { 13import {
4 CONSTRAINTS_FIELDS, 14 CONSTRAINTS_FIELDS,
5 DEFAULT_THEME_NAME, 15 DEFAULT_THEME_NAME,
@@ -11,24 +21,13 @@ import {
11 STATIC_PATHS, 21 STATIC_PATHS,
12 WEBSERVER 22 WEBSERVER
13} from '../initializers/constants' 23} from '../initializers/constants'
14import { cacheRoute } from '../middlewares/cache' 24import { getThemeOrDefault } from '../lib/plugins/theme-utils'
25import { getEnabledResolutions } from '../lib/video-transcoding'
15import { asyncMiddleware, videosDownloadValidator } from '../middlewares' 26import { asyncMiddleware, videosDownloadValidator } from '../middlewares'
16import { VideoModel } from '../models/video/video' 27import { cacheRoute } from '../middlewares/cache'
17import { UserModel } from '../models/account/user' 28import { UserModel } from '../models/account/user'
29import { VideoModel } from '../models/video/video'
18import { VideoCommentModel } from '../models/video/video-comment' 30import { VideoCommentModel } from '../models/video/video-comment'
19import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
20import { join } from 'path'
21import { root } from '../helpers/core-utils'
22import { getEnabledResolutions } from '../lib/video-transcoding'
23import { CONFIG, isEmailEnabled } from '../initializers/config'
24import { getPreview, getVideoCaption } from './lazy-static'
25import { VideoStreamingPlaylistType } from '@shared/models/videos/video-streaming-playlist.type'
26import { MVideoFile, MVideoFullLight } from '@server/types/models'
27import { getTorrentFilePath, getVideoFilePath } from '@server/lib/video-paths'
28import { getThemeOrDefault } from '../lib/plugins/theme-utils'
29import { getRegisteredPlugins, getRegisteredThemes } from '@server/controllers/api/config'
30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
31import { serveIndexHTML } from '@server/lib/client-html'
32 31
33const staticRouter = express.Router() 32const staticRouter = express.Router()
34 33
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 7beaca238..a9f7a8e58 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -24,7 +24,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
24 24
25// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
26 26
27const LAST_MIGRATION_VERSION = 570 27const LAST_MIGRATION_VERSION = 575
28 28
29// --------------------------------------------------------------------------- 29// ---------------------------------------------------------------------------
30 30
diff --git a/server/initializers/migrations/0575-duplicate-thumbnail.ts b/server/initializers/migrations/0575-duplicate-thumbnail.ts
new file mode 100644
index 000000000..4dbbe71d4
--- /dev/null
+++ b/server/initializers/migrations/0575-duplicate-thumbnail.ts
@@ -0,0 +1,24 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction
5 queryInterface: Sequelize.QueryInterface
6 sequelize: Sequelize.Sequelize
7 db: any
8}): Promise<void> {
9 {
10 const query = 'DELETE FROM "thumbnail" s1 ' +
11 'USING (SELECT MIN(id) as id, "filename", "type" FROM "thumbnail" GROUP BY "filename", "type" HAVING COUNT(*) > 1) s2 ' +
12 'WHERE s1."filename" = s2."filename" AND s1."type" = s2."type" AND s1.id <> s2.id'
13 await utils.sequelize.query(query)
14 }
15}
16
17function down (options) {
18 throw new Error('Not implemented.')
19}
20
21export {
22 up,
23 down
24}
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts
index 8545e5bad..b5a199e67 100644
--- a/server/lib/activitypub/videos.ts
+++ b/server/lib/activitypub/videos.ts
@@ -5,6 +5,7 @@ import { join } from 'path'
5import * as request from 'request' 5import * as request from 'request'
6import * as sequelize from 'sequelize' 6import * as sequelize from 'sequelize'
7import { VideoLiveModel } from '@server/models/video/video-live' 7import { VideoLiveModel } from '@server/models/video/video-live'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
8import { 9import {
9 ActivityHashTagObject, 10 ActivityHashTagObject,
10 ActivityMagnetUrlObject, 11 ActivityMagnetUrlObject,
@@ -15,7 +16,7 @@ import {
15 ActivityUrlObject, 16 ActivityUrlObject,
16 ActivityVideoUrlObject 17 ActivityVideoUrlObject
17} from '../../../shared/index' 18} from '../../../shared/index'
18import { VideoObject } from '../../../shared/models/activitypub/objects' 19import { ActivityIconObject, VideoObject } from '../../../shared/models/activitypub/objects'
19import { VideoPrivacy } from '../../../shared/models/videos' 20import { VideoPrivacy } from '../../../shared/models/videos'
20import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' 21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
21import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type' 22import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
@@ -76,7 +77,6 @@ import { sendCreateVideo, sendUpdateVideo } from './send'
76import { addVideoShares, shareVideoByServerAndChannel } from './share' 77import { addVideoShares, shareVideoByServerAndChannel } from './share'
77import { addVideoComments } from './video-comments' 78import { addVideoComments } from './video-comments'
78import { createRates } from './video-rates' 79import { createRates } from './video-rates'
79import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
80 80
81async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) { 81async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
82 const video = videoArg as MVideoAP 82 const video = videoArg as MVideoAP
@@ -360,7 +360,7 @@ async function updateVideoFromAP (options: {
360 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t) 360 if (thumbnailModel) await videoUpdated.addAndSaveThumbnail(thumbnailModel, t)
361 361
362 if (videoUpdated.getPreview()) { 362 if (videoUpdated.getPreview()) {
363 const previewUrl = videoUpdated.getPreview().getFileUrl(videoUpdated) 363 const previewUrl = getPreviewUrl(getPreviewFromIcons(videoObject), video)
364 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 364 const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
365 await videoUpdated.addAndSaveThumbnail(previewModel, t) 365 await videoUpdated.addAndSaveThumbnail(previewModel, t)
366 } 366 }
@@ -597,9 +597,7 @@ async function createVideo (videoObject: VideoObject, channel: MChannelAccountLi
597 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t) 597 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
598 598
599 const previewIcon = getPreviewFromIcons(videoObject) 599 const previewIcon = getPreviewFromIcons(videoObject)
600 const previewUrl = previewIcon 600 const previewUrl = getPreviewUrl(previewIcon, videoCreated)
601 ? previewIcon.url
602 : buildRemoteVideoBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
603 const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE) 601 const previewModel = createPlaceholderThumbnail(previewUrl, videoCreated, ThumbnailType.PREVIEW, PREVIEWS_SIZE)
604 602
605 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t) 603 if (thumbnailModel) await videoCreated.addAndSaveThumbnail(previewModel, t)
@@ -822,7 +820,11 @@ function getThumbnailFromIcons (videoObject: VideoObject) {
822function getPreviewFromIcons (videoObject: VideoObject) { 820function getPreviewFromIcons (videoObject: VideoObject) {
823 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth) 821 const validIcons = videoObject.icon.filter(i => i.width > PREVIEWS_SIZE.minWidth)
824 822
825 // FIXME: don't put a fallback here for compatibility with PeerTube <2.2
826
827 return maxBy(validIcons, 'width') 823 return maxBy(validIcons, 'width')
828} 824}
825
826function getPreviewUrl (previewIcon: ActivityIconObject, video: MVideoAccountLight) {
827 return previewIcon
828 ? previewIcon.url
829 : buildRemoteVideoBaseUrl(video, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
830}
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts
index d0d4fc5b5..51146d718 100644
--- a/server/lib/files-cache/videos-preview-cache.ts
+++ b/server/lib/files-cache/videos-preview-cache.ts
@@ -3,6 +3,9 @@ import { FILES_CACHE } from '../../initializers/constants'
3import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
4import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' 4import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
5import { doRequestAndSaveToFile } from '@server/helpers/requests' 5import { doRequestAndSaveToFile } from '@server/helpers/requests'
6import { ThumbnailModel } from '@server/models/video/thumbnail'
7import { ThumbnailType } from '@shared/models'
8import { logger } from '@server/helpers/logger'
6 9
7class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { 10class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
8 11
@@ -16,13 +19,13 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
16 return this.instance || (this.instance = new this()) 19 return this.instance || (this.instance = new this())
17 } 20 }
18 21
19 async getFilePathImpl (videoUUID: string) { 22 async getFilePathImpl (filename: string) {
20 const video = await VideoModel.loadByUUID(videoUUID) 23 const thumbnail = await ThumbnailModel.loadWithVideoByName(filename, ThumbnailType.PREVIEW)
21 if (!video) return undefined 24 if (!thumbnail) return undefined
22 25
23 if (video.isOwned()) return { isOwned: true, path: video.getPreview().getPath() } 26 if (thumbnail.Video.isOwned()) return { isOwned: true, path: thumbnail.getPath() }
24 27
25 return this.loadRemoteFile(videoUUID) 28 return this.loadRemoteFile(thumbnail.Video.uuid)
26 } 29 }
27 30
28 protected async loadRemoteFile (key: string) { 31 protected async loadRemoteFile (key: string) {
@@ -37,6 +40,8 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> {
37 const remoteUrl = preview.getFileUrl(video) 40 const remoteUrl = preview.getFileUrl(video)
38 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath) 41 await doRequestAndSaveToFile({ uri: remoteUrl }, destPath)
39 42
43 logger.debug('Fetched remote preview %s to %s.', remoteUrl, destPath)
44
40 return { isOwned: false, path: destPath } 45 return { isOwned: false, path: destPath }
41 } 46 }
42} 47}
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts
index dc86423f8..740b83acb 100644
--- a/server/lib/thumbnail.ts
+++ b/server/lib/thumbnail.ts
@@ -27,18 +27,28 @@ function createPlaylistMiniatureFromExisting (
27 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail }) 27 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, automaticallyGenerated, existingThumbnail })
28} 28}
29 29
30function createPlaylistMiniatureFromUrl (fileUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) { 30function createPlaylistMiniatureFromUrl (downloadUrl: string, playlist: MVideoPlaylistThumbnail, size?: ImageSize) {
31 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) 31 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size)
32 const type = ThumbnailType.MINIATURE 32 const type = ThumbnailType.MINIATURE
33 33
34 const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height }) 34 // Only save the file URL if it is a remote playlist
35 const fileUrl = playlist.isOwned()
36 ? null
37 : downloadUrl
38
39 const thumbnailCreator = () => downloadImage(downloadUrl, basePath, filename, { width, height })
35 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) 40 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
36} 41}
37 42
38function createVideoMiniatureFromUrl (fileUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) { 43function createVideoMiniatureFromUrl (downloadUrl: string, video: MVideoThumbnail, type: ThumbnailType, size?: ImageSize) {
39 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) 44 const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
40 const thumbnailCreator = () => downloadImage(fileUrl, basePath, filename, { width, height })
41 45
46 // Only save the file URL if it is a remote video
47 const fileUrl = video.isOwned()
48 ? null
49 : downloadUrl
50
51 const thumbnailCreator = () => downloadImage(downloadUrl, basePath, filename, { width, height })
42 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl }) 52 return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
43} 53}
44 54
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts
index 6878a3155..3cad6c668 100644
--- a/server/models/video/thumbnail.ts
+++ b/server/models/video/thumbnail.ts
@@ -1,3 +1,4 @@
1import { remove } from 'fs-extra'
1import { join } from 'path' 2import { join } from 'path'
2import { 3import {
3 AfterDestroy, 4 AfterDestroy,
@@ -12,15 +13,14 @@ import {
12 Table, 13 Table,
13 UpdatedAt 14 UpdatedAt
14} from 'sequelize-typescript' 15} from 'sequelize-typescript'
15import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants' 16import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
17import { MThumbnailVideo, MVideoAccountLight } from '@server/types/models'
18import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
16import { logger } from '../../helpers/logger' 19import { logger } from '../../helpers/logger'
17import { remove } from 'fs-extra'
18import { CONFIG } from '../../initializers/config' 20import { CONFIG } from '../../initializers/config'
21import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, STATIC_PATHS, WEBSERVER } from '../../initializers/constants'
19import { VideoModel } from './video' 22import { VideoModel } from './video'
20import { VideoPlaylistModel } from './video-playlist' 23import { VideoPlaylistModel } from './video-playlist'
21import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
22import { MVideoAccountLight } from '@server/types/models'
23import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
24 24
25@Table({ 25@Table({
26 tableName: 'thumbnail', 26 tableName: 'thumbnail',
@@ -31,6 +31,10 @@ import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
31 { 31 {
32 fields: [ 'videoPlaylistId' ], 32 fields: [ 'videoPlaylistId' ],
33 unique: true 33 unique: true
34 },
35 {
36 fields: [ 'filename', 'type' ],
37 unique: true
34 } 38 }
35 ] 39 ]
36}) 40})
@@ -114,20 +118,23 @@ export class ThumbnailModel extends Model {
114 .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err)) 118 .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err))
115 } 119 }
116 120
117 static loadByName (filename: string) { 121 static loadWithVideoByName (filename: string, thumbnailType: ThumbnailType): Promise<MThumbnailVideo> {
118 const query = { 122 const query = {
119 where: { 123 where: {
120 filename 124 filename,
121 } 125 type: thumbnailType
126 },
127 include: [
128 {
129 model: VideoModel.unscoped(),
130 required: true
131 }
132 ]
122 } 133 }
123 134
124 return ThumbnailModel.findOne(query) 135 return ThumbnailModel.findOne(query)
125 } 136 }
126 137
127 static generateDefaultPreviewName (videoUUID: string) {
128 return videoUUID + '.jpg'
129 }
130
131 getFileUrl (video: MVideoAccountLight) { 138 getFileUrl (video: MVideoAccountLight) {
132 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename 139 const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
133 140
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 14e80a3ba..3321deed3 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -130,6 +130,7 @@ import { VideoShareModel } from './video-share'
130import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 130import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
131import { VideoTagModel } from './video-tag' 131import { VideoTagModel } from './video-tag'
132import { VideoViewModel } from './video-view' 132import { VideoViewModel } from './video-view'
133import { v4 as uuidv4 } from 'uuid'
133 134
134export enum ScopeNames { 135export enum ScopeNames {
135 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 136 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
@@ -1827,7 +1828,7 @@ export class VideoModel extends Model {
1827 } 1828 }
1828 1829
1829 generateThumbnailName () { 1830 generateThumbnailName () {
1830 return this.uuid + '.jpg' 1831 return uuidv4() + '.jpg'
1831 } 1832 }
1832 1833
1833 getMiniature () { 1834 getMiniature () {
@@ -1837,7 +1838,7 @@ export class VideoModel extends Model {
1837 } 1838 }
1838 1839
1839 generatePreviewName () { 1840 generatePreviewName () {
1840 return this.uuid + '.jpg' 1841 return uuidv4() + '.jpg'
1841 } 1842 }
1842 1843
1843 hasPreview () { 1844 hasPreview () {
diff --git a/server/types/models/video/thumbnail.ts b/server/types/models/video/thumbnail.ts
index c03ba55ac..81a29e062 100644
--- a/server/types/models/video/thumbnail.ts
+++ b/server/types/models/video/thumbnail.ts
@@ -1,3 +1,15 @@
1import { PickWith } from '@shared/core-utils'
1import { ThumbnailModel } from '../../../models/video/thumbnail' 2import { ThumbnailModel } from '../../../models/video/thumbnail'
3import { MVideo } from './video'
4
5type Use<K extends keyof ThumbnailModel, M> = PickWith<ThumbnailModel, K, M>
6
7// ############################################################################
2 8
3export type MThumbnail = Omit<ThumbnailModel, 'Video' | 'VideoPlaylist'> 9export type MThumbnail = Omit<ThumbnailModel, 'Video' | 'VideoPlaylist'>
10
11// ############################################################################
12
13export type MThumbnailVideo =
14 MThumbnail &
15 Use<'Video', MVideo>