├── .gitignore ├── README.md ├── __init__.py ├── classes ├── __init__.py ├── enum_types.py ├── film_item.py ├── film_list.py └── plex_data.py ├── functions ├── __init__.py ├── collection.py ├── discord.py ├── playlists.py ├── plex_connection.py ├── plex_library │ ├── __init__.py │ ├── library_utils.py │ ├── movies.py │ └── shows.py ├── sources │ ├── __init__.py │ ├── imdb.py │ └── trakt.py └── users.py ├── global_vars.py ├── plex_playlist_update.py ├── pylintrc ├── requirements.txt ├── settings.ini.example ├── tests ├── parse_test.py └── test0.py ├── utils └── logger.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | settings.ini 2 | tmdb_ids.dat 3 | tmdb_ids.bak 4 | tmdb_ids.dir 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | # vscode debug 11 | *.vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRICATED 2 | 3 | Suggest plex-meta-manager as a replacement for this project. It can do everything this does and more. 4 | I am also working on adding a module into Overseerr to allow list management with a nice UI that was missing from this project. 5 | 6 | # plex_top_playlists 7 | **Python 3 ONLY. Python 2 no longer works. Please check out settings.ini.example and adjust.** 8 | **Requires Python >= 3.6** 9 | 10 | A python script to get top weekly or top popular lists and put them in plex as playlists. It will make a playlist for each user on your server. 11 | This is my first time ever creating a python script. Also the first time really adding something useful to GitHub 12 | 13 | Use at your own risk. I have it running nightly 14 | 15 | ## Read This 16 | 17 | This script is assuming you are using "Plex Movie" as your library check. If you are using TMDB it will not work. 18 | 19 | The TV Shows playlists give the last episode in your library for the show. You can easily click the show name to go to it. 20 | 21 | The playlists can be created for any of the following 22 | * All shared users (Normal or Managed) 23 | * Specific users by username 24 | * Only the script running user 25 | 26 | ## NEW - Collections 27 | * Can use same lists like playlists but add a collection tag to the media instead of putting them in a playlist 28 | * Best when order does not matter 29 | 30 | ## What lists this script currently retreives 31 | 32 | * Trakt Playlists Movies 33 | * Trakt Playlists Shows 34 | * IMDB Chart Lists 35 | * IMDB Custom lists 36 | * IMDB Search Lists 37 | * Missing Movies or Shows can be shown with relevent IDs to search in Sonarr or Radarr 38 | * Send message to discord with summary 39 | * Helper commands to see relevent information or one off playlist actions 40 | 41 | ## Future wants to add (any help is welcome) 42 | 43 | * Add Tautulli Lists 44 | * Auto Add to Radarr 45 | * Auto Add to Sonarr 46 | * Tautulli history algorithim to suggest unwatched to a playlist or collection 47 | 48 | # Getting Started 49 | 50 | ## Setup Instructions 51 | * [Linux](https://github.com/pbrink231/plex_top_playlists/wiki/Linux-Setup-and-Update) 52 | * [Windows](https://github.com/pbrink231/plex_top_playlists/wiki/Windows-Setup-and-Update) 53 | 54 | ## Obtain Required Plex key and other optional keys 55 | * [Plex](https://github.com/pbrink231/plex_top_playlists/wiki/Plex-token) 56 | * [Trakt](https://github.com/pbrink231/plex_top_playlists/wiki/Trakt-token) 57 | * [Discord](https://github.com/pbrink231/plex_top_playlists/wiki/Discord-token) 58 | 59 | 60 | 61 | # Examples 62 | 63 | This created a playlist for each user in plex for all found in the list. The output shows what was not added because it was missing. 64 | 65 | ![movie list output](https://github.com/pbrink231/plex_top_playlists/wiki/images/Movie-Output-example.PNG) 66 | 67 | ![movie list output](https://github.com/pbrink231/plex_top_playlists/wiki/images/discord-output.PNG) 68 | 69 | 70 | # Used references to create the script 71 | 72 | Thank you JonnyWong16 for his amazing scripts which I heavily referenced. 73 | 74 | https://gist.github.com/JonnyWong16/2607abf0e3431b6f133861bbe1bb694e 75 | https://gist.github.com/JonnyWong16/b1aa2c0f604ed92b9b3afaa6db18e5fd 76 | 77 | Thank you to [gkspranger](https://github.com/gkspranger/plex_top_playlists) for forking and updating the script while I was MIA. I heavily used his updates. 78 | 79 | 80 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrink231/plex_top_playlists/0396e5bd2f5eed963c7f1aa7bd18cf98b277ea7f/__init__.py -------------------------------------------------------------------------------- /classes/__init__.py: -------------------------------------------------------------------------------- 1 | """This modules exports the plex playlist classes and enumerables""" 2 | 3 | from classes.enum_types import FilmDB, FilmType, ListSource, FilmListKind 4 | from classes.film_list import FilmList 5 | from classes.film_item import FilmItem 6 | from classes.plex_data import PlexData 7 | -------------------------------------------------------------------------------- /classes/enum_types.py: -------------------------------------------------------------------------------- 1 | """ Enums used in app """ 2 | # Python code to demonstrate enumerations 3 | # iterations and hashing 4 | # importing enum for enumerations 5 | from enum import Enum 6 | 7 | class FilmListKind(Enum): 8 | """ Different kinds of lists """ 9 | PLAYLIST = 1 10 | COLLECTION = 2 11 | 12 | # creating enumerations using class 13 | class FilmDB(Enum): 14 | """ Film Database to use for lookup in the app """ 15 | IMDB = 1 16 | TVDB = 2 17 | 18 | # creating enumerations using class 19 | class FilmType(Enum): 20 | """ Film types possible """ 21 | MOVIE = 1 22 | SHOW = 2 23 | SEASON = 3 24 | EPISODE = 4 25 | 26 | class ListSource(Enum): 27 | """ Sources to pull lists from """ 28 | IMDB = 1 29 | TRAKT = 2 30 | -------------------------------------------------------------------------------- /classes/film_item.py: -------------------------------------------------------------------------------- 1 | """ Class declaration for Film Item """ 2 | 3 | class FilmItem: 4 | """ An Item for Film List """ 5 | def __init__(self, film_id, film_db, film_type, title=None, season_num=None, episode_num=None): 6 | self.film_id = film_id 7 | self.film_db = film_db # imdb, tvdb 8 | self.film_type = film_type # movie, show, episode 9 | self.film_title = title # For displaying when cannot find in Plex 10 | self.season_num = season_num 11 | self.episode_num = episode_num 12 | 13 | def display(self) -> str: 14 | """ display text for film item """ 15 | return f"{self.film_db.name}: {self.film_id}" 16 | -------------------------------------------------------------------------------- /classes/film_list.py: -------------------------------------------------------------------------------- 1 | """ A list of different movies, shows, seasons or episodes. """ 2 | from typing import List 3 | import global_vars 4 | from classes import ListSource, FilmListKind 5 | from functions.playlists import add_playlist_to_plex_users 6 | from functions.collection import add_library_items_to_collection 7 | from functions.discord import send_simple_message 8 | 9 | class FilmList(object): 10 | """__init__() functions as the class constructor""" 11 | def __init__(self, source, title: str, list_items, kind: str = "playlist", show_summary: bool = True): 12 | self.list_source = source # ListSource 13 | self.title = title 14 | self.show_summary = show_summary 15 | self.list_items = list_items 16 | self.matched_library_items = [] 17 | self.unmatched_film_items = [] 18 | 19 | # Setup Kind correclty 20 | film_kind = FilmListKind[kind.upper()] 21 | if not film_kind: 22 | raise Exception(f"Film List Kind Unknown {kind}") 23 | 24 | self.kind = film_kind 25 | 26 | def setup_playlist(self, plex_data): 27 | """ Match lists with plex data """ 28 | if self.list_items is None or not self.list_items: 29 | print(f"WARNING: {self.title} is empty so skipping") 30 | return 31 | 32 | print("{0}: finding matching movies for playlist with count {1}".format( 33 | self.title, 34 | len(self.list_items) 35 | )) 36 | 37 | self.match_items(plex_data) 38 | self.update_plex(plex_data) 39 | 40 | self.found_info() 41 | self.missing_info_print() 42 | self.missing_info_discord() 43 | 44 | def start_text(self): 45 | """ Text showing information before process start """ 46 | print("{0}: STARTING - SOURCE: {2} - KIND: {1}".format( 47 | self.title, 48 | self.kind, 49 | self.list_source.name 50 | )) 51 | 52 | def match_items(self, plex_data): 53 | """ Will Process the film list and create/add any items to the playlist/collection """ 54 | matched_library_items = [] 55 | unmatched_film_items = [] 56 | for film_item in self.list_items: 57 | library_item = plex_data.get_matching_item(film_item) 58 | if library_item: 59 | matched_library_items.append(library_item) 60 | else: 61 | unmatched_film_items.append(film_item) 62 | 63 | self.matched_library_items = matched_library_items 64 | self.unmatched_film_items = unmatched_film_items 65 | 66 | def update_plex(self, plex_data): 67 | """ Updates plex collection or playlist based on this list """ 68 | if self.kind == FilmListKind.COLLECTION: 69 | # Update plex with playlist 70 | add_library_items_to_collection(self.title, self.matched_library_items) 71 | return 72 | 73 | if self.kind == FilmListKind.PLAYLIST: 74 | # Update plex adding items to the collection 75 | add_playlist_to_plex_users( 76 | plex_data.plex, 77 | plex_data.shared_users_token, 78 | self.title, 79 | self.matched_library_items 80 | ) 81 | return 82 | 83 | 84 | raise Exception(f"Film List Kind Unknown {self.kind}") 85 | 86 | def add_library_items_to_collection(self): 87 | """ Adds all film items to a collection with list title """ 88 | for library_item in self.matched_library_items: 89 | library_item.addCollection(self.title) 90 | 91 | def found_info(self) -> str: 92 | """ Returns summary information for matches in text """ 93 | text = """ 94 | {match_lib_items_ct} of {items_ct} found in list: 95 | {playlist_name} 96 | MISSING: {miss_items_ct}""".format( 97 | playlist_name=self.title, 98 | items_ct=len(self.list_items), 99 | match_lib_items_ct=len(self.matched_library_items), 100 | miss_items_ct=len(self.unmatched_film_items), 101 | ) 102 | 103 | return text 104 | 105 | def missing_info(self) -> str: 106 | """ Text display of list stats with missing info """ 107 | id_list = "" 108 | list_length = len(self.unmatched_film_items) 109 | for i in range(list_length): 110 | film_item = self.unmatched_film_items[i] 111 | id_list += film_item.display() 112 | if i != 0 and i != list_length and (not (i + 1) % 4): 113 | id_list += '\n' 114 | elif i != list_length: 115 | id_list += ' | ' 116 | 117 | text = self.found_info() + '\n' + f"{id_list}" 118 | 119 | return text 120 | 121 | def missing_info_print(self): 122 | """Text for showing missing info""" 123 | if not global_vars.SHOW_MISSING or not self.show_summary: 124 | # skip showing missing_info 125 | return None 126 | 127 | print(self.missing_info()) 128 | 129 | def missing_info_discord(self): 130 | """Send information to discord""" 131 | if not global_vars.DISCORD_URL or not self.show_summary: 132 | # Skip sending to discord 133 | return None 134 | 135 | send_simple_message(self.title, self.missing_info()) 136 | -------------------------------------------------------------------------------- /classes/plex_data.py: -------------------------------------------------------------------------------- 1 | """ Overall plex data cacher to easily search movies and shows 2 | by IMDB ID and TVDB ID """ 3 | from functions.users import get_user_tokens 4 | from functions.plex_library.movies import get_library_movie_dictionary 5 | from functions.plex_library.shows import get_library_show_dictionary 6 | 7 | from utils.logger import log_output 8 | 9 | from classes import FilmType, FilmDB 10 | 11 | class PlexData: 12 | """ Class to hold cached plex data for testing against lists """ 13 | def __init__(self, plex): 14 | self.plex = plex 15 | self.shared_users_token = get_user_tokens(plex) 16 | self.all_movie_id_dict = get_library_movie_dictionary(plex) 17 | self.all_show_id_dict = get_library_show_dictionary(plex) 18 | 19 | def display_shared_users(self): 20 | """ Show users being used for all lists """ 21 | log_output("shared users list: {}".format(self.shared_users_token), 1) 22 | 23 | def get_matching_item(self, film_item): 24 | """ Grabs the matched library item with the film item """ 25 | found_item = None 26 | if film_item.film_type == FilmType.MOVIE: 27 | return self.all_movie_id_dict.get(film_item.film_id) 28 | else: 29 | return self.get_show_episode(film_item) 30 | 31 | raise Exception(f"Film DB Unknown {film_item.film_db}") 32 | 33 | def get_show_episode(self, film_item): 34 | """ Returns the corresponding episode """ 35 | found_show = self.all_show_id_dict.get(film_item.film_id) 36 | 37 | if not found_show: 38 | return found_show 39 | 40 | if film_item.film_type == FilmType.SHOW: 41 | # Return last episode 42 | return found_show.episodes()[-1] 43 | 44 | if film_item.film_type == FilmType.SEASON: 45 | # Return first episode in season 46 | # TO DO FIX 47 | return found_show.episodes()[-1] 48 | 49 | if film_item.film_type == FilmType.EPISODE: 50 | # Return specific episode 51 | # TO DO FIX 52 | return found_show.episodes()[-1] 53 | 54 | raise Exception(f"Film List Type Unknown {film_item.film_type}") 55 | -------------------------------------------------------------------------------- /functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrink231/plex_top_playlists/0396e5bd2f5eed963c7f1aa7bd18cf98b277ea7f/functions/__init__.py -------------------------------------------------------------------------------- /functions/collection.py: -------------------------------------------------------------------------------- 1 | """ Methods dealing with plex collections """ 2 | 3 | def add_library_items_to_collection(title, items): 4 | """ Adds plex items to a collection """ 5 | for library_item in items: 6 | library_item.addCollection(title) 7 | -------------------------------------------------------------------------------- /functions/discord.py: -------------------------------------------------------------------------------- 1 | """ Methods to send messages to discord """ 2 | import requests 3 | import global_vars 4 | 5 | def send_simple_message(title, description): 6 | """ Create a basic message to send to discord """ 7 | message = {} 8 | first_part = {} 9 | first_part["title"] = title 10 | first_part["description"] = description 11 | message['embeds'] = [first_part] 12 | send_to_discord(message) 13 | 14 | def send_to_discord(message): 15 | """ Send a preconfigured message to discord """ 16 | # JSON_DATA = json.dumps(MESSAGE) 17 | res = requests.post( 18 | global_vars.DISCORD_URL, 19 | headers={"Content-Type":"application/json"}, 20 | json=message 21 | ) 22 | if res.status_code == 204: 23 | print("You should see a message in discord.") 24 | -------------------------------------------------------------------------------- /functions/playlists.py: -------------------------------------------------------------------------------- 1 | """ Methods to work with plex playlists """ 2 | import os 3 | import sys 4 | 5 | from functions.plex_connection import plex_user_connection 6 | import global_vars 7 | 8 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 9 | 10 | 11 | def add_playlist_to_plex_users(plex, shared_users_token, title, items): 12 | """ Adds a playlist to all shared users """ 13 | if not items: 14 | print(f"WARNING {title}: list EMPTY so will only be REMOVED and not created") 15 | 16 | # update my list 17 | create_playlists(plex, title, items) 18 | print(f"{title}: Playlist made for primary user") 19 | 20 | # update list for shared users 21 | if global_vars.SYNC_WITH_SHARED_USERS: 22 | for user in shared_users_token: 23 | user_token = shared_users_token[user] 24 | user_plex = plex_user_connection(user_token) 25 | create_playlists(user_plex, title, items) 26 | print(f"{title}: playlist made for user {user}") 27 | 28 | else: 29 | print("Skipping adding to shared users") 30 | 31 | def create_playlists(plex, title, items): 32 | """ creates a playlist for the plex user """ 33 | try: 34 | remove_playlist(plex, title) 35 | if items: 36 | plex.createPlaylist(title, items) 37 | except Exception: # pylint: disable=broad-except 38 | print(f""" 39 | ERROR trying to create playlist '{title}' 40 | The number of movies/shows in the list provided was {len(items)} 41 | """) 42 | 43 | 44 | def remove_shared_playlist(plex, shared_users_token, title: str): 45 | """ removes playlists for main plex user and all shared users """ 46 | # update my list 47 | print(f"{title}: removing playlist for script user") 48 | remove_playlist(plex, title) 49 | 50 | # update list for shared users 51 | if global_vars.SYNC_WITH_SHARED_USERS: 52 | for user in shared_users_token: 53 | print(f"{title}: removing playlist for user {user}") 54 | user_token = shared_users_token[user] 55 | user_plex = plex_user_connection(user_token) 56 | remove_playlist(user_plex, title) 57 | else: 58 | print("Skipping removal from shared users") 59 | 60 | def remove_playlists_for_user(plex, film_lists): 61 | """ removes all of a users playlists on the Plex Server """ 62 | for film_list in film_lists: 63 | print(f"Removing playlists '{film_list.title}'") 64 | remove_playlist(plex, film_list.title) 65 | 66 | 67 | 68 | def remove_playlist(plex, title): 69 | """ deletes a playlist for the plex user """ 70 | for playlist in plex.playlists(): 71 | if playlist.title == title: 72 | try: 73 | playlist.delete() 74 | except Exception: # pylint: disable=broad-except 75 | print(f"ERROR - cannot delete playlist: {title}") 76 | 77 | return None 78 | -------------------------------------------------------------------------------- /functions/plex_connection.py: -------------------------------------------------------------------------------- 1 | """ Plex connection methods """ 2 | from plexapi.server import PlexServer 3 | import global_vars 4 | 5 | def plex_user_connection(user_token=global_vars.PLEX_TOKEN): 6 | """ returns a connection to plex for a user """ 7 | return PlexServer( 8 | baseurl=global_vars.PLEX_URL, 9 | token=user_token, 10 | timeout=global_vars.PLEX_TIMEOUT 11 | ) 12 | -------------------------------------------------------------------------------- /functions/plex_library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrink231/plex_top_playlists/0396e5bd2f5eed963c7f1aa7bd18cf98b277ea7f/functions/plex_library/__init__.py -------------------------------------------------------------------------------- /functions/plex_library/library_utils.py: -------------------------------------------------------------------------------- 1 | """ Utility methods for the plex library """ 2 | import sys 3 | 4 | def show_dict_progress(curnum, total): 5 | """ Displays the progress of adding to the dictionary """ 6 | bar_length = 50 7 | filled_len = int(round(bar_length * curnum / float(total))) 8 | percents = round(100.0 * (curnum / float(total)), 1) 9 | prog_bar = '=' * filled_len + '-' * (bar_length - filled_len) 10 | sys.stdout.write('[%s] %s%s ...%s\r' % (prog_bar, percents, '%', "{0} of {1}".format( 11 | curnum, 12 | total 13 | ))) 14 | sys.stdout.flush() 15 | -------------------------------------------------------------------------------- /functions/plex_library/movies.py: -------------------------------------------------------------------------------- 1 | """ Getting needed information plex libraries """ 2 | import re 3 | import shelve 4 | from tmdbv3api import TMDb, Movie 5 | import global_vars 6 | 7 | from functions.plex_library.library_utils import show_dict_progress 8 | 9 | from utils.logger import log_timer 10 | 11 | 12 | 13 | tmdb = TMDb() 14 | tmdb.api_key = global_vars.TMDB_API_KEY 15 | tmdb_movie = Movie() 16 | 17 | def get_library_movie_dictionary(plex): 18 | """ Returns a dictionary to easily reference movies by thier IMDB ID""" 19 | all_movies = get_library_movies(plex) 20 | 21 | print("Creating movie dictionary based on IMDB ID") 22 | return create_movie_id_dict(all_movies) 23 | 24 | 25 | def get_library_movies(plex): 26 | """ Getting Movie libraries dictionary """ 27 | # Get list of movies from the Plex server 28 | # split into array 29 | movie_libs = global_vars.MOVIE_LIBRARY_NAME.split(",") 30 | all_movies = [] 31 | 32 | # loop movie lib array 33 | for lib in movie_libs: 34 | lib = lib.strip() 35 | print("Retrieving a list of movies from the '{library}' " 36 | "library in Plex...".format(library=lib)) 37 | try: 38 | movie_library = plex.library.section(lib) 39 | new_movies = movie_library.all() 40 | all_movies = all_movies + new_movies 41 | print("Added {length} movies to your 'all movies' " 42 | "list from the '{library}' library in Plex...".format( 43 | library=lib, 44 | length=len(new_movies))) 45 | log_timer() 46 | except Exception: # pylint: disable=broad-except 47 | print("The '{library}' library does not exist in Plex.".format(library=lib)) 48 | 49 | print("Found {0} movies total in 'all movies' list from Plex...".format( 50 | len(all_movies) 51 | )) 52 | 53 | return all_movies 54 | 55 | 56 | def create_movie_id_dict(movies): 57 | """ Creates a dictionary for easy searching by imdb ID """ 58 | movie_id_dict = {} 59 | count = len(movies) 60 | cur = 1 61 | for movie in movies: 62 | movie_id_dict = append_movie_id_dict(movie, movie_id_dict) 63 | show_dict_progress(cur, count) 64 | cur += 1 65 | print(f"\nFinished Creating Movie Dictionary") 66 | return movie_id_dict 67 | 68 | def append_movie_id_dict(movie, movie_id_dict): 69 | """ Adds movie to dictionary with imdb ID as key and movie as value """ 70 | imdb_id = get_imdb_id(movie) 71 | if imdb_id is not None: 72 | movie_id_dict[imdb_id] = movie 73 | return movie_id_dict 74 | 75 | def get_imdb_id(movie): 76 | """Gets the IMDB based on the agent used 77 | 78 | Some Guid examples 79 | local://36071 80 | com.plexapp.agents.imdb://tt0137523?lang=en 81 | com.plexapp.agents.themoviedb://550?lang=en 82 | plex://movie/5d776a8b9ab544002150043a 83 | """ 84 | try: 85 | movie_reg = re.search(r'(?:plex://|com\.plexapp\.agents\.|)(.+?)(?:\:\/\/|\/)(.+?)(?:\?|$)', movie.guid) 86 | agent = movie_reg.group(1) 87 | movie_id = movie_reg.group(2) 88 | if agent == 'imdb': 89 | # com.plexapp.agents.imdb://tt0137523?lang=en 90 | return movie_id # "tt" + re.search(r'tt(\d+)\?', movie.guid).group(1) 91 | if agent == 'themoviedb': 92 | # com.plexapp.agents.themoviedb://550?lang=en 93 | if not global_vars.TMDB_API_KEY: 94 | print(f"WARNING: Skipping, No TMDB API key: {movie.title}") 95 | return None 96 | 97 | with shelve.open('tmdb_ids', 'c', writeback=True) as s_db: 98 | tmdb_id = movie_id 99 | if s_db.get(tmdb_id) is None: 100 | s_db[tmdb_id] = tmdb_movie.external_ids(tmdb_id)["imdb_id"] 101 | imdb_id = s_db[tmdb_id] 102 | return imdb_id 103 | if agent == 'movie': 104 | # plex://movie/5d776a8b9ab544002150043a 105 | # NEW AGENT, No external IDs available yet 106 | x_imdb_id = None 107 | for guid in movie.guids: 108 | if "imdb" in guid.id: 109 | x_imdb_id = guid.id.replace('imdb://','') 110 | 111 | return x_imdb_id 112 | 113 | if agent == 'local': 114 | print(f"WARNING: Skipping movie, using local agent: {movie.title}") 115 | return None 116 | 117 | except Exception as ex: # pylint: disable=broad-except 118 | print(f"IMDB ID ERROR: {movie.title}, {movie.guid}, {ex}") 119 | return None 120 | -------------------------------------------------------------------------------- /functions/plex_library/shows.py: -------------------------------------------------------------------------------- 1 | """ Methods to get show data from plex """ 2 | import global_vars 3 | 4 | from functions.plex_library.library_utils import show_dict_progress 5 | 6 | from utils.logger import log_timer 7 | 8 | 9 | def get_library_show_dictionary(plex): 10 | """ Returns a dictionary for all shows by ID for easier searching """ 11 | all_shows = get_library_shows(plex) 12 | 13 | print("Creating show dictionary based on TVDB ID") 14 | return create_show_id_dict(all_shows) 15 | 16 | 17 | 18 | def get_library_shows(plex): 19 | """ Gets a list of shows from plex libraries """ 20 | # Get list of shows from the Plex server 21 | # split into array 22 | show_libs = global_vars.SHOW_LIBRARY_NAME.split(",") 23 | all_shows = [] 24 | # loop movie lib array 25 | for lib in show_libs: 26 | lib = lib.strip() 27 | print("Retrieving a list of shows from the '{library}' " 28 | "library in Plex...".format(library=lib)) 29 | try: 30 | show_library = plex.library.section(lib) 31 | new_shows = show_library.all() 32 | all_shows = all_shows + new_shows 33 | print("Added {length} shows to your 'all shows' list from the '{library}' " 34 | "library in Plex...".format(library=lib, length=len(new_shows))) 35 | log_timer() 36 | except Exception: # pylint: disable=broad-except 37 | print("The '{library}' library does not exist in Plex.".format(library=lib)) 38 | 39 | print("Found {0} show total in 'all shows' list from Plex...".format( 40 | len(all_shows) 41 | )) 42 | 43 | return all_shows 44 | 45 | def create_show_id_dict(shows): 46 | """ Creates a dictionary for easy searching by tvdb ID """ 47 | show_id_dict = {} 48 | count = len(shows) 49 | cur = 1 50 | for show in shows: 51 | show_id_dict = append_show_id_dict(show, show_id_dict) 52 | show_dict_progress(cur, count) 53 | cur += 1 54 | print(f"\nFinished Creating Show Dictionary") 55 | return show_id_dict 56 | 57 | def append_show_id_dict(show, show_id_dict): 58 | """ Adds show to dictionary with tvdb ID as key and show as value """ 59 | tvdb_id = get_tvdb_id(show) 60 | if tvdb_id is not None: 61 | show_id_dict[tvdb_id] = show 62 | return show_id_dict 63 | 64 | def get_tvdb_id(show): 65 | """ Gets the tvdb ID from the show """ 66 | tvdb_id = None 67 | last_episode = show.episodes()[-1] 68 | if last_episode.guid != "" and 'thetvdb://' in last_episode.guid: 69 | tvdb_id = last_episode.guid.split('thetvdb://')[1].split('?')[0].split('/')[0] 70 | return tvdb_id 71 | -------------------------------------------------------------------------------- /functions/sources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbrink231/plex_top_playlists/0396e5bd2f5eed963c7f1aa7bd18cf98b277ea7f/functions/sources/__init__.py -------------------------------------------------------------------------------- /functions/sources/imdb.py: -------------------------------------------------------------------------------- 1 | """ IMDB lists translated into FilmLists """ 2 | from urllib.parse import urlparse, urlunparse, parse_qsl, urlencode 3 | from typing import List 4 | from lxml import html 5 | import requests 6 | 7 | from classes import FilmList, FilmItem, FilmDB, FilmType, ListSource 8 | import global_vars 9 | 10 | def imdb_list_loop() -> List[FilmList]: 11 | """ Loops IMDB lists and returns FilmLists """ 12 | print('count of imdb lists: {}'.format(len(global_vars.IMDB_LISTS))) 13 | film_lists = [] 14 | for runlist in global_vars.IMDB_LISTS: 15 | film_list = get_imdb_film_list(runlist) 16 | 17 | if film_list: 18 | film_lists.append(film_list) 19 | 20 | return film_lists 21 | 22 | def get_imdb_film_list(runlist): 23 | """ Gets a FilmList from and IMDB list """ 24 | using_url = runlist["url"] 25 | list_type = runlist["type"] 26 | imdb_ids, title = get_imdb_info(using_url, list_type) 27 | 28 | if runlist["title"]: 29 | title = runlist["title"] # imdb_list_name(tree, runlist["type"]) 30 | 31 | if not title: 32 | print("SKIPPING LIST because no title found") 33 | return None 34 | 35 | film_items = [] 36 | for imdb_id in imdb_ids: 37 | film_items.append(FilmItem(imdb_id, FilmDB.IMDB, FilmType.MOVIE)) 38 | 39 | kind = runlist.get("kind", 'playlist') 40 | show_summary = runlist.get("show_summary", True) 41 | 42 | return FilmList(ListSource.IMDB, title, film_items, kind, show_summary) 43 | 44 | def get_imdb_info(using_url, list_type): 45 | """ Gets the imdb_ids and title from a imdb url with type """ 46 | title = None 47 | imdb_ids = [] 48 | while True: 49 | print("getting imdb ids from url: {0}".format(using_url)) 50 | page = requests.get(using_url) 51 | tree = html.fromstring(page.content) 52 | new_ids = imdb_list_ids(tree, list_type) 53 | imdb_ids += new_ids 54 | 55 | if title is None: 56 | title = imdb_list_name(tree, list_type) 57 | 58 | is_next = tree.xpath("//a[@class='flat-button lister-page-next next-page']") 59 | if is_next: 60 | parsed = urlparse(using_url) 61 | url_parts = list(parsed) 62 | query = dict(parse_qsl(url_parts[4])) 63 | cur_page = query.get("page", 1) 64 | next_page = int(cur_page) + 1 65 | params = {'page': next_page} 66 | query.update(params) 67 | url_parts[4] = urlencode(query) 68 | using_url = urlunparse(url_parts) 69 | else: 70 | break 71 | 72 | return imdb_ids, title 73 | 74 | def imdb_list_name(tree, list_type): 75 | """ Gets the imdb list name from the html """ 76 | if list_type == "chart": 77 | return tree.xpath("//h1[contains(@class, 'header')]")[0].text.strip() 78 | if list_type == "search": 79 | return tree.xpath("//h1[contains(@class, 'header')]")[0].text.strip() 80 | if list_type == "custom": 81 | return tree.xpath("//h1[contains(@class, 'header list-name')]")[0].text.strip() 82 | return 83 | 84 | def imdb_list_ids(tree, list_type): 85 | """ Gets teh imdb id from the html """ 86 | if list_type == "chart": 87 | return tree.xpath("//table[contains(@class, 'chart')]//td[@class='ratingColumn']/div//@data-titleid") 88 | if list_type == "search": 89 | return tree.xpath("//img[@class='loadlate']/@data-tconst") 90 | if list_type == "custom": 91 | return tree.xpath("//div[contains(@class, 'lister-item-image ribbonize')]/@data-tconst") 92 | return 93 | -------------------------------------------------------------------------------- /functions/sources/trakt.py: -------------------------------------------------------------------------------- 1 | """ Methods to pull trakt data into Film Lists """ 2 | from urllib.request import Request, urlopen 3 | import json 4 | 5 | from classes import ListSource, FilmItem, FilmList, FilmDB, FilmType 6 | from utils.logger import log_output 7 | import global_vars 8 | 9 | def trakt_list_loop(): 10 | """ Returns all trakt film lists """ 11 | all_trakt_film_lists = [] 12 | all_trakt_film_lists += trakt_movie_list_loop() 13 | all_trakt_film_lists += trakt_show_list_loop() 14 | all_trakt_film_lists += trakt_user_list_loop() 15 | return all_trakt_film_lists 16 | 17 | def trakt_movie_list_loop(): 18 | """ returns all film lists from the trakt movies api """ 19 | if global_vars.TRAKT_API_KEY is None: 20 | print("No Trakt API key, skipping Trakt movie lists") 21 | return None 22 | 23 | print(f'count of trakt movies lists: {len(global_vars.TRAKT_MOVIE_LISTS)}') 24 | 25 | return get_film_lists(global_vars.TRAKT_MOVIE_LISTS, 'movie') 26 | 27 | def trakt_show_list_loop(): 28 | """ returns all film lists from the trakt shows api """ 29 | if global_vars.TRAKT_API_KEY is None: 30 | print("No Trakt API key, skipping Trakt Show lists") 31 | return None 32 | 33 | print(f'count of trakt shows lists: {len(global_vars.TRAKT_SHOW_LISTS)}') 34 | 35 | return get_film_lists(global_vars.TRAKT_SHOW_LISTS, 'show') 36 | 37 | def trakt_user_list_loop(): 38 | """ returns all film lists from the trakt shows api """ 39 | if global_vars.TRAKT_API_KEY is None: 40 | print("No Trakt API key, skipping Trakt Show lists") 41 | return None 42 | 43 | print(f'count of trakt users lists: {len(global_vars.TRAKT_USERS_LISTS)}') 44 | 45 | return get_film_lists(global_vars.TRAKT_USERS_LISTS) 46 | 47 | def get_film_lists(trakt_lists, response_base_type=None): 48 | """ Loops trakt settings lists and returns setup film lists """ 49 | film_lists = [] 50 | for runlist in trakt_lists: 51 | kind = runlist.get("kind", 'playlist') 52 | show_summary = runlist.get("show_summary", True) 53 | title = runlist["title"] 54 | print("PULLING LIST - {0}: URL: {1} - TYPE: {2} - KIND: {3}".format( 55 | title, 56 | runlist.get("url"), 57 | runlist.get("type"), 58 | kind 59 | )) 60 | item_base = None 61 | # Popular lists are based on type and returned JSON is different 62 | # Watched returns normal json 63 | if runlist.get("type") == "popular": 64 | if response_base_type: 65 | item_base = response_base_type 66 | else: 67 | raise Exception("Missing required base for popular list") 68 | 69 | trakt_data = request_trakt_list(runlist["url"], runlist["limit"]) 70 | if trakt_data is None: 71 | print(f"WARNING: SKIPPING LIST, No trakt data for list {title}") 72 | return [] 73 | 74 | trakt_film_items = get_film_items(trakt_data, item_base) 75 | 76 | film_lists.append(FilmList(ListSource.TRAKT, title, trakt_film_items, kind, show_summary)) 77 | 78 | return film_lists 79 | 80 | def get_film_items(trakt_json, item_base): 81 | """ converts data to film items from trakt user api endpoint """ 82 | log_output(f"trakt json {trakt_json}", 3) 83 | film_items = [] 84 | for item in trakt_json: 85 | film_items.append(get_film_item(item, item_base)) 86 | 87 | return film_items 88 | 89 | def get_film_item(item, item_base): 90 | """ Grabs the correct information and returns a Film Item """ 91 | log_output(f"trakt user item: {item}", 3) 92 | if item_base == 'movie': 93 | return FilmItem( 94 | film_id=item['ids']['imdb'], 95 | film_db=FilmDB.IMDB, 96 | film_type=FilmType.MOVIE, 97 | title=item['title'] 98 | ) 99 | if item_base == 'show': 100 | return FilmItem( 101 | film_id=item['ids']['tvdb'], 102 | film_db=FilmDB.TVDB, 103 | film_type=FilmType.SHOW, 104 | title=item['title'] 105 | ) 106 | if item.get('type') == 'movie' or item.get('movie'): 107 | return FilmItem( 108 | film_id=item['movie']['ids']['imdb'], 109 | film_db=FilmDB.IMDB, 110 | film_type=FilmType.MOVIE, 111 | title=item['movie']['title'] 112 | ) 113 | if item.get('type') == 'show' or item.get('show'): 114 | season_num = None 115 | episode_num = None 116 | if item.get('episode'): 117 | season_num = item['episode']['season'] 118 | episode_num = item['episode']['number'] 119 | 120 | if item.get('season'): 121 | season_num = item['season']['number'] 122 | 123 | return FilmItem( 124 | film_id=str(item['show']['ids']['tvdb']), 125 | film_db=FilmDB.TVDB, 126 | film_type=FilmType.SHOW, 127 | title=item['show']['title'], 128 | season_num=season_num, 129 | episode_num=episode_num 130 | ) 131 | 132 | 133 | def request_trakt_list(url, limit): 134 | """ retrieves data from trakt using the api """ 135 | headers = { 136 | 'Content-Type': 'application/json', 137 | 'trakt-api-version': '2', 138 | 'trakt-api-key': global_vars.TRAKT_API_KEY 139 | } 140 | request = Request('{}?page=1&limit={}'.format(url, limit), headers=headers) 141 | try: 142 | response = urlopen(request) 143 | trakt_data = json.load(response) 144 | return trakt_data 145 | except Exception as ex: # pylint: disable=broad-except 146 | print(f"ERROR: Bad Trakt Request: {ex}") 147 | return None -------------------------------------------------------------------------------- /functions/users.py: -------------------------------------------------------------------------------- 1 | """ Methods to pull user information from Plex """ 2 | import requests 3 | import xmltodict 4 | import global_vars 5 | from utils.logger import log_output 6 | 7 | def get_all_users(plex): 8 | """ Get all Plex Server users respecting settings.ini """ 9 | machine_id = plex.machineIdentifier 10 | log_output(f"Plex Machine Identifier: {machine_id}", 3) 11 | headers = {'Accept': 'application/json', 'X-Plex-Token': global_vars.PLEX_TOKEN} 12 | result = requests.get( 13 | 'https://plex.tv/api/servers/{server_id}/shared_servers?X-Plex-Token={token}'.format( 14 | server_id=machine_id, 15 | token=global_vars.PLEX_TOKEN 16 | ), 17 | headers=headers 18 | ) 19 | xml_data = xmltodict.parse(result.content) 20 | 21 | result2 = requests.get('https://plex.tv/api/users', headers=headers) 22 | log_output(result2, 3) 23 | xml_data_2 = xmltodict.parse(result2.content) 24 | log_output(xml_data_2, 3) 25 | 26 | users = {} 27 | user_ids = {} 28 | if 'User' in xml_data_2['MediaContainer'].keys(): 29 | # has atleast 1 shared user generally 30 | # if only 1 user then User will not be an array 31 | log_output(xml_data_2['MediaContainer']['User'], 3) 32 | 33 | if isinstance(xml_data_2['MediaContainer']['User'], list): 34 | user_ids = {plex_user['@id']: plex_user.get('@username', plex_user.get('@title')) for plex_user in xml_data_2['MediaContainer']['User']} 35 | else: 36 | plex_user = xml_data_2['MediaContainer']['User'] 37 | user_ids[plex_user['@id']] = plex_user.get('@username', plex_user.get('@title')) 38 | 39 | log_output(user_ids, 3) 40 | 41 | if 'SharedServer' in xml_data['MediaContainer']: 42 | # has atlease 1 shared server 43 | if isinstance(xml_data['MediaContainer']['SharedServer'], list): 44 | # more than 1 shared user 45 | for server_user in xml_data['MediaContainer']['SharedServer']: 46 | users[user_ids[server_user['@userID']]] = server_user['@accessToken'] 47 | else: 48 | # only 1 shared user 49 | server_user = xml_data['MediaContainer']['SharedServer'] 50 | users[user_ids[server_user['@userID']]] = server_user['@accessToken'] 51 | 52 | log_output(users, 3) 53 | 54 | return users 55 | 56 | 57 | def get_user_tokens(plex): 58 | """ Get the token for a user to connect to plex as them """ 59 | users = get_all_users(plex) 60 | allowed_users = {} 61 | for user in users: 62 | if ((not global_vars.ALLOW_SYNCED_USERS or user in global_vars.ALLOW_SYNCED_USERS) and 63 | user not in global_vars.NOT_ALLOW_SYNCED_USERS): 64 | allowed_users[user] = users[user] 65 | 66 | return allowed_users 67 | -------------------------------------------------------------------------------- /global_vars.py: -------------------------------------------------------------------------------- 1 | """ Container for all globals used in this app """ 2 | import configparser 3 | import os 4 | import sys 5 | import json 6 | import time 7 | 8 | from utils.logger import log_output 9 | 10 | CONFIG_PATH = os.path.join( 11 | os.path.dirname(os.path.realpath(__file__)), 12 | 'settings.ini' 13 | ) 14 | 15 | if not os.path.isfile(CONFIG_PATH): 16 | print("Please create a settings.ini file in the same folder as plex_playlist_update.py") 17 | sys.exit("No settings.ini file") 18 | 19 | CONFIG = configparser.SafeConfigParser() 20 | CONFIG.read(CONFIG_PATH) 21 | 22 | global START_TIME 23 | START_TIME = time.time() 24 | 25 | global TEST 26 | TEST = 'hello' 27 | 28 | global PLEX_URL 29 | PLEX_URL = CONFIG.get('Plex', 'plex-host') 30 | 31 | global PLEX_TOKEN 32 | PLEX_TOKEN = CONFIG.get('Plex', 'plex-token') 33 | 34 | global MOVIE_LIBRARY_NAME 35 | MOVIE_LIBRARY_NAME = CONFIG.get('Plex', 'movie-library') 36 | 37 | global SHOW_LIBRARY_NAME 38 | SHOW_LIBRARY_NAME = CONFIG.get('Plex', 'tv-library') 39 | 40 | global SYNC_WITH_SHARED_USERS 41 | SYNC_WITH_SHARED_USERS = CONFIG.getboolean('Plex', 'shared') 42 | 43 | global ALLOW_SYNCED_USERS 44 | ALLOW_SYNCED_USERS = json.loads(CONFIG.get('Plex', 'users')) 45 | 46 | global NOT_ALLOW_SYNCED_USERS 47 | NOT_ALLOW_SYNCED_USERS = json.loads(CONFIG.get('Plex', 'not_users')) 48 | 49 | global VERBOSE 50 | try: 51 | VERBOSE = int(CONFIG.get('Plex', 'verbose')) 52 | except Exception: # pylint: disable=broad-except 53 | VERBOSE = 0 54 | 55 | global PLEX_TIMEOUT 56 | try: 57 | PLEX_TIMEOUT = int(CONFIG.get('Plex', 'timeout')) 58 | except Exception: # pylint: disable=broad-except 59 | PLEX_TIMEOUT = 300 60 | 61 | global SHOW_MISSING 62 | try: 63 | SHOW_MISSING = CONFIG.getboolean('Plex', 'show_missing') 64 | except Exception: # pylint: disable=broad-except 65 | SHOW_MISSING = False 66 | 67 | global MISSING_COLUMNS 68 | try: 69 | MISSING_COLUMNS = int(CONFIG.get('Plex', 'missing_columns')) 70 | except Exception: # pylint: disable=broad-except 71 | MISSING_COLUMNS = 4 72 | 73 | try: 74 | TMDB_API_KEY = config.get('TMDb', 'api-key') 75 | except: 76 | TMDB_API_KEY = None 77 | 78 | global TRAKT_API_KEY 79 | try: 80 | TRAKT_API_KEY = CONFIG.get('Trakt', 'api-key') 81 | except Exception: # pylint: disable=broad-except 82 | TRAKT_API_KEY = None 83 | 84 | global TRAKT_MOVIE_LISTS 85 | try: 86 | TRAKT_MOVIE_LISTS = json.loads(CONFIG.get('Trakt', 'trakt-movie-list')) 87 | except Exception as ex: # pylint: disable=broad-except 88 | print(f"SKIPPING SETTINGS LIST: Problem with trakt-movie-list, {ex}") 89 | log_output(sys.exc_info(), 3) 90 | TRAKT_MOVIE_LISTS = [] 91 | 92 | global TRAKT_SHOW_LISTS 93 | try: 94 | TRAKT_SHOW_LISTS = json.loads(CONFIG.get('Trakt', 'trakt-tv-list')) 95 | except Exception as ex: # pylint: disable=broad-except 96 | print(f"SKIPPING SETTINGS LIST: Problem with trakt-show-list, {ex}") 97 | log_output(sys.exc_info(), 3) 98 | TRAKT_SHOW_LISTS = [] 99 | 100 | global TRAKT_USERS_LISTS 101 | try: 102 | TRAKT_USERS_LISTS = json.loads(CONFIG.get('Trakt', 'trakt-users-list')) 103 | except Exception as ex: # pylint: disable=broad-except 104 | print(f"SKIPPING SETTINGS LIST: Problem with trakt-user-list, {ex}") 105 | log_output(sys.exc_info(), 3) 106 | TRAKT_USERS_LISTS = [] 107 | 108 | global IMDB_LISTS 109 | try: 110 | IMDB_LISTS = json.loads(CONFIG.get('IMDb', 'imdb-lists')) 111 | except Exception as ex: # pylint: disable=broad-except 112 | print(f"SKIPPING SETTINGS LIST: Problem with imdb-lists, {ex}") 113 | log_output(sys.exc_info(), 3) 114 | IMDB_LISTS = [] 115 | 116 | global DISCORD_URL 117 | try: 118 | DISCORD_URL = CONFIG.get('Discord', 'webhook_url') 119 | except Exception: # pylint: disable=broad-except 120 | DISCORD_URL = None 121 | -------------------------------------------------------------------------------- /plex_playlist_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Main method to run playlist script along with helpful commands """ 3 | # a python test script 4 | # -*- coding: utf-8 -*- 5 | 6 | # ------------------------------------------------------------------------------ 7 | # 8 | # Automated Ranked Playlists - VikingGawd 9 | # 10 | # *** Use at your own risk! *** 11 | # *** I am not responsible for damages to your Plex server or libraries. *** 12 | # 13 | # ------------------------------------------------------------------------------ 14 | 15 | import sys 16 | import json 17 | import requests 18 | 19 | import global_vars 20 | 21 | # from classes.ListItem import ListItem 22 | from functions.users import get_all_users, get_user_tokens 23 | from functions.sources.imdb import imdb_list_loop, get_imdb_info 24 | from functions.sources.trakt import trakt_list_loop 25 | from functions.playlists import remove_shared_playlist, remove_playlists_for_user 26 | from functions.plex_connection import plex_user_connection 27 | 28 | from utils.logger import log_timer 29 | 30 | from classes import PlexData 31 | 32 | ####### CODE HERE (Nothing to change) ############ 33 | 34 | def list_updater(plex): 35 | """ Runs main method to update all playlists and collections """ 36 | plex_data = PlexData(plex) 37 | 38 | plex_data.display_shared_users() 39 | 40 | film_lists = [] 41 | 42 | film_lists = film_lists + trakt_list_loop() 43 | film_lists = film_lists + imdb_list_loop() 44 | 45 | # Process Lists 46 | for filmlist in film_lists: 47 | filmlist.setup_playlist(plex_data) 48 | 49 | if __name__ == "__main__": 50 | print("===================================================================") 51 | print(" Automated Playlist to Plex script ") 52 | print("===================================================================\n") 53 | 54 | if (len(sys.argv) == 1 or sys.argv[1] not in [ 55 | 'test', 56 | 'run', 57 | 'imdb_ids', 58 | 'show_users', 59 | 'show_allowed', 60 | 'remove_playlist', 61 | 'remove_all_playlists', 62 | 'discord_test' 63 | ]): 64 | print(""" 65 | Please use one of the following commands: 66 | run - Will start the normal process from your settings 67 | imdb_ids - needs url (in quotes probably) then type (custom,chart,search) to show list of imdb ids from url 68 | show_users - will give you a list of users to copy and paste to your settings file 69 | show_allowed - will give you a list of users your script will create playlists on 70 | remove_playlist - needs a second argument with playlist name to remove 71 | remove_all_playlists - will remove all playlists setup in the settings 72 | discord_test - send a test to your discord channel to make sure it works 73 | 74 | ex: 75 | python {0} run 76 | python {0} remove_playlist somename 77 | """.format(sys.argv[0])) 78 | sys.exit() 79 | 80 | # login to plex here. All commands will need it 81 | try: 82 | PLEX = plex_user_connection() 83 | except Exception: # pylint: disable=broad-except 84 | print("No Plex server found at: {base_url} or bad plex token code".format( 85 | base_url=global_vars.PLEX_URL 86 | )) 87 | input("press enter to exit") 88 | sys.exit() 89 | 90 | 91 | # run standard 92 | if sys.argv[1] == 'run': 93 | list_updater(PLEX) 94 | 95 | # check imdb list from url 96 | if sys.argv[1] == 'imdb_ids': 97 | if len(sys.argv) >= 3: 98 | IMDB_IDS, TITLE = get_imdb_info(sys.argv[2], sys.argv[3]) 99 | print('title: {0}, count: {2} ids: {1}'.format(TITLE, IMDB_IDS, len(IMDB_IDS))) 100 | else: 101 | print("Please supply url then type") 102 | 103 | 104 | # display available users 105 | if sys.argv[1] == 'show_users': 106 | USERS = get_all_users(PLEX) 107 | print('{} shared users'.format(len(USERS))) 108 | for key, value in USERS.items(): 109 | print('Username: {}'.format(key)) 110 | 111 | # display allowed users based on settings 112 | if sys.argv[1] == 'show_allowed': 113 | USERS = get_user_tokens(PLEX) 114 | print('{} shared users'.format(len(USERS))) 115 | for key, value in USERS.items(): 116 | print('Username: {}'.format(key)) 117 | 118 | if sys.argv[1] == 'remove_playlist': 119 | if len(sys.argv) >= 3: 120 | print('removing playlist {}'.format(sys.argv[2])) 121 | remove_shared_playlist(PLEX, get_all_users(PLEX), sys.argv[2]) 122 | else: 123 | print("Please supply a playlist name for the second command argument") 124 | 125 | if sys.argv[1] == 'remove_all_playlists': 126 | removing_film_lists = [] 127 | removing_film_lists += trakt_list_loop() 128 | removing_film_lists += imdb_list_loop() 129 | 130 | remove_playlists_for_user(PLEX, removing_film_lists) 131 | 132 | if sys.argv[1] == 'discord_test': 133 | print("Testing sending to discord") 134 | print("using URL: {}".format(global_vars.DISCORD_URL)) 135 | 136 | MESSAGE = {} 137 | FIRST_PART = {} 138 | FIRST_PART["title"] = "Playlists connected" 139 | MESSAGE['embeds'] = [FIRST_PART] 140 | JSON_DATA = json.dumps(MESSAGE) 141 | RES = requests.post( 142 | global_vars.DISCORD_URL, 143 | headers={"Content-Type":"application/json"}, 144 | json=MESSAGE 145 | ) 146 | if RES.status_code == 204: 147 | print("You should see a message in discord.") 148 | 149 | if sys.argv[1] == 'test': 150 | print("Testing") 151 | 152 | 153 | print("\n===================================================================") 154 | print(" Done! ") 155 | print("===================================================================\n") 156 | 157 | log_timer() 158 | -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | configparser==3.7.4 2 | lxml==4.3.2 3 | parse==1.11.1 4 | requests==2.21.0 5 | xmltodict==0.12.0 6 | plexapi==4.4.1 7 | tmdbv3api==1.5.1 8 | -------------------------------------------------------------------------------- /settings.ini.example: -------------------------------------------------------------------------------- 1 | [Plex] 2 | ### [REQUIRED] Plex server details - Github for instructions on token ### 3 | plex-host: http://localhost:32400 4 | plex-token: 5 | 6 | ### [REQUIRED] Current library info - Change defaults to match your current librarys ### 7 | # supports multiple libs, if separate by comma, no spaces 8 | # e.g.: movie-library: Adult Movies,Kid Movies 9 | # e.g.: tv-library: Adult TV,Kid TV 10 | movie-library: Movies 11 | tv-library: TV Shows 12 | 13 | # Share playlist with other user? 14 | # Choices True, False -- Caps matter, (if True, syncs all or list, if false, only token user) 15 | shared: False 16 | 17 | # (keep empty list for all users, comma list for specific users.) 18 | # EX: ["username","anotheruser"] << notice the use of double-quotes 19 | # $shared must be True. 20 | users: [] 21 | 22 | # (keep empty list to allow all users, comma list for specific users to block.) 23 | # EX ["greg","luiz"] << notice the use of double-quotes 24 | # $shared must be True. 25 | not_users: [] 26 | 27 | ### [OPTIONAL] Plex server timeout setting .. default 300secs 28 | # Uncomment and increase if you are experiencing timeout issues 29 | # timeout: 300 30 | 31 | ### [OPTIONAL] Show the list of missing movies 32 | # Set to True to see missing IMDB IDs from movie lists. default is False 33 | show_missing: False 34 | 35 | ### [OPTIONAL] logging level. value possibilities: [-1, 0, 1, 2, 3]. 36 | # -1 = quiet, 0 = normal, 1 = verbose, 2 = very verbose, 3 = debug 37 | verbose: 0 38 | 39 | ### [OPTIONAL] Missing list columns 40 | # number of columns wide to show list. default is 4 41 | missing_columns: 4 42 | 43 | [TMDb] 44 | ### [OPTIONAL] TMDb API Info ### 45 | # This is REQUIRED for TMDb. If you have your libraries set to use TMDb Agent you will not be able to create any playlists. 46 | api-key: 47 | 48 | [Trakt] 49 | ### [OPTIONAL] Trakt API Info ### 50 | # This is REQUIRED for Trakt. Without it you will not get any Trakt playlists 51 | api-key: 52 | 53 | ### Trakt Lists 54 | # has a title, type, limit & url 55 | # type is only required for popular lists 56 | # limit max is 100 57 | # COMMA's matter. 58 | 59 | # Currently 2 different kinds. "playlist" and "collection" 60 | # playlist is default if not supplied 61 | # adding "kind":"collection" will tag all movies and shows with a collection tag. A collection can go on movies or shows and will show together 62 | 63 | ## Movie list 64 | # the "type" must say 'popular' when using a popular trakt list. Otherwise can be omiited or set to any value 65 | # available lists 66 | # 'period' in below urls must be replaced with any of these values (weekly , monthly , yearly , all) 67 | # https://api.trakt.tv/movies/trending 68 | # https://api.trakt.tv/movies/popular MUST SET type TO popular 69 | # https://api.trakt.tv/movies/recommended/period 70 | # https://api.trakt.tv/movies/played/period 71 | # https://api.trakt.tv/movies/watched/period 72 | # https://api.trakt.tv/movies/collected/period 73 | # https://api.trakt.tv/movies/anticipated 74 | # https://api.trakt.tv/movies/boxoffice 75 | 76 | trakt-movie-list: [ 77 | { "title": "Movies Top Weekly", "type":"watched", "limit": 80, "url":"https://api.trakt.tv/movies/watched/weekly"}, 78 | { "title": "Movies Popular", "type":"popular", "limit": 80, "url":"https://api.trakt.tv/movies/popular", "show_summary": "False"} 79 | ] 80 | 81 | ## Show list 82 | # the "type" must say 'popular' when using a popular trakt list. Otherwise can be omiited or set to any value 83 | # available lists 84 | # 'period' in below urls must be replaced with any of these values (weekly , monthly , yearly , all) 85 | # https://api.trakt.tv/shows/trending 86 | # https://api.trakt.tv/shows/popular MUST SET type TO popular 87 | # https://api.trakt.tv/shows/recommended/period 88 | # https://api.trakt.tv/shows/played/period 89 | # https://api.trakt.tv/shows/watched/period 90 | # https://api.trakt.tv/shows/collected/period 91 | # https://api.trakt.tv/shows/anticipated 92 | 93 | trakt-tv-list: [ 94 | { "title": "Show Top Weekly", "type":"watched", "limit": 20, "url":"https://api.trakt.tv/shows/watched/weekly"}, 95 | { "title": "Show Popular", "type":"popular", "limit": 20, "url":"https://api.trakt.tv/shows/popular", "kind":"collection", "show_summary": "False"} 96 | ] 97 | 98 | ## Users list 99 | # The URL has an add '/items' at the end. Please be aware of that when copying the url from the website 100 | # Also when copying a url from web add the 'api' before trakt.tv 101 | 102 | trakt-users-list: [ 103 | { "title": "Show Top Weekly", "limit": 20, "url":"https://api.trakt.tv/users/donxy/lists/marvel-cinematic-universe/items"}, 104 | ] 105 | 106 | 107 | [IMDb] 108 | # this is a LIST of all your fav IMDB lists 109 | # types available are: ['chart', 'search', 'custom'] 110 | # put new list items on a new line 111 | # each line needs a title, url and type 112 | # if title is empty it will try and get the title from the imdb list 113 | # CHECK YOUR COMMAs. Need one after every line except last. Will break 114 | 115 | # Currently 2 different kinds. "playlist" and "collection" 116 | # playlist is default if not supplied 117 | # adding "kind":"collection" will tag all movies with a collection tag. A collection can go on movies or shows and will show together 118 | 119 | ### SEARCH TYPE 120 | # type: "search" 121 | # view MUST BE simple as a query param 122 | # e.g.: &view=simple 123 | # wrapped in double quotes, 124 | 125 | ### CHART TYPE 126 | # used for links with 'chart' in the url 127 | 128 | ### CUSTOM TYPE 129 | # used for links with 'list' in the url 130 | 131 | imdb-lists: [ 132 | { "title":"IMDB Top 250 Example", "type":"chart", "url":"https://www.imdb.com/chart/top", "kind":"collection" }, 133 | { "title":"IMDB Custom Example", "type":"custom", "url":"https://www.imdb.com/list/ls069751712/", "show_summary": "False" }, 134 | { "title":"IMDB Search Example", "type":"search", "url":"https://www.imdb.com/search/title?groups=oscar_best_picture_winners&sort=year,desc&view=simple&count=250" }, 135 | 136 | { "title":"Latest Best Picture-Winning Titles", "type":"search", "url":"http://www.imdb.com/search/title?groups=oscar_best_picture_winners&sort=year,desc&view=simple" }, 137 | { "title":"", "type":"search", "url":"http://www.imdb.com/search/title?year=2017,2017&title_type=feature&sort=moviemeter,asc&view=simple" }, 138 | 139 | { "title":"Star Wars Machete Order", "type":"custom", "url":"http://www.imdb.com/list/ls069751712/" }, 140 | { "title":"2018 Oscar Nominees", "type":"custom", "url":"http://www.imdb.com/list/ls009668531/" }, 141 | { "title":"", "type":"custom", "url":"http://www.imdb.com/list/ls009668531/", "kind":"collection"}, 142 | 143 | { "title":"Top Rated Movies", "type":"chart", "url":"http://www.imdb.com/chart/top"}, 144 | { "title":"Most Popular Movies", "type":"chart", "url":"http://www.imdb.com/chart/moviemeter"}, 145 | { "title":"Top Box Office", "type":"chart", "url":"http://www.imdb.com/chart/boxoffice"}, 146 | { "title":"Top Rated English Movies", "type":"chart", "url":"http://www.imdb.com/chart/top-english-movies"}, 147 | { "title":"", "type":"chart", "url":"http://www.imdb.com/chart/bottom"} 148 | 149 | ] 150 | 151 | 152 | [Discord] 153 | ### Testing phase 154 | # this will send a message for each movie/show summary finished to a channel 155 | # by adding the url you will recieve messages 156 | webhook_url: 157 | 158 | [Radarr] 159 | ### Testing 160 | # this will be used to auto add missing movies from lists 161 | radarr_token: 162 | add_movies: False 163 | # [REQUIRED] Used to pick the profile when adding the movie 164 | quality_profile: 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /tests/parse_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from lxml.html import parse 4 | 5 | #tree = parse("http://www.imdb.com/list/ls069751712/") 6 | tree = parse("http://www.imdb.com/list/ls009668531/") 7 | #custom_ids = tree.xpath("//div[contains(@class, 'lister-item-image ribbonize')]/@data-tconst") 8 | name = tree.xpath("//h1[contains(@class, 'header list-name')]")[0].text.strip() 9 | 10 | print type(tree) 11 | print tree 12 | print name 13 | -------------------------------------------------------------------------------- /tests/test0.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import re 4 | 5 | guid = "com.plexapp.agents.imdb://tt0366548?lang=en" 6 | 7 | print re.search(r'tt(\d+)\?', guid).group(0) 8 | print re.search(r'tt(\d+)\?', guid).group(1) 9 | print re.search(r'tt(\d+)\?', guid).group(2) 10 | -------------------------------------------------------------------------------- /utils/logger.py: -------------------------------------------------------------------------------- 1 | """ Logger for displaying info based on Verbosity """ 2 | import time 3 | import global_vars 4 | 5 | def log_timer(marker="", verbose=0): 6 | """ Shows the time since app started """ 7 | if global_vars.VERBOSE >= verbose and marker: 8 | print("{:.4f} -- {}".format( 9 | (time.time() - global_vars.START_TIME), 10 | marker 11 | )) 12 | 13 | def log_output(text, verbose): 14 | """ Prints log text if it has the right verbosity """ 15 | if global_vars.VERBOSE >= verbose and text: 16 | print(text) 17 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.03' --------------------------------------------------------------------------------