import { spawn } from 'child_process' ; import * as fs from 'fs' ; import { inject , injectable } from 'inversify' ; import * as path from 'path' ; import * as apid from '../../../../api' ; import Thumbnail from '../../../db/entities/Thumbnail' ; import FileUtil from '../../../util/FileUtil' ; import ProcessUtil from '../../../util/ProcessUtil' ; import IVideoUtil from '../../api/video/IVideoUtil' ; import IRecordedDB from '../../db/IRecordedDB' ; import IThumbnailDB from '../../db/IThumbnailDB' ; import IVideoFileDB from '../../db/IVideoFileDB' ; import IThumbnailEvent from '../../event/IThumbnailEvent' ; import IConfigFile from '../../IConfigFile' ; import IConfiguration from '../../IConfiguration' ; import ILogger from '../../ILogger' ; import ILoggerModel from '../../ILoggerModel' ; import { IPromiseQueue } from '../../IPromiseQueue' ; import IThumbnailManageModel from './IThumbnailManageModel' ; @ injectable ( ) export default class ThumbnailManageModel implements IThumbnailManageModel { private log : ILogger ; private config : IConfigFile ; private queue : IPromiseQueue ; private recordedDB : IRecordedDB ; private videoFileDB : IVideoFileDB ; private thumbnailDB : IThumbnailDB ; private thumbnailEvent : IThumbnailEvent ; private videoUtil : IVideoUtil ; constructor ( @ inject ( 'ILoggerModel' ) logger , @ inject ( 'IConfiguration' ) configuration , @ inject ( 'IPromiseQueue' ) queue , @ inject ( 'IRecordedDB' ) recordedDB , @ inject ( 'IVideoFileDB' ) videoFileDB , @ inject ( 'IThumbnailDB' ) thumbnailDB , @ inject ( 'IThumbnailEvent' ) thumbnailEvent , @ inject ( 'IVideoUtil' ) videoUtil ) { this . log = logger . getLogger ( ) ; this . config = configuration . getConfig ( ) ; this . queue = queue ; this . recordedDB = recordedDB ; this . videoFileDB = videoFileDB ; this . thumbnailDB = thumbnailDB ; this . thumbnailEvent = thumbnailEvent ; this . videoUtil = videoUtil ; } param videoFileId public add ( videoFileId ) { this . log . system . info ( `add thumbnail queue: ${ videoFileId }` ) ; this . queue . add < void > ( ( ) => { return this . create ( videoFileId ) . catch ( err => { this . log . system . error ( `create thumbnail error: ${ videoFileId }` ) ; this . log . system . error ( err ) ; } ) ; } ) ; } param videoFileId private async create ( videoFileId ) { const videoFile = await this . videoFileDB . findId ( videoFileId ) ; const videoFilePath = await this . videoUtil . getFullFilePathFromId ( videoFileId ) ; if ( videoFile === null || videoFilePath === null ) { this . log . system . error ( `video file is not found: ${ videoFileId }` ) ; throw new Error ( 'VideoFileIsNotFound' ) ; } try { await FileUtil . access ( this . config . thumbnail , fs . constants . R_OK | fs . constants . W_OK ) ; } catch ( err ) { if ( typeof err . code !== 'undefined' && err . code === 'ENOENT' ) { this . log . system . warn ( `mkdirp: ${ this . config . thumbnail }` ) ; await FileUtil . mkdir ( this . config . thumbnail ) ; } else { this . log . system . fatal ( `thumbnail dir permission error: ${ this . config . thumbnail }` ) ; this . log . system . fatal ( err ) ; throw err ; } } const fileName = await this . getSaveFileName ( videoFile . recordedId ) ; const output = path . join ( this . config . thumbnail , fileName ) ; const cmdStr = this . config . thumbnailCmd . replace ( /%FFMPEG%/g , this . config . ffmpeg ) ; const cmds = ProcessUtil . parseCmdStr ( cmdStr ) ; for ( let i = 0 ; i < cmds . args . length ; i ++ ) { cmds . args [ i ] = cmds . args [ i ] . replace ( /%INPUT%/ , videoFilePath ) . replace ( /%OUTPUT%/ , output ) . replace ( /%THUMBNAIL_POSITION%/ , `${ this . config . thumbnailPosition . toString ( 10 ) }` ) . replace ( /%THUMBNAIL_SIZE%/ , this . config . thumbnailSize ) ; } const child = spawn ( cmds . bin , cmds . args ) ; if ( child . stderr !== null ) { child . stderr . on ( 'data' , data => { this . log . system . debug ( String ( data ) ) ; } ) ; } if ( child . stdout !== null ) { child . stdout . on ( 'data' , ( ) => { } ) ; } const endProcessing = async ( code ) : Promise < boolean > => { if ( code !== 0 ) { this . log . system . error ( `create thumbnail cmd error: ${ code }` ) ; return false ; } this . log . system . info ( `create thumbnail: ${ videoFileId }, ${ output }` ) ; const thumbnail = new Thumbnail ( ) ; thumbnail . filePath = fileName ; thumbnail . recordedId = videoFile . recordedId ; try { await this . thumbnailDB . insertOnce ( thumbnail ) ; } catch ( err ) { this . log . system . error ( `thumbnail add DB error: ${ videoFileId }` ) ; this . log . system . error ( err ) ; await FileUtil . unlink ( output ) . catch ( err => { this . log . system . error ( `thumbnail delete error: ${ videoFileId }, ${ output }` ) ; this . log . system . error ( err ) ; } ) ; return false ; } this . thumbnailEvent . emitAdded ( videoFileId , videoFile . recordedId ) ; return true ; } ; return new Promise < void > ( async ( resolve , reject ) => { child . on ( 'exit' , async ( code ) => { if ( ( await endProcessing ( code ) ) === true ) { resolve ( ) ; } else { reject ( new Error ( 'CreateThumbnailExitError' ) ) ; } } ) ; child . on ( 'error' , err => { this . log . system . error ( `create thumbnail failed: ${ videoFileId }` ) ; reject ( err ) ; } ) ; if ( ProcessUtil . isExited ( child ) === true ) { child . removeAllListeners ( ) ; if ( ( await endProcessing ( child . exitCode ) ) === true ) { resolve ( ) ; } else { reject ( new Error ( 'CreateThumbnailExitError' ) ) ; } } } ) ; } param recordedId param conflict return private async getSaveFileName ( recordedId , conflict = 0 ) { const conflictStr = conflict === 0 ? '' : `(${ conflict })` ; const fileName = `${ recordedId }${ conflictStr }.jpg` ; const filePath = path . join ( this . config . thumbnail , fileName ) ; try { await FileUtil . stat ( filePath ) ; return this . getSaveFileName ( recordedId , conflict + 1 ) ; } catch ( err ) { return fileName ; } } param thumbnailId return public async delete ( thumbnailId ) { const thumbnail = await this . thumbnailDB . findId ( thumbnailId ) ; if ( thumbnail === null ) { throw new Error ( 'ThumbnailIsNotFound' ) ; } this . log . system . info ( `delete thumbnail ${ thumbnailId }` ) ; await this . thumbnailDB . deleteOnce ( thumbnailId ) . catch ( err => { this . log . system . error ( `delete thumbnail error: ${ thumbnailId }` ) ; this . log . system . error ( err ) ; throw err ; } ) ; const filePath = path . join ( this . config . thumbnail , thumbnail . filePath ) ; await FileUtil . unlink ( filePath ) . catch ( err => { this . log . system . error ( `delete thumbnail error: ${ thumbnailId }` ) ; this . log . system . error ( err ) ; throw err ; } ) ; this . thumbnailEvent . emitDeleted ( ) ; } return public async regenerate ( ) { this . log . system . info ( 'start regenerate thumbnail' ) ; const [ recordeds ] = await this . recordedDB . findAll ( { isHalfWidth : false , } , { isNeedVideoFiles : true , isNeedThumbnails : true , isNeedsDropLog : false , isNeedTags : false , } ) ; const videoFileIds = [ ] ; for ( const recorded of recordeds ) { if ( typeof recorded . videoFiles === 'undefined' || recorded . videoFiles . length === 0 ) { continue ; } if ( typeof recorded . thumbnails === 'undefined' || recorded . thumbnails . length === 0 ) { videoFileIds . push ( recorded . videoFiles [ 0 ] . id ) ; continue ; } const nonExistingThumbnailIds = [ ] ; let existingThumbnailCnt = 0 ; for ( const thumbnail of recorded . thumbnails ) { const thumbnailPath = path . join ( this . config . thumbnail , thumbnail . filePath ) ; try { await FileUtil . stat ( thumbnailPath ) ; existingThumbnailCnt ++ ; continue ; } catch ( err ) { nonExistingThumbnailIds . push ( thumbnail . id ) ; } } for ( const thumbnailId of nonExistingThumbnailIds ) { await this . thumbnailDB . deleteOnce ( thumbnailId ) . catch ( err => { this . log . system . error ( `failed to delete non-existing thumbnail data: ${ thumbnailId }` ) ; this . log . system . error ( err ) ; } ) ; } if ( existingThumbnailCnt === 0 ) { videoFileIds . push ( recorded . videoFiles [ 0 ] . id ) ; } } for ( const videoFileId of videoFileIds ) { this . add ( videoFileId ) ; } } /**
     * DB に登録されていないログファイル削除 &  DB に登録されているが存在しないログ情報の削除
     */ public async fileCleanup ( ) { this . log . system . info ( 'start thumbnail files cleanup' ) ; const thumbnails = await this . thumbnailDB . findAll ( ) ; const fileIndex = { } ; for ( const thumbnail of thumbnails ) { const filePath = path . join ( this . config . thumbnail , thumbnail . filePath ) ; if ( ( await this . checkFileExistence ( filePath ) ) === true ) { fileIndex [ filePath ] = true ; } else { this . log . system . warn ( `thumbnail file is not exist: ${ filePath }` ) ; await this . thumbnailDB . deleteOnce ( thumbnail . id ) . catch ( err => { this . log . system . error ( err ) ; } ) ; } } const list = await FileUtil . getFileList ( this . config . thumbnail ) ; for ( const file of list . files ) { if ( typeof fileIndex [ file ] !== 'undefined' ) { continue ; } this . log . system . info ( `delete thumbnail file: ${ file }` ) ; await FileUtil . unlink ( file ) . catch ( err => { this . log . system . error ( `failed to thumbnail file: ${ file }` ) ; this . log . system . error ( err ) ; } ) ; } this . log . system . info ( 'start thumbnail files cleanup completed' ) ; } param filePath return private async checkFileExistence ( filePath ) { try { await FileUtil . stat ( filePath ) ; return true ; } catch ( err ) { return false ; } } }