Skip to content

Instantly share code, notes, and snippets.

@andrey-yantsen
Last active August 29, 2023 16:45
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save andrey-yantsen/1c511036e523b32609dde27ffd15f137 to your computer and use it in GitHub Desktop.
Save andrey-yantsen/1c511036e523b32609dde27ffd15f137 to your computer and use it in GitHub Desktop.
Binge-watching scripts for sonarr+plex

Summary

Normally, when you're watching a TV-Show — you don't need all its episodes right away; you just need a few for the next hour or so. The following scripts help you with exactly this! plex-autotag.py marks the new shows you add with tag keep1; plex-autodelete.py removes watched episodes from your drive; sonarr-refresher.py triggers sonarr to download few new episodes, to keep you always having your next fix. All in all — sonarr will download you a new episode after you've watched one.

All this magic works only for "main" user.

The autodelete script was originally taken from plex-api and then it was modified to remove only watched episodes, plus I've added a couple of tags.

It would've been better to make the sonarr script to work directly with Plex API instead of relying on the deleted files, but I'm not there yet.

Installation

To get this working you'd need python3 with the packages listed in requirements.txt. Download all the *.py files in this gist to your local drive, change all the things in triangle braces (like <PLEX URL>) to the appropriate values, add the scripts to crontab like this:

@hourly	python3 -u /<path>/plex-autotag.py > /dev/null 2>&1
*/15 * * * *	bash -c 'python -u /<path>/plex-autodelete.py && python -u /<path>/sonarr-refresher.py' > /dev/null 2>&1

Details about the scripts

plex-autotag.py

Values to change in the script:

  • <PLEX URL> — URL to your plex setup, e.g. http://192.168.1.12:32400
  • <PLEX TOKEN> — Plex API Token, you can get one using script plex-gettoken.py

The smallest of them all. Just scans your TV-Shows libraries and adds a tag keep1 to all the shows which don't have any tags/collections. You'll find more detailed description of the tags in the following section.

plex-autodelete.py

Values to change in the script:

  • <PLEX URL> — URL to your plex setup, e.g. http://192.168.1.12:32400
  • <PLEX TOKEN> — Plex API Token, you can get one using script plex-gettoken.py

Clean-ups all the shows having a tag keep*. Tags keep0, keep1, keep3, ... limits how many episodes you always want to keep, and removes the extra watched episodes. E.g. let's assume you have a show Game of Thrones with episodes S01E01-06. If you assign tag keep1 to the show, that's what would be happening after the run of the script:

  • For the very first run when all the episodes are unwatched, nothing would happen
  • After you watched the first episode — it would be removed
  • ... all the watched episodes until 6th would be removed
  • When you'll watch the last episode, it'll stay in your library

I set keep1 for the ongoing shows and keep0 for the finished ones. This way, intro detection for the ongoing shows would be working just fine, while the finished shows would be deleted from my collection after watching the last episode.

P.S. I have now idea how keepSeason would work after my changes to the original script, never had to test it.

sonarr-refresher.py

Values to change in the script:

  • <SONARR URL> — URL to your sonarr setup, e.g. http://192.168.1.12:8989
  • <API KEY> — the API key for your sonarr

The script scans all your monitored shows and downloads missing episodes, always keeping you with at least REQUIRED_EPISODES_RUNTIME minutes of binge-time. Also, there's a separate runtime requirement set by REQUIRED_EPISODES_RUNTIME_FOR_PILOTS for a show with pilot tag, I set it for the shows I'm not sure in (i.e. collected from trakt anticipated and trakt trending lists).

E.g. Sherlock (UK, 2010) has the runtime of 90 minutes, with default values for REQUIRED_EPISODES_RUNTIME and REQUIRED_EPISODES_RUNTIME_FOR_PILOTS you'll have 2 episodes downloaded without pilot tag and 1 with it; Archer (2009) has 25 minutes runtime — with default values you'd have 8 and 4 episodes respectively.

I recommend tweaking those variables depending on your connection speed, so you'd always have the next episode when you need one.

