Created
December 12, 2024 10:27
-
-
Save NicolaSmaniotto/aff7d01ea7055c111c85ce1828fc7ed4 to your computer and use it in GitHub Desktop.
Jellyfin date fixer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Jellyfin date fixer | |
Nicola Smaniotto <[email protected]> | |
Runs hourly checks to correct the addition dates of modified movies and episodes. | |
Keeps an external database to compare the dates to. | |
Usage: python jellyfin-date-fixer.py | |
""" | |
import asyncio | |
import requests | |
import sqlite3 | |
import logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
JELLYFIN_URL = "http://your-jellyfin-url:8096" | |
API_KEY = "0123456789abcdef123456789abcdef" | |
DATABASE = "/date_backup.db" | |
def get_movies(): | |
headers = {"X-Emby-Token": API_KEY} | |
response = requests.get( | |
f"{JELLYFIN_URL}/Items", | |
headers=headers, | |
params={ | |
"IncludeItemTypes": "Movie", | |
"Recursive": True, | |
"Fields": "DateCreated, Id, ProductionYear", | |
"SortBy": "Random", # if there are multiple copies of the same movie | |
}, | |
) | |
response.raise_for_status() | |
dates = {} | |
for item in response.json()["Items"]: | |
title = item["Name"] | |
year = item["ProductionYear"] | |
dates[(title, year)] = { | |
"id": item["Id"], | |
"added": item["DateCreated"], | |
} | |
return dates | |
def get_episodes(): | |
headers = {"X-Emby-Token": API_KEY} | |
response = requests.get( | |
f"{JELLYFIN_URL}/Items", | |
headers=headers, | |
params={ | |
"IncludeItemTypes": "Episode", | |
"Recursive": True, | |
"Fields": "DateCreated, Id, ProductionYear, SeriesName, SeasonName", | |
"SortBy": "Random", # if there are multiple copies of the same movie | |
}, | |
) | |
response.raise_for_status() | |
dates = {} | |
for item in response.json()["Items"]: | |
series = item["SeriesName"] | |
season = item["SeasonName"] | |
title = item["Name"] | |
year = item["ProductionYear"] | |
dates[(series, season, title, year)] = { | |
"id": item["Id"], | |
"added": item["DateCreated"], | |
} | |
return dates | |
def set_date(id, date): | |
headers = {"X-Emby-Token": API_KEY} | |
# The API requires we pass back ALL fields, so we first have to request them all | |
response = requests.get( | |
f"{JELLYFIN_URL}/Items", | |
headers=headers, | |
params={ | |
"Ids": [id], | |
"Fields": "AirTime, CanDelete, CanDownload, ChannelInfo, Chapters, Trickplay, ChildCount, CumulativeRunTimeTicks, CustomRating, DateCreated, DateLastMediaAdded, DisplayPreferencesId, Etag, ExternalUrls, Genres, HomePageUrl, ItemCounts, MediaSourceCount, MediaSources, OriginalTitle, Overview, ParentId, Path, People, PlayAccess, ProductionLocations, ProviderIds, PrimaryImageAspectRatio, RecursiveItemCount, Settings, ScreenshotImageTags, SeriesPrimaryImage, SeriesStudio, SortName, SpecialEpisodeNumbers, Studios, Taglines, Tags, RemoteTrailers, MediaStreams, SeasonUserData, ServiceName, ThemeSongIds, ThemeVideoIds, ExternalEtag, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, SeriesPresentationUniqueKey, DateLastRefreshed, DateLastSaved, RefreshState, ChannelImage, EnableMediaSourceDisplay, Width, Height, ExtraIds, LocalTrailerCount, IsHD, SpecialFeatureCount", | |
}, | |
) | |
response.raise_for_status() | |
original = response.json()["Items"][0] | |
original["DateCreated"] = date | |
response = requests.post( | |
f"{JELLYFIN_URL}/Items/{id}", headers=headers, json=original | |
) | |
return True | |
def fix_movies(): | |
# Open the db file | |
con = sqlite3.connect(DATABASE) | |
cur = con.cursor() | |
# Get the dates from jellyfin | |
on_jellyfin = get_movies() | |
# Get the dates from the backup | |
in_database = cur.execute("SELECT * FROM movies").fetchall() | |
# Find the items that have been removed | |
for title, year, added in in_database: | |
logger.debug(f"Checking movie {title} {year}, with database date {added}") | |
if (title, year) not in on_jellyfin: | |
# The item has been removed | |
logger.info( | |
f"Movie {title} {year} has been removed from jellyfin, deleting backup" | |
) | |
cur.execute( | |
"DELETE FROM movies WHERE title = ? AND year = ?", (title, year) | |
) | |
con.commit() | |
else: | |
# The item is still present | |
id = on_jellyfin[(title, year)]["id"] | |
if added < on_jellyfin[(title, year)]["added"]: | |
# The item has been modified on jellyfin | |
logger.warning( | |
f"Movie {title} {year} ({id}) has been modified on jellyfin, correcting date from backup" | |
) | |
set_date(id, added) | |
elif added > on_jellyfin[(title, year)]["added"]: | |
# The item has been modified on jellyfin | |
logger.info( | |
f"Movie {title} {year} ({id}) is older than the backup, caching new date" | |
) | |
cur.execute( | |
"UPDATE movies SET added = ? WHERE title = ? AND year = ?", | |
(on_jellyfin[(title, year)]["added"], title, year), | |
) | |
con.commit() | |
# The item has been updated | |
del on_jellyfin[(title, year)] | |
# Save all the new items | |
for title, year in on_jellyfin: | |
id = on_jellyfin[(title, year)]["id"] | |
added = on_jellyfin[(title, year)]["added"] | |
logger.info(f"Saving date for movie {title} {year} ({id})") | |
cur.execute("INSERT INTO movies VALUES (?, ?, ?)", (title, year, added)) | |
con.commit() | |
def fix_episodes(): | |
# Open the db file | |
con = sqlite3.connect(DATABASE) | |
cur = con.cursor() | |
# Get the dates from jellyfin | |
on_jellyfin = get_episodes() | |
# Get the dates from the backup | |
in_database = cur.execute("SELECT * FROM episodes").fetchall() | |
# Find the items that have been removed | |
for series, season, title, year, added in in_database: | |
logger.debug( | |
f"Checking episode {series} {season} {title} {year}, with database date {added}" | |
) | |
if (series, season, title, year) not in on_jellyfin: | |
# The item has been removed | |
logger.info( | |
f"Episode {series} {season} {title} {year} has been removed from jellyfin, deleting backup" | |
) | |
cur.execute( | |
"DELETE FROM episodes WHERE series = ? AND season = ? AND title = ? AND year = ?", | |
(series, season, title, year), | |
) | |
con.commit() | |
else: | |
# The item is still present | |
id = on_jellyfin[(series, season, title, year)]["id"] | |
if added < on_jellyfin[(series, season, title, year)]["added"]: | |
# The item has been modified on jellyfin | |
logger.warning( | |
f"Episode {series} {season} {title} {year} ({id}) has been modified on jellyfin, correcting date from backup" | |
) | |
set_date(id, added) | |
elif added > on_jellyfin[(series, season, title, year)]["added"]: | |
# The item has been modified on jellyfin | |
logger.info( | |
f"Episode {series} {season} {title} {year} ({id}) is older than the backup, caching new date" | |
) | |
cur.execute( | |
"UPDATE episodes SET added = ? WHERE series = ? AND season = ? AND title = ? AND year = ?", | |
( | |
on_jellyfin[(series, season, title, year)]["added"], | |
series, | |
season, | |
title, | |
year, | |
), | |
) | |
con.commit() | |
# The item has been updated | |
del on_jellyfin[(series, season, title, year)] | |
# Save all the new items | |
for series, season, title, year in on_jellyfin: | |
id = on_jellyfin[(series, season, title, year)]["id"] | |
added = on_jellyfin[(series, season, title, year)]["added"] | |
logger.info(f"Saving date for {series} {season} {title} {year} ({id})") | |
cur.execute( | |
"INSERT INTO episodes VALUES (?, ?, ?, ?, ?)", | |
(series, season, title, year, added), | |
) | |
con.commit() | |
async def main(): | |
# Create databases if they are missing | |
con = sqlite3.connect(DATABASE) | |
cur = con.cursor() | |
# Check the movies table | |
exists = cur.execute("SELECT name FROM sqlite_master WHERE name='movies'") | |
if exists.fetchone() is None: | |
# The table does not exist, create it | |
cur.execute( | |
"CREATE TABLE movies(title varchar, year int, added datetime, PRIMARY KEY (title, year))" | |
) | |
logger.warning("Created a new movie table") | |
# Check the episodes table | |
exists = cur.execute("SELECT name FROM sqlite_master WHERE name='episodes'") | |
if exists.fetchone() is None: | |
# The table does not exist, create it | |
cur.execute( | |
"CREATE TABLE episodes(series varchar, season varchar, title varchar, year int, added datetime, PRIMARY KEY (series, season, title, year))" | |
) | |
logger.warning("Created a new episode table") | |
# Main loop | |
while True: | |
fix_movies() | |
fix_episodes() | |
await asyncio.sleep(3600) | |
if __name__ == "__main__": | |
asyncio.run(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've given the script its own repository. It handles a few edge cases and can be installed with docker.
https://gitlab.com/smaniottonicola/jellyfin-date-fixer