Ok so ive pretty much hacked together something which does what I want it to do. So basically what i want is to be able to have a "Next Up" playlist which has only the next episodes for in progress shows which have already aired and therefore excluding episodes airing "today".
Ive removed anything about my system from the script so for example my home path i've replaced with "~/" but in my actual script i've used full paths.
Ive set Kodi Callbacks to run the script when playback ends and when the library is updated. And ive setup the "001_NEXT_EPISODES" as a widget (special://profile/playlists/video/NEXT_EPISODES_PLAYLIST.xsp) and turned off "force refresh" which seemed cause the widget to become blank, presumably while the playlists are in the middle of being written.
Ive had to force it to make the playlists owned by root and read only. And also at several stages I run the "/home/osmc/scripts/playlist_fix_special.sh" script as kodi seems to want to change the path in my individual shows playlist xsp files from "special://profile/..." to "/special:/profile/..."
So the script creates the appropriate playlists for inprogress shows with only the next episode to watch and then combines these into the "001_NEXT_EPISODES" playlist, it then triggers the widgets to update at the end of the script by marking the oldest unwatched episode as watched and then unwatched again.
This seems to work pretty well and is fairly light weight so it should hopefully update faster than the Skin Helper Service Next Up widget, as well as the added bonus of not containing episodes which are technically unaired as the air date is "today"
python:
#!/usr/bin/env python
import requests
import json
import time, datetime
from datetime import date
import sys
import base64
import os, re, os.path
import sh
#make playlists writable, set read-only at end of script as kodi was mangling the path special:// to /special:/
#added a separate script to fix this as i was concerned it was kodi/python doing it
os.system('sudo chown osmc ~/.kodi/userdata/playlists/video/LAZY_*')
os.system('sudo chmod 644 -R ~/.kodi/userdata/playlists/video/LAZY_*')
sh.sh('~/scripts/playlist_fix_special.sh')
#Delete current playlist files which match the pattern - removed as it seemed to be causing issues
#pattern = "LAZY_*.*xsp$"
#mypath = "~/.kodi/userdata/playlists/video"
#for root, dirs, files in os.walk(mypath):
# for file in filter(lambda x: re.match(pattern, x), files):
# os.remove(os.path.join(root, file))
#setup kodi credentials for use with JSON if you have setup a username/password for Kodi. Modify IP etc as appropriate
kodi_credentials = b'user:password'
kodi_encoded_credentials = base64.b64encode(kodi_credentials)
kodi_authorization = b'Basic ' + kodi_encoded_credentials
kodi_header = { 'Content-Type': 'application/json', 'Authorization': kodi_authorization }
kodi_ip = '127.0.0.1'
kodi_port = '8080'
kodi_url = 'http://' + kodi_ip + ':' + kodi_port + '/jsonrpc'
#setup the JSON request parameters to use for the next request to Kodi
#we poll the video library for the list of inprogress TV shows
kodi_params = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "VideoLibrary.GetInProgressTVShows"})
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
#we format the JSON response so it can be read
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
json_object = json.loads(json_data)
#Below are some examples of how to get data back out of the json_object, limits are the number of inprogress shows.
#print(json_object['result']['limits']['start'])
#print(json_object['result']['limits']['end'])
#print(json_object['result']['tvshows'][0])
#json_object = json.loads(json.dumps(json_object['result'], indent=4, sort_keys=True))
#print(kodi_data.get('tvshows'))
#print(json_object['tvshows'])
#next we begin a loop to build the query which will poll the episodes of the inprogress shows to see where the next unwatched episode is
#we need to empty the kodi_params as we will use this to store the multi show query, we put the JSON request for each tvshowid on a line and braket it [ ] once #complete. For each inprogress show Kodi returns - ["season", "episode", "playcount", "firstaired", "tvshowid", "lastplayed"]
kodi_params = ''
for k in json_object['result']['tvshows']:
# print k['tvshowid']
# print k['label']
if kodi_params == '':
kodi_params = ('{"jsonrpc":"2.0","id":1,"method":"VideoLibrary.GetEpisodes","params":{"tvshowid": ' + str(k['tvshowid']) + ', "properties": ["season", "episode", "playcount", "firstaired", "tvshowid", "lastplayed"]}}')
elif kodi_params <> '':
kodi_params = kodi_params + ","+ "\n" + ('{"jsonrpc":"2.0","id":1,"method":"VideoLibrary.GetEpisodes","params":{"tvshowid": ' + str(k['tvshowid']) + ', "properties": ["season", "episode", "playcount", "firstaired", "tvshowid", "lastplayed"]}}')
kodi_params = "[" + kodi_params + "]"
#print(kodi_params)
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
#print(json_data)
json_object = json.loads(json_data)
#Kodi has now returned all the information for the episodes of the inprogress tvshows which we will need to process to determine the first unwatched episode.
playcounter = 1
kodi_params = ''
today = date.today()
#print(today.strftime("%Y-%m-%d"))
#we loop through the results for each show.
for k in json_object:
playcounter = 1
#we then loop through the episodes in each show, we set playcounter to 1 and it only gets set back to 0 if an episode of the show is unwatched, ie the next unwatched
#episode for that show. We test the playcount for each episode and if it is 0 set playcounter to 0.
for x in k['result']['episodes']:
if x['playcount'] < 1:
playcounter = 0
elif x['playcount'] > 0:
playcounter = 1
#then if the particular episode is the next unwatched for a show it will start writing the multiple "GetEpisodeDetails" which we will use to return only the next
#unwatched episodes as one result set, which can then be sorted using the 'firstaired' date. We also exclude shows which air "today" and therefore arent available
if playcounter == 0 and str(today.strftime("%Y-%m-%d")) <> str(x['firstaired']):
if kodi_params == '':
kodi_params = ('{"jsonrpc":"2.0","id":1,"method":"VideoLibrary.GetEpisodeDetails","params":{"episodeid":' + str(x['episodeid']) + ', "properties": ["title","firstaired","playcount","runtime","season","episode","showtitle","streamdetails","lastplayed","file","tvshowid","dateadded","uniqueid","seasonid"]}}')
elif kodi_params <> '':
kodi_params = kodi_params + ","+ "\n" + ('{"jsonrpc":"2.0","id":1,"method":"VideoLibrary.GetEpisodeDetails","params":{"episodeid":' + str(x['episodeid']) + ', "properties": ["title","firstaired","playcount", "runtime","season","episode","showtitle", "streamdetails","lastplayed","file", "tvshowid","dateadded", "uniqueid","seasonid"]}}')
# print(str(x['firstaired']) + ' ' + str(x['tvshowid'])+ ' ' + str(x['episodeid']))
break
#once we have found the first unwatched episode for a show we can break the loop and move onto the next show.
#once the loop is complete we write the multiple requests and bound with [] and send to kodi
kodi_params = "[" + kodi_params + "]"
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
#print(json_data)
#the only way i could figure to sort the json data was to convert it to an ordereddict
from collections import OrderedDict
json_object=json.loads(json_data, object_pairs_hook=OrderedDict)
#sort json data by given criteria
#print(json_object[1]['result']['episodedetails']['firstaired'])
#we sort the episodes we have requested in order of air date descending (ie newest episodes at the top)
json_object = sorted(json_object, key=lambda k: (k['result']['episodedetails']['firstaired']), reverse=True)
#we then start writing the "001_NEXT_EPISODES" playlist xsp which will combine all the individual xsp playlists created as k['result']['episodedetails']['showtitle']
big_xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' + "\n" + '<smartplaylist type="episodes">' + "\n"
big_xml = big_xml + ' <name>001_NEXT_EPISODES</name>' + "\n" + ' <match>one</match>'
#again make sure files are writable
os.system('sudo chown osmc ~/.kodi/userdata/playlists/video/LAZY_*')
os.system('sudo chmod 644 -R ~/.kodi/userdata/playlists/video/LAZY_*')
for k in json_object:
# print(k['result']['episodedetails']['file'])
# print(k['result']['episodedetails']['episodeid'])
# print(k['result']['episodedetails']['firstaired'])
# print(k['result']['episodedetails']['showtitle'])
path_str = os.path.dirname(os.path.abspath(k['result']['episodedetails']['file']))
path_str = (path_str.replace("~/special:/", "special://") + '/')
xml_str = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' + "\n" + '<smartplaylist type="episodes">' + "\n"
xml_str = xml_str + ' <name>' + k['result']['episodedetails']['showtitle'] + '</name>' + "\n" + ' <match>all</match>'
xml_str = xml_str + "\n" + ' <rule field=\"path\" operator=\"startwith\">'+"\n"+' <value>'+ path_str +'</value>' + "\n" + ' </rule>' + "\n"
xml_str = xml_str +' <rule field=\"episode\" operator="is">' +"\n"+' <value>'+str(k['result']['episodedetails']['episode'])+'</value>'+"\n"
xml_str = xml_str +' </rule>'+"\n" +'</smartplaylist>'
#the ordering seems to be a bit tricky, i have removed it from each individual show playlist as it has no effect here anyway.
#and we write the show playlist files as "LAZY_ + k['result']['episodedetails']['showtitle'] + .xsp"
f = open("~/.kodi/userdata/playlists/video/LAZY_" +k['result']['episodedetails']['showtitle']+ ".xsp", "w")
f.write(xml_str)
f.close()
#we then need to add each individual show to the combined "001_NEXT_EPISODES" playlist xml
big_xml = big_xml + "\n"+' <rule field=\"playlist\" operator="is">' +"\n"+' <value>'+str(k['result']['episodedetails']['showtitle'])+'</value>'+"\n"
big_xml = big_xml +' </rule>'
#and set the combined xml sort order by year descending as this seems to be the only order which works properly with widgets
#other orders seem to default in widgets to "playlist" order which would go episode 1X01,1X02,2X01,3X03 ie ascending order SeasonXEpisode
big_xml = big_xml + "\n"+' <order direction=\"descending\">year</order>' + "\n" +'</smartplaylist>'
#big_xml = big_xml + "\n"+'</smartplaylist>'
#print(big_xml)
f = open("~/.kodi/userdata/playlists/video/NEXT_EPISODES_PLAYLIST.xsp", "w")
f.write(big_xml)
f.close()
#writable - make sure the special:// path has been fixed
os.system('sudo chown osmc ~/.kodi/userdata/playlists/video/LAZY_*')
os.system('sudo chmod 644 -R ~/.kodi/userdata/playlists/video/LAZY_*')
sh.sh('~/scripts/playlist_fix_special.sh')
#i then include one last set of JSON-RPC calls to update the last episodeid retireved and set it watched/unwatched
#this causes the widgets to update
kodi_params = ('{"jsonrpc":"2.0","method":"VideoLibrary.SetEpisodeDetails","params":{"episodeid":' + str(k['result']['episodedetails']['episodeid'])+',"playcount": 1},"id":"1"}')
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
#print(json_data)
kodi_params = ('{"jsonrpc":"2.0","method":"VideoLibrary.SetEpisodeDetails","params":{"episodeid":' + str(k['result']['episodedetails']['episodeid'])+',"playcount": 0},"id":"1"}')
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
print(json_data)
#writable - one last check special has been fixed
os.system('sudo chown osmc ~/.kodi/userdata/playlists/video/LAZY_*')
os.system('sudo chmod 644 -R ~/.kodi/userdata/playlists/video/LAZY_*')
sh.sh('~/scripts/playlist_fix_special.sh')
#readonly - make files read only in a effort to keep the special:// path from being amended
os.system('sudo chmod 444 -R ~/.kodi/userdata/playlists/video/LAZY_*')
os.system('sudo chown root ~/.kodi/userdata/playlists/video/LAZY_*')
playlist_fix_special.sh
Code:
#!/bin/bash
sudo chown osmc ~/.kodi/userdata/playlists/video/LAZY_*
sed -i -e "s_/special:/_special://_g" ~/.kodi/userdata/playlists/video/LAZY_*
sudo chown root ~/.kodi/userdata/playlists/video/LAZY_*