diff --git a/src/api/fanart_api.js b/src/api/fanart_api.js index 0da4b5c8ff2bd626d4254177889a350a67e7f1fa..5fa6727a96b12b4a3dd214c41e6248c4676f2078 100644 --- a/src/api/fanart_api.js +++ b/src/api/fanart_api.js @@ -1,6 +1,6 @@ import fetch from 'node-fetch'; -class FanartApi { +export default class FanartApi { apiKey; baseUrl; @@ -40,10 +40,8 @@ class FanartApi { return JSON.parse(body); } }).catch(err => { - console.error(err); + console.error(`Requesting data from Fanart.tv failed: ${url}`, err); return null; }); } } - -export default FanartApi; diff --git a/src/api/tmdb_api.js b/src/api/tmdb_api.js index a93236a6f766b3af9427d3f34368bd99c21ab29a..d17586ebf908f15167c12e4b917393f66274feba 100644 --- a/src/api/tmdb_api.js +++ b/src/api/tmdb_api.js @@ -1,6 +1,6 @@ import fetch from 'node-fetch'; -class TmdbApi { +export default class TmdbApi { apiKey; baseUrl; @@ -43,10 +43,8 @@ class TmdbApi { } return JSON.parse(body); }).catch(err => { - console.error(err); + console.error(`Requesting data from TMDB failed: ${url}`, err); return null; }); } } - -export default TmdbApi; diff --git a/src/metadata_loader.js b/src/metadata_loader.js index d7e1098527563709d18126fd8dc8dd66d2ac78e9..4da6cdfdea1da5efe61a063995c3e6137a605827 100644 --- a/src/metadata_loader.js +++ b/src/metadata_loader.js @@ -254,46 +254,72 @@ class MetadataLoader { } chooseImages(originalLanguage, tmdbImages, fanartImages) { - function findbest(list) { - 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 { - 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, - ...element - } + function calculateConfidenceAndQuality(element) { + return { + confidence: ranking_confidence(element.vote_average, element.vote_count), + megapixels: (element.height * element.width) / 1000000, + ...element } } function imageComparator(a, b) { 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) } + 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 { - logo: fanartImages && fanartImages.hdmovielogo ? findbest(fanartImages.hdmovielogo) : null, - poster: tmdbImages.posters && tmdbImages.posters.map(calculateConfidenceAndQuality(true, originalLanguage)) - .sort(imageComparator)[0], - backdrop: tmdbImages.backdrops && tmdbImages.backdrops.map(calculateConfidenceAndQuality(false, originalLanguage)) - .sort(imageComparator)[0], - still: tmdbImages.stills && tmdbImages.stills.map(calculateConfidenceAndQuality(false, originalLanguage)) - .sort(imageComparator)[0], + logo: languages + .map(lang => sortedLogos.find(it => it.lang === lang)) + .filter(it => it !== undefined) + .map(transformFanartImage), + poster: languages + .map(lang => sortedPosters.find(it => it.iso_639_1 === lang)) + .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 { } async processImages(basePath, filePath, images) { - const imageData = !images ? [] : [ - !images.logo ? null : !images.logo.url ? null : { - kind: "logo", - url: images.logo.url, - src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `logo${path.extname(images.logo.url)}`))) - }, - !images.poster ? null : !images.poster.file_path ? null : { - kind: "poster", - url: this.tmdb.getImageUrl(images.poster.file_path), - src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `poster${path.extname(images.poster.file_path)}`))) - }, - !images.backdrop ? null : !images.backdrop.file_path ? null : { - kind: "backdrop", - url: this.tmdb.getImageUrl(images.backdrop.file_path), - src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `backdrop${path.extname(images.backdrop.file_path)}`))) - }, - !images.still ? null : !images.still.file_path ? null : { - kind: "still", - url: this.tmdb.getImageUrl(images.still.file_path), - src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `still${path.extname(images.still.file_path)}`))) - } - ].filter(el => el !== null); + const imageData = [ + ...images.logo.map(it => { + return { + kind: "logo", + language: it.lang || null, + url: it.url, + src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `logo.${it.lang || null}${path.extname(it.url)}`))) + } + }), + ...images.poster.map(it => { + return { + kind: "poster", + language: it.iso_639_1 || null, + 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.map(it => { + return { + kind: "backdrop", + language: it.iso_639_1 || null, + 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.map(it => { + return { + kind: "still", + language: it.iso_639_1 || null, + 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)}`))) + } + }) + ] 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 { mime: headers["content-type"], ...img, @@ -385,6 +424,7 @@ class MetadataLoader { for (let image of images) { const titleImage = await TitleImage.build({ kind: image.kind, + language: image.language, mime: image.mime, src: image.src, }) diff --git a/src/process_content.js b/src/process_content.js index fd6c32fbfaa56835f49fc6134e4dda19df984900..883dbc549f07158c7d9be13b306cbec27c18ad4a 100644 --- a/src/process_content.js +++ b/src/process_content.js @@ -5,23 +5,27 @@ async function processContent(basePath, fileManager, loader) { async function processMovie(filePath) { const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups; - const ids = (await fileManager.findIds(filePath)) || (await loader.identifyMovie(name, year)); - if (!ids) { - console.error(`Could not identify movie ${name} (${year}) at ${filePath}`) - return; - } - console.info(`Processing movie ${name} (${year})`); - const [media, {title, images}] = await Promise.all([ - fileManager.findMedia(filePath), - loader.loadMetadata(ids), - ]); - const imageData = await loader.processImages(basePath, filePath, images); - await loader.processImageMetadata(title, imageData); - await loader.processMediaMetadata(title, media); + try { + const ids = (await fileManager.findIds(filePath)) || (await loader.identifyMovie(name, year)); + if (!ids) { + console.error(`Could not identify movie ${name} (${year}) at ${filePath}`) + return; + } + console.info(`Processing movie ${name} (${year})`); + const [media, {title, images}] = await Promise.all([ + fileManager.findMedia(filePath), + loader.loadMetadata(ids), + ]); + const imageData = await loader.processImages(basePath, filePath, images); + await loader.processImageMetadata(title, imageData); + await loader.processMediaMetadata(title, media); - fsPromises.wr - await fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)); - console.info(`Finished movie ${name} (${year})`); + fsPromises.wr + await fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)); + console.info(`Finished movie ${name} (${year})`); + } catch (e) { + console.error(`Processing movie ${name} (${year}) failed`, e); + } } async function processEpisode(showIds, episodeIdentifier, filePath) { @@ -36,28 +40,32 @@ async function processContent(basePath, fileManager, loader) { async function processShow(filePath) { const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups; - const ids = (await fileManager.findIds(filePath)) || (await loader.identifyShow(name, year)); - if (!ids) { - console.error(`Could not identify show ${name} (${year}) ${ids.imdb} at ${filePath}`) - return; - } - console.info(`Processing show ${name} (${year}) ${ids.imdb}`); - const episodes = await fileManager.listEpisodes(filePath); + try { + const ids = (await fileManager.findIds(filePath)) || (await loader.identifyShow(name, year)); + if (!ids) { + console.error(`Could not identify show ${name} (${year}) at ${filePath}`) + return; + } + console.info(`Processing show ${name} (${year})`); + const episodes = await fileManager.listEpisodes(filePath); - console.info(`Loading metadata ${name} (${year}) ${ids.imdb}`); - const {title, images} = await loader.loadMetadata(ids); - console.info(`Processing images ${name} (${year}) ${ids.imdb}`); - const imageData = await loader.processImages(basePath, filePath, images); - console.info(`Processing image metadata ${name} (${year}) ${ids.imdb}`); - await loader.processImageMetadata(title, imageData); - console.info(`Processing episode data ${name} (${year}) ${ids.imdb}`); - await Promise.all([ - ...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); - })), - fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)), - ]); - console.log(`Finished show ${name} (${year}) ${ids.imdb}`); + console.info(`Loading metadata ${name} (${year})`); + const {title, images} = await loader.loadMetadata(ids); + console.info(`Processing images ${name} (${year})`); + const imageData = await loader.processImages(basePath, filePath, images); + console.info(`Processing image metadata ${name} (${year})`); + await loader.processImageMetadata(title, imageData); + console.info(`Processing episode data ${name} (${year})`); + await Promise.all([ + ...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); + })), + fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)), + ]); + console.log(`Finished show ${name} (${year})`); + } catch (e) { + console.error(`Processing show ${name} (${year}) failed`, e); + } } console.info("Processing content"); diff --git a/src/storage.js b/src/storage.js index a9b9bd3ef963984b059f6933550567dc6e84980e..89fdfc2c74af895fab916081775c7de623f8f039 100644 --- a/src/storage.js +++ b/src/storage.js @@ -260,6 +260,7 @@ class Backend { primaryKey: true }, kind: sequelize.DataTypes.TEXT, + language: sequelize.DataTypes.TEXT, mime: { type: sequelize.DataTypes.TEXT, allowNull: false, diff --git a/src/util/download-file.js b/src/util/download-file.js index 58555b8540ee97b92290a6ac547446847146c23d..e6f96a6a310ac8c8cb219209612d0825e351408a 100644 --- a/src/util/download-file.js +++ b/src/util/download-file.js @@ -1,12 +1,14 @@ import fs from 'fs'; import path from 'path'; +import http from 'http'; import https from 'https'; -function downloadFile(url, filePath) { +export default function downloadFile(url, filePath) { return new Promise(((resolve, reject) => { fs.mkdirSync(path.dirname(filePath), {recursive: true}) 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); file.on('close', function () { resolve(response.headers); @@ -15,11 +17,10 @@ function downloadFile(url, filePath) { file.close() }); }).on('error', function (err) { + console.error(`Downloading file failed: ${url}`, err); fs.unlink(filePath, function () { reject(err); }); }); })); } - -export default downloadFile; \ No newline at end of file