├── .gitignore ├── Dockerfile ├── README.md ├── Screenshots ├── Movies.png └── TV.png ├── config.ini.example ├── popular_plex.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | config.ini 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/vscode,python 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=vscode,python 5 | 6 | ### Python ### 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | pytestdebug.log 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | doc/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | pythonenv* 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | 140 | # pytype static type analyzer 141 | .pytype/ 142 | 143 | # profiling data 144 | .prof 145 | 146 | ### vscode ### 147 | .vscode/* 148 | .vscode/settings.json 149 | !.vscode/tasks.json 150 | !.vscode/launch.json 151 | !.vscode/extensions.json 152 | *.code-workspace 153 | 154 | # End of https://www.toptal.com/developers/gitignore/api/vscode,python 155 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:slim 2 | 3 | COPY . /opt/popularplex 4 | 5 | WORKDIR /opt/popularplex 6 | 7 | RUN pip install -r requirements.txt 8 | 9 | CMD ["python", "popular_plex.py"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Popular Plex 2 | 3 | This script is designed to run weekly, generating collections in Plex of most popular Movies and Television based on the data from the Tautulli API. It uses the [python-plexapi](https://github.com/pkkid/python-plexapi) package and [requests](http://python-requests.org/). It provides two options for login, Server/Access Token or Username/Password/Server, access token is preferred and when set username/password/server can be blank. 4 | 5 | The idea behind this project was to recreate Netflix's 'What's Popular' list 6 | 7 | ![Screenshot of Popular Movies Collections](/Screenshots/Movies.png) 8 | ![Screenshot of Popular TV Collections](/Screenshots/TV.png) 9 | 10 | ## Requirements 11 | 12 | - Python 3 13 | - [Plex](https://plex.tv) 14 | - [Tautulli](https://github.com/Tautulli/Tautulli) 15 | 16 | ## Setup 17 | 18 | ```sh 19 | cp config.ini.example config.ini 20 | ``` 21 | 22 | > Edit `config.ini` in your preferred text editor 23 | 24 | ```sh 25 | pip install -r requirements.txt 26 | python popular_plex.py 27 | ``` 28 | 29 | ### Running with Docker 30 | [Docker Hub](https://hub.docker.com/r/bearlikelion/popularplex) 31 | 32 | ```sh 33 | docker build -t popularplex . 34 | docker run popularplex 35 | ``` 36 | 37 | ## Scheduling 38 | 39 | For Linux schedule using a cronjob, on Windows create a scheduled task to launch the script 40 | 41 | ```sh 42 | 0 3 * * 6 python3 /path/popular_plex.py 43 | ``` 44 | -------------------------------------------------------------------------------- /Screenshots/Movies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearlikelion/popularplex/c4918e69adf22bc3c3e54dcf7907c6c8fc2603ee/Screenshots/Movies.png -------------------------------------------------------------------------------- /Screenshots/TV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bearlikelion/popularplex/c4918e69adf22bc3c3e54dcf7907c6c8fc2603ee/Screenshots/TV.png -------------------------------------------------------------------------------- /config.ini.example: -------------------------------------------------------------------------------- 1 | [Plex] 2 | url = http://plex:32400 3 | token = ACCESS_TOKEN 4 | username = bearlikelion 5 | password = password 6 | server = plex.arneman.home 7 | 8 | [Tautulli] 9 | url = http://plexpy 10 | apikey = API_KEY 11 | timerange = 7 12 | count = 10 13 | 14 | [Collections] 15 | movies = Most Popular Movies 16 | tv = Most Popular TV 17 | limit = 10 18 | 19 | [Libraries] 20 | movies = Movies 21 | tv = TV Shows -------------------------------------------------------------------------------- /popular_plex.py: -------------------------------------------------------------------------------- 1 | """Generate Plex Playlists based on most popular from Tatulli 2 | """ 3 | import configparser 4 | import requests 5 | import os 6 | 7 | from plexapi.myplex import MyPlexAccount 8 | from plexapi.server import PlexServer 9 | 10 | 11 | def get_popular(): 12 | """Get Home Statistics from Tautulli API 13 | """ 14 | print("Getting popular media") 15 | stats = requests.get(config['Tautulli']['url'] + '/api/v2?apikey=' + 16 | config['Tautulli']['apikey'] + '&cmd=get_home_stats&time_range=' + 17 | config['Tautulli']['timerange'] + "&stats_count=" + 18 | config['Tautulli']['count']) 19 | 20 | response = stats.json() 21 | _popular = {} 22 | 23 | for entry in response['response']['data']: 24 | if entry['stat_id'] == 'popular_movies': 25 | _popular['movies'] = entry['rows'] 26 | if entry['stat_id'] == 'popular_tv': 27 | _popular['tv'] = entry['rows'] 28 | 29 | return _popular 30 | 31 | 32 | def clear_collections(movies_library, tv_library): 33 | """ Clear media items from collections 34 | """ 35 | print("Clear Collections") 36 | movies_collection = movies_library.collection() 37 | for m_collection in movies_collection: 38 | if m_collection.title == config['Collections']['movies']: 39 | for movies in m_collection.children: 40 | print(" Removing %s from %s" % (movies.title, config['Collections']['movies'])) 41 | movies.removeCollection(config['Collections']['movies']) 42 | 43 | tv_collection = tv_library.collection() 44 | for t_collection in tv_collection: 45 | if t_collection.title == config['Collections']['tv']: 46 | for show in t_collection.children: 47 | print(" Removing %s from %s" % (show.title, config['Collections']['tv'])) 48 | show.removeCollection(config['Collections']['movies']) 49 | 50 | 51 | def generate_collections(popular_dict, movies_library, tv_library): 52 | """ Generate collections based on popular items 53 | 54 | Args: 55 | popular_dict (dict): Popular Movies and TV Dictionary 56 | """ 57 | current_int = 0 58 | limit = int(config['Collections']['limit']) 59 | 60 | print("Creating Movie Collection") 61 | for movie in popular_dict['movies']: 62 | if current_int < limit: 63 | _movie = plex.fetchItem(movie['rating_key']) 64 | if _movie.librarySectionID == movies_library.key: 65 | _movie.addCollection(config['Collections']['movies']) 66 | print(" Added %s to %s" % (_movie.title, config['Collections']['movies'])) 67 | current_int += 1 68 | else: 69 | print("Movie Collection limit reached") 70 | break 71 | 72 | print("Creating TV Collection") 73 | current_int = 0 74 | for show in popular_dict['tv']: 75 | if current_int < limit: 76 | _show = plex.fetchItem(show['rating_key']) 77 | if _show.librarySectionID == tv_library.key: 78 | _show.addCollection(config['Collections']['tv']) 79 | print(" Added %s to %s" % (_show.title, config['Collections']['tv'])) 80 | current_int += 1 81 | else: 82 | print("TV Collection limit reached") 83 | break 84 | 85 | 86 | if __name__ == '__main__': 87 | config = configparser.ConfigParser() 88 | 89 | if (os.path.exists('config.ini')): 90 | config.read('config.ini') 91 | else: 92 | config.read(os.path.dirname(__file__) + os.path.sep + 'config.ini') 93 | 94 | # Login to Plex 95 | if (config['Plex']['url'] != '' and config['Plex']['token'] != ''): 96 | print("Logging in using token") 97 | plex = PlexServer(config['Plex']['url'], config['Plex']['token']) 98 | elif (config['Plex']['username'] != '' and config['Plex']['password'] != ''): 99 | print("Logging in using username/password") 100 | account = MyPlexAccount(config['Plex']['username'], config['Plex']['password']) 101 | if config['Plex']['server'] != '': 102 | plex = account.resource(config['Plex']['server']).connect() 103 | else: 104 | Exception("[Config file invalid] - Missing [Plex] Server") 105 | else: 106 | Exception("[Config file invalid] - Failed to login to Plex") 107 | 108 | movies_library = plex.library.section(config['Libraries']['movies']) 109 | tv_library = plex.library.section(config['Libraries']['tv']) 110 | 111 | clear_collections(movies_library, tv_library) 112 | popular = get_popular() 113 | generate_collections(popular, movies_library, tv_library) 114 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.4.2 2 | certifi==2020.6.20 3 | chardet==3.0.4 4 | colorama==0.4.4 5 | idna==2.10 6 | isort==5.6.4 7 | lazy-object-proxy==1.4.3 8 | mccabe==0.6.1 9 | PlexAPI==4.1.2 10 | requests==2.24.0 11 | six==1.15.0 12 | toml==0.10.1 13 | urllib3==1.25.11 14 | wrapt==1.12.1 15 | --------------------------------------------------------------------------------