diff options
author | Chocobozzz <me@florianbigard.com> | 2019-04-17 10:07:00 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-04-24 16:25:52 +0200 |
commit | e8bafea35bc930cb8ac5b2d521a188642a1adffe (patch) | |
tree | 7537f957ed7307b464e3c90b71b813d992acaade /server | |
parent | 94565d52bb2883e09f16d1363170ac9c0dccb7a1 (diff) | |
download | PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.gz PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.zst PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.zip |
Create a dedicated table to track video thumbnails
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/video-playlist.ts | 47 | ||||
-rw-r--r-- | server/controllers/api/videos/import.ts | 70 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 62 | ||||
-rw-r--r-- | server/controllers/api/videos/ownership.ts | 1 | ||||
-rw-r--r-- | server/controllers/static.ts | 2 | ||||
-rw-r--r-- | server/helpers/image-utils.ts | 5 | ||||
-rw-r--r-- | server/initializers/database.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/playlist.ts | 25 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 92 | ||||
-rw-r--r-- | server/lib/files-cache/abstract-video-static-file-cache.ts | 32 | ||||
-rw-r--r-- | server/lib/files-cache/videos-caption-cache.ts | 5 | ||||
-rw-r--r-- | server/lib/files-cache/videos-preview-cache.ts | 11 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-import.ts | 41 | ||||
-rw-r--r-- | server/lib/thumbnail.ts | 151 | ||||
-rw-r--r-- | server/models/video/thumbnail.ts | 116 | ||||
-rw-r--r-- | server/models/video/video-format-utils.ts | 8 | ||||
-rw-r--r-- | server/models/video/video-playlist.ts | 85 | ||||
-rw-r--r-- | server/models/video/video.ts | 155 |
18 files changed, 649 insertions, 263 deletions
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index 71c244a60..99325aa9d 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -12,7 +12,7 @@ import { | |||
12 | } from '../../middlewares' | 12 | } from '../../middlewares' |
13 | import { videoPlaylistsSortValidator } from '../../middlewares/validators' | 13 | import { videoPlaylistsSortValidator } from '../../middlewares/validators' |
14 | import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 14 | import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
15 | import { MIMETYPES, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' | 15 | import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' |
16 | import { logger } from '../../helpers/logger' | 16 | import { logger } from '../../helpers/logger' |
17 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 17 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
18 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 18 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
@@ -28,7 +28,6 @@ import { | |||
28 | } from '../../middlewares/validators/videos/video-playlists' | 28 | } from '../../middlewares/validators/videos/video-playlists' |
29 | import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' | 29 | import { VideoPlaylistCreate } from '../../../shared/models/videos/playlist/video-playlist-create.model' |
30 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 30 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
31 | import { processImage } from '../../helpers/image-utils' | ||
32 | import { join } from 'path' | 31 | import { join } from 'path' |
33 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' | 32 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' |
34 | import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' | 33 | import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' |
@@ -37,12 +36,12 @@ import { VideoModel } from '../../models/video/video' | |||
37 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | 36 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' |
38 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' | 37 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' |
39 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' | 38 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' |
40 | import { copy, pathExists } from 'fs-extra' | ||
41 | import { AccountModel } from '../../models/account/account' | 39 | import { AccountModel } from '../../models/account/account' |
42 | import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model' | 40 | import { VideoPlaylistReorder } from '../../../shared/models/videos/playlist/video-playlist-reorder.model' |
43 | import { JobQueue } from '../../lib/job-queue' | 41 | import { JobQueue } from '../../lib/job-queue' |
44 | import { CONFIG } from '../../initializers/config' | 42 | import { CONFIG } from '../../initializers/config' |
45 | import { sequelizeTypescript } from '../../initializers/database' | 43 | import { sequelizeTypescript } from '../../initializers/database' |
44 | import { createPlaylistThumbnailFromExisting } from '../../lib/thumbnail' | ||
46 | 45 | ||
47 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) | 46 | const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR }) |
48 | 47 | ||
@@ -174,14 +173,18 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) { | |||
174 | } | 173 | } |
175 | 174 | ||
176 | const thumbnailField = req.files['thumbnailfile'] | 175 | const thumbnailField = req.files['thumbnailfile'] |
177 | if (thumbnailField) { | 176 | const thumbnailModel = thumbnailField |
178 | const thumbnailPhysicalFile = thumbnailField[ 0 ] | 177 | ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylist) |
179 | await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName()), THUMBNAILS_SIZE) | 178 | : undefined |
180 | } | ||
181 | 179 | ||
182 | const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { | 180 | const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => { |
183 | const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) | 181 | const videoPlaylistCreated = await videoPlaylist.save({ transaction: t }) |
184 | 182 | ||
183 | if (thumbnailModel) { | ||
184 | thumbnailModel.videoPlaylistId = videoPlaylistCreated.id | ||
185 | videoPlaylistCreated.setThumbnail(await thumbnailModel.save({ transaction: t })) | ||
186 | } | ||
187 | |||
185 | // We need more attributes for the federation | 188 | // We need more attributes for the federation |
186 | videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) | 189 | videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t) |
187 | await sendCreateVideoPlaylist(videoPlaylistCreated, t) | 190 | await sendCreateVideoPlaylist(videoPlaylistCreated, t) |
@@ -206,14 +209,9 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
206 | const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE | 209 | const wasPrivatePlaylist = videoPlaylistInstance.privacy === VideoPlaylistPrivacy.PRIVATE |
207 | 210 | ||
208 | const thumbnailField = req.files['thumbnailfile'] | 211 | const thumbnailField = req.files['thumbnailfile'] |
209 | if (thumbnailField) { | 212 | const thumbnailModel = thumbnailField |
210 | const thumbnailPhysicalFile = thumbnailField[ 0 ] | 213 | ? await createPlaylistThumbnailFromExisting(thumbnailField[0].path, videoPlaylistInstance) |
211 | await processImage( | 214 | : undefined |
212 | thumbnailPhysicalFile, | ||
213 | join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylistInstance.getThumbnailName()), | ||
214 | THUMBNAILS_SIZE | ||
215 | ) | ||
216 | } | ||
217 | 215 | ||
218 | try { | 216 | try { |
219 | await sequelizeTypescript.transaction(async t => { | 217 | await sequelizeTypescript.transaction(async t => { |
@@ -241,6 +239,11 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response) | |||
241 | 239 | ||
242 | const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) | 240 | const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions) |
243 | 241 | ||
242 | if (thumbnailModel) { | ||
243 | thumbnailModel.videoPlaylistId = playlistUpdated.id | ||
244 | playlistUpdated.setThumbnail(await thumbnailModel.save({ transaction: t })) | ||
245 | } | ||
246 | |||
244 | const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE | 247 | const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE |
245 | 248 | ||
246 | if (isNewPlaylist) { | 249 | if (isNewPlaylist) { |
@@ -307,15 +310,15 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response) | |||
307 | }) | 310 | }) |
308 | 311 | ||
309 | // If the user did not set a thumbnail, automatically take the video thumbnail | 312 | // If the user did not set a thumbnail, automatically take the video thumbnail |
310 | if (playlistElement.position === 1) { | 313 | if (playlistElement.position === 1 && videoPlaylist.hasThumbnail() === false) { |
311 | const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName()) | 314 | logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) |
312 | 315 | ||
313 | if (await pathExists(playlistThumbnailPath) === false) { | 316 | const inputPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnail().filename) |
314 | logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url) | 317 | const thumbnailModel = await createPlaylistThumbnailFromExisting(inputPath, videoPlaylist, true) |
315 | 318 | ||
316 | const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()) | 319 | thumbnailModel.videoPlaylistId = videoPlaylist.id |
317 | await copy(videoThumbnailPath, playlistThumbnailPath) | 320 | |
318 | } | 321 | await thumbnailModel.save() |
319 | } | 322 | } |
320 | 323 | ||
321 | logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) | 324 | logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position) |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index a72b8c72e..f9a24a0c2 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -3,7 +3,7 @@ import * as magnetUtil from 'magnet-uri' | |||
3 | import 'multer' | 3 | import 'multer' |
4 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' | 4 | import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' |
5 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' | 5 | import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoImportAddValidator } from '../../../middlewares' |
6 | import { MIMETYPES, PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../../../initializers/constants' | 6 | import { MIMETYPES } from '../../../initializers/constants' |
7 | import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' | 7 | import { getYoutubeDLInfo, YoutubeDLInfo } from '../../../helpers/youtube-dl' |
8 | import { createReqFiles } from '../../../helpers/express-utils' | 8 | import { createReqFiles } from '../../../helpers/express-utils' |
9 | import { logger } from '../../../helpers/logger' | 9 | import { logger } from '../../../helpers/logger' |
@@ -13,12 +13,10 @@ import { getVideoActivityPubUrl } from '../../../lib/activitypub' | |||
13 | import { TagModel } from '../../../models/video/tag' | 13 | import { TagModel } from '../../../models/video/tag' |
14 | import { VideoImportModel } from '../../../models/video/video-import' | 14 | import { VideoImportModel } from '../../../models/video/video-import' |
15 | import { JobQueue } from '../../../lib/job-queue/job-queue' | 15 | import { JobQueue } from '../../../lib/job-queue/job-queue' |
16 | import { processImage } from '../../../helpers/image-utils' | ||
17 | import { join } from 'path' | 16 | import { join } from 'path' |
18 | import { isArray } from '../../../helpers/custom-validators/misc' | 17 | import { isArray } from '../../../helpers/custom-validators/misc' |
19 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | 18 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' |
20 | import { VideoChannelModel } from '../../../models/video/video-channel' | 19 | import { VideoChannelModel } from '../../../models/video/video-channel' |
21 | import { UserModel } from '../../../models/account/user' | ||
22 | import * as Bluebird from 'bluebird' | 20 | import * as Bluebird from 'bluebird' |
23 | import * as parseTorrent from 'parse-torrent' | 21 | import * as parseTorrent from 'parse-torrent' |
24 | import { getSecureTorrentName } from '../../../helpers/utils' | 22 | import { getSecureTorrentName } from '../../../helpers/utils' |
@@ -26,6 +24,9 @@ import { move, readFile } from 'fs-extra' | |||
26 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 24 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
27 | import { CONFIG } from '../../../initializers/config' | 25 | import { CONFIG } from '../../../initializers/config' |
28 | import { sequelizeTypescript } from '../../../initializers/database' | 26 | import { sequelizeTypescript } from '../../../initializers/database' |
27 | import { createVideoThumbnailFromExisting } from '../../../lib/thumbnail' | ||
28 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | ||
29 | import { ThumbnailModel } from '../../../models/video/thumbnail' | ||
29 | 30 | ||
30 | const auditLogger = auditLoggerFactory('video-imports') | 31 | const auditLogger = auditLoggerFactory('video-imports') |
31 | const videoImportsRouter = express.Router() | 32 | const videoImportsRouter = express.Router() |
@@ -89,10 +90,10 @@ async function addTorrentImport (req: express.Request, res: express.Response, to | |||
89 | videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string | 90 | videoName = isArray(parsed.name) ? parsed.name[ 0 ] : parsed.name as string |
90 | } | 91 | } |
91 | 92 | ||
92 | const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }, user) | 93 | const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName }) |
93 | 94 | ||
94 | await processThumbnail(req, video) | 95 | const thumbnailModel = await processThumbnail(req, video) |
95 | await processPreview(req, video) | 96 | const previewModel = await processPreview(req, video) |
96 | 97 | ||
97 | const tags = body.tags || undefined | 98 | const tags = body.tags || undefined |
98 | const videoImportAttributes = { | 99 | const videoImportAttributes = { |
@@ -101,7 +102,14 @@ async function addTorrentImport (req: express.Request, res: express.Response, to | |||
101 | state: VideoImportState.PENDING, | 102 | state: VideoImportState.PENDING, |
102 | userId: user.id | 103 | userId: user.id |
103 | } | 104 | } |
104 | const videoImport = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) | 105 | const videoImport = await insertIntoDB({ |
106 | video, | ||
107 | thumbnailModel, | ||
108 | previewModel, | ||
109 | videoChannel: res.locals.videoChannel, | ||
110 | tags, | ||
111 | videoImportAttributes | ||
112 | }) | ||
105 | 113 | ||
106 | // Create job to import the video | 114 | // Create job to import the video |
107 | const payload = { | 115 | const payload = { |
@@ -132,10 +140,10 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
132 | }).end() | 140 | }).end() |
133 | } | 141 | } |
134 | 142 | ||
135 | const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo, user) | 143 | const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) |
136 | 144 | ||
137 | const downloadThumbnail = !await processThumbnail(req, video) | 145 | const thumbnailModel = await processThumbnail(req, video) |
138 | const downloadPreview = !await processPreview(req, video) | 146 | const previewModel = await processPreview(req, video) |
139 | 147 | ||
140 | const tags = body.tags || youtubeDLInfo.tags | 148 | const tags = body.tags || youtubeDLInfo.tags |
141 | const videoImportAttributes = { | 149 | const videoImportAttributes = { |
@@ -143,15 +151,22 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
143 | state: VideoImportState.PENDING, | 151 | state: VideoImportState.PENDING, |
144 | userId: user.id | 152 | userId: user.id |
145 | } | 153 | } |
146 | const videoImport = await insertIntoDB(video, res.locals.videoChannel, tags, videoImportAttributes) | 154 | const videoImport = await insertIntoDB({ |
155 | video: video, | ||
156 | thumbnailModel, | ||
157 | previewModel, | ||
158 | videoChannel: res.locals.videoChannel, | ||
159 | tags, | ||
160 | videoImportAttributes | ||
161 | }) | ||
147 | 162 | ||
148 | // Create job to import the video | 163 | // Create job to import the video |
149 | const payload = { | 164 | const payload = { |
150 | type: 'youtube-dl' as 'youtube-dl', | 165 | type: 'youtube-dl' as 'youtube-dl', |
151 | videoImportId: videoImport.id, | 166 | videoImportId: videoImport.id, |
152 | thumbnailUrl: youtubeDLInfo.thumbnailUrl, | 167 | thumbnailUrl: youtubeDLInfo.thumbnailUrl, |
153 | downloadThumbnail, | 168 | downloadThumbnail: !thumbnailModel, |
154 | downloadPreview | 169 | downloadPreview: !previewModel |
155 | } | 170 | } |
156 | await JobQueue.Instance.createJob({ type: 'video-import', payload }) | 171 | await JobQueue.Instance.createJob({ type: 'video-import', payload }) |
157 | 172 | ||
@@ -160,7 +175,7 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response) | |||
160 | return res.json(videoImport.toFormattedJSON()).end() | 175 | return res.json(videoImport.toFormattedJSON()).end() |
161 | } | 176 | } |
162 | 177 | ||
163 | function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo, user: UserModel) { | 178 | function buildVideo (channelId: number, body: VideoImportCreate, importData: YoutubeDLInfo) { |
164 | const videoData = { | 179 | const videoData = { |
165 | name: body.name || importData.name || 'Unknown name', | 180 | name: body.name || importData.name || 'Unknown name', |
166 | remote: false, | 181 | remote: false, |
@@ -189,32 +204,34 @@ async function processThumbnail (req: express.Request, video: VideoModel) { | |||
189 | const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined | 204 | const thumbnailField = req.files ? req.files['thumbnailfile'] : undefined |
190 | if (thumbnailField) { | 205 | if (thumbnailField) { |
191 | const thumbnailPhysicalFile = thumbnailField[ 0 ] | 206 | const thumbnailPhysicalFile = thumbnailField[ 0 ] |
192 | await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE) | ||
193 | 207 | ||
194 | return true | 208 | return createVideoThumbnailFromExisting(thumbnailPhysicalFile.path, video, ThumbnailType.THUMBNAIL) |
195 | } | 209 | } |
196 | 210 | ||
197 | return false | 211 | return undefined |
198 | } | 212 | } |
199 | 213 | ||
200 | async function processPreview (req: express.Request, video: VideoModel) { | 214 | async function processPreview (req: express.Request, video: VideoModel) { |
201 | const previewField = req.files ? req.files['previewfile'] : undefined | 215 | const previewField = req.files ? req.files['previewfile'] : undefined |
202 | if (previewField) { | 216 | if (previewField) { |
203 | const previewPhysicalFile = previewField[0] | 217 | const previewPhysicalFile = previewField[0] |
204 | await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE) | ||
205 | 218 | ||
206 | return true | 219 | return createVideoThumbnailFromExisting(previewPhysicalFile.path, video, ThumbnailType.PREVIEW) |
207 | } | 220 | } |
208 | 221 | ||
209 | return false | 222 | return undefined |
210 | } | 223 | } |
211 | 224 | ||
212 | function insertIntoDB ( | 225 | function insertIntoDB (parameters: { |
213 | video: VideoModel, | 226 | video: VideoModel, |
227 | thumbnailModel: ThumbnailModel, | ||
228 | previewModel: ThumbnailModel, | ||
214 | videoChannel: VideoChannelModel, | 229 | videoChannel: VideoChannelModel, |
215 | tags: string[], | 230 | tags: string[], |
216 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> | 231 | videoImportAttributes: FilteredModelAttributes<VideoImportModel> |
217 | ): Bluebird<VideoImportModel> { | 232 | }): Bluebird<VideoImportModel> { |
233 | let { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes } = parameters | ||
234 | |||
218 | return sequelizeTypescript.transaction(async t => { | 235 | return sequelizeTypescript.transaction(async t => { |
219 | const sequelizeOptions = { transaction: t } | 236 | const sequelizeOptions = { transaction: t } |
220 | 237 | ||
@@ -222,6 +239,15 @@ function insertIntoDB ( | |||
222 | const videoCreated = await video.save(sequelizeOptions) | 239 | const videoCreated = await video.save(sequelizeOptions) |
223 | videoCreated.VideoChannel = videoChannel | 240 | videoCreated.VideoChannel = videoChannel |
224 | 241 | ||
242 | if (thumbnailModel) { | ||
243 | thumbnailModel.videoId = videoCreated.id | ||
244 | videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t })) | ||
245 | } | ||
246 | if (previewModel) { | ||
247 | previewModel.videoId = videoCreated.id | ||
248 | videoCreated.addThumbnail(await previewModel.save({ transaction: t })) | ||
249 | } | ||
250 | |||
225 | await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t) | 251 | await autoBlacklistVideoIfNeeded(video, videoChannel.Account.User, t) |
226 | 252 | ||
227 | // Set tags to the video | 253 | // Set tags to the video |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index d6f513254..24721a17f 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -2,20 +2,11 @@ import * as express from 'express' | |||
2 | import { extname, join } from 'path' | 2 | import { extname, join } from 'path' |
3 | import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' | 3 | import { VideoCreate, VideoPrivacy, VideoState, VideoUpdate } from '../../../../shared' |
4 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 4 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
5 | import { processImage } from '../../../helpers/image-utils' | ||
6 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
7 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 6 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
8 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' | 7 | import { getFormattedObjects, getServerActor } from '../../../helpers/utils' |
9 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' | 8 | import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist' |
10 | import { | 9 | import { MIMETYPES, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants' |
11 | MIMETYPES, | ||
12 | PREVIEWS_SIZE, | ||
13 | THUMBNAILS_SIZE, | ||
14 | VIDEO_CATEGORIES, | ||
15 | VIDEO_LANGUAGES, | ||
16 | VIDEO_LICENCES, | ||
17 | VIDEO_PRIVACIES | ||
18 | } from '../../../initializers/constants' | ||
19 | import { | 10 | import { |
20 | changeVideoChannelShare, | 11 | changeVideoChannelShare, |
21 | federateVideoIfNeeded, | 12 | federateVideoIfNeeded, |
@@ -61,6 +52,8 @@ import { Notifier } from '../../../lib/notifier' | |||
61 | import { sendView } from '../../../lib/activitypub/send/send-view' | 52 | import { sendView } from '../../../lib/activitypub/send/send-view' |
62 | import { CONFIG } from '../../../initializers/config' | 53 | import { CONFIG } from '../../../initializers/config' |
63 | import { sequelizeTypescript } from '../../../initializers/database' | 54 | import { sequelizeTypescript } from '../../../initializers/database' |
55 | import { createVideoThumbnailFromExisting, generateVideoThumbnail } from '../../../lib/thumbnail' | ||
56 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | ||
64 | 57 | ||
65 | const auditLogger = auditLoggerFactory('videos') | 58 | const auditLogger = auditLoggerFactory('videos') |
66 | const videosRouter = express.Router() | 59 | const videosRouter = express.Router() |
@@ -220,21 +213,15 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
220 | 213 | ||
221 | // Process thumbnail or create it from the video | 214 | // Process thumbnail or create it from the video |
222 | const thumbnailField = req.files['thumbnailfile'] | 215 | const thumbnailField = req.files['thumbnailfile'] |
223 | if (thumbnailField) { | 216 | const thumbnailModel = thumbnailField |
224 | const thumbnailPhysicalFile = thumbnailField[0] | 217 | ? await createVideoThumbnailFromExisting(thumbnailField[0].path, video, ThumbnailType.THUMBNAIL) |
225 | await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName()), THUMBNAILS_SIZE) | 218 | : await generateVideoThumbnail(video, videoFile, ThumbnailType.THUMBNAIL) |
226 | } else { | ||
227 | await video.createThumbnail(videoFile) | ||
228 | } | ||
229 | 219 | ||
230 | // Process preview or create it from the video | 220 | // Process preview or create it from the video |
231 | const previewField = req.files['previewfile'] | 221 | const previewField = req.files['previewfile'] |
232 | if (previewField) { | 222 | const previewModel = previewField |
233 | const previewPhysicalFile = previewField[0] | 223 | ? await createVideoThumbnailFromExisting(previewField[0].path, video, ThumbnailType.PREVIEW) |
234 | await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()), PREVIEWS_SIZE) | 224 | : await generateVideoThumbnail(video, videoFile, ThumbnailType.PREVIEW) |
235 | } else { | ||
236 | await video.createPreview(videoFile) | ||
237 | } | ||
238 | 225 | ||
239 | // Create the torrent file | 226 | // Create the torrent file |
240 | await video.createTorrentAndSetInfoHash(videoFile) | 227 | await video.createTorrentAndSetInfoHash(videoFile) |
@@ -243,6 +230,13 @@ async function addVideo (req: express.Request, res: express.Response) { | |||
243 | const sequelizeOptions = { transaction: t } | 230 | const sequelizeOptions = { transaction: t } |
244 | 231 | ||
245 | const videoCreated = await video.save(sequelizeOptions) | 232 | const videoCreated = await video.save(sequelizeOptions) |
233 | |||
234 | thumbnailModel.videoId = videoCreated.id | ||
235 | previewModel.videoId = videoCreated.id | ||
236 | |||
237 | videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t })) | ||
238 | videoCreated.addThumbnail(await previewModel.save({ transaction: t })) | ||
239 | |||
246 | // Do not forget to add video channel information to the created video | 240 | // Do not forget to add video channel information to the created video |
247 | videoCreated.VideoChannel = res.locals.videoChannel | 241 | videoCreated.VideoChannel = res.locals.videoChannel |
248 | 242 | ||
@@ -313,16 +307,13 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
313 | const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED | 307 | const wasUnlistedVideo = videoInstance.privacy === VideoPrivacy.UNLISTED |
314 | 308 | ||
315 | // Process thumbnail or create it from the video | 309 | // Process thumbnail or create it from the video |
316 | if (req.files && req.files['thumbnailfile']) { | 310 | const thumbnailModel = req.files && req.files['thumbnailfile'] |
317 | const thumbnailPhysicalFile = req.files['thumbnailfile'][0] | 311 | ? await createVideoThumbnailFromExisting(req.files['thumbnailfile'][0].path, videoInstance, ThumbnailType.THUMBNAIL) |
318 | await processImage(thumbnailPhysicalFile, join(CONFIG.STORAGE.THUMBNAILS_DIR, videoInstance.getThumbnailName()), THUMBNAILS_SIZE) | 312 | : undefined |
319 | } | ||
320 | 313 | ||
321 | // Process preview or create it from the video | 314 | const previewModel = req.files && req.files['previewfile'] |
322 | if (req.files && req.files['previewfile']) { | 315 | ? await createVideoThumbnailFromExisting(req.files['previewfile'][0].path, videoInstance, ThumbnailType.PREVIEW) |
323 | const previewPhysicalFile = req.files['previewfile'][0] | 316 | : undefined |
324 | await processImage(previewPhysicalFile, join(CONFIG.STORAGE.PREVIEWS_DIR, videoInstance.getPreviewName()), PREVIEWS_SIZE) | ||
325 | } | ||
326 | 317 | ||
327 | try { | 318 | try { |
328 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { | 319 | const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => { |
@@ -355,6 +346,15 @@ async function updateVideo (req: express.Request, res: express.Response) { | |||
355 | 346 | ||
356 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) | 347 | const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) |
357 | 348 | ||
349 | if (thumbnailModel) { | ||
350 | thumbnailModel.videoId = videoInstanceUpdated.id | ||
351 | videoInstanceUpdated.addThumbnail(await thumbnailModel.save({ transaction: t })) | ||
352 | } | ||
353 | if (previewModel) { | ||
354 | previewModel.videoId = videoInstanceUpdated.id | ||
355 | videoInstanceUpdated.addThumbnail(await previewModel.save({ transaction: t })) | ||
356 | } | ||
357 | |||
358 | // Video tags update? | 358 | // Video tags update? |
359 | if (videoInfoToUpdate.tags !== undefined) { | 359 | if (videoInfoToUpdate.tags !== undefined) { |
360 | const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t) | 360 | const tagInstances = await TagModel.findOrCreateTags(videoInfoToUpdate.tags, t) |
diff --git a/server/controllers/api/videos/ownership.ts b/server/controllers/api/videos/ownership.ts index fc73856c9..bc247c4ee 100644 --- a/server/controllers/api/videos/ownership.ts +++ b/server/controllers/api/videos/ownership.ts | |||
@@ -17,7 +17,6 @@ import { VideoChannelModel } from '../../../models/video/video-channel' | |||
17 | import { getFormattedObjects } from '../../../helpers/utils' | 17 | import { getFormattedObjects } from '../../../helpers/utils' |
18 | import { changeVideoChannelShare } from '../../../lib/activitypub' | 18 | import { changeVideoChannelShare } from '../../../lib/activitypub' |
19 | import { sendUpdateVideo } from '../../../lib/activitypub/send' | 19 | import { sendUpdateVideo } from '../../../lib/activitypub/send' |
20 | import { UserModel } from '../../../models/account/user' | ||
21 | 20 | ||
22 | const ownershipVideoRouter = express.Router() | 21 | const ownershipVideoRouter = express.Router() |
23 | 22 | ||
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index f6bb88725..d75b95f52 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -164,7 +164,7 @@ export { | |||
164 | 164 | ||
165 | // --------------------------------------------------------------------------- | 165 | // --------------------------------------------------------------------------- |
166 | 166 | ||
167 | async function getPreview (req: express.Request, res: express.Response, next: express.NextFunction) { | 167 | async function getPreview (req: express.Request, res: express.Response) { |
168 | const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid) | 168 | const path = await VideosPreviewCache.Instance.getFilePath(req.params.uuid) |
169 | if (!path) return res.sendStatus(404) | 169 | if (!path) return res.sendStatus(404) |
170 | 170 | ||
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index e43ea3f1d..eeaef0f5d 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -6,7 +6,8 @@ import { logger } from './logger' | |||
6 | async function processImage ( | 6 | async function processImage ( |
7 | physicalFile: { path: string }, | 7 | physicalFile: { path: string }, |
8 | destination: string, | 8 | destination: string, |
9 | newSize: { width: number, height: number } | 9 | newSize: { width: number, height: number }, |
10 | keepOriginal = false | ||
10 | ) { | 11 | ) { |
11 | if (physicalFile.path === destination) { | 12 | if (physicalFile.path === destination) { |
12 | throw new Error('Sharp needs an input path different that the output path.') | 13 | throw new Error('Sharp needs an input path different that the output path.') |
@@ -24,7 +25,7 @@ async function processImage ( | |||
24 | .resize(newSize.width, newSize.height) | 25 | .resize(newSize.width, newSize.height) |
25 | .toFile(destination) | 26 | .toFile(destination) |
26 | 27 | ||
27 | await remove(physicalFile.path) | 28 | if (keepOriginal !== true) await remove(physicalFile.path) |
28 | } | 29 | } |
29 | 30 | ||
30 | // --------------------------------------------------------------------------- | 31 | // --------------------------------------------------------------------------- |
diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 872a56220..8f237eb23 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts | |||
@@ -36,6 +36,7 @@ import { UserNotificationSettingModel } from '../models/account/user-notificatio | |||
36 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 36 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
37 | import { VideoPlaylistModel } from '../models/video/video-playlist' | 37 | import { VideoPlaylistModel } from '../models/video/video-playlist' |
38 | import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' | 38 | import { VideoPlaylistElementModel } from '../models/video/video-playlist-element' |
39 | import { ThumbnailModel } from '../models/video/thumbnail' | ||
39 | 40 | ||
40 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string | 41 | require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string |
41 | 42 | ||
@@ -105,7 +106,8 @@ async function initDatabaseModels (silent: boolean) { | |||
105 | UserNotificationSettingModel, | 106 | UserNotificationSettingModel, |
106 | VideoStreamingPlaylistModel, | 107 | VideoStreamingPlaylistModel, |
107 | VideoPlaylistModel, | 108 | VideoPlaylistModel, |
108 | VideoPlaylistElementModel | 109 | VideoPlaylistElementModel, |
110 | ThumbnailModel | ||
109 | ]) | 111 | ]) |
110 | 112 | ||
111 | // Check extensions exist in the database | 113 | // Check extensions exist in the database |
diff --git a/server/lib/activitypub/playlist.ts b/server/lib/activitypub/playlist.ts index f312409bc..341e469f3 100644 --- a/server/lib/activitypub/playlist.ts +++ b/server/lib/activitypub/playlist.ts | |||
@@ -1,12 +1,12 @@ | |||
1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 1 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
2 | import { crawlCollectionPage } from './crawl' | 2 | import { crawlCollectionPage } from './crawl' |
3 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY, THUMBNAILS_SIZE } from '../../initializers/constants' | 3 | import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers/constants' |
4 | import { AccountModel } from '../../models/account/account' | 4 | import { AccountModel } from '../../models/account/account' |
5 | import { isArray } from '../../helpers/custom-validators/misc' | 5 | import { isArray } from '../../helpers/custom-validators/misc' |
6 | import { getOrCreateActorAndServerAndModel } from './actor' | 6 | import { getOrCreateActorAndServerAndModel } from './actor' |
7 | import { logger } from '../../helpers/logger' | 7 | import { logger } from '../../helpers/logger' |
8 | import { VideoPlaylistModel } from '../../models/video/video-playlist' | 8 | import { VideoPlaylistModel } from '../../models/video/video-playlist' |
9 | import { doRequest, downloadImage } from '../../helpers/requests' | 9 | import { doRequest } from '../../helpers/requests' |
10 | import { checkUrlsSameHost } from '../../helpers/activitypub' | 10 | import { checkUrlsSameHost } from '../../helpers/activitypub' |
11 | import * as Bluebird from 'bluebird' | 11 | import * as Bluebird from 'bluebird' |
12 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 12 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
@@ -16,9 +16,8 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele | |||
16 | import { VideoModel } from '../../models/video/video' | 16 | import { VideoModel } from '../../models/video/video' |
17 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | 17 | import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' |
18 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | 18 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' |
19 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
20 | import { CONFIG } from '../../initializers/config' | ||
21 | import { sequelizeTypescript } from '../../initializers/database' | 19 | import { sequelizeTypescript } from '../../initializers/database' |
20 | import { createPlaylistThumbnailFromUrl } from '../thumbnail' | ||
22 | 21 | ||
23 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { | 22 | function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount: AccountModel, to: string[]) { |
24 | const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED | 23 | const privacy = to.indexOf(ACTIVITY_PUB.PUBLIC) !== -1 ? VideoPlaylistPrivacy.PUBLIC : VideoPlaylistPrivacy.UNLISTED |
@@ -97,16 +96,20 @@ async function createOrUpdateVideoPlaylist (playlistObject: PlaylistObject, byAc | |||
97 | return Promise.resolve() | 96 | return Promise.resolve() |
98 | }) | 97 | }) |
99 | 98 | ||
100 | // Empty playlists generally do not have a miniature, so skip this | 99 | const refreshedPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(playlist.id, null) |
101 | if (accItems.length !== 0) { | 100 | |
101 | if (playlistObject.icon) { | ||
102 | try { | 102 | try { |
103 | await generateThumbnailFromUrl(playlist, playlistObject.icon) | 103 | const thumbnailModel = await createPlaylistThumbnailFromUrl(playlistObject.icon.url, refreshedPlaylist) |
104 | thumbnailModel.videoPlaylistId = refreshedPlaylist.id | ||
105 | |||
106 | refreshedPlaylist.setThumbnail(await thumbnailModel.save()) | ||
104 | } catch (err) { | 107 | } catch (err) { |
105 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) | 108 | logger.warn('Cannot generate thumbnail of %s.', playlistObject.id, { err }) |
106 | } | 109 | } |
107 | } | 110 | } |
108 | 111 | ||
109 | return resetVideoPlaylistElements(accItems, playlist) | 112 | return resetVideoPlaylistElements(accItems, refreshedPlaylist) |
110 | } | 113 | } |
111 | 114 | ||
112 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> { | 115 | async function refreshVideoPlaylistIfNeeded (videoPlaylist: VideoPlaylistModel): Promise<VideoPlaylistModel> { |
@@ -191,12 +194,6 @@ async function resetVideoPlaylistElements (elementUrls: string[], playlist: Vide | |||
191 | return undefined | 194 | return undefined |
192 | } | 195 | } |
193 | 196 | ||
194 | function generateThumbnailFromUrl (playlist: VideoPlaylistModel, icon: ActivityIconObject) { | ||
195 | const thumbnailName = playlist.getThumbnailName() | ||
196 | |||
197 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) | ||
198 | } | ||
199 | |||
200 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { | 197 | async function fetchRemoteVideoPlaylist (playlistUrl: string): Promise<{ statusCode: number, playlistObject: PlaylistObject }> { |
201 | const options = { | 198 | const options = { |
202 | uri: playlistUrl, | 199 | uri: playlistUrl, |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index b9252e363..16c37a55f 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -3,11 +3,10 @@ import * as sequelize from 'sequelize' | |||
3 | import * as magnetUtil from 'magnet-uri' | 3 | import * as magnetUtil from 'magnet-uri' |
4 | import * as request from 'request' | 4 | import * as request from 'request' |
5 | import { | 5 | import { |
6 | ActivityIconObject, | ||
7 | ActivityPlaylistSegmentHashesObject, | 6 | ActivityPlaylistSegmentHashesObject, |
8 | ActivityPlaylistUrlObject, | 7 | ActivityPlaylistUrlObject, |
9 | ActivityUrlObject, | 8 | ActivityUrlObject, |
10 | ActivityVideoUrlObject, | 9 | ActivityVideoUrlObject, VideoCreate, |
11 | VideoState | 10 | VideoState |
12 | } from '../../../shared/index' | 11 | } from '../../../shared/index' |
13 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' | 12 | import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' |
@@ -16,8 +15,15 @@ import { sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validat | |||
16 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' | 15 | import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' |
17 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' | 16 | import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils' |
18 | import { logger } from '../../helpers/logger' | 17 | import { logger } from '../../helpers/logger' |
19 | import { doRequest, downloadImage } from '../../helpers/requests' | 18 | import { doRequest } from '../../helpers/requests' |
20 | import { ACTIVITY_PUB, MIMETYPES, P2P_MEDIA_LOADER_PEER_VERSION, REMOTE_SCHEME, THUMBNAILS_SIZE } from '../../initializers/constants' | 19 | import { |
20 | ACTIVITY_PUB, | ||
21 | MIMETYPES, | ||
22 | P2P_MEDIA_LOADER_PEER_VERSION, | ||
23 | PREVIEWS_SIZE, | ||
24 | REMOTE_SCHEME, | ||
25 | STATIC_PATHS | ||
26 | } from '../../initializers/constants' | ||
21 | import { ActorModel } from '../../models/activitypub/actor' | 27 | import { ActorModel } from '../../models/activitypub/actor' |
22 | import { TagModel } from '../../models/video/tag' | 28 | import { TagModel } from '../../models/video/tag' |
23 | import { VideoModel } from '../../models/video/video' | 29 | import { VideoModel } from '../../models/video/video' |
@@ -43,8 +49,11 @@ import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model' | |||
43 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' | 49 | import { AccountVideoRateModel } from '../../models/account/account-video-rate' |
44 | import { VideoShareModel } from '../../models/video/video-share' | 50 | import { VideoShareModel } from '../../models/video/video-share' |
45 | import { VideoCommentModel } from '../../models/video/video-comment' | 51 | import { VideoCommentModel } from '../../models/video/video-comment' |
46 | import { CONFIG } from '../../initializers/config' | ||
47 | import { sequelizeTypescript } from '../../initializers/database' | 52 | import { sequelizeTypescript } from '../../initializers/database' |
53 | import { createPlaceholderThumbnail, createVideoThumbnailFromUrl } from '../thumbnail' | ||
54 | import { ThumbnailModel } from '../../models/video/thumbnail' | ||
55 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
56 | import { join } from 'path' | ||
48 | 57 | ||
49 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 58 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
50 | // If the video is not private and is published, we federate it | 59 | // If the video is not private and is published, we federate it |
@@ -100,18 +109,18 @@ async function fetchRemoteVideoDescription (video: VideoModel) { | |||
100 | } | 109 | } |
101 | 110 | ||
102 | function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) { | 111 | function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) { |
103 | const host = video.VideoChannel.Account.Actor.Server.host | 112 | const url = buildRemoteBaseUrl(video, path) |
104 | 113 | ||
105 | // We need to provide a callback, if no we could have an uncaught exception | 114 | // We need to provide a callback, if no we could have an uncaught exception |
106 | return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => { | 115 | return request.get(url, err => { |
107 | if (err) reject(err) | 116 | if (err) reject(err) |
108 | }) | 117 | }) |
109 | } | 118 | } |
110 | 119 | ||
111 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 120 | function buildRemoteBaseUrl (video: VideoModel, path: string) { |
112 | const thumbnailName = video.getThumbnailName() | 121 | const host = video.VideoChannel.Account.Actor.Server.host |
113 | 122 | ||
114 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) | 123 | return REMOTE_SCHEME.HTTP + '://' + host + path |
115 | } | 124 | } |
116 | 125 | ||
117 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 126 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { |
@@ -236,6 +245,14 @@ async function updateVideoFromAP (options: { | |||
236 | const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED | 245 | const wasUnlistedVideo = options.video.privacy === VideoPrivacy.UNLISTED |
237 | 246 | ||
238 | try { | 247 | try { |
248 | let thumbnailModel: ThumbnailModel | ||
249 | |||
250 | try { | ||
251 | thumbnailModel = await createVideoThumbnailFromUrl(options.videoObject.icon.url, options.video, ThumbnailType.THUMBNAIL) | ||
252 | } catch (err) { | ||
253 | logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }) | ||
254 | } | ||
255 | |||
239 | await sequelizeTypescript.transaction(async t => { | 256 | await sequelizeTypescript.transaction(async t => { |
240 | const sequelizeOptions = { transaction: t } | 257 | const sequelizeOptions = { transaction: t } |
241 | 258 | ||
@@ -272,6 +289,17 @@ async function updateVideoFromAP (options: { | |||
272 | 289 | ||
273 | await options.video.save(sequelizeOptions) | 290 | await options.video.save(sequelizeOptions) |
274 | 291 | ||
292 | if (thumbnailModel) { | ||
293 | thumbnailModel.videoId = options.video.id | ||
294 | options.video.addThumbnail(await thumbnailModel.save({ transaction: t })) | ||
295 | } | ||
296 | |||
297 | // FIXME: use icon URL instead | ||
298 | const previewUrl = buildRemoteBaseUrl(options.video, join(STATIC_PATHS.PREVIEWS, options.video.getPreview().filename)) | ||
299 | const previewModel = createPlaceholderThumbnail(previewUrl, options.video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) | ||
300 | |||
301 | options.video.addThumbnail(await previewModel.save({ transaction: t })) | ||
302 | |||
275 | { | 303 | { |
276 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) | 304 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(options.video, options.videoObject) |
277 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) | 305 | const newVideoFiles = videoFileAttributes.map(a => new VideoFileModel(a)) |
@@ -347,12 +375,6 @@ async function updateVideoFromAP (options: { | |||
347 | logger.debug('Cannot update the remote video.', { err }) | 375 | logger.debug('Cannot update the remote video.', { err }) |
348 | throw err | 376 | throw err |
349 | } | 377 | } |
350 | |||
351 | try { | ||
352 | await generateThumbnailFromUrl(options.video, options.videoObject.icon) | ||
353 | } catch (err) { | ||
354 | logger.warn('Cannot generate thumbnail of %s.', options.videoObject.id, { err }) | ||
355 | } | ||
356 | } | 378 | } |
357 | 379 | ||
358 | async function refreshVideoIfNeeded (options: { | 380 | async function refreshVideoIfNeeded (options: { |
@@ -412,7 +434,6 @@ export { | |||
412 | getOrCreateVideoAndAccountAndChannel, | 434 | getOrCreateVideoAndAccountAndChannel, |
413 | fetchRemoteVideoStaticFile, | 435 | fetchRemoteVideoStaticFile, |
414 | fetchRemoteVideoDescription, | 436 | fetchRemoteVideoDescription, |
415 | generateThumbnailFromUrl, | ||
416 | getOrCreateVideoChannelFromVideoObject | 437 | getOrCreateVideoChannelFromVideoObject |
417 | } | 438 | } |
418 | 439 | ||
@@ -440,13 +461,34 @@ function isAPPlaylistSegmentHashesUrlObject (tag: any): tag is ActivityPlaylistS | |||
440 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { | 461 | async function createVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) { |
441 | logger.debug('Adding remote video %s.', videoObject.id) | 462 | logger.debug('Adding remote video %s.', videoObject.id) |
442 | 463 | ||
464 | const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to) | ||
465 | const video = VideoModel.build(videoData) | ||
466 | |||
467 | const promiseThumbnail = createVideoThumbnailFromUrl(videoObject.icon.url, video, ThumbnailType.THUMBNAIL) | ||
468 | |||
469 | let thumbnailModel: ThumbnailModel | ||
470 | if (waitThumbnail === true) { | ||
471 | thumbnailModel = await promiseThumbnail | ||
472 | } | ||
473 | |||
443 | const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => { | 474 | const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => { |
444 | const sequelizeOptions = { transaction: t } | 475 | const sequelizeOptions = { transaction: t } |
445 | 476 | ||
446 | const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to) | ||
447 | const video = VideoModel.build(videoData) | ||
448 | |||
449 | const videoCreated = await video.save(sequelizeOptions) | 477 | const videoCreated = await video.save(sequelizeOptions) |
478 | videoCreated.VideoChannel = channelActor.VideoChannel | ||
479 | |||
480 | if (thumbnailModel) { | ||
481 | thumbnailModel.videoId = videoCreated.id | ||
482 | |||
483 | videoCreated.addThumbnail(await thumbnailModel.save({ transaction: t })) | ||
484 | } | ||
485 | |||
486 | // FIXME: use icon URL instead | ||
487 | const previewUrl = buildRemoteBaseUrl(videoCreated, join(STATIC_PATHS.PREVIEWS, video.generatePreviewName())) | ||
488 | const previewModel = createPlaceholderThumbnail(previewUrl, video, ThumbnailType.PREVIEW, PREVIEWS_SIZE) | ||
489 | previewModel.videoId = videoCreated.id | ||
490 | |||
491 | videoCreated.addThumbnail(await previewModel.save({ transaction: t })) | ||
450 | 492 | ||
451 | // Process files | 493 | // Process files |
452 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) | 494 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) |
@@ -476,14 +518,16 @@ async function createVideo (videoObject: VideoTorrentObject, channelActor: Actor | |||
476 | 518 | ||
477 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) | 519 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) |
478 | 520 | ||
479 | videoCreated.VideoChannel = channelActor.VideoChannel | ||
480 | return videoCreated | 521 | return videoCreated |
481 | }) | 522 | }) |
482 | 523 | ||
483 | const p = generateThumbnailFromUrl(videoCreated, videoObject.icon) | 524 | if (waitThumbnail === false) { |
484 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })) | 525 | promiseThumbnail.then(thumbnailModel => { |
526 | thumbnailModel = videoCreated.id | ||
485 | 527 | ||
486 | if (waitThumbnail === true) await p | 528 | return thumbnailModel.save() |
529 | }) | ||
530 | } | ||
487 | 531 | ||
488 | return videoCreated | 532 | return videoCreated |
489 | } | 533 | } |
diff --git a/server/lib/files-cache/abstract-video-static-file-cache.ts b/server/lib/files-cache/abstract-video-static-file-cache.ts index 7512f2b9d..61837e0f8 100644 --- a/server/lib/files-cache/abstract-video-static-file-cache.ts +++ b/server/lib/files-cache/abstract-video-static-file-cache.ts | |||
@@ -1,41 +1,29 @@ | |||
1 | import * as AsyncLRU from 'async-lru' | ||
2 | import { createWriteStream, remove } from 'fs-extra' | 1 | import { createWriteStream, remove } from 'fs-extra' |
3 | import { logger } from '../../helpers/logger' | 2 | import { logger } from '../../helpers/logger' |
4 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
5 | import { fetchRemoteVideoStaticFile } from '../activitypub' | 4 | import { fetchRemoteVideoStaticFile } from '../activitypub' |
5 | import * as memoizee from 'memoizee' | ||
6 | 6 | ||
7 | export abstract class AbstractVideoStaticFileCache <T> { | 7 | export abstract class AbstractVideoStaticFileCache <T> { |
8 | 8 | ||
9 | protected lru | 9 | getFilePath: (params: T) => Promise<string> |
10 | 10 | ||
11 | abstract getFilePath (params: T): Promise<string> | 11 | abstract getFilePathImpl (params: T): Promise<string> |
12 | 12 | ||
13 | // Load and save the remote file, then return the local path from filesystem | 13 | // Load and save the remote file, then return the local path from filesystem |
14 | protected abstract loadRemoteFile (key: string): Promise<string> | 14 | protected abstract loadRemoteFile (key: string): Promise<string> |
15 | 15 | ||
16 | init (max: number, maxAge: number) { | 16 | init (max: number, maxAge: number) { |
17 | this.lru = new AsyncLRU({ | 17 | this.getFilePath = memoizee(this.getFilePathImpl, { |
18 | max, | ||
19 | maxAge, | 18 | maxAge, |
20 | load: (key, cb) => { | 19 | max, |
21 | this.loadRemoteFile(key) | 20 | promise: true, |
22 | .then(res => cb(null, res)) | 21 | dispose: (value: string) => { |
23 | .catch(err => cb(err)) | 22 | remove(value) |
23 | .then(() => logger.debug('%s evicted from %s', value, this.constructor.name)) | ||
24 | .catch(err => logger.error('Cannot remove %s from cache %s.', value, this.constructor.name, { err })) | ||
24 | } | 25 | } |
25 | }) | 26 | }) |
26 | |||
27 | this.lru.on('evict', (obj: { key: string, value: string }) => { | ||
28 | remove(obj.value) | ||
29 | .then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name)) | ||
30 | }) | ||
31 | } | ||
32 | |||
33 | protected loadFromLRU (key: string) { | ||
34 | return new Promise<string>((res, rej) => { | ||
35 | this.lru.get(key, (err, value) => { | ||
36 | err ? rej(err) : res(value) | ||
37 | }) | ||
38 | }) | ||
39 | } | 27 | } |
40 | 28 | ||
41 | protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) { | 29 | protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) { |
diff --git a/server/lib/files-cache/videos-caption-cache.ts b/server/lib/files-cache/videos-caption-cache.ts index 0926f4009..d4a0a3345 100644 --- a/server/lib/files-cache/videos-caption-cache.ts +++ b/server/lib/files-cache/videos-caption-cache.ts | |||
@@ -20,14 +20,14 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> { | |||
20 | return this.instance || (this.instance = new this()) | 20 | return this.instance || (this.instance = new this()) |
21 | } | 21 | } |
22 | 22 | ||
23 | async getFilePath (params: GetPathParam) { | 23 | async getFilePathImpl (params: GetPathParam) { |
24 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language) | 24 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language) |
25 | if (!videoCaption) return undefined | 25 | if (!videoCaption) return undefined |
26 | 26 | ||
27 | if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) | 27 | if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) |
28 | 28 | ||
29 | const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language | 29 | const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language |
30 | return this.loadFromLRU(key) | 30 | return this.loadRemoteFile(key) |
31 | } | 31 | } |
32 | 32 | ||
33 | protected async loadRemoteFile (key: string) { | 33 | protected async loadRemoteFile (key: string) { |
@@ -42,6 +42,7 @@ class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> { | |||
42 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) | 42 | const video = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoId) |
43 | if (!video) return undefined | 43 | if (!video) return undefined |
44 | 44 | ||
45 | // FIXME: use URL | ||
45 | const remoteStaticPath = videoCaption.getCaptionStaticPath() | 46 | const remoteStaticPath = videoCaption.getCaptionStaticPath() |
46 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) | 47 | const destPath = join(FILES_CACHE.VIDEO_CAPTIONS.DIRECTORY, videoCaption.getCaptionName()) |
47 | 48 | ||
diff --git a/server/lib/files-cache/videos-preview-cache.ts b/server/lib/files-cache/videos-preview-cache.ts index 6575e1c83..fc0d92c78 100644 --- a/server/lib/files-cache/videos-preview-cache.ts +++ b/server/lib/files-cache/videos-preview-cache.ts | |||
@@ -16,13 +16,13 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
16 | return this.instance || (this.instance = new this()) | 16 | return this.instance || (this.instance = new this()) |
17 | } | 17 | } |
18 | 18 | ||
19 | async getFilePath (videoUUID: string) { | 19 | async getFilePathImpl (videoUUID: string) { |
20 | const video = await VideoModel.loadByUUIDWithFile(videoUUID) | 20 | const video = await VideoModel.loadByUUIDWithFile(videoUUID) |
21 | if (!video) return undefined | 21 | if (!video) return undefined |
22 | 22 | ||
23 | if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()) | 23 | if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreview().filename) |
24 | 24 | ||
25 | return this.loadFromLRU(videoUUID) | 25 | return this.loadRemoteFile(videoUUID) |
26 | } | 26 | } |
27 | 27 | ||
28 | protected async loadRemoteFile (key: string) { | 28 | protected async loadRemoteFile (key: string) { |
@@ -31,8 +31,9 @@ class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { | |||
31 | 31 | ||
32 | if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') | 32 | if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') |
33 | 33 | ||
34 | const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) | 34 | // FIXME: use URL |
35 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreviewName()) | 35 | const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreview().filename) |
36 | const destPath = join(FILES_CACHE.PREVIEWS.DIRECTORY, video.getPreview().filename) | ||
36 | 37 | ||
37 | return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) | 38 | return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) |
38 | } | 39 | } |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 8e8aa1597..3fa0dd65d 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -6,8 +6,7 @@ import { VideoImportState } from '../../../../shared/models/videos' | |||
6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 6 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' |
7 | import { extname, join } from 'path' | 7 | import { extname, join } from 'path' |
8 | import { VideoFileModel } from '../../../models/video/video-file' | 8 | import { VideoFileModel } from '../../../models/video/video-file' |
9 | import { PREVIEWS_SIZE, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' | 9 | import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants' |
10 | import { downloadImage } from '../../../helpers/requests' | ||
11 | import { VideoState } from '../../../../shared' | 10 | import { VideoState } from '../../../../shared' |
12 | import { JobQueue } from '../index' | 11 | import { JobQueue } from '../index' |
13 | import { federateVideoIfNeeded } from '../../activitypub' | 12 | import { federateVideoIfNeeded } from '../../activitypub' |
@@ -18,6 +17,9 @@ import { move, remove, stat } from 'fs-extra' | |||
18 | import { Notifier } from '../../notifier' | 17 | import { Notifier } from '../../notifier' |
19 | import { CONFIG } from '../../../initializers/config' | 18 | import { CONFIG } from '../../../initializers/config' |
20 | import { sequelizeTypescript } from '../../../initializers/database' | 19 | import { sequelizeTypescript } from '../../../initializers/database' |
20 | import { ThumbnailModel } from '../../../models/video/thumbnail' | ||
21 | import { createVideoThumbnailFromUrl, generateVideoThumbnail } from '../../thumbnail' | ||
22 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | ||
21 | 23 | ||
22 | type VideoImportYoutubeDLPayload = { | 24 | type VideoImportYoutubeDLPayload = { |
23 | type: 'youtube-dl' | 25 | type: 'youtube-dl' |
@@ -146,25 +148,19 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
146 | tempVideoPath = null // This path is not used anymore | 148 | tempVideoPath = null // This path is not used anymore |
147 | 149 | ||
148 | // Process thumbnail | 150 | // Process thumbnail |
149 | if (options.downloadThumbnail) { | 151 | let thumbnailModel: ThumbnailModel |
150 | if (options.thumbnailUrl) { | 152 | if (options.downloadThumbnail && options.thumbnailUrl) { |
151 | await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE) | 153 | thumbnailModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.THUMBNAIL) |
152 | } else { | 154 | } else if (options.generateThumbnail || options.downloadThumbnail) { |
153 | await videoImport.Video.createThumbnail(videoFile) | 155 | thumbnailModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.THUMBNAIL) |
154 | } | ||
155 | } else if (options.generateThumbnail) { | ||
156 | await videoImport.Video.createThumbnail(videoFile) | ||
157 | } | 156 | } |
158 | 157 | ||
159 | // Process preview | 158 | // Process preview |
160 | if (options.downloadPreview) { | 159 | let previewModel: ThumbnailModel |
161 | if (options.thumbnailUrl) { | 160 | if (options.downloadPreview && options.thumbnailUrl) { |
162 | await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE) | 161 | previewModel = await createVideoThumbnailFromUrl(options.thumbnailUrl, videoImport.Video, ThumbnailType.PREVIEW) |
163 | } else { | 162 | } else if (options.generatePreview || options.downloadPreview) { |
164 | await videoImport.Video.createPreview(videoFile) | 163 | previewModel = await generateVideoThumbnail(videoImport.Video, videoFile, ThumbnailType.PREVIEW) |
165 | } | ||
166 | } else if (options.generatePreview) { | ||
167 | await videoImport.Video.createPreview(videoFile) | ||
168 | } | 164 | } |
169 | 165 | ||
170 | // Create torrent | 166 | // Create torrent |
@@ -184,6 +180,15 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
184 | video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED | 180 | video.state = CONFIG.TRANSCODING.ENABLED ? VideoState.TO_TRANSCODE : VideoState.PUBLISHED |
185 | await video.save({ transaction: t }) | 181 | await video.save({ transaction: t }) |
186 | 182 | ||
183 | if (thumbnailModel) { | ||
184 | thumbnailModel.videoId = video.id | ||
185 | video.addThumbnail(await thumbnailModel.save({ transaction: t })) | ||
186 | } | ||
187 | if (previewModel) { | ||
188 | previewModel.videoId = video.id | ||
189 | video.addThumbnail(await previewModel.save({ transaction: t })) | ||
190 | } | ||
191 | |||
187 | // Now we can federate the video (reload from database, we need more attributes) | 192 | // Now we can federate the video (reload from database, we need more attributes) |
188 | const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) | 193 | const videoForFederation = await VideoModel.loadAndPopulateAccountAndServerAndTags(video.uuid, t) |
189 | await federateVideoIfNeeded(videoForFederation, true, t) | 194 | await federateVideoIfNeeded(videoForFederation, true, t) |
diff --git a/server/lib/thumbnail.ts b/server/lib/thumbnail.ts new file mode 100644 index 000000000..344c28566 --- /dev/null +++ b/server/lib/thumbnail.ts | |||
@@ -0,0 +1,151 @@ | |||
1 | import { VideoFileModel } from '../models/video/video-file' | ||
2 | import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils' | ||
3 | import { CONFIG } from '../initializers/config' | ||
4 | import { PREVIEWS_SIZE, THUMBNAILS_SIZE } from '../initializers/constants' | ||
5 | import { VideoModel } from '../models/video/video' | ||
6 | import { ThumbnailModel } from '../models/video/thumbnail' | ||
7 | import { ThumbnailType } from '../../shared/models/videos/thumbnail.type' | ||
8 | import { processImage } from '../helpers/image-utils' | ||
9 | import { join } from 'path' | ||
10 | import { downloadImage } from '../helpers/requests' | ||
11 | import { VideoPlaylistModel } from '../models/video/video-playlist' | ||
12 | |||
13 | type ImageSize = { height: number, width: number } | ||
14 | |||
15 | function createPlaylistThumbnailFromExisting (inputPath: string, playlist: VideoPlaylistModel, keepOriginal = false, size?: ImageSize) { | ||
16 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) | ||
17 | const type = ThumbnailType.THUMBNAIL | ||
18 | |||
19 | const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height }, keepOriginal) | ||
20 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | ||
21 | } | ||
22 | |||
23 | function createPlaylistThumbnailFromUrl (url: string, playlist: VideoPlaylistModel, size?: ImageSize) { | ||
24 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromPlaylist(playlist, size) | ||
25 | const type = ThumbnailType.THUMBNAIL | ||
26 | |||
27 | const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height }) | ||
28 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url }) | ||
29 | } | ||
30 | |||
31 | function createVideoThumbnailFromUrl (url: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) { | ||
32 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | ||
33 | const thumbnailCreator = () => downloadImage(url, basePath, filename, { width, height }) | ||
34 | |||
35 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, url }) | ||
36 | } | ||
37 | |||
38 | function createVideoThumbnailFromExisting (inputPath: string, video: VideoModel, type: ThumbnailType, size?: ImageSize) { | ||
39 | const { filename, outputPath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | ||
40 | const thumbnailCreator = () => processImage({ path: inputPath }, outputPath, { width, height }) | ||
41 | |||
42 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | ||
43 | } | ||
44 | |||
45 | function generateVideoThumbnail (video: VideoModel, videoFile: VideoFileModel, type: ThumbnailType) { | ||
46 | const input = video.getVideoFilePath(videoFile) | ||
47 | |||
48 | const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type) | ||
49 | const thumbnailCreator = () => generateImageFromVideoFile(input, basePath, filename, { height, width }) | ||
50 | |||
51 | return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail }) | ||
52 | } | ||
53 | |||
54 | function createPlaceholderThumbnail (url: string, video: VideoModel, type: ThumbnailType, size: ImageSize) { | ||
55 | const { filename, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size) | ||
56 | |||
57 | const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() | ||
58 | |||
59 | thumbnail.filename = filename | ||
60 | thumbnail.height = height | ||
61 | thumbnail.width = width | ||
62 | thumbnail.type = type | ||
63 | thumbnail.url = url | ||
64 | |||
65 | return thumbnail | ||
66 | } | ||
67 | |||
68 | // --------------------------------------------------------------------------- | ||
69 | |||
70 | export { | ||
71 | generateVideoThumbnail, | ||
72 | createVideoThumbnailFromUrl, | ||
73 | createVideoThumbnailFromExisting, | ||
74 | createPlaceholderThumbnail, | ||
75 | createPlaylistThumbnailFromUrl, | ||
76 | createPlaylistThumbnailFromExisting | ||
77 | } | ||
78 | |||
79 | function buildMetadataFromPlaylist (playlist: VideoPlaylistModel, size: ImageSize) { | ||
80 | const filename = playlist.generateThumbnailName() | ||
81 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR | ||
82 | |||
83 | return { | ||
84 | filename, | ||
85 | basePath, | ||
86 | existingThumbnail: playlist.Thumbnail, | ||
87 | outputPath: join(basePath, filename), | ||
88 | height: size ? size.height : THUMBNAILS_SIZE.height, | ||
89 | width: size ? size.width : THUMBNAILS_SIZE.width | ||
90 | } | ||
91 | } | ||
92 | |||
93 | function buildMetadataFromVideo (video: VideoModel, type: ThumbnailType, size?: ImageSize) { | ||
94 | const existingThumbnail = Array.isArray(video.Thumbnails) | ||
95 | ? video.Thumbnails.find(t => t.type === type) | ||
96 | : undefined | ||
97 | |||
98 | if (type === ThumbnailType.THUMBNAIL) { | ||
99 | const filename = video.generateThumbnailName() | ||
100 | const basePath = CONFIG.STORAGE.THUMBNAILS_DIR | ||
101 | |||
102 | return { | ||
103 | filename, | ||
104 | basePath, | ||
105 | existingThumbnail, | ||
106 | outputPath: join(basePath, filename), | ||
107 | height: size ? size.height : THUMBNAILS_SIZE.height, | ||
108 | width: size ? size.width : THUMBNAILS_SIZE.width | ||
109 | } | ||
110 | } | ||
111 | |||
112 | if (type === ThumbnailType.PREVIEW) { | ||
113 | const filename = video.generatePreviewName() | ||
114 | const basePath = CONFIG.STORAGE.PREVIEWS_DIR | ||
115 | |||
116 | return { | ||
117 | filename, | ||
118 | basePath, | ||
119 | existingThumbnail, | ||
120 | outputPath: join(basePath, filename), | ||
121 | height: size ? size.height : PREVIEWS_SIZE.height, | ||
122 | width: size ? size.width : PREVIEWS_SIZE.width | ||
123 | } | ||
124 | } | ||
125 | |||
126 | return undefined | ||
127 | } | ||
128 | |||
129 | async function createThumbnailFromFunction (parameters: { | ||
130 | thumbnailCreator: () => Promise<any>, | ||
131 | filename: string, | ||
132 | height: number, | ||
133 | width: number, | ||
134 | type: ThumbnailType, | ||
135 | url?: string, | ||
136 | existingThumbnail?: ThumbnailModel | ||
137 | }) { | ||
138 | const { thumbnailCreator, filename, width, height, type, existingThumbnail, url = null } = parameters | ||
139 | |||
140 | const thumbnail = existingThumbnail ? existingThumbnail : new ThumbnailModel() | ||
141 | |||
142 | thumbnail.filename = filename | ||
143 | thumbnail.height = height | ||
144 | thumbnail.width = width | ||
145 | thumbnail.type = type | ||
146 | thumbnail.url = url | ||
147 | |||
148 | await thumbnailCreator() | ||
149 | |||
150 | return thumbnail | ||
151 | } | ||
diff --git a/server/models/video/thumbnail.ts b/server/models/video/thumbnail.ts new file mode 100644 index 000000000..baa5533ac --- /dev/null +++ b/server/models/video/thumbnail.ts | |||
@@ -0,0 +1,116 @@ | |||
1 | import { join } from 'path' | ||
2 | import { AfterDestroy, AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript' | ||
3 | import { STATIC_PATHS, WEBSERVER } from '../../initializers/constants' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { remove } from 'fs-extra' | ||
6 | import { CONFIG } from '../../initializers/config' | ||
7 | import { VideoModel } from './video' | ||
8 | import { VideoPlaylistModel } from './video-playlist' | ||
9 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
10 | |||
11 | @Table({ | ||
12 | tableName: 'thumbnail', | ||
13 | indexes: [ | ||
14 | { | ||
15 | fields: [ 'videoId' ] | ||
16 | }, | ||
17 | { | ||
18 | fields: [ 'videoPlaylistId' ], | ||
19 | unique: true | ||
20 | } | ||
21 | ] | ||
22 | }) | ||
23 | export class ThumbnailModel extends Model<ThumbnailModel> { | ||
24 | |||
25 | @AllowNull(false) | ||
26 | @Column | ||
27 | filename: string | ||
28 | |||
29 | @AllowNull(true) | ||
30 | @Default(null) | ||
31 | @Column | ||
32 | height: number | ||
33 | |||
34 | @AllowNull(true) | ||
35 | @Default(null) | ||
36 | @Column | ||
37 | width: number | ||
38 | |||
39 | @AllowNull(false) | ||
40 | @Column | ||
41 | type: ThumbnailType | ||
42 | |||
43 | @AllowNull(true) | ||
44 | @Column | ||
45 | url: string | ||
46 | |||
47 | @ForeignKey(() => VideoModel) | ||
48 | @Column | ||
49 | videoId: number | ||
50 | |||
51 | @BelongsTo(() => VideoModel, { | ||
52 | foreignKey: { | ||
53 | allowNull: true | ||
54 | }, | ||
55 | onDelete: 'CASCADE' | ||
56 | }) | ||
57 | Video: VideoModel | ||
58 | |||
59 | @ForeignKey(() => VideoPlaylistModel) | ||
60 | @Column | ||
61 | videoPlaylistId: number | ||
62 | |||
63 | @BelongsTo(() => VideoPlaylistModel, { | ||
64 | foreignKey: { | ||
65 | allowNull: true | ||
66 | }, | ||
67 | onDelete: 'CASCADE' | ||
68 | }) | ||
69 | VideoPlaylist: VideoPlaylistModel | ||
70 | |||
71 | @CreatedAt | ||
72 | createdAt: Date | ||
73 | |||
74 | @UpdatedAt | ||
75 | updatedAt: Date | ||
76 | |||
77 | private static types: { [ id in ThumbnailType ]: { label: string, directory: string, staticPath: string } } = { | ||
78 | [ThumbnailType.THUMBNAIL]: { | ||
79 | label: 'thumbnail', | ||
80 | directory: CONFIG.STORAGE.THUMBNAILS_DIR, | ||
81 | staticPath: STATIC_PATHS.THUMBNAILS | ||
82 | }, | ||
83 | [ThumbnailType.PREVIEW]: { | ||
84 | label: 'preview', | ||
85 | directory: CONFIG.STORAGE.PREVIEWS_DIR, | ||
86 | staticPath: STATIC_PATHS.PREVIEWS | ||
87 | } | ||
88 | } | ||
89 | |||
90 | @AfterDestroy | ||
91 | static removeFilesAndSendDelete (instance: ThumbnailModel) { | ||
92 | logger.info('Removing %s file %s.', ThumbnailModel.types[instance.type].label, instance.filename) | ||
93 | |||
94 | // Don't block the transaction | ||
95 | instance.removeThumbnail() | ||
96 | .catch(err => logger.error('Cannot remove thumbnail file %s.', instance.filename, err)) | ||
97 | } | ||
98 | |||
99 | static generateDefaultPreviewName (videoUUID: string) { | ||
100 | return videoUUID + '.jpg' | ||
101 | } | ||
102 | |||
103 | getUrl () { | ||
104 | if (this.url) return this.url | ||
105 | |||
106 | const staticPath = ThumbnailModel.types[this.type].staticPath | ||
107 | return WEBSERVER.URL + staticPath + this.filename | ||
108 | } | ||
109 | |||
110 | removeThumbnail () { | ||
111 | const directory = ThumbnailModel.types[this.type].directory | ||
112 | const thumbnailPath = join(directory, this.filename) | ||
113 | |||
114 | return remove(thumbnailPath) | ||
115 | } | ||
116 | } | ||
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index 64771b1ff..89992a5a8 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | ActivityUrlObject, | 7 | ActivityUrlObject, |
8 | VideoTorrentObject | 8 | VideoTorrentObject |
9 | } from '../../../shared/models/activitypub/objects' | 9 | } from '../../../shared/models/activitypub/objects' |
10 | import { MIMETYPES, THUMBNAILS_SIZE, WEBSERVER } from '../../initializers/constants' | 10 | import { MIMETYPES, WEBSERVER } from '../../initializers/constants' |
11 | import { VideoCaptionModel } from './video-caption' | 11 | import { VideoCaptionModel } from './video-caption' |
12 | import { | 12 | import { |
13 | getVideoCommentsActivityPubUrl, | 13 | getVideoCommentsActivityPubUrl, |
@@ -326,10 +326,10 @@ function videoModelToActivityPubObject (video: VideoModel): VideoTorrentObject { | |||
326 | subtitleLanguage, | 326 | subtitleLanguage, |
327 | icon: { | 327 | icon: { |
328 | type: 'Image', | 328 | type: 'Image', |
329 | url: video.getThumbnailUrl(baseUrlHttp), | 329 | url: video.getThumbnail().getUrl(), |
330 | mediaType: 'image/jpeg', | 330 | mediaType: 'image/jpeg', |
331 | width: THUMBNAILS_SIZE.width, | 331 | width: video.getThumbnail().width, |
332 | height: THUMBNAILS_SIZE.height | 332 | height: video.getThumbnail().height |
333 | }, | 333 | }, |
334 | url, | 334 | url, |
335 | likes: getVideoLikesActivityPubUrl(video), | 335 | likes: getVideoLikesActivityPubUrl(video), |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 0725b752a..073609c24 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { | 1 | import { |
2 | AllowNull, | 2 | AllowNull, |
3 | BeforeDestroy, | ||
4 | BelongsTo, | 3 | BelongsTo, |
5 | Column, | 4 | Column, |
6 | CreatedAt, | 5 | CreatedAt, |
@@ -8,6 +7,7 @@ import { | |||
8 | Default, | 7 | Default, |
9 | ForeignKey, | 8 | ForeignKey, |
10 | HasMany, | 9 | HasMany, |
10 | HasOne, | ||
11 | Is, | 11 | Is, |
12 | IsUUID, | 12 | IsUUID, |
13 | Model, | 13 | Model, |
@@ -40,16 +40,16 @@ import { join } from 'path' | |||
40 | import { VideoPlaylistElementModel } from './video-playlist-element' | 40 | import { VideoPlaylistElementModel } from './video-playlist-element' |
41 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | 41 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' |
42 | import { activityPubCollectionPagination } from '../../helpers/activitypub' | 42 | import { activityPubCollectionPagination } from '../../helpers/activitypub' |
43 | import { remove } from 'fs-extra' | ||
44 | import { logger } from '../../helpers/logger' | ||
45 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' | 43 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' |
46 | import { CONFIG } from '../../initializers/config' | 44 | import { ThumbnailModel } from './thumbnail' |
45 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
47 | 46 | ||
48 | enum ScopeNames { | 47 | enum ScopeNames { |
49 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', | 48 | AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', |
50 | WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', | 49 | WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH', |
51 | WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY', | 50 | WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY', |
52 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 51 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
52 | WITH_THUMBNAIL = 'WITH_THUMBNAIL', | ||
53 | WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' | 53 | WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL' |
54 | } | 54 | } |
55 | 55 | ||
@@ -62,6 +62,14 @@ type AvailableForListOptions = { | |||
62 | } | 62 | } |
63 | 63 | ||
64 | @Scopes({ | 64 | @Scopes({ |
65 | [ ScopeNames.WITH_THUMBNAIL ]: { | ||
66 | include: [ | ||
67 | { | ||
68 | model: () => ThumbnailModel, | ||
69 | required: false | ||
70 | } | ||
71 | ] | ||
72 | }, | ||
65 | [ ScopeNames.WITH_VIDEOS_LENGTH ]: { | 73 | [ ScopeNames.WITH_VIDEOS_LENGTH ]: { |
66 | attributes: { | 74 | attributes: { |
67 | include: [ | 75 | include: [ |
@@ -256,12 +264,15 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
256 | }) | 264 | }) |
257 | VideoPlaylistElements: VideoPlaylistElementModel[] | 265 | VideoPlaylistElements: VideoPlaylistElementModel[] |
258 | 266 | ||
259 | @BeforeDestroy | 267 | @HasOne(() => ThumbnailModel, { |
260 | static async removeFiles (instance: VideoPlaylistModel) { | 268 | foreignKey: { |
261 | logger.info('Removing files of video playlist %s.', instance.url) | 269 | name: 'videoPlaylistId', |
262 | 270 | allowNull: true | |
263 | return instance.removeThumbnail() | 271 | }, |
264 | } | 272 | onDelete: 'CASCADE', |
273 | hooks: true | ||
274 | }) | ||
275 | Thumbnail: ThumbnailModel | ||
265 | 276 | ||
266 | static listForApi (options: { | 277 | static listForApi (options: { |
267 | followerActorId: number | 278 | followerActorId: number |
@@ -292,7 +303,8 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
292 | } as AvailableForListOptions | 303 | } as AvailableForListOptions |
293 | ] | 304 | ] |
294 | } as any, // FIXME: typings | 305 | } as any, // FIXME: typings |
295 | ScopeNames.WITH_VIDEOS_LENGTH | 306 | ScopeNames.WITH_VIDEOS_LENGTH, |
307 | ScopeNames.WITH_THUMBNAIL | ||
296 | ] | 308 | ] |
297 | 309 | ||
298 | return VideoPlaylistModel | 310 | return VideoPlaylistModel |
@@ -365,7 +377,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
365 | } | 377 | } |
366 | 378 | ||
367 | return VideoPlaylistModel | 379 | return VideoPlaylistModel |
368 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ]) | 380 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) |
369 | .findOne(query) | 381 | .findOne(query) |
370 | } | 382 | } |
371 | 383 | ||
@@ -378,7 +390,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
378 | } | 390 | } |
379 | 391 | ||
380 | return VideoPlaylistModel | 392 | return VideoPlaylistModel |
381 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ]) | 393 | .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ]) |
382 | .findOne(query) | 394 | .findOne(query) |
383 | } | 395 | } |
384 | 396 | ||
@@ -389,7 +401,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
389 | } | 401 | } |
390 | } | 402 | } |
391 | 403 | ||
392 | return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query) | 404 | return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query) |
393 | } | 405 | } |
394 | 406 | ||
395 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { | 407 | static getPrivacyLabel (privacy: VideoPlaylistPrivacy) { |
@@ -411,24 +423,34 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
411 | return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) | 423 | return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query) |
412 | } | 424 | } |
413 | 425 | ||
414 | getThumbnailName () { | 426 | setThumbnail (thumbnail: ThumbnailModel) { |
427 | this.Thumbnail = thumbnail | ||
428 | } | ||
429 | |||
430 | getThumbnail () { | ||
431 | return this.Thumbnail | ||
432 | } | ||
433 | |||
434 | hasThumbnail () { | ||
435 | return !!this.Thumbnail | ||
436 | } | ||
437 | |||
438 | generateThumbnailName () { | ||
415 | const extension = '.jpg' | 439 | const extension = '.jpg' |
416 | 440 | ||
417 | return 'playlist-' + this.uuid + extension | 441 | return 'playlist-' + this.uuid + extension |
418 | } | 442 | } |
419 | 443 | ||
420 | getThumbnailUrl () { | 444 | getThumbnailUrl () { |
421 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() | 445 | if (!this.hasThumbnail()) return null |
446 | |||
447 | return WEBSERVER.URL + STATIC_PATHS.THUMBNAILS + this.getThumbnail().filename | ||
422 | } | 448 | } |
423 | 449 | ||
424 | getThumbnailStaticPath () { | 450 | getThumbnailStaticPath () { |
425 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | 451 | if (!this.hasThumbnail()) return null |
426 | } | ||
427 | 452 | ||
428 | removeThumbnail () { | 453 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnail().filename) |
429 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | ||
430 | return remove(thumbnailPath) | ||
431 | .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err })) | ||
432 | } | 454 | } |
433 | 455 | ||
434 | setAsRefreshed () { | 456 | setAsRefreshed () { |
@@ -482,6 +504,17 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
482 | return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) | 504 | return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t) |
483 | } | 505 | } |
484 | 506 | ||
507 | let icon: ActivityIconObject | ||
508 | if (this.hasThumbnail()) { | ||
509 | icon = { | ||
510 | type: 'Image' as 'Image', | ||
511 | url: this.getThumbnailUrl(), | ||
512 | mediaType: 'image/jpeg' as 'image/jpeg', | ||
513 | width: THUMBNAILS_SIZE.width, | ||
514 | height: THUMBNAILS_SIZE.height | ||
515 | } | ||
516 | } | ||
517 | |||
485 | return activityPubCollectionPagination(this.url, handler, page) | 518 | return activityPubCollectionPagination(this.url, handler, page) |
486 | .then(o => { | 519 | .then(o => { |
487 | return Object.assign(o, { | 520 | return Object.assign(o, { |
@@ -492,13 +525,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
492 | published: this.createdAt.toISOString(), | 525 | published: this.createdAt.toISOString(), |
493 | updated: this.updatedAt.toISOString(), | 526 | updated: this.updatedAt.toISOString(), |
494 | attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], | 527 | attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [], |
495 | icon: { | 528 | icon |
496 | type: 'Image' as 'Image', | ||
497 | url: this.getThumbnailUrl(), | ||
498 | mediaType: 'image/jpeg' as 'image/jpeg', | ||
499 | width: THUMBNAILS_SIZE.width, | ||
500 | height: THUMBNAILS_SIZE.height | ||
501 | } | ||
502 | }) | 529 | }) |
503 | }) | 530 | }) |
504 | } | 531 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 38447797e..9840d17fd 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -107,6 +107,8 @@ import { VideoImportModel } from './video-import' | |||
107 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 107 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
108 | import { VideoPlaylistElementModel } from './video-playlist-element' | 108 | import { VideoPlaylistElementModel } from './video-playlist-element' |
109 | import { CONFIG } from '../../initializers/config' | 109 | import { CONFIG } from '../../initializers/config' |
110 | import { ThumbnailModel } from './thumbnail' | ||
111 | import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type' | ||
110 | 112 | ||
111 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation | 113 | // FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation |
112 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 114 | const indexes: Sequelize.DefineIndexesOptions[] = [ |
@@ -181,7 +183,8 @@ export enum ScopeNames { | |||
181 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 183 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
182 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', | 184 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
183 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 185 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
184 | WITH_USER_ID = 'WITH_USER_ID' | 186 | WITH_USER_ID = 'WITH_USER_ID', |
187 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' | ||
185 | } | 188 | } |
186 | 189 | ||
187 | type ForAPIOptions = { | 190 | type ForAPIOptions = { |
@@ -473,6 +476,14 @@ type AvailableForListIDsOptions = { | |||
473 | 476 | ||
474 | return query | 477 | return query |
475 | }, | 478 | }, |
479 | [ ScopeNames.WITH_THUMBNAILS ]: { | ||
480 | include: [ | ||
481 | { | ||
482 | model: () => ThumbnailModel, | ||
483 | required: false | ||
484 | } | ||
485 | ] | ||
486 | }, | ||
476 | [ ScopeNames.WITH_USER_ID ]: { | 487 | [ ScopeNames.WITH_USER_ID ]: { |
477 | include: [ | 488 | include: [ |
478 | { | 489 | { |
@@ -771,6 +782,16 @@ export class VideoModel extends Model<VideoModel> { | |||
771 | }) | 782 | }) |
772 | Tags: TagModel[] | 783 | Tags: TagModel[] |
773 | 784 | ||
785 | @HasMany(() => ThumbnailModel, { | ||
786 | foreignKey: { | ||
787 | name: 'videoId', | ||
788 | allowNull: true | ||
789 | }, | ||
790 | hooks: true, | ||
791 | onDelete: 'cascade' | ||
792 | }) | ||
793 | Thumbnails: ThumbnailModel[] | ||
794 | |||
774 | @HasMany(() => VideoPlaylistElementModel, { | 795 | @HasMany(() => VideoPlaylistElementModel, { |
775 | foreignKey: { | 796 | foreignKey: { |
776 | name: 'videoId', | 797 | name: 'videoId', |
@@ -920,15 +941,11 @@ export class VideoModel extends Model<VideoModel> { | |||
920 | 941 | ||
921 | logger.info('Removing files of video %s.', instance.url) | 942 | logger.info('Removing files of video %s.', instance.url) |
922 | 943 | ||
923 | tasks.push(instance.removeThumbnail()) | ||
924 | |||
925 | if (instance.isOwned()) { | 944 | if (instance.isOwned()) { |
926 | if (!Array.isArray(instance.VideoFiles)) { | 945 | if (!Array.isArray(instance.VideoFiles)) { |
927 | instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[] | 946 | instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[] |
928 | } | 947 | } |
929 | 948 | ||
930 | tasks.push(instance.removePreview()) | ||
931 | |||
932 | // Remove physical files and torrents | 949 | // Remove physical files and torrents |
933 | instance.VideoFiles.forEach(file => { | 950 | instance.VideoFiles.forEach(file => { |
934 | tasks.push(instance.removeFile(file)) | 951 | tasks.push(instance.removeFile(file)) |
@@ -955,7 +972,11 @@ export class VideoModel extends Model<VideoModel> { | |||
955 | } | 972 | } |
956 | } | 973 | } |
957 | 974 | ||
958 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query) | 975 | return VideoModel.scope([ |
976 | ScopeNames.WITH_FILES, | ||
977 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
978 | ScopeNames.WITH_THUMBNAILS | ||
979 | ]).findAll(query) | ||
959 | } | 980 | } |
960 | 981 | ||
961 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { | 982 | static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { |
@@ -1048,7 +1069,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1048 | 1069 | ||
1049 | return Bluebird.all([ | 1070 | return Bluebird.all([ |
1050 | // FIXME: typing issue | 1071 | // FIXME: typing issue |
1051 | VideoModel.findAll(query as any), | 1072 | VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query as any), |
1052 | VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) | 1073 | VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) |
1053 | ]).then(([ rows, totals ]) => { | 1074 | ]).then(([ rows, totals ]) => { |
1054 | // totals: totalVideos + totalVideoShares | 1075 | // totals: totalVideos + totalVideoShares |
@@ -1102,12 +1123,14 @@ export class VideoModel extends Model<VideoModel> { | |||
1102 | }) | 1123 | }) |
1103 | } | 1124 | } |
1104 | 1125 | ||
1105 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { | 1126 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS) |
1106 | return { | 1127 | .findAndCountAll(query) |
1107 | data: rows, | 1128 | .then(({ rows, count }) => { |
1108 | total: count | 1129 | return { |
1109 | } | 1130 | data: rows, |
1110 | }) | 1131 | total: count |
1132 | } | ||
1133 | }) | ||
1111 | } | 1134 | } |
1112 | 1135 | ||
1113 | static async listForApi (options: { | 1136 | static async listForApi (options: { |
@@ -1296,7 +1319,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1296 | transaction: t | 1319 | transaction: t |
1297 | } | 1320 | } |
1298 | 1321 | ||
1299 | return VideoModel.findOne(options) | 1322 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) |
1300 | } | 1323 | } |
1301 | 1324 | ||
1302 | static loadWithRights (id: number | string, t?: Sequelize.Transaction) { | 1325 | static loadWithRights (id: number | string, t?: Sequelize.Transaction) { |
@@ -1306,7 +1329,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1306 | transaction: t | 1329 | transaction: t |
1307 | } | 1330 | } |
1308 | 1331 | ||
1309 | return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options) | 1332 | return VideoModel.scope([ |
1333 | ScopeNames.WITH_BLACKLISTED, | ||
1334 | ScopeNames.WITH_USER_ID, | ||
1335 | ScopeNames.WITH_THUMBNAILS | ||
1336 | ]).findOne(options) | ||
1310 | } | 1337 | } |
1311 | 1338 | ||
1312 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { | 1339 | static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { |
@@ -1318,12 +1345,15 @@ export class VideoModel extends Model<VideoModel> { | |||
1318 | transaction: t | 1345 | transaction: t |
1319 | } | 1346 | } |
1320 | 1347 | ||
1321 | return VideoModel.findOne(options) | 1348 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) |
1322 | } | 1349 | } |
1323 | 1350 | ||
1324 | static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { | 1351 | static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { |
1325 | return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]) | 1352 | return VideoModel.scope([ |
1326 | .findByPk(id, { transaction: t, logging }) | 1353 | ScopeNames.WITH_FILES, |
1354 | ScopeNames.WITH_STREAMING_PLAYLISTS, | ||
1355 | ScopeNames.WITH_THUMBNAILS | ||
1356 | ]).findByPk(id, { transaction: t, logging }) | ||
1327 | } | 1357 | } |
1328 | 1358 | ||
1329 | static loadByUUIDWithFile (uuid: string) { | 1359 | static loadByUUIDWithFile (uuid: string) { |
@@ -1333,7 +1363,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1333 | } | 1363 | } |
1334 | } | 1364 | } |
1335 | 1365 | ||
1336 | return VideoModel.findOne(options) | 1366 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options) |
1337 | } | 1367 | } |
1338 | 1368 | ||
1339 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { | 1369 | static loadByUrl (url: string, transaction?: Sequelize.Transaction) { |
@@ -1344,7 +1374,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1344 | transaction | 1374 | transaction |
1345 | } | 1375 | } |
1346 | 1376 | ||
1347 | return VideoModel.findOne(query) | 1377 | return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query) |
1348 | } | 1378 | } |
1349 | 1379 | ||
1350 | static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { | 1380 | static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { |
@@ -1358,7 +1388,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1358 | return VideoModel.scope([ | 1388 | return VideoModel.scope([ |
1359 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1389 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1360 | ScopeNames.WITH_FILES, | 1390 | ScopeNames.WITH_FILES, |
1361 | ScopeNames.WITH_STREAMING_PLAYLISTS | 1391 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1392 | ScopeNames.WITH_THUMBNAILS | ||
1362 | ]).findOne(query) | 1393 | ]).findOne(query) |
1363 | } | 1394 | } |
1364 | 1395 | ||
@@ -1377,7 +1408,8 @@ export class VideoModel extends Model<VideoModel> { | |||
1377 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1408 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1378 | ScopeNames.WITH_SCHEDULED_UPDATE, | 1409 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1379 | ScopeNames.WITH_FILES, | 1410 | ScopeNames.WITH_FILES, |
1380 | ScopeNames.WITH_STREAMING_PLAYLISTS | 1411 | ScopeNames.WITH_STREAMING_PLAYLISTS, |
1412 | ScopeNames.WITH_THUMBNAILS | ||
1381 | ] | 1413 | ] |
1382 | 1414 | ||
1383 | if (userId) { | 1415 | if (userId) { |
@@ -1403,6 +1435,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1403 | ScopeNames.WITH_BLACKLISTED, | 1435 | ScopeNames.WITH_BLACKLISTED, |
1404 | ScopeNames.WITH_ACCOUNT_DETAILS, | 1436 | ScopeNames.WITH_ACCOUNT_DETAILS, |
1405 | ScopeNames.WITH_SCHEDULED_UPDATE, | 1437 | ScopeNames.WITH_SCHEDULED_UPDATE, |
1438 | ScopeNames.WITH_THUMBNAILS, | ||
1406 | { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings | 1439 | { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings |
1407 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings | 1440 | { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings |
1408 | ] | 1441 | ] |
@@ -1555,7 +1588,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1555 | } | 1588 | } |
1556 | 1589 | ||
1557 | // FIXME: typing | 1590 | // FIXME: typing |
1558 | const apiScope: any[] = [] | 1591 | const apiScope: any[] = [ ScopeNames.WITH_THUMBNAILS ] |
1559 | 1592 | ||
1560 | if (options.user) { | 1593 | if (options.user) { |
1561 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) | 1594 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) |
@@ -1611,18 +1644,37 @@ export class VideoModel extends Model<VideoModel> { | |||
1611 | return maxBy(this.VideoFiles, file => file.resolution) | 1644 | return maxBy(this.VideoFiles, file => file.resolution) |
1612 | } | 1645 | } |
1613 | 1646 | ||
1647 | addThumbnail (thumbnail: ThumbnailModel) { | ||
1648 | if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = [] | ||
1649 | |||
1650 | // Already have this thumbnail, skip | ||
1651 | if (this.Thumbnails.find(t => t.id === thumbnail.id)) return | ||
1652 | |||
1653 | this.Thumbnails.push(thumbnail) | ||
1654 | } | ||
1655 | |||
1614 | getVideoFilename (videoFile: VideoFileModel) { | 1656 | getVideoFilename (videoFile: VideoFileModel) { |
1615 | return this.uuid + '-' + videoFile.resolution + videoFile.extname | 1657 | return this.uuid + '-' + videoFile.resolution + videoFile.extname |
1616 | } | 1658 | } |
1617 | 1659 | ||
1618 | getThumbnailName () { | 1660 | generateThumbnailName () { |
1619 | const extension = '.jpg' | 1661 | return this.uuid + '.jpg' |
1620 | return this.uuid + extension | ||
1621 | } | 1662 | } |
1622 | 1663 | ||
1623 | getPreviewName () { | 1664 | getThumbnail () { |
1624 | const extension = '.jpg' | 1665 | if (Array.isArray(this.Thumbnails) === false) return undefined |
1625 | return this.uuid + extension | 1666 | |
1667 | return this.Thumbnails.find(t => t.type === ThumbnailType.THUMBNAIL) | ||
1668 | } | ||
1669 | |||
1670 | generatePreviewName () { | ||
1671 | return this.uuid + '.jpg' | ||
1672 | } | ||
1673 | |||
1674 | getPreview () { | ||
1675 | if (Array.isArray(this.Thumbnails) === false) return undefined | ||
1676 | |||
1677 | return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW) | ||
1626 | } | 1678 | } |
1627 | 1679 | ||
1628 | getTorrentFileName (videoFile: VideoFileModel) { | 1680 | getTorrentFileName (videoFile: VideoFileModel) { |
@@ -1634,24 +1686,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1634 | return this.remote === false | 1686 | return this.remote === false |
1635 | } | 1687 | } |
1636 | 1688 | ||
1637 | createPreview (videoFile: VideoFileModel) { | ||
1638 | return generateImageFromVideoFile( | ||
1639 | this.getVideoFilePath(videoFile), | ||
1640 | CONFIG.STORAGE.PREVIEWS_DIR, | ||
1641 | this.getPreviewName(), | ||
1642 | PREVIEWS_SIZE | ||
1643 | ) | ||
1644 | } | ||
1645 | |||
1646 | createThumbnail (videoFile: VideoFileModel) { | ||
1647 | return generateImageFromVideoFile( | ||
1648 | this.getVideoFilePath(videoFile), | ||
1649 | CONFIG.STORAGE.THUMBNAILS_DIR, | ||
1650 | this.getThumbnailName(), | ||
1651 | THUMBNAILS_SIZE | ||
1652 | ) | ||
1653 | } | ||
1654 | |||
1655 | getTorrentFilePath (videoFile: VideoFileModel) { | 1689 | getTorrentFilePath (videoFile: VideoFileModel) { |
1656 | return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) | 1690 | return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) |
1657 | } | 1691 | } |
@@ -1692,11 +1726,18 @@ export class VideoModel extends Model<VideoModel> { | |||
1692 | } | 1726 | } |
1693 | 1727 | ||
1694 | getThumbnailStaticPath () { | 1728 | getThumbnailStaticPath () { |
1695 | return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) | 1729 | const thumbnail = this.getThumbnail() |
1730 | if (!thumbnail) return null | ||
1731 | |||
1732 | return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename) | ||
1696 | } | 1733 | } |
1697 | 1734 | ||
1698 | getPreviewStaticPath () { | 1735 | getPreviewStaticPath () { |
1699 | return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) | 1736 | const preview = this.getPreview() |
1737 | if (!preview) return null | ||
1738 | |||
1739 | // We use a local cache, so specify our cache endpoint instead of potential remote URL | ||
1740 | return join(STATIC_PATHS.PREVIEWS, preview.filename) | ||
1700 | } | 1741 | } |
1701 | 1742 | ||
1702 | toFormattedJSON (options?: VideoFormattingJSONOptions): Video { | 1743 | toFormattedJSON (options?: VideoFormattingJSONOptions): Video { |
@@ -1732,18 +1773,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1732 | return `/api/${API_VERSION}/videos/${this.uuid}/description` | 1773 | return `/api/${API_VERSION}/videos/${this.uuid}/description` |
1733 | } | 1774 | } |
1734 | 1775 | ||
1735 | removeThumbnail () { | ||
1736 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName()) | ||
1737 | return remove(thumbnailPath) | ||
1738 | .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err })) | ||
1739 | } | ||
1740 | |||
1741 | removePreview () { | ||
1742 | const previewPath = join(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName()) | ||
1743 | return remove(previewPath) | ||
1744 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | ||
1745 | } | ||
1746 | |||
1747 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { | 1776 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1748 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR | 1777 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1749 | 1778 | ||
@@ -1816,10 +1845,6 @@ export class VideoModel extends Model<VideoModel> { | |||
1816 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] | 1845 | return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] |
1817 | } | 1846 | } |
1818 | 1847 | ||
1819 | getThumbnailUrl (baseUrlHttp: string) { | ||
1820 | return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() | ||
1821 | } | ||
1822 | |||
1823 | getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1848 | getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1824 | return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) | 1849 | return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) |
1825 | } | 1850 | } |