Skip to content

Instantly share code, notes, and snippets.

@NicolaSmaniotto
Created December 12, 2024 10:27
Show Gist options
  • Save NicolaSmaniotto/aff7d01ea7055c111c85ce1828fc7ed4 to your computer and use it in GitHub Desktop.
Save NicolaSmaniotto/aff7d01ea7055c111c85ce1828fc7ed4 to your computer and use it in GitHub Desktop.
Jellyfin date fixer
#!/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())
@NicolaSmaniotto
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment