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

Implement episode metadata handling

parent 2365b574
No related branches found
No related tags found
No related merge requests found
...@@ -29,6 +29,14 @@ class ImdbApi { ...@@ -29,6 +29,14 @@ class ImdbApi {
}); });
} }
findEpisodeById(id, seasonNumber, episodeNumber) {
return this.queryJson(ImdbApi.queryGetEpisode, {
1: id,
2: seasonNumber,
3: episodeNumber,
});
}
search(type, title, year) { search(type, title, year) {
return this.query(ImdbApi.querySearch, { return this.query(ImdbApi.querySearch, {
1: type, 1: type,
...@@ -119,6 +127,20 @@ class ImdbApi { ...@@ -119,6 +127,20 @@ class ImdbApi {
LEFT OUTER JOIN title_crew on title.tconst = title_crew.tconst LEFT OUTER JOIN title_crew on title.tconst = title_crew.tconst
WHERE title.tconst = ? WHERE title.tconst = ?
`; `;
static queryGetEpisode = `
SELECT json_object(
'id', title.tconst,
'primaryTitle', title.primaryTitle,
'originalTitle', title.originalTitle,
'runtimeMinutes', title.runtimeMinutes
) AS json
FROM title_episode
JOIN title ON title_episode.tconst = title.tconst
WHERE title_episode.parentTconst = ?
AND seasonNumber = ?
AND episodeNumber = ?
`;
} }
function promisify(db, fun) { function promisify(db, fun) {
......
...@@ -46,19 +46,43 @@ class FileManager { ...@@ -46,19 +46,43 @@ class FileManager {
.then(result => result .then(result => result
.filter(dir => dir.isDirectory()) .filter(dir => dir.isDirectory())
.map(dir => { .map(dir => {
return path.join(this.moviesPath, dir.name) return path.join(this.showsPath, dir.name)
}) })
); );
} }
async listEpisodes(showPath) {
return fsPromises
.readdir(showPath, {withFileTypes: true})
.then(result => result
.filter(dir => dir.isDirectory())
.map(dir => {
const match = /^(?:(?:(?<year>\d+)-(?<month>\d+)-(?<day>\d+))|(?:(?:S(?<season>\d+(?:[.\-–—&]\d+)?))?\p{L}*[\t\f ]*(?<episode>\d+(?:[.\-–—&]\d+)?)))(?:(?:[\t\f ]+(?:[\-–—:][\t\f ]*)?)(?<title>\S.*))?$/u.exec(dir.name);
if (!match) {
return null;
}
const {season, episode, year, month, day} = match.groups
return {
filePath: path.join(showPath, dir.name),
src: encodePath(path.relative(this.basePath, path.join(showPath, dir.name))),
episodeIdentifier: {
season: season,
episode: episode,
year: year,
month: month,
day: day,
},
}
}).filter(el => el !== null)
);
}
async findIds(filePath) { async findIds(filePath) {
return await fsPromises return await fsPromises
.readFile(path.join(filePath, "metadata.json")) .readFile(path.join(filePath, "ids.json"))
.then(result => { .then(result => {
if (!result) return null; if (!result) return null;
const json = JSON.parse(result) return JSON.parse(result);
if (!json) return null;
return json.ids
}).catch(_ => null); }).catch(_ => null);
} }
......
...@@ -31,6 +31,10 @@ async function main() { ...@@ -31,6 +31,10 @@ async function main() {
const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups; const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups;
const ids = (await fileManager.findIds(filePath)) || (await loader.identifyMovie(name, year)); const ids = (await fileManager.findIds(filePath)) || (await loader.identifyMovie(name, year));
if (!ids) {
console.error(`Could not identify movie ${name} (${year}) at ${filePath}`)
return;
}
const [media, {metadata, images}] = await Promise.all([ const [media, {metadata, images}] = await Promise.all([
fileManager.findMedia(filePath), fileManager.findMedia(filePath),
loader.loadMetadata(ids), loader.loadMetadata(ids),
...@@ -52,34 +56,52 @@ async function main() { ...@@ -52,34 +56,52 @@ async function main() {
]); ]);
} }
async function processEpisode(ids, episodeIdentifier, filePath) { async function processEpisode(showIds, episodeIdentifier, filePath) {
const episodeIds = loader.identifyEpisode(ids, episodeIdentifier); const [media, {metadata, images}] = await Promise.all([
const {episodeMetadata, _} = await loader.loadMetadata(episodeIds, true) fileManager.findMedia(filePath),
loader.loadEpisodeMetadata(showIds, episodeIdentifier),
]);
const imageData = await loader.processImages(basePath, filePath, images);
await Promise.all([ await Promise.all([
fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(episodeIds, null, 2)), fsPromises.writeFile(path.join(filePath, "metadata.json"), JSON.stringify({
fsPromises.writeFile(path.join(filePath, "metadata.json"), JSON.stringify(episodeMetadata, null, 2)), ...metadata,
...media,
images: imageData.map(img => {
return {
type: img.type,
src: img.src,
}
}),
}, null, 2)),
]); ]);
} }
async function processShow(filePath) { async function processShow(filePath) {
if (1) return;
const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups; const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups;
const ids = (await fileManager.findIds(filePath)) || (await loader.identifyShow(name, year)); const ids = (await fileManager.findIds(filePath)) || (await loader.identifyShow(name, year));
const episodes = fileManager.findEpisodes(filePath); if (!ids) {
console.error(`Could not identify show ${name} (${year}) at ${filePath}`)
return;
}
const episodes = await fileManager.listEpisodes(filePath);
const {metadata, rawImages} = await Promise.all([ const {metadata, images} = await loader.loadMetadata(ids);
loader.loadMetadata(ids) const imageData = await loader.processImages(basePath, filePath, images);
]);
const images = await loader.processImages(basePath, filePath, rawImages);
await Promise.all([ await Promise.all([
...episodes.map(async ({episodeIdentifier, filePath}) => await processEpisode(ids, episodeIdentifier, filePath)), ...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)), fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(ids, null, 2)),
fsPromises.writeFile(path.join(filePath, "metadata.json"), JSON.stringify({ fsPromises.writeFile(path.join(filePath, "metadata.json"), JSON.stringify({
...metadata, ...metadata,
episodes: episodes, episodes: episodes.map(episode => {
images: images, return {
src: episode.src,
episodeIdentifier: episode.episodeIdentifier,
}
}),
images: imageData,
}, null, 2)), }, null, 2)),
]); ]);
} }
......
...@@ -61,7 +61,7 @@ class MetadataLoader { ...@@ -61,7 +61,7 @@ class MetadataLoader {
}), }),
ratings: tmdbContentRatings.results.map(el => { ratings: tmdbContentRatings.results.map(el => {
const certification = const certification =
Array.isArray(el) ? el.release_dates.sort((a, b) => b.type - a.type).map(el => el.certification) : Array.isArray(el.release_dates) ? el.release_dates.sort((a, b) => b.type - a.type).map(el => el.certification)[0] :
el ? el.rating : el ? el.rating :
null; null;
return { return {
...@@ -72,6 +72,37 @@ class MetadataLoader { ...@@ -72,6 +72,37 @@ class MetadataLoader {
} }
} }
transformEpisodeData(ids, imdbResult, tmdbResult, tmdbTranslations) {
if (!imdbResult) return null;
if (!tmdbResult) return null;
if (!tmdbTranslations) return null;
return {
ids: ids,
originalLanguage: tmdbResult.original_language,
originalTitle: imdbResult.originalTitle,
primaryTitle: imdbResult.primaryTitle,
titles: tmdbTranslations.translations.map(el => {
return {
title: el.data.name,
region: el.iso_3166_1,
languages: el.iso_639_1 ? el.iso_639_1.split(",") : [],
}
}).filter(el => el.overview),
primaryDescription: {
overview: tmdbResult.overview,
},
descriptions: tmdbTranslations.translations.map(el => {
return {
region: el.iso_3166_1,
languages: el.iso_639_1 ? el.iso_639_1.split(",") : [],
overview: el.data.overview,
}
}).filter(el => el.overview),
runtime: imdbResult.runtimeMinutes
}
}
chooseImages(originalLanguage, tmdbImages, fanartImages) { chooseImages(originalLanguage, tmdbImages, fanartImages) {
function findbest(list) { function findbest(list) {
let sorted = list.sort((a, b) => b.likes - a.likes); let sorted = list.sort((a, b) => b.likes - a.likes);
...@@ -107,9 +138,11 @@ class MetadataLoader { ...@@ -107,9 +138,11 @@ class MetadataLoader {
return { return {
logo: fanartImages && fanartImages.hdmovielogo ? findbest(fanartImages.hdmovielogo) : null, logo: fanartImages && fanartImages.hdmovielogo ? findbest(fanartImages.hdmovielogo) : null,
poster: tmdbImages.posters.map(calculateConfidenceAndQuality(true, originalLanguage)) poster: tmdbImages.posters && tmdbImages.posters.map(calculateConfidenceAndQuality(true, originalLanguage))
.sort(imageComparator)[0], .sort(imageComparator)[0],
backdrop: tmdbImages.backdrops.map(calculateConfidenceAndQuality(false, originalLanguage)) backdrop: tmdbImages.backdrops && tmdbImages.backdrops.map(calculateConfidenceAndQuality(false, originalLanguage))
.sort(imageComparator)[0],
still: tmdbImages.stills && tmdbImages.stills.map(calculateConfidenceAndQuality(false, originalLanguage))
.sort(imageComparator)[0], .sort(imageComparator)[0],
} }
} }
...@@ -119,45 +152,59 @@ class MetadataLoader { ...@@ -119,45 +152,59 @@ class MetadataLoader {
query: title, query: title,
primary_release_year: year primary_release_year: year
}).catch((e) => console.error(e)); }).catch((e) => console.error(e));
if (!results) return null;
const result = results.results.sort((a, b) => { const result = results.results.sort((a, b) => {
return b.popularity - a.popularity; return b.popularity - a.popularity;
})[0]; })[0];
if (!result) return null;
const tmdbResult = await this.tmdb.request(`movie/${result.id}`);
if (!tmdbResult) return null;
return result ? { return {
uuid: uuid.v4(), uuid: uuid.v4(),
imdb: tmdbResult.imdb_id,
tmdb: result.id, tmdb: result.id,
imdb: (await this.tmdb.request(`movie/${result.id}`)).imdb_id tvdb: null,
} : null; }
} }
async identifyShow(title, year) { async identifyShow(showTitle, showYear) {
/* const tvdbResults = await this.tvdb.getSeriesByName(showTitle)
FIXME: Implement this properly, previous implementation for clarity below if (!tvdbResults) return null;
const imdbId = await this.imdb.search("tvSeries", title, year); const result = tvdbResults.find(show => {
const {year} = /^(?<year>\d+)(?:-(?<month>\d+)(?:-(?<day>\d+))?)?$/.exec(show.firstAired).groups;
return year === showYear;
});
if (!result) return null;
const tvdbResults = await this.tvdb.getSeriesByImdbId(imdbId).catch((e) => console.error(e)); const tvdbId = result.id;
const tvdbResult = tvdbResults[0]; if (!tvdbId) return null;
if (!tvdbResult) return null; const tvdbSeries = await this.tvdb.getSeriesById(tvdbId);
if (!tvdbSeries) return null;
const tmdbResults = await this.tmdb.request(`find/${imdbId}`, { const imdbId = tvdbSeries.imdbId;
"external_source": "imdb_id", const tmdbResults = (await this.tmdb.request(`find/${imdbId}`, {
}).catch((e) => console.error(e)); external_source: "imdb_id"
})) || (await this.tmdb.request(`find/${tvdbId}`, {
external_source: "tvdb_id"
}))
if (!tmdbResults) return null;
const tmdbResult = tmdbResults.tv_results.sort((a, b) => { const tmdbSeries = tmdbResults.tv_results ? tmdbResults.tv_results[0] : null;
return b.popularity - a.popularity; if (!tmdbSeries) return null;
})[0];
if (!tmdbResult) return null; const tmdbId = tmdbSeries.id;
return { return {
uuid: uuid.v4(),
imdb: imdbId, imdb: imdbId,
tvdb: tvdbResult.id, tvdb: tvdbId,
tmdb: tmdbResult.id, tmdb: tmdbId,
}; }
*/
} }
async processImages(basePath, filePath, images) { async processImages(basePath, filePath, images) {
...@@ -176,6 +223,11 @@ class MetadataLoader { ...@@ -176,6 +223,11 @@ class MetadataLoader {
type: "backdrop", type: "backdrop",
url: this.tmdb.getImageUrl(images.backdrop.file_path), url: this.tmdb.getImageUrl(images.backdrop.file_path),
src: encodePath(path.relative(basePath, path.join(filePath, "metadata", `backdrop${path.extname(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 : {
type: "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); ].filter(el => el !== null);
...@@ -184,16 +236,37 @@ class MetadataLoader { ...@@ -184,16 +236,37 @@ class MetadataLoader {
return imageData; return imageData;
} }
async loadMetadata(ids, isEpisode) { async loadEpisodeMetadata(ids, episodeIdentifier) {
const {season, episode} = episodeIdentifier;
const tmdbSources = [
`tv/${ids.tmdb}/season/${season}/episode/${episode}`,
`tv/${ids.tmdb}/season/${season}/episode/${episode}/translations`,
`tv/${ids.tmdb}/season/${season}/episode/${episode}/images`
]
const [imdbResult, tmdbResult, tmdbTranslations, tmdbImages] = await Promise.all([
this.imdb.findEpisodeById(ids.imdb, season, episode).catch(_ => null),
...tmdbSources.map(url => this.tmdb.request(url).catch(_ => null)),
].filter(el => el !== null));
const metadata = this.transformEpisodeData(ids, imdbResult, tmdbResult, tmdbTranslations);
if (!metadata) {
return null;
}
return {
metadata: metadata,
images: this.chooseImages(metadata.originalLanguage, tmdbImages),
};
}
async loadMetadata(ids) {
const titleType = await this.imdb.findTypeById(ids.imdb); const titleType = await this.imdb.findTypeById(ids.imdb);
function tmdbSources() { function tmdbSources() {
if (titleType !== "tvSeries") { if (titleType !== "tvSeries") {
return [`movie/${ids.tmdb}`, `movie/${ids.tmdb}/translations`, `movie/${ids.tmdb}/release_dates`, `movie/${ids.tmdb}/images`] return [`movie/${ids.tmdb}`, `movie/${ids.tmdb}/translations`, `movie/${ids.tmdb}/release_dates`, `movie/${ids.tmdb}/images`]
} else if (!isEpisode) {
return [`tv/${ids.tmdb}`, `tv/${ids.tmdb}/translations`, `tv/${ids.tmdb}/content_ratings`, `tv/${ids.tmdb}/images`]
} else { } else {
return [`tv/${ids.tmdb}`, `tv/${ids.tmdb}/translations`] return [`tv/${ids.tmdb}`, `tv/${ids.tmdb}/translations`, `tv/${ids.tmdb}/content_ratings`, `tv/${ids.tmdb}/images`]
} }
} }
...@@ -204,14 +277,13 @@ class MetadataLoader { ...@@ -204,14 +277,13 @@ class MetadataLoader {
const [imdbResult, tmdbResult, tmdbTranslations, tmdbContentRatings, tmdbImages, fanartImages] = await Promise.all([ const [imdbResult, tmdbResult, tmdbTranslations, tmdbContentRatings, tmdbImages, fanartImages] = await Promise.all([
this.imdb.findById(ids.imdb).catch(_ => null), this.imdb.findById(ids.imdb).catch(_ => null),
...tmdbSources().map(url => this.tmdb.request(url).catch(_ => null)), ...tmdbSources().map(url => this.tmdb.request(url).catch(_ => null)),
isEpisode ? null : this.fanart.request(fanartSource).catch(_ => null) // do nothing, it just means it wasn’t found this.fanart.request(fanartSource).catch(_ => null) // do nothing, it just means it wasn’t found
].filter(el => el !== null)); ].filter(el => el !== null));
const metadata = this.transformData(ids, imdbResult, tmdbResult, tmdbContentRatings, tmdbTranslations); const metadata = this.transformData(ids, imdbResult, tmdbResult, tmdbContentRatings, tmdbTranslations);
return { return {
metadata: metadata, metadata: metadata,
// also use tvdb for images images: this.chooseImages(metadata.originalLanguage, tmdbImages, fanartImages),
images: isEpisode ? null : this.chooseImages(metadata.originalLanguage, tmdbImages, fanartImages),
}; };
} }
} }
......
...@@ -18,11 +18,13 @@ class VideoMimeParser { ...@@ -18,11 +18,13 @@ class VideoMimeParser {
return { return {
container: "application/dash+xml", container: "application/dash+xml",
tracks: info.MPD.Period.AdaptationSet.map((track, i) => { tracks: info.MPD.Period.AdaptationSet.map((track, i) => {
const representations = track.Representation instanceof Array ? track.Representation : [track.Representation];
return { return {
id: i, id: i,
type: track.mimeType.substr(0, track.mimeType.indexOf('/')), type: track.mimeType.substr(0, track.mimeType.indexOf('/')),
codec: [...new Set(track.Representation.map(r => r.codecs))].join(","), codec: [...new Set(representations.map(r => r.codecs))].join(","),
bitrate: track.Representation.map(r => r.bandwidth).sort()[0], bitrate: representations.map(r => r.bandwidth).sort()[0],
language: track.lang language: track.lang
} }
}) })
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment