import { inject , injectable } from 'inversify' ; import mkdirp from 'mkdirp' ; import * as path from 'path' ; import * as apid from '../../../../api' ; import DropLogFile from '../../../db/entities/DropLogFile' ; import Recorded from '../../../db/entities/Recorded' ; import Thumbnail from '../../../db/entities/Thumbnail' ; import VideoFile from '../../../db/entities/VideoFile' ; import FileUtil from '../../../util/FileUtil' ; import StrUtil from '../../../util/StrUtil' ; import IVideoUtil from '../../api/video/IVideoUtil' ; import IDropLogFileDB from '../../db/IDropLogFileDB' ; import IRecordedDB from '../../db/IRecordedDB' ; import IRecordedHistoryDB from '../../db/IRecordedHistoryDB' ; import IThumbnailDB from '../../db/IThumbnailDB' ; import IVideoFileDB from '../../db/IVideoFileDB' ; import IRecordedEvent from '../../event/IRecordedEvent' ; import IConfigFile from '../../IConfigFile' ; import IConfiguration from '../../IConfiguration' ; import ILogger from '../../ILogger' ; import ILoggerModel from '../../ILoggerModel' ; import IRecordingManageModel from '../recording/IRecordingManageModel' ; import IRecordedManageModel , { AddVideoFileOption , UploadedVideoFileOption } from './IRecordedManageModel' ; @ injectable ( ) export default class RecordedManageModel implements IRecordedManageModel { private log : ILogger ; private config : IConfigFile ; private recordedDB : IRecordedDB ; private videoFileDB : IVideoFileDB ; private thumbnailDB : IThumbnailDB ; private dropLogFileDB : IDropLogFileDB ; private recordedHistoryDB : IRecordedHistoryDB ; private recordingManageModel : IRecordingManageModel ; private recordedEvent : IRecordedEvent ; private videoUtil : IVideoUtil ; constructor ( @ inject ( 'ILoggerModel' ) logger , @ inject ( 'IConfiguration' ) configuration , @ inject ( 'IRecordedDB' ) recordedDB , @ inject ( 'IVideoFileDB' ) videoFileDB , @ inject ( 'IThumbnailDB' ) thumbnailDB , @ inject ( 'IDropLogFileDB' ) dropLogFileDB , @ inject ( 'IRecordedHistoryDB' ) recordedHistoryDB , @ inject ( 'IRecordingManageModel' ) recordingManageModel , @ inject ( 'IRecordedEvent' ) recordedEvent , @ inject ( 'IVideoUtil' ) videoUtil ) { this . log = logger . getLogger ( ) ; this . config = configuration . getConfig ( ) ; this . recordedDB = recordedDB ; this . videoFileDB = videoFileDB ; this . thumbnailDB = thumbnailDB ; this . dropLogFileDB = dropLogFileDB ; this . recordedHistoryDB = recordedHistoryDB ; this . recordingManageModel = recordingManageModel ; this . recordedEvent = recordedEvent ; this . videoUtil = videoUtil ; } param recordedId param isIgnoreProtection return public async delete ( recordedId , isIgnoreProtection = false ) { this . log . system . info ( `delete recorded: ${ recordedId }` ) ; const recorded = await this . recordedDB . findId ( recordedId ) ; if ( recorded === null ) { this . log . system . warn ( `${ recordedId } is null` ) ; throw new Error ( 'RecordedIdIsNotFound' ) ; } if ( recorded . isProtected === true ) { this . log . system . warn ( `${ recordedId } is protected` ) ; throw new Error ( 'RecordedIsProtected' ) ; } if ( isIgnoreProtection === false && recorded . isRecording === true && recorded . reserveId !== null && this . recordingManageModel . hasReserve ( recorded . reserveId ) === true ) { this . log . system . info ( `cancel recording by recorded manager reserveId: ${ recorded } recordedId: ${ recorded . id }` ) ; await this . recordingManageModel . cancel ( recorded . reserveId , true ) ; } const hasThumbnails = typeof recorded . thumbnails !== 'undefined' && recorded . thumbnails . length > 0 ; const hasVideoFiles = typeof recorded . videoFiles !== 'undefined' && recorded . videoFiles . length > 0 ; if ( hasThumbnails === true && typeof recorded . thumbnails !== 'undefined' ) { for ( const t of recorded . thumbnails ) { const filePath = this . getThumbnailPath ( t ) ; this . log . system . info ( `delete: ${ filePath }` ) ; await FileUtil . unlink ( filePath ) . catch ( err => { this . log . system . error ( `failed to delete ${ filePath }` ) ; this . log . system . error ( err ) ; } ) ; } } if ( hasVideoFiles === true && typeof recorded . videoFiles !== 'undefined' ) { for ( const v of recorded . videoFiles ) { let filePath ; try { filePath = await this . videoUtil . getFullFilePathFromId ( v . id ) ; if ( filePath === null ) { throw new Error ( 'GetVideoFilePathError' ) ; } } catch ( err ) { this . log . system . error ( `get video file path error: ${ v . id }` ) ; this . log . system . error ( err ) ; this . log . system . error ( v ) ; continue ; } this . log . system . info ( `delete: ${ filePath }` ) ; await FileUtil . unlink ( filePath ) . catch ( err => { this . log . system . error ( `failed to delete ${ filePath }` ) ; this . log . system . error ( err ) ; } ) ; } } if ( typeof recorded . dropLogFile !== 'undefined' && recorded . dropLogFile !== null ) { const filePath = this . getDropLogFilePath ( recorded . dropLogFile ) ; this . log . system . info ( `delete: ${ filePath }` ) ; await FileUtil . unlink ( filePath ) . catch ( err => { this . log . system . error ( `failed to delete ${ filePath }` ) ; this . log . system . error ( err ) ; } ) ; } if ( hasThumbnails === true ) { this . thumbnailDB . deleteRecordedId ( recordedId ) . catch ( err => { this . log . system . error ( `falied to delete thumbnail data: ${ recordedId }` ) ; this . log . system . error ( err ) ; } ) ; } if ( hasVideoFiles === true ) { await this . videoFileDB . deleteRecordedId ( recordedId ) . catch ( err => { this . log . system . error ( `falied to delete video data: ${ recordedId }` ) ; this . log . system . error ( err ) ; } ) ; } await this . recordedDB . deleteOnce ( recordedId ) . catch ( err => { this . log . system . error ( `falied to delete recorded data: ${ recordedId }` ) ; this . log . system . error ( err ) ; } ) ; if ( typeof recorded . dropLogFile !== 'undefined' && recorded . dropLogFile !== null ) { await this . dropLogFileDB . deleteOnce ( recorded . dropLogFile . id ) . catch ( err => { this . log . system . error ( `failed to delete drop log data: ${ recorded . dropLogFile ?. id }` ) ; this . log . system . error ( err ) ; } ) ; } this . log . system . info ( `successful delete recorded: ${ recordedId }` ) ; this . recordedEvent . emitDeleteRecorded ( recorded ) ; } param thumbnail return private getThumbnailPath ( thumbnail ) { return path . join ( this . config . thumbnail , thumbnail . filePath ) ; } param dropLogFile return private getDropLogFilePath ( dropLogFile ) { return path . join ( this . config . dropLog , dropLogFile . filePath ) ; } param videoFileId return public async updateVideoFileSize ( videoFileId ) { this . log . system . info ( `update video file size: ${ videoFileId }` ) ; const filePath = await this . videoUtil . getFullFilePathFromId ( videoFileId ) ; if ( filePath === null ) { this . log . system . error ( `video file is not found: ${ videoFileId }` ) ; throw new Error ( 'VideoFileIsNotFound' ) ; } const fileSize = await FileUtil . getFileSize ( filePath ) ; await this . videoFileDB . updateSize ( videoFileId , fileSize ) ; this . recordedEvent . emitUpdateVideoFileSize ( videoFileId ) ; } param option return public async addVideoFile ( option ) { this . log . system . info ( `add video file: ${ option . recordedId } ${ option . filePath }` ) ; const parentDirPath = this . videoUtil . getParentDirPath ( option . parentDirectoryName ) ; if ( parentDirPath === null ) { this . log . system . error ( `parent directory is null: ${ option . parentDirectoryName }` ) ; throw new Error ( 'ParentDirectoryIsNull' ) ; } const fileSize = await FileUtil . getFileSize ( path . join ( parentDirPath , option . filePath ) ) ; const videoFile = new VideoFile ( ) ; videoFile . parentDirectoryName = option . parentDirectoryName ; videoFile . filePath = option . filePath ; videoFile . type = option . type ; videoFile . name = option . name ; videoFile . size = fileSize ; videoFile . recordedId = option . recordedId ; const newVideoFileId = await this . videoFileDB . insertOnce ( videoFile ) . catch ( err => { this . log . system . error ( `failed to add video: ${ option . parentDirectoryName }/${ option . filePath }` ) ; this . log . system . error ( err ) ; throw err ; } ) ; this . recordedEvent . emitAddVideoFile ( newVideoFileId ) ; return newVideoFileId ; } param option return public async addUploadedVideoFile ( option ) { this . log . system . info ( `add uploaded file: ${ option . recordedId }` ) ; const recorded = await this . recordedDB . findId ( option . recordedId ) ; if ( recorded === null ) { await FileUtil . unlink ( option . filePath ) . catch ( ( ) => { } ) ; throw new Error ( 'RecordedIdIsNull' ) ; } const parentDirPath = this . videoUtil . getParentDirPath ( option . parentDirectoryName ) ; if ( parentDirPath === null ) { this . log . system . error ( `parent directory is null: ${ option . parentDirectoryName }` ) ; await FileUtil . unlink ( option . filePath ) . catch ( ( ) => { } ) ; throw new Error ( 'ParentDirectoryIsNull' ) ; } let dirPath = parentDirPath ; if ( typeof option . subDirectory !== 'undefined' ) { dirPath = path . join ( dirPath , option . subDirectory ) ; try { await FileUtil . stat ( dirPath ) ; } catch ( err ) { this . log . system . info ( `mkdirp: ${ dirPath }` ) ; await mkdirp ( dirPath ) ; } } const filePath = await this . getUploadedVideoFilePath ( dirPath , option . fileName ) ; try { this . log . system . info ( `move file ${ option . filePath } -> ${ filePath }` ) ; await FileUtil . rename ( option . filePath , filePath ) ; } catch ( err ) { try { await FileUtil . move ( option . filePath , filePath ) ; } catch ( e ) { this . log . system . error ( 'move file error' ) ; this . log . system . error ( e ) ; await FileUtil . unlink ( option . filePath ) . catch ( ( ) => { } ) ; throw new Error ( 'FileMoveError' ) ; } } try { const fileName = path . basename ( filePath ) ; const videoFileId = await this . addVideoFile ( { recordedId : option . recordedId , parentDirectoryName : option . parentDirectoryName , filePath : typeof option . subDirectory === 'undefined' ? fileName : path . join ( option . subDirectory , fileName ) , type : option . fileType , name : option . viewName , } ) ; const needsCreateThumbnail = typeof recorded . thumbnails === 'undefined' || recorded . thumbnails . length === 0 ; this . recordedEvent . emitAddUploadedVideoFile ( videoFileId , needsCreateThumbnail ) ; } catch ( err ) { await FileUtil . unlink ( filePath ) . catch ( ( ) => { } ) ; throw err ; } } param dir param fileName param conflict return private async getUploadedVideoFilePath ( dir , fileName , conflict = 0 ) { const extname = path . extname ( fileName ) ; const name = fileName . slice ( 0 , fileName . length - extname . length ) ; const count = conflict > 0 ? `(${ conflict })` : '' ; const filePath = path . join ( dir , `${ name }${ count }${ extname }` ) ; try { await FileUtil . stat ( filePath ) ; return this . getUploadedVideoFilePath ( dir , fileName , conflict + 1 ) ; } catch ( err ) { return filePath ; } } param option return public async createNewRecorded ( option ) { this . log . system . info ( 'create new recorded' ) ; const recorded = new Recorded ( ) ; recorded . isRecording = false ; recorded . isProtected = false ; if ( typeof option . ruleId !== 'undefined' ) { recorded . ruleId = option . ruleId ; } recorded . channelId = option . channelId ; recorded . startAt = option . startAt ; recorded . endAt = option . endAt ; if ( option . startAt - option . endAt >= 0 ) { throw new Error ( 'TimeRangeError' ) ; } recorded . duration = option . endAt - option . startAt ; recorded . name = StrUtil . toDBStr ( option . name ) ; recorded . halfWidthName = StrUtil . toHalf ( option . name ) ; if ( typeof option . description !== 'undefined' ) { recorded . description = StrUtil . toDBStr ( option . description ) ; recorded . halfWidthDescription = StrUtil . toHalf ( recorded . description ) ; } if ( typeof option . extended !== 'undefined' ) { recorded . extended = StrUtil . toDBStr ( option . extended ) ; recorded . halfWidthExtended = StrUtil . toHalf ( recorded . extended ) ; } if ( typeof option . genre1 !== 'undefined' ) { recorded . genre1 = option . genre1 ; } if ( typeof option . subGenre1 !== 'undefined' ) { recorded . subGenre1 = option . subGenre1 ; } if ( typeof option . genre2 !== 'undefined' ) { recorded . genre2 = option . genre2 ; } if ( typeof option . subGenre2 !== 'undefined' ) { recorded . subGenre2 = option . subGenre2 ; } if ( typeof option . genre3 !== 'undefined' ) { recorded . genre3 = option . genre3 ; } if ( typeof option . subGenre3 !== 'undefined' ) { recorded . subGenre3 = option . subGenre3 ; } const recordedId = await this . recordedDB . insertOnce ( recorded ) . catch ( err => { this . log . system . error ( err ) ; throw err ; } ) ; this . log . system . info ( `created new recorded: ${ recordedId }` ) ; this . recordedEvent . emitCreateNewRecorded ( recordedId ) ; return recordedId ; } param videoFileid param isIgnoreProtection return public async deleteVideoFile ( videoFileid , isIgnoreProtection = false ) { this . log . system . info ( `delete video file: ${ videoFileid }` ) ; const video = await this . videoFileDB . findId ( videoFileid ) ; if ( video === null ) { this . log . system . info ( `video file is not found: ${ videoFileid }` ) ; throw new Error ( 'VideoFileIsNotFound' ) ; } let recorded = await this . recordedDB . findId ( video . recordedId ) ; if ( isIgnoreProtection === false && recorded !== null && recorded . isProtected === true ) { this . log . system . warn ( `${ videoFileid } is protected` ) ; throw new Error ( 'RecordedIsProtected' ) ; } if ( recorded ?. isRecording === true ) { return await this . delete ( video . recordedId , false ) ; } const filePath = await this . videoUtil . getFullFilePathFromId ( videoFileid ) ; if ( filePath !== null ) { this . log . system . info ( `delete: ${ filePath }` ) ; await FileUtil . unlink ( filePath ) . catch ( err => { this . log . system . error ( `failed to delete ${ filePath }` ) ; this . log . system . error ( err ) ; } ) ; } await this . videoFileDB . deleteOnce ( videoFileid ) ; recorded = await this . recordedDB . findId ( video . recordedId ) ; if ( recorded !== null && typeof recorded . videoFiles !== 'undefined' && recorded . videoFiles . length === 0 ) { this . log . system . info ( `empty video files: ${ video . recordedId }` ) ; await this . delete ( video . recordedId , false ) ; } else { this . recordedEvent . emitDeleteVideoFile ( videoFileid ) ; } } param recordedId param isProtect return public async changeProtect ( recordedId , isProtect ) { this . log . system . info ( ( isProtect === true ? 'set protect' : 'remove protect' ) + `: ${ recordedId }` ) ; await this . recordedDB . changeProtect ( recordedId , isProtect ) ; this . recordedEvent . emitChangeProtect ( recordedId , isProtect ) ; } return public async historyCleanup ( ) { const date = new Date ( ) . getTime ( ) - this . config . recordedHistoryRetentionPeriodDays * 24 * 60 * 60 * 1000 ; await this . recordedHistoryDB . delete ( date ) . catch ( err => { this . log . system . error ( 'failed to historyCleanup' ) ; this . log . system . error ( err ) ; } ) ; } return public async videoFileCleanup ( ) { this . log . system . info ( 'start video files cleanup' ) ; const videoFiles = await this . videoFileDB . findAll ( ) ; const fileIndex = { } ; const dirIndex = { } ; for ( const video of videoFiles ) { const videoFilePath = this . videoUtil . getFullFilePathFromVideoFile ( video ) ; if ( videoFilePath === null ) { continue ; } if ( ( await this . checkFileExistence ( videoFilePath ) ) === true ) { fileIndex [ videoFilePath ] = true ; const parentDir = path . dirname ( videoFilePath ) . replace ( new RegExp ( `\\${ path . sep }$` ) , '' ) ; dirIndex [ parentDir ] = true ; } else { await this . deleteVideoFile ( video . id ) . catch ( ( ) => { } ) ; } } const list = { files : [ ] , directories : [ ] , } ; for ( const r of this . config . recorded ) { const l = await FileUtil . getFileList ( r . path ) ; Array . prototype . push . apply ( list . files , l . files ) ; Array . prototype . push . apply ( list . directories , l . directories ) ; dirIndex [ r . path ] = true ; } list . directories . sort ( ( dir1 , dir2 ) => { return dir2 . length - dir1 . length ; } ) ; for ( const file of list . files ) { if ( typeof fileIndex [ file ] !== 'undefined' ) { continue ; } this . log . system . info ( `delete file: ${ file }` ) ; await FileUtil . unlink ( file ) . catch ( err => { this . log . system . error ( `failed to delete file: ${ file }` ) ; this . log . system . error ( err ) ; } ) ; } for ( const dir of list . directories ) { if ( typeof dirIndex [ dir ] !== 'undefined' ) { continue ; } this . log . system . info ( `delete directory: ${ dir }` ) ; try { if ( ( await FileUtil . isEmptyDirectory ( dir ) ) === true ) { await FileUtil . rmdir ( dir ) ; } else { this . log . system . warn ( `directory is not empty: ${ dir }` ) ; } } catch ( err ) { this . log . system . error ( `failed to delete directory: ${ dir }` ) ; this . log . system . error ( err ) ; } } this . log . system . info ( 'start video files cleanup completed' ) ; } /**
     * DB に登録されていないログファイル削除 &  DB に登録されているが存在しないログ情報の削除
     */ public async dropLogFileCleanup ( ) { this . log . system . info ( 'start drop log files cleanup' ) ; const dropLogs = await this . dropLogFileDB . findAll ( ) ; const fileIndex = { } ; for ( const dropLog of dropLogs ) { const filePath = this . getDropLogFilePath ( dropLog ) ; if ( ( await this . checkFileExistence ( filePath ) ) === true ) { fileIndex [ filePath ] = true ; } else { this . log . system . warn ( `drop file is not exist: ${ filePath }` ) ; try { await this . recordedDB . removeDropLogFileId ( dropLog . id ) ; await this . dropLogFileDB . deleteOnce ( dropLog . id ) ; } catch ( err ) { this . log . system . error ( err ) ; } } } const list = await FileUtil . getFileList ( this . config . dropLog ) ; for ( const file of list . files ) { if ( typeof fileIndex [ file ] !== 'undefined' ) { continue ; } this . log . system . info ( `delete drop log file: ${ file }` ) ; await FileUtil . unlink ( file ) . catch ( err => { this . log . system . error ( `failed to drop log file: ${ file }` ) ; this . log . system . error ( err ) ; } ) ; } this . log . system . info ( 'start drop log files cleanup completed' ) ; } param filePath return private async checkFileExistence ( filePath ) { try { await FileUtil . stat ( filePath ) ; return true ; } catch ( err ) { return false ; } } param ruleId public async removeRuleId ( ruleId ) { await this . recordedDB . removeRuleId ( ruleId ) ; } }