Skip to content
Snippets Groups Projects
Verified Commit c3dcdcab authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Add support for localized images

parent 27b23486
No related branches found
No related tags found
No related merge requests found
import fetch from 'node-fetch'; import fetch from 'node-fetch';
class FanartApi { export default class FanartApi {
apiKey; apiKey;
baseUrl; baseUrl;
...@@ -40,10 +40,8 @@ class FanartApi { ...@@ -40,10 +40,8 @@ class FanartApi {
return JSON.parse(body); return JSON.parse(body);
} }
}).catch(err => { }).catch(err => {
console.error(err); console.error(`Requesting data from Fanart.tv failed: ${url}`, err);
return null; return null;
}); });
} }
} }
export default FanartApi;
import fetch from 'node-fetch'; import fetch from 'node-fetch';
class TmdbApi { export default class TmdbApi {
apiKey; apiKey;
baseUrl; baseUrl;
...@@ -43,10 +43,8 @@ class TmdbApi { ...@@ -43,10 +43,8 @@ class TmdbApi {
} }
return JSON.parse(body); return JSON.parse(body);
}).catch(err => { }).catch(err => {
console.error(err); console.error(`Requesting data from TMDB failed: ${url}`, err);
return null; return null;
}); });
} }
} }
export default TmdbApi;
...@@ -254,46 +254,72 @@ class MetadataLoader { ...@@ -254,46 +254,72 @@ class MetadataLoader {
} }
chooseImages(originalLanguage, tmdbImages, fanartImages) { chooseImages(originalLanguage, tmdbImages, fanartImages) {
function findbest(list) { function calculateConfidenceAndQuality(element) {
let sorted = list.sort((a, b) => b.likes - a.likes);
return sorted.find(el => el.language === originalLanguage) || sorted[0];
}
function calculateConfidenceAndQuality(containsText, originalLanguage) {
return (element) => {
return { return {
confidence: ranking_confidence(element.vote_average, element.vote_count), confidence: ranking_confidence(element.vote_average, element.vote_count),
lang_quality: containsText ? (
element.iso_639_1 === originalLanguage ? 1.5 :
element.iso_639_1 === null ? 1 :
0
) : (
element.iso_639_1 === null ? 1.5 :
element.iso_639_1 === originalLanguage ? 1 :
0
),
megapixels: (element.height * element.width) / 1000000, megapixels: (element.height * element.width) / 1000000,
...element ...element
} }
} }
}
function imageComparator(a, b) { function imageComparator(a, b) {
function rank(element) { function rank(element) {
return element.lang_quality * (0.01 + element.confidence) * Math.sqrt(element.megapixels) return (0.01 + element.confidence) * Math.sqrt(element.megapixels)
} }
return rank(b) - rank(a) return rank(b) - rank(a)
} }
function transformFanartImage(image) {
if (!image) {
return null;
}
const modifiedUrl = new URL(image.url);
modifiedUrl.protocol = "http";
return {
...image,
url: modifiedUrl.href,
}
}
const languages = Array.from(new Set([
...Object.values(tmdbImages)
.flatMap(it => it)
.map(it => it.iso_639_1),
...(fanartImages && fanartImages.hdmovielogo || [])
.map(it => it.language)
]));
const sortedLogos = (fanartImages && fanartImages.hdmovielogo || [])
.sort((a, b) => b.likes - a.likes);
const sortedPosters = (tmdbImages.posters || [])
.map(calculateConfidenceAndQuality)
.sort(imageComparator);
const sortedBackdrops = (tmdbImages.backdrops || [])
.map(calculateConfidenceAndQuality)
.sort(imageComparator);
const sortedStills = (tmdbImages.stills || [])
.map(calculateConfidenceAndQuality)
.sort(imageComparator);
return { return {
logo: fanartImages && fanartImages.hdmovielogo ? findbest(fanartImages.hdmovielogo) : null, logo: languages
poster: tmdbImages.posters && tmdbImages.posters.map(calculateConfidenceAndQuality(true, originalLanguage)) .map(lang => sortedLogos.find(it => it.lang === lang))
.sort(imageComparator)[0], .filter(it => it !== undefined)
backdrop: tmdbImages.backdrops && tmdbImages.backdrops.map(calculateConfidenceAndQuality(false, originalLanguage)) .map(transformFanartImage),
.sort(imageComparator)[0], poster: languages
still: tmdbImages.stills && tmdbImages.stills.map(calculateConfidenceAndQuality(false, originalLanguage)) .map(lang => sortedPosters.find(it => it.iso_639_1 === lang))
.sort(imageComparator)[0], .filter(it => it !== undefined),
backdrop: languages
.map(lang => sortedBackdrops.find(it => it.iso_639_1 === lang))
.filter(it => it !== undefined),
still: languages
.map(lang => sortedStills.find(it => it.iso_639_1 === lang))
.filter(it => it !== undefined),
} }
} }
...@@ -344,31 +370,44 @@ class MetadataLoader { ...@@ -344,31 +370,44 @@ class MetadataLoader {
} }
async processImages(basePath, filePath, images) { async processImages(basePath, filePath, images) {
const imageData = !images ? [] : [ const imageData = [
!images.logo ? null : !images.logo.url ? null : { ...images.logo.map(it => {
return {
kind: "logo", kind: "logo",
url: images.logo.url, language: it.lang || null,
src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `logo${path.extname(images.logo.url)}`))) url: it.url,
}, src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `logo.${it.lang || null}${path.extname(it.url)}`)))
!images.poster ? null : !images.poster.file_path ? null : { }
}),
...images.poster.map(it => {
return {
kind: "poster", kind: "poster",
url: this.tmdb.getImageUrl(images.poster.file_path), language: it.iso_639_1 || null,
src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `poster${path.extname(images.poster.file_path)}`))) url: this.tmdb.getImageUrl(it.file_path),
}, src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `poster.${it.iso_639_1 || null}${path.extname(it.file_path)}`)))
!images.backdrop ? null : !images.backdrop.file_path ? null : { }
}),
...images.backdrop.map(it => {
return {
kind: "backdrop", kind: "backdrop",
url: this.tmdb.getImageUrl(images.backdrop.file_path), language: it.iso_639_1 || null,
src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `backdrop${path.extname(images.backdrop.file_path)}`))) url: this.tmdb.getImageUrl(it.file_path),
}, src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `backdrop.${it.iso_639_1 || null}${path.extname(it.file_path)}`)))
!images.still ? null : !images.still.file_path ? null : { }
}),
...images.still.map(it => {
return {
kind: "still", kind: "still",
url: this.tmdb.getImageUrl(images.still.file_path), language: it.iso_639_1 || null,
src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `still${path.extname(images.still.file_path)}`))) url: this.tmdb.getImageUrl(it.file_path),
src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `still.${it.iso_639_1 || null}${path.extname(it.file_path)}`)))
} }
].filter(el => el !== null); })
]
return await Promise.all(imageData.map(async img => { return await Promise.all(imageData.map(async img => {
const headers = await downloadFile(img.url, path.join(filePath, "metadata", img.kind + path.extname(img.url))); console.log("Downloading image " + img.kind + " (" + img.language+") for " + filePath);
const headers = await downloadFile(img.url, path.join(filePath, "metadata", img.kind + "." + img.language + path.extname(img.url)));
return { return {
mime: headers["content-type"], mime: headers["content-type"],
...img, ...img,
...@@ -385,6 +424,7 @@ class MetadataLoader { ...@@ -385,6 +424,7 @@ class MetadataLoader {
for (let image of images) { for (let image of images) {
const titleImage = await TitleImage.build({ const titleImage = await TitleImage.build({
kind: image.kind, kind: image.kind,
language: image.language,
mime: image.mime, mime: image.mime,
src: image.src, src: image.src,
}) })
......
...@@ -5,6 +5,7 @@ async function processContent(basePath, fileManager, loader) { ...@@ -5,6 +5,7 @@ async function processContent(basePath, fileManager, loader) {
async function processMovie(filePath) { async function processMovie(filePath) {
const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups; const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups;
try {
const ids = (await fileManager.findIds(filePath)) || (await loader.identifyMovie(name, year)); const ids = (await fileManager.findIds(filePath)) || (await loader.identifyMovie(name, year));
if (!ids) { if (!ids) {
console.error(`Could not identify movie ${name} (${year}) at ${filePath}`) console.error(`Could not identify movie ${name} (${year}) at ${filePath}`)
...@@ -22,6 +23,9 @@ async function processContent(basePath, fileManager, loader) { ...@@ -22,6 +23,9 @@ async function processContent(basePath, fileManager, loader) {
fsPromises.wr fsPromises.wr
await fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)); await fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2));
console.info(`Finished movie ${name} (${year})`); console.info(`Finished movie ${name} (${year})`);
} catch (e) {
console.error(`Processing movie ${name} (${year}) failed`, e);
}
} }
async function processEpisode(showIds, episodeIdentifier, filePath) { async function processEpisode(showIds, episodeIdentifier, filePath) {
...@@ -36,28 +40,32 @@ async function processContent(basePath, fileManager, loader) { ...@@ -36,28 +40,32 @@ async function processContent(basePath, fileManager, loader) {
async function processShow(filePath) { async function processShow(filePath) {
const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups; const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups;
try {
const ids = (await fileManager.findIds(filePath)) || (await loader.identifyShow(name, year)); const ids = (await fileManager.findIds(filePath)) || (await loader.identifyShow(name, year));
if (!ids) { if (!ids) {
console.error(`Could not identify show ${name} (${year}) ${ids.imdb} at ${filePath}`) console.error(`Could not identify show ${name} (${year}) at ${filePath}`)
return; return;
} }
console.info(`Processing show ${name} (${year}) ${ids.imdb}`); console.info(`Processing show ${name} (${year})`);
const episodes = await fileManager.listEpisodes(filePath); const episodes = await fileManager.listEpisodes(filePath);
console.info(`Loading metadata ${name} (${year}) ${ids.imdb}`); console.info(`Loading metadata ${name} (${year})`);
const {title, images} = await loader.loadMetadata(ids); const {title, images} = await loader.loadMetadata(ids);
console.info(`Processing images ${name} (${year}) ${ids.imdb}`); console.info(`Processing images ${name} (${year})`);
const imageData = await loader.processImages(basePath, filePath, images); const imageData = await loader.processImages(basePath, filePath, images);
console.info(`Processing image metadata ${name} (${year}) ${ids.imdb}`); console.info(`Processing image metadata ${name} (${year})`);
await loader.processImageMetadata(title, imageData); await loader.processImageMetadata(title, imageData);
console.info(`Processing episode data ${name} (${year}) ${ids.imdb}`); console.info(`Processing episode data ${name} (${year})`);
await Promise.all([ await Promise.all([
...episodes.map(async ({episodeIdentifier, filePath}) => await processEpisode(ids, episodeIdentifier, filePath).catch(err => { ...episodes.map(async ({episodeIdentifier, filePath}) => await processEpisode(ids, episodeIdentifier, filePath).catch(err => {
console.error(`Error processing episode ${JSON.stringify(episodeIdentifier)} of show ${JSON.stringify(ids)}: `, err); console.error(`Error processing episode ${JSON.stringify(episodeIdentifier)} of show ${JSON.stringify(ids)}: `, err);
})), })),
fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)), fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)),
]); ]);
console.log(`Finished show ${name} (${year}) ${ids.imdb}`); console.log(`Finished show ${name} (${year})`);
} catch (e) {
console.error(`Processing show ${name} (${year}) failed`, e);
}
} }
console.info("Processing content"); console.info("Processing content");
......
...@@ -260,6 +260,7 @@ class Backend { ...@@ -260,6 +260,7 @@ class Backend {
primaryKey: true primaryKey: true
}, },
kind: sequelize.DataTypes.TEXT, kind: sequelize.DataTypes.TEXT,
language: sequelize.DataTypes.TEXT,
mime: { mime: {
type: sequelize.DataTypes.TEXT, type: sequelize.DataTypes.TEXT,
allowNull: false, allowNull: false,
......
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import http from 'http';
import https from 'https'; import https from 'https';
function downloadFile(url, filePath) { export default function downloadFile(url, filePath) {
return new Promise(((resolve, reject) => { return new Promise(((resolve, reject) => {
fs.mkdirSync(path.dirname(filePath), {recursive: true}) fs.mkdirSync(path.dirname(filePath), {recursive: true})
const file = fs.createWriteStream(filePath); const file = fs.createWriteStream(filePath);
https.get(url, function (response) { const backend = url.startsWith("https") ? https : http;
backend.get(url, function (response) {
response.pipe(file); response.pipe(file);
file.on('close', function () { file.on('close', function () {
resolve(response.headers); resolve(response.headers);
...@@ -15,11 +17,10 @@ function downloadFile(url, filePath) { ...@@ -15,11 +17,10 @@ function downloadFile(url, filePath) {
file.close() file.close()
}); });
}).on('error', function (err) { }).on('error', function (err) {
console.error(`Downloading file failed: ${url}`, err);
fs.unlink(filePath, function () { fs.unlink(filePath, function () {
reject(err); reject(err);
}); });
}); });
})); }));
} }
export default downloadFile;
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment