Merge pull request #7 from BapRx/main

This commit is contained in:
Luke Arran 2022-02-19 14:17:06 +00:00 committed by GitHub
commit 463afb4d43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 230 additions and 168 deletions

View file

@ -1,18 +1,23 @@
# TV Time to Trakt - Import Script # TV Time to Trakt - Import Script
![](https://loch.digital/image_for_external_apps/4342799-01.png) ![](https://loch.digital/image_for_external_apps/4342799-01.png)
A Python script to import TV Time tracked episode data into Trakt.TV - using data export provided by TV Time through a GDPR request. A Python script to import TV Time tracked episode data into Trakt.TV - using data export provided by TV Time through a GDPR request.
# Issues # Issues
They'll be a few! This was quickly put together within a few hours or so for personal usage. If you come across anything then let me know in the 'Issue' section, and I'll provide support where possible. They'll be a few! This was quickly put together within a few hours or so for personal usage. If you come across anything then let me know in the 'Issue' section, and I'll provide support where possible.
# Notes # Notes
1. The script is using limited data provided from a GDPR request - so the accuracy isn't 100%. But you will be prompted to manually pick the Trakt show, when it can't be determined automatically. 1. The script is using limited data provided from a GDPR request - so the accuracy isn't 100%. But you will be prompted to manually pick the Trakt show, when it can't be determined automatically.
2. A delay of 5 seconds is added between each episode to ensure fair use of Trakt's API server. You should adjust this for your own import, but make sure it's at least 1 second to remain within the rate limit. 2. A delay of 1 second is added between each episode to ensure fair use of Trakt's API server. You can adjust this for your own import, but make sure it's at least 0.75 second to remain within the rate limit: https://trakt.docs.apiary.io/#introduction/rate-limiting
3. Episodes which have been processed will be saved to a TinyDB file `localStorage.json` - when you restart the script, the program will skip those episodes which have been marked 'imported'. 3. Episodes which have been processed will be saved to a TinyDB file `localStorage.json` - when you restart the script, the program will skip those episodes which have been marked 'imported'.
# Setup # Setup
## Get your Data ## Get your Data
TV Time's API is not open. In order to get access to your personal data, you will have to request it from TV Time's support via a GDPR request - or maybe just ask for it, whatever works, it's your data. TV Time's API is not open. In order to get access to your personal data, you will have to request it from TV Time's support via a GDPR request - or maybe just ask for it, whatever works, it's your data.
1. Copy the template provided by [www.datarequests.org](https://www.datarequests.org/blog/sample-letter-gdpr-access-request/) into an email 1. Copy the template provided by [www.datarequests.org](https://www.datarequests.org/blog/sample-letter-gdpr-access-request/) into an email
@ -21,6 +26,7 @@ TV Time's API is not open. In order to get access to your personal data, you wil
4. Extract the data somewhere safe on your local system 4. Extract the data somewhere safe on your local system
## Register API Access at Trakt ## Register API Access at Trakt
1. Go to "Settings" under your profile 1. Go to "Settings" under your profile
2. Select ["Your API Applications"](https://trakt.tv/oauth/applications) 2. Select ["Your API Applications"](https://trakt.tv/oauth/applications)
3. Select "New Application" 3. Select "New Application"
@ -30,11 +36,17 @@ TV Time's API is not open. In order to get access to your personal data, you wil
7. Make note of your details to be used later. 7. Make note of your details to be used later.
## Setup Script ## Setup Script
### Install Required Libraries ### Install Required Libraries
Install the following frameworks via Pip: Install the following frameworks via Pip:
1. `pip install trakt`
2. `pip install tinydb` ```
python -m pip install -r requirements.txt
```
### Setup Configuration ### Setup Configuration
Create a new file named `config.json` in the same directory of `TimeToTrakt.py`, using the below JSON contents (replace the values with your own). Create a new file named `config.json` in the same directory of `TimeToTrakt.py`, using the below JSON contents (replace the values with your own).
``` ```
@ -49,4 +61,5 @@ Create a new file named `config.json` in the same directory of `TimeToTrakt.py`,
Once the config is in place, execute the program using `python TimeToTrakt.py`. The process isn't 100% automated - you will need to pop back, especially with large imports, to check if the script requires a manual user input. Once the config is in place, execute the program using `python TimeToTrakt.py`. The process isn't 100% automated - you will need to pop back, especially with large imports, to check if the script requires a manual user input.
##### Credit ##### Credit
<a href='https://www.freepik.com/vectors/city'>City vector created by freepik - www.freepik.com</a> <a href='https://www.freepik.com/vectors/city'>City vector created by freepik - www.freepik.com</a>

View file

@ -1,37 +1,55 @@
# main.py #!/usr/bin/env python3
from logging import error
import sys
from trakt import *
import trakt.core
import os
import csv import csv
from datetime import datetime
import time
from tinydb import TinyDB, Query
import json import json
import logging
import os
import re import re
import sys import sys
import time
from datetime import datetime
from pathlib import Path
import trakt.core
from tinydb import Query, TinyDB
from trakt import init
from trakt.tv import TVShow from trakt.tv import TVShow
# Setup logger
logging.basicConfig(
format="%(asctime)s [%(levelname)7s] :: %(message)s",
level=logging.INFO,
datefmt="%Y-%m-%d %H:%M:%S",
)
# Adjust this value to increase/decrease your requests between episodes. # Adjust this value to increase/decrease your requests between episodes.
# Make sure it's above 1 seconds to remain within the rate limit. # Make to remain within the rate limit: https://trakt.docs.apiary.io/#introduction/rate-limiting
DELAY_BETWEEN_EPISODES_IN_SECONDS = 5 DELAY_BETWEEN_EPISODES_IN_SECONDS = 1
# Create a database to keep track of completed processes # Create a database to keep track of completed processes
database = TinyDB('localStorage.json') database = TinyDB("localStorage.json")
syncedEpisodesTable = database.table('SyncedEpisodes') syncedEpisodesTable = database.table("SyncedEpisodes")
userMatchedShowsTable = database.table('TvTimeTraktUserMatched') userMatchedShowsTable = database.table("TvTimeTraktUserMatched")
class Expando(object): class Expando(object):
pass pass
def isAuthenticated():
with open(f"{Path.home()}/.pytrakt.json") as f:
data = json.load(f)
daysBeforeExpiration = (
datetime.fromtimestamp(data["OAUTH_EXPIRES_AT"]) - datetime.now()
).days
if daysBeforeExpiration < 1:
return False
return True
def getConfiguration(): def getConfiguration():
configEx = Expando() configEx = Expando()
with open('config.json') as f: with open("config.json") as f:
data = json.load(f) data = json.load(f)
configEx.TRAKT_USERNAME = data["TRAKT_USERNAME"] configEx.TRAKT_USERNAME = data["TRAKT_USERNAME"]
@ -58,9 +76,17 @@ def getFollowedShowsPath():
def initTraktAuth(): def initTraktAuth():
if isAuthenticated():
return True
# Set the method of authentication # Set the method of authentication
trakt.core.AUTH_METHOD = trakt.core.OAUTH_AUTH trakt.core.AUTH_METHOD = trakt.core.OAUTH_AUTH
return init(config.TRAKT_USERNAME, store=True, client_id=config.CLIENT_ID, client_secret=config.CLIENT_SECRET) return init(
config.TRAKT_USERNAME,
store=True,
client_id=config.CLIENT_ID,
client_secret=config.CLIENT_SECRET,
)
# With a given title, check if it contains a year (e.g Doctor Who (2005)) # With a given title, check if it contains a year (e.g Doctor Who (2005))
# and then return this value, with the title and year removed to improve # and then return this value, with the title and year removed to improve
@ -75,18 +101,19 @@ def getYearFromTitle(title):
yearSearch = re.search(r"\(([A-Za-z0-9_]+)\)", title) yearSearch = re.search(r"\(([A-Za-z0-9_]+)\)", title)
yearValue = yearSearch.group(1) yearValue = yearSearch.group(1)
# Then, get the title without the year value included # Then, get the title without the year value included
titleValue = title.split('(')[0].strip() titleValue = title.split("(")[0].strip()
# Put this together into an object # Put this together into an object
ex.titleWithoutYear = titleValue ex.titleWithoutYear = titleValue
ex.yearValue = int(yearValue) ex.yearValue = int(yearValue)
return ex return ex
except: except Exception:
# If the above failed, then the title doesn't include a year # If the above failed, then the title doesn't include a year
# so return the object as is. # so return the object as is.
ex.titleWithoutYear = title ex.titleWithoutYear = title
ex.yearValue = -1 ex.yearValue = -1
return ex return ex
# Shows in TV Time are often different to Trakt.TV - in order to improve results and automation, # Shows in TV Time are often different to Trakt.TV - in order to improve results and automation,
# calculate how many words are in the title, and return true if more than 50% of the title is a match, # calculate how many words are in the title, and return true if more than 50% of the title is a match,
# It seems to improve automation, and reduce manual selection.... # It seems to improve automation, and reduce manual selection....
@ -116,6 +143,7 @@ def checkTitleNameMatch(tvTimeTitle, traktTitle):
# then return the title as a possibility to use # then return the title as a possibility to use
return percentage > 50 return percentage > 50
# Using TV Time data (Name of Show, Season No and Episode) - find the corresponding show # Using TV Time data (Name of Show, Season No and Episode) - find the corresponding show
# in Trakt.TV either by automation, or asking the user to confirm. # in Trakt.TV either by automation, or asking the user to confirm.
@ -145,7 +173,7 @@ def getShowByName(name, seasonNo, episodeNo):
if checkTitleNameMatch(name, show.title): if checkTitleNameMatch(name, show.title):
# If the title included the year of broadcast, then we can be more picky in the results # If the title included the year of broadcast, then we can be more picky in the results
# to look for a show with a broadcast year that matches # to look for a show with a broadcast year that matches
if doesTitleIncludeYear == True: if doesTitleIncludeYear:
# If the show title is a 1:1 match, with the same broadcast year, then bingo! # If the show title is a 1:1 match, with the same broadcast year, then bingo!
if (name == show.title) and (show.year == titleObj.yearValue): if (name == show.title) and (show.year == titleObj.yearValue):
# Clear previous results, and only use this one # Clear previous results, and only use this one
@ -168,7 +196,7 @@ def getShowByName(name, seasonNo, episodeNo):
if nameFromSearch.title == name: if nameFromSearch.title == name:
completeMatchNames.append(nameFromSearch) completeMatchNames.append(nameFromSearch)
if (len(completeMatchNames) == 1): if len(completeMatchNames) == 1:
showsWithSameName = completeMatchNames showsWithSameName = completeMatchNames
# If the search contains multiple results, then we need to confirm with the user which show # If the search contains multiple results, then we need to confirm with the user which show
@ -178,8 +206,7 @@ def getShowByName(name, seasonNo, episodeNo):
# Query the local database for existing selection # Query the local database for existing selection
userMatchedQuery = Query() userMatchedQuery = Query()
queryResult = userMatchedShowsTable.search( queryResult = userMatchedShowsTable.search(userMatchedQuery.ShowName == name)
userMatchedQuery.ShowName == name)
# If the local database already contains an entry for a manual selection # If the local database already contains an entry for a manual selection
# then don't bother prompting the user to select it again! # then don't bother prompting the user to select it again!
@ -187,12 +214,12 @@ def getShowByName(name, seasonNo, episodeNo):
# Get the first result from the query # Get the first result from the query
firstMatch = queryResult[0] firstMatch = queryResult[0]
# Get the value contains the selection index # Get the value contains the selection index
firstMatchSelectedIndex = int(firstMatch.get('UserSelectedIndex')) firstMatchSelectedIndex = int(firstMatch.get("UserSelectedIndex"))
# Check if the user previously requested to skip the show # Check if the user previously requested to skip the show
skipShow = firstMatch.get('SkipShow') skipShow = firstMatch.get("SkipShow")
# If the user did not skip, but provided an index selection, get the # If the user did not skip, but provided an index selection, get the
# matching show # matching show
if skipShow == False: if not skipShow:
return showsWithSameName[firstMatchSelectedIndex] return showsWithSameName[firstMatchSelectedIndex]
# Otherwise, return None, which will trigger the script to skip # Otherwise, return None, which will trigger the script to skip
# and move onto the next show # and move onto the next show
@ -202,22 +229,25 @@ def getShowByName(name, seasonNo, episodeNo):
# then prompt the user to make a selection # then prompt the user to make a selection
else: else:
print( print(
f"INFO - MANUAL INPUT REQUIRED: The TV Time data for Show '{name}' (Season {seasonNo}, Episode {episodeNo}) has {len(showsWithSameName)} matching Trakt shows with the same name.") f"INFO - MANUAL INPUT REQUIRED: The TV Time data for Show '{name}' (Season {seasonNo}, Episode {episodeNo}) has {len(showsWithSameName)} matching Trakt shows with the same name."
)
# Output each show for manual selection # Output each show for manual selection
for idx, item in enumerate(showsWithSameName): for idx, item in enumerate(showsWithSameName):
# Display the show's title, broadcast year, amount of seasons and a link to the Trakt page. # Display the show's title, broadcast year, amount of seasons and a link to the Trakt page.
# This will provide the user with enough information to make a selection. # This will provide the user with enough information to make a selection.
print( print(
f" ({idx + 1}) {item.title} - {item.year} - {len(item.seasons)} Season(s) - More Info: https://trakt.tv/{item.ext}") f" ({idx + 1}) {item.title} - {item.year} - {len(item.seasons)} Season(s) - More Info: https://trakt.tv/{item.ext}"
)
while(True): while True:
try: try:
# Get the user's selection, either a numerical input, or a string 'SKIP' value # Get the user's selection, either a numerical input, or a string 'SKIP' value
indexSelected = (input( indexSelected = input(
f"Please make a selection from above (or enter SKIP):")) "Please make a selection from above (or enter SKIP):"
)
if indexSelected != 'SKIP': if indexSelected != "SKIP":
# Since the value isn't 'skip', check that the result is numerical # Since the value isn't 'skip', check that the result is numerical
indexSelected = int(indexSelected) - 1 indexSelected = int(indexSelected) - 1
# Exit the selection loop # Exit the selection loop
@ -229,17 +259,19 @@ def getShowByName(name, seasonNo, episodeNo):
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit("Cancel requested...") sys.exit("Cancel requested...")
# Otherwise, the user has entered an invalid value, warn the user to try again # Otherwise, the user has entered an invalid value, warn the user to try again
except: except Exception:
print( logging.error(
f"Sorry! Please select a value between 0 to {len(showsWithSameName)}") f"Sorry! Please select a value between 0 to {len(showsWithSameName)}"
)
# If the user entered 'SKIP', then exit from the loop with no selection, which # If the user entered 'SKIP', then exit from the loop with no selection, which
# will trigger the program to move onto the next episode # will trigger the program to move onto the next episode
if (indexSelected == 'SKIP'): if indexSelected == "SKIP":
# Record that the user has skipped the TV Show for import, so that # Record that the user has skipped the TV Show for import, so that
# manual input isn't required everytime # manual input isn't required everytime
userMatchedShowsTable.insert( userMatchedShowsTable.insert(
{'ShowName': name, 'UserSelectedIndex': 0, 'SkipShow': True}) {"ShowName": name, "UserSelectedIndex": 0, "SkipShow": True}
)
return None return None
# Otherwise, return the selection which the user made from the list # Otherwise, return the selection which the user made from the list
@ -247,18 +279,24 @@ def getShowByName(name, seasonNo, episodeNo):
selectedShow = showsWithSameName[int(indexSelected)] selectedShow = showsWithSameName[int(indexSelected)]
userMatchedShowsTable.insert( userMatchedShowsTable.insert(
{'ShowName': name, 'UserSelectedIndex': indexSelected, 'SkipShow': False}) {
"ShowName": name,
"UserSelectedIndex": indexSelected,
"SkipShow": False,
}
)
return selectedShow return selectedShow
else: else:
if (len(showsWithSameName) > 0): if len(showsWithSameName) > 0:
# If the search returned only one result, then awesome! # If the search returned only one result, then awesome!
# Return the show, so the import automation can continue. # Return the show, so the import automation can continue.
return showsWithSameName[0] return showsWithSameName[0]
else: else:
return None return None
# Since the Trakt.Py starts the indexing of seasons in the array from 0 (e.g Season 1 in Index 0), then # Since the Trakt.Py starts the indexing of seasons in the array from 0 (e.g Season 1 in Index 0), then
# subtract the TV Time numerical value by 1 so it starts from 0 as well. However, when a TV series includes # subtract the TV Time numerical value by 1 so it starts from 0 as well. However, when a TV series includes
# a 'special' season, Trakt.Py will place this as the first season in the array - so, don't subtract, since # a 'special' season, Trakt.Py will place this as the first season in the array - so, don't subtract, since
@ -279,7 +317,7 @@ def parseSeasonNo(seasonNo, traktShowObj):
# Otherwise, if the Trakt seasons start with no specials, then return the seasonNo, # Otherwise, if the Trakt seasons start with no specials, then return the seasonNo,
# but subtracted by one (e.g Season 1 in TV Time, will be 0) # but subtracted by one (e.g Season 1 in TV Time, will be 0)
else: else:
# Only subtract is the TV Time season number is greater than 0. # Only subtract if the TV Time season number is greater than 0.
if seasonNo != 0: if seasonNo != 0:
return seasonNo - 1 return seasonNo - 1
# Otherwise, the TV Time season is a special! Then you don't need to change the starting position # Otherwise, the TV Time season is a special! Then you don't need to change the starting position
@ -291,157 +329,161 @@ def processWatchedShows():
# Total amount of rows which have been processed in the CSV file # Total amount of rows which have been processed in the CSV file
rowsCount = 0 rowsCount = 0
# Total amount of rows in the CSV file # Total amount of rows in the CSV file
rowsTotal = 0
# Total amount of errors which have occurred in one streak
errorStreak = 0 errorStreak = 0
# Get the total amount of rows in the CSV file,
# which is helpful for keeping track of progress.
# However, if you have a VERY large CSV file (e.g above 100,000 rows)
# then it might be a good idea to remove this due to the performance
# overhead.
with open(getWatchedShowsPath()) as f:
rowsTotal = sum(1 for line in f)
# Open the CSV file within the GDPR exported data # Open the CSV file within the GDPR exported data
with open(getWatchedShowsPath(), newline='') as csvfile: with open(getWatchedShowsPath(), newline="") as csvfile:
# Create the CSV reader, which will break up the fields using the delimiter ',' # Create the CSV reader, which will break up the fields using the delimiter ','
showsReader = csv.reader(csvfile, delimiter=',') showsReader = csv.DictReader(csvfile, delimiter=",")
# Get the total amount of rows in the CSV file,
rowsTotal = len(list(showsReader))
# Move position to the beginning of the file
csvfile.seek(0, 0)
# Loop through each line/record of the CSV file # Loop through each line/record of the CSV file
for row in showsReader: # Ignore the header row
# Increment the row counter to keep track of progress completing the next(showsReader, None)
# records during the import process. for rowsCount, row in enumerate(showsReader):
rowsCount += 1
# Get the name of the TV show # Get the name of the TV show
tvShowName = row[8] tvShowName = row["tv_show_name"]
# Get the TV Time Episode Id
tvShowEpisodeId = row["episode_id"]
# Get the TV Time Season Number
tvShowSeasonNo = row["episode_season_number"]
# Get the TV Time Episode Number
tvShowEpisodeNo = row["episode_number"]
# Get the date which the show was marked 'watched' in TV Time
tvShowDateWatched = row["updated_at"]
# Parse the watched date value into a Python type
tvShowDateWatchedConverted = datetime.strptime(
tvShowDateWatched, "%Y-%m-%d %H:%M:%S"
)
# Ignore the header row # Query the local database for previous entries indicating that
if rowsCount > 1: # the episode has already been imported in the past. Which will
# Get the TV Time Episode Id # ease pressure on TV Time's API server during a retry of the import
tvShowEpisodeId = row[4] # process, and just save time overall without needing to create network requests
# Get the TV Time Season Number episodeCompletedQuery = Query()
tvShowSeasonNo = row[5] queryResult = syncedEpisodesTable.search(
# Get the TV Time Episode Number episodeCompletedQuery.episodeId == tvShowEpisodeId
tvShowEpisodeNo = row[6] )
# Get the date which the show was marked 'watched' in TV Time
tvShowDateWatched = row[7]
# Parse the watched date value into a Python type
tvShowDateWatchedConverted = datetime.strptime(
tvShowDateWatched, '%Y-%m-%d %H:%M:%S')
# Query the local database for previous entries indicating that # If the query returned no results, then continue to import it into Trakt
# the episode has already been imported in the past. Which will if len(queryResult) == 0:
# ease pressure on TV Time's API server during a retry of the import # Create a repeating loop, which will break on success, but repeats on failures
# process, and just save time overall without needing to create network requests while True:
episodeCompletedQuery = Query() # If more than 10 errors occurred in one streak, whilst trying to import the episode
queryResult = syncedEpisodesTable.search( # then give up, and move onto the next episode, but warn the user.
episodeCompletedQuery.episodeId == tvShowEpisodeId) if errorStreak > 10:
logging.warning(
# If the query returned no results, then continue to import it into Trakt "An error occurred 10 times in a row... skipping episode..."
if len(queryResult) == 0: )
# Create a repeating loop, which will break on success, but repeats on failures break
while True: try:
# If more than 10 errors occurred in one streak, whilst trying to import the episode # Sleep for a second between each process, before going onto the next watched episode.
# then give up, and move onto the next episode, but warn the user. # This is required to remain within the API rate limit, and use the API server fairly.
if (errorStreak > 10): # Other developers share the service, for free - so be considerate of your usage.
print( time.sleep(DELAY_BETWEEN_EPISODES_IN_SECONDS)
f"WARNING: An error occurred 10 times in a row... skipping episode...") # Search Trakt for the TV show matching TV Time's title value
traktShowObj = getShowByName(
tvShowName, tvShowSeasonNo, tvShowEpisodeNo
)
# If the method returned 'None', then this is an indication to skip the episode, and
# move onto the next one
if traktShowObj is None:
break break
try: # Show the progress of the import on-screen
# Sleep for a second between each process, before going onto the next watched episode. logging.info(
# This is required to remain within the API rate limit, and use the API server fairly. f"({rowsCount+1}/{rowsTotal}) - Processing '{tvShowName}' Season {tvShowSeasonNo} / Episode {tvShowEpisodeNo}"
# Other developers share the service, for free - so be considerate of your usage. )
time.sleep(DELAY_BETWEEN_EPISODES_IN_SECONDS) # Get the season from the Trakt API
# Search Trakt for the TV show matching TV Time's title value season = traktShowObj.seasons[
traktShowObj = getShowByName( parseSeasonNo(tvShowSeasonNo, traktShowObj)
tvShowName, tvShowSeasonNo, tvShowEpisodeNo) ]
# If the method returned 'None', then this is an indication to skip the episode, and # Get the episode from the season
# move onto the next one episode = season.episodes[int(tvShowEpisodeNo) - 1]
if traktShowObj == None: # Mark the episode as watched!
break episode.mark_as_seen(tvShowDateWatchedConverted)
# Show the progress of the import on-screen # Add the episode to the local database as imported, so it can be skipped,
print( # if the process is repeated
f"({rowsCount}/{rowsTotal}) Processing Show {tvShowName} on Season {tvShowSeasonNo} - Episode {tvShowEpisodeNo}") syncedEpisodesTable.insert({"episodeId": tvShowEpisodeId})
# Get the season from the Trakt API # Clear the error streak on completing the method without errors
season = traktShowObj.seasons[parseSeasonNo( errorStreak = 0
tvShowSeasonNo, traktShowObj)] break
# Get the episode from the season # Catch errors which occur because of an incorrect array index. This occurs when
episode = season.episodes[int(tvShowEpisodeNo) - 1] # an incorrect Trakt show has been selected, with season/episodes which don't match TV Time.
# Mark the episode as watched! # It can also occur due to a bug in Trakt Py, whereby some seasons contain an empty array of episodes.
episode.mark_as_seen(tvShowDateWatchedConverted) except IndexError:
# Add the episode to the local database as imported, so it can be skipped, tvShowSlug = traktShowObj.to_json()["shows"][0]["ids"]["ids"][
# if the process is repeated "slug"
syncedEpisodesTable.insert( ]
{'episodeId': tvShowEpisodeId}) logging.warning(
# Clear the error streak on completing the method without errors f"({rowsCount}/{rowsTotal}) - {tvShowName} Season {tvShowSeasonNo}, Episode {tvShowEpisodeNo} does not exist in Trakt! (https://trakt.tv/shows/{tvShowSlug}/seasons/{tvShowSeasonNo}/episodes/{tvShowEpisodeNo})"
errorStreak = 0 )
break break
# Catch errors which occur because of an incorrect array index. This occurs when # Catch any errors which are raised because a show could not be found in Trakt
# an incorrect Trakt show has been selected, with season/episodes which don't match TV Time. except trakt.errors.NotFoundException:
# It can also occur due to a bug in Trakt Py, whereby some seasons contain an empty array of episodes. logging.warning(
except IndexError: f"({rowsCount}/{rowsTotal}) - {tvShowName} Season {tvShowSeasonNo}, Episode {tvShowEpisodeNo} does not exist (search) in Trakt!"
print( )
f"({rowsCount}/{rowsTotal}) WARNING: {tvShowName} Season {tvShowSeasonNo}, Episode {tvShowEpisodeNo} does not exist (season/episode index) in Trakt!") break
break # Catch errors because of the program breaching the Trakt API rate limit
# Catch any errors which are raised because a show could not be found in Trakt except trakt.errors.RateLimitException:
except trakt.errors.NotFoundException: logging.warning(
print( "The program is running too quickly and has hit Trakt's API rate limit! Please increase the delay between "
f"({rowsCount}/{rowsTotal}) WARNING: {tvShowName} Season {tvShowSeasonNo}, Episode {tvShowEpisodeNo} does not exist (search) in Trakt!") + "episdoes via the variable 'DELAY_BETWEEN_EPISODES_IN_SECONDS'. The program will now wait 60 seconds before "
break + "trying again."
# Catch errors because of the program breaching the Trakt API rate limit )
except trakt.errors.RateLimitException: time.sleep(60)
print(
"WARNING: The program is running too quickly and has hit Trakt's API rate limit! Please increase the delay between " +
"episdoes via the variable 'DELAY_BETWEEN_EPISODES_IN_SECONDS'. The program will now wait 60 seconds before " +
"trying again.")
time.sleep(60)
# Mark the exception in the error streak # Mark the exception in the error streak
errorStreak += 1 errorStreak += 1
# Catch a JSON decode error - this can be raised when the API server is down and produces a HTML page, instead of JSON # Catch a JSON decode error - this can be raised when the API server is down and produces a HTML page, instead of JSON
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
print( logging.warning(
f"({rowsCount}/{rowsTotal}) WARNING: A JSON decode error occuring whilst processing {tvShowName} " + f"({rowsCount}/{rowsTotal}) - A JSON decode error occuring whilst processing {tvShowName} "
f"Season {tvShowSeasonNo}, Episode {tvShowEpisodeNo}! This might occur when the server is down and has produced " + + f"Season {tvShowSeasonNo}, Episode {tvShowEpisodeNo}! This might occur when the server is down and has produced "
"a HTML document instead of JSON. The script will wait 60 seconds before trying again.") + "a HTML document instead of JSON. The script will wait 60 seconds before trying again."
)
# Wait 60 seconds # Wait 60 seconds
time.sleep(60) time.sleep(60)
# Mark the exception in the error streak # Mark the exception in the error streak
errorStreak += 1 errorStreak += 1
# Catch a CTRL + C keyboard input, and exits the program # Catch a CTRL + C keyboard input, and exits the program
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit("Cancel requested...") sys.exit("Cancel requested...")
# Skip the episode # Skip the episode
else: else:
print( logging.info(
f"({rowsCount}/{rowsTotal}) Skipping '{tvShowName}' Season {tvShowSeasonNo} Episode {tvShowEpisodeNo}. It's already been imported.") f"({rowsCount}/{rowsTotal}) - Already imported, skipping '{tvShowName}' Season {tvShowSeasonNo} / Episode {tvShowEpisodeNo}."
)
def start(): def start():
# Create the initial authentication with Trakt, before starting the process # Create the initial authentication with Trakt, before starting the process
if initTraktAuth(): if initTraktAuth():
# Display a menu selection # Display a menu selection
print(f">> What do you want to do?") print(">> What do you want to do?")
print(f" 1) Import Watch History from TV Time") print(" 1) Import Watch History from TV Time")
while True: while True:
try: try:
menuSelection = int(input(f"Enter your menu selection: ")) menuSelection = input("Enter your menu selection: ")
menuSelection = 1 if not menuSelection else int(menuSelection)
break break
except ValueError: except ValueError:
print("Invalid input. Please enter a numerical number.") logging.warning("Invalid input. Please enter a numerical number.")
# Start the process which is required # Start the process which is required
if menuSelection == 1: if menuSelection == 1:
# Invoke the method which will import episodes which have been watched # Invoke the method which will import episodes which have been watched
# from TV Time into Trakt # from TV Time into Trakt
processWatchedShows() processWatchedShows()
else: else:
print("Sorry - that's an unknown menu selection") logging.warning("Sorry - that's an unknown menu selection")
else: else:
print("ERROR: Unable to complete authentication to Trakt - please try again.") logging.error(
"ERROR: Unable to complete authentication to Trakt - please try again."
)
if __name__ == "__main__": if __name__ == "__main__":
@ -451,7 +493,12 @@ if __name__ == "__main__":
if os.path.isdir(config.GDPR_WORKSPACE_PATH): if os.path.isdir(config.GDPR_WORKSPACE_PATH):
start() start()
else: else:
print("Oops! The TV Time GDPR folder '" + config.GDPR_WORKSPACE_PATH + logging.error(
"' does not exist on the local system. Please check it, and try again.") "Oops! The TV Time GDPR folder '"
+ config.GDPR_WORKSPACE_PATH
+ "' does not exist on the local system. Please check it, and try again."
)
else: else:
print(f"ERROR: The 'config.json' file cannot be found - have you created it yet?") logging.error(
"The 'config.json' file cannot be found - have you created it yet?"
)

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
trakt==3.4.0
tinydb==4.6.1