def get_plex_server(): plex_token = CONFIG["PLEX_TOKEN"] plex_baseurl = CONFIG["PLEX_BASEURL"] plex_fallbackurl = CONFIG["PLEX_FALLBACKURL"] if plex_token == '-': plex_token = "" server = None # if connection fails, it will try : # 1. url expected by new ssl certificate # 2. url without ssl # 3. fallback url (localhost) try: server = plexapi.server.PlexServer( token=plex_token, baseurl=plex_baseurl) except plexapi.server.requests.exceptions.SSLError as e: m = "Plex connection error: {}, fallback url {} didn't respond either.".format(str(e), plex_fallbackurl) excep_msg = str(e.__context__) if "doesn't match '*." in excep_msg: hash_pos = excep_msg.find("*.") + 2 new_hash = excep_msg[hash_pos:hash_pos + 32] end_pos = plex_baseurl.find(".plex.direct") new_plex_baseurl = plex_baseurl[:end_pos - 32] + new_hash + plex_baseurl[end_pos:] try: # 1 server = plexapi.server.PlexServer( token=plex_token, baseurl=new_plex_baseurl) # save new url to .env with open(env_file, 'w') as txt: txt.write("PLEX_USERNAME="******"\n") txt.write("PLEX_TOKEN=" + plex_token + "\n") txt.write("PLEX_BASEURL=" + new_plex_baseurl + "\n") txt.write("PLEX_FALLBACKURL=" + plex_fallbackurl + "\n") txt.write("TRAKT_USERNAME="******"\n") logging.info("Plex server url changed to {}".format(new_plex_baseurl)) except Exception: pass if server is None and plex_baseurl[:5] == "https": new_plex_baseurl = plex_baseurl.replace("https", "http") try: # 2 server = plexapi.server.PlexServer( token=plex_token, baseurl=new_plex_baseurl) logging.warning("Switched to Plex unsecure connection because of SSLError.") except Exception: pass except Exception as e: m = "Plex connection error: {}, fallback url {} didn't respond either.".format(str(e), plex_fallbackurl) pass if server is None: try: # 3 server = plexapi.server.PlexServer( token=plex_token, baseurl=plex_fallbackurl) logging.warning("No response from {}, fallback to {}".format(plex_baseurl, plex_fallbackurl)) except Exception: logging.error(m) print(m) exit(1) return server
def process_movie_section(s, watched_set, ratings_dict, listutil, collection): # args: a section of plex movies, a set comprised of the trakt ids of all watched movies and a dict with key=slug and value=rating (1-10) ############### # Sync movies with trakt ############### with requests_cache.disabled(): allMovies = s.all() logging.info( "Now working on movie section {} containing {} elements".format( s.title, len(allMovies))) for movie in allMovies: # find id to search movie guid = movie.guid if guid.startswith('plex://movie/'): if len(movie.guids) > 0: logging.debug("trying first alternative guid: " + str(movie.guids[0].id)) guid = movie.guids[0].id x = provider = None if guid.startswith('local') or 'agents.none' in guid: # ignore this guid, it's not matched logging.warning( "Movie [{} ({})]: GUID ({}) is local or none, ignoring".format( movie.title, movie.year, guid)) continue elif 'imdb' in guid: x = guid.split('//')[1] x = x.split('?')[0] provider = 'imdb' elif 'themoviedb' in guid or 'tmdb' in guid: x = guid.split('//')[1] x = x.split('?')[0] provider = 'tmdb' elif 'xbmcnfo' in guid: x = guid.split('//')[1] x = x.split('?')[0] provider = CONFIG['xbmc-providers']['movies'] else: logging.error('Movie [{} ({})]: Unrecognized GUID {}'.format( movie.title, movie.year, movie.guid)) continue # search and sync movie try: search = trakt.sync.search_by_id(x, id_type=provider) m = None # look for the first movie in the results for result in search: if type(result) is trakt.movies.Movie: m = result break if m is None: logging.error('Movie [{} ({})]: Not found. Aborting'.format( movie.title, movie.year)) continue last_time = time() if CONFIG['sync']['collection']: # add to collection if necessary if m.trakt not in collection: retry = 0 while retry < 5: try: last_time = respect_trakt_rate(last_time) m.add_to_library() logging.info( 'Movie [{} ({})]: Added to trakt collection'. format(movie.title, movie.year)) break except trakt.errors.RateLimitException as e: delay = int( e.response.headers.get("Retry-After", 1)) logging.warning( "Movie [{} ({})]: Rate Limited on adding to collection. Sleeping {} sec from trakt (GUID: {})" .format(movie.title, movie.year, delay, guid)) sleep(delay) retry += retry if retry == 5: logging.warning( "Movie [{} ({})]: Rate Limited 5 times on watched update. Abort trakt request." .format(movie.title, movie.year)) # compare ratings if CONFIG['sync']['ratings']: if m.slug in ratings_dict: trakt_rating = int(ratings_dict[m.slug]) else: trakt_rating = None plex_rating = int( movie.userRating) if movie.userRating is not None else None identical = plex_rating is trakt_rating # plex rating takes precedence over trakt rating if plex_rating is not None and not identical: retry = 0 while retry < 5: try: last_time = respect_trakt_rate(last_time) with requests_cache.disabled(): m.rate(plex_rating) logging.info( "Movie [{} ({})]: Rating with {} on trakt". format(movie.title, movie.year, plex_rating)) break except trakt.errors.RateLimitException as e: delay = int( e.response.headers.get("Retry-After", 1)) logging.warning( "Movie [{} ({})]: Rate Limited on rating update. Sleeping {} sec from trakt (GUID: {})" .format(movie.title, movie.year, delay, guid)) sleep(delay) retry += retry if retry == 5: logging.warning( "Movie [{} ({})]: Rate Limited 5 times on watched update. Abort trakt request." .format(movie.title, movie.year)) elif trakt_rating is not None and not identical: with requests_cache.disabled(): movie.rate(trakt_rating) logging.info( "Movie [{} ({})]: Rating with {} on plex".format( movie.title, movie.year, trakt_rating)) # sync watch status if CONFIG['sync']['watched_status']: watchedOnPlex = movie.isWatched watchedOnTrakt = m.trakt in watched_set if watchedOnPlex is not watchedOnTrakt: # if watch status is not synced # send watched status from plex to trakt if watchedOnPlex: retry = 0 while retry < 5: try: last_time = respect_trakt_rate(last_time) with requests_cache.disabled(): seen_date = (movie.lastViewedAt if movie.lastViewedAt else datetime.now()) m.mark_as_seen( seen_date.astimezone( datetime.timezone.utc)) logging.info( "Movie [{} ({})]: marking as watched on Trakt..." .format(movie.title, movie.year)) break except ValueError: # for py<3.6 with requests_cache.disabled(): m.mark_as_seen(seen_date) except trakt.errors.RateLimitException as e: delay = int( e.response.headers.get("Retry-After", 1)) logging.warning( "Movie [{} ({})]: Rate Limited on watched update. Sleeping {} sec from trakt (GUID: {})" .format(movie.title, movie.year, delay, guid)) sleep(delay) retry += retry if retry == 5: logging.warning( "Movie [{} ({})]: Rate Limited 5 times on watched update. Abort trakt request." .format(movie.title, movie.year)) # set watched status if movie is watched on trakt elif watchedOnTrakt: logging.info( "Movie [{} ({})]: marking as watched in Plex...". format(movie.title, movie.year)) with requests_cache.disabled(): movie.markWatched() # add to plex lists listutil.addPlexItemToLists(m.trakt, movie) logging.info("Movie [{} ({})]: Finished sync".format( movie.title, movie.year)) if CONFIG['display-log']['movies'] == "true": if movie.title[0:4].lower() == "the ": FirstLetter = movie.title[4].lower() elif movie.title[0:2].lower() == "a ": FirstLetter = movie.title[2].lower() else: FirstLetter = movie.title[0].lower() vLetInt = ord(FirstLetter) - 96 vPercent = round(vLetInt * 100 / 26) if vPercent < 0: vPercent = 1 if vPercent > 99: vPercent = 100 if vLetInt > 25: vLetInt = 26 if vLetInt < 0: vLetInt = 1 if len(str(vPercent)) == 1: PerPad = " " elif len(str(vPercent)) == 2: PerPad = " " MovieTrunk = (movie.title[:16] + '..') if len(movie.title) > 16 else movie.title # sys.stdout.write('\x1b[1A') # sys.stdout.write('\x1b[2K') print(PerPad + str(vPercent) + '% ', end="") for i in range(vLetInt): print('\u2588', end="") for i in range(26 - vLetInt): print(' ', end="") print('|', end="") print(" - {} ({})".format(MovieTrunk, movie.year) + " ", end='\r') del MovieTrunk del vPercent del vLetInt del FirstLetter except trakt.errors.NotFoundException: logging.error("Movie [{} ({})]: GUID {} not found on trakt".format( movie.title, movie.year, guid)) except trakt.errors.RateLimitException as e: delay = int(e.response.headers.get("Retry-After", 1)) logging.warning( "Movie [{} ({})]: Rate Limited. Sleeping {} sec from trakt (GUID: {})" .format(movie.title, movie.year, delay, guid)) sleep(delay) except Exception as e: logging.warning("Movie [{} ({})]: {} (GUID: {})".format( movie.title, movie.year, e, guid))
def main(): start_time = time() listutil = TraktListUtil() # do not use the cache for account specific stuff as this is subject to change start_msg = "Starting two way sync of {} (Plex) and {} (Trakt)\n".format( CONFIG['PLEX_USERNAME'], CONFIG['TRAKT_USERNAME']) print(start_msg) logging.info(start_msg) with requests_cache.disabled(): try: trakt_user = trakt.users.User('me') except trakt.errors.OAuthException as e: m = "Trakt authentication error: {}".format(str(e)) logging.info(m) print(m) exit(1) if CONFIG['sync']['liked_lists']: liked_lists = pytrakt_extensions.get_liked_lists() trakt_watched_movies = set( map(lambda m: m.trakt, trakt_user.watched_movies)) logging.debug( "Watched movies from trakt: {}".format(trakt_watched_movies)) trakt_movie_collection = set( map(lambda m: m.trakt, trakt_user.movie_collection)) # logging.debug("Movie collection from trakt:", trakt_movie_collection) trakt_watched_shows = pytrakt_extensions.allwatched() if CONFIG['sync']['watchlist']: listutil.addList(None, "Trakt Watchlist", traktid_list=list( map(lambda m: m.trakt, trakt_user.watchlist_movies))) # logging.debug("Movie watchlist from trakt:", trakt_movie_watchlist) user_ratings = trakt_user.get_ratings(media_type='movies') if CONFIG['sync']['liked_lists']: for lst in liked_lists: listutil.addList(lst['username'], lst['listname']) ratings = {} for r in user_ratings: ratings[r['movie']['ids']['slug']] = r['rating'] logging.debug("Movie ratings from trakt: {}".format(ratings)) logging.info('Loaded Trakt lists.') with requests_cache.disabled(): # xxxxx try: plex = get_plex_server() logging.info("Server version {} updated at: {}".format( plex.version, plex.updatedAt)) logging.info("Recently added: {}".format( plex.library.recentlyAdded()[:5])) except: input( "\nThis error may occur when Hawke.one (or other Plex server you are using) is unavailable.\nThis is usually due to the server running a scan or reboot.\nPlease try again in a few minutes." ) exit() with requests_cache.disabled(): sections = plex.library.sections() for section in sections: if section.title in CONFIG['excluded-libraries']: continue # process movie sections section_start_time = time() if type(section) is plexapi.library.MovieSection: # clean_collections_in_section(section) print("Processing " + section.title + " library") if CONFIG['display-log']['movies'] == "true": print(" Loading movies..", end='\r') process_movie_section(section, trakt_watched_movies, ratings, listutil, trakt_movie_collection) # process show sections elif type(section) is plexapi.library.ShowSection: print("Processing " + section.title + " library") if CONFIG['display-log']['shows'] == "true": print(" Loading shows..", end='\r') process_show_section(section, trakt_watched_shows, listutil) else: continue timedelta = time() - section_start_time m, s = divmod(timedelta, 60) logging.warning("Completed section sync in " + (m > 0) * "{:.0f} min ".format(m) + (s > 0) * "{:.1f} seconds".format(s)) # sys.stdout.write('\x1b[1A') # sys.stdout.write('\x1b[2K') print( '100% \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 - ', end="") print("Completed in " + (m > 0) * "{:.0f} min ".format(m) + (s > 0) * "{:.1f} seconds\n".format(s)) listutil.updatePlexLists(plex) logging.info("Updated plex watchlist") timedelta = time() - start_time m, s = divmod(timedelta, 60) logging.info("Completed full sync in " + (m > 0) * "{:.0f} min ".format(m) + (s > 0) * "{:.1f} seconds".format(s)) print("Completed server sync in " + (m > 0) * "{:.0f} min ".format(m) + (s > 0) * "{:.1f} seconds\n\n".format(s)) input("Press enter to exit")
def process_show_section(s, watched_set, listutil): with requests_cache.disabled(): allShows = s.all() logging.info( "Now working on show section {} containing {} elements".format( s.title, len(allShows))) for show in allShows: guid = show.guid if guid.startswith('local') or 'agents.none' in guid: # ignore this guid, it's not matched logging.warning("Show [{} ({})]: GUID is local, ignoring".format( show.title, show.year)) continue elif 'thetvdb' in guid: x = guid.split('//')[1] x = x.split('?')[0] provider = 'tvdb' elif 'themoviedb' in guid: x = guid.split('//')[1] x = x.split('?')[0] provider = 'tmdb' elif 'xbmcnfotv' in guid: x = guid.split('//')[1] x = x.split('?')[0] provider = CONFIG['xbmc-providers']['shows'] else: logging.error("Show [{} ({})]: Unrecognized GUID {}".format( show.title, show.year, guid)) continue try: # find show logging.debug("Show [{} ({})]: Started sync".format( show.title, show.year)) search = trakt.sync.search_by_id(x, id_type=provider) trakt_show = None # look for the first tv show in the results for result in search: if type(result) is trakt.tv.TVShow: trakt_show = result break if trakt_show is None: logging.error( "Show [{} ({})]: Did not find on Trakt. Aborting. GUID: {}" .format(show.title, show.year, guid)) continue with requests_cache.disabled(): trakt_collected = pytrakt_extensions.collected( trakt_show.trakt) start_time = last_time = time() # this lookup-table is accessible via lookup[season][episode] with requests_cache.disabled(): lookup = pytrakt_extensions.lookup_table(trakt_show) logging.debug("Show [{} ({})]: Generated LUT in {} seconds".format( show.title, show.year, (time() - start_time))) # loop over episodes in plex db for episode in show.episodes(): try: eps = lookup[episode.seasonNumber][episode.index] except KeyError: try: logging.warning( "Show [{} ({})]: Key not found, did not record episode S{:02}E{:02}" .format(show.title, show.year, episode.seasonNumber, episode.index)) except TypeError: logging.error( "Show [{} ({})]: Invalid episode {}".format( show.title, show.year, episode)) continue watched = watched_set.get_completed(trakt_show.trakt, episode.seasonNumber, episode.index) collected = trakt_collected.get_completed( episode.seasonNumber, episode.index) # sync collected if CONFIG['sync']['collection']: if not collected: retry = 0 while retry < 5: try: last_time = respect_trakt_rate(last_time) with requests_cache.disabled(): eps.instance.add_to_library() logging.info( "Show [{} ({})]: Collected episode S{:02}E{:02}" .format(show.title, show.year, episode.seasonNumber, episode.index)) break except JSONDecodeError as e: logging.error("JSON decode error: {}".format( str(e))) except trakt.errors.RateLimitException as e: delay = int( e.response.headers.get("Retry-After", 1)) logging.warning( "Show [{} ({})]: Rate limit on collected episode S{:02}E{:02}. Sleeping {} sec from trakt" .format(show.title, show.year, episode.seasonNumber, episode.index, delay)) sleep(delay) retry += retry if retry == 5: logging.warning( "Show [{} ({})]: Rate Limited 5 times on collected episode S{:02}E{:02}. Abort trakt request." .format(show.title, show.year, episode.seasonNumber, episode.index)) # sync watched status if CONFIG['sync']['watched_status']: if episode.isWatched != watched: if episode.isWatched: retry = 0 while retry < 5: try: last_time = respect_trakt_rate(last_time) with requests_cache.disabled(): seen_date = (episode.lastViewedAt if episode.lastViewedAt else datetime.now()) eps.instance.mark_as_seen( seen_date.astimezone( datetime.timezone.utc)) logging.info( "Show [{} ({})]: Marked as watched on trakt: episode S{:02}E{:02}" .format(show.title, show.year, episode.seasonNumber, episode.index)) break except JSONDecodeError as e: logging.error( "JSON decode error: {}".format(str(e))) except ValueError: # for py<3.6 with requests_cache.disabled(): eps.instance.mark_as_seen(seen_date) except trakt.errors.RateLimitException as e: delay = int( e.response.headers.get( "Retry-After", 1)) logging.warning( "Show [{} ({})]: Rate limit on watched episode S{:02}E{:02}. Sleep {} sec from trakt" .format(show.title, show.year, episode.seasonNumber, episode.index, delay)) retry += retry sleep(delay) if retry == 5: logging.warning( "Show [{} ({})]: Rate Limited 5 times on collected episode S{:02}E{:02}. Abort trakt request." .format(show.title, show.year, episode.seasonNumber, episode.index)) elif watched: with requests_cache.disabled(): episode.markWatched() logging.info( "Show [{} ({})]: Marked as watched on plex: episode S{:02}E{:02}" .format(show.title, show.year, episode.seasonNumber, episode.index)) else: logging.warning( "Episode.isWatched: {}, watched: {} isWatched != watched: {}" .format(episode.isWatched, watched, episode.isWatched != watched)) logging.debug( "Show [{} ({})]: Synced episode S{:02}E{:02}".format( show.title, show.year, episode.seasonNumber, episode.index)) # add to plex lists listutil.addPlexItemToLists(eps.instance.trakt, episode) logging.info("Show [{} ({})]: Finished sync".format( show.title, show.year)) if CONFIG['display-log']['shows'] == "true": if show.title[0:4].lower() == "the ": FirstLetter = show.title[4].lower() elif show.title[0:2].lower() == "a ": FirstLetter = show.title[2].lower() else: FirstLetter = show.title[0].lower() vLetInt = ord(FirstLetter) - 96 vPercent = round(vLetInt * 100 / 26) if vPercent < 0: vPercent = 1 if vPercent > 99: vPercent = 100 if vLetInt > 25: vLetInt = 26 if vLetInt < 0: vLetInt = 1 ShowTrunk = (show.title[:16] + '..') if len(show.title) > 16 else show.title # sys.stdout.write('\x1b[1A') # sys.stdout.write('\x1b[2K') print(str(vPercent) + '% ', end="") for i in range(vLetInt): print('\u2588', end="") for i in range(26 - vLetInt): print('_', end="") print('|', end="") print(" - {} ({})".format(ShowTrunk, show.year) + " ", end='\r') except trakt.errors.NotFoundException: logging.error("Show [{} ({})]: GUID {} not found on trakt".format( show.title, show.year, guid)) except trakt.errors.RateLimitException as e: delay = int(e.response.headers.get("Retry-After", 1)) logging.debug( "Show [{} ({})]: Rate Limited. Sleeping {} sec from trakt". format(show.title, show.year, delay)) sleep(delay) except Exception as e: logging.error("Show [{} ({})]: {} (GUID {})".format( show.title, show.year, e, guid))
def main(): start_time = time() listutil = TraktListUtil() # do not use the cache for account specific stuff as this is subject to change start_msg = "Starting sync Plex {} and Trakt {}".format(CONFIG['PLEX_USERNAME'], CONFIG['TRAKT_USERNAME']) print(start_msg) logging.info(start_msg) with requests_cache.disabled(): try: trakt_user = trakt.users.User('me') except trakt.errors.OAuthException as e: m = "Trakt authentication error: {}".format(str(e)) logging.info(m) print(m) exit(1) if CONFIG['sync']['liked_lists']: liked_lists = pytrakt_extensions.get_liked_lists() trakt_watched_movies = set( map(lambda m: m.trakt, trakt_user.watched_movies)) logging.debug("Watched movies from trakt: {}".format( trakt_watched_movies)) trakt_movie_collection = set( map(lambda m: m.trakt, trakt_user.movie_collection)) # logging.debug("Movie collection from trakt:", trakt_movie_collection) trakt_watched_shows = pytrakt_extensions.allwatched() if CONFIG['sync']['watchlist']: listutil.addList(None, "Trakt Watchlist", traktid_list=list( map(lambda m: m.trakt, trakt_user.watchlist_movies))) # logging.debug("Movie watchlist from trakt:", trakt_movie_watchlist) user_ratings = trakt_user.get_ratings(media_type='movies') if CONFIG['sync']['liked_lists']: for lst in liked_lists: listutil.addList(lst['username'], lst['listname']) ratings = {} for r in user_ratings: ratings[r['movie']['ids']['slug']] = r['rating'] logging.debug("Movie ratings from trakt: {}".format(ratings)) logging.info('Loaded Trakt lists.') with requests_cache.disabled(): plex = get_plex_server() logging.info("Server version {} updated at: {}".format( plex.version, plex.updatedAt)) logging.info("Recently added: {}".format( plex.library.recentlyAdded()[:5])) with requests_cache.disabled(): sections = plex.library.sections() for section in sections: if section.title in CONFIG['excluded-libraries']: continue # process movie sections section_start_time = time() if type(section) is plexapi.library.MovieSection: # clean_collections_in_section(section) print("Processing section", section.title) process_movie_section( section, trakt_watched_movies, ratings, listutil, trakt_movie_collection) # process show sections elif type(section) is plexapi.library.ShowSection: print("Processing section", section.title) process_show_section(section, trakt_watched_shows, listutil) else: continue timedelta = time() - section_start_time m, s = divmod(timedelta, 60) logging.warning("Completed section sync in " + (m>0) * "{:.0f} min ".format(m) + (s>0) * "{:.1f} seconds".format(s)) listutil.updatePlexLists(plex) logging.info("Updated plex watchlist") timedelta = time() - start_time m, s = divmod(timedelta, 60) logging.info("Completed full sync in " + (m>0) * "{:.0f} min ".format(m) + (s>0) * "{:.1f} seconds".format(s)) print("Completed full sync in " + (m>0) * "{:.0f} min ".format(m) + (s>0) * "{:.1f} seconds".format(s))