DockerERTFF/lib/modules/metadata.js

484 lines
16 KiB
JavaScript

const fs = require('fs-extra');
const imageTypes = ['PNG', 'GIF', 'BMP', 'JPEG', 'TIFF'];
const videoTypes = ['AVI', 'MP4', 'MOV', 'MKV', 'WEBM', 'OGV'];
const soundTypes = ['OGG', 'WAV', 'MP3', 'FLAC'];
const read = async (path, offset, length) => {
const fp = await fs.open(path);
const buff = Buffer.alloc(length);
await fs.read(fd, buff, 0, length, offset);
await fs.close();
return buff;
};
const parser = {
PNG: async (path, result) => {
const buff = await read(path, 18, 6);
result.width = buff.readUInt16BE(0);
result.height = buff.readUInt16BE(4);
},
GIF: async (path, result) => {
const buff = await read(path, 6, 4);
result.width = buff.readUInt16LE(0);
result.height = buff.readUInt16LE(2);
},
BMP: async (path, result) => {
const buff = await read(path, 18, 8);
result.width = buff.readUInt32LE(0);
result.height = buff.readUInt32LE(4);
},
JPEG: async (path, result) => {
const sof = [0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf, 0xde];
const buff = await read(path, 2, 64000);
let pos = 0;
while (buff[pos++] == 0xff) {
let marker = buff[pos++];
let size = buff.readUInt16BE(pos);
if (marker == 0xda) break;
if (sof.includes(marker)) {
result.height = buff.readUInt16BE(pos + 3);
result.width = buff.readUInt16BE(pos + 5);
break;
}
pos += size;
}
},
TIFF: async (path, result) => {
const buff = await read(path, 0, 64000);
const le = buff.toString('ascii', 0, 2) == 'II';
let pos = 0;
const readUInt16 = () => { pos += 2; return buff[le ? 'readUInt16LE' : 'readUInt16BE'](pos - 2); }
const readUInt32 = () => { pos += 4; return buff[le ? 'readUInt32LE' : 'readUInt32BE'](pos - 4); }
let offset = readUInt32();
while (pos < buff.length && offset > 0) {
let entries = readUInt16(offset);
let start = pos;
for (let i = 0; i < entries; i++) {
let tag = readUInt16();
let type = readUInt16();
let length = readUInt32();
let data = (type == 3) ? readUInt16() : readUInt32();
if (type == 3) pos += 2;
if (tag == 256) {
result.width = data;
} else if (tag == 257) {
result.height = data;
}
if (result.width > 0 && result.height > 0) {
return;
}
}
offset = readUInt32();
pos += offset;
}
},
AVI: async (path, result) => {
const buff = await read(path, 0, 144);
result.width = buff.readUInt32LE(64);
result.height = buff.readUInt32LE(68);
result.duration = ~~(buff.readUInt32LE(128) / buff.readUInt32LE(132) * buff.readUInt32LE(140));
},
MP4: async (path, result) => {
return parser.MOV(path, result);
},
MOV: async (path, result, pos = 0) => {
const buff = await read(path, 0, 64000);
while (pos < buff.length) {
let size = buff.readUInt32BE(pos);
let name = buff.toString('ascii', pos + 4, 4);
if (name == 'mvhd') {
let scale = buff.readUInt32BE(pos + 20);
let duration = buff.readUInt32BE(pos + 24);
result.duration = ~~(duration / scale);
}
if (name == 'tkhd') {
let m0 = buff.readUInt32BE(pos + 48);
let m4 = buff.readUInt32BE(pos + 64);
let w = buff.readUInt32BE(pos + 84);
let h = buff.readUInt32BE(pos + 88);
if (w > 0 && h > 0) {
result.width = w / m0;
result.height = h / m4;
return;
}
}
if (name == 'moov' || name == 'trak') {
await parser.MOV(path, pos + 8);
}
pos += size;
}
},
WEBM: async (path, result) => {
return parser.EBML(path, result);
},
MKV: async (path, result) => {
return parser.EBML(path, result);
},
EBML: async (path, result) => {
const containers = ['\x1a\x45\xdf\xa3', '\x18\x53\x80\x67', '\x15\x49\xa9\x66', '\x16\x54\xae\x6b', '\xae', '\xe0'];
const buff = await read(path, 0, 64000);
// TODO parse EBML
},
OGV: async (path, result) => {
return parser.OGG(path, result);
},
OGG: async (path, result) => {
const buff = await read(apth, 0, 64000);
let pos = 0, vorbis;
while (buff.toString('ascii', pos, pos + 4) == 'OggS') {
let version = buff[pos + 4];
let b = buff[pos + 5];
let continuation = !!(b & 0x01);
let bos = !!(b & 0x02);
let eos = !!(b & 0x04);
let position = Number(buff.readBigUInt64LE(pos + 6));
let serial = buff.readUInt32LE(pos + 14);
let pageNumber = buff.readUInt32LE(pos + 18);
let checksum = buff.readUInt32LE(pos + 22);
let pageSegments = buff[path + 26];
let lacing = buff.slice(pos + 27, pos + 27 + pageSegments);
let pageSize = lacing.reduce((p, v) => p + v, 0);
let start = pos + 27 + pageSegments;
let pageHeader = buff.slice(start, start + 7);
if (pageHeader.compare(Buffer.from([0x01, 'v', 'o', 'r', 'b', 'i', 's']))) {
vorbis = { serial, sampleRate: buff.readUInt32LE(start + 12) };
}
if (pageHeader.compare(Buffer.from([0x80, 't', 'h', 'e', 'o', 'r', 'a']))) {
let version = buff.slice(start + 7, start + 10);
result.width = buff.readUInt16BE(start + 10) << 4;
result.height = buff.readUInt16BE(start + 12) << 4;
if (version >= 0x030200) {
let width = buff.slice(start + 14, start + 17);
let height = buff.slice(start + 17, start + 20);
if (width <= result.width && width > result.width - 16 && height <= result.height && height > result.height - 16) {
result.width = width;
result.height = height;
}
}
}
if (eos && vorbis && serail == vorbis.serial) {
result.duration = ~~(position / vorbis.sampleRate);
}
pos = start + pageSize;
}
},
WAV: async (path, result) => {
const buff = await read(path, 0, 32);
let size = buff.readUInt32LE(4);
let rate = buff.readUInt32LE(28);
result.duration = ~~(size / rate);
},
MP3: async (path, result) => {
const versions = [2.5, 0, 2, 1];
const layers = [0, 3, 2, 1];
const bitrates = [
[ // version 2.5
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
[0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3
[0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2
[0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256] // layer 1
],
[ // reserved
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] // reserved
],
[ // version 2
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
[0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 3
[0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160], // layer 2
[0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256] // layer 1
],
[ // version 1
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // reserved
[0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320], // layer 3
[0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384], // layer 2
[0,32,64,96,128,160,192,224,256,288,320,352,384,416,448] // layer 1
]
]
const srates = [
[11025, 12000, 8000, 0], // mpeg 2.5
[ 0, 0, 0, 0], // reserved
[22050, 24000, 16000, 0], // mpeg 2
[44100, 48000, 32000, 0] // mpeg 1
]
const tsamples = [
[0, 576, 1152, 384], // mpeg 2.5
[0, 0, 0, 0], // reserved
[0, 576, 1152, 384], // mpeg 2
[0, 1152, 1152, 384] // mpeg 1
];
const slotSizes = [0, 1, 1, 4];
const modes = ['stereo', 'joint_stereo', 'dual_channel', 'mono'];
const buff = await read(path, 0, 64000);
let duration = 0;
let count = 0;
let skip = 0;
let pos = 0;
while (pos < buff.length) {
let start = pos;
if (buff.toString('ascii', pos, 4) == 'TAG+') {
skip += 227;
pos += 227;
} else if (buff.toString('ascii', pos, 3) == 'TAG') {
skip += 128;
pos += 128;
} else if (buff.toString('ascii', pos, 3) == 'ID3') {
let bytes = buff.readUInt32BE(pos + 6);
let size = 10 + (bytes[0] << 21 | bytes[1] << 14 | bytes[2] << 7 | bytes[3]);
skip += size;
pos += size;
} else {
let hdr = buff.slice(pos, pos + 4);
while (pos < buff.length && !(hdr[0] == 0xff && (hdr[1] & 0xe0) == 0xe0)) {
pos++;
hdr = buff.slice(pos, pos + 4);
}
let ver = (hdr[1] & 0x18) >> 3;
let lyr = (hdr[1] & 0x06) >> 1;
let pad = (hdr[2] & 0x02) >> 1;
let brx = (hdr[2] & 0xf0) >> 4;
let srx = (hdr[2] & 0x0c) >> 2;
let mdx = (hdr[3] & 0xc0) >> 6;
let version = versions[ver];
let layer = layers[lyr];
let bitrate = bitrates[ver][lyr][brx] * 1000;
let samprate = srates[ver][srx];
let samples = tsamples[ver][lyr];
let slotSize = slotSizes[lyr];
let mode = modes[mdx];
let fsize = ~~(((samples / 8 * bitrate) / samprate) + (pad ? slotSize : 0));
count++;
if (count == 1) {
if (layer != 3) {
pos += 2;
} else {
if (mode != 'mono') {
if (version == 1) {
pos += 32;
} else {
pos += 17;
}
} else {
if (version == 1) {
pos += 17;
} else {
pos += 9;
}
}
}
if (buff.toString('ascii', pos, pos + 4) == 'Xing' && (buff.readUInt32BE(pos + 4) & 0x0001) == 0x0001) {
let totalFrames = buff.readUInt32BE(pos + 8);
duration = totalFrames * samples / samprate;
break;
}
}
if (fsize < 1) break;
pos = start + fsize;
duration += (samples / samprate);
}
}
result.duration = ~~duration;
},
FLAC: async (path, result) => {
const buff = await read(path, 18, 8);
let rate = (buff[0] << 12) | (buff[1] << 4) | ((buff[2] & 0xf0) >> 4);
let size = ((buff[3] & 0x0f) << 32) | (buff[4] << 24) | (buff[5] << 16) | (buff[6] << 8) | buff[7];
result.duration = ~~(size / rate);
},
};
async function detect(path) {
const buff = await read(path, 0, 12);
if (buff.slice(0, 8).compare(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
return 'PNG';
}
if (buff.toString('ascii', 0, 3) == 'GIF') {
return 'GIF';
}
if (buff.toString('ascii', 0, 2) == 'BM') {
return 'BMP';
}
if (buff.slice(0, 2).compare(Buffer.from([0xff, 0xd8]))) {
return 'JPEG';
}
if (buff.toString('ascii', 0, 2) == 'II' && buff.readUInt16LE(2) == 42) {
return 'TIFF';
}
if (buff.toString('ascii', 0, 2) == 'MM' && buff.readUInt16BE(2) == 42) {
return 'TIFF';
}
if (buff.toString('ascii', 0, 4) == 'RIFF' && buff.toString('ascii', 8, 4) == 'AVI ') {
return 'AVI';
}
if (buff.toString('ascii', 4, 4) == 'ftyp') {
return 'MP4';
}
if (buff.toString('ascii', 4, 4) == 'moov') {
return 'MOV';
}
if (buff.slice(0, 4).compare(Buffer.from([0x1a, 0x45, 0xdf, 0xa3]))) {
// TODO detect MKV
return 'EBML';
}
if (buff.toString('ascii', 0, 4) == 'OggS') {
// TODO detect OGV
return 'OGG'
}
if (buff.toString('ascii', 0, 4) == 'RIFF', buff.toString('ascii', 8, 4) == 'WAVE') {
return 'WAV';
}
if (buff.toString('ascii', 0, 3) == 'ID3' || (buf[0] == 0xff && (buff[1] & 0xe0))) {
return 'MP3';
}
if (buff.toString('ascii', 0, 4) == 'fLaC') {
return 'FLAC';
}
return null
}
module.exports = {
detect: async function(options) {
let path = this.parseRequired(options.path, 'string', 'metadata.detect: path is required.');
return detect(path);
},
isImage: async function(options) {
let path = this.parseRequired(options.path, 'string', 'metadata.isImage: path is required.');
let type = await detect(path);
let cond = imageTypes.includes(type);
if (cond) {
if (options.then) {
await this.exec(options.then, true);
}
} else if (options.else) {
await this.exec(options.else, true);
}
return cond;
},
isVideo: async function(options) {
let path = this.parseRequired(options.path, 'string', 'metadata.isVideo: path is required.');
let type = await detect(path);
let cond = videoTypes.includes(type);
if (cond) {
if (options.then) {
await this.exec(options.then, true);
}
} else if (options.else) {
await this.exec(options.else, true);
}
return cond;
},
isSound: async function(options) {
let path = this.parseRequired(options.path, 'string', 'metadata.isSound: path is required.');
let type = await detect(path);
let cond = soundTypes.includes(type);
if (cond) {
if (options.then) {
await this.exec(options.then, true);
}
} else if (options.else) {
await this.exec(options.else, true);
}
return cond;
},
fileinfo: async function(options) {
let path = this.parseRequired(options.path, 'string', 'metadata.fileinfo: path is required.');
let type = await detect(path);
let result = { type, width: null, height: null, duration: null };
if (parser[type]) {
await parser[type](path, result);
}
return result;
},
};