diff options
author | Chocobozzz <me@florianbigard.com> | 2018-07-12 19:02:00 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-07-16 11:50:08 +0200 |
commit | 40e87e9ecc54e3513fb586928330a7855eb192c6 (patch) | |
tree | af1111ecba85f9cd8286811ff332a67cf21be2f6 /server/lib | |
parent | d4557fd3ecc8d4ed4fb0e5c868929bc36c959ed2 (diff) | |
download | PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.gz PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.tar.zst PeerTube-40e87e9ecc54e3513fb586928330a7855eb192c6.zip |
Implement captions/subtitles
Diffstat (limited to 'server/lib')
-rw-r--r-- | server/lib/activitypub/process/process-update.ts | 12 | ||||
-rw-r--r-- | server/lib/activitypub/videos.ts | 29 | ||||
-rw-r--r-- | server/lib/cache/abstract-video-static-file-cache.ts | 54 | ||||
-rw-r--r-- | server/lib/cache/videos-caption-cache.ts | 53 | ||||
-rw-r--r-- | server/lib/cache/videos-preview-cache.ts | 60 |
5 files changed, 155 insertions, 53 deletions
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 73db461c3..62791ff1b 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -19,6 +19,7 @@ import { | |||
19 | videoFileActivityUrlToDBAttributes | 19 | videoFileActivityUrlToDBAttributes |
20 | } from '../videos' | 20 | } from '../videos' |
21 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' | 21 | import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' |
22 | import { VideoCaptionModel } from '../../../models/video/video-caption' | ||
22 | 23 | ||
23 | async function processUpdateActivity (activity: ActivityUpdate) { | 24 | async function processUpdateActivity (activity: ActivityUpdate) { |
24 | const actor = await getOrCreateActorAndServerAndModel(activity.actor) | 25 | const actor = await getOrCreateActorAndServerAndModel(activity.actor) |
@@ -110,9 +111,18 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) | |||
110 | const tasks = videoFileAttributes.map(f => VideoFileModel.create(f)) | 111 | const tasks = videoFileAttributes.map(f => VideoFileModel.create(f)) |
111 | await Promise.all(tasks) | 112 | await Promise.all(tasks) |
112 | 113 | ||
113 | const tags = videoObject.tag.map(t => t.name) | 114 | // Update Tags |
115 | const tags = videoObject.tag.map(tag => tag.name) | ||
114 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 116 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
115 | await videoInstance.$set('Tags', tagInstances, sequelizeOptions) | 117 | await videoInstance.$set('Tags', tagInstances, sequelizeOptions) |
118 | |||
119 | // Update captions | ||
120 | await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoInstance.id, t) | ||
121 | |||
122 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { | ||
123 | return VideoCaptionModel.insertOrReplaceLanguage(videoInstance.id, c.identifier, t) | ||
124 | }) | ||
125 | await Promise.all(videoCaptionsPromises) | ||
116 | }) | 126 | }) |
117 | 127 | ||
118 | logger.info('Remote video with uuid %s updated', videoObject.uuid) | 128 | logger.info('Remote video with uuid %s updated', videoObject.uuid) |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index a16828fda..fdc082b61 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -24,10 +24,20 @@ import { addVideoComments } from './video-comments' | |||
24 | import { crawlCollectionPage } from './crawl' | 24 | import { crawlCollectionPage } from './crawl' |
25 | import { sendCreateVideo, sendUpdateVideo } from './send' | 25 | import { sendCreateVideo, sendUpdateVideo } from './send' |
26 | import { shareVideoByServerAndChannel } from './index' | 26 | import { shareVideoByServerAndChannel } from './index' |
27 | import { isArray } from '../../helpers/custom-validators/misc' | ||
28 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
27 | 29 | ||
28 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { | 30 | async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { |
29 | // If the video is not private and published, we federate it | 31 | // If the video is not private and published, we federate it |
30 | if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) { | 32 | if (video.privacy !== VideoPrivacy.PRIVATE && video.state === VideoState.PUBLISHED) { |
33 | // Fetch more attributes that we will need to serialize in AP object | ||
34 | if (isArray(video.VideoCaptions) === false) { | ||
35 | video.VideoCaptions = await video.$get('VideoCaptions', { | ||
36 | attributes: [ 'language' ], | ||
37 | transaction | ||
38 | }) as VideoCaptionModel[] | ||
39 | } | ||
40 | |||
31 | if (isNewVideo === true) { | 41 | if (isNewVideo === true) { |
32 | // Now we'll add the video's meta data to our followers | 42 | // Now we'll add the video's meta data to our followers |
33 | await sendCreateVideo(video, transaction) | 43 | await sendCreateVideo(video, transaction) |
@@ -38,9 +48,8 @@ async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, tr | |||
38 | } | 48 | } |
39 | } | 49 | } |
40 | 50 | ||
41 | function fetchRemoteVideoPreview (video: VideoModel, reject: Function) { | 51 | function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Function) { |
42 | const host = video.VideoChannel.Account.Actor.Server.host | 52 | const host = video.VideoChannel.Account.Actor.Server.host |
43 | const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) | ||
44 | 53 | ||
45 | // We need to provide a callback, if no we could have an uncaught exception | 54 | // We need to provide a callback, if no we could have an uncaught exception |
46 | return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => { | 55 | return request.get(REMOTE_SCHEME.HTTP + '://' + host + path, err => { |
@@ -179,24 +188,32 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: | |||
179 | const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to) | 188 | const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to) |
180 | const video = VideoModel.build(videoData) | 189 | const video = VideoModel.build(videoData) |
181 | 190 | ||
182 | // Don't block on request | 191 | // Don't block on remote HTTP request (we are in a transaction!) |
183 | generateThumbnailFromUrl(video, videoObject.icon) | 192 | generateThumbnailFromUrl(video, videoObject.icon) |
184 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })) | 193 | .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err })) |
185 | 194 | ||
186 | const videoCreated = await video.save(sequelizeOptions) | 195 | const videoCreated = await video.save(sequelizeOptions) |
187 | 196 | ||
197 | // Process files | ||
188 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) | 198 | const videoFileAttributes = videoFileActivityUrlToDBAttributes(videoCreated, videoObject) |
189 | if (videoFileAttributes.length === 0) { | 199 | if (videoFileAttributes.length === 0) { |
190 | throw new Error('Cannot find valid files for video %s ' + videoObject.url) | 200 | throw new Error('Cannot find valid files for video %s ' + videoObject.url) |
191 | } | 201 | } |
192 | 202 | ||
193 | const tasks = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) | 203 | const videoFilePromises = videoFileAttributes.map(f => VideoFileModel.create(f, { transaction: t })) |
194 | await Promise.all(tasks) | 204 | await Promise.all(videoFilePromises) |
195 | 205 | ||
206 | // Process tags | ||
196 | const tags = videoObject.tag.map(t => t.name) | 207 | const tags = videoObject.tag.map(t => t.name) |
197 | const tagInstances = await TagModel.findOrCreateTags(tags, t) | 208 | const tagInstances = await TagModel.findOrCreateTags(tags, t) |
198 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) | 209 | await videoCreated.$set('Tags', tagInstances, sequelizeOptions) |
199 | 210 | ||
211 | // Process captions | ||
212 | const videoCaptionsPromises = videoObject.subtitleLanguage.map(c => { | ||
213 | return VideoCaptionModel.insertOrReplaceLanguage(videoCreated.id, c.identifier, t) | ||
214 | }) | ||
215 | await Promise.all(videoCaptionsPromises) | ||
216 | |||
200 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) | 217 | logger.info('Remote video with uuid %s inserted.', videoObject.uuid) |
201 | 218 | ||
202 | videoCreated.VideoChannel = channelActor.VideoChannel | 219 | videoCreated.VideoChannel = channelActor.VideoChannel |
@@ -328,7 +345,7 @@ export { | |||
328 | federateVideoIfNeeded, | 345 | federateVideoIfNeeded, |
329 | fetchRemoteVideo, | 346 | fetchRemoteVideo, |
330 | getOrCreateAccountAndVideoAndChannel, | 347 | getOrCreateAccountAndVideoAndChannel, |
331 | fetchRemoteVideoPreview, | 348 | fetchRemoteVideoStaticFile, |
332 | fetchRemoteVideoDescription, | 349 | fetchRemoteVideoDescription, |
333 | generateThumbnailFromUrl, | 350 | generateThumbnailFromUrl, |
334 | videoActivityObjectToDBAttributes, | 351 | videoActivityObjectToDBAttributes, |
diff --git a/server/lib/cache/abstract-video-static-file-cache.ts b/server/lib/cache/abstract-video-static-file-cache.ts new file mode 100644 index 000000000..7eeeb6b3a --- /dev/null +++ b/server/lib/cache/abstract-video-static-file-cache.ts | |||
@@ -0,0 +1,54 @@ | |||
1 | import * as AsyncLRU from 'async-lru' | ||
2 | import { createWriteStream } from 'fs' | ||
3 | import { join } from 'path' | ||
4 | import { unlinkPromise } from '../../helpers/core-utils' | ||
5 | import { logger } from '../../helpers/logger' | ||
6 | import { CACHE, CONFIG } from '../../initializers' | ||
7 | import { VideoModel } from '../../models/video/video' | ||
8 | import { fetchRemoteVideoStaticFile } from '../activitypub' | ||
9 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
10 | |||
11 | export abstract class AbstractVideoStaticFileCache <T> { | ||
12 | |||
13 | protected lru | ||
14 | |||
15 | abstract getFilePath (params: T): Promise<string> | ||
16 | |||
17 | // Load and save the remote file, then return the local path from filesystem | ||
18 | protected abstract loadRemoteFile (key: string): Promise<string> | ||
19 | |||
20 | init (max: number) { | ||
21 | this.lru = new AsyncLRU({ | ||
22 | max, | ||
23 | load: (key, cb) => { | ||
24 | this.loadRemoteFile(key) | ||
25 | .then(res => cb(null, res)) | ||
26 | .catch(err => cb(err)) | ||
27 | } | ||
28 | }) | ||
29 | |||
30 | this.lru.on('evict', (obj: { key: string, value: string }) => { | ||
31 | unlinkPromise(obj.value).then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name)) | ||
32 | }) | ||
33 | } | ||
34 | |||
35 | protected loadFromLRU (key: string) { | ||
36 | return new Promise<string>((res, rej) => { | ||
37 | this.lru.get(key, (err, value) => { | ||
38 | err ? rej(err) : res(value) | ||
39 | }) | ||
40 | }) | ||
41 | } | ||
42 | |||
43 | protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) { | ||
44 | return new Promise<string>((res, rej) => { | ||
45 | const req = fetchRemoteVideoStaticFile(video, remoteStaticPath, rej) | ||
46 | |||
47 | const stream = createWriteStream(destPath) | ||
48 | |||
49 | req.pipe(stream) | ||
50 | .on('error', (err) => rej(err)) | ||
51 | .on('finish', () => res(destPath)) | ||
52 | }) | ||
53 | } | ||
54 | } | ||
diff --git a/server/lib/cache/videos-caption-cache.ts b/server/lib/cache/videos-caption-cache.ts new file mode 100644 index 000000000..1336610b2 --- /dev/null +++ b/server/lib/cache/videos-caption-cache.ts | |||
@@ -0,0 +1,53 @@ | |||
1 | import { join } from 'path' | ||
2 | import { CACHE, CONFIG } from '../../initializers' | ||
3 | import { VideoModel } from '../../models/video/video' | ||
4 | import { VideoCaptionModel } from '../../models/video/video-caption' | ||
5 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' | ||
6 | |||
7 | type GetPathParam = { videoId: string, language: string } | ||
8 | |||
9 | class VideosCaptionCache extends AbstractVideoStaticFileCache <GetPathParam> { | ||
10 | |||
11 | private static readonly KEY_DELIMITER = '%' | ||
12 | private static instance: VideosCaptionCache | ||
13 | |||
14 | private constructor () { | ||
15 | super() | ||
16 | } | ||
17 | |||
18 | static get Instance () { | ||
19 | return this.instance || (this.instance = new this()) | ||
20 | } | ||
21 | |||
22 | async getFilePath (params: GetPathParam) { | ||
23 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language) | ||
24 | if (!videoCaption) return undefined | ||
25 | |||
26 | if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) | ||
27 | |||
28 | const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language | ||
29 | return this.loadFromLRU(key) | ||
30 | } | ||
31 | |||
32 | protected async loadRemoteFile (key: string) { | ||
33 | const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER) | ||
34 | |||
35 | const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language) | ||
36 | if (!videoCaption) return undefined | ||
37 | |||
38 | if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.') | ||
39 | |||
40 | // Used to fetch the path | ||
41 | const video = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(videoId) | ||
42 | if (!video) return undefined | ||
43 | |||
44 | const remoteStaticPath = videoCaption.getCaptionStaticPath() | ||
45 | const destPath = join(CACHE.DIRECTORIES.VIDEO_CAPTIONS, videoCaption.getCaptionName()) | ||
46 | |||
47 | return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | export { | ||
52 | VideosCaptionCache | ||
53 | } | ||
diff --git a/server/lib/cache/videos-preview-cache.ts b/server/lib/cache/videos-preview-cache.ts index d09d55e11..1c0e7ed9d 100644 --- a/server/lib/cache/videos-preview-cache.ts +++ b/server/lib/cache/videos-preview-cache.ts | |||
@@ -1,71 +1,39 @@ | |||
1 | import * as asyncLRU from 'async-lru' | ||
2 | import { createWriteStream } from 'fs' | ||
3 | import { join } from 'path' | 1 | import { join } from 'path' |
4 | import { unlinkPromise } from '../../helpers/core-utils' | 2 | import { CACHE, CONFIG, STATIC_PATHS } from '../../initializers' |
5 | import { logger } from '../../helpers/logger' | ||
6 | import { CACHE, CONFIG } from '../../initializers' | ||
7 | import { VideoModel } from '../../models/video/video' | 3 | import { VideoModel } from '../../models/video/video' |
8 | import { fetchRemoteVideoPreview } from '../activitypub' | 4 | import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' |
9 | 5 | ||
10 | class VideosPreviewCache { | 6 | class VideosPreviewCache extends AbstractVideoStaticFileCache <string> { |
11 | 7 | ||
12 | private static instance: VideosPreviewCache | 8 | private static instance: VideosPreviewCache |
13 | 9 | ||
14 | private lru | 10 | private constructor () { |
15 | 11 | super() | |
16 | private constructor () { } | 12 | } |
17 | 13 | ||
18 | static get Instance () { | 14 | static get Instance () { |
19 | return this.instance || (this.instance = new this()) | 15 | return this.instance || (this.instance = new this()) |
20 | } | 16 | } |
21 | 17 | ||
22 | init (max: number) { | 18 | async getFilePath (videoUUID: string) { |
23 | this.lru = new asyncLRU({ | 19 | const video = await VideoModel.loadByUUID(videoUUID) |
24 | max, | ||
25 | load: (key, cb) => { | ||
26 | this.loadPreviews(key) | ||
27 | .then(res => cb(null, res)) | ||
28 | .catch(err => cb(err)) | ||
29 | } | ||
30 | }) | ||
31 | |||
32 | this.lru.on('evict', (obj: { key: string, value: string }) => { | ||
33 | unlinkPromise(obj.value).then(() => logger.debug('%s evicted from VideosPreviewCache', obj.value)) | ||
34 | }) | ||
35 | } | ||
36 | |||
37 | async getPreviewPath (key: string) { | ||
38 | const video = await VideoModel.loadByUUID(key) | ||
39 | if (!video) return undefined | 20 | if (!video) return undefined |
40 | 21 | ||
41 | if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()) | 22 | if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()) |
42 | 23 | ||
43 | return new Promise<string>((res, rej) => { | 24 | return this.loadFromLRU(videoUUID) |
44 | this.lru.get(key, (err, value) => { | ||
45 | err ? rej(err) : res(value) | ||
46 | }) | ||
47 | }) | ||
48 | } | 25 | } |
49 | 26 | ||
50 | private async loadPreviews (key: string) { | 27 | protected async loadRemoteFile (key: string) { |
51 | const video = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(key) | 28 | const video = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(key) |
52 | if (!video) return undefined | 29 | if (!video) return undefined |
53 | 30 | ||
54 | if (video.isOwned()) throw new Error('Cannot load preview of owned video.') | 31 | if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') |
55 | |||
56 | return this.saveRemotePreviewAndReturnPath(video) | ||
57 | } | ||
58 | 32 | ||
59 | private saveRemotePreviewAndReturnPath (video: VideoModel) { | 33 | const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) |
60 | return new Promise<string>((res, rej) => { | 34 | const destPath = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) |
61 | const req = fetchRemoteVideoPreview(video, rej) | ||
62 | const path = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) | ||
63 | const stream = createWriteStream(path) | ||
64 | 35 | ||
65 | req.pipe(stream) | 36 | return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) |
66 | .on('error', (err) => rej(err)) | ||
67 | .on('finish', () => res(path)) | ||
68 | }) | ||
69 | } | 37 | } |
70 | } | 38 | } |
71 | 39 | ||