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 {
});
}
findEpisodeById(id, seasonNumber, episodeNumber) {
return this.queryJson(ImdbApi.queryGetEpisode, {
1: id,
2: seasonNumber,
3: episodeNumber,
});
}
search(type, title, year) {
return this.query(ImdbApi.querySearch, {
1: type,
......@@ -119,6 +127,20 @@ class ImdbApi {
LEFT OUTER JOIN title_crew on title.tconst = title_crew.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) {
......
......@@ -46,19 +46,43 @@ class FileManager {
.then(result => result
.filter(dir => dir.isDirectory())
.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) {
return await fsPromises
.readFile(path.join(filePath, "metadata.json"))
.readFile(path.join(filePath, "ids.json"))
.then(result => {
if (!result) return null;
const json = JSON.parse(result)
if (!json) return null;
return json.ids
return JSON.parse(result);
}).catch(_ => null);
}
......
......@@ -31,6 +31,10 @@ async function main() {
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;
}
const [media, {metadata, images}] = await Promise.all([
fileManager.findMedia(filePath),
loader.loadMetadata(ids),
......@@ -52,34 +56,52 @@ async function main() {
]);
}
async function processEpisode(ids, episodeIdentifier, filePath) {
const episodeIds = loader.identifyEpisode(ids, episodeIdentifier);
const {episodeMetadata, _} = await loader.loadMetadata(episodeIds, true)
async function processEpisode(showIds, episodeIdentifier, filePath) {
const [media, {metadata, images}] = await Promise.all([
fileManager.findMedia(filePath),
loader.loadEpisodeMetadata(showIds, episodeIdentifier),
]);
const imageData = await loader.processImages(basePath, filePath, images);
await Promise.all([
fsPromises.writeFile(path.join(filePath, "ids.json"), JSON.stringify(episodeIds, null, 2)),
fsPromises.writeFile(path.join(filePath, "metadata.json"), JSON.stringify(episodeMetadata, null, 2)),
fsPromises.writeFile(path.join(filePath, "metadata.json"), JSON.stringify({
...metadata,
...media,
images: imageData.map(img => {
return {
type: img.type,
src: img.src,
}
}),
}, null, 2)),
]);
}
async function processShow(filePath) {
if (1) return;
const {name, year} = /^(?<name>.+) \((?<year>\d+)\)$/.exec(path.basename(filePath)).groups;
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([
loader.loadMetadata(ids)
]);
const images = await loader.processImages(basePath, filePath, rawImages);
const {metadata, images} = await loader.loadMetadata(ids);
const imageData = await loader.processImages(basePath, filePath, images);
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, "metadata.json"), JSON.stringify({
...metadata,
episodes: episodes,
images: images,
episodes: episodes.map(episode => {
return {
src: episode.src,
episodeIdentifier: episode.episodeIdentifier,
}
}),
images: imageData,
}, null, 2)),
]);
}
......
......@@ -61,7 +61,7 @@ class MetadataLoader {
}),
ratings: tmdbContentRatings.results.map(el => {
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 :
null;
return {
......@@ -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) {
function findbest(list) {
let sorted = list.sort((a, b) => b.likes - a.likes);
......@@ -107,9 +138,11 @@ class MetadataLoader {
return {
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],
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],
}
}
......@@ -119,45 +152,59 @@ class MetadataLoader {
query: title,
primary_release_year: year
}).catch((e) => console.error(e));
if (!results) return null;
const result = results.results.sort((a, b) => {
return b.popularity - a.popularity;
})[0];
if (!result) return null;
const tmdbResult = await this.tmdb.request(`movie/${result.id}`);
if (!tmdbResult) return null;
return result ? {
return {
uuid: uuid.v4(),
imdb: tmdbResult.imdb_id,
tmdb: result.id,
imdb: (await this.tmdb.request(`movie/${result.id}`)).imdb_id
} : null;
tvdb: null,
}
}
async identifyShow(title, year) {
/*
FIXME: Implement this properly, previous implementation for clarity below
async identifyShow(showTitle, showYear) {
const tvdbResults = await this.tvdb.getSeriesByName(showTitle)
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 tvdbResult = tvdbResults[0];
const tvdbId = result.id;
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}`, {
"external_source": "imdb_id",
}).catch((e) => console.error(e));
const imdbId = tvdbSeries.imdbId;
const tmdbResults = (await this.tmdb.request(`find/${imdbId}`, {
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) => {
return b.popularity - a.popularity;
})[0];
const tmdbSeries = tmdbResults.tv_results ? tmdbResults.tv_results[0] : null;
if (!tmdbSeries) return null;
if (!tmdbResult) return null;
const tmdbId = tmdbSeries.id;
return {
uuid: uuid.v4(),
imdb: imdbId,
tvdb: tvdbResult.id,
tmdb: tmdbResult.id,
};
*/
tvdb: tvdbId,
tmdb: tmdbId,
}
}
async processImages(basePath, filePath, images) {
......@@ -176,6 +223,11 @@ class MetadataLoader {
type: "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 : {
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);
......@@ -184,16 +236,37 @@ class MetadataLoader {
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);
function tmdbSources() {
if (titleType !== "tvSeries") {
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 {
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 {
const [imdbResult, tmdbResult, tmdbTranslations, tmdbContentRatings, tmdbImages, fanartImages] = await Promise.all([
this.imdb.findById(ids.imdb).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));
const metadata = this.transformData(ids, imdbResult, tmdbResult, tmdbContentRatings, tmdbTranslations);
return {
metadata: metadata,
// also use tvdb for images
images: isEpisode ? null : this.chooseImages(metadata.originalLanguage, tmdbImages, fanartImages),
images: this.chooseImages(metadata.originalLanguage, tmdbImages, fanartImages),
};
}
}
......
......@@ -18,11 +18,13 @@ class VideoMimeParser {
return {
container: "application/dash+xml",
tracks: info.MPD.Period.AdaptationSet.map((track, i) => {
const representations = track.Representation instanceof Array ? track.Representation : [track.Representation];
return {
id: i,
type: track.mimeType.substr(0, track.mimeType.indexOf('/')),
codec: [...new Set(track.Representation.map(r => r.codecs))].join(","),
bitrate: track.Representation.map(r => r.bandwidth).sort()[0],
codec: [...new Set(representations.map(r => r.codecs))].join(","),
bitrate: representations.map(r => r.bandwidth).sort()[0],
language: track.lang
}
})
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment