Skip to content

Commit

Permalink
Fixed being unable to extract video album and title
Browse files Browse the repository at this point in the history
When there was no artist metadata on a video all other metadata
was also ignored. This is fixed for new imports. For old imports
please run the "Rescan video files for missing metadata" task.
This will add missing metadata from the video files to the database.
It will not clobber any existing metadata in the DB.
  • Loading branch information
SimplyBoo committed Apr 22, 2021
1 parent a74dcc0 commit d8d336b
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 56 deletions.
50 changes: 49 additions & 1 deletion server/src/cache/import-utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import ChildProcess from 'child_process';
import FFMpeg from 'fluent-ffmpeg';
import FS from 'fs';
import GM from 'gm';
import Path from 'path';
import Rimraf from 'rimraf';
import Stream from 'stream';
import Util from 'util';

import { BaseMedia, Media, MediaType, SegmentMetadata } from '../types';
import { BaseMedia, Media, MediaType, Metadata, SegmentMetadata } from '../types';
import Config from '../config';

export interface Quality {
Expand Down Expand Up @@ -36,6 +37,53 @@ export class ImportUtils {
return Math.round(creationDate.getTime() / 1000);
}

public static async getVideoMetadata(absolutePath: string): Promise<Metadata> {
// The ffprobe typings are broken with promisify.
const data = await Util.promisify(FFMpeg.ffprobe as any)(absolutePath);

const mediaData = data.streams.find((stream: any) => stream.codec_type === 'video');
if (!mediaData) {
throw new Error('No video streams found to extract metadata from');
}

const metadata: Metadata = {
length: Math.ceil(data.format.duration),
qualityCache: [],
width: mediaData.width || mediaData.coded_width,
height: mediaData.height || mediaData.coded_height,
codec: mediaData.codec_name,
...(data.format.tags
? {
artist: data.format.tags.artist || data.format.tags.album_artist,
album: data.format.tags.album,
title: data.format.tags.title,
}
: {}),
};

// Delete them so they're not passed around as undefined.
if (!metadata.artist) {
delete metadata.artist;
}
if (!metadata.album) {
delete metadata.album;
}
if (!metadata.title) {
delete metadata.title;
}

return metadata;
}

public static async getImageMetadata(absolutePath: string): Promise<Metadata> {
const gm = GM.subClass({ imageMagick: true })(absolutePath);
const size: Record<string, number> = (await Util.promisify(gm.size.bind(gm))()) as any;
return {
width: size.width,
height: size.height,
};
}

public static getType(filename: string): MediaType {
const ext = Path.extname(filename || '').split('.');
switch (ext[ext.length - 1].toLowerCase()) {
Expand Down
56 changes: 3 additions & 53 deletions server/src/tasks/indexer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { ExecutorPromise, execute } from 'proper-job';
import FFMpeg from 'fluent-ffmpeg';
import GM from 'gm';
import Path from 'path';
import Util from 'util';

import { Database, Media, Metadata, RouterTask, TaskRunnerCallback } from '../types';
import { Database, Media, RouterTask, TaskRunnerCallback } from '../types';
import { ImportUtils } from '../cache/import-utils';
import { Scanner } from './scanner';
import { createHash } from '../cache/hash';
Expand All @@ -13,53 +10,6 @@ import Config from '../config';
export class Indexer {
private database: Database;

public static async getVideoMetadata(absolutePath: string): Promise<Metadata> {
// The ffprobe typings are broken with promisify.
const data = await Util.promisify(FFMpeg.ffprobe as any)(absolutePath);

const mediaData = data.streams.find((stream: any) => stream.codec_type === 'video');
if (!mediaData) {
throw new Error('No video streams found to extract metadata from');
}

const metadata: Metadata = {
length: Math.ceil(data.format.duration),
qualityCache: [],
width: mediaData.width || mediaData.coded_width,
height: mediaData.height || mediaData.coded_height,
codec: mediaData.codec_name,
...(data.format.tags
? {
artist: data.format.tags.artist || data.format.tags.album_artist,
album: data.format.tags.album,
title: data.format.tags.title,
}
: {}),
};

// Delete them so they're not passed around as undefined.
if (!metadata.artist) {
delete metadata.artist;
}
if (!metadata.artist) {
delete metadata.album;
}
if (!metadata.artist) {
delete metadata.title;
}

return metadata;
}

private static async getImageMetadata(absolutePath: string): Promise<Metadata> {
const gm = GM.subClass({ imageMagick: true })(absolutePath);
const size: Record<string, number> = (await Util.promisify(gm.size.bind(gm))()) as any;
return {
width: size.width,
height: size.height,
};
}

public constructor(database: Database) {
this.database = database;
}
Expand All @@ -72,8 +22,8 @@ export class Indexer {
hash: await createHash(absolutePath),
metadata: {
...(type === 'video'
? await Indexer.getVideoMetadata(absolutePath)
: await Indexer.getImageMetadata(absolutePath)),
? await ImportUtils.getVideoMetadata(absolutePath)
: await ImportUtils.getImageMetadata(absolutePath)),
createdAt: await ImportUtils.getFileCreationTime(absolutePath),
},
path: file,
Expand Down
70 changes: 70 additions & 0 deletions server/src/tasks/rescan-video-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { execute } from 'proper-job';

import { Database, RouterTask, TaskRunnerCallback } from '../types';
import { ImportUtils } from '../cache/import-utils';

const BATCH_SIZE = 4;

export function getTask(db: Database): RouterTask {
return {
id: 'RESCAN-VIDEO-METADATA',
description: 'Rescan video files for missing metadata',
runner: (updateStatus: TaskRunnerCallback) => {
return execute(
async () => {
const videos = await db.subset({
corrupted: false,
type: { equalsAll: ['video'] },
});
updateStatus(0, videos.length);

return {
iterable: videos,
init: {
current: 0,
max: videos.length,
},
};
},
async (hash, init) => {
if (!init) {
throw new Error('init not defined');
}

try {
const media = await db.getMedia(hash);
if (!media) {
throw new Error(`Couldn't find media to update metadata: ${hash}`);
}
if (media.metadata?.album && media.metadata?.artist && media.metadata?.title) {
return;
}
const fileMetadata = await ImportUtils.getVideoMetadata(media.absolutePath);
if (!fileMetadata.album && !fileMetadata.artist && !fileMetadata.title) {
return;
}

try {
await db.saveMedia(media.hash, {
metadata: {
...(media.metadata?.album ? {} : { album: fileMetadata.album }),
...(media.metadata?.artist ? {} : { artist: fileMetadata.artist }),
...(media.metadata?.title ? {} : { title: fileMetadata.title }),
},
});
} catch (err) {
console.log('Failed to save media preview state.', err, media);
}
} catch (err) {
console.log(`Error getting metadata for ${hash}.`, err);
await db.saveMedia(hash, { corrupted: true });
throw new Error(`Error getting metadata for ${hash}.`);
} finally {
updateStatus(init.current++, init.max);
}
},
{ parallel: BATCH_SIZE },
);
},
};
}
3 changes: 1 addition & 2 deletions server/src/utils/import-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import Util from 'util';

import { BaseMedia, Database, DumpFile } from '../types';
import { ImportUtils } from '../cache/import-utils';
import { Indexer } from '../tasks/indexer';
import { createHash } from '../cache/hash';
import { setup as setupDb } from '../database';
import Config from '../config';
Expand Down Expand Up @@ -59,7 +58,7 @@ async function importMedia(db: Database, media: BaseMedia, version?: number): Pr
if (media.type === 'video') {
// In older versions, maxCopy was the default.
media.metadata.maxCopy = true;
const metadata = await Indexer.getVideoMetadata(Path.resolve(libraryDir, media.path));
const metadata = await ImportUtils.getVideoMetadata(Path.resolve(libraryDir, media.path));
// Some videos in older versions didn't correctly get the width/height.
media.metadata.width = metadata.width;
media.metadata.height = metadata.height;
Expand Down

0 comments on commit d8d336b

Please sign in to comment.