From 40e87e9ecc54e3513fb586928330a7855eb192c6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 12 Jul 2018 19:02:00 +0200 Subject: Implement captions/subtitles --- .../lib/cache/abstract-video-static-file-cache.ts | 54 +++++++++++++++++++ server/lib/cache/videos-caption-cache.ts | 53 +++++++++++++++++++ server/lib/cache/videos-preview-cache.ts | 60 +++++----------------- 3 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 server/lib/cache/abstract-video-static-file-cache.ts create mode 100644 server/lib/cache/videos-caption-cache.ts (limited to 'server/lib/cache') 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 @@ +import * as AsyncLRU from 'async-lru' +import { createWriteStream } from 'fs' +import { join } from 'path' +import { unlinkPromise } from '../../helpers/core-utils' +import { logger } from '../../helpers/logger' +import { CACHE, CONFIG } from '../../initializers' +import { VideoModel } from '../../models/video/video' +import { fetchRemoteVideoStaticFile } from '../activitypub' +import { VideoCaptionModel } from '../../models/video/video-caption' + +export abstract class AbstractVideoStaticFileCache { + + protected lru + + abstract getFilePath (params: T): Promise + + // Load and save the remote file, then return the local path from filesystem + protected abstract loadRemoteFile (key: string): Promise + + init (max: number) { + this.lru = new AsyncLRU({ + max, + load: (key, cb) => { + this.loadRemoteFile(key) + .then(res => cb(null, res)) + .catch(err => cb(err)) + } + }) + + this.lru.on('evict', (obj: { key: string, value: string }) => { + unlinkPromise(obj.value).then(() => logger.debug('%s evicted from %s', obj.value, this.constructor.name)) + }) + } + + protected loadFromLRU (key: string) { + return new Promise((res, rej) => { + this.lru.get(key, (err, value) => { + err ? rej(err) : res(value) + }) + }) + } + + protected saveRemoteVideoFileAndReturnPath (video: VideoModel, remoteStaticPath: string, destPath: string) { + return new Promise((res, rej) => { + const req = fetchRemoteVideoStaticFile(video, remoteStaticPath, rej) + + const stream = createWriteStream(destPath) + + req.pipe(stream) + .on('error', (err) => rej(err)) + .on('finish', () => res(destPath)) + }) + } +} 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 @@ +import { join } from 'path' +import { CACHE, CONFIG } from '../../initializers' +import { VideoModel } from '../../models/video/video' +import { VideoCaptionModel } from '../../models/video/video-caption' +import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' + +type GetPathParam = { videoId: string, language: string } + +class VideosCaptionCache extends AbstractVideoStaticFileCache { + + private static readonly KEY_DELIMITER = '%' + private static instance: VideosCaptionCache + + private constructor () { + super() + } + + static get Instance () { + return this.instance || (this.instance = new this()) + } + + async getFilePath (params: GetPathParam) { + const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(params.videoId, params.language) + if (!videoCaption) return undefined + + if (videoCaption.isOwned()) return join(CONFIG.STORAGE.CAPTIONS_DIR, videoCaption.getCaptionName()) + + const key = params.videoId + VideosCaptionCache.KEY_DELIMITER + params.language + return this.loadFromLRU(key) + } + + protected async loadRemoteFile (key: string) { + const [ videoId, language ] = key.split(VideosCaptionCache.KEY_DELIMITER) + + const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(videoId, language) + if (!videoCaption) return undefined + + if (videoCaption.isOwned()) throw new Error('Cannot load remote caption of owned video.') + + // Used to fetch the path + const video = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(videoId) + if (!video) return undefined + + const remoteStaticPath = videoCaption.getCaptionStaticPath() + const destPath = join(CACHE.DIRECTORIES.VIDEO_CAPTIONS, videoCaption.getCaptionName()) + + return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) + } +} + +export { + VideosCaptionCache +} 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 @@ -import * as asyncLRU from 'async-lru' -import { createWriteStream } from 'fs' import { join } from 'path' -import { unlinkPromise } from '../../helpers/core-utils' -import { logger } from '../../helpers/logger' -import { CACHE, CONFIG } from '../../initializers' +import { CACHE, CONFIG, STATIC_PATHS } from '../../initializers' import { VideoModel } from '../../models/video/video' -import { fetchRemoteVideoPreview } from '../activitypub' +import { AbstractVideoStaticFileCache } from './abstract-video-static-file-cache' -class VideosPreviewCache { +class VideosPreviewCache extends AbstractVideoStaticFileCache { private static instance: VideosPreviewCache - private lru - - private constructor () { } + private constructor () { + super() + } static get Instance () { return this.instance || (this.instance = new this()) } - init (max: number) { - this.lru = new asyncLRU({ - max, - load: (key, cb) => { - this.loadPreviews(key) - .then(res => cb(null, res)) - .catch(err => cb(err)) - } - }) - - this.lru.on('evict', (obj: { key: string, value: string }) => { - unlinkPromise(obj.value).then(() => logger.debug('%s evicted from VideosPreviewCache', obj.value)) - }) - } - - async getPreviewPath (key: string) { - const video = await VideoModel.loadByUUID(key) + async getFilePath (videoUUID: string) { + const video = await VideoModel.loadByUUID(videoUUID) if (!video) return undefined if (video.isOwned()) return join(CONFIG.STORAGE.PREVIEWS_DIR, video.getPreviewName()) - return new Promise((res, rej) => { - this.lru.get(key, (err, value) => { - err ? rej(err) : res(value) - }) - }) + return this.loadFromLRU(videoUUID) } - private async loadPreviews (key: string) { + protected async loadRemoteFile (key: string) { const video = await VideoModel.loadByUUIDAndPopulateAccountAndServerAndTags(key) if (!video) return undefined - if (video.isOwned()) throw new Error('Cannot load preview of owned video.') - - return this.saveRemotePreviewAndReturnPath(video) - } + if (video.isOwned()) throw new Error('Cannot load remote preview of owned video.') - private saveRemotePreviewAndReturnPath (video: VideoModel) { - return new Promise((res, rej) => { - const req = fetchRemoteVideoPreview(video, rej) - const path = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) - const stream = createWriteStream(path) + const remoteStaticPath = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) + const destPath = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) - req.pipe(stream) - .on('error', (err) => rej(err)) - .on('finish', () => res(path)) - }) + return this.saveRemoteVideoFileAndReturnPath(video, remoteStaticPath, destPath) } } -- cgit v1.2.3