aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/lib
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-07-12 19:02:00 +0200
committerChocobozzz <me@florianbigard.com>2018-07-16 11:50:08 +0200
commit40e87e9ecc54e3513fb586928330a7855eb192c6 (patch)
treeaf1111ecba85f9cd8286811ff332a67cf21be2f6 /server/lib
parentd4557fd3ecc8d4ed4fb0e5c868929bc36c959ed2 (diff)
downloadPeerTube-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.ts12
-rw-r--r--server/lib/activitypub/videos.ts29
-rw-r--r--server/lib/cache/abstract-video-static-file-cache.ts54
-rw-r--r--server/lib/cache/videos-caption-cache.ts53
-rw-r--r--server/lib/cache/videos-preview-cache.ts60
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'
21import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos' 21import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
22import { VideoCaptionModel } from '../../../models/video/video-caption'
22 23
23async function processUpdateActivity (activity: ActivityUpdate) { 24async 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'
24import { crawlCollectionPage } from './crawl' 24import { crawlCollectionPage } from './crawl'
25import { sendCreateVideo, sendUpdateVideo } from './send' 25import { sendCreateVideo, sendUpdateVideo } from './send'
26import { shareVideoByServerAndChannel } from './index' 26import { shareVideoByServerAndChannel } from './index'
27import { isArray } from '../../helpers/custom-validators/misc'
28import { VideoCaptionModel } from '../../models/video/video-caption'
27 29
28async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) { 30async 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
41function fetchRemoteVideoPreview (video: VideoModel, reject: Function) { 51function 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 @@
1import * as AsyncLRU from 'async-lru'
2import { createWriteStream } from 'fs'
3import { join } from 'path'
4import { unlinkPromise } from '../../helpers/core-utils'
5import { logger } from '../../helpers/logger'
6import { CACHE, CONFIG } from '../../initializers'
7import { VideoModel } from '../../models/video/video'
8import { fetchRemoteVideoStaticFile } from '../activitypub'
9import { VideoCaptionModel } from '../../models/video/video-caption'
10
11export 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 @@
1import { join } from 'path'
2import { CACHE, CONFIG } from '../../initializers'
3import { VideoModel } from '../../models/video/video'
4import { VideoCaptionModel } from '../../models/video/video-caption'
5import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
6
7type GetPathParam = { videoId: string, language: string }
8
9class 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
51export {
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 @@
1import * as asyncLRU from 'async-lru'
2import { createWriteStream } from 'fs'
3import { join } from 'path' 1import { join } from 'path'
4import { unlinkPromise } from '../../helpers/core-utils' 2import { CACHE, CONFIG, STATIC_PATHS } from '../../initializers'
5import { logger } from '../../helpers/logger'
6import { CACHE, CONFIG } from '../../initializers'
7import { VideoModel } from '../../models/video/video' 3import { VideoModel } from '../../models/video/video'
8import { fetchRemoteVideoPreview } from '../activitypub' 4import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache'
9 5
10class VideosPreviewCache { 6class 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