In addition to the default runtime requirements, you can set individual requirements for a show, by using tags like download_666_minutes, where 666 is the runtime of the show you'd like to always have.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Plex-AutoDelete is a useful to delete all but the last X episodes of a show.
This comes in handy when you have a show you keep downloaded, but do not
religiously keep every single episode that is downloaded.
Usage:
Intended usage is to add one of the tags keep0, keep1, keep5, keep10, keep15, keepSeason, to
any show you want to have this behaviour. Then simply add this script to run on
a schedule and you should be all set.
Example Crontab:
@daily /home/atodd/plex-autodelete.py >> /home/atodd/plex-autodelete.log 2>&1
"""
import os
from datetime import datetime
from plexapi.server import PlexServer
TAGS = {'keep0':0, 'keep1':1, 'keep3':3, 'keep5':5, 'keep10':10, 'keep15':15, 'keepSeason':'season'}
datestr = lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def keep_episodes(show, keep):
""" Delete all but last count episodes in show. """
deleted = 0
episodes = show.episodes()
if len(episodes) <= keep:
return deleted
sort = lambda x:x.seasonEpisode
items = sorted(episodes, key=sort, reverse=True)
for episode in items[keep:]:
if show.title == 'Charmed':
print(episode.isWatched)
if episode.isWatched:
episode.delete()
deleted += 1
return deleted
def keep_season(show, keep):
""" Keep only the latest season. """
deleted = 0
print('%s Cleaning %s to latest season.' % (datestr(), show.title))
seasons = list(filter(lambda season: season.isWatched, show.seasons()))
for season in seasons:
for episode in season.episodes():
episode.delete()
deleted += 1
return deleted
def auto_delete():
print('%s Starting plex-autodelete script..' % datestr())
plex = PlexServer('<PLEX URL>', '<PLEX TOKEN>')
for section in plex.library.sections():
if section.type in ('show',):
deleted = 0
for tag, keep in TAGS.items():
func = keep_season if keep == 'season' else keep_episodes
for show in section.search(collection=tag):
deleted += func(show, keep)
if __name__ == '__main__':
auto_delete()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
from datetime import datetime
from plexapi.server import PlexServer
datestr = lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if __name__ == '__main__':
print('%s Starting plex-autotag script..' % datestr())
plex = PlexServer('<PLEX URL>', '<PLEX TOKEN>')
for section in plex.library.sections():
if section.type in ('show',):
for show in section.all():
if len(show.collections) == 0:
print('Adding keep1 to %s' % show.title)
show.addCollection(['keep1'])
PlexAPI
requests
import requests
import time
import datetime
from math import ceil
API_HOST = '<SONARR URL>/api/'
API_KEY = '<API KEY>'
REQUIRED_EPISODES_RUNTIME = 180
REQUIRED_EPISODES_RUNTIME_FOR_PILOTS = 80
def get(endpoint: str, extra_params: dict):
params = {'apikey': API_KEY}
params.update(extra_params)
return requests.get(API_HOST + endpoint, params=params).json()
def post(endpoint: str, body: dict):
params = {'apikey': API_KEY}
return requests.post(API_HOST + endpoint, json=body, params=params).json()
def put(endpoint: str, body: dict):
params = {'apikey': API_KEY}
return requests.put(API_HOST + endpoint, json=body, params=params).json()
def cmd(params: dict, delay: int):
command = post('command', params)
while True:
time.sleep(delay)
command = get('command/' + str(command['id']), {})
if command['state'] == 'completed':
break
series = get('series', {})
datestr = lambda: datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
tags = {}
import re
magic_title_re = re.compile(r'download_(\d+)_minutes')
for tag in get('tag', {}):
if tag['label'] == 'pilot':
tags[tag['id']] = REQUIRED_EPISODES_RUNTIME_FOR_PILOTS
else:
match = magic_title_re.match(tag['label'])
if match:
tags[tag['id']] = int(match.group(1))
for show in series:
if not show['monitored']:
continue
if not show.get('totalEpisodeCount'):
continue
print('%s Rescanning series: %s' % (datestr(), show['title']))
if show['runtime'] < 5:
required_cnt = 2
else:
runtime_by_tags = [tags.get(tag_id, 0) for tag_id in show['tags']]
max_runtime_by_tags = 0
if len(runtime_by_tags):
max_runtime_by_tags = max(runtime_by_tags)
required_runtime = max_runtime_by_tags if max_runtime_by_tags > 0 else REQUIRED_EPISODES_RUNTIME
required_cnt = ceil(required_runtime / show['runtime'])
cmd({'name': 'RescanSeries', 'seriesId': show['id']}, 1)
episodes = [episode for episode in get('episode', {'seriesId': show['id']}) if episode['seasonNumber'] > 0]
had_watched_episode = False
search_required = False
available_episodes = 0
required_seasons = set()
for episode in episodes:
if episode['hasFile'] or episode.get('downloading', False):
required_seasons.add(episode['seasonNumber'])
had_watched_episode = True
available_episodes += 1
continue
if not episode.get('airDate'):
continue
air_date = datetime.datetime.strptime(episode.get('airDate'), '%Y-%m-%d').date()
if available_episodes >= required_cnt and episode['monitored'] and not episode['hasFile']:
episode['monitored'] = False
print('%s Unmonitoring %s: S%02dE%02d - %s' % (datestr(), show['title'], episode['seasonNumber'], episode['episodeNumber'], episode['title']))
put('episode/' + str(episode['id']), episode)
if had_watched_episode and not episode['hasFile'] and not episode.get('downloading', False) and available_episodes < required_cnt:
if air_date < datetime.date.today():
search_required = True
available_episodes += 1
if not episode['monitored']:
episode['monitored'] = True
print('%s Requesting %s: S%02dE%02d - %s' % (datestr(), show['title'], episode['seasonNumber'], episode['episodeNumber'], episode['title']))
required_seasons.add(episode['seasonNumber'])
put('episode/' + str(episode['id']), episode)
if required_seasons:
new_seasons = []
update_needed = False
for season in show['seasons']:
if season['seasonNumber'] in required_seasons and not season['monitored']:
season['monitored'] = True
update_needed = True
new_seasons.append(season['seasonNumber'])
elif season['seasonNumber'] not in required_seasons and season['monitored']:
season['monitored'] = False
update_needed = True
if update_needed:
put('series/' + str(show['id']), show)
for episode in episodes:
if not episode['monitored'] and episode['seasonNumber'] in new_seasons:
put('episode/' + str(episode['id']), episode)
if search_required:
cmd({'name': 'SeriesSearch', 'seriesId': show['id']}, 30)
@mowl111
Copy link

mowl111 commented Jan 31, 2022

Hey, I found the button to Download the Protocol in HA, but nothing happened in frontend, can you help?

@andrey-yantsen
Copy link
Author

@mowl111, would you mind elaborating what are you talking about?

@mowl111
Copy link

mowl111 commented Jan 31, 2022

@andrey-yantsen of course, sorry... I clicked comment below your moes tuya report file....now I'm landet here..... I tried to share my moes Thermostat tuya report, like you do. I use HA beta core 2022_2. Even when I clicked the button to download the Moes tuya report nothing happens... Could you Pls help me to find the file to share it with the others

@andrey-yantsen
Copy link
Author

@mowl111 you couldn't have landed here from the HA ticket :) Please ask all the questions you have about HA on github/forum/reddit — I've used the diagnostics dump tool just once and have no idea how to resolve any issues with it.

@Turbogame
Copy link

Might I suggest a few revisions for the sonarr refresher for logging purposes?

Rescanning series output /w series title;
print('%s Rescanning series: %s' % (datestr(), show['title']))

Unmonitoring /w series title:
print('%s Unmonitoring %s: S%02dE%02d - %s' % (datestr(), show['title'], episode['seasonNumber'], episode['episodeNumber'], episode['title']))

Requesting /w series title:
print('%s Requesting %s: S%02dE%02d - %s' % (datestr(), show['title'], episode['seasonNumber'], episode['episodeNumber'], episode['title']))

Love this script by the way! Also, I think requirements.txt should be updated to read PlexAPI instead of plex-api.

@andrey-yantsen
Copy link
Author

@Turbogame, I'm glad you loved id! Thanks for the suggestions; I've updated the code.

@0xMshari
Copy link

This is awesome thanks !.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment