diff --git a/.gitignore b/.gitignore index bc081898..d347fffa 100644 --- a/.gitignore +++ b/.gitignore @@ -75,4 +75,9 @@ pyinstrument/ # Now managed by templates addon.xml +*.zip +*.db +*.db-shm +*.db-wal + *.log diff --git a/jellyfin_kodi/database/queries.py b/jellyfin_kodi/database/queries.py index 41a59cda..c7f3d43a 100644 --- a/jellyfin_kodi/database/queries.py +++ b/jellyfin_kodi/database/queries.py @@ -28,6 +28,7 @@ FROM jellyfin WHERE media_folder = ? """ get_item_by_parent_movie_obj = ["{KodiId}", "movie"] +get_item_by_parent_extra_obj = ["{KodiId}", "extra"] get_item_by_parent_tvshow_obj = ["{ParentId}", "tvshow"] get_item_by_parent_season_obj = ["{ParentId}", "season"] get_item_by_parent_episode_obj = ["{ParentId}", "episode"] @@ -106,6 +107,18 @@ add_reference_movie_obj = [ "{LibraryId}", "{JellyfinParentId}", ] +add_reference_extra_obj = [ + "{Id}", + "{ExtraId}", + "{FileId}", + "{PathId}", + "Extra", + "extra", + "{MovieId}", + "{Checksum}", + "{LibraryId}", + "{JellyfinParentId}", +] add_reference_boxset_obj = [ "{Id}", "{SetId}", diff --git a/jellyfin_kodi/downloader.py b/jellyfin_kodi/downloader.py index 147efece..335c7484 100644 --- a/jellyfin_kodi/downloader.py +++ b/jellyfin_kodi/downloader.py @@ -95,6 +95,21 @@ def get_movies_by_boxset(boxset_id): yield items +def get_extra_by_movie(movie_id): + + query = { + "url": "Items/%s/SpecialFeatures" % movie_id, + "params": { + "EnableUserData": True, + "EnableImages": True, + "UserId": "{UserId}", + "Fields": api.info(), + }, + } + for items in _get_items(query): + yield items + + def get_episode_by_show(show_id): query = { @@ -213,9 +228,16 @@ def _get_items(query, server_id=None): test_params["Limit"] = 1 test_params["EnableTotalRecordCount"] = True - items["TotalRecordCount"] = _get(url, test_params, server_id=server_id)[ - "TotalRecordCount" - ] + response = _get(url, test_params, server_id=server_id) + + if "TotalRecordCount" in response: + items["TotalRecordCount"] = response["TotalRecordCount"] + elif isinstance(response, list): + yield { + "Items": response, + "TotalRecordCount": len(response), + "StartIndex": 0, + } except Exception as error: LOG.exception( diff --git a/jellyfin_kodi/full_sync.py b/jellyfin_kodi/full_sync.py index 1448028a..3e2d349c 100644 --- a/jellyfin_kodi/full_sync.py +++ b/jellyfin_kodi/full_sync.py @@ -341,6 +341,8 @@ class FullSync(object): message=movie["Name"], ) obj.movie(movie) + obj.add_extras(movie) + processed_ids.append(movie["Id"]) with self.video_database_locks() as (videodb, jellyfindb): diff --git a/jellyfin_kodi/library.py b/jellyfin_kodi/library.py index 13604d9a..a7c75d31 100644 --- a/jellyfin_kodi/library.py +++ b/jellyfin_kodi/library.py @@ -740,6 +740,8 @@ class UpdateWorker(threading.Thread): LOG.debug("{} - {}".format(item["Type"], item["Name"])) if item["Type"] == "Movie": movies.movie(item) + movies.remove_extras(item) + movies.add_extras(item) elif item["Type"] == "BoxSet": movies.boxset(item) elif item["Type"] == "Series": diff --git a/jellyfin_kodi/objects/kodi/artwork.py b/jellyfin_kodi/objects/kodi/artwork.py index 7162c050..d8aa274e 100644 --- a/jellyfin_kodi/objects/kodi/artwork.py +++ b/jellyfin_kodi/objects/kodi/artwork.py @@ -80,6 +80,22 @@ class Artwork(object): elif artwork.get(art): self.update(*(artwork[art],) + args + (KODI[art],)) + def add_extra(self, artwork, *args): + """Add all artworks.""" + KODI = { + "Thumb": ["landscape", "thumb", "poster"], + "Primary": ["landscape", "thumb", "poster"], + } + + for art in KODI: + if art == "Primary": + for kodi_image in KODI["Primary"]: + self.update(*(artwork["Primary"],) + args + (kodi_image,)) + + elif art == "Thumb": + for kodi_image in KODI["Thumb"]: + self.update(*(artwork["Thumb"],) + args + (kodi_image,)) + def delete(self, *args): """Delete artwork from kodi database""" self.cursor.execute(QU.delete_art, args) diff --git a/jellyfin_kodi/objects/kodi/movies.py b/jellyfin_kodi/objects/kodi/movies.py index da959806..dfc25f0a 100644 --- a/jellyfin_kodi/objects/kodi/movies.py +++ b/jellyfin_kodi/objects/kodi/movies.py @@ -47,14 +47,25 @@ class Movies(Kodi): return self.cursor.fetchone()[0] + 1 - def get(self, *args): + def create_entry_extra(self): + self.cursor.execute(QU.create_extra) + return self.cursor.fetchone()[0] + 1 + + def get(self, *args): try: self.cursor.execute(QU.get_movie, args) return self.cursor.fetchone()[0] except TypeError: return + def get_extra(self, *args): + try: + self.cursor.execute(QU.get_extra, args) + return self.cursor.fetchone()[0] + except TypeError: + return + def add(self, *args): self.cursor.execute(QU.add_movie, args) @@ -66,6 +77,18 @@ class Movies(Kodi): def update(self, *args): self.cursor.execute(QU.update_movie, args) + def update_extra(self, *args): + self.cursor.execute(QU.update_video_version_extra, args) + + def update_video_version_type_extra(self, *args): + self.cursor.execute(QU.update_video_version_type_extra, args) + + def delete_extra(self, kodi_id, file_id): + self.cursor.execute(QU.delete_video_version_extra, (kodi_id,)) + self.cursor.execute(QU.delete_video_version_type_extra, (kodi_id,)) + self.cursor.execute(QU.delete_file, (file_id,)) + self.cursor.execute(QU.delete_streams, (file_id,)) + def delete(self, kodi_id, file_id): self.cursor.execute(QU.delete_movie, (kodi_id,)) diff --git a/jellyfin_kodi/objects/kodi/queries.py b/jellyfin_kodi/objects/kodi/queries.py index 86b0b8ba..9217574d 100644 --- a/jellyfin_kodi/objects/kodi/queries.py +++ b/jellyfin_kodi/objects/kodi/queries.py @@ -40,6 +40,11 @@ create_movie = """ SELECT coalesce(max(idMovie), 0) FROM movie """ +# Kodi extras start at id 40801 +create_extra = """ +SELECT coalesce(max(id), 40800) +FROM videoversiontype +""" create_musicvideo = """ SELECT coalesce(max(idMVideo), 0) FROM musicvideo @@ -120,6 +125,12 @@ FROM movie WHERE idMovie = ? """ get_movie_obj = ["{MovieId}"] +get_extra = """ +SELECT * +FROM videoversiontype +WHERE id = ? +""" +get_extra_obj = ["{ExtraId}"] get_rating = """ SELECT rating_id FROM rating @@ -418,6 +429,17 @@ add_video_version_obj = [ "{VideoVersionItemType}", 40400, ] +update_video_version_extra = """ +INSERT OR REPLACE INTO videoversion(idFile, idMedia, media_type, itemType, idType) +VALUES (?, ?, ?, ?, ?) +""" +update_video_version_extra_obj = [ + "{FileId}", + "{MovieId}", + "movie", + "1", + "{VideoVersionIdType}", +] get_videoversion_itemtype = """ SELECT itemType FROM videoversiontype WHERE id = ? """ @@ -653,6 +675,16 @@ update_unique_id_episode_obj = [ "{ProviderName}", "{Unique}", ] +update_video_version_type_extra = """ +INSERT OR REPLACE INTO videoversiontype(id, name, owner, itemType) +VALUES (?, ?, ?, ?) +""" +update_video_version_type_extra_obj = [ + "{VideoVersionIdType}", + "{Title}", + "1", + "1", +] update_country = """ INSERT OR REPLACE INTO country_link(country_id, media_id, media_type) VALUES (?, ?, ?) @@ -797,6 +829,7 @@ DELETE FROM movie WHERE idMovie = ? """ delete_movie_obj = ["{KodiId}", "{FileId}"] +delete_extra_obj = ["{KodiId}", "{FileId}"] delete_video_version = """ DELETE FROM videoversion WHERE idFile = ? @@ -840,6 +873,16 @@ WHERE media_id = ? AND media_type = ? AND type LIKE ? """ +delete_video_version_extra = """ +DELETE FROM videoversion +WHERE idType = ? +AND itemType = 1 +""" +delete_video_version_type_extra = """ +DELETE FROM videoversiontype +WHERE id = ? +AND itemType = 1 +""" get_missing_versions = """ SELECT idFile,idMovie FROM movie diff --git a/jellyfin_kodi/objects/movies.py b/jellyfin_kodi/objects/movies.py index e155af9e..413155f5 100644 --- a/jellyfin_kodi/objects/movies.py +++ b/jellyfin_kodi/objects/movies.py @@ -189,6 +189,172 @@ class Movies(KodiDb): obj["Title"], ) + def add_extras(self, item): + for extras in server.get_extra_by_movie(item["Id"]): + for extra in extras["Items"]: + LOG.debug("Extras: {}".format(extra)) + extra["MovieId"] = item["Id"] + if extra.get("Path"): + self.extra(extra) + + @stop + @jellyfin_item + def extra(self, item, e_item): + """If item does not exist, entry will be added. + If item exists, entry will be updated. + + Create additional entry for widgets. + """ + server_address = self.server.auth.get_server_info(self.server.auth.server_id)[ + "address" + ] + API = api.API(item, server_address) + obj = self.objects.map(item, "Extra") + update = True + + if "Location" in obj and obj["Location"] == "Virtual": + LOG.info("Skipping virtual item %s: %s", obj["Title"], obj["Id"]) + return + + try: + obj["ExtraId"] = e_item[0] + obj["FileId"] = e_item[1] + obj["PathId"] = e_item[2] + obj["LibraryId"] = e_item[6] + obj["LibraryName"] = self.jellyfin_db.get_view_name(obj["LibraryId"]) + except TypeError: + update = False + LOG.debug("ExtraId %s not found", obj["Id"]) + + temp_obj = dict(obj) + temp_obj["Id"] = obj["MovieId"] + + library = self.library or find_library(self.server, temp_obj) + if not library: + # This item doesn't belong to a whitelisted library + return + + obj["ExtraId"] = self.create_entry_extra() + obj["LibraryId"] = library["Id"] + obj["LibraryName"] = library["Name"] + + else: + if self.get_extra(*values(obj, QU.get_extra_obj)) is None: + + update = False + LOG.info( + "ExtraId %s missing from kodi. repairing the entry.", + obj["ExtraId"], + ) + + try: + temp_obj = dict(obj) + temp_obj["Id"] = obj["MovieId"] + temp_obj["MovieId"] = self.jellyfin_db.get_item_by_id( + *values(temp_obj, QUEM.get_item_obj) + )[0] + except TypeError: + LOG.info("Failed to process extra %s to movie.", temp_obj["Title"]) + + return update + + obj["Path"] = API.get_file_path(obj["Path"]) + obj["Index"] = obj["Index"] or -1 + obj["Resume"] = API.adjust_resume((obj["Resume"] or 0) / 10000000.0) + obj["Runtime"] = round(float((obj["Runtime"] or 0) / 10000000.0), 6) + obj["DateAdded"] = Local(obj["DateAdded"]).split(".")[0].replace("T", " ") + obj["DatePlayed"] = ( + None + if not obj["DatePlayed"] + else Local(obj["DatePlayed"]).split(".")[0].replace("T", " ") + ) + obj["PlayCount"] = API.get_playcount(obj["Played"], obj["PlayCount"]) + obj["Artwork"] = API.get_all_artwork( + self.objects.map(item, "ArtworkParent"), True + ) + obj["Video"] = API.video_streams(obj["Video"] or [], obj["Container"]) + obj["Audio"] = API.audio_streams(obj["Audio"] or []) + obj["Streams"] = API.media_streams(obj["Video"], obj["Audio"], obj["Subtitles"]) + + self.get_path_filename(obj) + + obj["MovieId"] = temp_obj["MovieId"] + + if obj["Countries"]: + self.add_countries(*values(obj, QU.update_country_obj)) + + tags = list(obj["Tags"] or []) + tags.append(obj["LibraryName"]) + + if obj["Favorite"]: + tags.append("Favorite extras") + + obj["Tags"] = tags + + self.extra_add(obj) + + self.update_file(*values(obj, QU.update_file_obj)) + self.add_playstate(*values(obj, QU.add_bookmark_obj)) + self.add_streams(*values(obj, QU.add_streams_obj)) + self.artwork.add_extra(obj["Artwork"], obj["FileId"], "videoversion") + self.item_ids.append(obj["Id"]) + + return not update + + def extra_add(self, obj): + """Add object to kodi.""" + obj["PathId"] = self.add_path(*values(obj, QU.add_path_obj)) + obj["FileId"] = self.add_file(*values(obj, QU.add_file_obj)) + obj["VideoVersionIdType"] = obj["ExtraId"] + + self.update_extra(*values(obj, QU.update_video_version_extra_obj)) + self.update_video_version_type_extra( + *values(obj, QU.update_video_version_type_extra_obj) + ) + self.jellyfin_db.add_reference(*values(obj, QUEM.add_reference_extra_obj)) + + LOG.debug( + "ADD extra [%s/%s/%s] %s: %s", + obj["PathId"], + obj["FileId"], + obj["ExtraId"], + obj["Id"], + obj["Title"], + ) + + @stop + @jellyfin_item + def remove_extras(self, item, e_item): + """Remove movieid, fileid, jellyfin reference. + Remove artwork, boxset + """ + + try: + item["KodiId"] = e_item[0] + item["FileId"] = e_item[1] + item["Media"] = e_item[4] + except TypeError: + return + + for extra in self.jellyfin_db.get_item_by_parent_id( + *values(item, QUEM.get_item_by_parent_extra_obj) + ): + temp_obj = dict() + temp_obj["Id"] = extra[0] + temp_obj["KodiId"] = extra[1] + temp_obj["FileId"] = extra[2] + + self.delete_extra(*values(temp_obj, QU.delete_extra_obj)) + + self.jellyfin_db.remove_item(*values(temp_obj, QUEM.delete_item_obj)) + + LOG.debug( + "DELETE Extra [%s/%s] %s", + temp_obj["FileId"], + temp_obj["KodiId"], + temp_obj["Id"], + ) + def trailer(self, obj): try: diff --git a/jellyfin_kodi/objects/obj_map.json b/jellyfin_kodi/objects/obj_map.json index e503bf69..e876cc86 100644 --- a/jellyfin_kodi/objects/obj_map.json +++ b/jellyfin_kodi/objects/obj_map.json @@ -54,6 +54,47 @@ "DatePlayed": "UserData/LastPlayedDate", "Played": "UserData/Played" }, + "Extra": { + "Id": "Id", + "MovieId": "MovieId", + "Index": "IndexNumber", + "Title": "MediaSources/0/Name", + "SortTitle": "SortName", + "Path": "Path", + "Location": "LocationType", + "Genres": "Genres", + "UniqueId": "ProviderIds/Imdb", + "Rating": "CommunityRating", + "Year": "ProductionYear", + "Premiere": "PremiereDate,ProductionYear", + "Plot": "Overview", + "People": "People", + "Writers": "People:?Type=Writer$Name", + "Directors": "People:?Type=Director$Name", + "Cast": "People:?Type=Actor$Name", + "Tagline": "Taglines/0", + "Mpaa": "OfficialRating", + "Country": "ProductionLocations/0", + "Countries": "ProductionLocations", + "Studios": "Studios:?$Name", + "Studio": "Studios/0/Name", + "Runtime": "RunTimeTicks,CumulativeRunTimeTicks", + "LocalTrailer": "LocalTrailerCount", + "Trailer": "RemoteTrailers/0/Url", + "DateAdded": "DateCreated", + "Played": "UserData/Played", + "PlayCount": "UserData/PlayCount", + "DatePlayed": "UserData/LastPlayedDate", + "Favorite": "UserData/IsFavorite", + "Resume": "UserData/PlaybackPositionTicks", + "Tags": "Tags", + "Subtitles": "MediaSources/0/MediaStreams:?Type=Subtitle$Language", + "Audio": "MediaSources/0/MediaStreams:?Type=Audio", + "Video": "MediaSources/0/MediaStreams:?Type=Video", + "Container": "MediaSources/0/Container", + "JellyfinParentId": "ParentId", + "CriticRating": "CriticRating" + }, "Boxset": { "Id": "Id", "Title": "Name", @@ -360,4 +401,4 @@ "rating": "CommunityRating", "firstaired": "ProductionYear" } -} +} \ No newline at end of file