From 248be3ab08d76ece72c31a3ed898010ad736f746 Mon Sep 17 00:00:00 2001 From: Aaron Sewall Date: Thu, 23 Jan 2025 11:24:09 -0500 Subject: [PATCH] formatted --- IMDBTraktSyncer/IMDBTraktSyncer.py | 2123 ++++++++++++++++++-------- IMDBTraktSyncer/arguments.py | 145 +- IMDBTraktSyncer/authTrakt.py | 83 +- IMDBTraktSyncer/checkChrome.py | 421 +++-- IMDBTraktSyncer/checkVersion.py | 36 +- IMDBTraktSyncer/errorHandling.py | 472 ++++-- IMDBTraktSyncer/errorLogger.py | 14 +- IMDBTraktSyncer/imdbData.py | 551 +++++-- IMDBTraktSyncer/traktData.py | 380 +++-- IMDBTraktSyncer/verifyCredentials.py | 338 ++-- 10 files changed, 3140 insertions(+), 1423 deletions(-) diff --git a/IMDBTraktSyncer/IMDBTraktSyncer.py b/IMDBTraktSyncer/IMDBTraktSyncer.py index 86b040a..2a214cc 100644 --- a/IMDBTraktSyncer/IMDBTraktSyncer.py +++ b/IMDBTraktSyncer/IMDBTraktSyncer.py @@ -12,47 +12,81 @@ from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys -from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException +from selenium.common.exceptions import ( + NoSuchElementException, + TimeoutException, + StaleElementReferenceException, +) from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import SessionNotCreatedException from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from IMDBTraktSyncer import arguments + class PageLoadException(Exception): pass + def main(): parser = argparse.ArgumentParser(description="IMDBTraktSyncer CLI") - parser.add_argument("--clear-user-data", action="store_true", help="Clears user entered credentials.") - parser.add_argument("--clear-cache", action="store_true", help="Clears cached browsers, drivers and error logs.") - parser.add_argument("--uninstall", action="store_true", help="Clears cached browsers and drivers before uninstalling.") - parser.add_argument("--clean-uninstall", action="store_true", help="Clears all cached data, inluding user credentials, cached browsers and drivers before uninstalling.") - parser.add_argument("--directory", action="store_true", help="Prints the package install directory.") - + parser.add_argument( + "--clear-user-data", + action="store_true", + help="Clears user entered credentials.", + ) + parser.add_argument( + "--clear-cache", + action="store_true", + help="Clears cached browsers, drivers and error logs.", + ) + parser.add_argument( + "--uninstall", + action="store_true", + help="Clears cached browsers and drivers before uninstalling.", + ) + parser.add_argument( + "--clean-uninstall", + action="store_true", + help="Clears all cached data, inluding user credentials, cached browsers and" + "drivers before uninstalling.", + ) + parser.add_argument( + "--directory", action="store_true", help="Prints the package install directory." + ) + args = parser.parse_args() - + main_directory = os.path.dirname(os.path.realpath(__file__)) if args.clear_user_data: arguments.clear_user_data(main_directory) - + if args.clear_cache: arguments.clear_cache(main_directory) - + if args.uninstall: arguments.uninstall(main_directory) - + if args.clean_uninstall: arguments.clean_uninstall(main_directory) - + if args.directory: arguments.print_directory(main_directory) - + # If no arguments are passed, run the main package logic - if not any([args.clear_user_data, args.clear_cache, args.uninstall, args.clean_uninstall, args.directory]): - + if not any( + [ + args.clear_user_data, + args.clear_cache, + args.uninstall, + args.clean_uninstall, + args.directory, + ] + ): + # Run main package print("Starting IMDBTraktSyncer....") from IMDBTraktSyncer import checkVersion as CV @@ -62,24 +96,29 @@ def main(): from IMDBTraktSyncer import imdbData from IMDBTraktSyncer import errorHandling as EH from IMDBTraktSyncer import errorLogger as EL - + # Check if package is up to date CV.checkVersion() - + try: # Print credentials directory VC.print_directory(main_directory) - + # Get credentials _, _, _, _, imdb_username, imdb_password = VC.prompt_get_credentials() sync_watchlist_value = VC.prompt_sync_watchlist() sync_ratings_value = VC.prompt_sync_ratings() - remove_watched_from_watchlists_value = VC.prompt_remove_watched_from_watchlists() + remove_watched_from_watchlists_value = ( + VC.prompt_remove_watched_from_watchlists() + ) sync_reviews_value = VC.prompt_sync_reviews() sync_watch_history_value = VC.prompt_sync_watch_history() mark_rated_as_watched_value = VC.prompt_mark_rated_as_watched() - remove_watchlist_items_older_than_x_days_value, watchlist_days_to_remove_value = VC.prompt_remove_watchlist_items_older_than_x_days() - + ( + remove_watchlist_items_older_than_x_days_value, + watchlist_days_to_remove_value, + ) = VC.prompt_remove_watchlist_items_older_than_x_days() + # Check if Chrome portable browser is downloaded and up to date CC.checkChrome() browser_type, headless = CC.get_browser_type() @@ -88,40 +127,49 @@ def main(): directory = os.path.dirname(os.path.realpath(__file__)) # Start WebDriver - print('Starting WebDriver...') - - chrome_binary_path = CC.get_chrome_binary_path(directory) - chromedriver_binary_path = CC.get_chromedriver_binary_path(directory) + print("Starting WebDriver...") + + chrome_binary_path = CC.get_chrome_binary_path(directory) + chromedriver_binary_path = CC.get_chromedriver_binary_path(directory) user_data_directory = CC.get_user_data_directory() - + # Initialize Chrome options options = Options() options.binary_location = chrome_binary_path options.add_argument(f"--user-data-dir={user_data_directory}") if headless == True: options.add_argument("--headless=new") - options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36') - options.add_experimental_option("prefs", { - "download.default_directory": directory, - "download.directory_upgrade": True, - "download.prompt_for_download": False, - "profile.default_content_setting_values.automatic_downloads": 1, - "credentials_enable_service": False, - "profile.password_manager_enabled": False - }) - options.add_argument('--disable-gpu') - options.add_argument('--start-maximized') - options.add_argument('--disable-notifications') + options.add_argument( + "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/" + "537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + ) + options.add_experimental_option( + "prefs", + { + "download.default_directory": directory, + "download.directory_upgrade": True, + "download.prompt_for_download": False, + "profile.default_content_setting_values.automatic_downloads": 1, + "credentials_enable_service": False, + "profile.password_manager_enabled": False, + }, + ) + options.add_argument("--disable-gpu") + options.add_argument("--start-maximized") + options.add_argument("--disable-notifications") options.add_argument("--disable-third-party-cookies") options.add_argument("--disable-dev-shm-usage") options.add_argument("--no-sandbox") options.add_argument("--disable-extensions") - options.add_experimental_option("excludeSwitches", ["enable-automation", "enable-logging"]) - options.add_argument('--log-level=3') - + options.add_experimental_option( + "excludeSwitches", ["enable-automation", "enable-logging"] + ) + options.add_argument("--log-level=3") + service = Service(executable_path=chromedriver_binary_path) - - # Temporary solution for removing "DevTools listening on ws:" line on Windows for better readability + + # Temporary solution for removing "DevTools listening on ws:" line on + # Windows for better readability # Only use CREATE_NO_WINDOW on Windows systems (32-bit or 64-bit) if browser_type == "chrome": if sys.platform == "win32": @@ -134,132 +182,231 @@ def main(): driver.set_page_load_timeout(60) except Exception as e: - error_message = (f"Error initializing WebDriver: {str(e)}") + error_message = f"Error initializing WebDriver: {str(e)}" print(f"{error_message}") EL.logger.error(error_message) raise SystemExit - + # Example: Wait for an element and interact with it wait = WebDriverWait(driver, 10) - + # go to IMDB homepage - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) time.sleep(2) # Check if still signed in from previous session - element = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".nav__userMenu.navbar__user"))) - if element.find_elements(By.CSS_SELECTOR, ".imdb-header__account-toggle--logged-in"): + element = wait.until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, ".nav__userMenu.navbar__user") + ) + ) + if element.find_elements( + By.CSS_SELECTOR, ".imdb-header__account-toggle--logged-in" + ): print("Successfully signed in to IMDB") else: # Not signed in from previous session, proceed with sign in logic time.sleep(2) - + # Load sign in page - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/registration/signin', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/registration/signin", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) # wait for sign in link to appear and then click it - sign_in_link = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.list-group-item > .auth-provider-text'))) - if 'IMDb' in sign_in_link.text: + sign_in_link = wait.until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "a.list-group-item > .auth-provider-text") + ) + ) + if "IMDb" in sign_in_link.text: sign_in_link.click() - # wait for email input field and password input field to appear, then enter credentials and submit - email_input = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "input[type='email']")))[0] - password_input = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, "input[type='password']")))[0] + # wait for email input field and password input field to appear, then + # enter credentials and submit + email_input = wait.until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, "input[type='email']") + ) + )[0] + password_input = wait.until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, "input[type='password']") + ) + )[0] email_input.send_keys(imdb_username) password_input.send_keys(imdb_password) - submit_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "input[type='submit']"))) + submit_button = wait.until( + EC.element_to_be_clickable( + (By.CSS_SELECTOR, "input[type='submit']") + ) + ) submit_button.click() time.sleep(2) # go to IMDB homepage - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) time.sleep(2) # Check if signed in - element = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".nav__userMenu.navbar__user"))) - if element.find_elements(By.CSS_SELECTOR, ".imdb-header__account-toggle--logged-in"): + element = wait.until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, ".nav__userMenu.navbar__user") + ) + ) + if element.find_elements( + By.CSS_SELECTOR, ".imdb-header__account-toggle--logged-in" + ): print("Successfully signed in to IMDB") else: print("\nError: Not signed in to IMDB") print("\nPossible Causes and Solutions:") print("- IMDB captcha check triggered or incorrect IMDB login.") - + print("\n1. IMDB Captcha Check:") - print(" If your login is correct, the issue is likely due to an IMDB captcha check.") + print( + " If your login is correct, the issue is likely due to an " + "IMDB captcha check." + ) print(" To resolve this, follow these steps:") - print(" - Log in to IMDB on your browser (preferably Chrome) and on the same computer.") + print( + " - Log in to IMDB on your browser (preferably Chrome) and on" + " the same computer." + ) print(" - If already logged in, log out and log back in.") - print(" - Repeat this process until a captcha check is triggered.") + print( + " - Repeat this process until a captcha check is triggered." + ) print(" - Complete the captcha and finish logging in.") print(" - After successfully logging in, run the script again.") - print(" - You may need to repeat these steps until the captcha check is no longer triggered.") - + print( + " - You may need to repeat these steps until the captcha " + "check is no longer triggered." + ) print("\n2. Incorrect IMDB Login:") - print(" If your IMDB login is incorrect, update your login credentials:") - print(" - Edit the 'credentials.txt' file in your settings directory with the correct login information.") - print(" - Alternatively, delete the 'credentials.txt' file and run the script again.") - + print( + " If your IMDB login is incorrect, update your login " + "credentials:" + ) + print( + " - Edit the 'credentials.txt' file in your settings " + "directory with the correct login information." + ) + print( + " - Alternatively, delete the 'credentials.txt' file and run " + "the script again." + ) print("\nFor more details, see the following GitHub link:") print("https://github.com/RileyXX/IMDB-Trakt-Syncer/issues/2") - + print("\nStopping script...") - + EL.logger.error("Error: Not signed in to IMDB") driver.close() driver.quit() service.stop() raise SystemExit - + # Check IMDB Language for compatability # Get Current Language - language_element = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "span[id*='nav-language-selector-contents'] .selected"))).get_attribute("aria-label") + language_element = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + "span[id*='nav-language-selector-contents'] .selected", + ) + ) + ).get_attribute("aria-label") original_language = language_element - if (original_language != "English (United States)"): - print("Temporarily changing IMDB Language to English for compatability. See: https://www.imdb.com/preferences/general") + if original_language != "English (United States)": + print( + "Temporarily changing IMDB Language to English for compatability. " + "See: https://www.imdb.com/preferences/general" + ) # Open Language Dropdown - language_dropdown = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "label[for*='nav-language-selector']"))) + language_dropdown = wait.until( + EC.element_to_be_clickable( + (By.CSS_SELECTOR, "label[for*='nav-language-selector']") + ) + ) driver.execute_script("arguments[0].click();", language_dropdown) # Change Language to English - english_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "span[id*='nav-language-selector-contents'] li[aria-label*='English (United States)']"))) + english_element = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "span[id*='nav-language-selector-contents'] li[aria-label*=" + "'English (United States)']", + ) + ) + ) driver.execute_script("arguments[0].click();", english_element) - - # Check IMDB reference view setting for compatability. See: https://www.imdb.com/preferences/general + + # Check IMDB reference view setting for compatability. See: + # https://www.imdb.com/preferences/general # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(f'https://www.imdb.com/preferences/general', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + f"https://www.imdb.com/preferences/general", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) # Find reference view checkbox - reference_checkbox = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[id*='ttdp']"))).get_attribute("checked") + reference_checkbox = wait.until( + EC.presence_of_element_located((By.CSS_SELECTOR, "input[id*='ttdp']")) + ).get_attribute("checked") reference_view_changed = False if reference_checkbox: - print("Temporarily disabling reference view IMDB setting for compatability. See: https://www.imdb.com/preferences/general") + print( + "Temporarily disabling reference view IMDB setting for " + "compatability. See: https://www.imdb.com/preferences/general" + ) # Click reference view checkbox - reference_checkbox = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "input[id*='ttdp']"))) + reference_checkbox = wait.until( + EC.element_to_be_clickable((By.CSS_SELECTOR, "input[id*='ttdp']")) + ) driver.execute_script("arguments[0].click();", reference_checkbox) # Submit - submit = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".article input[type*='submit']"))) + submit = wait.until( + EC.element_to_be_clickable( + (By.CSS_SELECTOR, ".article input[type*='submit']") + ) + ) driver.execute_script("arguments[0].click();", submit) reference_view_changed = True time.sleep(1) - + # Initalize list values - trakt_watchlist = trakt_ratings = trakt_reviews = trakt_watch_history = imdb_watchlist = imdb_ratings = imdb_reviews = imdb_watch_history = [] - + trakt_watchlist = trakt_ratings = trakt_reviews = trakt_watch_history = ( + imdb_watchlist + ) = imdb_ratings = imdb_reviews = imdb_watch_history = [] + # Get Trakt Data - print('Processing Trakt Data') + print("Processing Trakt Data") trakt_encoded_username = traktData.get_trakt_encoded_username() if sync_watchlist_value or remove_watched_from_watchlists_value: trakt_watchlist = traktData.get_trakt_watchlist(trakt_encoded_username) @@ -267,73 +414,170 @@ def main(): trakt_ratings = traktData.get_trakt_ratings(trakt_encoded_username) if sync_reviews_value: trakt_reviews = traktData.get_trakt_comments(trakt_encoded_username) - if sync_watch_history_value or remove_watched_from_watchlists_value or mark_rated_as_watched_value: - trakt_watch_history = traktData.get_trakt_watch_history(trakt_encoded_username) - print('Processing Trakt Data Complete') - + if ( + sync_watch_history_value + or remove_watched_from_watchlists_value + or mark_rated_as_watched_value + ): + trakt_watch_history = traktData.get_trakt_watch_history( + trakt_encoded_username + ) + print("Processing Trakt Data Complete") + # Get IMDB Data - print('Processing IMDB Data') - driver, wait = imdbData.generate_imdb_exports(driver, wait, directory, sync_watchlist_value, sync_ratings_value, sync_watch_history_value, remove_watched_from_watchlists_value, mark_rated_as_watched_value) - driver, wait = imdbData.download_imdb_exports(driver, wait, directory, sync_watchlist_value, sync_ratings_value, sync_watch_history_value, remove_watched_from_watchlists_value, mark_rated_as_watched_value) + print("Processing IMDB Data") + driver, wait = imdbData.generate_imdb_exports( + driver, + wait, + directory, + sync_watchlist_value, + sync_ratings_value, + sync_watch_history_value, + remove_watched_from_watchlists_value, + mark_rated_as_watched_value, + ) + driver, wait = imdbData.download_imdb_exports( + driver, + wait, + directory, + sync_watchlist_value, + sync_ratings_value, + sync_watch_history_value, + remove_watched_from_watchlists_value, + mark_rated_as_watched_value, + ) if sync_watchlist_value or remove_watched_from_watchlists_value: - imdb_watchlist, driver, wait = imdbData.get_imdb_watchlist(driver, wait, directory) + imdb_watchlist, driver, wait = imdbData.get_imdb_watchlist( + driver, wait, directory + ) if sync_ratings_value or mark_rated_as_watched_value: - imdb_ratings, driver, wait = imdbData.get_imdb_ratings(driver, wait, directory) + imdb_ratings, driver, wait = imdbData.get_imdb_ratings( + driver, wait, directory + ) if sync_reviews_value: - imdb_reviews, errors_found_getting_imdb_reviews, driver, wait = imdbData.get_imdb_reviews(driver, wait, directory) - if sync_watch_history_value or remove_watched_from_watchlists_value or mark_rated_as_watched_value: - imdb_watch_history, driver, wait = imdbData.get_imdb_checkins(driver, wait, directory) - print('Processing IMDB Data Complete') - + imdb_reviews, errors_found_getting_imdb_reviews, driver, wait = ( + imdbData.get_imdb_reviews(driver, wait, directory) + ) + if ( + sync_watch_history_value + or remove_watched_from_watchlists_value + or mark_rated_as_watched_value + ): + imdb_watch_history, driver, wait = imdbData.get_imdb_checkins( + driver, wait, directory + ) + print("Processing IMDB Data Complete") + if sync_watchlist_value: - # Check if IMDB watchlist has reached the 10,000 item limit. If limit is reached, disable syncing watchlists. - imdb_watchlist_limit_reached = EH.check_if_watchlist_limit_reached(imdb_watchlist) - + # Check if IMDB watchlist has reached the 10,000 item limit. If limit is + # reached, disable syncing watchlists. + imdb_watchlist_limit_reached = EH.check_if_watchlist_limit_reached( + imdb_watchlist + ) if sync_watch_history_value or mark_rated_as_watched_value: - # Check if IMDB watch history has reached the 10,000 item limit. If limit is reached, disable syncing watch history. - imdb_watch_history_limit_reached = EH.check_if_watch_history_limit_reached(imdb_watch_history) - + # Check if IMDB watch history has reached the 10,000 item limit. If + # limit is reached, disable syncing watch history. + imdb_watch_history_limit_reached = ( + EH.check_if_watch_history_limit_reached(imdb_watch_history) + ) # Remove duplicates from Trakt watch_history trakt_watch_history = EH.remove_duplicates_by_imdb_id(trakt_watch_history) - - # Update outdated IMDB_IDs from trakt lists based on matching Title and Type comparison - trakt_ratings, imdb_ratings, driver, wait = EH.update_outdated_imdb_ids_from_trakt(trakt_ratings, imdb_ratings, driver, wait) - trakt_reviews, imdb_reviews, driver, wait = EH.update_outdated_imdb_ids_from_trakt(trakt_reviews, imdb_reviews, driver, wait) - trakt_watchlist, imdb_watchlist, driver, wait = EH.update_outdated_imdb_ids_from_trakt(trakt_watchlist, imdb_watchlist, driver, wait) - trakt_watch_history, imdb_watch_history, driver, wait = EH.update_outdated_imdb_ids_from_trakt(trakt_watch_history, imdb_watch_history, driver, wait) - - ''' + + # Update outdated IMDB_IDs from trakt lists based on matching Title and Type + # comparison + trakt_ratings, imdb_ratings, driver, wait = ( + EH.update_outdated_imdb_ids_from_trakt( + trakt_ratings, imdb_ratings, driver, wait + ) + ) + trakt_reviews, imdb_reviews, driver, wait = ( + EH.update_outdated_imdb_ids_from_trakt( + trakt_reviews, imdb_reviews, driver, wait + ) + ) + trakt_watchlist, imdb_watchlist, driver, wait = ( + EH.update_outdated_imdb_ids_from_trakt( + trakt_watchlist, imdb_watchlist, driver, wait + ) + ) + trakt_watch_history, imdb_watch_history, driver, wait = ( + EH.update_outdated_imdb_ids_from_trakt( + trakt_watch_history, imdb_watch_history, driver, wait + ) + ) + + """ # Removed temporarily to monitor impact. Most conflicts should be resolved by update_outdated_imdb_ids_from_trakt() function - # Filter out items that share the same Title, Year and Type, AND have non-matching IMDB_ID values - trakt_ratings, imdb_ratings = EH.filter_out_mismatched_items(trakt_ratings, imdb_ratings) - trakt_reviews, imdb_reviews = EH.filter_out_mismatched_items(trakt_reviews, imdb_reviews) - trakt_watchlist, imdb_watchlist = EH.filter_out_mismatched_items(trakt_watchlist, imdb_watchlist) - trakt_watch_history, imdb_watch_history = EH.filter_out_mismatched_items(trakt_watch_history, imdb_watch_history) - ''' - + # Filter out items that share the same Title, Year and Type, AND have non- + # matching IMDB_ID values + trakt_ratings, imdb_ratings = EH.filter_out_mismatched_items( + trakt_ratings, imdb_ratings + ) + trakt_reviews, imdb_reviews = EH.filter_out_mismatched_items( + trakt_reviews, imdb_reviews + ) + trakt_watchlist, imdb_watchlist = EH.filter_out_mismatched_items( + trakt_watchlist, imdb_watchlist + ) + trakt_watch_history, imdb_watch_history = EH.filter_out_mismatched_items( + trakt_watch_history, imdb_watch_history + ) + """ # Get trakt and imdb data and filter out items with missing imdb id - trakt_ratings = [rating for rating in trakt_ratings if rating.get('IMDB_ID') is not None] - imdb_ratings = [rating for rating in imdb_ratings if rating.get('IMDB_ID') is not None] - trakt_reviews = [review for review in trakt_reviews if review.get('IMDB_ID') is not None] - imdb_reviews = [review for review in imdb_reviews if review.get('IMDB_ID') is not None] - trakt_watchlist = [item for item in trakt_watchlist if item.get('IMDB_ID') is not None] - imdb_watchlist = [item for item in imdb_watchlist if item.get('IMDB_ID') is not None] - trakt_watch_history = [item for item in trakt_watch_history if item.get('IMDB_ID') is not None] - imdb_watch_history = [item for item in imdb_watch_history if item.get('IMDB_ID') is not None] - - # Filter out items already set: Filters items from the target_list that are not already present in the source_list based on key - imdb_ratings_to_set = EH.filter_items(imdb_ratings, trakt_ratings, key="IMDB_ID") - trakt_ratings_to_set = EH.filter_items(trakt_ratings, imdb_ratings, key="IMDB_ID") - - imdb_reviews_to_set = EH.filter_items(imdb_reviews, trakt_reviews, key="IMDB_ID") - trakt_reviews_to_set = EH.filter_items(trakt_reviews, imdb_reviews, key="IMDB_ID") + trakt_ratings = [ + rating for rating in trakt_ratings if rating.get("IMDB_ID") is not None + ] + imdb_ratings = [ + rating for rating in imdb_ratings if rating.get("IMDB_ID") is not None + ] + trakt_reviews = [ + review for review in trakt_reviews if review.get("IMDB_ID") is not None + ] + imdb_reviews = [ + review for review in imdb_reviews if review.get("IMDB_ID") is not None + ] + trakt_watchlist = [ + item for item in trakt_watchlist if item.get("IMDB_ID") is not None + ] + imdb_watchlist = [ + item for item in imdb_watchlist if item.get("IMDB_ID") is not None + ] + trakt_watch_history = [ + item for item in trakt_watch_history if item.get("IMDB_ID") is not None + ] + imdb_watch_history = [ + item for item in imdb_watch_history if item.get("IMDB_ID") is not None + ] - imdb_watchlist_to_set = EH.filter_items(imdb_watchlist, trakt_watchlist, key="IMDB_ID") - trakt_watchlist_to_set = EH.filter_items(trakt_watchlist, imdb_watchlist, key="IMDB_ID") + # Filter out items already set: Filters items from the target_list that are not already present in the source_list based on key + imdb_ratings_to_set = EH.filter_items( + imdb_ratings, trakt_ratings, key="IMDB_ID" + ) + trakt_ratings_to_set = EH.filter_items( + trakt_ratings, imdb_ratings, key="IMDB_ID" + ) + + imdb_reviews_to_set = EH.filter_items( + imdb_reviews, trakt_reviews, key="IMDB_ID" + ) + trakt_reviews_to_set = EH.filter_items( + trakt_reviews, imdb_reviews, key="IMDB_ID" + ) + + imdb_watchlist_to_set = EH.filter_items( + imdb_watchlist, trakt_watchlist, key="IMDB_ID" + ) + trakt_watchlist_to_set = EH.filter_items( + trakt_watchlist, imdb_watchlist, key="IMDB_ID" + ) + + imdb_watch_history_to_set = EH.filter_items( + imdb_watch_history, trakt_watch_history, key="IMDB_ID" + ) + trakt_watch_history_to_set = EH.filter_items( + trakt_watch_history, imdb_watch_history, key="IMDB_ID" + ) - imdb_watch_history_to_set = EH.filter_items(imdb_watch_history, trakt_watch_history, key="IMDB_ID") - trakt_watch_history_to_set = EH.filter_items(trakt_watch_history, imdb_watch_history, key="IMDB_ID") - if mark_rated_as_watched_value: # Combine Trakt and IMDB Ratings into one list combined_ratings = trakt_ratings + imdb_ratings @@ -341,49 +585,79 @@ def main(): # Remove duplicates from combined_ratings by IMDB_ID combined_ratings = EH.remove_duplicates_by_imdb_id(combined_ratings) - # Loop through combined ratings and check if they are already in both watch histories + # Loop through combined ratings and check if they are already in both + # watch histories for item in combined_ratings: - imdb_id = item['IMDB_ID'] + imdb_id = item["IMDB_ID"] - # Skip items with 'Type' as 'show' (shows cannot be marked as watched on Trakt) - if item['Type'] == 'show': + # Skip items with 'Type' as 'show' (shows cannot be marked as + # watched on Trakt) + if item["Type"] == "show": continue - - # Check if this imdb_id exists in both trakt_watch_history and imdb_watch_history - if not any(imdb_id == watch_item['IMDB_ID'] for watch_item in trakt_watch_history) and \ - not any(imdb_id == watch_item['IMDB_ID'] for watch_item in imdb_watch_history): - # If not found in both, add to the appropriate watch history to set list + + # Check if this imdb_id exists in both trakt_watch_history and + # imdb_watch_history + if not any( + imdb_id == watch_item["IMDB_ID"] + for watch_item in trakt_watch_history + ) and not any( + imdb_id == watch_item["IMDB_ID"] + for watch_item in imdb_watch_history + ): + # If not found in both, add to the appropriate watch history to + # set list trakt_watch_history_to_set.append(item) imdb_watch_history_to_set.append(item) trakt_watch_history.append(item) imdb_watch_history.append(item) - - # Remove duplicates from trakt and imdb watch history (in case items added with mark_rated_as_watched_value) - trakt_watch_history = EH.remove_duplicates_by_imdb_id(trakt_watch_history) - imdb_watch_history = EH.remove_duplicates_by_imdb_id(imdb_watch_history) - - # Skip adding shows to trakt watch history, because it will mark all episodes as watched + + # Remove duplicates from trakt and imdb watch history (in case + # items added with mark_rated_as_watched_value) + trakt_watch_history = EH.remove_duplicates_by_imdb_id( + trakt_watch_history + ) + imdb_watch_history = EH.remove_duplicates_by_imdb_id( + imdb_watch_history + ) + + # Skip adding shows to trakt watch history, because it will mark all + # episodes as watched trakt_watch_history_to_set = EH.remove_shows(trakt_watch_history_to_set) - + # Filter ratings to update imdb_ratings_to_update = [] trakt_ratings_to_update = [] - # Dictionary to store IMDB_IDs and their corresponding ratings for IMDB and Trakt - imdb_ratings_dict = {rating['IMDB_ID']: rating for rating in imdb_ratings} - trakt_ratings_dict = {rating['IMDB_ID']: rating for rating in trakt_ratings} + # Dictionary to store IMDB_IDs and their corresponding ratings for IMDB and + # Trakt + imdb_ratings_dict = {rating["IMDB_ID"]: rating for rating in imdb_ratings} + trakt_ratings_dict = {rating["IMDB_ID"]: rating for rating in trakt_ratings} - # Include only items with the same IMDB_ID and different ratings and prefer the most recent rating + # Include only items with the same IMDB_ID and different ratings and prefer + # the most recent rating for imdb_id, imdb_rating in imdb_ratings_dict.items(): if imdb_id in trakt_ratings_dict: trakt_rating = trakt_ratings_dict[imdb_id] - if imdb_rating['Rating'] != trakt_rating['Rating']: - imdb_date_added = datetime.fromisoformat(imdb_rating['Date_Added'].replace('Z', '')).replace(tzinfo=timezone.utc) - trakt_date_added = datetime.fromisoformat(trakt_rating['Date_Added'].replace('Z', '')).replace(tzinfo=timezone.utc) - + if imdb_rating["Rating"] != trakt_rating["Rating"]: + imdb_date_added = datetime.fromisoformat( + imdb_rating["Date_Added"].replace("Z", "") + ).replace(tzinfo=timezone.utc) + trakt_date_added = datetime.fromisoformat( + trakt_rating["Date_Added"].replace("Z", "") + ).replace(tzinfo=timezone.utc) + # Check if ratings were added on different days - if (imdb_date_added.year, imdb_date_added.month, imdb_date_added.day) != (trakt_date_added.year, trakt_date_added.month, trakt_date_added.day): - # If IMDB rating is more recent, add the Trakt rating to the update list, and vice versa + if ( + imdb_date_added.year, + imdb_date_added.month, + imdb_date_added.day, + ) != ( + trakt_date_added.year, + trakt_date_added.month, + trakt_date_added.day, + ): + # If IMDB rating is more recent, add the Trakt rating to the + # update list, and vice versa if imdb_date_added > trakt_date_added: trakt_ratings_to_update.append(imdb_rating) else: @@ -392,81 +666,116 @@ def main(): # Update ratings_to_set imdb_ratings_to_set.extend(imdb_ratings_to_update) trakt_ratings_to_set.extend(trakt_ratings_to_update) - - # Filter out setting review IMDB where the comment length is less than 600 characters + + # Filter out setting review IMDB where the comment length is less than 600 + # characters imdb_reviews_to_set = EH.filter_by_comment_length(imdb_reviews_to_set, 600) - + # Initialize watchlist_items_to_remove variables trakt_watchlist_items_to_remove = [] imdb_watchlist_items_to_remove = [] - + # If remove_watched_from_watchlists_value is true if remove_watched_from_watchlists_value: # Combine Trakt and IMDB Watch History into one list watched_content = trakt_watch_history + imdb_watch_history - + # Remove duplicates from watched_content watched_content = EH.remove_duplicates_by_imdb_id(watched_content) - + # Get the IDs from watched_content - watched_content_ids = set(item['IMDB_ID'] for item in watched_content if item['IMDB_ID']) - + watched_content_ids = set( + item["IMDB_ID"] for item in watched_content if item["IMDB_ID"] + ) + # Filter out watched content from trakt_watchlist_to_set - trakt_watchlist_to_set = [item for item in trakt_watchlist_to_set if item['IMDB_ID'] not in watched_content_ids] + trakt_watchlist_to_set = [ + item + for item in trakt_watchlist_to_set + if item["IMDB_ID"] not in watched_content_ids + ] # Filter out watched content from imdb_watchlist_to_set - imdb_watchlist_to_set = [item for item in imdb_watchlist_to_set if item['IMDB_ID'] not in watched_content_ids] - + imdb_watchlist_to_set = [ + item + for item in imdb_watchlist_to_set + if item["IMDB_ID"] not in watched_content_ids + ] + # Find items to remove from trakt_watchlist - trakt_watchlist_items_to_remove = [item for item in trakt_watchlist if item['IMDB_ID'] in watched_content_ids] + trakt_watchlist_items_to_remove = [ + item + for item in trakt_watchlist + if item["IMDB_ID"] in watched_content_ids + ] # Find items to remove from imdb_watchlist - imdb_watchlist_items_to_remove = [item for item in imdb_watchlist if item['IMDB_ID'] in watched_content_ids] - + imdb_watchlist_items_to_remove = [ + item + for item in imdb_watchlist + if item["IMDB_ID"] in watched_content_ids + ] + # Sort lists by date - trakt_watchlist_items_to_remove = EH.sort_by_date_added(trakt_watchlist_items_to_remove) - imdb_watchlist_items_to_remove = EH.sort_by_date_added(imdb_watchlist_items_to_remove) - - # If remove_watchlist_items_older_than_x_days_value is true, add items older than x days to watchlist_items_to_remove lists + trakt_watchlist_items_to_remove = EH.sort_by_date_added( + trakt_watchlist_items_to_remove + ) + imdb_watchlist_items_to_remove = EH.sort_by_date_added( + imdb_watchlist_items_to_remove + ) + + # If remove_watchlist_items_older_than_x_days_value is true, add items older + # than x days to watchlist_items_to_remove lists if remove_watchlist_items_older_than_x_days_value: days = watchlist_days_to_remove_value - + combined_watchlist = trakt_watchlist + imdb_watchlist combined_watchlist = EH.remove_duplicates_by_imdb_id(combined_watchlist) - + # Get items older than x days - combined_watchlist_to_remove = EH.get_items_older_than_x_days(combined_watchlist, days) - + combined_watchlist_to_remove = EH.get_items_older_than_x_days( + combined_watchlist, days + ) + # Append items to remove to the watchlist_items_to_remove lists trakt_watchlist_items_to_remove.extend(combined_watchlist_to_remove) imdb_watchlist_items_to_remove.extend(combined_watchlist_to_remove) - + # Remove combined_watchlist_to_remove items from watchlist_to_set lists - imdb_watchlist_to_set, trakt_watchlist_to_set = EH.remove_combined_watchlist_to_remove_items_from_watchlist_to_set_lists_by_imdb_id(combined_watchlist_to_remove, imdb_watchlist_to_set, trakt_watchlist_to_set) - + imdb_watchlist_to_set, trakt_watchlist_to_set = ( + # TODO rename long function? + EH.remove_combined_watchlist_to_remove_items_from_watchlist_to_set_lists_by_imdb_id( + combined_watchlist_to_remove, + imdb_watchlist_to_set, + trakt_watchlist_to_set, + ) + ) + # Sort lists by date imdb_ratings_to_set = EH.sort_by_date_added(imdb_ratings_to_set) trakt_ratings_to_set = EH.sort_by_date_added(trakt_ratings_to_set) imdb_watchlist_to_set = EH.sort_by_date_added(imdb_watchlist_to_set) trakt_watchlist_to_set = EH.sort_by_date_added(trakt_watchlist_to_set) imdb_watch_history_to_set = EH.sort_by_date_added(imdb_watch_history_to_set) - trakt_watch_history_to_set = EH.sort_by_date_added(trakt_watch_history_to_set) - + trakt_watch_history_to_set = EH.sort_by_date_added( + trakt_watch_history_to_set + ) + if sync_watchlist_value and imdb_watchlist_limit_reached: # IMDB watchlist limit reached, skip watchlist actions for IMDB imdb_watchlist_to_set = [] - + if sync_watch_history_value and imdb_watch_history_limit_reached: # IMDB watch history limit reached, skip watch history actions for IMDB imdb_watch_history_to_set = [] - + if mark_rated_as_watched_value and imdb_watch_history_limit_reached: # IMDB watch history limit reached, skip watch history actions for IMDB imdb_watch_history_to_set = [] - + # If sync_watchlist_value is true if sync_watchlist_value: # Set Trakt Watchlist Items if trakt_watchlist_to_set: - print('Setting Trakt Watchlist Items') + print("Setting Trakt Watchlist Items") # Count the total number of items num_items = len(trakt_watchlist_to_set) @@ -474,321 +783,543 @@ def main(): for item in trakt_watchlist_to_set: item_count += 1 - - imdb_id = item['IMDB_ID'] - media_type = item['Type'] # 'movie', 'show', or 'episode' + + imdb_id = item["IMDB_ID"] + media_type = item["Type"] # 'movie', 'show', or 'episode' url = f"https://api.trakt.tv/sync/watchlist" - data = { - "movies": [], - "shows": [], - "episodes": [] - } - - if media_type == 'movie': - data['movies'].append({ - "ids": { - "imdb": imdb_id - } - }) - elif media_type == 'show': - data['shows'].append({ - "ids": { - "imdb": imdb_id - } - }) - elif media_type == 'episode': - data['episodes'].append({ - "ids": { - "imdb": imdb_id - } - }) + data = {"movies": [], "shows": [], "episodes": []} + + if media_type == "movie": + data["movies"].append({"ids": {"imdb": imdb_id}}) + elif media_type == "show": + data["shows"].append({"ids": {"imdb": imdb_id}}) + elif media_type == "episode": + data["episodes"].append({"ids": {"imdb": imdb_id}}) else: data = None if data: response = EH.make_trakt_request(url, payload=data) - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - + episode_title = "" + if response and response.status_code in [200, 201, 204]: - print(f" - Added {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) to Trakt Watchlist ({item['IMDB_ID']})") - + print( + f" - Added {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) to Trakt Watchlist " + f"({item['IMDB_ID']})" + ) else: - error_message = f"Failed to add {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) to Trakt Watchlist ({item['IMDB_ID']})" + error_message = ( + f"Failed to add {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) to Trakt Watchlist " + f"({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) - print('Setting Trakt Watchlist Items Complete') + print("Setting Trakt Watchlist Items Complete") else: - print('No Trakt Watchlist Items To Set') + print("No Trakt Watchlist Items To Set") # Set IMDB Watchlist Items if imdb_watchlist_to_set: - print('Setting IMDB Watchlist Items') - + print("Setting IMDB Watchlist Items") + # Count the total number of items num_items = len(imdb_watchlist_to_set) item_count = 0 - + for item in imdb_watchlist_to_set: - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - - year_str = f' ({item["Year"]})' if item["Year"] is not None else '' # sometimes year is None for episodes from trakt so remove it from the print string + episode_title = "" + + year_str = ( + f' ({item["Year"]})' if item["Year"] is not None else "" + ) + # sometimes year is None for episodes from trakt so remove it + # from the print string try: item_count += 1 - + # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(f'https://www.imdb.com/title/{item["IMDB_ID"]}/', driver, wait) + success, status_code, url, driver, wait = ( + EH.get_page_with_retries( + f'https://www.imdb.com/title/{item["IMDB_ID"]}/', + driver, + wait, + ) + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. " + f"URL: {url}" + ) current_url = driver.current_url - + # Check if the URL doesn't contain "/reference" if "/reference" not in current_url: - # Wait until the loader has disappeared, indicating the watchlist button has loaded - wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, '[data-testid="tm-box-wl-loader"]'))) - + # Wait until the loader has disappeared, indicating the + # watchlist button has loaded + wait.until( + EC.invisibility_of_element_located( + ( + By.CSS_SELECTOR, + '[data-testid="tm-box-wl-loader"]', + ) + ) + ) # Scroll the page to bring the element into view - watchlist_button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[data-testid="tm-box-wl-button"]'))) - driver.execute_script("arguments[0].scrollIntoView(true);", watchlist_button) - + watchlist_button = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + 'button[data-testid="tm-box-wl-button"]', + ) + ) + ) + driver.execute_script( + "arguments[0].scrollIntoView(true);", + watchlist_button, + ) + # Wait for the element to be clickable - watchlist_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button[data-testid="tm-box-wl-button"]'))) - - # Check if item is already in watchlist otherwise skip it - if 'ipc-icon--done' not in watchlist_button.get_attribute('innerHTML'): + watchlist_button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + 'button[data-testid="tm-box-wl-button"]', + ) + ) + ) + + # Check if item is already in watchlist otherwise skip + # it + if ( + "ipc-icon--done" + not in watchlist_button.get_attribute("innerHTML") + ): retry_count = 0 while retry_count < 2: - driver.execute_script("arguments[0].click();", watchlist_button) + driver.execute_script( + "arguments[0].click();", watchlist_button + ) try: - WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[data-testid="tm-box-wl-button"] .ipc-icon--done'))) - - print(f" - Added {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} to IMDB Watchlist ({item['IMDB_ID']})") - + WebDriverWait(driver, 3).until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + ( + "button[data-testid=" + '"tm-box-wl-button"] ' + ".ipc-icon--done" + ), + ) + ) + ) + + print( + f" - Added {item['Type']} ({item_count}" + f" of {num_items}): {episode_title}" + f"{item['Title']}{year_str} to IMDB" + f"Watchlist ({item['IMDB_ID']})" + ) + break # Break the loop if successful except TimeoutException: retry_count += 1 if retry_count == 2: - error_message = f"Failed to add item ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} to IMDB Watchlist ({item['IMDB_ID']})" + error_message = ( + f"Failed to add item ({item_count} of " + f"{num_items}): {episode_title}" + f"{item['Title']}{year_str} to IMDB " + f"Watchlist ({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) else: - error_message1 = f" - Failed to add item ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} to IMDB Watchlist ({item['IMDB_ID']})" - error_message2 = f" - {item['Type'].capitalize()} already exists in IMDB watchlist." + error_message1 = ( + f" - Failed to add item ({item_count} of " + f"{num_items}): {episode_title}{item['Title']}" + f"{year_str} to IMDB Watchlist " + f"({item['IMDB_ID']})" + ) + error_message2 = ( + f" - {item['Type'].capitalize()} already " + "exists in IMDB watchlist." + ) EL.logger.error(error_message1) EL.logger.error(error_message2) else: # Handle the case when the URL contains "/reference" - + # Scroll the page to bring the element into view - watchlist_button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.titlereference-watch-ribbon > .wl-ribbon'))) - driver.execute_script("arguments[0].scrollIntoView(true);", watchlist_button) - - # Check if watchlist_button has class .not-inWL before clicking - if 'not-inWL' in watchlist_button.get_attribute('class'): - driver.execute_script("arguments[0].click();", watchlist_button) - - except (NoSuchElementException, TimeoutException, PageLoadException): - error_message = f"Failed to add item ({item_count} of {num_items}): {item['Title']}{year_str} to IMDB Watchlist ({item['IMDB_ID']})" + watchlist_button = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + ".titlereference-watch-ribbon > .wl-ribbon", + ) + ) + ) + driver.execute_script( + "arguments[0].scrollIntoView(true);", + watchlist_button, + ) + + # Check if watchlist_button has class .not-inWL before + # clicking + if "not-inWL" in watchlist_button.get_attribute( + "class" + ): + driver.execute_script( + "arguments[0].click();", watchlist_button + ) + + except ( + NoSuchElementException, + TimeoutException, + PageLoadException, + ): + error_message = ( + f"Failed to add item ({item_count} of {num_items}): " + f"{item['Title']}{year_str} to IMDB Watchlist " + f"({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message, exc_info=True) pass - - print('Setting IMDB Watchlist Items Complete') + print("Setting IMDB Watchlist Items Complete") else: - print('No IMDB Watchlist Items To Set') - + print("No IMDB Watchlist Items To Set") + # If sync_ratings_value is true if sync_ratings_value: - - #Set Trakt Ratings + + # Set Trakt Ratings if trakt_ratings_to_set: - print('Setting Trakt Ratings') + print("Setting Trakt Ratings") # Set the API endpoints rate_url = "https://api.trakt.tv/sync/ratings" - + # Count the total number of items num_items = len(trakt_ratings_to_set) item_count = 0 - + # Loop through your data table and rate each item on Trakt for item in trakt_ratings_to_set: item_count += 1 if item["Type"] == "show": # This is a TV show data = { - "shows": [{ - "ids": { - "imdb": item["IMDB_ID"] - }, - "rating": item["Rating"] - }] + "shows": [ + { + "ids": {"imdb": item["IMDB_ID"]}, + "rating": item["Rating"], + } + ] } elif item["Type"] == "movie": # This is a movie data = { - "movies": [{ - "ids": { - "imdb": item["IMDB_ID"] - }, - "rating": item["Rating"] - }] + "movies": [ + { + "ids": {"imdb": item["IMDB_ID"]}, + "rating": item["Rating"], + } + ] } elif item["Type"] == "episode": # This is an episode data = { - "episodes": [{ - "ids": { - "imdb": item["IMDB_ID"] - }, - "rating": item["Rating"] - }] + "episodes": [ + { + "ids": {"imdb": item["IMDB_ID"]}, + "rating": item["Rating"], + } + ] } else: data = None - + if data: # Make the API call to rate the item response = EH.make_trakt_request(rate_url, payload=data) - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - + episode_title = "" + if response and response.status_code in [200, 201, 204]: - print(f" - Rated {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}): {item['Rating']}/10 on Trakt ({item['IMDB_ID']})") + print( + f" - Rated {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}): {item['Rating']}/10 on Trakt " + f"({item['IMDB_ID']})" + ) else: - error_message = f"Failed rating {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}): {item['Rating']}/10 on Trakt ({item['IMDB_ID']})" + error_message = ( + f"Failed rating {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}): {item['Rating']}/10 on Trakt " + f"({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) - print('Setting Trakt Ratings Complete') + print("Setting Trakt Ratings Complete") else: - print('No Trakt Ratings To Set') + print("No Trakt Ratings To Set") # Set IMDB Ratings if imdb_ratings_to_set: - print('Setting IMDB Ratings') - - # loop through each movie and TV show rating and submit rating on IMDB website + print("Setting IMDB Ratings") + + # loop through each movie and TV show rating and submit rating on + # IMDB website for i, item in enumerate(imdb_ratings_to_set, 1): - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - - year_str = f' ({item["Year"]})' if item["Year"] is not None else '' # sometimes year is None for episodes from trakt so remove it from the print string - + episode_title = "" + + year_str = ( + f' ({item["Year"]})' if item["Year"] is not None else "" + ) + # sometimes year is None for episodes from trakt so remove it + # from the print string try: # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(f'https://www.imdb.com/title/{item["IMDB_ID"]}/', driver, wait) + success, status_code, url, driver, wait = ( + EH.get_page_with_retries( + f'https://www.imdb.com/title/{item["IMDB_ID"]}/', + driver, + wait, + ) + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. " + f"URL: {url}" + ) current_url = driver.current_url - + # Check if the URL doesn't contain "/reference" if "/reference" not in current_url: # Wait until the rating bar has loaded - wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, '[data-testid="hero-rating-bar__loading"]'))) - - # Wait until rate button is located and scroll to it - button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '[data-testid="hero-rating-bar__user-rating"] button.ipc-btn'))) - # driver.execute_script("arguments[0].scrollIntoView(true);", button) + wait.until( + EC.invisibility_of_element_located( + ( + By.CSS_SELECTOR, + '[data-testid="hero-rating-bar__loading"]', + ) + ) + ) - # click on "Rate" button and select rating option, then submit rating - button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '[data-testid="hero-rating-bar__user-rating"] button.ipc-btn'))) - element_rating_bar = button.find_element(By.CSS_SELECTOR, '[data-testid*="hero-rating-bar__user-rating__"]') + # Wait until rate button is located and scroll to it + button = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + ( + "[data-testid=" + '"hero-rating-bar__user-rating"] ' + "button.ipc-btn" + ), + ) + ) + ) + # driver.execute_script( + # "arguments[0].scrollIntoView(true);", button + # ) + + # click on "Rate" button and select rating option, then + # submit rating + button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + ( + "[data-testid=" + '"hero-rating-bar__user-rating"] ' + "button.ipc-btn" + ), + ) + ) + ) + element_rating_bar = button.find_element( + By.CSS_SELECTOR, + '[data-testid*="hero-rating-bar__user-rating__"]', + ) if element_rating_bar: try: - has_existing_rating = button.find_element(By.CSS_SELECTOR, '[data-testid*="hero-rating-bar__user-rating__"] span') - existing_rating = int(has_existing_rating.text.strip()) + has_existing_rating = button.find_element( + By.CSS_SELECTOR, + "[data-testid*=" + '"hero-rating-bar__user-rating__"] span', + ) + existing_rating = int( + has_existing_rating.text.strip() + ) except NoSuchElementException: - existing_rating = None - + existing_rating = None + if existing_rating != item["Rating"]: - driver.execute_script("arguments[0].click();", button) - rating_option_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, f'button[aria-label="Rate {item["Rating"]}"]'))) - driver.execute_script("arguments[0].click();", rating_option_element) - submit_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button.ipc-rating-prompt__rate-button'))) + driver.execute_script( + "arguments[0].click();", button + ) + rating_option_element = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "button[aria-label=" + f'"Rate {item["Rating"]}"]', + ) + ) + ) + driver.execute_script( + "arguments[0].click();", + rating_option_element, + ) + submit_element = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "button.ipc-rating-prompt__rate-" + "button", + ) + ) + ) submit_element.click() time.sleep(1) - - print(f' - Rated {item["Type"]}: ({i} of {len(imdb_ratings_to_set)}) {episode_title}{item["Title"]}{year_str}: {item["Rating"]}/10 on IMDB ({item["IMDB_ID"]})') - + + print( + f' - Rated {item["Type"]}: ({i} of ' + f"{len(imdb_ratings_to_set)}) " + f'{episode_title}{item["Title"]}{year_str}:' + f' {item["Rating"]}/10 on IMDB ' + f'({item["IMDB_ID"]})' + ) else: - error_message1 = f' - Failed to rate {item["Type"]}: ({i} of {len(imdb_ratings_to_set)}) {episode_title}{item["Title"]}{year_str}: {item["Rating"]}/10 on IMDB ({item["IMDB_ID"]})' - error_message2 = f" - Rating already exists on IMDB for this {item['Type']}. Rating: ({item['Rating']})" + error_message1 = ( + f' - Failed to rate {item["Type"]}: ({i} of' + f" {len(imdb_ratings_to_set)}) " + f'{episode_title}{item["Title"]}{year_str}:' + f' {item["Rating"]}/10 on IMDB ' + f'({item["IMDB_ID"]})' + ) + error_message2 = ( + f" - Rating already exists on IMDB for " + f"this {item['Type']}. Rating: " + f"({item['Rating']})" + ) EL.logger.error(error_message1) EL.logger.error(error_message2) else: # Handle the case when the URL contains "/reference" - - # Wait until rate button is located and scroll to it - button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.ipl-rating-interactive__star-container'))) - driver.execute_script("arguments[0].scrollIntoView(true);", button) - # click on "Rate" button and select rating option, then submit rating - button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '.ipl-rating-interactive__star-container'))) + # Wait until rate button is located and scroll to it + button = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + ".ipl-rating-interactive__star-container", + ) + ) + ) + driver.execute_script( + "arguments[0].scrollIntoView(true);", button + ) + + # click on "Rate" button and select rating option, then + # submit rating + button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + ".ipl-rating-interactive__star-container", + ) + ) + ) driver.execute_script("arguments[0].click();", button) - - # Find the rating option element based on the data-value attribute - rating_option_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, f'.ipl-rating-selector__star-link[data-value="{item["Rating"]}"]'))) - driver.execute_script("arguments[0].click();", rating_option_element) - + + # Find the rating option element based on the data-value + # attribute + rating_option_element = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + ".ipl-rating-selector__star-link[" + f'data-value="{item["Rating"]}"]', + ) + ) + ) + driver.execute_script( + "arguments[0].click();", rating_option_element + ) + time.sleep(1) - - except (NoSuchElementException, TimeoutException, PageLoadException): - error_message = f'Failed to rate {item["Type"]}: ({i} of {len(imdb_ratings_to_set)}) {episode_title}{item["Title"]}{year_str}: {item["Rating"]}/10 on IMDB ({item["IMDB_ID"]})' + + except ( + NoSuchElementException, + TimeoutException, + PageLoadException, + ): + error_message = ( + f'Failed to rate {item["Type"]}: ({i} of ' + f"{len(imdb_ratings_to_set)}) {episode_title}" + f'{item["Title"]}{year_str}: {item["Rating"]}/10 on ' + f'IMDB ({item["IMDB_ID"]})' + ) print(f" - {error_message}") EL.logger.error(error_message, exc_info=True) pass - print('Setting IMDB Ratings Complete') + print("Setting IMDB Ratings Complete") else: - print('No IMDB Ratings To Set') + print("No IMDB Ratings To Set") # If sync_reviews_value is true if sync_reviews_value: - + # Check if there was an error getting IMDB reviews if not errors_found_getting_imdb_reviews: - + # Set Trakt Reviews if trakt_reviews_to_set: - print('Setting Trakt Reviews') + print("Setting Trakt Reviews") # Count the total number of items num_items = len(trakt_reviews_to_set) @@ -796,131 +1327,192 @@ def main(): for item in trakt_reviews_to_set: item_count += 1 - - imdb_id = item['IMDB_ID'] - comment = item['Comment'] - media_type = item['Type'] # 'movie', 'show', or 'episode' + + imdb_id = item["IMDB_ID"] + comment = item["Comment"] + media_type = item["Type"] # 'movie', 'show', or 'episode' url = f"https://api.trakt.tv/comments" - data = { - "comment": comment - } + data = {"comment": comment} - if media_type == 'movie': - data['movie'] = { - "ids": { - "imdb": imdb_id - } - } - elif media_type == 'show': - data['show'] = { - "ids": { - "imdb": imdb_id - } - } - elif media_type == 'episode': - data['episode'] = { - "ids": { - "imdb": episode_id - } - } + if media_type == "movie": + data["movie"] = {"ids": {"imdb": imdb_id}} + elif media_type == "show": + data["show"] = {"ids": {"imdb": imdb_id}} + elif media_type == "episode": + data["episode"] = {"ids": {"imdb": episode_id}} else: data = None - + if data: response = EH.make_trakt_request(url, payload=data) - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = ( + f"[S{season_number}E{episode_number}] " + ) else: - episode_title = '' + episode_title = "" if response and response.status_code in [200, 201, 204]: - print(f" - Submitted comment ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) on Trakt ({item['IMDB_ID']})") + print( + f" - Submitted comment ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) on Trakt ({item['IMDB_ID']})" + ) else: - error_message = f"Failed to submit comment ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) on Trakt ({item['IMDB_ID']})" + error_message = ( + f"Failed to submit comment ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) on Trakt ({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) - print('Trakt Reviews Set Successfully') + print("Trakt Reviews Set Successfully") else: - print('No Trakt Reviews To Set') + print("No Trakt Reviews To Set") # Set IMDB Reviews if imdb_reviews_to_set: # Call the check_last_run() function if check_imdb_reviews_last_submitted(): - print('Setting IMDB Reviews') - + print("Setting IMDB Reviews") + # Count the total number of items num_items = len(imdb_reviews_to_set) item_count = 0 - + for item in imdb_reviews_to_set: item_count += 1 try: - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = ( + f"[S{season_number}E{episode_number}] " + ) else: - episode_title = '' - - print(f" - Submitting review ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) on IMDB ({item['IMDB_ID']})") - + episode_title = "" + + print( + f" - Submitting review ({item_count} of" + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) on IMDB ({item['IMDB_ID']})" + ) # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(f'https://contribute.imdb.com/review/{item["IMDB_ID"]}/add?bus=imdb', driver, wait) + success, status_code, url, driver, wait = ( + EH.get_page_with_retries( + "https://contribute.imdb.com/review/" + f'{item["IMDB_ID"]}/add?bus=imdb', + driver, + wait, + ) + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - - review_title_input = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input.klondike-input"))) - review_input = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.klondike-textarea"))) - + raise PageLoadException( + f"Failed to load page. Status code: " + f"{status_code}. URL: {url}" + ) + + review_title_input = wait.until( + EC.presence_of_element_located( + (By.CSS_SELECTOR, "input.klondike-input") + ) + ) + review_input = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + "textarea.klondike-textarea", + ) + ) + ) review_title_input.send_keys("My Review") review_input.send_keys(item["Comment"]) - - no_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "ul.klondike-userreview-spoiler li:nth-child(2)"))) - yes_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "ul.klondike-userreview-spoiler li:nth-child(1)"))) - + + no_element = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "ul.klondike-userreview-spoiler " + "li:nth-child(2)", + ) + ) + ) + yes_element = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "ul.klondike-userreview-spoiler " + "li:nth-child(1)", + ) + ) + ) if item["Spoiler"]: - yes_element.click() + yes_element.click() else: no_element.click() - - submit_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "input.a-button-input[type='submit']"))) + + submit_button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "input.a-button-input[type='submit']", + ) + ) + ) submit_button.click() - - time.sleep(3) # wait for rating to submit - except (NoSuchElementException, TimeoutException, PageLoadException): - error_message = f"Failed to submit review ({item_count} of {num_items}): {item['Title']} ({item['Year']}) on IMDB ({item['IMDB_ID']})" + + time.sleep(3) # wait for rating to submit + except ( + NoSuchElementException, + TimeoutException, + PageLoadException, + ): + error_message = ( + f"Failed to submit review " + f"({item_count} of {num_items}): " + f"{item['Title']} ({item['Year']}) on IMDB " + f"({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message, exc_info=True) pass - - print('Setting IMDB Reviews Complete') + + print("Setting IMDB Reviews Complete") else: - print('IMDB reviews were submitted within the last 10 days. Skipping IMDB review submission.') + print( + "IMDB reviews were submitted within the last 10 days." + "Skipping IMDB review submission." + ) else: - print('No IMDB Reviews To Set') + print("No IMDB Reviews To Set") else: - print('There was an error getting IMDB reviews. See exception. Skipping reviews submissions.') + print( + "There was an error getting IMDB reviews. See exception." + "Skipping reviews submissions." + ) # If remove_watched_from_watchlists_value is true - if remove_watched_from_watchlists_value or remove_watchlist_items_older_than_x_days_value: - + if ( + remove_watched_from_watchlists_value + or remove_watchlist_items_older_than_x_days_value + ): + # Remove Watched Items Trakt Watchlist if trakt_watchlist_items_to_remove: - print('Removing Watched Items From Trakt Watchlist') + print("Removing Watched Items From Trakt Watchlist") # Set the API endpoint remove_url = "https://api.trakt.tv/sync/watchlist/remove" @@ -934,32 +1526,14 @@ def main(): item_count += 1 if item["Type"] == "show": # This is a TV show - data = { - "shows": [{ - "ids": { - "trakt": item["TraktID"] - } - }] - } + data = {"shows": [{"ids": {"trakt": item["TraktID"]}}]} elif item["Type"] == "movie": # This is a movie - data = { - "movies": [{ - "ids": { - "trakt": item["TraktID"] - } - }] - } + data = {"movies": [{"ids": {"trakt": item["TraktID"]}}]} elif item["Type"] == "episode": - + # This is an episode - data = { - "episodes": [{ - "ids": { - "trakt": item["TraktID"] - } - }] - } + data = {"episodes": [{"ids": {"trakt": item["TraktID"]}}]} else: data = None @@ -967,346 +1541,601 @@ def main(): # Make the API call to remove the item from the watchlist response = EH.make_trakt_request(remove_url, payload=data) - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - + episode_title = "" + if response and response.status_code in [200, 201, 204]: - print(f" - Removed {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) from Trakt Watchlist ({item['IMDB_ID']})") + print( + f" - Removed {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) from Trakt Watchlist " + f"({item['IMDB_ID']})" + ) else: - error_message = f"Failed removing {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) from Trakt Watchlist ({item['IMDB_ID']})" + error_message = ( + f"Failed removing {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) from Trakt Watchlist " + f"({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) - print('Removing Watched Items From Trakt Watchlist Complete') + print("Removing Watched Items From Trakt Watchlist Complete") else: - print('No Trakt Watchlist Items To Remove') + print("No Trakt Watchlist Items To Remove") # Remove Watched Items IMDB Watchlist if imdb_watchlist_items_to_remove: - print('Removing Watched Items From IMDB Watchlist') - + print("Removing Watched Items From IMDB Watchlist") + # Count the total number of items num_items = len(imdb_watchlist_items_to_remove) item_count = 0 - + for item in imdb_watchlist_items_to_remove: - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - - year_str = f' ({item["Year"]})' if item["Year"] is not None else '' # sometimes year is None for episodes from trakt so remove it from the print string - + episode_title = "" + + year_str = ( + f' ({item["Year"]})' if item["Year"] is not None else "" + ) + # sometimes year is None for episodes from trakt so remove it + # from the print string try: item_count += 1 - + # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(f'https://www.imdb.com/title/{item["IMDB_ID"]}/', driver, wait) + success, status_code, url, driver, wait = ( + EH.get_page_with_retries( + f'https://www.imdb.com/title/{item["IMDB_ID"]}/', + driver, + wait, + ) + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. " + f"URL: {url}" + ) current_url = driver.current_url - + # Check if the URL doesn't contain "/reference" if "/reference" not in current_url: - # Wait until the loader has disappeared, indicating the watchlist button has loaded - wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, '[data-testid="tm-box-wl-loader"]'))) - + # Wait until the loader has disappeared, indicating the + # watchlist button has loaded + wait.until( + EC.invisibility_of_element_located( + ( + By.CSS_SELECTOR, + '[data-testid="tm-box-wl-loader"]', + ) + ) + ) # Scroll the page to bring the element into view - watchlist_button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[data-testid="tm-box-wl-button"]'))) - driver.execute_script("arguments[0].scrollIntoView(true);", watchlist_button) - + watchlist_button = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + 'button[data-testid="tm-box-wl-button"]', + ) + ) + ) + driver.execute_script( + "arguments[0].scrollIntoView(true);", + watchlist_button, + ) + # Wait for the element to be clickable - watchlist_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button[data-testid="tm-box-wl-button"]'))) - + watchlist_button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + 'button[data-testid="tm-box-wl-button"]', + ) + ) + ) + # Check if item is not in watchlist otherwise skip it - if 'ipc-icon--add' not in watchlist_button.get_attribute('innerHTML'): + if ( + "ipc-icon--add" + not in watchlist_button.get_attribute("innerHTML") + ): retry_count = 0 while retry_count < 2: - driver.execute_script("arguments[0].click();", watchlist_button) + driver.execute_script( + "arguments[0].click();", watchlist_button + ) try: - WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[data-testid="tm-box-wl-button"] .ipc-icon--add'))) - - print(f" - Removed {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} from IMDB Watchlist ({item['IMDB_ID']})") - + WebDriverWait(driver, 3).until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + "button[data-testid=" + '"tm-box-wl-button"] ' + ".ipc-icon--add", + ) + ) + ) + + print( + f" - Removed {item['Type']} " + f"({item_count} of {num_items}): " + f"{episode_title}{item['Title']}" + f"{year_str} from IMDB Watchlist " + f"({item['IMDB_ID']})" + ) + break # Break the loop if successful except TimeoutException: retry_count += 1 if retry_count == 2: - error_message = f"Failed to remove {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} from IMDB Watchlist ({item['IMDB_ID']})" + error_message = ( + f"Failed to remove {item['Type']} " + f"({item_count} of {num_items}): " + f"{episode_title}{item['Title']}{year_str} " + f"from IMDB Watchlist ({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) - + else: - error_message1 = f" - Failed to remove {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} from IMDB Watchlist ({item['IMDB_ID']})" - error_message2 = f" - {item['Type'].capitalize()} not in IMDB watchlist." + error_message1 = ( + f" - Failed to remove {item['Type']} " + f"({item_count} of {num_items}): " + f"{episode_title}{item['Title']}{year_str} from" + f" IMDB Watchlist ({item['IMDB_ID']})" + ) + error_message2 = ( + f" - {item['Type'].capitalize()} not in IMDB " + "watchlist." + ) EL.logger.error(error_message1) EL.logger.error(error_message2) - + else: # Handle the case when the URL contains "/reference" - + # Scroll the page to bring the element into view - watchlist_button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.titlereference-watch-ribbon > .wl-ribbon'))) - driver.execute_script("arguments[0].scrollIntoView(true);", watchlist_button) - - # Check if watchlist_button doesn't have the class .not-inWL before clicking - if 'not-inWL' not in watchlist_button.get_attribute('class'): - driver.execute_script("arguments[0].click();", watchlist_button) - - except (NoSuchElementException, TimeoutException, PageLoadException): - error_message = f"Failed to remove {item['Type']} ({item_count} of {num_items}): {item['Title']}{year_str} from IMDB Watchlist ({item['IMDB_ID']})" + watchlist_button = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + ".titlereference-watch-ribbon > .wl-ribbon", + ) + ) + ) + driver.execute_script( + "arguments[0].scrollIntoView(true);", + watchlist_button, + ) + + # Check if watchlist_button doesn't have the class + # .not-inWL before clicking + if "not-inWL" not in watchlist_button.get_attribute( + "class" + ): + driver.execute_script( + "arguments[0].click();", watchlist_button + ) + + except ( + NoSuchElementException, + TimeoutException, + PageLoadException, + ): + error_message = ( + f"Failed to remove {item['Type']} ({item_count} of " + f"{num_items}): {item['Title']}{year_str} from IMDB " + f"Watchlist ({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message, exc_info=True) pass - - print('Removing Watched Items From IMDB Watchlist Complete') + print("Removing Watched Items From IMDB Watchlist Complete") else: - print('No IMDB Watchlist Items To Remove') - + print("No IMDB Watchlist Items To Remove") + # If sync_watch_history_value is true if sync_watch_history_value or mark_rated_as_watched_value: - + # Set Trakt Watch History if trakt_watch_history_to_set: - print('Setting Trakt Watch History') + print("Setting Trakt Watch History") # Set the API endpoint for syncing watch history watch_history_url = "https://api.trakt.tv/sync/history" - + # Count the total number of items num_items = len(trakt_watch_history_to_set) item_count = 0 - + # Loop through your data table and set watch history for each item for item in trakt_watch_history_to_set: item_count += 1 - + # Initialize the data variable for the current item data = None - + if item["Type"] == "movie": # This is a movie data = { - "movies": [{ - "ids": { - "imdb": item["IMDB_ID"] - }, - "watched_at": item["WatchedAt"] # Mark when the movie was watched - }] + "movies": [ + { + "ids": {"imdb": item["IMDB_ID"]}, + "watched_at": item[ + "WatchedAt" + ], # Mark when the movie was watched + } + ] } - + elif item["Type"] == "episode": - - # This is an episode + + # This is an episode data = { - "episodes": [{ - "ids": { - "imdb": item["IMDB_ID"] - }, - "watched_at": item["WatchedAt"] # Mark when the episode was watched - }] + "episodes": [ + { + "ids": {"imdb": item["IMDB_ID"]}, + "watched_at": item[ + "WatchedAt" + ], # Mark when the episode was watched + } + ] } - - ''' - # Skip adding shows, because it will mark all episodes as watched + + """ + # Skip adding shows, because it will mark all episodes as + # watched elif item["Type"] == "show": # This is an episode data = { - "shows": [{ - "ids": { - "imdb": item["IMDB_ID"] - }, - "watched_at": item["WatchedAt"] # Mark when the episode was watched - }] + "shows": [ + { + "ids": {"imdb": item["IMDB_ID"]}, + "watched_at": item[ + "WatchedAt" + ], # Mark when the episode was watched + } + ] } - ''' - + """ if data: # Make the API call to mark the item as watched - response = EH.make_trakt_request(watch_history_url, payload=data) - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + response = EH.make_trakt_request( + watch_history_url, payload=data + ) + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - - year_str = f' ({item["Year"]})' if item["Year"] is not None else '' # sometimes year is None for episodes from trakt so remove it from the print string - + episode_title = "" + + year_str = ( + f' ({item["Year"]})' if item["Year"] is not None else "" + ) + # sometimes year is None for episodes from trakt so remove + # it from the print string if response and response.status_code in [200, 201, 204]: - print(f" - Adding {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) to Trakt Watch History ({item['IMDB_ID']})") - + print( + f" - Adding {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) to Trakt Watch History " + f"({item['IMDB_ID']})" + ) else: - error_message = f"Failed to add {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']} ({item['Year']}) to Trakt Watch History ({item['IMDB_ID']})" + error_message = ( + f"Failed to add {item['Type']} ({item_count} of " + f"{num_items}): {episode_title}{item['Title']} " + f"({item['Year']}) to Trakt Watch History " + f"({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) - print('Setting Trakt Watch History Complete') + print("Setting Trakt Watch History Complete") else: - print('No Trakt Watch History To Set') - + print("No Trakt Watch History To Set") + # Set IMDB Watch History Items if imdb_watch_history_to_set: - print('Setting IMDB Watch History Items') - + print("Setting IMDB Watch History Items") + # Count the total number of items num_items = len(imdb_watch_history_to_set) item_count = 0 - + for item in imdb_watch_history_to_set: - - season_number = item.get('SeasonNumber') - episode_number = item.get('EpisodeNumber') + + season_number = item.get("SeasonNumber") + episode_number = item.get("EpisodeNumber") if season_number and episode_number: season_number = str(season_number).zfill(2) episode_number = str(episode_number).zfill(2) - episode_title = f'[S{season_number}E{episode_number}] ' + episode_title = f"[S{season_number}E{episode_number}] " else: - episode_title = '' - - year_str = f' ({item.get("Year")})' if item.get("Year") is not None else '' # Handles None safely - + episode_title = "" + + year_str = ( + f' ({item.get("Year")})' + if item.get("Year") is not None + else "" + ) # Handles None safely + try: item_count += 1 - + # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(f'https://www.imdb.com/title/{item["IMDB_ID"]}/', driver, wait) + success, status_code, url, driver, wait = ( + EH.get_page_with_retries( + f'https://www.imdb.com/title/{item["IMDB_ID"]}/', + driver, + wait, + ) + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. " + f"URL: {url}" + ) current_url = driver.current_url - + # Check if the URL doesn't contain "/reference" if "/reference" not in current_url: - # Wait until the loader has disappeared, indicating the watch history button has loaded - wait.until(EC.invisibility_of_element_located((By.CSS_SELECTOR, '[data-testid="tm-box-wl-loader"]'))) - + # Wait until the loader has disappeared, indicating the + # watch history button has loaded + wait.until( + EC.invisibility_of_element_located( + ( + By.CSS_SELECTOR, + '[data-testid="tm-box-wl-loader"]', + ) + ) + ) # Scroll the page to bring the element into view - watch_history_button = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'button[data-testid="tm-box-addtolist-button"]'))) - driver.execute_script("arguments[0].scrollIntoView(true);", watch_history_button) - + watch_history_button = wait.until( + EC.presence_of_element_located( + ( + By.CSS_SELECTOR, + "button[data-testid=" + '"tm-box-addtolist-button"]', + ) + ) + ) + driver.execute_script( + "arguments[0].scrollIntoView(true);", + watch_history_button, + ) # Wait for the element to be clickable - watch_history_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, 'button[data-testid="tm-box-addtolist-button"]'))) - - driver.execute_script("arguments[0].click();", watch_history_button) - - watch_history_button = wait.until(EC.presence_of_element_located((By.XPATH, "//div[contains(text(), 'Your Check-Ins')]"))) - - watch_history_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//div[contains(text(), 'Your Check-Ins')]"))) - - # Check if item is already in watch history otherwise skip it - if 'true' not in watch_history_button.get_attribute('data-titleinlist'): + watch_history_button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "button[data-testid=" + '"tm-box-addtolist-button"]', + ) + ) + ) + + driver.execute_script( + "arguments[0].click();", watch_history_button + ) + + watch_history_button = wait.until( + EC.presence_of_element_located( + ( + By.XPATH, + "//div[contains(text(), 'Your Check-Ins')]", + ) + ) + ) + + watch_history_button = wait.until( + EC.element_to_be_clickable( + ( + By.XPATH, + "//div[contains(text(), 'Your Check-Ins')]", + ) + ) + ) + + # Check if item is already in watch history otherwise + # skip it + if "true" not in watch_history_button.get_attribute( + "data-titleinlist" + ): retry_count = 0 while retry_count < 2: - driver.execute_script("arguments[0].click();", watch_history_button) + driver.execute_script( + "arguments[0].click();", + watch_history_button, + ) try: - WebDriverWait(driver, 3).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'ipc-promptable-base__content')]//div[@data-titleinlist='true']"))) - - print(f" - Adding {item.get('Type')} ({item_count} of {num_items}): {episode_title}{item.get('Title')}{year_str} to IMDB Watch History ({item.get('IMDB_ID')})") - + WebDriverWait(driver, 3).until( + EC.presence_of_element_located( + ( + By.XPATH, + "//div[contains(@class, 'ipc-" + "promptable-base__content')]//" + "div[@data-titleinlist='true']", + ) + ) + ) + + print( + f" - Adding {item.get('Type')} " + f"({item_count} of {num_items}): " + f"{episode_title}{item.get('Title')}" + f"{year_str} to IMDB Watch History " + f"({item.get('IMDB_ID')})" + ) break # Break the loop if successful except TimeoutException: retry_count += 1 if retry_count == 2: - error_message = f"Failed to add {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} to IMDB Watch History ({item['IMDB_ID']})" + error_message = ( + f"Failed to add {item['Type']} " + f"({item_count} of {num_items}): " + f"{episode_title}{item['Title']}{year_str} " + f"to IMDB Watch History ({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message) else: - error_message1 = f" - Failed to add {item['Type']} ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} to IMDB Watch History ({item['IMDB_ID']})" - error_message2 = f" - {item['Type'].capitalize()} already exists in IMDB watch history." + error_message1 = ( + f" - Failed to add {item['Type']} ({item_count}" + f" of {num_items}): {episode_title}" + f"{item['Title']}{year_str} to IMDB Watch " + f"History ({item['IMDB_ID']})" + ) + error_message2 = ( + f" - {item['Type'].capitalize()} already " + "exists in IMDB watch history." + ) EL.logger.error(error_message1) EL.logger.error(error_message2) else: # Handle the case when the URL contains "/reference" - error_message1 = f"IMDB reference view setting is enabled. Adding items to IMDB Check-ins is not supported. See: https://www.imdb.com/preferences/general" - error_message2 = f"Failed to add item ({item_count} of {num_items}): {item['Title']}{year_str} to IMDB Watch History ({item['IMDB_ID']})" + error_message1 = ( + "IMDB reference view setting is enabled. Adding " + "items to IMDB Check-ins is not supported. See: " + "https://www.imdb.com/preferences/general" + ) + error_message2 = ( + f"Failed to add item ({item_count} of {num_items}):" + f" {item['Title']}{year_str} to IMDB Watch History " + f"({item['IMDB_ID']})" + ) print(f" - {error_message1}") print(f" - {error_message2}") EL.logger.error(error_message1) EL.logger.error(error_message2) - - except (NoSuchElementException, TimeoutException, PageLoadException): - error_message = f"Failed to add item ({item_count} of {num_items}): {episode_title}{item['Title']}{year_str} to IMDB Watch History ({item['IMDB_ID']})" + + except ( + NoSuchElementException, + TimeoutException, + PageLoadException, + ): + error_message = ( + f"Failed to add item ({item_count} of {num_items}): " + f"{episode_title}{item['Title']}{year_str} to IMDB " + f"Watch History ({item['IMDB_ID']})" + ) print(f" - {error_message}") EL.logger.error(error_message, exc_info=True) pass - - print('Setting IMDB Watch History Items Complete') + print("Setting IMDB Watch History Items Complete") else: - print('No IMDB Watch History Items To Set') - + print("No IMDB Watch History Items To Set") + # Change language back to original if was changed - if (original_language != "English (United States)"): - print("Changing IMDB Language Back to Original. See: https://www.imdb.com/preferences/general") + if original_language != "English (United States)": + print( + "Changing IMDB Language Back to Original. See: " + "https://www.imdb.com/preferences/general" + ) # go to IMDB homepage - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) + # Change Language Back to Original # Open Language Dropdown - language_dropdown = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "label[for*='nav-language-selector']"))) + language_dropdown = wait.until( + EC.element_to_be_clickable( + (By.CSS_SELECTOR, "label[for*='nav-language-selector']") + ) + ) driver.execute_script("arguments[0].click();", language_dropdown) # Change Language to Original - original_language_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, f"span[id*='nav-language-selector-contents'] li[aria-label*='{original_language}']"))) - driver.execute_script("arguments[0].click();", original_language_element) - + original_language_element = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + f"span[id*='nav-language-selector-contents'] " + f"li[aria-label*='{original_language}']", + ) + ) + ) + driver.execute_script( + "arguments[0].click();", original_language_element + ) # Find reference view checkbox if reference_view_changed: - print("Changing reference view IMDB setting back to original. See: https://www.imdb.com/preferences/general") + print( + "Changing reference view IMDB setting back to original. See: " + "https://www.imdb.com/preferences/general" + ) # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(f'https://www.imdb.com/preferences/general', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + f"https://www.imdb.com/preferences/general", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) # Click reference view checkbox - reference_checkbox = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "input[id*='ttdp']"))) + reference_checkbox = wait.until( + EC.element_to_be_clickable((By.CSS_SELECTOR, "input[id*='ttdp']")) + ) driver.execute_script("arguments[0].click();", reference_checkbox) # Submit - submit = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, ".article input[type*='submit']"))) + submit = wait.until( + EC.element_to_be_clickable( + (By.CSS_SELECTOR, ".article input[type*='submit']") + ) + ) driver.execute_script("arguments[0].click();", submit) time.sleep(1) - - #Close web driver + + # Close web driver print("Closing webdriver...") driver.close() driver.quit() service.stop() print("IMDBTraktSyncer Complete") - + except Exception as e: error_message = "An error occurred while running the script." EH.report_error(error_message) EL.logger.error(error_message, exc_info=True) - + # Close the driver and stop the service if they were initialized - if 'driver' in locals() and driver is not None: + if "driver" in locals() and driver is not None: driver.close() driver.quit() - if 'service' in locals() and service is not None: + if "service" in locals() and service is not None: service.stop() -if __name__ == '__main__': - main() \ No newline at end of file + +if __name__ == "__main__": + main() diff --git a/IMDBTraktSyncer/arguments.py b/IMDBTraktSyncer/arguments.py index 439b1d7..31567ff 100644 --- a/IMDBTraktSyncer/arguments.py +++ b/IMDBTraktSyncer/arguments.py @@ -6,6 +6,7 @@ import platform import stat + def try_remove(file_path, retries=3, delay=1): """ Tries to remove a file or directory, retrying if it's in use or read-only. @@ -22,21 +23,21 @@ def try_remove(file_path, retries=3, delay=1): for root, dirs, files in os.walk(file_path, topdown=False): for name in files: file = os.path.join(root, name) - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(file, 0o777) # Make file writable os.remove(file) for name in dirs: folder = os.path.join(root, name) - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(folder, 0o777) # Make folder writable os.rmdir(folder) - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(file_path, 0o777) # Make the top-level folder writable os.rmdir(file_path) # Finally, remove the directory else: # It's a file, ensure it's writable and remove it - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(file_path, 0o777) # Make it writable os.remove(file_path) @@ -50,7 +51,7 @@ def try_remove(file_path, retries=3, delay=1): time.sleep(delay) # If running on Windows, handle read-only files - if sys.platform == 'win32': + if sys.platform == "win32": try: # Remove read-only attribute on Windows if os.path.exists(file_path): @@ -66,39 +67,56 @@ def try_remove(file_path, retries=3, delay=1): return False + def get_selenium_install_location(): try: # Use pip show to get Selenium installation details - result = subprocess.run([sys.executable, '-m', 'pip', 'show', 'selenium'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + result = subprocess.run( + [sys.executable, "-m", "pip", "show", "selenium"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) for line in result.stdout.splitlines(): if line.startswith("Location:"): site_packages_directory = line.split("Location:")[1].strip() - selenium_directory = os.path.join(site_packages_directory, 'selenium') + selenium_directory = os.path.join(site_packages_directory, "selenium") return selenium_directory except Exception as e: print("Error finding Selenium install location using pip show:", e) return None + def clear_selenium_manager_cache(): try: # Get the Selenium install location selenium_install_location = get_selenium_install_location() if not selenium_install_location: - print("Could not determine Selenium install location. Skipping cache clear.") + print( + "Could not determine Selenium install location. Skipping cache clear." + ) return - webdriver_common_path = os.path.join(selenium_install_location, "webdriver", "common") - + webdriver_common_path = os.path.join( + selenium_install_location, "webdriver", "common" + ) + # Determine the OS and set the appropriate folder and file name os_name = platform.system().lower() if os_name == "windows": - selenium_manager_path = os.path.join(webdriver_common_path, "windows", "selenium-manager.exe") + selenium_manager_path = os.path.join( + webdriver_common_path, "windows", "selenium-manager.exe" + ) elif os_name == "linux": - selenium_manager_path = os.path.join(webdriver_common_path, "linux", "selenium-manager") + selenium_manager_path = os.path.join( + webdriver_common_path, "linux", "selenium-manager" + ) elif os_name == "darwin": # macOS - selenium_manager_path = os.path.join(webdriver_common_path, "macos", "selenium-manager") + selenium_manager_path = os.path.join( + webdriver_common_path, "macos", "selenium-manager" + ) else: print("Unsupported operating system.") return @@ -109,22 +127,35 @@ def clear_selenium_manager_cache(): return # Build the command - command = f"{selenium_manager_path} --clear-cache --browser chrome --driver chromedriver" + command = ( + f"{selenium_manager_path} --clear-cache --browser chrome " + f"--driver chromedriver" + ) try: # Run the command - result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + result = subprocess.run( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) print("Selenium Chromedriver cache cleared") except subprocess.CalledProcessError as e: print("Error running Selenium Manager command:", e.stderr) except Exception as e: print("An unexpected error occurred:", e) + def clear_user_data(main_directory): """ - Deletes the credentials.txt file and Chrome user data directories under the given main directory. + Deletes the credentials.txt file and Chrome user data directories under the given + main directory. - :param main_directory: Directory path where credentials.txt and user data should be deleted. + :param main_directory: Directory path where credentials.txt and user data should be + deleted. """ credentials_path = os.path.join(main_directory, "credentials.txt") @@ -135,7 +166,7 @@ def clear_user_data(main_directory): # Construct the Chrome user data directory path dynamically chrome_directory = os.path.join(main_directory, "Chrome") user_data_found = False - + if os.path.exists(chrome_directory): for root, dirs, files in os.walk(chrome_directory): for dir_name in dirs: @@ -143,46 +174,47 @@ def clear_user_data(main_directory): user_data_directory = os.path.join(root, dir_name) user_data_found = True break - + if user_data_found: try_remove(user_data_directory) print("Clear user data complete.") + def clear_cache(main_directory): """ - Deletes all folders, .zip files, .txt files (except credentials.txt) in the given directory and clears selenium chromedriver cache. - + Deletes all folders, .zip files, .txt files (except credentials.txt) in the given + directory and clears selenium chromedriver cache. :param main_directory: Directory path where data should be cleared. """ # Check if the given directory exists if not os.path.exists(main_directory): print(f"Error: The directory {main_directory} does not exist.") return - + # Walk through all files and folders in the directory for root, dirs, files in os.walk(main_directory, topdown=False): # Delete files first for file in files: file_path = os.path.join(root, file) - + # Skip deleting credentials.txt if file == "credentials.txt": print(f"Skipping {file_path} (credentials.txt)") continue # Skip this file - + # Remove .zip files if file.endswith(".zip"): try_remove(file_path) - + # Remove .txt files (except credentials.txt) elif file.endswith(".txt"): try_remove(file_path) - + # Remove .csv files elif file.endswith(".csv"): try_remove(file_path) - + # Delete directories after files for dir_name in dirs: dir_path = os.path.join(root, dir_name) @@ -195,46 +227,47 @@ def clear_cache(main_directory): print(f"Permission error removing directory {dir_path}: {e}") except Exception as e: print(f"Error removing directory {dir_path}: {e}") - + # Clear selenium chromedriver cache clear_selenium_manager_cache() - + print("Clear cache complete.") - + + def uninstall(main_directory): """ - Deletes all folders, .zip files, .txt files (except credentials.txt and log.txt) in the given directory and clears selenium chromedriver cache before uninstalling. - + Deletes all folders, .zip files, .txt files (except credentials.txt and log.txt) in + the given directory and clears selenium chromedriver cache before uninstalling. :param main_directory: Directory path where data should be cleared. """ # Check if the given directory exists if not os.path.exists(main_directory): print(f"Error: The directory {main_directory} does not exist.") return - + # Walk through all files and folders in the directory for root, dirs, files in os.walk(main_directory, topdown=False): # Delete files first for file in files: file_path = os.path.join(root, file) - + # Skip deleting credentials.txt if file in ["credentials.txt", "log.txt"]: print(f"Skipping {file_path} (credentials.txt)") continue # Skip this file - + # Remove .zip files if file.endswith(".zip"): try_remove(file_path) - + # Remove .txt files (except credentials.txt) elif file.endswith(".txt"): try_remove(file_path) - + # Remove .csv files elif file.endswith(".csv"): try_remove(file_path) - + # Delete directories after files for dir_name in dirs: dir_path = os.path.join(root, dir_name) @@ -247,45 +280,48 @@ def uninstall(main_directory): print(f"Permission error removing directory {dir_path}: {e}") except Exception as e: print(f"Error removing directory {dir_path}: {e}") - + # Clear selenium chromedriver cache clear_selenium_manager_cache() - + # Uninstall the package print("Uninstalling the package...") - subprocess.run([sys.executable, "-m", "pip", "uninstall", "-y", "IMDBTraktSyncer"], check=True) + subprocess.run( + [sys.executable, "-m", "pip", "uninstall", "-y", "IMDBTraktSyncer"], check=True + ) print("Uninstall complete.") + def clean_uninstall(main_directory): """ - Deletes all folders, .zip files, .txt files in the given directory and clears selenium chromedriver cache before uninstalling. - + Deletes all folders, .zip files, .txt files in the given directory and clears + selenium chromedriver cache before uninstalling. :param main_directory: Directory path where data should be cleared. """ # Check if the given directory exists if not os.path.exists(main_directory): print(f"Error: The directory {main_directory} does not exist.") return - + # Walk through all files and folders in the directory for root, dirs, files in os.walk(main_directory, topdown=False): # Delete files first for file in files: file_path = os.path.join(root, file) - + # Remove .zip files if file.endswith(".zip"): try_remove(file_path) - + # Remove .txt files (except credentials.txt) elif file.endswith(".txt"): try_remove(file_path) - + # Remove .csv files elif file.endswith(".csv"): try_remove(file_path) - + # Delete directories after files for dir_name in dirs: dir_path = os.path.join(root, dir_name) @@ -298,15 +334,18 @@ def clean_uninstall(main_directory): print(f"Permission error removing directory {dir_path}: {e}") except Exception as e: print(f"Error removing directory {dir_path}: {e}") - + # Clear selenium chromedriver cache clear_selenium_manager_cache() - + # Uninstall the package print("Uninstalling the package...") - subprocess.run([sys.executable, "-m", "pip", "uninstall", "-y", "IMDBTraktSyncer"], check=True) + subprocess.run( + [sys.executable, "-m", "pip", "uninstall", "-y", "IMDBTraktSyncer"], check=True + ) print("Clean uninstall complete.") + def print_directory(main_directory): - print(f"Install Directory: {main_directory}") \ No newline at end of file + print(f"Install Directory: {main_directory}") diff --git a/IMDBTraktSyncer/authTrakt.py b/IMDBTraktSyncer/authTrakt.py index b3bca9e..0813146 100644 --- a/IMDBTraktSyncer/authTrakt.py +++ b/IMDBTraktSyncer/authTrakt.py @@ -2,36 +2,38 @@ import time import sys from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from IMDBTraktSyncer import errorHandling as EH from IMDBTraktSyncer import errorLogger as EL + def authenticate(client_id, client_secret, refresh_token=None): CLIENT_ID = client_id CLIENT_SECRET = client_secret - REDIRECT_URI = 'urn:ietf:wg:oauth:2.0:oob' + REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob" if refresh_token: # If a refresh token is provided, use it to get a new access token data = { - 'refresh_token': refresh_token, - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET, - 'redirect_uri': REDIRECT_URI, - 'grant_type': 'refresh_token' - } - headers = { - 'Content-Type': 'application/json', + "refresh_token": refresh_token, + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "redirect_uri": REDIRECT_URI, + "grant_type": "refresh_token", } + headers = {"Content-Type": "application/json"} # Use make_trakt_request for the POST request - response = EH.make_trakt_request('https://api.trakt.tv/oauth/token', headers=headers, payload=data) + response = EH.make_trakt_request( + "https://api.trakt.tv/oauth/token", headers=headers, payload=data + ) if response: json_data = response.json() - ACCESS_TOKEN = json_data['access_token'] - REFRESH_TOKEN = json_data['refresh_token'] + ACCESS_TOKEN = json_data["access_token"] + REFRESH_TOKEN = json_data["refresh_token"] return ACCESS_TOKEN, REFRESH_TOKEN else: # empty response, invalid refresh token, prompt user to re-authenticate @@ -39,53 +41,58 @@ def authenticate(client_id, client_secret, refresh_token=None): else: # Set up the authorization endpoint URL - auth_url = 'https://trakt.tv/oauth/authorize' + auth_url = "https://trakt.tv/oauth/authorize" # Construct the authorization URL with the necessary parameters params = { - 'response_type': 'code', - 'client_id': CLIENT_ID, - 'redirect_uri': REDIRECT_URI, + "response_type": "code", + "client_id": CLIENT_ID, + "redirect_uri": REDIRECT_URI, } - auth_url += '?' + '&'.join([f'{key}={value}' for key, value in params.items()]) - + auth_url += "?" + "&".join([f"{key}={value}" for key, value in params.items()]) + # Print out the authorization URL and instruct the user to visit it - print(f'\nPlease visit the following URL to authorize this application: \n{auth_url}\n') - - # After the user grants authorization, they will be redirected back to the redirect URI with a temporary authorization code. - # Extract the authorization code from the URL and use it to request an access token from the Trakt API. - authorization_code = input('Please enter the authorization code from the URL: ') + print( + f"\nPlease visit the following URL to authorize this application: \n" + f"{auth_url}\n" + ) + + # After the user grants authorization, they will be redirected back to the + # redirect URI with a temporary authorization code. + # Extract the authorization code from the URL and use it to request an access + # token from the Trakt API. + authorization_code = input("Please enter the authorization code from the URL: ") if not authorization_code.strip(): raise ValueError("Authorization code cannot be empty.") # Set up the access token request data = { - 'code': authorization_code, - 'client_id': CLIENT_ID, - 'client_secret': CLIENT_SECRET, - 'redirect_uri': REDIRECT_URI, - 'grant_type': 'authorization_code' + "code": authorization_code, + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "redirect_uri": REDIRECT_URI, + "grant_type": "authorization_code", } - headers = { - 'Content-Type': 'application/json', - } - + headers = {"Content-Type": "application/json"} + # Use make_trakt_request for the POST request - response = EH.make_trakt_request('https://api.trakt.tv/oauth/token', headers=headers, payload=data) - + response = EH.make_trakt_request( + "https://api.trakt.tv/oauth/token", headers=headers, payload=data + ) + if response: # Parse the JSON response from the API json_data = response.json() # Extract the access token from the response - ACCESS_TOKEN = json_data['access_token'] + ACCESS_TOKEN = json_data["access_token"] # Extract the refresh token from the response - REFRESH_TOKEN = json_data['refresh_token'] - + REFRESH_TOKEN = json_data["refresh_token"] + return ACCESS_TOKEN, REFRESH_TOKEN else: # empty response, invalid refresh token, prompt user to re-authenticate return authenticate(CLIENT_ID, CLIENT_SECRET) - return None \ No newline at end of file + return None diff --git a/IMDBTraktSyncer/checkChrome.py b/IMDBTraktSyncer/checkChrome.py index 03cb5b0..422faee 100644 --- a/IMDBTraktSyncer/checkChrome.py +++ b/IMDBTraktSyncer/checkChrome.py @@ -11,24 +11,28 @@ from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from IMDBTraktSyncer import errorHandling as EH + def get_main_directory(): directory = os.path.dirname(os.path.realpath(__file__)) return directory - + + def get_browser_type(): # Change browser type # Valid options: "chrome" or "chrome-headless-shell" browser_type = "chrome" - + # Run headless setting # Valid options: True or False headless = True - + return browser_type, headless + def try_remove(file_path, retries=3, delay=1): """ Tries to remove a file or directory, retrying if it's in use or read-only. @@ -45,21 +49,21 @@ def try_remove(file_path, retries=3, delay=1): for root, dirs, files in os.walk(file_path, topdown=False): for name in files: file = os.path.join(root, name) - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(file, 0o777) # Make file writable os.remove(file) for name in dirs: folder = os.path.join(root, name) - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(folder, 0o777) # Make folder writable os.rmdir(folder) - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(file_path, 0o777) # Make the top-level folder writable os.rmdir(file_path) # Finally, remove the directory else: # It's a file, ensure it's writable and remove it - if sys.platform != 'win32': # chmod on Linux/macOS + if sys.platform != "win32": # chmod on Linux/macOS os.chmod(file_path, 0o777) # Make it writable os.remove(file_path) @@ -73,7 +77,7 @@ def try_remove(file_path, retries=3, delay=1): time.sleep(delay) # If running on Windows, handle read-only files - if sys.platform == 'win32': + if sys.platform == "win32": try: # Remove read-only attribute on Windows if os.path.exists(file_path): @@ -88,15 +92,17 @@ def try_remove(file_path, retries=3, delay=1): print(f"Error removing {file_path} on Windows: {e}") return False - + + def grant_permissions(path: Path): """ Recursively remove read-only attribute from a folder and its contents. - Ensures directories are accessible (add execute permission) and files are executable where needed. + Ensures directories are accessible (add execute permission) and files are executable + where needed. Compatible with macOS, Linux, and Windows. """ # Determine the operating system - is_windows = sys.platform.startswith('win') + is_windows = sys.platform.startswith("win") for root, dirs, files in os.walk(path): for item in dirs + files: @@ -105,27 +111,41 @@ def grant_permissions(path: Path): # For Windows: Ensure the file is not read-only if is_windows: # Set the read-only attribute to False for files (Windows) - os.chmod(item_path, item_path.stat().st_mode & ~stat.S_IREAD) # Remove read-only attribute + os.chmod( + item_path, item_path.stat().st_mode & ~stat.S_IREAD + ) # Remove read-only attribute else: # For Linux/macOS, add write permissions current_permissions = item_path.stat().st_mode - item_path.chmod(current_permissions | 0o777) # Add write permission (u+w) + item_path.chmod( + current_permissions | 0o777 + ) # Add write permission (u+w) # Ensure directories are executable if item_path.is_dir(): - item_path.chmod(current_permissions | 0o777) # Add execute permission (u+x) for directories + item_path.chmod( + current_permissions | 0o777 + ) # Add execute permission (u+x) for directories else: - # For files (including chromedriver), make sure they are executable - item_path.chmod(current_permissions | 0o777) # Add execute permission (u+x, g+x, o+x) for files + # For files (including chromedriver), make sure they are + # executable + item_path.chmod( + current_permissions | 0o777 + ) # Add execute permission (u+x, g+x, o+x) for files except PermissionError: print(f"Permission denied: Unable to modify {item_path}") except Exception as e: print(f"Error modifying permissions for {item_path}: {e}") + def get_user_data_directory(): - directory = os.path.dirname(os.path.realpath(__file__)) # Current script's directory - version = get_latest_stable_version() # Assuming this function exists and returns a version string + directory = os.path.dirname( + os.path.realpath(__file__) + ) # Current script's directory + version = ( + get_latest_stable_version() + ) # Assuming this function exists and returns a version string # Path to the version directory version_directory = Path(directory) / "Chrome" / version @@ -138,7 +158,9 @@ def get_user_data_directory(): break # Assume there's only one Chrome binary directory if not chrome_binary_directory: - raise FileNotFoundError(f"No Chrome binary directory found under {version_directory}") + raise FileNotFoundError( + f"No Chrome binary directory found under {version_directory}" + ) # Define the user data directory path user_data_directory = version_directory / chrome_binary_directory / "userData" @@ -151,19 +173,27 @@ def get_user_data_directory(): return user_data_directory + def get_latest_stable_version(): # Step 1: Get the latest stable version - stable_url = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json" + stable_url = ( + "https://googlechromelabs.github.io/chrome-for-testing/" + "last-known-good-versions.json" + ) stable_response = EH.make_request_with_retries(stable_url) stable_response.raise_for_status() stable_data = stable_response.json() - stable_version = stable_data['channels']['Stable']['version'] - + stable_version = stable_data["channels"]["Stable"]["version"] + return stable_version - + + def get_version_data(version): # Step 1: Fetch the data from the URL - versions_url = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json" + versions_url = ( + "https://googlechromelabs.github.io/chrome-for-testing/" + "known-good-versions-with-downloads.json" + ) response = EH.make_request_with_retries(versions_url) response.raise_for_status() data = response.json() @@ -173,10 +203,11 @@ def get_version_data(version): if entry["version"] == version: # Step 3: Return data for the matching version return entry - + # Step 4: If version is not found, return None return None + def get_platform(): system = platform.system().lower() arch = platform.machine().lower() @@ -190,35 +221,63 @@ def get_platform(): else: raise ValueError("Unsupported operating system") + def is_chrome_up_to_date(main_directory, current_version): chrome_dir = Path(main_directory) / "Chrome" / current_version - + if not chrome_dir.exists(): - # Chrome directory for version not found. Chrome not downloaded or not up to date. + # Chrome directory for version not found. Chrome not downloaded or not up to + # date. return False # Check for the Chrome binary depending on the platform platform_binary = { - "win32": ["chrome-headless-shell.exe", "chrome.exe"], # Two possible filenames for Windows - "win64": ["chrome-headless-shell.exe", "chrome.exe"], # Two possible filenames for Windows - "mac-arm64": ["chrome-headless-shell", "Google Chrome for Testing"], # Two possible filenames for macOS - "mac-x64": ["chrome-headless-shell", "Google Chrome for Testing"], # Two possible filenames for macOS - "linux64": ["chrome-headless-shell", "chrome"] # Two possible filenames for Linux + "win32": [ + "chrome-headless-shell.exe", + "chrome.exe", + ], # Two possible filenames for Windows + "win64": [ + "chrome-headless-shell.exe", + "chrome.exe", + ], # Two possible filenames for Windows + "mac-arm64": [ + "chrome-headless-shell", + "Google Chrome for Testing", + ], # Two possible filenames for macOS + "mac-x64": [ + "chrome-headless-shell", + "Google Chrome for Testing", + ], # Two possible filenames for macOS + "linux64": [ + "chrome-headless-shell", + "chrome", + ], # Two possible filenames for Linux } platform_key = get_platform() - binary_names = platform_binary.get(platform_key, ["chrome-headless-shell", "chrome"]) # Default to both names - + binary_names = platform_binary.get( + platform_key, ["chrome-headless-shell", "chrome"] + ) # Default to both names + # Logic for macOS special cases if platform_key in ["mac-arm64", "mac-x64"]: for subfolder in chrome_dir.iterdir(): if subfolder.is_dir(): for binary_name in binary_names: if binary_name == "Google Chrome for Testing": - # For macOS regular Chrome, the binary is inside the .app bundle in the version directory - binary_path = chrome_dir / subfolder / "Google Chrome for Testing.app" / "Contents" / "MacOS" / "Google Chrome for Testing" + # For macOS regular Chrome, the binary is inside the .app bundle + # in the version directory + binary_path = ( + chrome_dir + / subfolder + / "Google Chrome for Testing.app" + / "Contents" + / "MacOS" + / "Google Chrome for Testing" + ) else: - # For macOS headless shell, the binary is directly under the version directory + # For macOS headless shell, the binary is directly under the + # version directory binary_path = chrome_dir / subfolder / binary_name if binary_path.exists(): @@ -234,12 +293,14 @@ def is_chrome_up_to_date(main_directory, current_version): print(f"Chrome binary not found under {chrome_dir}.") return False - + + def is_chromedriver_up_to_date(main_directory, current_version): chromedriver_dir = Path(main_directory) / "Chromedriver" / current_version - + if not chromedriver_dir.exists(): - # Chromedriver directory for version not found. Chrome not downloaded or not up to date. + # Chromedriver directory for version not found. Chrome not downloaded or not up + # to date. return False # Check for the Chromedriver binary depending on the platform @@ -252,7 +313,9 @@ def is_chromedriver_up_to_date(main_directory, current_version): } platform_key = get_platform() - binary_names = platform_binary.get(platform_key, ["chromedriver"]) # Default to chromedriver + binary_names = platform_binary.get( + platform_key, ["chromedriver"] + ) # Default to chromedriver # Handle the additional subfolder under version for subfolder in chromedriver_dir.iterdir(): @@ -266,7 +329,10 @@ def is_chromedriver_up_to_date(main_directory, current_version): print(f"Chromedriver binary not found under {chromedriver_dir}.") return False -def download_and_extract_chrome(download_url, main_directory, version, max_wait_time=300, wait_interval=60): + +def download_and_extract_chrome( + download_url, main_directory, version, max_wait_time=300, wait_interval=60 +): zip_path = Path(main_directory) / f"chrome-{version}.zip" extract_path = Path(main_directory) / "Chrome" / version @@ -279,7 +345,7 @@ def download_and_extract_chrome(download_url, main_directory, version, max_wait_ response.raise_for_status() # Get the expected file size from the response headers (if available) - expected_file_size = int(response.headers.get('Content-Length', 0)) + expected_file_size = int(response.headers.get("Content-Length", 0)) print(f" - Expected file size: {expected_file_size} bytes") # Write the zip file to the final location @@ -289,29 +355,36 @@ def download_and_extract_chrome(download_url, main_directory, version, max_wait_ # Final wait to ensure the download is complete before extracting time.sleep(wait_interval) - + # Validate the downloaded file size actual_file_size = zip_path.stat().st_size print(f" - Downloaded file size: {actual_file_size} bytes") if expected_file_size and actual_file_size != expected_file_size: - raise RuntimeError(f" - Downloaded file size mismatch: expected {expected_file_size} bytes, got {actual_file_size} bytes") + raise RuntimeError( + f" - Downloaded file size mismatch: expected {expected_file_size} bytes" + f", got {actual_file_size} bytes" + ) # Verify the integrity of the ZIP file before extraction if not zipfile.is_zipfile(zip_path): - raise RuntimeError(f" - The downloaded file is not a valid ZIP archive: {zip_path}") + raise RuntimeError( + f" - The downloaded file is not a valid ZIP archive: {zip_path}" + ) # Extract the zip file with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(extract_path) print(f" - Extraction complete to: {extract_path}") - + # Remove read-only attribute from the extracted folder grant_permissions(extract_path) except Exception as e: - raise RuntimeError(f" - Failed to download or extract Chrome version {version}: {e}") + raise RuntimeError( + f" - Failed to download or extract Chrome version {version}: {e}" + ) finally: # Cleanup the ZIP file @@ -330,13 +403,19 @@ def download_and_extract_chrome(download_url, main_directory, version, max_wait_ print(f" - Failed to delete file {file}: {e}") except PermissionError: - print(f" - Permission denied when trying to delete {zip_path}. Ensure no other process is using it.") + print( + f" - Permission denied when trying to delete {zip_path}. Ensure no " + "other process is using it." + ) except Exception as e: print(f" - Unexpected error while deleting {zip_path}: {e}") return extract_path - -def download_and_extract_chromedriver(download_url, main_directory, version, max_wait_time=300, wait_interval=60): + + +def download_and_extract_chromedriver( + download_url, main_directory, version, max_wait_time=300, wait_interval=60 +): zip_path = Path(main_directory) / f"chromedriver-{version}.zip" extract_path = Path(main_directory) / "Chromedriver" / version @@ -349,7 +428,7 @@ def download_and_extract_chromedriver(download_url, main_directory, version, max response.raise_for_status() # Get the expected file size from the response headers (if available) - expected_file_size = int(response.headers.get('Content-Length', 0)) + expected_file_size = int(response.headers.get("Content-Length", 0)) print(f" - Expected file size: {expected_file_size} bytes") # Write the zip file to the final location @@ -359,29 +438,36 @@ def download_and_extract_chromedriver(download_url, main_directory, version, max # Final wait to ensure the download is complete before extracting time.sleep(wait_interval) - + # Validate the downloaded file size actual_file_size = zip_path.stat().st_size print(f" - Downloaded file size: {actual_file_size} bytes") if expected_file_size and actual_file_size != expected_file_size: - raise RuntimeError(f" - Downloaded file size mismatch: expected {expected_file_size} bytes, got {actual_file_size} bytes") + raise RuntimeError( + f" - Downloaded file size mismatch: expected {expected_file_size} bytes" + f", got {actual_file_size} bytes" + ) # Verify the integrity of the ZIP file before extraction if not zipfile.is_zipfile(zip_path): - raise RuntimeError(f" - The downloaded file is not a valid ZIP archive: {zip_path}") + raise RuntimeError( + f" - The downloaded file is not a valid ZIP archive: {zip_path}" + ) # Extract the zip file with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(extract_path) print(f" - Extraction complete to: {extract_path}") - + # Remove read-only attribute from the extracted folder grant_permissions(extract_path) except Exception as e: - raise RuntimeError(f" - Failed to download or extract Chromedriver version {version}: {e}") + raise RuntimeError( + f" - Failed to download or extract Chromedriver version {version}: {e}" + ) finally: # Clean up extracted files (excluding the chromedriver executable) @@ -392,13 +478,17 @@ def download_and_extract_chromedriver(download_url, main_directory, version, max # Clean up all files except the chromedriver executable if chromedriver_dir.exists(): for item in chromedriver_dir.iterdir(): - # Check if the item is a subfolder (assumed to be the one containing the binary files) + # Check if the item is a subfolder (assumed to be the one containing + # the binary files) if item.is_dir(): subfolder = item # Now, clean up files inside the subfolder for sub_item in subfolder.iterdir(): # Skip deleting chromedriver executable - if sub_item.name.lower() in ["chromedriver.exe", "chromedriver"]: + if sub_item.name.lower() in [ + "chromedriver.exe", + "chromedriver", + ]: continue try_remove(sub_item) # Remove other files @@ -417,27 +507,40 @@ def download_and_extract_chromedriver(download_url, main_directory, version, max print(f" - Failed to delete file {file}: {e}") except PermissionError: - print(f" - Permission denied when trying to delete {zip_path}. Ensure no other process is using it.") + print( + f" - Permission denied when trying to delete {zip_path}. Ensure no " + "other process is using it." + ) except Exception as e: print(f" - Unexpected error while deleting {zip_path}: {e}") return extract_path + def remove_old_versions(main_directory, latest_version, browser_type): chrome_dir = Path(main_directory) / "Chrome" chromedriver_dir = Path(main_directory) / "Chromedriver" - # Delete downloaded browser if not the correct type (chrome vs chrome-headless-shell) + # Delete downloaded browser if not the correct type (chrome vs + # chrome-headless-shell) if browser_type == "chrome": for version_dir in chrome_dir.iterdir(): if version_dir.is_dir(): for sub_dir in version_dir.iterdir(): if sub_dir.is_dir(): - # All platforms (including macOS) use the same path for chrome-headless-shell - headless_shell_path = sub_dir / "chrome-headless-shell" if os.name != "nt" else sub_dir / "chrome-headless-shell.exe" - + # All platforms (including macOS) use the same path for + # chrome-headless-shell + headless_shell_path = ( + sub_dir / "chrome-headless-shell" + if os.name != "nt" + else sub_dir / "chrome-headless-shell.exe" + ) if headless_shell_path.exists(): - print(f"chrome-headless-shell found in {headless_shell_path}, but script is set to use regular Chrome. Removing entire contents of {chrome_dir}...") + print( + f"chrome-headless-shell found in {headless_shell_path}," + f" but script is set to use regular Chrome. Removing " + f"entire contents of {chrome_dir}..." + ) try_remove(version_dir) return # Exit the function after removal elif browser_type == "chrome-headless-shell": @@ -447,12 +550,26 @@ def remove_old_versions(main_directory, latest_version, browser_type): if sub_dir.is_dir(): # macOS-specific path for regular Chrome binary if os.name == "posix": # macOS - chrome_path = sub_dir / "Google Chrome for Testing.app" / "Contents" / "MacOS" / "Google Chrome for Testing" + chrome_path = ( + sub_dir + / "Google Chrome for Testing.app" + / "Contents" + / "MacOS" + / "Google Chrome for Testing" + ) else: # Windows or Linux - chrome_path = sub_dir / "chrome" if os.name != "nt" else sub_dir / "chrome.exe" - + chrome_path = ( + sub_dir / "chrome" + if os.name != "nt" + else sub_dir / "chrome.exe" + ) + if chrome_path.exists(): - print(f"Chrome found in {chrome_path}, but script is set to use chrome-headless-shell. Removing entire contents of {chrome_dir}...") + print( + f"Chrome found in {chrome_path}, but script is set to " + "use chrome-headless-shell. Removing entire contents " + f"of {chrome_dir}..." + ) try_remove(version_dir) return # Exit the function after removal @@ -467,6 +584,7 @@ def remove_old_versions(main_directory, latest_version, browser_type): print(f"Removing old Chromedriver version: {version_dir.name}...") try_remove(version_dir) + def get_chrome_binary_path(main_directory): version = get_latest_stable_version() @@ -478,15 +596,32 @@ def get_chrome_binary_path(main_directory): # Define possible binary names for each platform platform_binary = { - "win32": ["chrome-headless-shell.exe", "chrome.exe"], # Two possible filenames for Windows - "win64": ["chrome-headless-shell.exe", "chrome.exe"], # Two possible filenames for Windows - "mac-arm64": ["chrome-headless-shell", "Google Chrome for Testing"], # Updated for macOS - "mac-x64": ["chrome-headless-shell", "Google Chrome for Testing"], # Updated for macOS - "linux64": ["chrome-headless-shell", "chrome"] # Two possible filenames for Linux + "win32": [ + "chrome-headless-shell.exe", + "chrome.exe", + ], # Two possible filenames for Windows + "win64": [ + "chrome-headless-shell.exe", + "chrome.exe", + ], # Two possible filenames for Windows + "mac-arm64": [ + "chrome-headless-shell", + "Google Chrome for Testing", + ], # Updated for macOS + "mac-x64": [ + "chrome-headless-shell", + "Google Chrome for Testing", + ], # Updated for macOS + "linux64": [ + "chrome-headless-shell", + "chrome", + ], # Two possible filenames for Linux } platform_key = get_platform() # Get the platform key (e.g., win32, mac-arm64, etc.) - binary_names = platform_binary.get(platform_key, ["chrome-headless-shell", "chrome"]) # Default to both names + binary_names = platform_binary.get( + platform_key, ["chrome-headless-shell", "chrome"] + ) # Default to both names # Logic for macOS special cases if platform_key in ["mac-arm64", "mac-x64"]: @@ -494,10 +629,19 @@ def get_chrome_binary_path(main_directory): if subfolder.is_dir(): for binary_name in binary_names: if binary_name == "Google Chrome for Testing": - # For macOS regular Chrome, the binary is inside the .app bundle in the version directory - binary_path = chrome_dir / subfolder / "Google Chrome for Testing.app" / "Contents" / "MacOS" / "Google Chrome for Testing" + # For macOS regular Chrome, the binary is inside the .app bundle + # in the version directory + binary_path = ( + chrome_dir + / subfolder + / "Google Chrome for Testing.app" + / "Contents" + / "MacOS" + / "Google Chrome for Testing" + ) else: - # For macOS headless shell, the binary is directly under the version directory + # For macOS headless shell, the binary is directly under the + # version directory binary_path = chrome_dir / subfolder / binary_name if binary_path.exists(): @@ -511,27 +655,34 @@ def get_chrome_binary_path(main_directory): if binary_path.exists(): # Check if the binary file exists return str(binary_path) - raise FileNotFoundError(f"No valid Chrome binary found for platform {platform_key} in {chrome_dir}") - + raise FileNotFoundError( + f"No valid Chrome binary found for platform {platform_key} in {chrome_dir}" + ) + + def get_chromedriver_binary_path(main_directory): version = get_latest_stable_version() chromedriver_dir = Path(main_directory) / "Chromedriver" / version - + if not chromedriver_dir.exists(): - raise FileNotFoundError(f"Chromedriver version {version} not found in {chromedriver_dir}") + raise FileNotFoundError( + f"Chromedriver version {version} not found in {chromedriver_dir}" + ) # Define possible binary names for each platform platform_binary = { "win32": ["chromedriver.exe"], # Windows binaries "win64": ["chromedriver.exe"], # Windows binaries - "mac-arm64": ["chromedriver"], # macOS binaries - "mac-x64": ["chromedriver"], # macOS binaries - "linux64": ["chromedriver"], # Linux binaries + "mac-arm64": ["chromedriver"], # macOS binaries + "mac-x64": ["chromedriver"], # macOS binaries + "linux64": ["chromedriver"], # Linux binaries } platform_key = get_platform() - binary_names = platform_binary.get(platform_key, ["chromedriver"]) # Default to chromedriver + binary_names = platform_binary.get( + platform_key, ["chromedriver"] + ) # Default to chromedriver # Look for the binary in the version directory for subfolder in chromedriver_dir.iterdir(): @@ -542,56 +693,75 @@ def get_chromedriver_binary_path(main_directory): return str(binary_path) raise FileNotFoundError(f"Chromedriver binary not found under {chromedriver_dir}.") - + + def create_chrome_directory(main_directory): chrome_dir = Path(main_directory) / "Chrome" - + if not chrome_dir.exists(): chrome_dir.mkdir(exist_ok=True) return chrome_dir - + + def create_chromedriver_directory(main_directory): chrome_dir = Path(main_directory) / "Chromedriver" - + if not chrome_dir.exists(): chrome_dir.mkdir(exist_ok=True) return chrome_dir - + + def get_selenium_install_location(): try: # Use pip show to get Selenium installation details - result = subprocess.run([sys.executable, '-m', 'pip', 'show', 'selenium'], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + result = subprocess.run( + [sys.executable, "-m", "pip", "show", "selenium"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) for line in result.stdout.splitlines(): if line.startswith("Location:"): site_packages_directory = line.split("Location:")[1].strip() - selenium_directory = os.path.join(site_packages_directory, 'selenium') + selenium_directory = os.path.join(site_packages_directory, "selenium") return selenium_directory except Exception as e: print("Error finding Selenium install location using pip show:", e) return None + def clear_selenium_manager_cache(): try: # Get the Selenium install location selenium_install_location = get_selenium_install_location() if not selenium_install_location: - print("Could not determine Selenium install location. Skipping cache clear.") + print( + "Could not determine Selenium install location. Skipping cache clear." + ) return - webdriver_common_path = os.path.join(selenium_install_location, "webdriver", "common") - + webdriver_common_path = os.path.join( + selenium_install_location, "webdriver", "common" + ) + # Determine the OS and set the appropriate folder and file name os_name = platform.system().lower() if os_name == "windows": - selenium_manager_path = os.path.join(webdriver_common_path, "windows", "selenium-manager.exe") + selenium_manager_path = os.path.join( + webdriver_common_path, "windows", "selenium-manager.exe" + ) elif os_name == "linux": - selenium_manager_path = os.path.join(webdriver_common_path, "linux", "selenium-manager") + selenium_manager_path = os.path.join( + webdriver_common_path, "linux", "selenium-manager" + ) elif os_name == "darwin": # macOS - selenium_manager_path = os.path.join(webdriver_common_path, "macos", "selenium-manager") + selenium_manager_path = os.path.join( + webdriver_common_path, "macos", "selenium-manager" + ) else: print("Unsupported operating system.") return @@ -602,20 +772,31 @@ def clear_selenium_manager_cache(): return # Build the command - command = f"{selenium_manager_path} --clear-cache --browser chrome --driver chromedriver" + command = ( + f"{selenium_manager_path} --clear-cache --browser chrome " + "--driver chromedriver" + ) try: # Run the command - result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + result = subprocess.run( + command, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) print("Selenium Chromedriver cache cleared") except subprocess.CalledProcessError as e: print("Error running Selenium Manager command:", e.stderr) except Exception as e: print("An unexpected error occurred:", e) + def checkChrome(): main_directory = get_main_directory() - + browser_type, _ = get_browser_type() # Get latest version details @@ -625,38 +806,40 @@ def checkChrome(): # Create Chrome directory if it doesn't exist create_chrome_directory(main_directory) - + # Create Chromedriver directory if it doesn't exist create_chromedriver_directory(main_directory) - + # Always remove old versions, even if the latest version is already downloaded remove_old_versions(main_directory, latest_version, browser_type) - + # Remove "read-only" attribute grant_permissions(main_directory) # Check if the latest versions of Chrome and Chromedriver are already downloaded - if is_chrome_up_to_date(main_directory, latest_version) and is_chromedriver_up_to_date(main_directory, latest_version): + if is_chrome_up_to_date( + main_directory, latest_version + ) and is_chromedriver_up_to_date(main_directory, latest_version): # Chrome and Chromedriver are already up to date return # Get the Chrome download URL for the relevant platform chrome_download_url = None - for entry in latest_version_data['downloads'][browser_type]: - if entry['platform'] == platform_key: - chrome_download_url = entry['url'] + for entry in latest_version_data["downloads"][browser_type]: + if entry["platform"] == platform_key: + chrome_download_url = entry["url"] break if not chrome_download_url: raise ValueError(f"No download available for platform {platform_key}") - + # Get the Chromedriver download URL for the relevant platform chromedriver_download_url = None - for entry in latest_version_data['downloads']["chromedriver"]: - if entry['platform'] == platform_key: - chromedriver_download_url = entry['url'] + for entry in latest_version_data["downloads"]["chromedriver"]: + if entry["platform"] == platform_key: + chromedriver_download_url = entry["url"] break if not chromedriver_download_url: @@ -666,11 +849,13 @@ def checkChrome(): print(f"Downloading Chrome version {latest_version}...") download_and_extract_chrome(chrome_download_url, main_directory, latest_version) print(f"Chrome version {latest_version} downloaded successfully.") - + # Download and extract the latest version of Chromedriver print(f"Downloading Chromedriver version {latest_version}...") - download_and_extract_chromedriver(chromedriver_download_url, main_directory, latest_version) + download_and_extract_chromedriver( + chromedriver_download_url, main_directory, latest_version + ) print(f"Chromedriver version {latest_version} downloaded successfully.") - + # Clear the Chromedriver cache after downloading the new version of Chrome - clear_selenium_manager_cache() \ No newline at end of file + clear_selenium_manager_cache() diff --git a/IMDBTraktSyncer/checkVersion.py b/IMDBTraktSyncer/checkVersion.py index 7159ebf..01a6208 100644 --- a/IMDBTraktSyncer/checkVersion.py +++ b/IMDBTraktSyncer/checkVersion.py @@ -3,43 +3,54 @@ import subprocess import sys + def get_installed_version(): try: result = subprocess.run( - [sys.executable, '-m', 'pip', 'show', 'IMDBTraktSyncer'], + [sys.executable, "-m", "pip", "show", "IMDBTraktSyncer"], capture_output=True, text=True, - check=True + check=True, ) for line in result.stdout.splitlines(): if line.startswith("Version:"): return line.split()[1] except subprocess.CalledProcessError as e: - print(f"Error: Could not retrieve IMDBTraktSyncer version using '{sys.executable} -m pip': {e}") + print( + f"Error: Could not retrieve IMDBTraktSyncer version using '{sys.executable}" + f" -m pip': {e}" + ) except FileNotFoundError: - print(f"Error: Python executable '{sys.executable}' does not have pip installed.") + print( + f"Error: Python executable '{sys.executable}' does not have pip installed." + ) return None + def get_latest_version(): try: - with urllib.request.urlopen("https://pypi.org/rss/project/imdbtraktsyncer/releases.xml") as response: + with urllib.request.urlopen( + "https://pypi.org/rss/project/imdbtraktsyncer/releases.xml" + ) as response: xml_data = response.read() root = ET.fromstring(xml_data) - for item in root.findall('./channel/item'): - title = item.find('title').text + for item in root.findall("./channel/item"): + title = item.find("title").text if title: return title except Exception as e: print(f"Error retrieving latest version: {e}") return None + def compare_versions(installed, latest): def parse_version(v): - return tuple(map(int, v.split('.'))) - + return tuple(map(int, v.split("."))) + return parse_version(installed) < parse_version(latest) + def checkVersion(): installed_version = get_installed_version() if not installed_version: @@ -52,8 +63,11 @@ def checkVersion(): return if compare_versions(installed_version, latest_version): - print(f"A new version of IMDBTraktSyncer is available: {latest_version} (installed: {installed_version}).") + print( + f"A new version of IMDBTraktSyncer is available: {latest_version} " + f"(installed: {installed_version})." + ) print("To update use: python -m pip install IMDBTraktSyncer --upgrade") print("Documentation: https://github.com/RileyXX/IMDB-Trakt-Syncer/releases") # else: - # print(f"IMDBTraktSyncer is up to date (installed: {installed_version})") \ No newline at end of file + # print(f"IMDBTraktSyncer is up to date (installed: {installed_version})") diff --git a/IMDBTraktSyncer/errorHandling.py b/IMDBTraktSyncer/errorHandling.py index d3c12dc..e76778b 100644 --- a/IMDBTraktSyncer/errorHandling.py +++ b/IMDBTraktSyncer/errorHandling.py @@ -1,6 +1,13 @@ import traceback import requests -from requests.exceptions import ConnectionError, RequestException, Timeout, TooManyRedirects, SSLError, ProxyError +from requests.exceptions import ( + ConnectionError, + RequestException, + Timeout, + TooManyRedirects, + SSLError, + ProxyError, +) import time import os import inspect @@ -10,22 +17,33 @@ from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By -from selenium.common.exceptions import WebDriverException, NoSuchElementException, TimeoutException, StaleElementReferenceException +from selenium.common.exceptions import ( + WebDriverException, + NoSuchElementException, + TimeoutException, + StaleElementReferenceException, +) from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import SessionNotCreatedException import sys from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from IMDBTraktSyncer import verifyCredentials as VC from IMDBTraktSyncer import errorLogger as EL + class PageLoadException(Exception): pass + def report_error(error_message): - github_issue_url = "https://github.com/RileyXX/IMDB-Trakt-Syncer/issues/new?template=bug_report.yml" + github_issue_url = ( + "https://github.com/RileyXX/IMDB-Trakt-Syncer/issues/new" + "?template=bug_report.yml" + ) traceback_info = traceback.format_exc() print("\n--- ERROR ---") @@ -37,24 +55,27 @@ def report_error(error_message): print(f"Submit the error here: {github_issue_url}") print("-" * 50) + def make_trakt_request(url, headers=None, params=None, payload=None, max_retries=5): - + # Set default headers if none are provided if headers is None: # Get credentials trakt_client_id, _, trakt_access_token, _, _, _ = VC.prompt_get_credentials() - + headers = { - 'Content-Type': 'application/json', - 'trakt-api-version': '2', - 'trakt-api-key': trakt_client_id, - 'Authorization': f'Bearer {trakt_access_token}' + "Content-Type": "application/json", + "trakt-api-version": "2", + "trakt-api-key": trakt_client_id, + "Authorization": f"Bearer {trakt_access_token}", } - + retry_delay = 1 # Initial delay between retries (in seconds) retry_attempts = 0 # Count of retry attempts made connection_timeout = 20 # Timeout for requests (in seconds) - total_wait_time = sum(retry_delay * (2 ** i) for i in range(max_retries)) # Total possible wait time + total_wait_time = sum( + retry_delay * (2**i) for i in range(max_retries) + ) # Total possible wait time # Retry loop to handle network errors or server overload scenarios while retry_attempts < max_retries: @@ -64,51 +85,79 @@ def make_trakt_request(url, headers=None, params=None, payload=None, max_retries if payload is None: if params: # GET request with query parameters - response = requests.get(url, headers=headers, params=params, timeout=connection_timeout) + response = requests.get( + url, headers=headers, params=params, timeout=connection_timeout + ) else: # GET request without query parameters - response = requests.get(url, headers=headers, timeout=connection_timeout) + response = requests.get( + url, headers=headers, timeout=connection_timeout + ) else: # POST request with JSON payload - response = requests.post(url, headers=headers, json=payload, timeout=connection_timeout) + response = requests.post( + url, headers=headers, json=payload, timeout=connection_timeout + ) # If request is successful, return the response if response.status_code in [200, 201, 204]: return response - + # Handle retryable server errors and rate limit exceeded elif response.status_code in [429, 500, 502, 503, 504, 520, 521, 522]: retry_attempts += 1 # Increment retry counter # Respect the 'Retry-After' header if provided, otherwise use default delay - retry_after = int(response.headers.get('Retry-After', retry_delay)) + retry_after = int(response.headers.get("Retry-After", retry_delay)) if response.status_code != 429: - remaining_time = total_wait_time - sum(retry_delay * (2 ** i) for i in range(retry_attempts)) - print(f" - Server returned {response.status_code}. Retrying after {retry_after}s... " - f"({retry_attempts}/{max_retries}) - Time remaining: {remaining_time}s") - EL.logger.warning(f"Server returned {response.status_code}. Retrying after {retry_after}s... " - f"({retry_attempts}/{max_retries}) - Time remaining: {remaining_time}s") + remaining_time = total_wait_time - sum( + retry_delay * (2**i) for i in range(retry_attempts) + ) + print( + f" - Server returned {response.status_code}. Retrying after " + f"{retry_after}s... ({retry_attempts}/{max_retries}) - Time " + f"remaining: {remaining_time}s" + ) + EL.logger.warning( + f"Server returned {response.status_code}. Retrying after " + f"{retry_after}s... ({retry_attempts}/{max_retries}) - Time " + f"remaining: {remaining_time}s" + ) time.sleep(retry_after) # Wait before retrying retry_delay *= 2 # Apply exponential backoff for retries - + else: # Handle non-retryable HTTP status codes status_message = get_trakt_message(response.status_code) - error_message = f"Request failed with status code {response.status_code}: {status_message}" + error_message = ( + f"Request failed with status code {response.status_code}: " + f"{status_message}" + ) print(f" - {error_message}") EL.logger.error(f"{error_message}. URL: {url}") return response # Exit with failure for non-retryable errors # Handle Network errors (connection issues, timeouts, SSL, etc.) - except (ConnectionError, Timeout, TooManyRedirects, SSLError, ProxyError) as network_error: + except ( + ConnectionError, + Timeout, + TooManyRedirects, + SSLError, + ProxyError, + ) as network_error: retry_attempts += 1 # Increment retry counter - remaining_time = total_wait_time - sum(retry_delay * (2 ** i) for i in range(retry_attempts)) - print(f" - Network error: {network_error}. Retrying ({retry_attempts}/{max_retries})... " - f"Time remaining: {remaining_time}s") - EL.logger.warning(f"Network error: {network_error}. Retrying ({retry_attempts}/{max_retries})... " - f"Time remaining: {remaining_time}s") - + remaining_time = total_wait_time - sum( + retry_delay * (2**i) for i in range(retry_attempts) + ) + print( + f" - Network error: {network_error}. Retrying ({retry_attempts}/" + f"{max_retries})... Time remaining: {remaining_time}s" + ) + EL.logger.warning( + f"Network error: {network_error}. Retrying ({retry_attempts}/" + f"{max_retries})... Time remaining: {remaining_time}s" + ) time.sleep(retry_delay) # Wait before retrying retry_delay *= 2 # Apply exponential backoff for retries @@ -125,6 +174,7 @@ def make_trakt_request(url, headers=None, params=None, payload=None, max_retries EL.logger.error(error_message) return None + def get_trakt_message(status_code): error_messages = { 200: "Success", @@ -148,10 +198,11 @@ def get_trakt_message(status_code): 504: "Service Unavailable - server overloaded (try again in 30s)", 520: "Service Unavailable - Cloudflare error", 521: "Service Unavailable - Cloudflare error", - 522: "Service Unavailable - Cloudflare error" + 522: "Service Unavailable - Cloudflare error", } return error_messages.get(status_code, "Unknown error") + def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_time=5): num_retries = total_wait_time // initial_wait_time wait_time = total_wait_time / num_retries @@ -162,8 +213,9 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t for retry in range(max_retries): try: start_time = time.time() # Track time taken for each retry attempt - - # Temporary solution to Chromium bug: Restart tab logic. See: https://issues.chromium.org/issues/386887881 + + # Temporary solution to Chromium bug: Restart tab logic. See: + # https://issues.chromium.org/issues/386887881 # Open a new tab and close any extras from previous iterations driver.execute_script("window.open();") new_tab = driver.window_handles[-1] @@ -179,21 +231,27 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t driver.get(url) # Wait until the status code becomes available - wait.until(lambda driver: driver.execute_script( - "return window.performance.getEntries().length > 0 && window.performance.getEntries()[0].responseStatus !== undefined" - )) + wait.until( + lambda driver: driver.execute_script( + "return window.performance.getEntries().length > 0 && " + "window.performance.getEntries()[0].responseStatus !== undefined" + ) + ) # Get the HTTP status code of the page using JavaScript status_code = driver.execute_script( "return window.performance.getEntries()[0].responseStatus;" ) - + # Update resolved_url with the current URL after potential redirects resolved_url = driver.current_url # Handle status codes if status_code is None or status_code == 0: - print(f" - Unable to determine page load status. Status code returned 0 or None. Retrying...") + print( + f" - Unable to determine page load status. Status code returned 0" + f" or None. Retrying..." + ) elapsed_time = time.time() - start_time # Time taken for this attempt total_time_spent += elapsed_time @@ -201,12 +259,17 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t print(" - Total wait time exceeded. Aborting.") return False, None, url, driver, wait - print(f" - Remaining time for retries: {int(total_wait_time - total_time_spent)} seconds.") + print( + " - Remaining time for retries: " + f"{int(total_wait_time - total_time_spent)} seconds." + ) time.sleep(wait_time) continue elif status_code >= 400: - raise PageLoadException(f'Failed to load page. Status code: {status_code}. URL: {url}') + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) else: # Page loaded successfully @@ -216,7 +279,10 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t frame = inspect.currentframe() lineno = frame.f_lineno filename = os.path.basename(inspect.getfile(frame)) - print(f" - TimeoutException: {str(e).splitlines()[0]} URL: {url} (File: {filename}, Line: {lineno})") + print( + f" - TimeoutException: {str(e).splitlines()[0]} URL: {url} (File: " + f"{filename}, Line: {lineno})" + ) elapsed_time = time.time() - start_time total_time_spent += elapsed_time @@ -225,7 +291,10 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t print(" - Total wait time exceeded. Aborting after timeout.") return False, None, url, driver, wait - print(f" - Retrying ({retry + 1}/{max_retries}) Time Remaining: {int(total_wait_time - total_time_spent)}s") + print( + f" - Retrying ({retry + 1}/{max_retries}) Time Remaining: " + f"{int(total_wait_time - total_time_spent)}s" + ) time.sleep(wait_time) continue @@ -233,7 +302,10 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t frame = inspect.currentframe() lineno = frame.f_lineno filename = os.path.basename(inspect.getfile(frame)) - print(f" - Selenium WebDriver Error: {str(e).splitlines()[0]} URL: {url} (File: {filename}, Line: {lineno})") + print( + f" - Selenium WebDriver Error: {str(e).splitlines()[0]} URL: {url} " + f"(File: {filename}, Line: {lineno})" + ) retryable_errors = [ "net::ERR_NAME_NOT_RESOLVED", @@ -246,7 +318,7 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t "net::ERR_SSL_PROTOCOL_ERROR", "net::ERR_CERT_COMMON_NAME_INVALID", "net::ERR_CERT_DATE_INVALID", - "net::ERR_NETWORK_CHANGED" + "net::ERR_NETWORK_CHANGED", ] if any(error in str(e) for error in retryable_errors): @@ -254,10 +326,15 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t total_time_spent += elapsed_time if total_time_spent >= total_wait_time: - print(" - Total wait time exceeded. Aborting after WebDriver error.") + print( + " - Total wait time exceeded. Aborting after WebDriver error." + ) return False, None, url, driver, wait - print(f" - Retryable network error detected. Retrying... Time Remaining: {int(total_wait_time - total_time_spent)}s") + print( + " - Retryable network error detected. Retrying... Time " + f"Remaining: {int(total_wait_time - total_time_spent)}s" + ) time.sleep(wait_time) continue @@ -269,7 +346,10 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t frame = inspect.currentframe() lineno = frame.f_lineno filename = os.path.basename(inspect.getfile(frame)) - print(f" - PageLoadException: {str(e).splitlines()[0]} URL: {url} (File: {filename}, Line: {lineno})") + print( + f" - PageLoadException: {str(e).splitlines()[0]} URL: {url} (File: " + f"{filename}, Line: {lineno})" + ) retryable_error_codes = [408, 425, 429, 500, 502, 503, 504] @@ -278,10 +358,15 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t total_time_spent += elapsed_time if total_time_spent >= total_wait_time: - print(" - Total wait time exceeded. Aborting after page load exception.") + print( + " - Total wait time exceeded. Aborting after page load exception." + ) return False, None, url, driver, wait - print(f" - Retryable error detected. Retrying... Time Remaining: {int(total_wait_time - total_time_spent)}s") + print( + " - Retryable error detected. Retrying... Time Remaining: " + f"{int(total_wait_time - total_time_spent)}s" + ) time.sleep(wait_time) continue @@ -291,8 +376,18 @@ def get_page_with_retries(url, driver, wait, total_wait_time=180, initial_wait_t print(" - All retries failed. Unable to load page.") return False, None, url, driver, wait - -def make_request_with_retries(url, method="GET", headers=None, params=None, payload=None, max_retries=5, timeout=(30, 300), stream=False): + + +def make_request_with_retries( + url, + method="GET", + headers=None, + params=None, + payload=None, + max_retries=5, + timeout=(30, 300), + stream=False, +): """ Make an HTTP request with retry logic for handling server and connection errors. @@ -317,9 +412,13 @@ def make_request_with_retries(url, method="GET", headers=None, params=None, payl try: # Make the HTTP request based on the method if method.upper() == "GET": - response = requests.get(url, headers=headers, params=params, timeout=timeout, stream=stream) + response = requests.get( + url, headers=headers, params=params, timeout=timeout, stream=stream + ) elif method.upper() == "POST": - response = requests.post(url, headers=headers, json=payload, timeout=timeout, stream=stream) + response = requests.post( + url, headers=headers, json=payload, timeout=timeout, stream=stream + ) else: raise ValueError(f"Unsupported HTTP method: {method}") @@ -330,19 +429,34 @@ def make_request_with_retries(url, method="GET", headers=None, params=None, payl # Handle retryable HTTP status codes (rate limits or server errors) if response.status_code in [429, 500, 502, 503, 504]: retry_after = int(response.headers.get("Retry-After", retry_delay)) - print(f"Server error {response.status_code}. Retrying in {retry_after} seconds...") + print( + f"Server error {response.status_code}. Retrying in {retry_after} " + "seconds..." + ) time.sleep(retry_after) retry_delay *= 2 # Exponential backoff retry_attempts += 1 else: # Non-retryable errors - print(f"Request failed with status code {response.status_code}: {response.text}") + print( + f"Request failed with status code {response.status_code}: " + f"{response.text}" + ) return None - except (ConnectionError, Timeout, TooManyRedirects, SSLError, ProxyError) as network_err: + except ( + ConnectionError, + Timeout, + TooManyRedirects, + SSLError, + ProxyError, + ) as network_err: # Handle network-related errors retry_attempts += 1 - print(f"Network error: {network_err}. Retrying in {retry_delay} seconds... (Attempt {retry_attempts}/{max_retries})") + print( + f"Network error: {network_err}. Retrying in {retry_delay} seconds... " + f"(Attempt {retry_attempts}/{max_retries})" + ) time.sleep(retry_delay) retry_delay *= 2 # Exponential backoff @@ -354,22 +468,28 @@ def make_request_with_retries(url, method="GET", headers=None, params=None, payl # If retries are exhausted, log the failure print(f"Max retries reached. Request to {url} failed.") return None - + + # Function to clean a title by removing non-alphanumeric characters def clean_title(title): - return re.sub(r'[^a-zA-Z0-9. ]', '', title).lower() - + return re.sub(r"[^a-zA-Z0-9. ]", "", title).lower() + + # Function to resolve IMDB_ID redirection using the driver def resolve_imdb_id_with_driver(imdb_id, driver, wait): try: # Construct the IMDB URL url = f"https://www.imdb.com/title/{imdb_id}/" - + # Load URL - success, status_code, resolved_url, driver, wait = get_page_with_retries(url, driver, wait) + success, status_code, resolved_url, driver, wait = get_page_with_retries( + url, driver, wait + ) if not success: - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {resolved_url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {resolved_url}" + ) + # Extract the redirected IMDB_ID from the resolved URL final_imdb_id = resolved_url.split("/title/")[1].split("/")[0] return final_imdb_id, driver, wait @@ -377,38 +497,41 @@ def resolve_imdb_id_with_driver(imdb_id, driver, wait): except Exception as e: print(f"Error resolving IMDB_ID {imdb_id}: {e}") return imdb_id, driver, wait # Return the original ID if an error occurs - -# Function to resolve and update outdated IMDB_IDs from the trakt list based on matching Title and Type comparison + + +# Function to resolve and update outdated IMDB_IDs from the trakt list based on matching +# Title and Type comparison def update_outdated_imdb_ids_from_trakt(trakt_list, imdb_list, driver, wait): - comparison_keys = ['Title', 'Type', 'IMDB_ID'] # Only compare Title and Type + comparison_keys = ["Title", "Type", "IMDB_ID"] # Only compare Title and Type # Group items by (Title, Type), cleaning the Title trakt_grouped = {} for item in trakt_list: if all(key in item for key in comparison_keys): # Clean Title before creating the key - cleaned_title = clean_title(item['Title']) - key = (cleaned_title, item['Type']) - trakt_grouped.setdefault(key, set()).add(item['IMDB_ID']) + cleaned_title = clean_title(item["Title"]) + key = (cleaned_title, item["Type"]) + trakt_grouped.setdefault(key, set()).add(item["IMDB_ID"]) imdb_grouped = {} for item in imdb_list: if all(key in item for key in comparison_keys): # Clean Title before creating the key - cleaned_title = clean_title(item['Title']) - key = (cleaned_title, item['Type']) - imdb_grouped.setdefault(key, set()).add(item['IMDB_ID']) + cleaned_title = clean_title(item["Title"]) + key = (cleaned_title, item["Type"]) + imdb_grouped.setdefault(key, set()).add(item["IMDB_ID"]) # Find conflicting items based on Title and Type where IMDB_IDs are different conflicting_items = { - key for key in trakt_grouped.keys() & imdb_grouped.keys() + key + for key in trakt_grouped.keys() & imdb_grouped.keys() if trakt_grouped[key] != imdb_grouped[key] } - - ''' + + """ print(f"Initial Conflicting Items: {conflicting_items}") - ''' - + """ + # Resolve conflicts by checking IMDB_ID redirection using the driver for key in conflicting_items: trakt_ids = trakt_grouped[key] @@ -417,65 +540,76 @@ def update_outdated_imdb_ids_from_trakt(trakt_list, imdb_list, driver, wait): # Resolve IMDB_IDs using the driver only for trakt_list resolved_trakt_ids = set() for trakt_id in trakt_ids: - resolved_id, driver, wait = resolve_imdb_id_with_driver(trakt_id, driver, wait) + resolved_id, driver, wait = resolve_imdb_id_with_driver( + trakt_id, driver, wait + ) resolved_trakt_ids.add(resolved_id) # Directly update IMDB_ID in the original trakt_list for item in trakt_list: - if item['IMDB_ID'] == trakt_id: - item['IMDB_ID'] = resolved_id + if item["IMDB_ID"] == trakt_id: + item["IMDB_ID"] = resolved_id # Skip resolving IMDB_IDs in imdb_list as they're already current resolved_imdb_ids = imdb_ids - - ''' + + """ # If resolved trakt IDs match imdb IDs, the conflict is considered resolved if resolved_trakt_ids == resolved_imdb_ids: print(f"Resolved conflict for: {key}") else: print(f"Conflict not resolved for: {key}") - ''' - - + """ + return trakt_list, imdb_list, driver, wait - + + # Function to filter out items that share the same Title, Year, and Type # AND have non-matching IMDB_ID values, using cleaned titles for comparison def filter_out_mismatched_items(trakt_list, IMDB_list): # Define the keys to be used for comparison - comparison_keys = ['Title', 'Year', 'Type', 'IMDB_ID'] + comparison_keys = ["Title", "Year", "Type", "IMDB_ID"] # Group items by (Title, Year, Type), cleaning the Title for comparison trakt_grouped = {} for item in trakt_list: if all(key in item for key in comparison_keys): - cleaned_title = clean_title(item['Title']) # Clean the Title for comparison - key = (cleaned_title, item['Year'], item['Type']) - trakt_grouped.setdefault(key, set()).add(item['IMDB_ID']) + cleaned_title = clean_title(item["Title"]) # Clean the Title for comparison + key = (cleaned_title, item["Year"], item["Type"]) + trakt_grouped.setdefault(key, set()).add(item["IMDB_ID"]) IMDB_grouped = {} for item in IMDB_list: if all(key in item for key in comparison_keys): - cleaned_title = clean_title(item['Title']) # Clean the Title for comparison - key = (cleaned_title, item['Year'], item['Type']) - IMDB_grouped.setdefault(key, set()).add(item['IMDB_ID']) + cleaned_title = clean_title(item["Title"]) # Clean the Title for comparison + key = (cleaned_title, item["Year"], item["Type"]) + IMDB_grouped.setdefault(key, set()).add(item["IMDB_ID"]) # Find conflicting items (same Title, Year, Type but different IMDB_IDs) conflicting_items = { - key for key in trakt_grouped.keys() & IMDB_grouped.keys() # Only consider shared keys + key + for key in trakt_grouped.keys() + & IMDB_grouped.keys() # Only consider shared keys if trakt_grouped[key] != IMDB_grouped[key] # Check if IMDB_IDs differ } # Filter out conflicting items from both lists filtered_trakt_list = [ - item for item in trakt_list if (clean_title(item['Title']), item['Year'], item['Type']) not in conflicting_items + item + for item in trakt_list + if (clean_title(item["Title"]), item["Year"], item["Type"]) + not in conflicting_items ] filtered_IMDB_list = [ - item for item in IMDB_list if (clean_title(item['Title']), item['Year'], item['Type']) not in conflicting_items + item + for item in IMDB_list + if (clean_title(item["Title"]), item["Year"], item["Type"]) + not in conflicting_items ] return filtered_trakt_list, filtered_IMDB_list - + + def filter_items(source_list, target_list, key="IMDB_ID"): """ Filters items from the target_list that are not already present in the source_list based on a key. @@ -490,49 +624,61 @@ def filter_items(source_list, target_list, key="IMDB_ID"): """ source_set = {item[key] for item in source_list} return [item for item in target_list if item[key] not in source_set] - -def remove_combined_watchlist_to_remove_items_from_watchlist_to_set_lists_by_imdb_id(combined_watchlist_to_remove, imdb_watchlist_to_set, trakt_watchlist_to_set): + + +def remove_combined_watchlist_to_remove_items_from_watchlist_to_set_lists_by_imdb_id( + combined_watchlist_to_remove, imdb_watchlist_to_set, trakt_watchlist_to_set +): # Extract IMDB_IDs from the items to remove - imdb_ids_to_remove = {item['IMDB_ID'] for item in combined_watchlist_to_remove} + imdb_ids_to_remove = {item["IMDB_ID"] for item in combined_watchlist_to_remove} # Filter imdb_watchlist_to_set, keeping items not in imdb_ids_to_remove imdb_watchlist_to_set = [ - item for item in imdb_watchlist_to_set if item['IMDB_ID'] not in imdb_ids_to_remove + item + for item in imdb_watchlist_to_set + if item["IMDB_ID"] not in imdb_ids_to_remove ] # Filter trakt_watchlist_to_set, keeping items not in imdb_ids_to_remove trakt_watchlist_to_set = [ - item for item in trakt_watchlist_to_set if item['IMDB_ID'] not in imdb_ids_to_remove + item + for item in trakt_watchlist_to_set + if item["IMDB_ID"] not in imdb_ids_to_remove ] return imdb_watchlist_to_set, trakt_watchlist_to_set - + + # Function to remove duplicates based on IMDB_ID def remove_duplicates_by_imdb_id(watched_content): seen = set() unique_content = [] for item in watched_content: - if item['IMDB_ID'] not in seen: + if item["IMDB_ID"] not in seen: unique_content.append(item) - seen.add(item['IMDB_ID']) + seen.add(item["IMDB_ID"]) return unique_content - + + # Function to remove items with Type 'show' def remove_shows(watched_content): filtered_content = [] for item in watched_content: - if item['Type'] != 'show': + if item["Type"] != "show": filtered_content.append(item) return filtered_content + # Filter out setting review IMDB where the comment length is less than 600 characters def filter_by_comment_length(lst, min_comment_length=None): result = [] for item in lst: - if min_comment_length is None or ('Comment' in item and len(item['Comment']) >= min_comment_length): + if min_comment_length is None or ( + "Comment" in item and len(item["Comment"]) >= min_comment_length + ): result.append(item) return result - + def sort_by_date_added(items, descending=False): """ @@ -540,13 +686,15 @@ def sort_by_date_added(items, descending=False): Args: items (list): A list of dictionaries or objects with a 'Date_Added' field. - descending (bool): Whether to sort in descending order. Defaults to False (ascending). + descending (bool): Whether to sort in descending order. Defaults to False + (ascending). Returns: list: A sorted list of items by the 'Date_Added' field. """ + def parse_date(item): - date_str = item.get('Date_Added') # Safely get the Date_Added field + date_str = item.get("Date_Added") # Safely get the Date_Added field if date_str: try: return datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ") @@ -555,7 +703,8 @@ def parse_date(item): return datetime.min # Use the earliest date as a fallback return sorted(items, key=parse_date, reverse=descending) - + + def get_items_older_than_x_days(items, days): """ Returns items older than a specified number of days based on the 'Date_Added' field. @@ -565,10 +714,12 @@ def get_items_older_than_x_days(items, days): days (int): The number of days to use as the cutoff. Returns: - list: A filtered list of items where 'Date_Added' is older than the specified number of days. + list: A filtered list of items where 'Date_Added' is older than the specified + number of days. """ + def is_older(item): - date_str = item.get('Date_Added') # Safely get the Date_Added field + date_str = item.get("Date_Added") # Safely get the Date_Added field if date_str: try: date_added = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ") @@ -579,98 +730,119 @@ def is_older(item): return False # Exclude items with invalid or missing dates return [item for item in items if is_older(item)] - + + def check_if_watch_history_limit_reached(list): """ Checks if the list has 10,000 or more items. If true, updates the sync_watch_history in credentials.txt to False and marks the watch history limit as reached. - + Args: list (list): List of the user's watch history. - + Returns: bool: True if the watch history limit has been reached, False otherwise. """ - - ''' + + """ # Define the file path for credentials.txt here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') - + file_path = os.path.join(here, "credentials.txt") + # Load the credentials file credentials = {} try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) except FileNotFoundError: - print("Credentials file not found. A new file will be created if needed.", exc_info=True) + print( + "Credentials file not found. A new file will be created if needed.", + exc_info=True, + ) return False # Return False if the file doesn't exist - ''' + """ # Check if list has 10,000 or more items if len(list) >= 9999: - ''' + """ # Update sync_watch_history to False credentials['sync_watch_history'] = False - ''' - print("WARNING: IMDB watch history has reached the 10,000 limit. New watch history items will be not added to IMDB.") + """ + print( + "WARNING: IMDB watch history has reached the 10,000 limit. New watch " + "history items will be not added to IMDB." + ) return True # Return True indicating limit reached and updated the credentials - - ''' + + """ # Mark that the watch history limit has been reached try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) - print("IMDB watch history has reached the 10,000 item limit. sync_watch_history value set to False. Watch history will no longer be synced.") - return True # Return True indicating limit reached and updated the credentials + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) + print( + "IMDB watch history has reached the 10,000 item limit. " + "sync_watch_history value set to False. Watch history will no longer be" + " synced." + ) + return ( + True # Return True indicating limit reached and updated the credentials + ) except Exception as e: print("Failed to write to credentials file.", exc_info=True) return False # Return False if there was an error while updating the file - ''' - + """ # Return False if the limit hasn't been reached return False - + + def check_if_watchlist_limit_reached(list): """ Checks if the list has 10,000 or more items. If true, updates the sync_watchlist in credentials.txt to False and marks the watchlist limit as reached. - + Args: list (list): List of the user's watchlist. - + Returns: bool: True if the watchlist limit has been reached, False otherwise. """ # Define the file path for credentials.txt here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') - + file_path = os.path.join(here, "credentials.txt") + # Load the credentials file credentials = {} try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) except FileNotFoundError: - print("Credentials file not found. A new file will be created if needed.", exc_info=True) + print( + "Credentials file not found. A new file will be created if needed.", + exc_info=True, + ) return False # Return False if the file doesn't exist # Check if list has 10,000 or more items if len(list) >= 9999: # Update sync_watchlist to False - credentials['sync_watchlist'] = False - + credentials["sync_watchlist"] = False + # Mark that the watchlist limit has been reached try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) - print("IMDB watchlist has reached the 10,000 item limit. sync_watchlist value set to False. Watchlist will no longer be synced.") - return True # Return True indicating limit reached and updated the credentials + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) + print( + "IMDB watchlist has reached the 10,000 item limit. sync_watchlist value " + "set to False. Watchlist will no longer be synced." + ) + return ( + True # Return True indicating limit reached and updated the credentials + ) except Exception as e: print("Failed to write to credentials file.", exc_info=True) return False # Return False if there was an error while updating the file # Return False if the limit hasn't been reached - return False \ No newline at end of file + return False diff --git a/IMDBTraktSyncer/errorLogger.py b/IMDBTraktSyncer/errorLogger.py index 1b7d0b5..d9e8fc3 100644 --- a/IMDBTraktSyncer/errorLogger.py +++ b/IMDBTraktSyncer/errorLogger.py @@ -4,6 +4,7 @@ import selenium.webdriver from logging.handlers import RotatingFileHandler + class CustomFormatter(logging.Formatter): def formatException(self, exc_info): result = super().formatException(exc_info) @@ -25,22 +26,25 @@ def format(self, record): message = super().format(record) return f"{'`' * 100}\n{message}\n{'`' * 100}\n" + # Get the directory of the script script_dir = os.path.dirname(os.path.abspath(__file__)) # Set up the logging configuration -log_file = os.path.join(script_dir, 'log.txt') +log_file = os.path.join(script_dir, "log.txt") max_file_size = 1024 * 1024 # 1 MB backup_count = 0 # Set to 0 for only one log file # Create a rotating file handler -handler = RotatingFileHandler(log_file, maxBytes=max_file_size, backupCount=backup_count) +handler = RotatingFileHandler( + log_file, maxBytes=max_file_size, backupCount=backup_count +) handler.setLevel(logging.ERROR) # Set the log format -formatter = CustomFormatter('%(asctime)s - %(levelname)s - %(message)s') +formatter = CustomFormatter("%(asctime)s - %(levelname)s - %(message)s") handler.setFormatter(formatter) # Get the root logger and add the handler -logger = logging.getLogger('') -logger.addHandler(handler) \ No newline at end of file +logger = logging.getLogger("") +logger.addHandler(handler) diff --git a/IMDBTraktSyncer/imdbData.py b/IMDBTraktSyncer/imdbData.py index 63cba86..fdbbef0 100644 --- a/IMDBTraktSyncer/imdbData.py +++ b/IMDBTraktSyncer/imdbData.py @@ -9,55 +9,108 @@ from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys -from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException +from selenium.common.exceptions import ( + NoSuchElementException, + TimeoutException, + StaleElementReferenceException, +) from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import SessionNotCreatedException import sys from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from IMDBTraktSyncer import errorHandling as EH from IMDBTraktSyncer import errorLogger as EL + class PageLoadException(Exception): pass -def generate_imdb_exports(driver, wait, directory, sync_watchlist_value, sync_ratings_value, sync_watch_history_value, remove_watched_from_watchlists_value, mark_rated_as_watched_value): + +def generate_imdb_exports( + driver, + wait, + directory, + sync_watchlist_value, + sync_ratings_value, + sync_watch_history_value, + remove_watched_from_watchlists_value, + mark_rated_as_watched_value, +): # Generate IMDB .csv exports - + # Generate watchlist export if sync_watchlist_value is True if sync_watchlist_value or remove_watched_from_watchlists_value: - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/list/watchlist', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/list/watchlist", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) - export_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "div[data-testid*='hero-list-subnav-export-button'] button"))) + export_button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "div[data-testid*='hero-list-subnav-export-button'] button", + ) + ) + ) export_button.click() time.sleep(3) - + # Generate ratings export if sync_ratings_value is True if sync_ratings_value or mark_rated_as_watched_value: - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/list/ratings', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/list/ratings", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) - export_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "div[data-testid*='hero-list-subnav-export-button'] button"))) + export_button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "div[data-testid*='hero-list-subnav-export-button'] button", + ) + ) + ) export_button.click() time.sleep(3) - + # Generate checkins export if sync_watch_history_value is True - if sync_watch_history_value or remove_watched_from_watchlists_value or mark_rated_as_watched_value: - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/list/checkins', driver, wait) + if ( + sync_watch_history_value + or remove_watched_from_watchlists_value + or mark_rated_as_watched_value + ): + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/list/checkins", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) - export_button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "div[data-testid*='hero-list-subnav-export-button'] button"))) + export_button = wait.until( + EC.element_to_be_clickable( + ( + By.CSS_SELECTOR, + "div[data-testid*='hero-list-subnav-export-button'] button", + ) + ) + ) export_button.click() time.sleep(3) - + # Wait for export processing to finish # Function to check if any summary item contains "in progress" def check_in_progress(summary_items): @@ -65,32 +118,44 @@ def check_in_progress(summary_items): if "in progress" in item.text.lower(): return True return False + # Maximum time to wait in seconds max_wait_time = 1200 start_time = time.time() while time.time() - start_time < max_wait_time: # Wait for export processing to finish - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/exports/', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/exports/", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) + # Locate all elements with the selector - summary_items = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".ipc-metadata-list-summary-item"))) + summary_items = wait.until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, ".ipc-metadata-list-summary-item") + ) + ) # Check if any summary item contains "in progress" if not check_in_progress(summary_items): - #print("No 'in progress' found. Proceeding.") + # print("No 'in progress' found. Proceeding.") break else: - #print("'In progress' found. Waiting for 30 seconds before retrying.") + # print("'In progress' found. Waiting for 30 seconds before retrying.") time.sleep(30) else: - raise TimeoutError("IMDB data processing did not complete within the allotted 20 minutes.") - + raise TimeoutError( + "IMDB data processing did not complete within the allotted 20 minutes." + ) + return driver, wait - + + def grant_permissions_and_rename_file(src_path, dest_name): """ Grant full permissions to the file and rename it to the given name. @@ -100,67 +165,105 @@ def grant_permissions_and_rename_file(src_path, dest_name): try: # Grant full permissions os.chmod(src_path, 0o777) - + # Rename the file dest_path = os.path.join(os.path.dirname(src_path), dest_name) os.rename(src_path, dest_path) except Exception as e: print(f"Error while renaming file {src_path} to {dest_name}: {e}") - -def download_imdb_exports(driver, wait, directory, sync_watchlist_value, sync_ratings_value, sync_watch_history_value, remove_watched_from_watchlists_value, mark_rated_as_watched_value): + + +def download_imdb_exports( + driver, + wait, + directory, + sync_watchlist_value, + sync_ratings_value, + sync_watch_history_value, + remove_watched_from_watchlists_value, + mark_rated_as_watched_value, +): """ Download IMDB Exports and rename files with correct permissions. """ # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/exports/', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/exports/", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) # Locate all elements with the selector - summary_items = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".ipc-metadata-list-summary-item"))) + summary_items = wait.until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, ".ipc-metadata-list-summary-item") + ) + ) # Helper function to find buttons for CSV downloads def find_button(item_text): for item in summary_items: if item_text.lower() in item.text.lower(): - button = item.find_element(By.CSS_SELECTOR, "button[data-testid*='export-status-button']") + button = item.find_element( + By.CSS_SELECTOR, "button[data-testid*='export-status-button']" + ) if button: return button return None # Find download buttons - watchlist_csv_link = find_button("watchlist") if sync_watchlist_value or remove_watched_from_watchlists_value else None - ratings_csv_link = find_button("ratings") if sync_ratings_value or mark_rated_as_watched_value else None - checkins_csv_link = find_button("check-ins") if sync_watch_history_value or remove_watched_from_watchlists_value or mark_rated_as_watched_value else None + watchlist_csv_link = ( + find_button("watchlist") + if sync_watchlist_value or remove_watched_from_watchlists_value + else None + ) + ratings_csv_link = ( + find_button("ratings") + if sync_ratings_value or mark_rated_as_watched_value + else None + ) + checkins_csv_link = ( + find_button("check-ins") + if sync_watch_history_value + or remove_watched_from_watchlists_value + or mark_rated_as_watched_value + else None + ) # Clear any previous csv files for file in os.listdir(directory): - if file.endswith('.csv'): + if file.endswith(".csv"): os.remove(os.path.join(directory, file)) # Download each file and rename accordingly file_mappings = [ (watchlist_csv_link, "watchlist.csv"), (ratings_csv_link, "ratings.csv"), - (checkins_csv_link, "checkins.csv") + (checkins_csv_link, "checkins.csv"), ] - + for csv_link, file_name in file_mappings: if csv_link: # Scroll into view and click the button driver.execute_script("arguments[0].scrollIntoView(true);", csv_link) wait.until(EC.visibility_of(csv_link)) driver.execute_script("arguments[0].click();", csv_link) - + # Wait for download to complete time.sleep(10) # Find the most recent file in the directory downloaded_files = sorted( - [os.path.join(directory, f) for f in os.listdir(directory) if f.endswith('.csv')], + [ + os.path.join(directory, f) + for f in os.listdir(directory) + if f.endswith(".csv") + ], key=os.path.getmtime, - reverse=True + reverse=True, ) if downloaded_files: grant_permissions_and_rename_file(downloaded_files[0], file_name) @@ -169,279 +272,377 @@ def find_button(item_text): return driver, wait + def get_imdb_watchlist(driver, wait, directory): # Get IMDB Watchlist Items imdb_watchlist = [] - try: + try: # Look for 'watchlist.csv' - watchlist_filename = 'watchlist.csv' + watchlist_filename = "watchlist.csv" watchlist_path = os.path.join(directory, watchlist_filename) if not os.path.exists(watchlist_path): - raise FileNotFoundError(f"IMDB watchlist data not found. '{watchlist_filename}' not found in the directory") - + raise FileNotFoundError( + f"IMDB watchlist data not found. '{watchlist_filename}' not found in " + "the directory" + ) # Open and process the 'watchlist.csv' file - with open(watchlist_path, 'r', encoding='utf-8') as file: + with open(watchlist_path, "r", encoding="utf-8") as file: reader = csv.reader(file) header = next(reader) # Read the header row # Create a mapping from header names to their index - header_index = {column_name: index for index, column_name in enumerate(header)} - + header_index = { + column_name: index for index, column_name in enumerate(header) + } + required_columns = ["Title", "Year", "Const", "Created", "Title Type"] - missing_columns = [col for col in required_columns if col not in header_index] + missing_columns = [ + col for col in required_columns if col not in header_index + ] if missing_columns: - raise ValueError(f"Required columns missing from CSV file: {', '.join(missing_columns)}") + raise ValueError( + f"Required columns missing from CSV file: " + f"{', '.join(missing_columns)}" + ) for row in reader: - title = row[header_index['Title']] - year = row[header_index['Year']] + title = row[header_index["Title"]] + year = row[header_index["Year"]] year = int(year) if year else None - imdb_id = row[header_index['Const']] - date_added = row[header_index['Created']] - media_type = row[header_index['Title Type']] + imdb_id = row[header_index["Const"]] + date_added = row[header_index["Created"]] + media_type = row[header_index["Title Type"]] # Convert date format - date_added = datetime.strptime(date_added, '%Y-%m-%d').strftime('%Y-%m-%dT%H:%M:%S.000Z') + date_added = datetime.strptime(date_added, "%Y-%m-%d").strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ) if media_type in ["TV Series", "TV Mini Series"]: media_type = "show" elif media_type == "TV Episode": media_type = "episode" - elif media_type in ["Movie", "TV Special", "TV Movie", "TV Short", "Video"]: + elif media_type in [ + "Movie", + "TV Special", + "TV Movie", + "TV Short", + "Video", + ]: media_type = "movie" else: media_type = "unknown" if media_type != "unknown": - imdb_watchlist.append({ - 'Title': title, - 'Year': year, - 'IMDB_ID': imdb_id, - 'Date_Added': date_added, - 'Type': media_type - }) - + imdb_watchlist.append( + { + "Title": title, + "Year": year, + "IMDB_ID": imdb_id, + "Date_Added": date_added, + "Type": media_type, + } + ) + # Delete 'watchlist.csv' if os.path.exists(watchlist_path): os.remove(watchlist_path) - + except FileNotFoundError as e: error_message = str(e) print(f"Error: {error_message}") traceback.print_exc() EL.logger.error(error_message, exc_info=True) - + except (NoSuchElementException, TimeoutException): # No IMDB Watchlist Items imdb_watchlist = [] pass - + return imdb_watchlist, driver, wait + def get_imdb_ratings(driver, wait, directory): # Get IMDB Ratings imdb_ratings = [] try: # Look for 'ratings.csv' - ratings_filename = 'ratings.csv' + ratings_filename = "ratings.csv" ratings_path = os.path.join(directory, ratings_filename) if not os.path.exists(ratings_path): - raise FileNotFoundError(f"IMDB ratings data not found. '{ratings_filename}' not found in the directory") - + raise FileNotFoundError( + f"IMDB ratings data not found. '{ratings_filename}' not found in the " + f"directory" + ) # Open and process the 'ratings.csv' file - with open(ratings_path, 'r', encoding='utf-8') as file: + with open(ratings_path, "r", encoding="utf-8") as file: reader = csv.reader(file) header = next(reader) # Read the header row # Create a mapping from header names to their index header_index = {column: index for index, column in enumerate(header)} - - required_columns = ["Title", "Year", "Your Rating", "Const", "Date Rated", "Title Type"] - missing_columns = [col for col in required_columns if col not in header_index] + + required_columns = [ + "Title", + "Year", + "Your Rating", + "Const", + "Date Rated", + "Title Type", + ] + missing_columns = [ + col for col in required_columns if col not in header_index + ] if missing_columns: - raise ValueError(f"Required columns missing from CSV file: {', '.join(missing_columns)}") + raise ValueError( + f"Required columns missing from CSV file: " + f"{', '.join(missing_columns)}" + ) for row in reader: - title = row[header_index['Title']] - year = row[header_index['Year']] + title = row[header_index["Title"]] + year = row[header_index["Year"]] year = int(year) if year else None - rating = row[header_index['Your Rating']] - imdb_id = row[header_index['Const']] - date_added = row[header_index['Date Rated']] - watched_at = row[header_index['Date Rated']] - media_type = row[header_index['Title Type']] + rating = row[header_index["Your Rating"]] + imdb_id = row[header_index["Const"]] + date_added = row[header_index["Date Rated"]] + watched_at = row[header_index["Date Rated"]] + media_type = row[header_index["Title Type"]] # Convert date format - date_added = datetime.strptime(date_added, '%Y-%m-%d').strftime('%Y-%m-%dT%H:%M:%S.000Z') + date_added = datetime.strptime(date_added, "%Y-%m-%d").strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ) if media_type == "TV Series" or media_type == "TV Mini Series": media_type = "show" elif media_type == "TV Episode": media_type = "episode" - elif media_type in ["Movie", "TV Special", "TV Movie", "TV Short", "Video"]: + elif media_type in [ + "Movie", + "TV Special", + "TV Movie", + "TV Short", + "Video", + ]: media_type = "movie" else: media_type = "unknown" - + # Append to the list if media_type != "unknown": - imdb_ratings.append({ - 'Title': title, - 'Year': year, - 'Rating': int(rating), - 'IMDB_ID': imdb_id, - 'Date_Added': date_added, - 'WatchedAt': date_added, - 'Type': media_type - }) - + imdb_ratings.append( + { + "Title": title, + "Year": year, + "Rating": int(rating), + "IMDB_ID": imdb_id, + "Date_Added": date_added, + "WatchedAt": date_added, + "Type": media_type, + } + ) + # Delete 'ratings.csv' if os.path.exists(ratings_path): os.remove(ratings_path) - + except FileNotFoundError as e: error_message = str(e) print(f"Error: {error_message}") traceback.print_exc() EL.logger.error(error_message, exc_info=True) - + except (NoSuchElementException, TimeoutException): # No IMDB Ratings Items imdb_ratings = [] pass - + return imdb_ratings, driver, wait - + + def get_imdb_checkins(driver, wait, directory): # Get IMDB Check-ins imdb_checkins = [] try: # Look for 'checkins.csv' - checkins_filename = 'checkins.csv' + checkins_filename = "checkins.csv" checkins_path = os.path.join(directory, checkins_filename) if not os.path.exists(checkins_path): - raise FileNotFoundError(f"IMDB Check-ins data not found. '{checkins_filename}' not found in the directory") - + raise FileNotFoundError( + f"IMDB Check-ins data not found. '{checkins_filename}' not found in the" + f" directory" + ) # Open and process the 'checkins.csv' file - with open(checkins_path, 'r', encoding='utf-8') as file: + with open(checkins_path, "r", encoding="utf-8") as file: reader = csv.reader(file) header = next(reader) # Read the header row # Create a mapping from header names to their index header_index = {column: index for index, column in enumerate(header)} - + required_columns = ["Title", "Year", "Const", "Created", "Title Type"] - missing_columns = [col for col in required_columns if col not in header_index] + missing_columns = [ + col for col in required_columns if col not in header_index + ] if missing_columns: - raise ValueError(f"Required columns missing from CSV file: {', '.join(missing_columns)}") + raise ValueError( + f"Required columns missing from CSV file: " + f"{', '.join(missing_columns)}" + ) for row in reader: - title = row[header_index['Title']] - year = row[header_index['Year']] + title = row[header_index["Title"]] + year = row[header_index["Year"]] year = int(year) if year else None - imdb_id = row[header_index['Const']] - date_added = row[header_index['Created']] - media_type = row[header_index['Title Type']] + imdb_id = row[header_index["Const"]] + date_added = row[header_index["Created"]] + media_type = row[header_index["Title Type"]] # Convert date format - date_added = datetime.strptime(date_added, '%Y-%m-%d').strftime('%Y-%m-%dT%H:%M:%S.000Z') + date_added = datetime.strptime(date_added, "%Y-%m-%d").strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ) if media_type in ["TV Series", "TV Mini Series"]: media_type = "show" elif media_type == "TV Episode": media_type = "episode" - elif media_type in ["Movie", "TV Special", "TV Movie", "TV Short", "Video"]: + elif media_type in [ + "Movie", + "TV Special", + "TV Movie", + "TV Short", + "Video", + ]: media_type = "movie" else: media_type = "unknown" if media_type != "unknown": - imdb_checkins.append({ - 'Title': title, - 'Year': year, - 'IMDB_ID': imdb_id, - 'Date_Added': date_added, - 'WatchedAt': date_added, - 'Type': media_type - }) - + imdb_checkins.append( + { + "Title": title, + "Year": year, + "IMDB_ID": imdb_id, + "Date_Added": date_added, + "WatchedAt": date_added, + "Type": media_type, + } + ) + # Delete 'checkins.csv' if os.path.exists(checkins_path): os.remove(checkins_path) - + except FileNotFoundError as e: error_message = str(e) print(f"Error: {error_message}") traceback.print_exc() EL.logger.error(error_message, exc_info=True) - + except (NoSuchElementException, TimeoutException): # No IMDB Check-in Items imdb_checkins = [] pass - + return imdb_checkins, driver, wait - + + def get_media_type(imdb_id): url = f"https://api.trakt.tv/search/imdb/{imdb_id}" response = EH.make_trakt_request(url) if response: results = response.json() if results: - media_type = results[0].get('type') + media_type = results[0].get("type") return media_type return None + def get_imdb_reviews(driver, wait, directory): - #Get IMDB Reviews - + # Get IMDB Reviews + # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries('https://www.imdb.com/profile', driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + "https://www.imdb.com/profile", driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) + reviews = [] errors_found_getting_imdb_reviews = False try: # Wait until the current page URL contains the string "user/" wait.until(lambda driver: "user/" in driver.current_url) - + # Copy the full URL to a variable and append reviews to it reviews_url = driver.current_url + "reviews/" - + # Load page - success, status_code, url, driver, wait = EH.get_page_with_retries(reviews_url, driver, wait) + success, status_code, url, driver, wait = EH.get_page_with_retries( + reviews_url, driver, wait + ) if not success: # Page failed to load, raise an exception - raise PageLoadException(f"Failed to load page. Status code: {status_code}. URL: {url}") - + raise PageLoadException( + f"Failed to load page. Status code: {status_code}. URL: {url}" + ) + while True: try: try: - review_elements = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".imdb-user-review"))) + review_elements = wait.until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, ".imdb-user-review") + ) + ) except (NoSuchElementException, TimeoutException): - # No review elements found. There are no reviews. Exit the loop without an error. - error_message = "No review elements found. There are no reviews. Exit the loop without an error." + # No review elements found. There are no reviews. Exit the loop + # without an error. + error_message = ( + "No review elements found. There are no reviews. Exit the loop " + "without an error." + ) EL.logger.error(error_message, exc_info=True) break - + for element in review_elements: review = {} # Extract review details - review['Title'] = element.find_element(By.CSS_SELECTOR, ".lister-item-header a").text - review['Year'] = element.find_element(By.CSS_SELECTOR, ".lister-item-header span").text.strip('()').split('–')[0].strip() - review['Year'] = int(review['Year']) if review['Year'] else None - review['IMDB_ID'] = element.find_element(By.CSS_SELECTOR, ".lister-item-header a").get_attribute("href").split('/')[4] - review['IMDBReviewID'] = element.get_attribute("data-review-id") - review['Comment'] = element.find_element(By.CSS_SELECTOR, ".content > .text").text.strip() - spoiler_warning_elements = element.find_elements(By.CSS_SELECTOR, ".spoiler-warning") - review['Spoiler'] = len(spoiler_warning_elements) > 0 + review["Title"] = element.find_element( + By.CSS_SELECTOR, ".lister-item-header a" + ).text + review["Year"] = ( + element.find_element( + By.CSS_SELECTOR, ".lister-item-header span" + ) + .text.strip("()") + .split("–")[0] + .strip() + ) + review["Year"] = int(review["Year"]) if review["Year"] else None + review["IMDB_ID"] = ( + element.find_element(By.CSS_SELECTOR, ".lister-item-header a") + .get_attribute("href") + .split("/")[4] + ) + review["IMDBReviewID"] = element.get_attribute("data-review-id") + review["Comment"] = element.find_element( + By.CSS_SELECTOR, ".content > .text" + ).text.strip() + spoiler_warning_elements = element.find_elements( + By.CSS_SELECTOR, ".spoiler-warning" + ) + review["Spoiler"] = len(spoiler_warning_elements) > 0 # Get the media type using Trakt API - media_type = get_media_type(review['IMDB_ID']) + media_type = get_media_type(review["IMDB_ID"]) if media_type: - review['Type'] = media_type + review["Type"] = media_type else: - review['Type'] = 'unknown' + review["Type"] = "unknown" - if review['Type'] != 'unknown': + if review["Type"] != "unknown": reviews.append(review) try: @@ -449,36 +650,50 @@ def get_imdb_reviews(driver, wait, directory): next_link = driver.find_element(By.CSS_SELECTOR, "a.next-page") if next_link.get_attribute("href") == "#": break # No more pages, exit the loop - + # Get the current url before clicking the "Next" link current_url = driver.current_url - + # Click the "Next" link next_link.click() # Wait until the URL changes wait.until(lambda driver: driver.current_url != current_url) - + # Refresh review_elements - review_elements = wait.until(EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".imdb-user-review"))) + review_elements = wait.until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, ".imdb-user-review") + ) + ) except NoSuchElementException: - # "Next" link not found on IMDB reviews page, exit the loop without error - error_message = '"Next" link not found on IMDB reviews page. Exiting the loop without error.' + # "Next" link not found on IMDB reviews page, exit the loop without + # error + error_message = ( + '"Next" link not found on IMDB reviews page. Exiting the loop ' + "without error." + ) EL.logger.warning(error_message, exc_info=True) break except TimeoutException: - # Timed out waiting for URL change or next page review elements on IMDB reviews page - error_message = 'Timed out waiting for URL change or next page review elements on IMDB reviews page. Exiting the loop without error.' + # Timed out waiting for URL change or next page review elements on + # IMDB reviews page + error_message = ( + "Timed out waiting for URL change or next page review elements " + "on IMDB reviews page. Exiting the loop without error." + ) EL.logger.error(error_message, exc_info=True) break except Exception as e: errors_found_getting_imdb_reviews = True - error_message = f"Exception occurred while getting IMDB reviews: {type(e)}" + error_message = ( + f"Exception occurred while getting IMDB reviews: {type(e)}" + ) print(f"{error_message}") EL.logger.error(error_message, exc_info=True) break - + except Exception as e: errors_found_getting_imdb_reviews = True error_message = f"Exception occurred while getting IMDB reviews: {type(e)}" @@ -490,9 +705,9 @@ def get_imdb_reviews(driver, wait, directory): filtered_reviews = [] seen = set() for item in reviews: - if item['IMDB_ID'] not in seen: - seen.add(item['IMDB_ID']) + if item["IMDB_ID"] not in seen: + seen.add(item["IMDB_ID"]) filtered_reviews.append(item) imdb_reviews = filtered_reviews - - return imdb_reviews, errors_found_getting_imdb_reviews, driver, wait \ No newline at end of file + + return imdb_reviews, errors_found_getting_imdb_reviews, driver, wait diff --git a/IMDBTraktSyncer/traktData.py b/IMDBTraktSyncer/traktData.py index 918524b..52f3fd2 100644 --- a/IMDBTraktSyncer/traktData.py +++ b/IMDBTraktSyncer/traktData.py @@ -6,57 +6,93 @@ import datetime import sys from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from IMDBTraktSyncer import errorHandling as EH from IMDBTraktSyncer import errorLogger as EL + def remove_slashes(string): - string = string.replace('/', '') if string is not None else None + string = string.replace("/", "") if string is not None else None return string + def get_trakt_encoded_username(): # Process Trakt Ratings and Comments - response = EH.make_trakt_request('https://api.trakt.tv/users/me') + response = EH.make_trakt_request("https://api.trakt.tv/users/me") json_data = json.loads(response.text) - username_slug = json_data['ids']['slug'] + username_slug = json_data["ids"]["slug"] encoded_username = urllib.parse.quote(username_slug) return encoded_username - + + def get_trakt_watchlist(encoded_username): # Get Trakt Watchlist Items - response = EH.make_trakt_request(f'https://api.trakt.tv/users/{encoded_username}/watchlist?sort=added,asc') + response = EH.make_trakt_request( + f"https://api.trakt.tv/users/{encoded_username}/watchlist?sort=added,asc" + ) json_data = json.loads(response.text) trakt_watchlist = [] for item in json_data: - if item['type'] == 'movie': - movie = item.get('movie') - imdb_movie_id = movie.get('ids', {}).get('imdb') + if item["type"] == "movie": + movie = item.get("movie") + imdb_movie_id = movie.get("ids", {}).get("imdb") imdb_movie_id = remove_slashes(imdb_movie_id) - trakt_movie_id = movie.get('ids', {}).get('trakt') - trakt_watchlist.append({'Title': movie.get('title'), 'Year': movie.get('year'), 'IMDB_ID': imdb_movie_id, 'TraktID': trakt_movie_id, 'Date_Added': item.get('listed_at'), 'Type': 'movie'}) - elif item['type'] == 'show': - show = item.get('show') - imdb_show_id = show.get('ids', {}).get('imdb') + trakt_movie_id = movie.get("ids", {}).get("trakt") + trakt_watchlist.append( + { + "Title": movie.get("title"), + "Year": movie.get("year"), + "IMDB_ID": imdb_movie_id, + "TraktID": trakt_movie_id, + "Date_Added": item.get("listed_at"), + "Type": "movie", + } + ) + elif item["type"] == "show": + show = item.get("show") + imdb_show_id = show.get("ids", {}).get("imdb") imdb_show_id = remove_slashes(imdb_show_id) - trakt_show_id = show.get('ids', {}).get('trakt') - trakt_watchlist.append({'Title': show.get('title'), 'Year': show.get('year'), 'IMDB_ID': imdb_show_id, 'TraktID': trakt_show_id, 'Date_Added': item.get('listed_at'), 'Type': 'show'}) - elif item['type'] == 'episode': - show = item.get('show') - show_title = show.get('title') - episode = item.get('episode') - imdb_episode_id = episode.get('ids', {}).get('imdb') + trakt_show_id = show.get("ids", {}).get("trakt") + trakt_watchlist.append( + { + "Title": show.get("title"), + "Year": show.get("year"), + "IMDB_ID": imdb_show_id, + "TraktID": trakt_show_id, + "Date_Added": item.get("listed_at"), + "Type": "show", + } + ) + elif item["type"] == "episode": + show = item.get("show") + show_title = show.get("title") + episode = item.get("episode") + imdb_episode_id = episode.get("ids", {}).get("imdb") imdb_episode_id = remove_slashes(imdb_episode_id) - trakt_episode_id = episode.get('ids', {}).get('trakt') + trakt_episode_id = episode.get("ids", {}).get("trakt") episode_title = f'{show_title}: {episode.get("title")}' - trakt_watchlist.append({'Title': episode_title, 'Year': episode.get('year'), 'IMDB_ID': imdb_episode_id, 'TraktID': trakt_episode_id, 'Date_Added': item.get('listed_at'), 'Type': 'episode'}) - + trakt_watchlist.append( + { + "Title": episode_title, + "Year": episode.get("year"), + "IMDB_ID": imdb_episode_id, + "TraktID": trakt_episode_id, + "Date_Added": item.get("listed_at"), + "Type": "episode", + } + ) + return trakt_watchlist -def get_trakt_ratings(encoded_username): + +def get_trakt_ratings(encoded_username): # Get Trakt Ratings - response = EH.make_trakt_request(f'https://api.trakt.tv/users/{encoded_username}/ratings?sort=newest') + response = EH.make_trakt_request( + f"https://api.trakt.tv/users/{encoded_username}/ratings?sort=newest" + ) json_data = json.loads(response.text) movie_ratings = [] @@ -64,67 +100,113 @@ def get_trakt_ratings(encoded_username): episode_ratings = [] for item in json_data: - if item['type'] == 'movie': - movie = item.get('movie') - movie_id = movie.get('ids', {}).get('imdb') + if item["type"] == "movie": + movie = item.get("movie") + movie_id = movie.get("ids", {}).get("imdb") movie_id = remove_slashes(movie_id) - movie_ratings.append({'Title': movie.get('title'), 'Year': movie.get('year'), 'Rating': item.get('rating'), 'IMDB_ID': movie_id, 'Date_Added': item.get('rated_at'), 'WatchedAt': item.get('rated_at'), 'Type': 'movie'}) - elif item['type'] == 'show': - show = item.get('show') - show_id = show.get('ids', {}).get('imdb') + movie_ratings.append( + { + "Title": movie.get("title"), + "Year": movie.get("year"), + "Rating": item.get("rating"), + "IMDB_ID": movie_id, + "Date_Added": item.get("rated_at"), + "WatchedAt": item.get("rated_at"), + "Type": "movie", + } + ) + elif item["type"] == "show": + show = item.get("show") + show_id = show.get("ids", {}).get("imdb") show_id = remove_slashes(show_id) - show_ratings.append({'Title': show.get('title'), 'Year': show.get('year'), 'Rating': item.get('rating'), 'IMDB_ID': show_id, 'Date_Added': item.get('rated_at'), 'WatchedAt': item.get('rated_at'), 'Type': 'show'}) - elif item['type'] == 'episode': - show = item.get('show') - show_title = show.get('title') - episode = item.get('episode') - episode_id = episode.get('ids', {}).get('imdb') + show_ratings.append( + { + "Title": show.get("title"), + "Year": show.get("year"), + "Rating": item.get("rating"), + "IMDB_ID": show_id, + "Date_Added": item.get("rated_at"), + "WatchedAt": item.get("rated_at"), + "Type": "show", + } + ) + elif item["type"] == "episode": + show = item.get("show") + show_title = show.get("title") + episode = item.get("episode") + episode_id = episode.get("ids", {}).get("imdb") episode_id = remove_slashes(episode_id) episode_title = f'{show_title}: {episode.get("title")}' - episode_ratings.append({'Title': episode_title, 'Year': episode.get('year'), 'Rating': item.get('rating'), 'IMDB_ID': episode_id, 'Date_Added': item.get('rated_at'), 'WatchedAt': item.get('rated_at'), 'Type': 'episode'}) + episode_ratings.append( + { + "Title": episode_title, + "Year": episode.get("year"), + "Rating": item.get("rating"), + "IMDB_ID": episode_id, + "Date_Added": item.get("rated_at"), + "WatchedAt": item.get("rated_at"), + "Type": "episode", + } + ) trakt_ratings = movie_ratings + show_ratings + episode_ratings - + return trakt_ratings -def get_trakt_comments(encoded_username): + +def get_trakt_comments(encoded_username): # Get Trakt Comments - response = EH.make_trakt_request(f'https://api.trakt.tv/users/{encoded_username}/comments?sort=newest') + response = EH.make_trakt_request( + f"https://api.trakt.tv/users/{encoded_username}/comments?sort=newest" + ) json_data = json.loads(response.text) - total_pages = response.headers.get('X-Pagination-Page-Count') + total_pages = response.headers.get("X-Pagination-Page-Count") trakt_comments = [] for page in range(1, int(total_pages) + 1): - response = EH.make_trakt_request(f'https://api.trakt.tv/users/{encoded_username}/comments', params={'page': page}) + response = EH.make_trakt_request( + f"https://api.trakt.tv/users/{encoded_username}/comments", + params={"page": page}, + ) json_data = json.loads(response.text) for comment in json_data: - comment_type = comment['type'] - spoiler = comment.get('spoiler', False) - - if comment_type == 'movie': - movie = comment.get('movie') - show_movie_or_episode_title = movie.get('title') - show_movie_or_episode_year = movie.get('year') - show_movie_or_episode_imdb_id = movie.get('ids', {}).get('imdb') - show_movie_or_episode_imdb_id = remove_slashes(show_movie_or_episode_imdb_id) - elif comment_type == 'episode': - show = comment.get('show') - episode = comment.get('episode') - show_movie_or_episode_title = f"{show.get('title')}: {episode.get('title')}" - show_movie_or_episode_year = show.get('year') - show_movie_or_episode_imdb_id = episode.get('ids', {}).get('imdb') - show_movie_or_episode_imdb_id = remove_slashes(show_movie_or_episode_imdb_id) - elif comment_type == 'show': - show = comment.get('show') - show_movie_or_episode_title = show.get('title') - show_movie_or_episode_year = show.get('year') - show_movie_or_episode_imdb_id = show.get('ids', {}).get('imdb') - show_movie_or_episode_imdb_id = remove_slashes(show_movie_or_episode_imdb_id) - elif comment_type == 'season': - show = comment.get('show') - season = comment.get('season') - show_movie_or_episode_title = f"{show.get('title')}: Season {season.get('number')}" + comment_type = comment["type"] + spoiler = comment.get("spoiler", False) + + if comment_type == "movie": + movie = comment.get("movie") + show_movie_or_episode_title = movie.get("title") + show_movie_or_episode_year = movie.get("year") + show_movie_or_episode_imdb_id = movie.get("ids", {}).get("imdb") + show_movie_or_episode_imdb_id = remove_slashes( + show_movie_or_episode_imdb_id + ) + elif comment_type == "episode": + show = comment.get("show") + episode = comment.get("episode") + show_movie_or_episode_title = ( + f"{show.get('title')}: {episode.get('title')}" + ) + show_movie_or_episode_year = show.get("year") + show_movie_or_episode_imdb_id = episode.get("ids", {}).get("imdb") + show_movie_or_episode_imdb_id = remove_slashes( + show_movie_or_episode_imdb_id + ) + elif comment_type == "show": + show = comment.get("show") + show_movie_or_episode_title = show.get("title") + show_movie_or_episode_year = show.get("year") + show_movie_or_episode_imdb_id = show.get("ids", {}).get("imdb") + show_movie_or_episode_imdb_id = remove_slashes( + show_movie_or_episode_imdb_id + ) + elif comment_type == "season": + show = comment.get("show") + season = comment.get("season") + show_movie_or_episode_title = ( + f"{show.get('title')}: Season {season.get('number')}" + ) show_movie_or_episode_year = None show_movie_or_episode_imdb_id = None else: @@ -132,36 +214,41 @@ def get_trakt_comments(encoded_username): show_movie_or_episode_year = None show_movie_or_episode_imdb_id = None - comment_info = comment['comment'] - trakt_comment_id = comment_info.get('id') - trakt_comment = comment_info.get('comment') + comment_info = comment["comment"] + trakt_comment_id = comment_info.get("id") + trakt_comment = comment_info.get("comment") - trakt_comments.append({ - 'Title': show_movie_or_episode_title, - 'Year': show_movie_or_episode_year, - 'IMDB_ID': show_movie_or_episode_imdb_id, - 'TraktCommentID': trakt_comment_id, - 'Comment': trakt_comment, - 'Spoiler': spoiler, - 'Type': comment_type - }) + trakt_comments.append( + { + "Title": show_movie_or_episode_title, + "Year": show_movie_or_episode_year, + "IMDB_ID": show_movie_or_episode_imdb_id, + "TraktCommentID": trakt_comment_id, + "Comment": trakt_comment, + "Spoiler": spoiler, + "Type": comment_type, + } + ) # Filter out duplicate comments for the same item based on ID filtered_comments = [] seen = set() for item in trakt_comments: - if item['IMDB_ID'] not in seen: - seen.add(item['IMDB_ID']) + if item["IMDB_ID"] not in seen: + seen.add(item["IMDB_ID"]) filtered_comments.append(item) trakt_comments = filtered_comments - + return trakt_comments - -def get_trakt_watch_history(encoded_username): + + +def get_trakt_watch_history(encoded_username): # Get Trakt Watch History - response = EH.make_trakt_request(f'https://api.trakt.tv/users/{encoded_username}/history?limit=100') + response = EH.make_trakt_request( + f"https://api.trakt.tv/users/{encoded_username}/history?limit=100" + ) json_data = json.loads(response.text) - total_pages = response.headers.get('X-Pagination-Page-Count') + total_pages = response.headers.get("X-Pagination-Page-Count") watched_movies = [] watched_shows = [] @@ -169,59 +256,110 @@ def get_trakt_watch_history(encoded_username): seen_ids = set() for page in range(1, int(total_pages) + 1): - response = EH.make_trakt_request(f'https://api.trakt.tv/users/{encoded_username}/history?extended=full', params={'page': page, 'limit': 100}) + response = EH.make_trakt_request( + f"https://api.trakt.tv/users/{encoded_username}/history?extended=full", + params={"page": page, "limit": 100}, + ) json_data = json.loads(response.text) for item in json_data: - if item['type'] == 'movie': - movie = item.get('movie') - imdb_movie_id = movie.get('ids', {}).get('imdb') + if item["type"] == "movie": + movie = item.get("movie") + imdb_movie_id = movie.get("ids", {}).get("imdb") imdb_movie_id = remove_slashes(imdb_movie_id) - trakt_movie_id = movie.get('ids', {}).get('trakt') + trakt_movie_id = movie.get("ids", {}).get("trakt") if trakt_movie_id and trakt_movie_id not in seen_ids: - watched_movies.append({'Title': movie.get('title'), 'Year': movie.get('year'), 'IMDB_ID': imdb_movie_id, 'TraktID': trakt_movie_id, 'Date_Added': item.get('watched_at'), 'WatchedAt': item.get('watched_at'), 'Type': 'movie'}) + watched_movies.append( + { + "Title": movie.get("title"), + "Year": movie.get("year"), + "IMDB_ID": imdb_movie_id, + "TraktID": trakt_movie_id, + "Date_Added": item.get("watched_at"), + "WatchedAt": item.get("watched_at"), + "Type": "movie", + } + ) seen_ids.add(trakt_movie_id) - elif item['type'] == 'episode': - show = item.get('show') - imdb_show_id = show.get('ids', {}).get('imdb') + elif item["type"] == "episode": + show = item.get("show") + imdb_show_id = show.get("ids", {}).get("imdb") imdb_show_id = remove_slashes(imdb_show_id) - trakt_show_id = show.get('ids', {}).get('trakt') - show_status = show.get('status') - aired_episodes = show.get('aired_episodes') - + trakt_show_id = show.get("ids", {}).get("trakt") + show_status = show.get("status") + aired_episodes = show.get("aired_episodes") + if trakt_show_id and trakt_show_id not in seen_ids: - watched_shows.append({'Title': show.get('title'), 'Year': show.get('year'), 'IMDB_ID': imdb_show_id, 'TraktID': trakt_show_id, 'Date_Added': item.get('watched_at'), 'WatchedAt': item.get('watched_at'), 'ShowStatus': show_status, 'AiredEpisodes': aired_episodes, 'Type': 'show'}) + watched_shows.append( + { + "Title": show.get("title"), + "Year": show.get("year"), + "IMDB_ID": imdb_show_id, + "TraktID": trakt_show_id, + "Date_Added": item.get("watched_at"), + "WatchedAt": item.get("watched_at"), + "ShowStatus": show_status, + "AiredEpisodes": aired_episodes, + "Type": "show", + } + ) seen_ids.add(trakt_show_id) - show_title = show.get('title') - episode = item.get('episode') - season_number = episode.get('season') - episode_number = episode.get('number') - imdb_episode_id = episode.get('ids', {}).get('imdb') + show_title = show.get("title") + episode = item.get("episode") + season_number = episode.get("season") + episode_number = episode.get("number") + imdb_episode_id = episode.get("ids", {}).get("imdb") imdb_episode_id = remove_slashes(imdb_episode_id) - trakt_episode_id = episode.get('ids', {}).get('trakt') + trakt_episode_id = episode.get("ids", {}).get("trakt") episode_title = f'{show_title}: {episode.get("title")}' - episode_year = datetime.datetime.strptime(episode.get('first_aired'), "%Y-%m-%dT%H:%M:%S.%fZ").year if episode.get('first_aired') else None - watched_at = item.get('watched_at') + episode_year = ( + datetime.datetime.strptime( + episode.get("first_aired"), "%Y-%m-%dT%H:%M:%S.%fZ" + ).year + if episode.get("first_aired") + else None + ) + watched_at = item.get("watched_at") if trakt_episode_id and trakt_episode_id not in seen_ids: - watched_episodes.append({'Title': episode_title, 'Year': episode_year, 'IMDB_ID': imdb_episode_id, 'TraktID': trakt_episode_id, 'TraktShowID': trakt_show_id, 'SeasonNumber': season_number, 'EpisodeNumber': episode_number, 'Date_Added': watched_at, 'WatchedAt': watched_at, 'Type': 'episode'}) + watched_episodes.append( + { + "Title": episode_title, + "Year": episode_year, + "IMDB_ID": imdb_episode_id, + "TraktID": trakt_episode_id, + "TraktShowID": trakt_show_id, + "SeasonNumber": season_number, + "EpisodeNumber": episode_number, + "Date_Added": watched_at, + "WatchedAt": watched_at, + "Type": "episode", + } + ) seen_ids.add(trakt_episode_id) - # Filter watched_shows for completed shows where 80% or more of the show has been watched AND where the show's status is "ended" or "cancelled" + # Filter watched_shows for completed shows where 80% or more of the show has been + # watched AND where the show's status is "ended" or "cancelled" filtered_watched_shows = [] for show in watched_shows: - trakt_show_id = show['TraktID'] - show_status = show['ShowStatus'] - aired_episodes = show['AiredEpisodes'] - episode_numbers = [episode['EpisodeNumber'] for episode in watched_episodes if episode['Type'] == 'episode' and episode['TraktShowID'] == trakt_show_id] + trakt_show_id = show["TraktID"] + show_status = show["ShowStatus"] + aired_episodes = show["AiredEpisodes"] + episode_numbers = [ + episode["EpisodeNumber"] + for episode in watched_episodes + if episode["Type"] == "episode" and episode["TraktShowID"] == trakt_show_id + ] unique_watched_episode_count = len(episode_numbers) - - if (show_status.lower() in ['ended', 'cancelled', 'canceled']) and (unique_watched_episode_count >= 0.8 * int(aired_episodes)): + + if (show_status.lower() in ["ended", "cancelled", "canceled"]) and ( + unique_watched_episode_count >= 0.8 * int(aired_episodes) + ): filtered_watched_shows.append(show) # Update watched_shows with the filtered results watched_shows = filtered_watched_shows trakt_watch_history = watched_movies + watched_shows + watched_episodes - - return trakt_watch_history \ No newline at end of file + + return trakt_watch_history diff --git a/IMDBTraktSyncer/verifyCredentials.py b/IMDBTraktSyncer/verifyCredentials.py index ed51905..45625e0 100644 --- a/IMDBTraktSyncer/verifyCredentials.py +++ b/IMDBTraktSyncer/verifyCredentials.py @@ -4,17 +4,20 @@ import datetime from datetime import timedelta from pathlib import Path + sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from IMDBTraktSyncer import authTrakt from IMDBTraktSyncer import errorLogger as EL + def print_directory(main_directory): print(f"Your settings are saved at:\n{main_directory}") + def prompt_get_credentials(): # Define the file path here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') + file_path = os.path.join(here, "credentials.txt") # Default values for missing credentials default_values = { @@ -24,13 +27,13 @@ def prompt_get_credentials(): "trakt_refresh_token": "empty", "last_trakt_token_refresh": "empty", "imdb_username": "empty", - "imdb_password": "empty" + "imdb_password": "empty", } # Load existing file data if os.path.isfile(file_path) and os.path.getsize(file_path) > 0: # Read the credentials file - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, "r", encoding="utf-8") as f: try: file_data = json.load(f) except json.decoder.JSONDecodeError as e: @@ -40,18 +43,30 @@ def prompt_get_credentials(): file_data = {} # Update only the keys related to default values - values = {key: file_data.get(key, default_value) for key, default_value in default_values.items()} - + values = { + key: file_data.get(key, default_value) + for key, default_value in default_values.items() + } + # Prompt user for missing credentials, excluding tokens for key, value in values.items(): - if value == "empty" and key not in ["trakt_access_token", "trakt_refresh_token", "last_trakt_token_refresh"]: + if value == "empty" and key not in [ + "trakt_access_token", + "trakt_refresh_token", + "last_trakt_token_refresh", + ]: if key == "imdb_username": - prompt_message = f"Please enter a value for {key} (email or phone number): " + prompt_message = ( + f"Please enter a value for {key} (email or phone number): " + ) elif key == "trakt_client_id": print("\n") print("***** TRAKT API SETUP *****") print("Follow the instructions to setup your Trakt API application:") - print(" 1. Login to Trakt and navigate to your API apps page: https://trakt.tv/oauth/applications") + print( + " 1. Login to Trakt and navigate to your API apps page: " + "https://trakt.tv/oauth/applications" + ) print(" 2. Create a new API application named 'IMDBTraktSyncer'.") print(" 3. Use 'urn:ietf:wg:oauth:2.0:oob' as the Redirect URI.") print("\n") @@ -65,8 +80,12 @@ def prompt_get_credentials(): should_refresh = True if last_trakt_token_refresh != "empty": try: - last_trakt_token_refresh_time = datetime.datetime.fromisoformat(last_trakt_token_refresh) - if datetime.datetime.now() - last_trakt_token_refresh_time < timedelta(days=7): + last_trakt_token_refresh_time = datetime.datetime.fromisoformat( + last_trakt_token_refresh + ) + if datetime.datetime.now() - last_trakt_token_refresh_time < timedelta( + days=7 + ): should_refresh = False except ValueError: pass # Invalid date format, treat as refresh needed @@ -79,9 +98,13 @@ def prompt_get_credentials(): if "trakt_refresh_token" in values and values["trakt_refresh_token"] != "empty": trakt_access_token = values["trakt_refresh_token"] - trakt_access_token, trakt_refresh_token = authTrakt.authenticate(client_id, client_secret, trakt_access_token) + trakt_access_token, trakt_refresh_token = authTrakt.authenticate( + client_id, client_secret, trakt_access_token + ) else: - trakt_access_token, trakt_refresh_token = authTrakt.authenticate(client_id, client_secret) + trakt_access_token, trakt_refresh_token = authTrakt.authenticate( + client_id, client_secret + ) # Update tokens and last refresh time values["trakt_access_token"] = trakt_access_token @@ -92,104 +115,120 @@ def prompt_get_credentials(): file_data.update(values) # Save updated credentials back to the file - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(file_data, f, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as f: + json.dump(file_data, f, indent=4, separators=(", ", ": ")) # Return the credentials - return values["trakt_client_id"], values["trakt_client_secret"], values["trakt_access_token"], values["trakt_refresh_token"], values["imdb_username"], values["imdb_password"] + return ( + values["trakt_client_id"], + values["trakt_client_secret"], + values["trakt_access_token"], + values["trakt_refresh_token"], + values["imdb_username"], + values["imdb_password"], + ) + def prompt_sync_ratings(): """ - Prompts the user to enable or disable syncing of ratings and updates the credentials file accordingly. + Prompts the user to enable or disable syncing of ratings and updates the credentials + file accordingly. """ # Define the file path here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') + file_path = os.path.join(here, "credentials.txt") # Initialize credentials dictionary credentials = {} # Load existing credentials if the file exists try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) except FileNotFoundError: - # Log the error if the file is missing, continue with an empty credentials dictionary + # Log the error if the file is missing, continue with an empty credentials + # dictionary logging.error("Credentials file not found.", exc_info=True) # Check if the sync_ratings value is already set and valid - sync_ratings_value = credentials.get('sync_ratings') + sync_ratings_value = credentials.get("sync_ratings") if sync_ratings_value is not None and sync_ratings_value != "empty": return sync_ratings_value # Prompt the user until a valid input is received while True: user_input = input("Do you want to sync ratings? (y/n): ").strip().lower() - if user_input == 'y': + if user_input == "y": sync_ratings_value = True break - elif user_input == 'n': + elif user_input == "n": sync_ratings_value = False break else: print("Invalid input. Please enter 'y' or 'n'.") # Update the sync_ratings value and write back to the file - credentials['sync_ratings'] = sync_ratings_value + credentials["sync_ratings"] = sync_ratings_value try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) except IOError as e: # Log any errors during file write operation logging.error("Failed to write to credentials file.", exc_info=True) return sync_ratings_value + def prompt_sync_watchlist(): """ - Prompts the user to sync their watchlist if not already configured in credentials.txt. + Prompts the user to sync their watchlist if not already configured in + credentials.txt. Reads and writes to the credentials file only when necessary. """ # Define the file path here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') + file_path = os.path.join(here, "credentials.txt") # Load credentials file if it exists credentials = {} try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) except FileNotFoundError: - logging.error("Credentials file not found. A new file will be created if needed.", exc_info=True) + logging.error( + "Credentials file not found. A new file will be created if needed.", + exc_info=True, + ) # Check existing sync_watchlist value - sync_watchlist_value = credentials.get('sync_watchlist') + sync_watchlist_value = credentials.get("sync_watchlist") if sync_watchlist_value not in [None, "empty"]: return sync_watchlist_value # Prompt the user for input until valid while True: user_input = input("Do you want to sync watchlists? (y/n): ").strip().lower() - if user_input == 'y': + if user_input == "y": sync_watchlist_value = True break - elif user_input == 'n': + elif user_input == "n": sync_watchlist_value = False break else: print("Invalid input. Please enter 'y' or 'n'.") # Update and save the credentials only if the file will change - credentials['sync_watchlist'] = sync_watchlist_value + credentials["sync_watchlist"] = sync_watchlist_value try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) except Exception as e: logging.error("Failed to write to credentials file.", exc_info=True) raise return sync_watchlist_value - + + def prompt_sync_watch_history(): """ Prompts the user to sync their watch history @@ -198,46 +237,56 @@ def prompt_sync_watch_history(): """ # Define the file path here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') + file_path = os.path.join(here, "credentials.txt") # Load credentials file if it exists credentials = {} try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) except FileNotFoundError: - logging.error("Credentials file not found. A new file will be created if needed.", exc_info=True) + logging.error( + "Credentials file not found. A new file will be created if needed.", + exc_info=True, + ) # Check existing sync_watch_history value - sync_watch_history_value = credentials.get('sync_watch_history') + sync_watch_history_value = credentials.get("sync_watch_history") if sync_watch_history_value not in [None, "empty"]: return sync_watch_history_value # Prompt the user for input until valid while True: print("Trakt watch history is synced using IMDB Check-ins.") - print("See FAQ: https://help.imdb.com/article/imdb/track-movies-tv/check-ins-faq/GG59ELYW45FMC7J3") - user_input = input("Do you want to sync your watch history? (y/n): ").strip().lower() - if user_input == 'y': + print( + "See FAQ: " + "https://help.imdb.com/article/imdb/track-movies-tv/check-ins-faq/" + "GG59ELYW45FMC7J3" + ) + user_input = ( + input("Do you want to sync your watch history? (y/n): ").strip().lower() + ) + if user_input == "y": sync_watch_history_value = True break - elif user_input == 'n': + elif user_input == "n": sync_watch_history_value = False break else: print("Invalid input. Please enter 'y' or 'n'.") # Update and save the credentials only if the file will change - credentials['sync_watch_history'] = sync_watch_history_value + credentials["sync_watch_history"] = sync_watch_history_value try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) except Exception as e: logging.error("Failed to write to credentials file.", exc_info=True) raise return sync_watch_history_value - + + def prompt_mark_rated_as_watched(): """ Prompts the user to mark rated movies, shows, and episodes as watched @@ -246,44 +295,52 @@ def prompt_mark_rated_as_watched(): """ # Define the file path here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') + file_path = os.path.join(here, "credentials.txt") # Load credentials file if it exists credentials = {} try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) except FileNotFoundError: - logging.error("Credentials file not found. A new file will be created if needed.", exc_info=True) + logging.error( + "Credentials file not found. A new file will be created if needed.", + exc_info=True, + ) # Check existing mark_rated_as_watched value - mark_rated_as_watched_value = credentials.get('mark_rated_as_watched') + mark_rated_as_watched_value = credentials.get("mark_rated_as_watched") if mark_rated_as_watched_value not in [None, "empty"]: return mark_rated_as_watched_value # Prompt the user for input until valid while True: - user_input = input("Do you want to mark rated movies and episodes as watched? (y/n): ").strip().lower() - if user_input == 'y': + user_input = ( + input("Do you want to mark rated movies and episodes as watched? (y/n): ") + .strip() + .lower() + ) + if user_input == "y": mark_rated_as_watched_value = True break - elif user_input == 'n': + elif user_input == "n": mark_rated_as_watched_value = False break else: print("Invalid input. Please enter 'y' or 'n'.") # Update and save the credentials only if the file will change - credentials['mark_rated_as_watched'] = mark_rated_as_watched_value + credentials["mark_rated_as_watched"] = mark_rated_as_watched_value try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) except Exception as e: logging.error("Failed to write to credentials file.", exc_info=True) raise return mark_rated_as_watched_value + def check_imdb_reviews_last_submitted(): """ Check if 240 hours (10 days) have passed since the last IMDb reviews submission. @@ -293,25 +350,29 @@ def check_imdb_reviews_last_submitted(): bool: True if 240 hours have passed, otherwise False. """ # Define file path - file_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'credentials.txt') + file_path = os.path.join( + os.path.abspath(os.path.dirname(__file__)), "credentials.txt" + ) # Initialize default credentials credentials = {} # Load credentials if the file exists if os.path.exists(file_path): - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: try: credentials = json.load(file) except json.JSONDecodeError: pass # Handle the case where the file is not a valid JSON # Retrieve last submission date or default to 10 days ago - last_submitted_str = credentials.get('imdb_reviews_last_submitted_date') + last_submitted_str = credentials.get("imdb_reviews_last_submitted_date") last_submitted_date = None if last_submitted_str: try: - last_submitted_date = datetime.datetime.strptime(last_submitted_str, '%Y-%m-%d %H:%M:%S') + last_submitted_date = datetime.datetime.strptime( + last_submitted_str, "%Y-%m-%d %H:%M:%S" + ) except ValueError: pass # Handle invalid date format @@ -322,30 +383,36 @@ def check_imdb_reviews_last_submitted(): # Check if 240 hours have passed if datetime.datetime.now() - last_submitted_date >= datetime.timedelta(hours=240): # Update the timestamp - credentials['imdb_reviews_last_submitted_date'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + credentials["imdb_reviews_last_submitted_date"] = ( + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ) # Save updated credentials to file - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) return True return False - + + def prompt_sync_reviews(): """ - Handles the synchronization preference for reviews by reading and updating a credentials file. + Handles the synchronization preference for reviews by reading and updating a + credentials file. Returns: bool: True if user wants to sync reviews, False otherwise. """ # Define the file path - file_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'credentials.txt') + file_path = os.path.join( + os.path.abspath(os.path.dirname(__file__)), "credentials.txt" + ) # Attempt to read the sync_reviews value from the credentials file try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) - sync_reviews_value = credentials.get('sync_reviews') + sync_reviews_value = credentials.get("sync_reviews") # Return the value if it exists and is not "empty" if sync_reviews_value is not None and sync_reviews_value != "empty": @@ -357,29 +424,33 @@ def prompt_sync_reviews(): # Prompt the user for input until a valid response is given while True: - print("Please note: reviews synced to IMDB will use 'My Review' as the title field.") + print( + "Please note: reviews synced to IMDB will use 'My Review' as the title " + "field." + ) user_input = input("Do you want to sync reviews? (y/n): ").strip().lower() - if user_input == 'y': + if user_input == "y": sync_reviews_value = True break - elif user_input == 'n': + elif user_input == "n": sync_reviews_value = False break else: print("Invalid input. Please enter 'y' or 'n'.") # Update the sync_reviews value in the credentials file - credentials['sync_reviews'] = sync_reviews_value + credentials["sync_reviews"] = sync_reviews_value try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) except Exception as e: EL.error("Failed to write to credentials file", exc_info=True) return sync_reviews_value + def prompt_remove_watched_from_watchlists(): """ Prompts the user to decide if watched items should be removed from watchlists. @@ -387,15 +458,21 @@ def prompt_remove_watched_from_watchlists(): """ # Define the file path for credentials here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') + file_path = os.path.join(here, "credentials.txt") # Attempt to read the existing configuration try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) - remove_watched_from_watchlists_value = credentials.get('remove_watched_from_watchlists') - if remove_watched_from_watchlists_value is not None and remove_watched_from_watchlists_value != "empty": - return remove_watched_from_watchlists_value # Return the stored value if it exists + remove_watched_from_watchlists_value = credentials.get( + "remove_watched_from_watchlists" + ) + if ( + remove_watched_from_watchlists_value is not None + and remove_watched_from_watchlists_value != "empty" + ): + # Return the stored value if it exists + return remove_watched_from_watchlists_value except FileNotFoundError: # Log the error if the file is missing but continue execution EL.logger.error("Credentials file not found.", exc_info=True) @@ -404,51 +481,63 @@ def prompt_remove_watched_from_watchlists(): # Prompt the user for input until a valid choice is made while True: print("Movies and Episodes are removed from watchlists after 1 play.") - print("Shows are removed when at least 80% of the episodes are watched AND the series is marked as ended or cancelled.") + print( + "Shows are removed when at least 80% of the episodes are watched AND the " + "series is marked as ended or cancelled." + ) print("Do you want to remove watched items from watchlists? (y/n)") user_input = input("Enter your choice: ").strip().lower() - if user_input == 'y': + if user_input == "y": remove_watched_from_watchlists_value = True break - elif user_input == 'n': + elif user_input == "n": remove_watched_from_watchlists_value = False break else: print("Invalid input. Please enter 'y' or 'n'.") # Save the user's choice to the credentials file - credentials['remove_watched_from_watchlists'] = remove_watched_from_watchlists_value + credentials["remove_watched_from_watchlists"] = remove_watched_from_watchlists_value try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) except Exception as e: EL.logger.error("Failed to write to credentials file.", exc_info=True) raise e return remove_watched_from_watchlists_value - + + def prompt_remove_watchlist_items_older_than_x_days(): """ - Prompts the user to decide if watchlist items older than a certain number of days should be removed. - Reads and updates the decision and the number of days in a credentials file to avoid repeated prompting. + Prompts the user to decide if watchlist items older than a certain number of days + should be removed. + Reads and updates the decision and the number of days in a credentials file to avoid + repeated prompting. """ # Define the file path for credentials here = os.path.abspath(os.path.dirname(__file__)) - file_path = os.path.join(here, 'credentials.txt') + file_path = os.path.join(here, "credentials.txt") # Attempt to read the existing configuration try: - with open(file_path, 'r', encoding='utf-8') as file: + with open(file_path, "r", encoding="utf-8") as file: credentials = json.load(file) - remove_watchlist_items_older_than_x_days = credentials.get('remove_watchlist_items_older_than_x_days') - watchlist_days_to_remove = credentials.get('watchlist_days_to_remove') + remove_watchlist_items_older_than_x_days = credentials.get( + "remove_watchlist_items_older_than_x_days" + ) + watchlist_days_to_remove = credentials.get("watchlist_days_to_remove") # If the user has previously configured this, return the stored values if remove_watchlist_items_older_than_x_days is not None and ( - not remove_watchlist_items_older_than_x_days or watchlist_days_to_remove is not None + not remove_watchlist_items_older_than_x_days + or watchlist_days_to_remove is not None ): - return remove_watchlist_items_older_than_x_days, watchlist_days_to_remove + return ( + remove_watchlist_items_older_than_x_days, + watchlist_days_to_remove, + ) except FileNotFoundError: print("Credentials file not found. A new one will be created.") credentials = {} @@ -459,18 +548,35 @@ def prompt_remove_watchlist_items_older_than_x_days(): # Prompt the user for input until a valid choice is made while True: - print('If choosing (y) in the following, you will be prompted to enter the number of days.') - print('This setting is meant to help address the 100 item limit in Trakt watchlists for free tier users.') - print('In order to prevent old items from being re-added, it is recommended to disable Trakt watchlist sync') - print('in other projects when enabling this setting. Such as Reelgood and other similar apps.') - print('If you use the PlexTraktSync project, it is recommended to disable watchlist sync from Plex to Trakt.') - print("Do you want to remove watchlist items older than x number of days? (y/n)") + print( + "If choosing (y) in the following, you will be prompted to enter the number" + " of days." + ) + print( + "This setting is meant to help address the 100 item limit in Trakt " + "watchlists for free tier users." + ) + print( + "In order to prevent old items from being re-added, it is recommended to " + "disable Trakt watchlist sync" + ) + print( + "in other projects when enabling this setting. Such as Reelgood and other " + "similar apps." + ) + print( + "If you use the PlexTraktSync project, it is recommended to disable " + "watchlist sync from Plex to Trakt." + ) + print( + "Do you want to remove watchlist items older than x number of days? (y/n)" + ) user_input = input("Enter your choice: ").strip().lower() - if user_input == 'y': + if user_input == "y": remove_watchlist_items_older_than_x_days = True break - elif user_input == 'n': + elif user_input == "n": remove_watchlist_items_older_than_x_days = False break else: @@ -480,22 +586,30 @@ def prompt_remove_watchlist_items_older_than_x_days(): if remove_watchlist_items_older_than_x_days: while True: try: - print("For reference: (30 = 1 month, 90 = 3 months, 180 = 6 months, 365 = 1 year). Any number of days is valid.") + print( + "For reference: (30 = 1 month, 90 = 3 months, 180 = 6 months, 365 =" + " 1 year). Any number of days is valid." + ) print("How many days old should the items be to be removed?") - watchlist_days_to_remove = int(input("Enter the number of days: ").strip()) + watchlist_days_to_remove = int( + input("Enter the number of days: ").strip() + ) break except ValueError: print("Invalid input. Please enter a valid number.") # Save the user's choice and the number of days only if both are valid - credentials['remove_watchlist_items_older_than_x_days'] = remove_watchlist_items_older_than_x_days - credentials['watchlist_days_to_remove'] = watchlist_days_to_remove if remove_watchlist_items_older_than_x_days else None - + credentials["remove_watchlist_items_older_than_x_days"] = ( + remove_watchlist_items_older_than_x_days + ) + credentials["watchlist_days_to_remove"] = ( + watchlist_days_to_remove if remove_watchlist_items_older_than_x_days else None + ) try: - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(credentials, file, indent=4, separators=(', ', ': ')) + with open(file_path, "w", encoding="utf-8") as file: + json.dump(credentials, file, indent=4, separators=(", ", ": ")) except Exception as e: print("Failed to write to credentials file.", e) raise e - return remove_watchlist_items_older_than_x_days, watchlist_days_to_remove \ No newline at end of file + return remove_watchlist_items_older_than_x_days, watchlist_days_to_remove