diff --git a/interfaces/final-object.interface.ts b/interfaces/final-object.interface.ts index bd187718..1cc648ad 100644 --- a/interfaces/final-object.interface.ts +++ b/interfaces/final-object.interface.ts @@ -5,6 +5,8 @@ export type AllowedScreenshotHeight = 144 | 216 | 288 | 360 | 432 | 504; export type ResolutionString = '' | 'SD' | '720' | '720+' | '1080' | '1080+' | '4K' | '4K+'; +export type FileType = 'video' | 'image'; + export interface SourceFolder { path: string; watch: boolean; @@ -25,12 +27,13 @@ export interface FinalObject { } export interface ImageElement { + type: FileType; // type of file birthtime: number; // file creation time cleanName: string; // file name cleaned of dots, underscores,and file extension; for searching. Can be *FOLDER* sometimes duration: number; // number of seconds - duration of film fileName: string; // full file name with extension - for opening the file fileSize: number; // file size in bytes - bitrate: number; // bitrate of the displayed video file - (fileSize/duration)*1024 + bitrate: number; // bitrate of the displayed video file - (fileSize/duration)*1024 fps: number; // base frame rate of the video in fps hash: string; // used for detecting changed files and as a screenshot identifier height: AllowedScreenshotHeight; // height of the video (px) @@ -67,6 +70,7 @@ export interface ImageElementPlus extends ImageElement { // Use this to create a new ImageElement if needed export function NewImageElement(): ImageElement { return { + type: 'video', birthtime: 0, cleanName: '', duration: 0, diff --git a/node/main-extract-async.ts b/node/main-extract-async.ts index ca447a63..1fb425fa 100644 --- a/node/main-extract-async.ts +++ b/node/main-extract-async.ts @@ -12,7 +12,7 @@ import { fdir } from 'fdir'; import { GLOBALS } from './main-globals'; import type { ImageElement, ImageElementPlus } from '../interfaces/final-object.interface'; -import { acceptableFiles } from './main-filenames'; +import { acceptableImageFiles, acceptableVideoFiles } from './main-filenames'; import { extractAll } from './main-extract'; import { sendCurrentProgress, insertTemporaryFieldsSingle, extractMetadataAsync, cleanUpFileName } from './main-support'; @@ -238,7 +238,7 @@ function superFastSystemScan(inputDir: string, inputSource: number): void { console.log('Found ', files.length, ' files in given directory'); // ============================================================================================= - const allAcceptableFiles: string[] = [...acceptableFiles, ...GLOBALS.additionalExtensions]; + const allAcceptableFiles: string[] = [...acceptableVideoFiles, ...acceptableImageFiles, ...GLOBALS.additionalExtensions]; files.forEach((fullPath: string) => { @@ -315,7 +315,7 @@ export function startFileSystemWatching(inputDir: string, inputSource: number, p const watcher: FSWatcher = chokidar.watch(inputDir, watcherConfig); - const allAcceptableFiles: string[] = [...acceptableFiles, ...GLOBALS.additionalExtensions]; + const allAcceptableFiles: string[] = [...acceptableVideoFiles, ...acceptableImageFiles, ...GLOBALS.additionalExtensions]; metadataQueue.pause(); thumbQueue.pause(); diff --git a/node/main-extract.ts b/node/main-extract.ts index ce58b1c0..7737e0e0 100644 --- a/node/main-extract.ts +++ b/node/main-extract.ts @@ -30,6 +30,7 @@ const ffmpegPath = require('ffmpeg-static').replace('app.asar', 'app.asar.unpack import { GLOBALS } from './main-globals'; import type { ImageElement, ScreenshotSettings } from '../interfaces/final-object.interface'; +import { acceptableImageFiles } from './main-filenames'; // ======================================================================================== @@ -44,16 +45,26 @@ import type { ImageElement, ScreenshotSettings } from '../interfaces/final-objec * @param savePath */ const extractSingleFrameArgs = ( + currentElement: ImageElement, pathToVideo: string, screenshotHeight: number, - duration: number, savePath: string, ): string[] => { const ssWidth: number = screenshotHeight * (16 / 9); + if (currentElement.type == 'image') { + const args: string[] = [ + '-i', pathToVideo, + '-q:v', '2', + '-vf', scaleAndPadString(ssWidth, screenshotHeight), + savePath, + ]; + return args; + } + const args: string[] = [ - '-ss', (duration / 10).toString(), + '-ss', (currentElement.duration / 10).toString(), '-i', pathToVideo, '-frames', '1', '-q:v', '2', @@ -280,7 +291,7 @@ export function extractAll( return true; } else { const ffmpegArgs: string[] = extractSingleFrameArgs( - pathToVideo, screenshotHeight, duration, thumbnailSavePath + currentElement, pathToVideo, screenshotHeight, thumbnailSavePath ); return spawn_ffmpeg_and_run(ffmpegArgs, maxRunTime.thumb, 'thumb'); // (3) @@ -292,6 +303,11 @@ export function extractAll( if (!thumbSuccess) { throw new Error('SINGLE SCREENSHOT EXTRACTION TIMED OUT - LIKELY CORRUPT'); } else { + if (currentElement.type == 'image') { + // Bale out early, images don't need anything else + done(); + return; + } return checkFileExists(filmstripSavePath); // (4) } }) diff --git a/node/main-filenames.ts b/node/main-filenames.ts index 288815af..628616eb 100644 --- a/node/main-filenames.ts +++ b/node/main-filenames.ts @@ -1,4 +1,4 @@ -export const acceptableFiles = [ +export const acceptableVideoFiles = [ '264', '265', '3g2', @@ -32,3 +32,7 @@ export const acceptableFiles = [ 'webm', 'wmv' ]; + +export const acceptableImageFiles = [ + 'jpg' +]; diff --git a/node/main-support.ts b/node/main-support.ts index b87d3755..9ef63c9e 100644 --- a/node/main-support.ts +++ b/node/main-support.ts @@ -17,9 +17,10 @@ const fs = require('fs'); const hasher = require('crypto').createHash; import type { Stats } from 'fs'; -import type { FinalObject, ImageElement, ScreenshotSettings, InputSources, ResolutionString} from '../interfaces/final-object.interface'; +import type { FinalObject, ImageElement, ScreenshotSettings, InputSources, ResolutionString, FileType} from '../interfaces/final-object.interface'; import { NewImageElement } from '../interfaces/final-object.interface'; import { startFileSystemWatching, resetWatchers } from './main-extract-async'; +import { acceptableImageFiles, acceptableVideoFiles } from './main-filenames'; interface ResolutionMeta { label: ResolutionString; @@ -305,7 +306,7 @@ function getBestStream(metadata) { */ function getFileDuration(metadata): number { if (metadata?.streams?.[0]?.duration) { - + return metadata.streams[0].duration; } else if (metadata?.format?.duration) { @@ -451,12 +452,14 @@ export function extractMetadataAsync( const origWidth = stream.width || 0; // ffprobe does not detect it on some MKV streams const origHeight = stream.height || 0; + fs.stat(filePath, (err2, fileStat) => { if (err2) { reject(); } const imageElement = NewImageElement(); + imageElement.type = getFileType(filePath); imageElement.birthtime = Math.round(fileStat.birthtimeMs); imageElement.duration = duration; imageElement.fileSize = fileStat.size; @@ -478,6 +481,15 @@ export function extractMetadataAsync( }); } +export function getFileType(filePath: string): FileType { + const fileExtension = path.parse(filePath).ext.substr(1).toLowerCase(); + if (acceptableVideoFiles.includes(fileExtension)) { + return 'video'; + } else if (acceptableImageFiles.includes(fileExtension)) { + return 'image'; + } +} + /** * Sends progress to Angular App * @param current number diff --git a/src/app/components/views/details/details.component.ts b/src/app/components/views/details/details.component.ts index 8cee6acf..01fc94ca 100644 --- a/src/app/components/views/details/details.component.ts +++ b/src/app/components/views/details/details.component.ts @@ -91,7 +91,11 @@ export class DetailsComponent implements OnInit { ngOnInit() { this.firstFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'thumbnails', this.video.hash); - this.filmstripPath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); + if (this.video.type == 'image') { + this.filmstripPath = this.firstFilePath; + } else { + this.filmstripPath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); + } if (this.video.defaultScreen !== undefined) { this.percentOffset = this.getDefaultScreenOffset(this.video); } diff --git a/src/app/components/views/filmstrip/filmstrip.component.ts b/src/app/components/views/filmstrip/filmstrip.component.ts index 2507f579..da0685fa 100644 --- a/src/app/components/views/filmstrip/filmstrip.component.ts +++ b/src/app/components/views/filmstrip/filmstrip.component.ts @@ -48,7 +48,11 @@ export class FilmstripComponent implements OnInit { ) { } ngOnInit() { - this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); + if (this.video.type == 'image') { + this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'thumbnails', this.video.hash); + } else { + this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); + } } updateFilmXoffset($event) { diff --git a/src/app/components/views/full/full.component.ts b/src/app/components/views/full/full.component.ts index 820d0555..0330e949 100644 --- a/src/app/components/views/full/full.component.ts +++ b/src/app/components/views/full/full.component.ts @@ -57,8 +57,11 @@ export class FullViewComponent implements OnInit { ) { } ngOnInit() { - this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); - this.render(); + if (this.video.type == 'image') { + this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'thumbnails', this.video.hash); + } else { + this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); + } this.render(); } render(): void { diff --git a/src/app/components/views/thumbnail/thumbnail.component.ts b/src/app/components/views/thumbnail/thumbnail.component.ts index f9320434..3549b92f 100644 --- a/src/app/components/views/thumbnail/thumbnail.component.ts +++ b/src/app/components/views/thumbnail/thumbnail.component.ts @@ -66,7 +66,11 @@ export class ThumbnailComponent implements OnInit, OnDestroy { }); } else { this.firstFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'thumbnails', this.video.hash); - this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); + if (this.video.type == 'image') { + this.fullFilePath = this.firstFilePath; + } else { + this.fullFilePath = this.filePathService.createFilePath(this.folderPath, this.hubName, 'filmstrips', this.video.hash); + } this.folderThumbPaths.push(this.firstFilePath); }