├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── logs └── .keep ├── pingrr.py ├── pingrr ├── __init__.py ├── allflicks.py ├── config.py ├── justWatch.py ├── netflix.py ├── notifications.py ├── pushover.py ├── radarr.py ├── slack.py ├── sonarr.py └── trakt.py └── requirements.txt /.dockerignore: -------------------------------------------------------------------------------- 1 | # Git 2 | .git 3 | .gitignore 4 | 5 | # Logs 6 | logs/* 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Config 2 | *.json 3 | 4 | # Logs 5 | logs/*.log* 6 | 7 | # Python cache 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | *.pyc 12 | 13 | # Pyenv 14 | .python-version 15 | 16 | # Keep empty folders 17 | !.keep 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge 2 | 3 | # Directory where to deploy 4 | ENV APP_DIR=pingrr 5 | 6 | RUN \ 7 | # Upgrade all packages 8 | apk --no-cache -U upgrade && \ 9 | # Install OS dependencies 10 | apk --no-cache -U add python2 && \ 11 | apk --no-cache -U add --virtual .build-deps \ 12 | git \ 13 | gcc \ 14 | linux-headers \ 15 | python2-dev \ 16 | musl-dev \ 17 | libxml2-dev \ 18 | libxslt-dev \ 19 | && \ 20 | # Python2 PIP 21 | python -m ensurepip && \ 22 | # Get Pingrr 23 | git clone --depth 1 --single-branch https://github.com/Dec64/pingrr.git /${APP_DIR} && \ 24 | # Install PIP dependencies 25 | pip install --no-cache-dir --upgrade pip setuptools && \ 26 | pip install --no-cache-dir --upgrade -r /${APP_DIR}/requirements.txt && \ 27 | # Remove build dependencies 28 | apk --no-cache del .build-deps 29 | 30 | # Change directory 31 | WORKDIR /${APP_DIR} 32 | 33 | # Config volume 34 | VOLUME /config 35 | 36 | # Pingrr config file 37 | ENV PINGRR_CONFIG=/config/config.json 38 | # Pingrr log file 39 | ENV PINGRR_LOGFILE=/config/pingrr.log 40 | # Blacklist file 41 | ENV PINGRR_BLACKLIST=/config/blacklist.json 42 | 43 | # Entrypoint 44 | ENTRYPOINT ["python2", "pingrr.py"] 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Check out traktarr https://github.com/l3uddz/traktarr for a cool alternative if pingrr isn't for you 2 | 3 | ![alt text](http://img.pixady.com/2017/09/143837_pingrr.png) 4 | # pingrr. 5 | 6 | Python script that checks certain lists on [Trakt](http://trakt.tv) and [Netflix's](https://www.allflicks.net/) recently added shows/movies, as well as 7 | [JustWatch](https://www.justwatch.com/)'s recent shows/movies and if they meet your configured filters, adds them to your sonarr/radarr library. 8 | 9 | Currently supports: 10 | 1. [Trakt Trending](https://trakt.tv/shows/trending) 11 | 2. [Trakt Anticipated](https://trakt.tv/shows/anticipated) 12 | 3. [Trakt Popular](https://trakt.tv/shows/popular) 13 | 4. [Netflix recently added](https://www.allflicks.net/) # Currently not supported, use justwatch 14 | 5. [Just Watch recently added](https://www.justwatch.com/) 15 | 16 | ## Getting Started 17 | 18 | You will need a trakt.tv account with a [Trakt API key](https://trakt.tv/oauth/applications/new), 19 | as well as your Sonarr API. 20 | 21 | Quick warning when setting up the script. If you activate lots of lists with no filters, 22 | you will get a lot of shows that you may not want. 23 | At a minimum, it is recommended to use the language code filter and limit shows added per run. 24 | 25 | ### Prerequisites 26 | 27 | You will need a Trakt account and an API key (client ID), 28 | you can create a new API app in trakt [here](https://trakt.tv/oauth/applications/new) 29 | 30 | 1. Python 2.7 31 | 2. requests 32 | 3. BeautifulSoup 33 | 4. fuzzywuzzy 34 | 5. ImdbPY 35 | 36 | `sudo apt-get install python` 37 | 38 | `pip install -r requirements.txt` 39 | 40 | ### Installing 41 | 42 | `git clone https://github.com/Dec64/pingrr.git` 43 | 44 | `cd pingrr` 45 | 46 | `python pingrr.py` 47 | 48 | Follow the instructions in the console to complete the configuration file. 49 | This is only needed on the first run. Once complete you will need to re-run Pingrr. 50 | 51 | `python pingrr.py` 52 | 53 | If you intend to leave it running, it would be best to use systemd startup or screen. 54 | 55 | `screen -S pingrr python pingrr.py` 56 | 57 | Check it's all running fine by tailing the log 58 | 59 | `tail -f logs/pingrr.log` 60 | 61 | 62 | ### Command line arguments 63 | 64 | If you want to define a config location for pingrr (running multiple 65 | instances with different categories/folder locations/lists) you can define 66 | the config location via: 67 | 68 | - Command line by using `-c` or `--config=`: 69 | - `python pingrr.py -c /my/config/location/conf.json` 70 | - `python pingrr.py --config=/my/config/location/conf.json` 71 | 72 | 73 | - Environment variable by setting `PINGRR_CONFIG`: 74 | - `PINGRR_CONFIG=/my/config/location/conf.json python pingrr.py` 75 | 76 | Logging level can be increased if you are having issues and want more output in the log/console by using `--loglevel`: 77 | - `python pingrr.py --loglevel=DEBUG` 78 | 79 | To list all possible arguments and options run: 80 | - `python pingrr.py -h` 81 | 82 | ### Blacklist 83 | 84 | There is a blacklist.json file created on the first run. This file will 85 | have failed shows and any show you want to add to it outside of filters that 86 | you never want to add. To add a show, either use the shows IMDB id or TVDB id. 87 | 88 | ``` 89 | {"blacklist": ["imdb id", "or tvdb id", "332493", "75760", "79168", "334069"]} 90 | ``` 91 | 92 | ### Just Watch 93 | 94 | https://www.justwatch.com/ 95 | 96 | Early support for justwatch is included. 97 | 98 | ``` 99 | "just_watch": { 100 | "enabled": true, 101 | "country": "GB", 102 | "pages": "1" 103 | } 104 | ``` 105 | 106 | Each page is a single day, so if you set 1 page, it will check against 107 | all services justwatch supports in your chosen country for the last day. 108 | 109 | Set more pages to scan back more days, doing this will cause the scan to 110 | take longer and potentially add a lot of shows. 111 | 112 | Should support any two-letter country code that Just Watch has data for. 113 | 114 | E.g: 115 | ``` 116 | Germany 117 | Austria 118 | Switzerland 119 | United Kingdom 120 | Ireland 121 | Russia 122 | Italy 123 | France 124 | Spain 125 | Netherlands 126 | Norway 127 | Sweden 128 | Denmark 129 | Finland 130 | Lithuania 131 | Latvia 132 | Estonia 133 | USA 134 | Canada 135 | Mexico 136 | 137 | and more 138 | ``` 139 | 140 | ### Config 141 | 142 | There are a few config settings that are not set by the user on the first run 143 | if you fancy messing with more advanced settings you can open up the config.json 144 | file and change as needed. 145 | 146 | Please see below for an explanation for the current config 147 | 148 | ``` 149 | { 150 | "allflicks": { 151 | "enabled": { 152 | "movies": true, 153 | "shows": false 154 | }, 155 | "rating_match": 94 156 | }, 157 | "filters": { 158 | "allow_canceled": true, ## allow shows that are cancelled to be added 159 | "allow_ended": true, ## allow shows that are finished to be added 160 | "country": ["gb","us","ca"], ## What countrys shows/movies are allowed to be from (whitelist) 161 | "genre": [], ## What genres shows/movies are NOT allowed to be from (blacklist) 162 | "language": "en", ## What language is allowed for shows/movies (whitelist) 163 | "network": "YouTube", ## What networks to NOT allow (blacklist) 164 | "rating": 3, ## Min rating show/movie must have to be added 165 | "runtime": 0, ## Min runtime show/movie must have to be added 166 | "votes": 1, ## Min votes show/movie must have to be added 167 | "year": { 168 | "movies": 0, ## Min year movie must be to be added 169 | "shows": 0 ## Min year show must be to be added 170 | } 171 | }, 172 | "pingrr": { 173 | "limit": { 174 | "sonarr": 0, ## Amount of shows to be added per cycle 175 | "radarr": 0 ## Amount of movies to be added per cycle 176 | }, 177 | "log_level": "info", ## Log level for pingrr 178 | "timer": 1.00, ## How long to wait, in hours, untill next scan 179 | "aired": 10 ## If show has less episodes then this aired, do not count towards limit 180 | "dry_run": False ## Set to true to not add any shows, just test filter results 181 | }, 182 | "pushover": { 183 | "app_token": "", 184 | "enabled": true, 185 | "user_token": "" 186 | }, 187 | "slack": { 188 | "channel": "", 189 | "enabled": false, 190 | "sender_icon": ":robot_face:", 191 | "sender_name": "Pingrr", 192 | "webhook_url": "" 193 | }, 194 | "sonarr": { 195 | "api": "", 196 | "folder_path": "/mnt/media/TV/", ## Default root folder path shows will be sent to 197 | "quality_profile": 1, ## Quality profile ID to assign to show in sonarr 198 | "monitored": true, ## Add show monitored or not 199 | "search_missing_episodes": true, ## Search for missing episodes on add or not 200 | "genre_paths": true, ## Use genre paths, change folder path for show depending on catagory 201 | "path_root": "/mnt/media/", ## Root of the path. Only need if using genre paths 202 | "paths": { ## "Kids-TV": [children] == /mnt/media/Kids-TV for any show with genre "children". Add more as needed. Resolves in order. If show is Anime, will be sent to anime before Kids-TV in this example. 203 | "Anime": [ 204 | "anime" 205 | ], 206 | "Kids-TV": [ 207 | "children" 208 | ], 209 | "Travel-TV": [ 210 | "documentary" 211 | ], 212 | "Reality-TV": [ 213 | "reality", 214 | "game-show" 215 | ] 216 | } 217 | }, 218 | "radarr": { 219 | "api": "", 220 | "folder_path": "/mnt/media/Movies/", 221 | "host": "http://localhost:7878", 222 | "quality_profile": 1, 223 | "monitored": true, 224 | "genre_paths": true, 225 | "path_root": "/mnt/media/", 226 | "paths": { 227 | "Anime": [ 228 | "anime" 229 | ], 230 | "Kids-TV": [ 231 | "children" 232 | ], 233 | "Travel-TV": [ 234 | "documentary" 235 | ], 236 | "Reality-TV": [ 237 | "reality", 238 | "game-show" 239 | ] 240 | } 241 | }, 242 | "trakt": { 243 | "api": "", ## Client ID from trakt 244 | "imdb_info": false, ## Use imdb info for votes/genres/vote count if possible instead of trakt. WARNING, MUCH SLOWER THEN TRAKT. 245 | "limit": 2000, ## Max shows trakt will return. Use a higher number, e.g. 1000 to return a lot. 246 | "tv_list": { 247 | "anticipated": false, ## Enable trakt tv anticipated 248 | "popular": false, ## Enable trakt tv popular 249 | "trending": false ## Enable trakt tv trending 250 | }, 251 | "movie_list": { 252 | "anticipated": true, ## Enable trakt movie anticipated 253 | "popular": true, ## Enable trakt movie popular 254 | "trending": true ## Enable trakt movie trending 255 | } 256 | }, 257 | "just_watch": { 258 | "enabled": { 259 | "movies": true, ## Enable justwatch for movies 260 | "shows": false ## Enable justwatch for shows 261 | }, 262 | "country": "GB", ## What country to get justwatch info based on, e.g. GB or US 263 | "pages": "20" ## How many pages to get info from, 1 page = 1 day. Add more pages to add more content. Will be slower with more pages 264 | } 265 | } 266 | ``` 267 | 268 | ##### `"rating_match":94` 269 | How close the match has to be out of 100 when parsing Netflix titles. 270 | If you feel you are missing some titles feel free to play with this figure, but anything lower 271 | then 90 will most likely result in incorrect matches. 272 | 273 | For the unwanted genres list, you need to type them as found on IMDB, here are some examples: 274 | 275 | ``` 276 | comedy 277 | action 278 | drama 279 | fantasy 280 | science-fiction 281 | adventure 282 | reality 283 | superhero 284 | crime 285 | mystery 286 | horror 287 | thriller 288 | romance 289 | animation 290 | children 291 | home-and-garden 292 | anime 293 | family 294 | documentary 295 | game-show 296 | suspense 297 | ``` 298 | 299 | ### Start on boot 300 | 301 | To have pingrr start on boot on Ubuntu 302 | 303 | `cd /opt` 304 | 305 | `git clone https://github.com/Dec64/pingrr.git` 306 | 307 | run pingrr, and complete the config. 308 | 309 | `sudo nano /etc/systemd/system/pingrr.service` 310 | 311 | copy and paste the below, after editing `User=` and `Group=` 312 | 313 | ``` 314 | [Unit] 315 | Description=pingrr 316 | After=network-online.target 317 | 318 | [Service] 319 | User=YOUR_USER 320 | Group=YOUR_USER 321 | Type=simple 322 | WorkingDirectory=/opt/pingrr/ 323 | ExecStart=/usr/bin/python /opt/pingrr/pingrr.py 324 | Restart=always 325 | RestartSec=10 326 | 327 | [Install] 328 | WantedBy=default.target 329 | ``` 330 | 331 | Then enabled and start it with: 332 | 333 | `sudo systemctl enable pingrr.service` 334 | 335 | `sudo systemctl start pingrr` 336 | 337 | *** 338 | 339 | If you are have set pingrr to not loop (setting the timer to 0) then you 340 | must remove `Restart=always RestartSec=10` from systemd file, otherwise, it will restart every 10 seconds. 341 | 342 | ### Todo 343 | 344 | 1. Learn python 345 | 346 | ### Thanks 347 | 348 | Thanks to horjulf and l3uddz for assisting me with a few bits so far, 349 | and everyone that helped test it along the way. 350 | 351 | If you have any suggestions for features ect feel free to create a new issue 352 | on GitHub for it! 353 | 354 | ### Donate 355 | 356 | If you absolutely feel the need to donate, the link is below. 357 | Pull requests and help cleaning up my amateur python would be more helpful :) 358 | 359 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.me/Dec64) 360 | -------------------------------------------------------------------------------- /logs/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dec64/pingrr/5cf00c44dfd770a9213b392027e6ef8efb4d71d6/logs/.keep -------------------------------------------------------------------------------- /pingrr.py: -------------------------------------------------------------------------------- 1 | import pingrr.config as config 2 | 3 | import json 4 | import logging 5 | from logging.handlers import RotatingFileHandler 6 | import sys 7 | import requests 8 | 9 | from time import sleep 10 | #from imdb import IMDb 11 | 12 | #i = IMDb() 13 | 14 | ################################ 15 | # Logging 16 | ################################ 17 | 18 | # Logging format 19 | formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 20 | 21 | # root logger 22 | logger = logging.getLogger() 23 | # Set initial level to INFO 24 | logger.setLevel(logging.DEBUG) 25 | 26 | # Console handler, log to stdout 27 | consoleHandler = logging.StreamHandler(sys.stdout) 28 | consoleHandler.setFormatter(formatter) 29 | logger.addHandler(consoleHandler) 30 | 31 | # Other modules logging levels 32 | logging.getLogger("requests").setLevel(logging.WARNING) 33 | 34 | ################################ 35 | # Load config 36 | ################################ 37 | 38 | # Load initial config 39 | configuration = config.Config() 40 | 41 | # Set configured log level 42 | logger.setLevel(configuration.settings['loglevel']) 43 | 44 | # Load config file 45 | configuration.load() 46 | conf = configuration.config 47 | 48 | # Print config file to log on run 49 | logger.info(json.dumps(conf, sort_keys=True, indent=4, separators=(',', ': '))) 50 | 51 | # Log file handler 52 | fileHandler = RotatingFileHandler(configuration.settings['logfile'], maxBytes=1024 * 1024 * 2, backupCount=1) 53 | fileHandler.setFormatter(formatter) 54 | logger.addHandler(fileHandler) 55 | 56 | ################################ 57 | # Init 58 | ################################ 59 | 60 | import pingrr.trakt as trakt 61 | import pingrr.sonarr as sonarr 62 | 63 | import pingrr.justWatch as justWatch 64 | import pingrr.radarr as radarr 65 | from pingrr.notifications import Notifications 66 | 67 | new = [] 68 | delay_time = conf['pingrr']['timer'] * 3600 69 | options = {"ignoreEpisodesWithFiles": False, "ignoreEpisodesWithoutFiles": False, 70 | "searchForMissingEpisodes": conf['sonarr']['search_missing_episodes']} 71 | notify = Notifications() 72 | 73 | if conf['pushover']['enabled']: 74 | notify.load(service="pushover", app_token=conf['pushover']['app_token'], user_token=conf['pushover']['user_token']) 75 | 76 | if conf['slack']['enabled']: 77 | notify.load(service="slack", webhook_url=conf['slack']['webhook_url'], sender_name=conf['slack']['sender_name'], 78 | sender_icon=conf['slack']['sender_icon'], channel=conf['slack']['channel']) 79 | 80 | 81 | ################################ 82 | # Main 83 | ################################ 84 | 85 | 86 | def create_path(genres, program): 87 | """Create path based on genre for sonarr/radarr""" 88 | 89 | # Set root folder for path creation 90 | root_folder = conf[program]['path_root'] 91 | 92 | # Check if any of the genres match up, only if genre paths are in config 93 | if 'paths' in conf[program]: 94 | for key in conf[program]['paths']: 95 | for genre in conf[program]['paths'][key]: 96 | if genre in genres: 97 | return root_folder + key + '/' 98 | 99 | # If no match, return default path 100 | return conf[program]['folder_path'] 101 | 102 | 103 | def send_to_sonarr(a, b, genres): 104 | """Send found tv program to sonarr""" 105 | 106 | logger.info("Attempting to send to sonarr") 107 | 108 | path = create_path(genres, "sonarr") 109 | 110 | payload = {"tvdbId": a, "title": b, "qualityProfileId": conf['sonarr']['quality_profile'], "images": [], 111 | "seasons": [], "seasonFolder": True, "monitored": conf['sonarr']['monitored'], "rootFolderPath": path, 112 | "addOptions": options, } 113 | 114 | if conf['pingrr']['dry_run']: 115 | logger.info("dry run is on, not sending to sonarr") 116 | return True 117 | 118 | r = requests.post(sonarr.url + '/api/series', headers=sonarr.headers, data=json.dumps(payload), timeout=30) 119 | 120 | if r.status_code == 201: 121 | logger.debug("sent to sonarr successfully") 122 | return True 123 | 124 | else: 125 | logger.debug("failed to send to sonarr, code return: %r", r.status_code) 126 | return False 127 | 128 | 129 | def send_to_radarr(a, b, genres, year): 130 | """Send found tv program to radarr""" 131 | 132 | logger.info("Attempting to send to radarr") 133 | 134 | path = create_path(genres, "radarr") 135 | 136 | payload = {"tmdbId": a, 137 | "title": b, 138 | "qualityProfileId": conf['radarr']['quality_profile'], 139 | "images": [], 140 | "monitored": conf['radarr']['monitored'], 141 | "titleSlug": b, 142 | "rootFolderPath": path, 143 | "minimumAvailability": "preDB", 144 | "year": year 145 | } 146 | 147 | if conf['pingrr']['dry_run']: 148 | logger.info("dry run is on, not sending to radarr") 149 | return True 150 | 151 | r = requests.post(radarr.url + '/api/movie', headers=radarr.headers, data=json.dumps(payload), timeout=30) 152 | 153 | response = r.json() 154 | 155 | if r.status_code == 201: 156 | radarr.search_movie(response['id']) 157 | logger.debug("sent to radarr successfully") 158 | return True 159 | 160 | else: 161 | logger.debug("failed to send to radarr, code return: %r", r.status_code) 162 | return False 163 | 164 | 165 | def add_media(program): 166 | added_list = [] 167 | n = 0 168 | 169 | limit = conf['pingrr']['limit'][program] 170 | 171 | for media in new: 172 | title = media['title'] 173 | 174 | if program == "radarr": 175 | media_id = media['tmdb'] 176 | elif program == "sonarr": 177 | media_id = media['tvdb'] 178 | 179 | try: 180 | logger.debug('Sending media to {}: {}'.format(program, media['title'].encode('utf8'))) 181 | 182 | if program == "sonarr": 183 | if send_to_sonarr(media_id, title, media['genres']): 184 | logger.info('{} has been added to Sonarr'.format(title.encode('utf8'))) 185 | added_list.append(media['title']) 186 | 187 | if media['aired'] >= conf['pingrr']['aired']: 188 | n += 1 189 | else: 190 | logger.info( 191 | "{} only has {} episodes, does not count towards add limit".format(media['title'].encode('utf8'), 192 | media['aired'])) 193 | if 0 < limit == n: 194 | logger.info('{} shows added limit reached'.format(str(n))) 195 | break 196 | 197 | elif limit > 0 and not n == limit: 198 | logger.debug('limit not yet reached: {}'.format(str(n))) 199 | 200 | else: 201 | configuration.blacklist.add(str(media["tvdb"])) 202 | logger.warning('{} failed to be added to Sonarr! Adding to blacklist'.format(title.encode('utf8'))) 203 | 204 | if program == "radarr": 205 | if send_to_radarr(media_id, title, media['genres'], media['year']): 206 | logger.info('{} has been added to Radarr'.format(title.encode('utf8'))) 207 | added_list.append(media['title']) 208 | n += 1 209 | 210 | if 0 < limit == n: 211 | logger.info('{} shows added limit reached'.format(str(n))) 212 | break 213 | 214 | elif limit > 0 and not n == limit: 215 | logger.debug('limit not yet reached: {}'.format(str(n))) 216 | 217 | else: 218 | configuration.blacklist.add(str(media["tmdb"])) 219 | logger.warning('{} failed to be added to Radarr! Adding to blacklist'.format(title.encode('utf8'))) 220 | 221 | except IOError: 222 | logger.warning('error sending media: {} id: {}'.format(title.encode('utf8'), str(media_id))) 223 | 224 | if conf['pushover']['enabled'] or conf['slack']['enabled'] and n != 0: 225 | message = "The following {} item(s) out of {} added to {}:\n{}".format(str(len(added_list)), str(len(new)), program, 226 | "\n".join(added_list)) 227 | logger.warning(message) 228 | notify.send(message=message) 229 | 230 | 231 | def new_check(item_type): 232 | """Check for new trakt items in list""" 233 | if item_type == "movies": 234 | library = radarr.get_library() 235 | program = "radarr" 236 | else: 237 | library = sonarr.get_library() 238 | program = "sonarr" 239 | 240 | global new 241 | 242 | new = filter_list(item_type) 243 | logger.info('checking for new {} in lists'.format(item_type)) 244 | 245 | if item_type == "movies": 246 | item_id = "imdb" 247 | else: 248 | item_id = "tvdb" 249 | 250 | for x in new: 251 | logger.debug('checking {} from list: {}'.format(item_type, x['title'].encode('utf8'))) 252 | if x[item_id] not in library and conf['filters']['allow_ended']: 253 | logger.info('new media found, adding {} {} now'.format(len(new), item_type)) 254 | add_media(program) 255 | break 256 | 257 | if item_type == "shows": 258 | if x[item_id] not in library and not x['status'] == 'ended': 259 | logger.info('new continuing show(s) found, adding shows now') 260 | add_media(program) 261 | break 262 | 263 | 264 | def check_lists(arg, arg2): 265 | for filters in conf['filters'][arg]: 266 | for data in arg2: 267 | if filters == data: 268 | return True 269 | return False 270 | 271 | 272 | def filter_check(title, item_type): 273 | 274 | if item_type == "shows": 275 | if len(title['country']): 276 | country = title['country'].lower() 277 | else: 278 | country = False 279 | type_id = "tvdb" 280 | library = sonarr_library 281 | elif item_type == "movies": 282 | type_id = "tmdb" 283 | library = radarr_library 284 | country = False 285 | else: 286 | return False 287 | 288 | lang = title['language'] 289 | 290 | if title[type_id] not in library: 291 | if str(title['imdb']) in configuration.blacklist or str(title[type_id]) in configuration.blacklist: 292 | logger.info("{} was rejected as it was found in the blacklist".format(title['title'].encode('utf8'))) 293 | return False 294 | logger.debug("Checking year: {}".format(title['year'])) 295 | if conf['filters']['year'][item_type] > title['year']: 296 | logger.info("{} was rejected as it was outside allowed year range: {}".format(title['title'].encode('utf8'), 297 | str(title['year']))) 298 | return False 299 | 300 | logger.debug("Checking runtime: {}".format(title['runtime'])) 301 | if conf['filters']['runtime'] > title['runtime']: 302 | logger.info("{} was rejected as it was outside allowed runtime: {}".format(title['title'].encode('utf8'), 303 | str(title['runtime']))) 304 | return False 305 | 306 | if item_type == "shows": 307 | if len(conf['filters']['network']) > 0: 308 | if title['network'] is None or conf['filters']['network'] in title['network']: 309 | logger.info("{} was rejected as it was by a disallowed network: {}" 310 | .format(title['title'].encode('utf8'), str(title['network']).encode('utf8'))) 311 | return False 312 | logger.debug("Checking votes: {}".format(title['votes'])) 313 | if conf['filters']['votes'] > title['votes']: 314 | logger.info( 315 | "{} was rejected as it did not meet vote requirement: {}".format(title['title'].encode('utf8'), 316 | str(title['votes']))) 317 | return False 318 | 319 | if conf['filters']['allow_ended'] is False and 'ended' in title['status']: 320 | logger.info("{} was rejected as it is an ended tv series".format(title['title'].encode('utf8'))) 321 | return False 322 | 323 | if item_type == "shows": 324 | if conf['filters']['allow_canceled'] is False and 'canceled' in title['status']: 325 | logger.info("{} was rejected as it an canceled tv show".format(title['title'].encode('utf8'))) 326 | return False 327 | 328 | logger.debug("Checking rating: {}".format(title['rating'])) 329 | if float(title['rating']) < float(conf['filters']['rating']): 330 | logger.info( 331 | "{} was rejected as it was outside the allowed ratings: {}".format(title['title'].encode('utf8'), 332 | str(title['rating']))) 333 | return False 334 | 335 | logger.debug("Checking genres: {}".format(title['genres'])) 336 | if isinstance(conf['filters']['genre'], list): 337 | if check_lists('genre', title['genres']): 338 | logger.info("{} was rejected as it wasn't a wanted genre: {}".format(title['title'].encode('utf8'), 339 | str(title['genres']))) 340 | return False 341 | 342 | elif conf['filters']['genre'] == title['genres']: 343 | logger.info("{} was rejected as it wasn't a wanted genre: {}".format(title['title'].encode('utf8'), 344 | str(title['genres']))) 345 | return False 346 | 347 | logger.debug("Checking country: {}".format(country)) 348 | if country and country not in conf['filters']['country']: 349 | logger.info("{} was rejected as it wasn't a wanted country: {}".format(title['title'].encode('utf8'), 350 | str(title['country']))) 351 | return False 352 | logger.debug("Checking language: {}".format(lang)) 353 | if lang not in conf['filters']['language']: 354 | logger.info("{} was rejected as it wasn't a wanted language: {}".format(title['title'].encode('utf8'), lang)) 355 | return False 356 | return True 357 | 358 | else: 359 | logger.info("{} was rejected as it is already in {} library".format(title['title'].encode('utf8'), item_type)) 360 | 361 | 362 | def filter_list(list_type): 363 | # Create the lists ready to be filtered down 364 | if list_type == 'shows': 365 | raw_list = [] 366 | item_id = "tvdb" 367 | for trakt_list in conf['trakt']['tv_list']: 368 | if conf['trakt']['tv_list'][trakt_list]: 369 | raw_list = trakt.get_info('tv') 370 | break 371 | # if conf['allflicks']['enabled']['shows']: 372 | # raw_list += allflicks.create_list() 373 | if conf['just_watch']['enabled']['shows']: 374 | raw_list += justWatch.create_list("shows") 375 | if list_type == 'movies': 376 | item_id = "tmdb" 377 | raw_list = [] 378 | for trakt_list in conf['trakt']['movie_list']: 379 | if conf['trakt']['movie_list'][trakt_list]: 380 | raw_list = trakt.get_info('movie') 381 | break 382 | if conf['just_watch']['enabled']['movies']: 383 | raw_list += justWatch.create_list("movies") 384 | fixed_raw = [] 385 | for raw in raw_list: 386 | try: 387 | fixed_raw.append(raw[0]) 388 | except KeyError: 389 | fixed_raw.append(raw) 390 | raw_list = fixed_raw 391 | 392 | filtered = [] 393 | 394 | for title in raw_list: 395 | try: 396 | # If not already in the list, check against filters 397 | if filter_check(title, list_type) and title[item_id] not in filtered: 398 | logger.info('adding {} to potential add list'.format(title['title'].encode('utf8'))) 399 | filtered.append(title) 400 | except TypeError: 401 | logger.debug('{} failed to check against filters'.format(title['title'].encode('utf8'))) 402 | logger.debug("Filtered list successfully") 403 | 404 | return filtered 405 | 406 | if __name__ == "__main__": 407 | while True: 408 | 409 | logger.info("\n\n###### Checking if TV lists are wanted ######\n") 410 | 411 | if conf['sonarr']['api']: 412 | try: 413 | sonarr_library = sonarr.get_library() 414 | new_check('shows') 415 | except requests.exceptions.ReadTimeout: 416 | logger.warning("Sonarr library timed out, skipping for now") 417 | except requests.exceptions.ConnectionError: 418 | logger.warning("Can not connect to Sonarr, check sonarr is running or host is correct") 419 | 420 | logger.info("\n\n###### Checking if Movie lists are wanted ######\n") 421 | 422 | if conf['radarr']['api']: 423 | try: 424 | radarr_library = radarr.get_library() 425 | new_check('movies') 426 | except requests.exceptions.ReadTimeout: 427 | logger.warning("Radarr library timed out, skipping for now") 428 | except requests.exceptions.ConnectionError: 429 | logger.warning("Can not connect to Radarr, check Radarr is running or host is correct") 430 | 431 | # Save updated blacklist 432 | configuration.save_blacklist() 433 | 434 | if conf['pingrr']['timer'] == 0: 435 | logger.info('Scan finished, shutting down') 436 | sys.exit() 437 | 438 | if conf['pingrr']['timer'] > 1: 439 | hours = "s" 440 | else: 441 | hours = "" 442 | 443 | logger.info("check finish, sleeping for {} hour{}".format(conf['pingrr']['timer'], hours)) 444 | sleep(float(delay_time)) 445 | logger.debug('sleep over, checking again') 446 | -------------------------------------------------------------------------------- /pingrr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dec64/pingrr/5cf00c44dfd770a9213b392027e6ef8efb4d71d6/pingrr/__init__.py -------------------------------------------------------------------------------- /pingrr/allflicks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import requests 4 | import re 5 | import trakt 6 | import urllib 7 | from bs4 import BeautifulSoup 8 | from fuzzywuzzy import fuzz 9 | 10 | ################################ 11 | # Load config 12 | ################################ 13 | 14 | conf = config.Config().config 15 | 16 | ################################ 17 | # Logging 18 | ################################ 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | ################################ 23 | # Init 24 | ################################ 25 | 26 | headers = { 27 | 'content-type': 'application/json', 28 | 'trakt-api-version': '2', 29 | 'trakt-api-key': conf['trakt']['api'] 30 | } 31 | 32 | ################################ 33 | # get cookie info 34 | ################################ 35 | 36 | 37 | def get_ident(): 38 | page = requests.get("https://www.allflicks.net") 39 | data = page.content 40 | pattern = re.compile(r'identifier=(\w*)', re.MULTILINE | re.DOTALL) 41 | soup = BeautifulSoup(data, "html.parser") 42 | script = soup.find("script", text=pattern) 43 | if script: 44 | match = pattern.search(script.text) 45 | if match: 46 | ident = match.group(1) 47 | return ident 48 | 49 | 50 | ################################ 51 | # Main 52 | ################################ 53 | 54 | 55 | def format_string(text_input): 56 | string = text_input 57 | pattern = re.compile('\W') 58 | string = re.sub(pattern, '', string) 59 | return string 60 | 61 | 62 | def get_info_search(tv_id): 63 | """Get info for a tv show""" 64 | url = "https://api.trakt.tv/search/show?query=" + urllib.quote_plus(tv_id) + "&extended=full" 65 | logger.debug('getting info from trakt for ' + tv_id) 66 | r = requests.get(url=url, headers=headers, timeout=10) 67 | if r.status_code == requests.codes.ok: 68 | x = [] 69 | y = r.json() 70 | y = y[0]['show'] 71 | x.append({'title': y['title'], 72 | 'status': y['status'], 73 | 'tvdb': y['ids']['tvdb'], 74 | 'imdb': y['ids']['imdb'], 75 | 'trakt': y['ids']['trakt'], 76 | 'rating': y['rating'], 77 | 'language': y['language'], 78 | 'genres': y['genres'], 79 | 'year': y['year'] 80 | }) 81 | logger.debug('got tv show info successfully') 82 | return x 83 | else: 84 | logger.debug('failed to get trakt show info, code return: ' + str(r.status_code)) 85 | return False 86 | 87 | 88 | def create_list(): 89 | with requests.session() as req: 90 | head = {'Accept-Language': 'en-US,en;q=0.8,pt-PT;q=0.6,pt;q=0.4', 91 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36' 92 | '(KHTML, like Gecko) Chrome/61.0.3163.59 Safari/537.36', 'Connection': 'keep-alive', 93 | 'DNT': '1', 94 | 'cookie': 'identifier=' + get_ident() 95 | } 96 | resp = req.get("https://www.allflicks.net", headers=head) 97 | if resp.status_code == 200: 98 | logger.debug("sending payload to processing_us.php") 99 | params = { 100 | "draw": 1, 101 | "columns[0][data]": "box_art", 102 | "columns[0][name]": "", 103 | "columns[0][searchable]": "true", 104 | "columns[0][orderable]": "false", 105 | "columns[0][search][value]": "", 106 | "columns[0][search][regex]": "false", 107 | "columns[1][data]": "title", 108 | "columns[1][name]": "", 109 | "columns[1][searchable]": "true", 110 | "columns[1][orderable]": "true", 111 | "columns[1][search][value]": "", 112 | "columns[1][search][regex]": "false", 113 | "columns[2][data]": "year", 114 | "columns[2][name]": "", 115 | "columns[2][searchable]": "true", 116 | "columns[2][orderable]": "true", 117 | "columns[2][search][value]": "", 118 | "columns[2][search][regex]": "false", 119 | "columns[3][data]": "genre", 120 | "columns[3][name]": "", 121 | "columns[3][searchable]": "true", 122 | "columns[3][orderable]": "false", 123 | "columns[3][search][value]": "", 124 | "columns[3][search][regex]": "false", 125 | "columns[4][data]": "rating", 126 | "columns[4][name]": "", 127 | "columns[4][searchable]": "true", 128 | "columns[4][orderable]": "true", 129 | "columns[4][search][value]": "", 130 | "columns[4][search][regex]": "false", 131 | "columns[5][data]": "available", 132 | "columns[5][name]": "", 133 | "columns[5][searchable]": "true", 134 | "columns[5][orderable]": "true", 135 | "columns[5][search][value]": "", 136 | "columns[5][search][regex]": "false", 137 | "columns[6][data]": "director", 138 | "columns[6][name]": "", 139 | "columns[6][searchable]": "true", 140 | "columns[6][orderable]": "true", 141 | "columns[6][search][value]": "", 142 | "columns[6][search][regex]": "false", 143 | "columns[7][data]": "cast", 144 | "columns[7][name]": "", 145 | "columns[7][searchable]": "true", 146 | "columns[7][orderable]": "true", 147 | "columns[7][search][value]": "", 148 | "columns[7][search][regex]": "false", 149 | "order[0][column]": 5, 150 | "order[0][dir]": "desc", 151 | "start": 0, 152 | "length": 100, 153 | "search[value]": "", 154 | "search[regex]": "false", 155 | "movies": "false", 156 | "shows": "true", 157 | "documentaries": "false", 158 | "min": 1900, 159 | "max": 2017 160 | } 161 | 162 | headers2 = { 163 | 'Origin': 'https://www.allflicks.net', 164 | 'Accept-Encoding': 'gzip, deflate, br', 165 | 'Accept-Language': 'en-US,en;q=0.8,pt-PT;q=0.6,pt;q=0.4', 166 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36' 167 | '(KHTML, like Gecko) Chrome/61.0.3163.59 Safari/537.36', 168 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 169 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 170 | 'Referer': 'https://www.allflicks.net/genre/tv-shows/', 'X-Requested-With': 'XMLHttpRequest', 171 | 'Connection': 'keep-alive', 172 | 'DNT': '1', 173 | 'cookie': 'identifier=' + get_ident() 174 | } 175 | 176 | resp = req.post( 177 | "https://www.allflicks.net/wp-content/themes/responsive/processing/processing_us.php", 178 | data=params, 179 | headers=headers2 180 | ) 181 | 182 | if resp.status_code == 200: 183 | logger.info('creating list from allflicks.net') 184 | shows = resp.json() 185 | y = [] 186 | for tv in shows['data']: 187 | try: 188 | x = trakt.search(tv['title'], "show", "search") 189 | title1 = x[0]['title'] 190 | title2 = tv['title'] 191 | s1 = fuzz.token_sort_ratio(title1, title1) 192 | s2 = fuzz.partial_ratio(title1, title2) 193 | average = (s1 + s2) / 2 194 | if title1 == title2: 195 | y.append(x) 196 | logger.debug("match found for: " + x[0]['title'] + " / " + tv['title']) 197 | continue 198 | if format_string(title1) == format_string(title2): 199 | y.append(x) 200 | logger.debug("match found for: " + x[0]['title'] + " / " + tv['title']) 201 | continue 202 | elif x[0]['year'] == tv['year']: 203 | average += 10 204 | if average >= conf['allflicks']['rating_match']: 205 | y.append(x) 206 | logger.debug("match found for: " + x[0]['title'] + " / " + tv['title']) 207 | continue 208 | else: 209 | logger.debug("no match found for: " + x[0]['title'] + " / " + tv['title']) 210 | except Exception: 211 | logger.debug("no match on trakt for title: " + tv['title']) 212 | logger.info('Allflicks list created, ' + str(len(y)) + ' shows found') 213 | return y 214 | -------------------------------------------------------------------------------- /pingrr/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import argparse 3 | import sys 4 | import os 5 | import json 6 | import requests 7 | 8 | ################################ 9 | # Logging 10 | ################################ 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | ################################ 15 | # Config class 16 | ################################ 17 | 18 | 19 | class Singleton(type): 20 | _instances = {} 21 | 22 | def __call__(cls, *args, **kwargs): 23 | if cls not in cls._instances: 24 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) 25 | 26 | return cls._instances[cls] 27 | 28 | 29 | class Config(object): 30 | __metaclass__ = Singleton 31 | 32 | base_settings = { 33 | 'config': { 34 | 'argv': '--config', 35 | 'env': 'PINGRR_CONFIG', 36 | 'default': os.path.join(os.path.dirname(sys.argv[0]), 'config.json') 37 | }, 38 | 'logfile': { 39 | 'argv': '--logfile', 40 | 'env': 'PINGRR_LOGFILE', 41 | 'default': os.path.join(os.path.dirname(sys.argv[0]), 'logs', 'pingrr.log') 42 | }, 43 | 'loglevel': { 44 | 'argv': '--loglevel', 45 | 'env': 'PINGRR_LOGLEVEL', 46 | 'default': 'INFO' 47 | }, 48 | 'blacklist': { 49 | 'argv': '--blacklist', 50 | 'env': 'PINGRR_BLACKLIST', 51 | 'default': os.path.join(os.path.dirname(sys.argv[0]), 'blacklist.json') 52 | } 53 | } 54 | 55 | def __init__(self): 56 | """Initializes config""" 57 | # Args and settings 58 | self.args = self.parse_args() 59 | self.settings = self.get_settings() 60 | # Config 61 | self.config = None 62 | self.blacklist = set() 63 | 64 | # Parse command line arguments 65 | def parse_args(self): 66 | parser = argparse.ArgumentParser( 67 | description=( 68 | "Checks lists on Trakt and Netflix's recently added shows, and if they \n" 69 | "meet your configured filters, adds them to your sonarr library." 70 | ), 71 | formatter_class=argparse.RawTextHelpFormatter 72 | ) 73 | 74 | # Config file 75 | parser.add_argument( 76 | self.base_settings['config']['argv'], 77 | '-c', 78 | nargs='?', 79 | const=None, 80 | help='Config file location (default: %s)' % self.base_settings['config']['default'] 81 | ) 82 | 83 | # Log file 84 | parser.add_argument( 85 | self.base_settings['logfile']['argv'], 86 | nargs='?', 87 | const=None, 88 | help='Log to the file (default: %s)' % self.base_settings['logfile']['default'] 89 | ) 90 | 91 | # Logging level 92 | parser.add_argument( 93 | self.base_settings['loglevel']['argv'], 94 | choices=('WARN', 'INFO', 'DEBUG'), 95 | help='Log level (default: %s)' % self.base_settings['loglevel']['default'] 96 | ) 97 | 98 | # Blacklist file 99 | parser.add_argument( 100 | self.base_settings['blacklist']['argv'], 101 | nargs='?', 102 | const=None, 103 | help='Blacklist file (default: %s)' % self.base_settings['blacklist']['default'] 104 | ) 105 | 106 | return vars(parser.parse_args()) 107 | 108 | def get_settings(self): 109 | setts = {} 110 | for name, data in self.base_settings.items(): 111 | # Argrument priority: cmd < environment < default 112 | try: 113 | value = None 114 | # Command line argument 115 | if self.args[name]: 116 | value = self.args[name] 117 | logger.info("Using ARG setting %s=%s", name, value) 118 | 119 | # Envirnoment variable 120 | elif data['env'] in os.environ: 121 | value = os.environ[data['env']] 122 | logger.info("Using ENV setting %s=%s" % ( 123 | data['env'], 124 | value 125 | )) 126 | 127 | # Default 128 | else: 129 | value = data['default'] 130 | logger.info("Using default setting %s=%s" % ( 131 | data['argv'], 132 | value 133 | )) 134 | 135 | setts[name] = value 136 | 137 | except Exception: 138 | logger.exception("Exception retrieving setting value: %r" % name) 139 | 140 | return setts 141 | 142 | def save_blacklist(self): 143 | with open(self.settings['blacklist'], 'w') as data_file: 144 | json.dump( 145 | {"blacklist": list(self.blacklist)}, 146 | data_file 147 | ) 148 | 149 | def load_blacklist(self): 150 | try: 151 | with open(self.settings['blacklist']) as data_file: 152 | self.blacklist = set(json.load(data_file)['blacklist']) 153 | 154 | except IOError: 155 | logger.info("No blacklist file, creating a blank file now.") 156 | self.save_blacklist() 157 | 158 | except (TypeError, IndexError, ValueError): 159 | logger.warning("Blacklist file contains invalid syntax, please check.") 160 | sys.exit(1) 161 | 162 | def load(self): 163 | if os.path.exists(self.settings['config']): 164 | with open(self.settings['config'], 'r') as f: 165 | self.config = json.load(f) 166 | 167 | else: 168 | logger.warn("No config file found, creating new config.") 169 | self.create_config(self.settings['config']) 170 | 171 | # Load blacklist 172 | self.load_blacklist() 173 | 174 | ################################ 175 | # Create config 176 | ################################ 177 | 178 | @staticmethod 179 | def ask_year(low, high): 180 | while True: 181 | try: 182 | number = int(raw_input("What is the minimum year to grab a tv show from? (0 for all ): \n")) 183 | except ValueError: 184 | continue 185 | if low <= number <= high: 186 | return number 187 | 188 | @staticmethod 189 | def ask_rating(): 190 | while True: 191 | try: 192 | number = int(raw_input("Enter the minimum rating a show must have to be added (0-10) \n")) 193 | except ValueError: 194 | continue 195 | if 0 <= number <= 10: 196 | return number 197 | 198 | @staticmethod 199 | def genre_list(): 200 | string_input = raw_input("Enter which genres you do NOT want to grab(Enter to skip/allow all): \n") 201 | string_input = string_input.lower() 202 | created_list = string_input.split() 203 | return created_list 204 | 205 | @staticmethod 206 | def get_quality_profiles(sonarr_url, key): 207 | url = sonarr_url 208 | r = requests.get(url + '/api/profile', headers={'X-Api-Key': key}, timeout=30) 209 | data = r.json() 210 | for profile in data: 211 | if profile['name']: 212 | print("{}: {}".format(profile['id'], profile['name'])) 213 | wanted = '' 214 | while wanted is not int and len(data) < wanted: 215 | try: 216 | wanted = int(raw_input("Which quality profile do you want to download shows with?: \n")) 217 | except ValueError: 218 | print("Please enter the ID of your profile you want") 219 | continue 220 | return wanted 221 | 222 | @staticmethod 223 | def check_api(sonarr_url, api_key): 224 | url = sonarr_url 225 | r = requests.get(url + '/api/system/status', headers={'X-Api-Key': api_key}, timeout=30) 226 | if r.status_code == 200: 227 | return True 228 | else: 229 | return False 230 | 231 | @staticmethod 232 | def check_host(sonarr_url): 233 | url = sonarr_url 234 | try: 235 | r = requests.get(url + '/api/system/status', timeout=30) 236 | if r.status_code == 401: 237 | return True 238 | else: 239 | return False 240 | except Exception: 241 | return False 242 | 243 | @staticmethod 244 | def str2bool(v): 245 | return str(v).lower() in ("yes", "true", "t", "1", "y") 246 | 247 | def create_config(self, arg_loc): 248 | print '\033[93m' + "\nPingrr has no config, please follow the instructions to create one\n" + '\x1b[0m' 249 | 250 | print "\n" 251 | print "####################################\n" \ 252 | "############ SONARR ################\n" \ 253 | "####################################\n" 254 | 255 | sonarr_host = raw_input("Enter URL for your sonarr server, normally http://localhost:8989: \n") 256 | while not self.check_host(sonarr_host): 257 | try: 258 | sonarr_host = raw_input("Sonarr URL invalid, check URL and try again: \n") 259 | except Exception: 260 | print "Error getting host, try again" 261 | print '\033[94m' + str(sonarr_host) + '\x1b[0m' + '\n' 262 | 263 | sonarr_api = raw_input("Enter your sonarr API: \n") 264 | while not self.check_api(sonarr_host, sonarr_api): 265 | try: 266 | sonarr_api = raw_input("Sonarr API invalid, check API and try again: \n") 267 | except Exception: 268 | print "Error with api key, try again" 269 | print '\033[94m' + str(sonarr_api) + '\x1b[0m' + '\n' 270 | 271 | sonarr_folder_path = raw_input("Enter the folder path you want your shows to download to: \n") 272 | print '\033[94m' + str(sonarr_folder_path) + '\x1b[0m' + '\n' 273 | sonarr_quality_profile = self.get_quality_profiles(sonarr_host, sonarr_api) 274 | print '\033[94m' + str(sonarr_quality_profile) + '\x1b[0m' + '\n' 275 | 276 | sonarr_monitored = raw_input("Add TV Shows as monitored? (yes/no): \n") 277 | print '\033[94m' + str(self.str2bool(sonarr_monitored)) + '\x1b[0m' + '\n' 278 | 279 | sonarr_search_episodes = raw_input("Search for missing episodes? (yes/no): \n") 280 | print '\033[94m' + str(self.str2bool(sonarr_search_episodes)) + '\x1b[0m' + '\n' 281 | 282 | print "\n" 283 | print "####################################\n" \ 284 | "############# TRAKT ################\n" \ 285 | "####################################\n" 286 | 287 | trakt_api = raw_input("Enter your trakt.tv api key: \n") 288 | print '\033[94m' + str(trakt_api) + '\x1b[0m' + '\n' 289 | trakt_list_anticipated = raw_input("Do you want to use Trakt's anticipated list? (yes/no): \n") 290 | print '\033[94m' + str(self.str2bool(trakt_list_anticipated)) + '\x1b[0m' + '\n' 291 | trakt_list_popular = raw_input("Do you want to use Trakt's popular list? (yes/no): \n") 292 | print '\033[94m' + str(self.str2bool(trakt_list_popular)) + '\x1b[0m' + '\n' 293 | trakt_list_trending = raw_input("Do you want to use Trakt's trending list? (yes/no): \n") 294 | print '\033[94m' + str(self.str2bool(trakt_list_trending)) + '\x1b[0m' + '\n' 295 | 296 | print "\n" 297 | print "####################################\n" \ 298 | "############ PINGRR ################\n" \ 299 | "####################################\n" 300 | 301 | pingrr_timer = raw_input("How often do you want Pingrr to re-check for new shows, in hours? (0 for never): \n") 302 | print '\033[94m' + str(pingrr_timer) + '\x1b[0m' + '\n' 303 | pingrr_limit = raw_input("How many shows to add per check?(0 for no limit): \n") 304 | print '\033[94m' + str(pingrr_limit) + '\x1b[0m' + '\n' 305 | 306 | print "\n" 307 | print "####################################\n" \ 308 | "######## NOTIFICATIONS #############\n" \ 309 | "####################################\n" 310 | 311 | notifications_wanted = raw_input("Would you like to use notifications?(yes/no): \n") 312 | print '\033[94m' + str(self.str2bool(notifications_wanted)) + '\x1b[0m' + '\n' 313 | if self.str2bool(notifications_wanted): 314 | pushover = raw_input("Enable Pushover notifications?(yes/no): \n") 315 | print '\033[94m' + str(self.str2bool(pushover)) + '\x1b[0m' + '\n' 316 | if self.str2bool(pushover): 317 | pushover_user_token = raw_input("Enter your pushover user token: \n") 318 | print '\033[94m' + str(pushover_user_token) + '\x1b[0m' + '\n' 319 | pushover_app_token = raw_input("Enter your pushover app token: \n") 320 | print '\033[94m' + str(pushover_app_token) + '\x1b[0m' + '\n' 321 | else: 322 | pushover_user_token = '' 323 | pushover_app_token = '' 324 | slack = raw_input("Enable Slack notifications?(yes/no): \n") 325 | print '\033[94m' + str(self.str2bool(slack)) + '\x1b[0m' + '\n' 326 | if self.str2bool(slack): 327 | slack_webhook_url = raw_input("Enter your Slack webhook URL: \n") 328 | print '\033[94m' + str(slack_webhook_url) + '\x1b[0m' + '\n' 329 | slack_channel = raw_input("Enter the Slack channel to send messages to: \n") 330 | print '\033[94m' + str(slack_channel) + '\x1b[0m' + '\n' 331 | else: 332 | slack_webhook_url = '' 333 | slack_channel = '' 334 | else: 335 | pushover = False 336 | slack = False 337 | pushover_user_token = '' 338 | pushover_app_token = '' 339 | slack_webhook_url = '' 340 | slack_channel = '' 341 | 342 | print "\n" 343 | # print "####################################\n" \ 344 | # "############ NETFLIX ###############\n" \ 345 | # "####################################\n" 346 | # allflicks_enabled = raw_input("Enable recently added Netflix list?(yes/no): \n") 347 | # print '\033[94m' + str(self.str2bool(allflicks_enabled)) + '\x1b[0m' + '\n' 348 | # 349 | # print "\n" 350 | print "####################################\n" \ 351 | "############ FILTERS ###############\n" \ 352 | "####################################\n" 353 | 354 | print '\033[93m' + "\nIf you have selected more then one list, it is highly recommended that\n" \ 355 | "you add filters to avoid spamming Soanrr with rubbish content\n" + '\x1b[0m' 356 | 357 | filters_rating = self.ask_rating() 358 | print '\033[94m' + str(filters_rating) + '\x1b[0m' + '\n' 359 | filters_genre = self.genre_list() 360 | print '\033[94m' + str(filters_genre) + '\x1b[0m' + '\n' 361 | filters_lang = raw_input("Enter the two letter language code for the language a show must be in(e.g. en): \n") 362 | print '\033[94m' + str(filters_lang) + '\x1b[0m' + '\n' 363 | filters_year = self.ask_year(0, 3000) 364 | print '\033[94m' + str(filters_year) + '\x1b[0m' + '\n' 365 | filters_end = raw_input("Do you want to add shows that have finished?(yes/no): \n") 366 | print '\033[94m' + str(self.str2bool(filters_end)) + '\x1b[0m' + '\n' 367 | filters_cancel = raw_input("Do you want to add shows that have been cancelled?(yes/no): \n") 368 | print '\033[94m' + str(self.str2bool(filters_cancel)) + '\x1b[0m' + '\n' 369 | filters_runtime = raw_input("What is the minimum runtime for a show to be grabbed?: \n") 370 | print '\033[94m' + filters_runtime + '\x1b[0m' + '\n' 371 | filters_votes = raw_input("What is the minimum number of votes for a show to be grabbed?: \n") 372 | print '\033[94m' + filters_votes + '\x1b[0m' + '\n' 373 | 374 | fresh_config = { 375 | "sonarr": { 376 | "host": sonarr_host, 377 | "quality_profile": sonarr_quality_profile, 378 | "folder_path": sonarr_folder_path, 379 | "api": sonarr_api, 380 | "monitored": self.str2bool(sonarr_monitored), 381 | "search_missing_episodes": self.str2bool(sonarr_search_episodes), 382 | "genre_paths": False, 383 | "path_root": "/mnt/media/", 384 | "paths": { 385 | "Anime": ["anime"], 386 | "Kids-TV": ["children, family"], 387 | "Doc-TV": ["documentary"], 388 | "Reality-TV": ["reality", "game-show"]} 389 | }, 390 | "radarr": { 391 | "host": "localhost:7878", 392 | "quality_profile": "1", 393 | "folder_path": "/mnt/movies", 394 | "api": "", 395 | "monitored": True, 396 | "genre_paths": False, 397 | "path_root": "/mnt/media/", 398 | "paths": { 399 | "Anime-movies": ["anime"], 400 | "Kids": ["children, family"], 401 | "Docs": ["documentary"] 402 | } 403 | }, 404 | "trakt": { 405 | "api": trakt_api, 406 | "imdb_info": False, 407 | "limit": 0, 408 | "tv_list": { 409 | "anticipated": self.str2bool(trakt_list_anticipated), 410 | "popular": self.str2bool(trakt_list_popular), 411 | "trending": self.str2bool(trakt_list_trending) 412 | }, 413 | "movie_list": { 414 | "anticipated": False, 415 | "popular": False, 416 | "trending": False 417 | } 418 | }, 419 | "pingrr": { 420 | "limit": { 421 | "sonarr": int(pingrr_limit), 422 | "radarr": 0 423 | }, 424 | "timer": int(pingrr_timer), 425 | "log_level": "info", 426 | "aired": 0, 427 | "dry_run": False 428 | }, 429 | "pushover": { 430 | "enabled": self.str2bool(pushover), 431 | "user_token": pushover_user_token, 432 | "app_token": pushover_app_token 433 | }, 434 | "slack": { 435 | "enabled": self.str2bool(slack), 436 | "webhook_url": slack_webhook_url, 437 | "sender_name": "Pingrr", 438 | "sender_icon": ":robot_face:", 439 | "channel": slack_channel 440 | }, 441 | "filters": { 442 | "rating": int(filters_rating), 443 | "genre": filters_genre, 444 | "language": filters_lang, 445 | "allow_ended": self.str2bool(filters_end), 446 | "allow_canceled": self.str2bool(filters_cancel), 447 | "runtime": int(filters_runtime), 448 | "votes": int(filters_votes), 449 | "network": "", 450 | "country": "", 451 | "year": { 452 | "movies": 0, 453 | "shows": filters_year 454 | } 455 | }, 456 | "just_watch": { 457 | "enabled": { 458 | "movies": False, 459 | "shows": False 460 | }, 461 | "country": 'US', 462 | "pages": 1 463 | } 464 | } 465 | 466 | with open(arg_loc, 'w') as outfile: 467 | json.dump(fresh_config, outfile, indent=4, sort_keys=True) 468 | 469 | logger.info('config file created, please check config and re-run Pingrr') 470 | sys.exit() 471 | -------------------------------------------------------------------------------- /pingrr/justWatch.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import config 4 | import re 5 | import pingrr.trakt as trakt 6 | 7 | from time import sleep 8 | 9 | ################################ 10 | # Load config 11 | ################################ 12 | 13 | conf = config.Config().config 14 | 15 | ################################ 16 | # Logging 17 | ################################ 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | ################################ 22 | # Main 23 | ################################ 24 | 25 | # def get_providers(): 26 | # r = requests.get("https://apis.justwatch.com/content/providers/locale/en_" + conf['just_watch']['country']) 27 | # providers = [] 28 | # for x in r.json(): 29 | # providers.append(str(x['short_name'])) 30 | # return providers 31 | 32 | 33 | def get_recent(page, get_type): 34 | 35 | if get_type == "movies": 36 | content_type = "%5B%22movie%22%5D" 37 | elif get_type == "shows": 38 | content_type = "%5B%22show_season%22%5D" 39 | 40 | r = requests.get("https://apis.justwatch.com/content/titles/en_{}/new?body=%7B%22" 41 | "age_certifications%22:null," 42 | "%22content_types%22:{}," 43 | "%22genres%22:null," 44 | "%22languages%22:null," 45 | "%22max_price%22:null," 46 | "%22min_price%22:null," 47 | "%22monetization_types%22:%5B%22flatrate%22," 48 | "%22rent%22,%22buy%22," 49 | "%22free%22,%22ads%22%5D," 50 | "%22page%22:{}," 51 | "%22page_size%22:null," 52 | "%22presentation_types%22:null," 53 | "%22providers%22:null," 54 | "%22release_year_from%22:null," 55 | "%22release_year_until%22:null," 56 | "%22scoring_filter_types%22:null," 57 | "%22titles_per_provider%22:6%7D".format(conf['just_watch']['country'].upper(), content_type, str(page))) 58 | 59 | try: 60 | if r.status_code == 200: 61 | return r.json() 62 | else: 63 | logger.debug("Failed to get JustWatch list") 64 | return [] 65 | 66 | except ValueError: 67 | logger.warning("Value Error while getting recent from just watch") 68 | 69 | 70 | def create_list(wanted): 71 | logger.info("Creating Just Watch list") 72 | tv_list = [] 73 | movie_list = [] 74 | 75 | try: 76 | pages = int(conf['just_watch']['pages']) 77 | except TypeError: 78 | pages = 1 79 | logger.warning("WARNING: NO PAGES SET IN CONFIG, SETTING TO 1 TO BE SAFE") 80 | x = 1 81 | 82 | while x <= pages: 83 | 84 | if wanted == "shows": 85 | data = get_recent(x, "shows") 86 | elif wanted == "movies": 87 | data = get_recent(x, "movies") 88 | 89 | if data: 90 | for day in data['days']: 91 | logger.info("Getting new releases from: {}".format(str(day['date']))) 92 | for provider in day['providers']: 93 | for item in provider['items']: 94 | skip = False 95 | 96 | # Get TV from Just Watch 97 | 98 | if item['object_type'] == 'show_season' and wanted == "shows": 99 | 100 | for obj in tv_list: 101 | if item['show_title'].lower() in obj['title'].lower(): 102 | skip = True 103 | 104 | show_title = item['show_title'] 105 | show_title = re.sub(r'([^\s\w]|_)+', '', show_title) 106 | 107 | # Sleep for half a second to avoid trakt api rate limit - Needs more testing 108 | # sleep(0.5) 109 | 110 | if not skip: 111 | y = trakt.search(show_title, "show") 112 | 113 | if not y: 114 | logger.debug("failed to get show continuing, will be missing some possible shows") 115 | break 116 | 117 | if y: 118 | if y[0]['title'].lower().replace(":", "") == \ 119 | item['show_title'].lower().replace(":", ""): 120 | tv_list.append(y[0]) 121 | else: 122 | try: 123 | logger.debug("Failed to get data on show: {}".format(str(item['show_title'].encode('utf8')))) 124 | except UnicodeEncodeError: 125 | logger.debug("Failed to get data on show, unicode error: {}".format(item)) 126 | 127 | # Get movies from Just Watch 128 | 129 | elif item['object_type'] == 'movie' and wanted == "movies": 130 | 131 | for obj in movie_list: 132 | if item['title'].lower() in obj['title'].lower(): 133 | skip = True 134 | movie_title = item['title'] 135 | #movie_title = re.sub(r'([^\s\w]|_)+', '', movie_title) 136 | movie_title = re.sub(r'[^\w\s\-]*', '', movie_title) 137 | 138 | # Sleep for half a second to avoid trakt api rate limit - Needs more testing 139 | # sleep(0.5) 140 | 141 | if not skip: 142 | y = trakt.search(movie_title, "movie") 143 | 144 | if not y: 145 | logger.info("failed to get movie continuing, will be missing some possible movies") 146 | break 147 | 148 | if y: 149 | if y[0]['title'].lower().replace(":", "") == item['title'].lower().replace(":", ""): 150 | movie_list.append(y[0]) 151 | else: 152 | try: 153 | logger.info("Failed to get data on show: {}".format(str(item[movie_title].encode('utf8')))) 154 | except UnicodeEncodeError: 155 | logger.debug("Failed to get data on show, unicode error: {}".format(item)) 156 | 157 | logger.debug('page {}'.format(str(x))) 158 | x += 1 159 | 160 | if wanted == "movies": 161 | logger.info("Just Watch list created, {} movies found".format(len(movie_list))) 162 | return movie_list 163 | 164 | elif wanted == "shows": 165 | logger.info("Just Watch list created, {} shows found".format(len(tv_list))) 166 | return tv_list 167 | -------------------------------------------------------------------------------- /pingrr/netflix.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import requests 4 | 5 | import trakt 6 | 7 | ################################ 8 | # Load config 9 | ################################ 10 | 11 | conf = config.Config().config 12 | 13 | ################################ 14 | # Logging 15 | ################################ 16 | 17 | logger = logging.getLogger("Netflix") 18 | 19 | ################################ 20 | # Init 21 | ################################ 22 | 23 | url = "https://unogs-unogs-v1.p.mashape.com/aaapi.cgi?q=get:new" + conf['unogs']['days'] + ":" + conf['unogs'][ 24 | 'country'] + "&p=1&t=ns&st=adv" 25 | headers = {"X-Mashape-Key": conf['unogs']['api'], "Accept": "application/json"} 26 | 27 | ################################ 28 | # Main 29 | ################################ 30 | 31 | 32 | def get_list(): 33 | """get list of recently added netflix items""" 34 | r = requests.get(url=url, headers=headers, timeout=10) 35 | if r.status_code == requests.codes.ok: 36 | logger.debug('got raw netflix list successfully') 37 | return r.json() 38 | else: 39 | logger.debug('failed to get raw netflix list, code return: ' + str(r.status_code)) 40 | return False 41 | 42 | 43 | def create_list(): 44 | """create list of tv-shows from netflix data""" 45 | logger.info('creating list from netflix recent') 46 | if len(conf['unogs']['api']) > 0: 47 | logger.debug('unogs api key found, starting to create netflix list') 48 | data = get_list() 49 | x = [] 50 | for item in data['ITEMS']: 51 | if item['type'] == 'series': 52 | try: 53 | info = trakt.get_info(item['imdbid']) 54 | if info is False: 55 | logger.debug('Show: ', item['title'], ' does not have imdb ID, skipping') 56 | pass 57 | else: 58 | x.append(info) 59 | logger.debug('Show: ', item['title'], ' added to netflix list') 60 | except Exception: 61 | logger.warning('can not read netflix data, error creating list') 62 | logger.info('Netflix list created, ' + str(len(x)) + ' shows found') 63 | return x 64 | return [] 65 | -------------------------------------------------------------------------------- /pingrr/notifications.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pushover import Pushover 4 | from slack import Slack 5 | 6 | logger = logging.getLogger("Notifications") 7 | 8 | SERVICES = { 9 | 'pushover': Pushover, 10 | 'slack': Slack 11 | } 12 | 13 | 14 | class Notifications: 15 | def __init__(self): 16 | self.services = [] 17 | logger.debug("Initialized") 18 | 19 | def load(self, **kwargs): 20 | if 'service' not in kwargs: 21 | logger.error("You must specify a service to load with the service parameter") 22 | return False 23 | elif kwargs['service'].lower() not in SERVICES: 24 | logger.error("You specified an invalid service to load: %s", kwargs['service']) 25 | return False 26 | 27 | try: 28 | chosen_service = SERVICES[kwargs['service']] 29 | del kwargs['service'] 30 | 31 | # load service 32 | service = chosen_service(**kwargs) 33 | self.services.append(service) 34 | 35 | except Exception: 36 | logger.exception("Exception while loading service, kwargs=%r:", kwargs) 37 | 38 | def send(self, **kwargs): 39 | # remove service keyword if supplied 40 | if 'service' in kwargs: 41 | # send notification to specified service 42 | chosen_service = kwargs['service'].lower() 43 | del kwargs['service'] 44 | else: 45 | chosen_service = None 46 | 47 | # send notification(s) 48 | for service in self.services: 49 | if chosen_service and service.NAME.lower() != chosen_service: 50 | continue 51 | elif service.send(**kwargs): 52 | logger.info("Sent notification with %s", service.NAME) 53 | -------------------------------------------------------------------------------- /pingrr/pushover.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class Pushover: 8 | NAME = "Pushover" 9 | 10 | def __init__(self, app_token, user_token): 11 | self.app_token = app_token 12 | self.user_token = user_token 13 | logger.debug("Initialized") 14 | 15 | def send(self, **kwargs): 16 | if not self.app_token or not self.user_token: 17 | logger.error("You must specify an app_token and user_token when initializing this class") 18 | return False 19 | 20 | # send notification 21 | try: 22 | payload = { 23 | 'token': self.app_token, 24 | 'user': self.user_token, 25 | 'message': kwargs['message'] 26 | } 27 | resp = requests.post('https://api.pushover.net/1/messages.json', data=payload, timeout=10) 28 | return True if resp.status_code == 200 else False 29 | 30 | except Exception as ex: 31 | logger.exception("Error sending notification to %r", self.user_token) 32 | return False 33 | -------------------------------------------------------------------------------- /pingrr/radarr.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import sys 4 | import requests 5 | import json 6 | 7 | ################################ 8 | # Load config 9 | ################################ 10 | 11 | conf = config.Config().config 12 | 13 | ################################ 14 | # Logging 15 | ################################ 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | ################################ 20 | # Init 21 | ################################ 22 | 23 | url = conf['radarr']['host'] 24 | headers = {'X-Api-Key': conf['radarr']['api']} 25 | 26 | ################################ 27 | # Main 28 | ################################ 29 | 30 | 31 | def search_movie(movie_id): 32 | 33 | payload = { 34 | "name": "moviesSearch", 35 | "movieIds": [movie_id] 36 | } 37 | 38 | r = requests.post(url + '/api/command', headers=headers, data=json.dumps(payload), timeout=30) 39 | 40 | if r.status_code == 201: 41 | logger.info("radarr search request OK") 42 | return True 43 | else: 44 | return False 45 | 46 | 47 | def get_library(): 48 | """Get radarr library in a list of imdb ids""" 49 | library = [] 50 | r = requests.get(url + '/api/movie', headers=headers, timeout=60) 51 | try: 52 | if r.status_code == 401: 53 | logger.warning("Error when connecting to radarr, unauthorised. check api/url") 54 | sys.exit(1) 55 | movie_lib_raw = r.json() 56 | for n in movie_lib_raw: 57 | library.append(n['tmdbId']) 58 | except requests.ConnectionError: 59 | logger.warning("Can not connect to radarr check if radarr is up, or URL is right") 60 | sys.exit(1) 61 | return library 62 | -------------------------------------------------------------------------------- /pingrr/slack.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class Slack: 8 | NAME = "Slack" 9 | 10 | def __init__(self, webhook_url, sender_name='Notifications', sender_icon=':exclamation:', channel=None): 11 | self.webhook_url = webhook_url 12 | self.sender_name = sender_name 13 | self.sender_icon = sender_icon 14 | self.channel = channel 15 | logger.debug("Initialized") 16 | 17 | def send(self, **kwargs): 18 | if not self.webhook_url or not self.sender_name or not self.sender_icon: 19 | logger.error("You must specify an webhook_url, sender_name and sender_icon when initializing this class") 20 | return False 21 | 22 | # send notification 23 | try: 24 | payload = { 25 | 'text': kwargs['message'], 26 | 'username': self.sender_name, 27 | 'icon_emoji': self.sender_icon, 28 | } 29 | if self.channel: 30 | payload['channel'] = self.channel 31 | 32 | resp = requests.post(self.webhook_url, json=payload, timeout=10) 33 | return True if resp.status_code == 200 else False 34 | 35 | except Exception as ex: 36 | logger.exception("Error sending notification to %r", self.webhook_url) 37 | return False 38 | -------------------------------------------------------------------------------- /pingrr/sonarr.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import sys 4 | import requests 5 | 6 | ################################ 7 | # Load config 8 | ################################ 9 | 10 | conf = config.Config().config 11 | 12 | ################################ 13 | # Logging 14 | ################################ 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | ################################ 19 | # Init 20 | ################################ 21 | 22 | url = conf['sonarr']['host'] 23 | headers = {'X-Api-Key': conf['sonarr']['api']} 24 | 25 | ################################ 26 | # Main 27 | ################################ 28 | 29 | 30 | def get_library(): 31 | """Get sonarr library in a list of tvdbid ids""" 32 | library = [] 33 | r = requests.get(url + '/api/series', headers=headers, timeout=60) 34 | try: 35 | if r.status_code == 401: 36 | logger.warning("Error when connecting to sonarr, unauthorised. check api/url") 37 | sys.exit(1) 38 | tv_lib_raw = r.json() 39 | for n in tv_lib_raw: 40 | library.append(n['tvdbId']) 41 | except requests.ConnectionError: 42 | logger.warning("Can not connect to sonarr check if sonarr is up, or URL is right") 43 | sys.exit(1) 44 | return library 45 | -------------------------------------------------------------------------------- /pingrr/trakt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import config 3 | import requests 4 | import urllib 5 | import re 6 | 7 | #import imdb 8 | 9 | ################################ 10 | # Load config 11 | ################################ 12 | 13 | conf = config.Config().config 14 | 15 | ################################ 16 | # Logging 17 | ################################ 18 | 19 | logger = logging.getLogger(__name__) 20 | 21 | ################################ 22 | # Init 23 | ################################ 24 | 25 | #i = imdb.IMDb() 26 | 27 | data = [] 28 | headers = { 29 | 'content-type': 'application/json', 30 | 'trakt-api-version': '2', 31 | 'trakt-api-key': conf['trakt']['api'] 32 | } 33 | popular = [] 34 | anticipated = [] 35 | trending = [] 36 | 37 | ################################ 38 | # Main 39 | ################################ 40 | 41 | 42 | def search(search_string, trakt_type): 43 | """Get info for a tv show or movie""" 44 | 45 | if search_string is None: 46 | return False 47 | 48 | url = "https://api.trakt.tv/search/{}?query={}&extended=full"\ 49 | .format(trakt_type, urllib.quote_plus(search_string.encode('utf8'))) 50 | logger.debug('getting info from trakt for {}'.format(search_string.encode('utf8'))) 51 | r = requests.get(url=url, headers=headers, timeout=10) 52 | 53 | # If request was as ok, and json data returned continue 54 | if r.status_code == requests.codes.ok and r.json(): 55 | x = [] 56 | y = r.json() 57 | 58 | # If the titles do not match exactly do not process 59 | 60 | title1 = re.sub(r'[^\w\s\-]*', '', y[0][trakt_type]['title'].lower()) 61 | title2 = re.sub(r'[^\w\s\-]*', '', search_string.lower()) 62 | 63 | if title1 not in title2: 64 | logger.debug("Can't get info for {}, does not match with {}".format(search_string.encode('utf8'), (y[0][trakt_type]['title'].encode('utf8')))) 65 | return False 66 | 67 | if trakt_type == "movie": 68 | y = y[0]['movie'] 69 | elif trakt_type == "show": 70 | y = y[0]['show'] 71 | 72 | user_rating = y['rating'] 73 | genre = y['genres'] 74 | votes = y['votes'] 75 | 76 | if conf['trakt']['imdb_info']: 77 | # Load imdb api for show/movie 78 | try: 79 | m = i.get_movie(y['ids']['imdb'][2:]) 80 | except TypeError: 81 | return False 82 | 83 | # Get imdb user rating for show/movie 84 | try: 85 | user_rating = m['user rating'] 86 | except KeyError: 87 | logger.info("{0}:{2} using trakt rating ({1}), not imdb".format(y[0][trakt_type]['title'], user_rating, search_string.encode('utf8'))) 88 | 89 | # Get imdb genres for show/movie 90 | try: 91 | genre = m['genre'] 92 | except KeyError: 93 | logger.info("{0}:{2} using trakt genres ({1}), not imdb".format(y[0][trakt_type]['title'], genre, search_string.encode('utf8'))) 94 | 95 | # Get imdb votes for show/movie 96 | try: 97 | votes = m['votes'] 98 | except KeyError: 99 | logger.info("{0}:{2} using trakt votes ({1}), not imdb".format(y[0][trakt_type]['title'], votes, search_string.encode('utf8'))) 100 | 101 | # if movie details where requested return movie payload 102 | if trakt_type == "movie": 103 | x.append({'title': y['title'], 104 | 'tmdb': y['ids']['tmdb'], 105 | 'imdb': y['ids']['imdb'], 106 | 'trakt': y['ids']['trakt'], 107 | 'rating': user_rating, 108 | 'language': y['language'], 109 | 'genres': genre, 110 | 'votes': votes, 111 | 'runtime': y['runtime'], 112 | 'certification': y['certification'], 113 | 'released': y['released'], 114 | 'year': y['year']}) 115 | logger.debug("got {}'s info successfully".format(y['title'].encode('utf8'))) 116 | return x 117 | 118 | # if TV details where requested return TV payload 119 | elif trakt_type == "show": 120 | x.append({'title': y['title'], 121 | 'status': y['status'], 122 | 'tvdb': y['ids']['tvdb'], 123 | 'imdb': y['ids']['imdb'], 124 | 'trakt': y['ids']['trakt'], 125 | 'rating': user_rating, 126 | 'language': y['language'], 127 | 'country': y['country'], 128 | 'genres': genre, 129 | 'network': y['network'], 130 | 'votes': votes, 131 | 'runtime': y['runtime'], 132 | 'year': y['year'], 133 | 'aired': y['aired_episodes']}) 134 | logger.debug("got {}'s info successfully".format(y['title'].encode('utf8'))) 135 | return x 136 | 137 | else: 138 | logger.debug('failed to get trakt show info for {}, code return: {}'.format(search_string, str(r.status_code))) 139 | return False 140 | 141 | 142 | def get_trakt_data(name, cat): 143 | """Get trakt list info""" 144 | 145 | if cat == 'trending': 146 | url = "https://api.trakt.tv/{}/{}/?limit=100&extended=full".format(name, cat) 147 | else: 148 | url = "https://api.trakt.tv/{}/{}/?limit={}&extended=full".format(name, cat, str(conf['trakt']['limit'])) 149 | 150 | r = requests.get(url=url, headers=headers) 151 | 152 | if r.status_code == requests.codes.ok: 153 | logger.debug('got trakt {} {} list successfully'.format(name, cat)) 154 | else: 155 | logger.debug('failed to get trakt {} {} list, code return: {}'.format(name, cat, str(r.status_code))) 156 | return False 157 | 158 | response = r.json() 159 | 160 | x = [] 161 | 162 | for element in response: 163 | 164 | if cat == 'trending' or cat == 'anticipated': 165 | if name == 'shows': 166 | obj = element['show'] 167 | if name == 'movies': 168 | obj = element['movie'] 169 | else: 170 | obj = element 171 | 172 | user_rating = obj['rating'] 173 | genre = obj['genres'] 174 | votes = obj['votes'] 175 | 176 | if conf['trakt']['imdb_info']: 177 | 178 | # Load imdb api for show/movie 179 | try: 180 | m = i.get_movie(obj['ids']['imdb'][2:]) 181 | except TypeError: 182 | return False 183 | 184 | # Get imdb user rating for show/movie 185 | try: 186 | user_rating = m['user rating'] 187 | except KeyError: 188 | user_rating = obj['rating'] 189 | 190 | # Get imdb genres for show/movie 191 | try: 192 | genre = m['genre'] 193 | except KeyError: 194 | genre = obj['genres'] 195 | 196 | # Get imdb votes for show/movie 197 | try: 198 | votes = m['votes'] 199 | except KeyError: 200 | votes = obj['votes'] 201 | 202 | if name == 'movies': 203 | x.append({'title': obj['title'], 204 | 'tmdb': obj['ids']['tmdb'], 205 | 'imdb': obj['ids']['imdb'], 206 | 'trakt': obj['ids']['trakt'], 207 | 'rating': user_rating, 208 | 'language': obj['language'], 209 | 'genres': genre, 210 | 'votes': votes, 211 | 'runtime': obj['runtime'], 212 | 'certification': obj['certification'], 213 | 'released': obj['released'], 214 | 'year': obj['year']}) 215 | logger.debug("got {}'s info successfully".format(obj['title'].encode('utf8'))) 216 | else: 217 | x.append({'title': obj['title'], 218 | 'status': obj['status'], 219 | 'tvdb': obj['ids']['tvdb'], 220 | 'imdb': obj['ids']['imdb'], 221 | 'trakt': obj['ids']['trakt'], 222 | 'rating': user_rating, 223 | 'language': obj['language'], 224 | 'country': obj['country'], 225 | 'genres': genre, 226 | 'network': obj['network'], 227 | 'votes': votes, 228 | 'runtime': obj['runtime'], 229 | 'year': obj['year'], 230 | 'aired': obj['aired_episodes']}) 231 | logger.debug("got {}'s info successfully".format(obj['title'].encode('utf8'))) 232 | return x 233 | 234 | 235 | # def get_json_data(tvdb): 236 | # url = "http://skyhook.sonarr.tv/v1/tvdb/shows/en/{}".format(tvdb) 237 | # r = requests.get(url) 238 | # if r.status_code == requests.codes.ok: 239 | # logger.debug('got json data for {} successfully'.format(tvdb)) 240 | # return r.json() 241 | # else: 242 | # logger.debug('failed to get json data for {}'.format(tvdb)) 243 | # return False 244 | 245 | 246 | def get_info(arg): 247 | 248 | trakt_temp_tv = [] 249 | trakt_temp_movie = [] 250 | 251 | if arg == 'tv': 252 | logger.info("Checking if any trakt tv lists are required") 253 | for tv_list in conf['trakt']['tv_list']: 254 | if conf['trakt']['tv_list'][tv_list]: 255 | logger.info("Getting {} tv list from trakt".format(tv_list)) 256 | trakt_temp_tv.append(get_trakt_data('shows', tv_list)) 257 | 258 | trakt_complete_tv = [] 259 | 260 | logger.info(trakt_temp_tv) 261 | 262 | for trakt_list in trakt_temp_tv: 263 | for line in trakt_list: 264 | if line not in trakt_complete_tv: 265 | trakt_complete_tv.append(line) 266 | 267 | return trakt_complete_tv 268 | 269 | if arg == 'movie': 270 | logger.info("Checking if any trakt movie lists are required") 271 | for movie_list in conf['trakt']['movie_list']: 272 | if conf['trakt']['movie_list'][movie_list]: 273 | logger.info("Getting {} movie list from trakt".format(movie_list)) 274 | trakt_temp_movie.append(get_trakt_data('movies', movie_list)) 275 | 276 | trakt_complete_movie = [] 277 | 278 | for trakt_list in trakt_temp_movie: 279 | for line in trakt_list: 280 | if line not in trakt_complete_movie: 281 | trakt_complete_movie.append(line) 282 | 283 | return trakt_complete_movie 284 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests ~= 2.18.4 2 | BeautifulSoup4 ~= 4.6.0 3 | fuzzywuzzy ~= 0.15.1 4 | python-Levenshtein ~= 0.12.0 --------------------------------------------------------------------------------