'use strict'; const config = require('config'); const Promise = require('bluebird'); const path = require('path'); const fs = require('fs-extra'); const bhttp = require('bhttp'); const mime = require('mime'); const sharp = require('sharp'); const blake2 = require('blake2'); const knex = require('./knex'); function getHash(buffer) { const hash = blake2.createHash('blake2b', { digestLength: 24 }); hash.update(buffer); return hash.digest('hex'); } async function getThumbnail(buffer) { return sharp(buffer) .resize({ height: config.media.thumbnailSize, withoutEnlargement: true, }) .toBuffer(); } async function createReleaseMediaDirectory(release, releaseId) { if (release.poster || (release.photos && release.photos.length) || release.trailer) { await fs.mkdir( path.join(config.media.path, 'releases', release.site.network.slug, release.site.slug, releaseId.toString()), { recursive: true }, ); } } async function createActorMediaDirectory(profile, actor) { if (profile.avatars && profile.avatars.length) { await fs.mkdir( path.join(config.media.path, 'actors', actor.slug), { recursive: true }, ); } } async function storePoster(release, releaseId) { if (!release.poster) { console.warn(`No poster available for (${release.site.name}, ${releaseId}}) "${release.title}"`); return; } console.log(`Storing poster for (${release.site.name}, ${releaseId}) "${release.title}"`); const res = await bhttp.get(release.poster); if (res.statusCode === 200) { const thumbnail = await getThumbnail(res.body); const { pathname } = new URL(release.poster); const mimetype = res.headers['content-type'] || mime.getType(pathname) || 'image/jpeg'; const extension = mime.getExtension(mimetype); const filepath = path.join('releases', release.site.network.slug, release.site.slug, releaseId.toString(), `poster.${extension}`); const thumbpath = path.join('releases', release.site.network.slug, release.site.slug, releaseId.toString(), `poster_thumb.${extension}`); const hash = getHash(res.body); await Promise.all([ fs.writeFile(path.join(config.media.path, filepath), res.body), fs.writeFile(path.join(config.media.path, thumbpath), thumbnail), ]); await knex('media').insert({ path: filepath, thumbnail: thumbpath, mime: mimetype, hash, source: release.poster, domain: 'releases', target_id: releaseId, role: 'poster', }); return; } console.warn(`Failed to store poster for (${release.site.name}, ${releaseId}) "${release.title}": ${res.statusCode}`); } async function storePhotos(release, releaseId) { if (!release.photos || release.photos.length === 0) { console.warn(`No photos available for (${release.site.name}, ${releaseId}}) "${release.title}"`); return; } console.log(`Storing ${release.photos.length} photos for (${release.site.name}, ${releaseId}) "${release.title}"`); const files = await Promise.map(release.photos, async (photoUrl, index) => { const { pathname } = new URL(photoUrl); const mimetype = mime.getType(pathname); try { const res = await bhttp.get(photoUrl); if (res.statusCode === 200) { const thumbnail = await getThumbnail(res.body); const extension = mime.getExtension(mimetype); const filepath = path.join('releases', release.site.network.slug, release.site.slug, releaseId.toString(), `${index + 1}.${extension}`); const thumbpath = path.join('releases', release.site.network.slug, release.site.slug, releaseId.toString(), `${index + 1}_thumb.${extension}`); const hash = getHash(res.body); await Promise.all([ fs.writeFile(path.join(config.media.path, filepath), res.body), fs.writeFile(path.join(config.media.path, thumbpath), thumbnail), ]); return { filepath, thumbpath, mimetype, hash, source: photoUrl, }; } throw new Error(`Response ${res.statusCode} not OK`); } catch (error) { console.warn(`Failed to store photo ${index + 1} for "${release.title}" (${photoUrl}, ${release.url}, ${release.site.name}, ${releaseId}): ${error}`); return null; } }, { concurrency: 10, }); await knex('media') .insert(files.filter(file => file) .map((file, index) => ({ path: file.filepath, thumbnail: file.thumbpath, mime: file.mimetype, hash: file.hash, source: file.source, index, domain: 'releases', target_id: releaseId, role: 'photo', }))); } async function storeTrailer(release, releaseId) { if (!release.trailer || !release.trailer.src) { console.warn(`No trailer available for (${release.site.name}, ${releaseId}}) "${release.title}"`); return; } console.log(`Storing trailer for (${release.site.name}, ${releaseId}) "${release.title}"`); const { pathname } = new URL(release.trailer.src); const mimetype = release.trailer.type || mime.getType(pathname); const res = await bhttp.get(release.trailer.src); const filepath = path.join('releases', release.site.network.slug, release.site.slug, releaseId.toString(), `trailer${release.trailer.quality ? `_${release.trailer.quality}` : ''}.${mime.getExtension(mimetype)}`); await Promise.all([ fs.writeFile(path.join(config.media.path, filepath), res.body), knex('media').insert({ path: filepath, mime: mimetype, source: release.trailer.src, domain: 'releases', target_id: releaseId, role: 'trailer', quality: release.trailer.quality || null, }), ]); } async function storeAvatars(profile, actor) { if (!profile.avatars || profile.avatars.length === 0) { console.warn(`No avatars available for '${profile.name}'`); return; } console.log(`Storing ${profile.avatars.length} avatars for '${profile.name}'`); const files = await Promise.map(profile.avatars, async (avatarUrl, index) => { try { const { pathname } = new URL(avatarUrl); const mimetype = mime.getType(pathname); const res = await bhttp.get(avatarUrl); if (res.statusCode === 200) { const thumbnail = await getThumbnail(res.body); const extension = mime.getExtension(mimetype); const timestamp = new Date().getTime(); const filepath = path.join('actors', actor.slug, `${timestamp + index}.${extension}`); const thumbpath = path.join('actors', actor.slug, `${timestamp + index}_thumb.${extension}`); const hash = getHash(res.body); await Promise.all([ fs.writeFile(path.join(config.media.path, filepath), res.body), fs.writeFile(path.join(config.media.path, thumbpath), thumbnail), ]); return { filepath, thumbpath, mimetype, hash, source: avatarUrl, }; } throw new Error(`Response ${res.statusCode} not OK`); } catch (error) { console.warn(`Failed to store avatar ${index + 1} for '${profile.name}': ${avatarUrl}`); return null; } }, { concurrency: 10, }); const avatars = files.filter(file => file); const existingAvatars = await knex('media') .whereIn('hash', avatars.map(file => file.hash)); const newAvatars = avatars.filter(file => !existingAvatars.some(avatar => file.hash === avatar.hash)); const hasAvatar = existingAvatars.some(avatar => avatar.role === 'avatar'); await knex('media') .insert(newAvatars.map((file, index) => ({ path: file.filepath, thumbnail: file.thumbpath, mime: file.mimetype, hash: file.hash, source: file.source, index, domain: 'actors', target_id: actor.id, role: index === 0 && !hasAvatar ? 'avatar' : 'photo', }))); } module.exports = { createActorMediaDirectory, createReleaseMediaDirectory, storeAvatars, storePoster, storePhotos, storeTrailer, };