Drastically improved memory performance media module using streams and temp files.
|
@ -32,7 +32,7 @@
|
||||||
"updates",
|
"updates",
|
||||||
"nsfw"
|
"nsfw"
|
||||||
],
|
],
|
||||||
"author": "Niels Simenon",
|
"author": "DebaucheryLibrarian",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@babel/cli": "^7.8.4",
|
||||||
|
|
Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 189 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 881 KiB After Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 803 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 253 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 39 KiB |
|
@ -17,7 +17,7 @@ const tagPosters = [
|
||||||
['bukkake', 'poster', 'Mia Malkova in "Facialized 2" for HardX'],
|
['bukkake', 'poster', 'Mia Malkova in "Facialized 2" for HardX'],
|
||||||
['caucasian', 0, 'Remy Lacroix for HardX'],
|
['caucasian', 0, 'Remy Lacroix for HardX'],
|
||||||
['creampie', 'poster', 'ALina Lopez in "Making Yourself Unforgettable" for Blacked'],
|
['creampie', 'poster', 'ALina Lopez in "Making Yourself Unforgettable" for Blacked'],
|
||||||
['cum-in-mouth', 1, 'Keisha Grey in Brazzers House'],
|
['cum-in-mouth', 1, 'Sarah Vandella in "Blow Bang Vandella" for HardX'],
|
||||||
['da-tp', 0, 'Natasha Teen in LegalPorno SZ2164'],
|
['da-tp', 0, 'Natasha Teen in LegalPorno SZ2164'],
|
||||||
['deepthroat', 0, 'Chanel Grey in "Deepthroating Is Fun" for Throated'],
|
['deepthroat', 0, 'Chanel Grey in "Deepthroating Is Fun" for Throated'],
|
||||||
['double-anal', 7, 'Adriana Chechik in "DP Masters 6" for Jules Jordan'],
|
['double-anal', 7, 'Adriana Chechik in "DP Masters 6" for Jules Jordan'],
|
||||||
|
@ -26,10 +26,11 @@ const tagPosters = [
|
||||||
['double-vaginal', 'poster', 'Riley Reid in "Pizza That Ass" for Reid My Lips'],
|
['double-vaginal', 'poster', 'Riley Reid in "Pizza That Ass" for Reid My Lips'],
|
||||||
['dv-tp', 'poster', 'Juelz Ventura in "Gangbanged 5" for Elegant Angel'],
|
['dv-tp', 'poster', 'Juelz Ventura in "Gangbanged 5" for Elegant Angel'],
|
||||||
['ebony', 1, 'Ana Foxxx in "DP Me 4" for HardX'],
|
['ebony', 1, 'Ana Foxxx in "DP Me 4" for HardX'],
|
||||||
['facefucking', 1, 'Carrie for Young Throats'],
|
['facefucking', 2, 'Jynx Maze for Throated'],
|
||||||
['facial', 0, 'Brooklyn Gray in "All About Ass 4" for Evil Angel'],
|
['facial', 0, 'Brooklyn Gray in "All About Ass 4" for Evil Angel'],
|
||||||
|
['fake-boobs', 0, 'Marsha May in "Once You Go Black 7" for Jules Jordan'],
|
||||||
['family', 0, 'Teanna Trump in "A Family Appear: Part One" for Brazzers'],
|
['family', 0, 'Teanna Trump in "A Family Appear: Part One" for Brazzers'],
|
||||||
['gangbang', 'poster', 'Kristen Scott in "Interracial Gangbang!" for Jules Jordan'],
|
['gangbang', 4, 'Marley Brinx in "The Gangbang of Marley Brinx" for Jules Jordan'],
|
||||||
['gaping', 1, 'Vina Sky in "Vina Sky Does Anal" for HardX'],
|
['gaping', 1, 'Vina Sky in "Vina Sky Does Anal" for HardX'],
|
||||||
['interracial', 0, 'Kali Roses and Jax Slayher in "Kali Roses Gets An Interracial Creampie" for Jules Jordan'],
|
['interracial', 0, 'Kali Roses and Jax Slayher in "Kali Roses Gets An Interracial Creampie" for Jules Jordan'],
|
||||||
['latina', 'poster', 'Alexis Love for Penthouse'],
|
['latina', 'poster', 'Alexis Love for Penthouse'],
|
||||||
|
@ -93,14 +94,15 @@ const tagPhotos = [
|
||||||
['dv-tp', 0, 'Luna Rival in LegalPorno SZ1490'],
|
['dv-tp', 0, 'Luna Rival in LegalPorno SZ1490'],
|
||||||
['facial', 1, 'Ella Knox in "Mr Saltys Adult Emporium Adventure 2" for Aziani'],
|
['facial', 1, 'Ella Knox in "Mr Saltys Adult Emporium Adventure 2" for Aziani'],
|
||||||
['facial', 'poster', 'Jynx Maze'],
|
['facial', 'poster', 'Jynx Maze'],
|
||||||
['facefucking', 2, 'Jynx Maze for Throated'],
|
['facefucking', 1, 'Carrie for Young Throats'],
|
||||||
['interracial', 'poster', 'Khloe Kapri and Jax Slayher in "First Interracial Anal" for Hussie Pass'],
|
['gangbang', 'poster', 'Kristen Scott in "Interracial Gangbang!" for Jules Jordan'],
|
||||||
['latina', 0, 'Abby Lee Brazil for Bang Bros'],
|
|
||||||
['gangbang', 0, '"4 On 1 Gangbangs" for Doghouse Digital'],
|
['gangbang', 0, '"4 On 1 Gangbangs" for Doghouse Digital'],
|
||||||
['gangbang', 1, 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall for Puritan No. 10, 1984. This photo pushed the boundaries of pornography at the time, as depicting a woman \'fully occupied\' was unheard of.'],
|
['gangbang', 1, 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall for Puritan No. 10, 1984. This photo pushed the boundaries of pornography at the time, as depicting a woman \'fully occupied\' was unheard of.'],
|
||||||
['gangbang', 2, 'Riley Reid\'s double anal in "The Gangbang of Riley Reid" for Jules Jordan'],
|
['gangbang', 2, 'Riley Reid\'s double anal in "The Gangbang of Riley Reid" for Jules Jordan'],
|
||||||
['gaping', 'poster', 'Zoey Monroe in "Manuel DPs Them All 5" for Jules Jordan'],
|
['gaping', 'poster', 'Zoey Monroe in "Manuel DPs Them All 5" for Jules Jordan'],
|
||||||
['gaping', 2, 'Alex Grey in "DP Masters 5" for Jules Jordan'],
|
['gaping', 2, 'Alex Grey in "DP Masters 5" for Jules Jordan'],
|
||||||
|
['interracial', 'poster', 'Khloe Kapri and Jax Slayher in "First Interracial Anal" for Hussie Pass'],
|
||||||
|
['latina', 0, 'Abby Lee Brazil for Bang Bros'],
|
||||||
// ['mfm', 0, 'Vina Sky in "Jules Jordan\'s Three Ways" for Jules Jordan'],
|
// ['mfm', 0, 'Vina Sky in "Jules Jordan\'s Three Ways" for Jules Jordan'],
|
||||||
['mfm', 1, 'Jynx Maze in "Don\'t Make Me Beg 4" for Evil Angel'],
|
['mfm', 1, 'Jynx Maze in "Don\'t Make Me Beg 4" for Evil Angel'],
|
||||||
['orgy', 'poster', 'Zoey Mornoe (DP), Jillian Janson (sex), Frida Sante, Katerina Kay and Natasha Starr in "Orgy Masters 6" for Jules Jordan'],
|
['orgy', 'poster', 'Zoey Mornoe (DP), Jillian Janson (sex), Frida Sante, Katerina Kay and Natasha Starr in "Orgy Masters 6" for Jules Jordan'],
|
||||||
|
|
247
src/media.js
|
@ -2,12 +2,10 @@
|
||||||
|
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const Promise = require('bluebird');
|
const Promise = require('bluebird');
|
||||||
const util = require('util');
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const fsPromises = require('fs').promises;
|
const fsPromises = require('fs').promises;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const stream = require('stream');
|
const { PassThrough } = require('stream');
|
||||||
const { once } = require('events');
|
|
||||||
const nanoid = require('nanoid/non-secure');
|
const nanoid = require('nanoid/non-secure');
|
||||||
const mime = require('mime');
|
const mime = require('mime');
|
||||||
// const fileType = require('file-type');
|
// const fileType = require('file-type');
|
||||||
|
@ -20,9 +18,6 @@ const knex = require('./knex');
|
||||||
const http = require('./utils/http');
|
const http = require('./utils/http');
|
||||||
const { get } = require('./utils/qu');
|
const { get } = require('./utils/qu');
|
||||||
|
|
||||||
const PassThrough = stream.PassThrough;
|
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
|
||||||
|
|
||||||
function getMemoryUsage() {
|
function getMemoryUsage() {
|
||||||
return process.memoryUsage().rss / (10 ** 6);
|
return process.memoryUsage().rss / (10 ** 6);
|
||||||
}
|
}
|
||||||
|
@ -268,142 +263,154 @@ async function extractSource(baseSource, { existingExtractMediaByUrl }) {
|
||||||
throw new Error(`Could not extract source from ${baseSource.url}: ${res.status}`);
|
throw new Error(`Could not extract source from ${baseSource.url}: ${res.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storeFile(media) {
|
async function storeImageFile(media, hashDir, hashSubDir, filename, filedir, filepath) {
|
||||||
const hashDir = media.meta.hash.slice(0, 2);
|
const thumbdir = path.join(media.role, 'thumbs', hashDir, hashSubDir);
|
||||||
const hashSubDir = media.meta.hash.slice(2, 4);
|
const thumbpath = path.join(thumbdir, filename);
|
||||||
const hashFilename = media.meta.hash.slice(4);
|
|
||||||
|
|
||||||
const filename = media.quality
|
const image = sharp(media.file.path);
|
||||||
? `${hashFilename}_${media.quality}.${media.meta.extension}`
|
|
||||||
: `${hashFilename}.${media.meta.extension}`;
|
|
||||||
|
|
||||||
const filedir = path.join(media.role, hashDir, hashSubDir);
|
const [info, stat] = await Promise.all([
|
||||||
const filepath = path.join(filedir, filename);
|
image.metadata(),
|
||||||
|
fsPromises.stat(media.file.path),
|
||||||
|
]);
|
||||||
|
|
||||||
if (media.meta.type === 'image') {
|
await Promise.all([
|
||||||
const thumbdir = path.join(media.role, 'thumbs', hashDir, hashSubDir);
|
fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true }),
|
||||||
const thumbpath = path.join(thumbdir, filename);
|
fsPromises.mkdir(path.join(config.media.path, thumbdir), { recursive: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
// generate thumbnail
|
||||||
fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true }),
|
await sharp(media.file.path)
|
||||||
fsPromises.mkdir(path.join(config.media.path, thumbdir), { recursive: true }),
|
.resize({
|
||||||
]);
|
height: config.media.thumbnailSize,
|
||||||
|
withoutEnlargement: true,
|
||||||
|
})
|
||||||
|
.jpeg({ quality: config.media.thumbnailQuality })
|
||||||
|
.toFile(path.join(config.media.path, thumbpath));
|
||||||
|
|
||||||
await Promise.all([
|
if (media.meta.subtype === 'jpeg') {
|
||||||
fsPromises.rename(media.file.path, path.join(config.media.path, filepath)),
|
// move temp file to permanent location
|
||||||
fsPromises.rename(media.file.thumbnail, path.join(config.media.path, thumbpath)),
|
await fsPromises.rename(media.file.path, path.join(config.media.path, filepath));
|
||||||
]);
|
} else {
|
||||||
|
// convert to JPEG and write to permanent location
|
||||||
|
await sharp(media.file.path)
|
||||||
|
.jpeg()
|
||||||
|
.toFile(path.join(config.media.path, filepath));
|
||||||
|
|
||||||
return {
|
// remove temp file
|
||||||
...media,
|
await fsPromises.unlink(media.file.path);
|
||||||
file: {
|
|
||||||
path: filepath,
|
|
||||||
thumbnail: thumbpath,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true });
|
const memoryUsage = getMemoryUsage();
|
||||||
await fsPromises.rename(media.file.path, path.join(config.media.path, filepath));
|
peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage);
|
||||||
|
logger.silly(`Stored thumbnail and media from ${media.src}, memory usage ${memoryUsage.toFixed(2)} MB`);
|
||||||
logger.silly(`Stored media file at permanent location ${filepath}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...media,
|
...media,
|
||||||
file: {
|
file: {
|
||||||
path: filepath,
|
path: filepath,
|
||||||
|
thumbnail: thumbpath,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
...media.meta,
|
||||||
|
width: info.width,
|
||||||
|
height: info.height,
|
||||||
|
size: stat.size,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function storeFile(media) {
|
||||||
|
try {
|
||||||
|
const hashDir = media.meta.hash.slice(0, 2);
|
||||||
|
const hashSubDir = media.meta.hash.slice(2, 4);
|
||||||
|
const hashFilename = media.meta.hash.slice(4);
|
||||||
|
|
||||||
|
const filename = media.quality
|
||||||
|
? `${hashFilename}_${media.quality}.${media.meta.extension}`
|
||||||
|
: `${hashFilename}.${media.meta.extension}`;
|
||||||
|
|
||||||
|
const filedir = path.join(media.role, hashDir, hashSubDir);
|
||||||
|
const filepath = path.join(filedir, filename);
|
||||||
|
|
||||||
|
if (media.meta.type === 'image') {
|
||||||
|
return storeImageFile(media, hashDir, hashSubDir, filename, filedir, filepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [stat] = await Promise.all([
|
||||||
|
fsPromises.stat(media.file.path),
|
||||||
|
fsPromises.mkdir(path.join(config.media.path, filedir), { recursive: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await fsPromises.rename(media.file.path, path.join(config.media.path, filepath));
|
||||||
|
|
||||||
|
logger.silly(`Stored media file at permanent location ${filepath}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...media,
|
||||||
|
file: {
|
||||||
|
path: filepath,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
...media.meta,
|
||||||
|
size: stat.size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Failed to store ${media.src}: ${error.message}`);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchSource(source, baseMedia) {
|
async function fetchSource(source, baseMedia) {
|
||||||
logger.silly(`Fetching media from ${source.src}`);
|
logger.silly(`Fetching media from ${source.src}`);
|
||||||
// attempts
|
// attempts
|
||||||
|
|
||||||
async function attempt(attempts = 1) {
|
async function attempt(attempts = 1) {
|
||||||
try {
|
try {
|
||||||
|
const tempFilePath = path.join(config.media.path, 'temp', `${baseMedia.id}`);
|
||||||
|
|
||||||
|
const hasher = new blake2.Hash('blake2b');
|
||||||
|
hasher.setEncoding('hex');
|
||||||
|
|
||||||
|
const tempFileTarget = fs.createWriteStream(tempFilePath);
|
||||||
|
const hashStream = new PassThrough();
|
||||||
|
|
||||||
|
hashStream.on('data', chunk => hasher.write(chunk));
|
||||||
|
|
||||||
const res = await http.get(source.src, {
|
const res = await http.get(source.src, {
|
||||||
...(source.referer && { referer: source.referer }),
|
...(source.referer && { referer: source.referer }),
|
||||||
...(source.host && { host: source.host }),
|
...(source.host && { host: source.host }),
|
||||||
}, {
|
}, {
|
||||||
stream: true, // sources are fetched in parallel, don't gobble up memory
|
stream: true, // sources are fetched in parallel, don't gobble up memory
|
||||||
|
transforms: [hashStream],
|
||||||
|
destination: tempFileTarget,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
hasher.end();
|
||||||
|
|
||||||
|
const hash = hasher.read();
|
||||||
|
const { pathname } = new URL(source.src);
|
||||||
|
const mimetype = res.headers['content-type'] || mime.getType(pathname);
|
||||||
|
const [type, subtype] = mimetype.split('/');
|
||||||
|
const extension = mime.getExtension(mimetype);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Response ${res.status} not OK`);
|
throw new Error(`Response ${res.status} not OK`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pathname } = new URL(source.src);
|
|
||||||
const mimetype = res.headers['content-type'] || mime.getType(pathname);
|
|
||||||
const extension = mime.getExtension(mimetype);
|
|
||||||
const type = mimetype?.split('/')[0] || 'image';
|
|
||||||
|
|
||||||
const hasher = new blake2.Hash('blake2b');
|
|
||||||
hasher.setEncoding('hex');
|
|
||||||
|
|
||||||
const hashStream = new PassThrough();
|
|
||||||
const metaStream = type === 'image'
|
|
||||||
? sharp()
|
|
||||||
: new PassThrough();
|
|
||||||
|
|
||||||
const tempFilePath = path.join(config.media.path, 'temp', `${baseMedia.id}.${extension}`);
|
|
||||||
const tempThumbPath = path.join(config.media.path, 'temp', `${baseMedia.id}_thumb.${extension}`);
|
|
||||||
|
|
||||||
const tempFileTarget = fs.createWriteStream(tempFilePath);
|
|
||||||
const tempThumbTarget = fs.createWriteStream(tempThumbPath);
|
|
||||||
|
|
||||||
hashStream.on('data', chunk => hasher.write(chunk));
|
|
||||||
|
|
||||||
if (type === 'image') {
|
|
||||||
// generate thumbnail
|
|
||||||
metaStream
|
|
||||||
.clone()
|
|
||||||
.resize({
|
|
||||||
height: config.media.thumbnailSize,
|
|
||||||
withoutEnlargement: true,
|
|
||||||
})
|
|
||||||
.jpeg({ quality: config.media.thumbnailQuality })
|
|
||||||
.pipe(tempThumbTarget)
|
|
||||||
.on('error', error => logger.error(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
// pipeline destroys streams, so attach info event first
|
|
||||||
const infoPromise = type === 'image' ? once(metaStream, 'info') : Promise.resolve([{}]);
|
|
||||||
const metaPromise = type === 'image' ? metaStream.stats() : Promise.resolve();
|
|
||||||
|
|
||||||
await pipeline(
|
|
||||||
res.originalRes,
|
|
||||||
metaStream,
|
|
||||||
hashStream,
|
|
||||||
tempFileTarget,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [stats, info] = await Promise.all([metaPromise, infoPromise]);
|
|
||||||
|
|
||||||
hasher.end();
|
|
||||||
|
|
||||||
const hash = hasher.read();
|
|
||||||
const [{ size, width, height }] = info;
|
|
||||||
|
|
||||||
peakMemoryUsage = Math.max(getMemoryUsage(), peakMemoryUsage);
|
|
||||||
|
|
||||||
logger.silly(`Fetched media from ${source.src}, memory usage ${peakMemoryUsage.toFixed(2)} MB`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...source,
|
...source,
|
||||||
file: {
|
file: {
|
||||||
path: tempFilePath,
|
path: tempFilePath,
|
||||||
thumbnail: tempThumbPath,
|
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
|
hash,
|
||||||
mimetype,
|
mimetype,
|
||||||
extension,
|
extension,
|
||||||
type,
|
type,
|
||||||
hash,
|
subtype,
|
||||||
entropy: stats?.entropy,
|
|
||||||
size,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -412,20 +419,14 @@ async function fetchSource(source, baseMedia) {
|
||||||
if (attempts < 3) {
|
if (attempts < 3) {
|
||||||
await Promise.delay(1000);
|
await Promise.delay(1000);
|
||||||
|
|
||||||
return Promise.race([
|
return attempt(attempts + 1);
|
||||||
attempt(attempts + 1),
|
|
||||||
Promise.delay(120 * 1000).then(() => { throw new Error(`Media fetch attempt ${attempts}/3 timed out, aborting ${source.src}`); }),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to fetch ${source.src}: ${error.message}`);
|
throw new Error(`Failed to fetch ${source.src}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.race([
|
return attempt(1);
|
||||||
attempt(1),
|
|
||||||
Promise.delay(120 * 1000).then(() => { throw new Error(`Media fetch timed out, aborting ${source.src}`); }),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function trySource(baseSource, existingMedias, baseMedia) {
|
async function trySource(baseSource, existingMedias, baseMedia) {
|
||||||
|
@ -467,25 +468,14 @@ async function fetchMedia(baseMedia, existingMedias) {
|
||||||
Promise.reject(new Error()),
|
Promise.reject(new Error()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (source.entry) {
|
const memoryUsage = getMemoryUsage();
|
||||||
// don't save media, already in database
|
peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage);
|
||||||
return {
|
logger.silly(`Fetched media from ${source.src}, memory usage ${memoryUsage.toFixed(2)} MB`);
|
||||||
...baseMedia,
|
|
||||||
...source,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return storeFile({
|
return {
|
||||||
...baseMedia,
|
...baseMedia,
|
||||||
...source,
|
...source,
|
||||||
});
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
return saveMedia({
|
|
||||||
...baseMedia,
|
|
||||||
...source,
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(error.message);
|
logger.warn(error.message);
|
||||||
|
|
||||||
|
@ -528,14 +518,19 @@ async function storeMedias(baseMedias) {
|
||||||
|
|
||||||
const [existingSourceMediaByUrl, existingExtractMediaByUrl] = await findSourceDuplicates(baseMedias);
|
const [existingSourceMediaByUrl, existingExtractMediaByUrl] = await findSourceDuplicates(baseMedias);
|
||||||
|
|
||||||
const savedMedias = await Promise.map(
|
const fetchedMedias = await Promise.map(
|
||||||
baseMedias,
|
baseMedias,
|
||||||
async baseMedia => fetchMedia(baseMedia, { existingSourceMediaByUrl, existingExtractMediaByUrl }),
|
async baseMedia => fetchMedia(baseMedia, { existingSourceMediaByUrl, existingExtractMediaByUrl }),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [uniqueHashMedias, existingHashMedias] = await findHashDuplicates(savedMedias);
|
const [uniqueHashMedias, existingHashMedias] = await findHashDuplicates(fetchedMedias);
|
||||||
|
|
||||||
const newMediaWithEntries = uniqueHashMedias.map((media, index) => curateMediaEntry(media, index));
|
const savedMedias = await Promise.map(
|
||||||
|
uniqueHashMedias,
|
||||||
|
async baseMedia => storeFile(baseMedia),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newMediaWithEntries = savedMedias.map((media, index) => curateMediaEntry(media, index));
|
||||||
const newMediaEntries = newMediaWithEntries.filter(media => media.newEntry).map(media => media.entry);
|
const newMediaEntries = newMediaWithEntries.filter(media => media.newEntry).map(media => media.entry);
|
||||||
|
|
||||||
await knex('media').insert(newMediaEntries);
|
await knex('media').insert(newMediaEntries);
|
||||||
|
|
|
@ -53,7 +53,7 @@ async function filterUniqueReleases(latestReleases, accReleases) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function needNextPage(uniqueReleases, pageAccReleases) {
|
function needNextPage(uniqueReleases, pageAccReleases) {
|
||||||
if (uniqueReleases === 0) {
|
if (uniqueReleases.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const util = require('util');
|
||||||
|
const stream = require('stream');
|
||||||
const config = require('config');
|
const config = require('config');
|
||||||
const tunnel = require('tunnel');
|
const tunnel = require('tunnel');
|
||||||
const bhttp = require('bhttp');
|
const bhttp = require('bhttp');
|
||||||
const taskQueue = require('promise-task-queue');
|
const taskQueue = require('promise-task-queue');
|
||||||
|
|
||||||
|
const pipeline = util.promisify(stream.pipeline);
|
||||||
const logger = require('../logger')(__filename);
|
const logger = require('../logger')(__filename);
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
|
@ -68,6 +71,10 @@ queue.define('http', async ({
|
||||||
? await bhttp[method.toLowerCase()](url, body, reqOptions)
|
? await bhttp[method.toLowerCase()](url, body, reqOptions)
|
||||||
: await bhttp[method.toLowerCase()](url, reqOptions);
|
: await bhttp[method.toLowerCase()](url, reqOptions);
|
||||||
|
|
||||||
|
if (options.stream && options.destination) {
|
||||||
|
await pipeline(res, ...(options.transforms || []), options.destination);
|
||||||
|
}
|
||||||
|
|
||||||
const html = Buffer.isBuffer(res.body) ? res.body.toString() : null;
|
const html = Buffer.isBuffer(res.body) ? res.body.toString() : null;
|
||||||
const json = Buffer.isBuffer(res.body) ? null : res.body;
|
const json = Buffer.isBuffer(res.body) ? null : res.body;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const config = require('config');
|
||||||
|
const fs = require('fs');
|
||||||
|
const fsPromises = require('fs').promises;
|
||||||
|
const Promise = require('bluebird');
|
||||||
|
const blake2 = require('blake2');
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const nanoid = require('nanoid');
|
||||||
|
const { PassThrough } = require('stream');
|
||||||
|
|
||||||
|
const http = require('./http');
|
||||||
|
|
||||||
|
function getMemoryUsage() {
|
||||||
|
return process.memoryUsage().rss / (10 ** 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
let peakMemoryUsage = getMemoryUsage();
|
||||||
|
|
||||||
|
async function fetchSource(link) {
|
||||||
|
const id = nanoid();
|
||||||
|
|
||||||
|
const hasher = new blake2.Hash('blake2b');
|
||||||
|
hasher.setEncoding('hex');
|
||||||
|
|
||||||
|
const tempFilePath = `/home/niels/Pictures/thumbs/temp/${id}.jpeg`;
|
||||||
|
const tempFileStream = fs.createWriteStream(tempFilePath);
|
||||||
|
const hashStream = new PassThrough();
|
||||||
|
|
||||||
|
hashStream.on('data', chunk => hasher.write(chunk));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await http.get(link, null, {
|
||||||
|
stream: true,
|
||||||
|
transforms: [hashStream],
|
||||||
|
destination: tempFileStream,
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(res.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher.end();
|
||||||
|
const hash = hasher.read();
|
||||||
|
|
||||||
|
const memoryUsage = getMemoryUsage();
|
||||||
|
peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage);
|
||||||
|
|
||||||
|
console.log(`Stored ${tempFilePath}, memory usage: ${memoryUsage.toFixed(2)} MB`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
path: tempFilePath,
|
||||||
|
hash,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await fsPromises.unlink(tempFilePath);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const linksFile = await fsPromises.readFile('/home/niels/Pictures/photos', 'utf8');
|
||||||
|
const links = linksFile.split('\n').filter(Boolean);
|
||||||
|
|
||||||
|
await fsPromises.mkdir('/home/niels/Pictures/thumbs/temp', { recursive: true });
|
||||||
|
|
||||||
|
console.time('thumbs');
|
||||||
|
|
||||||
|
const files = await Promise.map(links, async (link) => {
|
||||||
|
try {
|
||||||
|
return await fetchSource(link);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Failed to fetch ${link}: ${error.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.map(files.filter(Boolean), async (file) => {
|
||||||
|
const image = sharp(file.path).jpeg();
|
||||||
|
|
||||||
|
const [{ width, height }, { size }] = await Promise.all([
|
||||||
|
image.metadata(),
|
||||||
|
fsPromises.stat(file.path),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
image
|
||||||
|
.toFile(`/home/niels/Pictures/thumbs/${file.hash}.jpeg`),
|
||||||
|
image
|
||||||
|
.resize({
|
||||||
|
height: config.media.thumbnailSize,
|
||||||
|
withoutEnlargement: true,
|
||||||
|
})
|
||||||
|
.toFile(`/home/niels/Pictures/thumbs/${file.hash}_thumb.jpeg`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const memoryUsage = getMemoryUsage();
|
||||||
|
peakMemoryUsage = Math.max(memoryUsage, peakMemoryUsage);
|
||||||
|
|
||||||
|
console.log(`Resized ${file.id} (${width}, ${height}, ${size}), memory usage: ${memoryUsage.toFixed(2)} MB`);
|
||||||
|
}, { concurrency: 10 });
|
||||||
|
|
||||||
|
console.log(`Peak memory usage: ${peakMemoryUsage.toFixed(2)} MB`);
|
||||||
|
console.timeEnd('thumbs');
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|