From a853ffc394f955133703d08249a49c94b3c74ead Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Fri, 15 May 2020 15:33:22 +0200
Subject: [PATCH] Cleanup data model to allow graphql server backend to handle
 this better

---
 src/api/imdb_api.js    |   4 +-
 src/metadata_loader.js |  84 ++++++++++++---------
 src/storage.js         | 166 +++++++++++++++++++++++++++++++----------
 3 files changed, 174 insertions(+), 80 deletions(-)

diff --git a/src/api/imdb_api.js b/src/api/imdb_api.js
index db88097..ac641b6 100644
--- a/src/api/imdb_api.js
+++ b/src/api/imdb_api.js
@@ -69,7 +69,7 @@ class ImdbApi {
                        'isAdult', json(case when title.isAdult = 0 then 'false' else 'true' end),
                        'startYear', title.startYear,
                        'endYear', title.endYear,
-                       'runtimeMinutes', title.runtimeMinutes,
+                       'runtime', title.runtimeMinutes,
                        'genres', json('["' || replace(title.genres, ',', '","') || '"]'),
                        'rating', json_object(
                                'averageRating', title_ratings.averageRating,
@@ -133,7 +133,7 @@ class ImdbApi {
                        'id', title.tconst,
                        'primaryTitle', title.primaryTitle,
                        'originalTitle', title.originalTitle,
-                       'runtimeMinutes', title.runtimeMinutes
+                       'runtime', title.runtimeMinutes
                    ) AS json
         FROM title_episode
                  JOIN title ON title_episode.tconst = title.tconst
diff --git a/src/metadata_loader.js b/src/metadata_loader.js
index 9e08464..b04b9d5 100644
--- a/src/metadata_loader.js
+++ b/src/metadata_loader.js
@@ -50,16 +50,16 @@ class MetadataLoader {
         })
         const primaryTitleName = await TitleName.build({
             region: null,
-            languages: null,
-            original: false,
+            languages: [],
+            kind: "primary",
             name: imdbResult.primaryTitle,
         });
         await primaryTitleName.setTitle(title.id, {save: false});
         await primaryTitleName.save();
         const originalTitleName = await TitleName.build({
             region: null,
-            languages: null,
-            original: true,
+            languages: [],
+            kind: "original",
             name: imdbResult.originalTitle,
         });
         await originalTitleName.setTitle(title.id, {save: false});
@@ -68,7 +68,7 @@ class MetadataLoader {
             const titleName = await TitleName.build({
                 region: el.region,
                 languages: el.languages ? el.languages.split(",") : [],
-                original: false,
+                kind: "localized",
                 name: el.title,
             })
             await titleName.setTitle(title.id, {save: false});
@@ -81,7 +81,8 @@ class MetadataLoader {
         })
         const originalTitleDescription = await TitleDescription.build({
             region: null,
-            languages: null,
+            languages: [],
+            kind: "original",
             overview: tmdbResult.overview,
             tagline: tmdbResult.tagline,
         });
@@ -91,6 +92,7 @@ class MetadataLoader {
             const titleDescription = await TitleDescription.build({
                 region: el.iso_3166_1,
                 languages: el.iso_639_1 ? el.iso_639_1.split(",") : [],
+                kind: "localized",
                 overview: el.data.overview,
                 tagline: el.data.tagline,
             })
@@ -110,7 +112,7 @@ class MetadataLoader {
 
             const titleCast = await TitleCast.build({
                 category: el.category,
-                characters: el.characters,
+                characters: el.characters || [],
                 job: el.job,
             });
             await titleCast.setTitle(title.id, {save: false});
@@ -165,7 +167,7 @@ class MetadataLoader {
         const showTitle = await Title.findByPk(ids.uuid);
         const [mapping] = await TitleEpisode.findOrBuild({
             where: {
-                show_id: showTitle.id,
+                parent_id: showTitle.id,
                 season_number: episodeIdentifier.season,
                 episode_number: episodeIdentifier.episode,
             },
@@ -179,11 +181,13 @@ class MetadataLoader {
             tmdb_id: tmdbResult.id,
             tvdb_id: null,
             original_language: showTitle.original_language,
+            runtime: imdbResult.runtime,
         }, {returning: true});
         mapping.air_date = tmdbResult.air_date;
-        await mapping.setShow(showTitle.id, {save: false});
-        await mapping.setEpisode(episodeTitle, {save: false});
+        await mapping.setParent(showTitle, {save: false});
         await mapping.save();
+        await episodeTitle.setParent(mapping, { save: false});
+        await episodeTitle.save();
         await TitleName.destroy({
             where: {
                 title_id: episodeTitle.id,
@@ -191,29 +195,31 @@ class MetadataLoader {
         })
         const primaryTitleName = await TitleName.build({
             region: null,
-            languages: null,
-            original: false,
+            languages: [],
+            kind: "primary",
             name: imdbResult.primaryTitle,
         });
         await primaryTitleName.setTitle(episodeTitle.id, {save: false});
         await primaryTitleName.save();
         const originalTitleName = await TitleName.build({
             region: null,
-            languages: null,
-            original: true,
+            languages: [],
+            kind: "original",
             name: imdbResult.originalTitle,
         });
         await originalTitleName.setTitle(episodeTitle.id, {save: false});
         await originalTitleName.save();
         for (let el of tmdbTranslations.translations) {
-            const titleName = await TitleName.build({
-                region: el.iso_3166_1,
-                languages: el.iso_639_1 ? el.iso_639_1.split(",") : [],
-                original: false,
-                name: el.data.name,
-            })
-            await titleName.setTitle(episodeTitle.id, {save: false});
-            await titleName.save();
+            if (el.data.name) {
+                const titleName = await TitleName.build({
+                    region: el.iso_3166_1,
+                    languages: el.iso_639_1 ? el.iso_639_1.split(",") : [],
+                    kind: "localized",
+                    name: el.data.name,
+                })
+                await titleName.setTitle(episodeTitle.id, {save: false});
+                await titleName.save();
+            }
         }
         await TitleDescription.destroy({
             where: {
@@ -222,21 +228,25 @@ class MetadataLoader {
         })
         const originalTitleDescription = await TitleDescription.build({
             region: null,
-            languages: null,
+            languages: [],
+            kind: "original",
             overview: tmdbResult.overview,
             tagline: tmdbResult.tagline,
         });
         await originalTitleDescription.setTitle(episodeTitle.id, {save: false});
         await originalTitleDescription.save();
         for (let el of tmdbTranslations.translations) {
-            const titleDescription = await TitleDescription.build({
-                region: el.iso_3166_1,
-                languages: el.iso_639_1 ? el.iso_639_1.split(",") : [],
-                overview: el.data.overview,
-                tagline: el.data.tagline,
-            })
-            await titleDescription.setTitle(episodeTitle.id, {save: false});
-            await titleDescription.save();
+            if (el.data.overview) {
+                const titleDescription = await TitleDescription.build({
+                    region: el.iso_3166_1,
+                    languages: el.iso_639_1 ? el.iso_639_1.split(",") : [],
+                    kind: "localized",
+                    overview: el.data.overview,
+                    tagline: el.data.tagline,
+                })
+                await titleDescription.setTitle(episodeTitle.id, {save: false});
+                await titleDescription.save();
+            }
         }
 
         return episodeTitle;
@@ -349,29 +359,29 @@ class MetadataLoader {
     async processImages(basePath, filePath, images) {
         const imageData = !images ? [] : [
             !images.logo ? null : !images.logo.url ? null : {
-                type: "logo",
+                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 : {
-                type: "poster",
+                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 : {
-                type: "backdrop",
+                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 : {
-                type: "still",
+                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);
 
         return await Promise.all(imageData.map(async img => {
-            const headers = await downloadFile(img.url, path.join(filePath, "metadata", img.type + path.extname(img.url)));
+            const headers = await downloadFile(img.url, path.join(filePath, "metadata", img.kind + path.extname(img.url)));
             return {
                 mime: headers["content-type"],
                 ...img,
@@ -387,7 +397,7 @@ class MetadataLoader {
         })
         for (let image of images) {
             const titleImage = await TitleImage.build({
-                type: image.type,
+                kind: image.kind,
                 mime: image.mime,
                 src: image.src,
             })
diff --git a/src/storage.js b/src/storage.js
index b2eb8c5..b9a6bb0 100644
--- a/src/storage.js
+++ b/src/storage.js
@@ -31,7 +31,10 @@ class Backend {
                 primaryKey: true
             },
             tmdb_id: sequelize.DataTypes.INTEGER,
-            name: sequelize.DataTypes.TEXT,
+            name: {
+                type: sequelize.DataTypes.TEXT,
+                allowNull: false,
+            },
         }, {
             sequelize: this.db,
             underscored: true,
@@ -45,8 +48,11 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            imdb_id: sequelize.DataTypes.STRING(64),
-            name: sequelize.DataTypes.TEXT,
+            imdb_id: sequelize.DataTypes.TEXT,
+            name: {
+                type: sequelize.DataTypes.TEXT,
+                allowNull: false,
+            },
         }, {
             sequelize: this.db,
             underscored: true,
@@ -60,10 +66,10 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            imdb_id: sequelize.DataTypes.STRING(64),
+            imdb_id: sequelize.DataTypes.TEXT,
             tmdb_id: sequelize.DataTypes.INTEGER,
             tvdb_id: sequelize.DataTypes.INTEGER,
-            original_language: sequelize.DataTypes.STRING(32),
+            original_language: sequelize.DataTypes.TEXT,
             runtime: sequelize.DataTypes.INTEGER,
             year_start: sequelize.DataTypes.INTEGER,
             year_end: sequelize.DataTypes.INTEGER,
@@ -81,7 +87,10 @@ class Backend {
                 primaryKey: true
             },
             category: sequelize.DataTypes.TEXT,
-            characters: sequelize.DataTypes.ARRAY(sequelize.DataTypes.TEXT),
+            characters: {
+                type: sequelize.DataTypes.ARRAY(sequelize.DataTypes.TEXT),
+                allowNull: false,
+            },
             job: sequelize.DataTypes.TEXT,
         }, {
             sequelize: this.db,
@@ -89,9 +98,17 @@ class Backend {
             modelName: 'title_cast',
             indexes: []
         });
-        TitleCast.belongsTo(Title);
+        TitleCast.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleCast);
-        TitleCast.belongsTo(Person);
+        TitleCast.belongsTo(Person, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Person.hasMany(TitleCast);
 
         TitleDescription.init({
@@ -101,9 +118,19 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            region: sequelize.DataTypes.STRING(32),
-            languages: sequelize.DataTypes.ARRAY(sequelize.DataTypes.STRING(32)),
-            overview: sequelize.DataTypes.TEXT,
+            region: sequelize.DataTypes.TEXT,
+            languages: {
+                type: sequelize.DataTypes.ARRAY(sequelize.DataTypes.TEXT),
+                allowNull: false,
+            },
+            kind: {
+                type: sequelize.DataTypes.TEXT,
+                allowNull: false,
+            },
+            overview: {
+                type: sequelize.DataTypes.TEXT,
+                allowNull: false,
+            },
             tagline: sequelize.DataTypes.TEXT,
         }, {
             sequelize: this.db,
@@ -111,7 +138,11 @@ class Backend {
             modelName: 'title_description',
             indexes: []
         });
-        TitleDescription.belongsTo(Title);
+        TitleDescription.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleDescription);
 
         TitleEpisode.init({
@@ -121,8 +152,8 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            season_number: sequelize.DataTypes.STRING(64),
-            episode_number: sequelize.DataTypes.STRING(64),
+            season_number: sequelize.DataTypes.TEXT,
+            episode_number: sequelize.DataTypes.TEXT,
             air_date: sequelize.DataTypes.DATEONLY,
         }, {
             sequelize: this.db,
@@ -131,7 +162,7 @@ class Backend {
             indexes: [
                 {
                     fields: [
-                        'show_id',
+                        'parent_id',
                         {
                             attribute: 'season_number',
                             collate: 'C',
@@ -147,7 +178,7 @@ class Backend {
                 {
                     using: 'BTREE',
                     fields: [
-                        'show_id',
+                        'parent_id',
                         {
                             attribute: 'air_date',
                             order: 'ASC',
@@ -156,10 +187,20 @@ class Backend {
                 }
             ]
         });
-        TitleEpisode.belongsTo(Title, {as: "Show", foreignKey: "show_id"});
-        TitleEpisode.belongsTo(Title, {as: "Episode", foreignKey: "episode_id"});
-        Title.hasMany(TitleEpisode, { foreignKey: "show_id", as: 'Episodes'});
-        Title.hasOne(TitleEpisode, { foreignKey: "episode_id", as: 'Show'});
+        TitleEpisode.belongsTo(Title, {
+            as: "Parent",
+            foreignKey: {
+                name: "parent_id",
+                allowNull: false,
+            }
+        })
+        Title.belongsTo(TitleEpisode, {
+            as: "Parent",
+            foreignKey: {
+                name: "parent_id",
+                allowNull: true,
+            }
+        })
         TitleGenre.init({
             id: {
                 type: sequelize.DataTypes.UUID,
@@ -175,9 +216,17 @@ class Backend {
             modelName: 'title_genre',
             indexes: []
         });
-        TitleGenre.belongsTo(Title);
+        TitleGenre.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleGenre);
-        TitleGenre.belongsTo(Genre);
+        TitleGenre.belongsTo(Genre, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Genre.hasMany(TitleGenre);
         TitleImage.init({
             id: {
@@ -186,7 +235,7 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            type: sequelize.DataTypes.STRING(64),
+            kind: sequelize.DataTypes.TEXT,
             mime: sequelize.DataTypes.TEXT,
             src: sequelize.DataTypes.TEXT,
         }, {
@@ -195,7 +244,11 @@ class Backend {
             modelName: 'title_image',
             indexes: []
         });
-        TitleImage.belongsTo(Title);
+        TitleImage.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleImage);
         TitleMedia.init({
             id: {
@@ -204,9 +257,15 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            mime: sequelize.DataTypes.STRING(64),
-            codecs: sequelize.DataTypes.ARRAY(sequelize.DataTypes.STRING(64)),
-            languages: sequelize.DataTypes.ARRAY(sequelize.DataTypes.STRING(32)),
+            mime: sequelize.DataTypes.TEXT,
+            codecs: {
+                type: sequelize.DataTypes.ARRAY(sequelize.DataTypes.TEXT),
+                allowNull: false,
+            },
+            languages: {
+                type: sequelize.DataTypes.ARRAY(sequelize.DataTypes.TEXT),
+                allowNull: false,
+            },
             src: sequelize.DataTypes.TEXT,
         }, {
             sequelize: this.db,
@@ -214,7 +273,11 @@ class Backend {
             modelName: 'title_media',
             indexes: []
         });
-        TitleMedia.belongsTo(Title);
+        TitleMedia.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleMedia);
         TitleName.init({
             id: {
@@ -223,17 +286,30 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            region: sequelize.DataTypes.STRING(32),
-            languages: sequelize.DataTypes.ARRAY(sequelize.DataTypes.STRING(32)),
-            original: sequelize.DataTypes.BOOLEAN,
-            name: sequelize.DataTypes.TEXT,
+            region: sequelize.DataTypes.TEXT,
+            languages: {
+                type: sequelize.DataTypes.ARRAY(sequelize.DataTypes.TEXT),
+                allowNull: false,
+            },
+            kind: {
+                type: sequelize.DataTypes.TEXT,
+                allowNull: false,
+            },
+            name: {
+                type: sequelize.DataTypes.TEXT,
+                allowNull: false,
+            },
         }, {
             sequelize: this.db,
             underscored: true,
             modelName: 'title_name',
             indexes: []
         });
-        TitleName.belongsTo(Title);
+        TitleName.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleName);
         TitleRating.init({
             id: {
@@ -242,15 +318,19 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            region: sequelize.DataTypes.STRING(32),
-            certification: sequelize.DataTypes.STRING(32),
+            region: sequelize.DataTypes.TEXT,
+            certification: sequelize.DataTypes.TEXT,
         }, {
             sequelize: this.db,
             underscored: true,
             modelName: 'title_rating',
             indexes: []
         });
-        TitleRating.belongsTo(Title);
+        TitleRating.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleRating);
         TitleSubtitles.init({
             id: {
@@ -259,10 +339,10 @@ class Backend {
                 allowNull: false,
                 primaryKey: true
             },
-            format: sequelize.DataTypes.STRING(64),
-            language: sequelize.DataTypes.STRING(64),
-            region: sequelize.DataTypes.STRING(64),
-            specifier: sequelize.DataTypes.STRING(64),
+            format: sequelize.DataTypes.TEXT,
+            language: sequelize.DataTypes.TEXT,
+            region: sequelize.DataTypes.TEXT,
+            specifier: sequelize.DataTypes.TEXT,
             src: sequelize.DataTypes.TEXT,
         }, {
             sequelize: this.db,
@@ -270,7 +350,11 @@ class Backend {
             modelName: 'title_subtitles',
             indexes: []
         });
-        TitleSubtitles.belongsTo(Title);
+        TitleSubtitles.belongsTo(Title, {
+            foreignKey: {
+                allowNull: false,
+            }
+        });
         Title.hasMany(TitleSubtitles);
     }
 
-- 
GitLab