├── run.bat ├── providers.txt ├── .gitattributes ├── requirements.txt ├── config.yaml.example ├── providers.py ├── .gitignore ├── README.md └── elsewherr.py /run.bat: -------------------------------------------------------------------------------- 1 | call python elsewherr.py -------------------------------------------------------------------------------- /providers.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adman1020/Elsewherr/HEAD/providers.txt -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adman1020/Elsewherr/HEAD/requirements.txt -------------------------------------------------------------------------------- /config.yaml.example: -------------------------------------------------------------------------------- 1 | tmdbApiKey: 2 | providerRegion: GB 3 | radarrApiKey: 4 | radarrUrl: http://localhost:7878 5 | requiredProviders: 6 | - Netflix 7 | - Amazon Prime Video 8 | - Disney Plus 9 | tagPrefix: elsewherr- -------------------------------------------------------------------------------- /providers.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import yaml 4 | 5 | config = yaml.safe_load(open("config.yaml")) 6 | 7 | try: 8 | os.remove("providers.txt") 9 | except: 10 | pass 11 | 12 | tmdbHeaders = {'Content-Type': 'application/json'} 13 | 14 | tmdbResponseRegions = requests.get('https://api.themoviedb.org/3/watch/providers/regions?api_key='+config["tmdbApiKey"], headers=tmdbHeaders) 15 | tmdbRegions = tmdbResponseRegions.json() 16 | 17 | tmdbResponseProviders = requests.get('https://api.themoviedb.org/3/watch/providers/movie?api_key='+config["tmdbApiKey"], headers=tmdbHeaders) 18 | tmdbProviders = tmdbResponseProviders.json() 19 | allProviders = [] 20 | for p in tmdbProviders["results"]: 21 | allProviders.append(p["provider_name"]) 22 | providers = sorted(set(allProviders)) 23 | 24 | 25 | f = open("providers.txt", "a") 26 | f.write("Regions\n-------\n") 27 | for r in tmdbRegions["results"]: 28 | f.write(str(r["iso_3166_1"])+"\t"+str(r["english_name"])+"\n") 29 | f.write("\n\nProviders\n---------\n") 30 | for p in providers: 31 | f.write(str(p)+"\n") 32 | f.close() 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env/ 104 | venv/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | .vscode 128 | config.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **DISCLAIMER: This was thrown together by me late at night with limited python skills. Use it at your own risk. I provide zero warranty. If this nerfs your Radarr library I am really sorry, but theres nothing I can do.** 2 | 3 | # Elsewherr 4 | 5 | **What is it?** 6 | 7 | Elsewherr will see if your movies from Radarr are available on a streaming service, and add a tag against the movie if it is. 8 | 9 | **How does it work?** 10 | 11 | The script will check The Movie Database (https://www.themoviedb.org/) via their API, which in turn uses Just Watch (https://www.justwatch.com/), to get all streaming services each movie is on. If it matches one of your chosen list to monitor, it then adds this tag in Radarr. 12 | 13 | **Why?** 14 | 15 | Why not? What you do with this information is up to you. You might want to remove movies that are on Netflix to save space, or just like to know theres an option availble other than your local library. 16 | 17 | **How do I use it?** 18 | - Download, clone, or otherwise obtain this repo and put it somewhere 19 | - Run `python -m pip install -r requirements.txt` or `pip install -r requirements.txt` 20 | - Get an account at TMDb (https://www.themoviedb.org/) and grab an API key 21 | - Rename `config.yaml.example` to `config.yaml` 22 | - Edit `config.yaml` as per the table below 23 | - Run `python elsewherr.py`, or `run.bat` to run the script. 24 | 25 | You might want to setup a scheduled task or something to run this regularly to keep the list up to date as moves are added to or drop off streaming services. 26 | 27 | **Parameters** 28 | 29 | |Parameter|Description| 30 | |---|---| 31 | |tmdbApiKey|API Key for The Movie Database| 32 | |providerRegion|2 digit region code to use to check the availability of movies on that regions streaming service. The `providers.txt` file contains a list of codes| 33 | |radarrApiKey|Your API key for Radarr| 34 | |radarrUrl|Full URL including port to Radarr| 35 | |requiredProviders|List of the providers you would like to search for. Providers must be entered *exactly* as they appear in the Providers list from TMDb to work. | 36 | |tagPrefix|Prefix that will be included in the tags added to Radarr| 37 | 38 | A list of Regions and Providers is available in `providers.txt`, but you can also run the `providers.py` script to grab an up to date list. 39 | 40 | **Logging/Debugging** 41 | 42 | By default Elsewherr will log all INFO logs out to `elsewhere.log`. If you incluide the '-d' or '--debug' argument when running the script (i.e. `python elsewherr.py -d` or `python elsewherr.py --debug`) it will up the logging to DEBUG and output much more information to the logs. 43 | 44 | The log file is overwritten each time the script is run. 45 | 46 | **Note:** The prefix is important, its used to remove all tags before re-adding to catch movies being removed from services. If you don't use a prefix, this script will remove all your tags from your movies. You can change it from the default *elsewherr-*, just make sure its unique. 47 | 48 | 49 | -------------------------------------------------------------------------------- /elsewherr.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import re 3 | import time 4 | import yaml 5 | import logging 6 | import sys 7 | import argparse 8 | 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument( 11 | '-d', '--debug', 12 | help="Print lots of debugging statements", 13 | action="store_const", dest="loglevel", const=logging.DEBUG, 14 | default=logging.INFO, 15 | ) 16 | args = parser.parse_args() 17 | logging.basicConfig(level=args.loglevel, filename='elsewherr.log', filemode='w', format='%(asctime)s :: %(levelname)s :: %(message)s') 18 | 19 | logging.debug('DEBUG Logging Enabled') 20 | logging.debug('Loading Config and setting the list of required Providers') 21 | config = yaml.safe_load(open("config.yaml")) 22 | requiredProvidersLower = [re.sub('[^A-Za-z0-9]+', '', x).lower() for x in config["requiredProviders"]] 23 | logging.debug(f'requiredProvidersLower: {requiredProvidersLower}') 24 | 25 | # Request Headers 26 | radarrHeaders = {'Content-Type': 'application/json', "X-Api-Key":config["radarrApiKey"]} 27 | tmdbHeaders = {'Content-Type': 'application/json'} 28 | 29 | # Create all Tags for Providers 30 | logging.debug('Create all Tags for Providers within Radarr') 31 | for requiredProvider in config["requiredProviders"]: 32 | providerTag = (config["tagPrefix"] + re.sub('[^A-Za-z0-9]+', '', requiredProvider)).lower() 33 | newTagJson = { 34 | 'label': providerTag, 35 | 'id': 0 36 | } 37 | logging.debug(f'newTagJson: {newTagJson}') 38 | radarrTagsPost = requests.post(config["radarrUrl"]+'/api/v3/tag', json=newTagJson, headers=radarrHeaders) 39 | logging.debug(f'radarrTagsPost Response: {radarrTagsPost}') 40 | 41 | # Get all Tags and create lists of those to remove and add 42 | logging.debug('Get all Tags and create lists of those to remove and add') 43 | radarrTagsGet = requests.get(config["radarrUrl"]+'/api/v3/tag', headers=radarrHeaders) 44 | logging.debug(f'radarrTagsGet Response: {radarrTagsGet}') 45 | existingTags = radarrTagsGet.json() 46 | logging.debug(f'existingTags: {existingTags}') 47 | providerTagsToRemove = [] 48 | providerTagsToAdd = [] 49 | 50 | for existingTag in existingTags: 51 | if config["tagPrefix"].lower() in existingTag["label"]: 52 | logging.debug(f'Adding tag [{existingTag}] to the list of tags to be removed') 53 | providerTagsToRemove.append(existingTag) 54 | if str(existingTag["label"]).replace(config["tagPrefix"].lower(), '') in requiredProvidersLower: 55 | logging.debug(f'Adding tag [{existingTag}] to the list of tags to be added') 56 | providerTagsToAdd.append(existingTag) 57 | 58 | # Get all Movies from Radarr 59 | logging.debug('Getting all Movies from Radarr') 60 | radarrResponse = requests.get(config["radarrUrl"]+'/api/v3/movie', headers=radarrHeaders) 61 | logging.debug(f'radarrResponse Response: {radarrResponse}') 62 | movies = radarrResponse.json() 63 | logging.debug(f'Number of Movies: {len(movies)}') 64 | 65 | # Work on each movie 66 | logging.debug('Working on all movies in turn') 67 | for movie in movies: 68 | update = movie 69 | #time.sleep(1) 70 | logging.info("-------------------------------------------------------------------------------------------------") 71 | logging.info("Movie: "+movie["title"]) 72 | logging.info("TMDB ID: "+str(movie["tmdbId"])) 73 | logging.debug(f'Movie record from Radarr: {movie}') 74 | 75 | logging.debug("Getting the available providers for: "+movie["title"]) 76 | tmdbResponse = requests.get('https://api.themoviedb.org/3/movie/'+str(movie["tmdbId"])+'/watch/providers?api_key='+config["tmdbApiKey"], headers=tmdbHeaders) 77 | logging.debug(f'tmdbResponse Response: {tmdbResponse}') 78 | tmdbProviders = tmdbResponse.json() 79 | logging.debug(f'Total Providers: {len(tmdbProviders["results"])}') 80 | 81 | # Check that flatrate providers exist for the chosen region 82 | logging.debug("Check that flatrate providers exist for the chosen region") 83 | try: 84 | providers = tmdbProviders["results"][config["providerRegion"]]["flatrate"] 85 | logging.debug(f'Flat Rate Providers: {providers}') 86 | except KeyError: 87 | logging.info("No Flatrate Providers") 88 | continue 89 | 90 | # Remove all provider tags from movie 91 | logging.debug("Remove all provider tags from movie") 92 | updateTags = movie.get("tags", []) 93 | logging.debug(f'updateTags - Start: {updateTags}') 94 | for providerIdToRemove in (providerIdsToRemove["id"] for providerIdsToRemove in providerTagsToRemove): 95 | try: 96 | updateTags.remove(providerIdToRemove) 97 | logging.debug(f'Removing providerId: {providerIdToRemove}') 98 | except: 99 | continue 100 | 101 | # Add all required providers 102 | logging.debug("Adding all provider tags to movie") 103 | for provider in providers: 104 | providerName = provider["provider_name"] 105 | tagToAdd = (config["tagPrefix"] + re.sub('[^A-Za-z0-9]+', '', providerName)).lower() 106 | for providerTagToAdd in providerTagsToAdd: 107 | if tagToAdd in providerTagToAdd["label"]: 108 | logging.info("Adding tag "+tagToAdd) 109 | updateTags.append(providerTagToAdd["id"]) 110 | 111 | logging.debug(f'updateTags - End: {updateTags}') 112 | update["tags"] = updateTags 113 | logging.debug(f'Updated Movie record to send to Radarr: {update}') 114 | 115 | # Update movie in Radarr 116 | radarrUpdate = requests.put(config["radarrUrl"]+'/api/v3/movie', json=update, headers=radarrHeaders) 117 | logging.info(radarrUpdate) 118 | 119 | --------------------------------------------------------------------------------