Kodi Community Forum

Full Version: Kodi + Playlists programmatic generation??
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello

I am trying to solve a specific problem (unaired episodes showing up in the "next up" type playlists) and have been trying to figure out if I can create my own playlists and filter out any episodes which have airdates in the future.

LazyTV can seemingly create smart playlists on the fly and its this type of process that I think would solve my problem.

Im not the best at python but i've figured out how to query all the in progress tv shows and get episode lists and then filter for only the next unwatched and/or non future dated episodes.

So i can get the specific 'episodeid' for each of the episode(s) which i want to add.  I figured that would be the hard part and then once i've got a specific episodeid/path adding it to a playlist would be relatively easy.

However all the "Playlist.Add" method seems to do is add the item to the internal "Video" playlist which if you wanted to save as a playlist you cant do programatically?

Am i right in thinking that it is not possible to create a smart playlist/basic playlist via JSON-RPC and add a specific episodeid to it?

So my only option would be to create a smart playlist XML .xsp file for each show/episode and then combine these into one master playlist?

If i am going to go down the route of creating an XSP for each in progress show and limit it to the episode(s) i have identified, is there anyway to do this so the playlists will be hidden in kodi??
Ie if i create the XSP files in a subfolder of special://profile/playlists/video/  could i then access these playlists?

Because it seems playlists can only be accessed by their name, and if you rename them ".playlist.xsp" (so its hidden) or move them to a subfolder, kodi doesnt see them anymore.

So is there anyway to access a playlist by the path to the xsp file? eg plugin://script.lazytv/playlist/video/lazytv.xsp (as an example?)

And once ive generated my multiple playlists, and combined them into one playlist, how do I sort them in an order thats actually useful?
For reference here is the code i am using to query the database to get my list of episodes, all properly sorted in descending order of airdate, all i need to do is to figure out how to create a playlist from this (or even better populate a widget).

Code:
#!/usr/bin/env python
import requests
import json
import time
import sys
import base64
import os, re, os.path

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))

kodi_credentials = b'USERNAMETongueASSWORD' 
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'

kodi_params = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "VideoLibrary.GetInProgressTVShows"})
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)

json_object  = json.loads(json_data)

kodi_params = ''
for k in json_object['result']['tvshows']:
    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 + "]"

kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)

json_object  = json.loads(json_data)

playcounter = 1
kodi_params = ''
for k in json_object:
    playcounter = 1
    for x in k['result']['episodes']:
        if x['playcount'] < 1:
            playcounter = 0
        elif x['playcount'] > 0:
            playcounter = 1
        if playcounter == 0:
            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"]}}')
            break

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)

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'])


json_object = sorted(json_object, key=lambda k: (k['result']['episodedetails']['firstaired']), reverse=True)
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>'
for k in json_object:
#    print(k['result']['episodedetails']['file'])
#    print(k['result']['episodedetails']['episode'])
#    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("/home/osmc/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"+'    <order direction=\"descending\">year</order>' + "\n" +'</smartplaylist>'
    f = open("/home/osmc/.kodi/userdata/playlists/video/LAZY_" +k['result']['episodedetails']['showtitle']+ ".xsp", "w")
    f.write(xml_str)
    f.close()
    big_xml = big_xml + "\n"+'    <rule field=\"playlist\" operator="is">' +"\n"+'        <value>'+str(k['result']['episodedetails']['showtitle'])+'</value>'+"\n"
    big_xml = big_xml +'    </rule>'

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()
(2019-05-18, 22:42)henryjfry Wrote: [ -> ]Hello

I am trying to solve a specific problem (unaired episodes showing up in the "next up" type playlists) and have been trying to figure out if I can create my own playlists and filter out any episodes which have airdates in the future.

LazyTV can seemingly create smart playlists on the fly and its this type of process that I think would solve my problem.

Im not the best at python but i've figured out how to query all the in progress tv shows and get episode lists and then filter for only the next unwatched and/or non future dated episodes.

So i can get the specific 'episodeid' for each of the episode(s) which i want to add.  I figured that would be the hard part and then once i've got a specific episodeid/path adding it to a playlist would be relatively easy.

However all the "Playlist.Add" method seems to do is add the item to the internal "Video" playlist which if you wanted to save as a playlist you cant do programatically?

Am i right in thinking that it is not possible to create a smart playlist/basic playlist via JSON-RPC and add a specific episodeid to it?

So my only option would be to create a smart playlist XML .xsp file for each show/episode and then combine these into one master playlist?

If i am going to go down the route of creating an XSP for each in progress show and limit it to the episode(s) i have identified, is there anyway to do this so the playlists will be hidden in kodi??
Ie if i create the XSP files in a subfolder of special://profile/playlists/video/  could i then access these playlists?

Because it seems playlists can only be accessed by their name, and if you rename them ".playlist.xsp" (so its hidden) or move them to a subfolder, kodi doesnt see them anymore.

So is there anyway to access a playlist by the path to the xsp file? eg plugin://script.lazytv/playlist/video/lazytv.xsp (as an example?)

And once ive generated my multiple playlists, and combined them into one playlist, how do I sort them in an order thats actually useful?
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_*
(2019-05-19, 16:23)henryjfry Wrote: [ -> ]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_*

Wow, this seems to be right what I needed. In another thread I am "complaining" that the skin helper next episodes widget seems quite slow, at least on my system.

Unfortunately I am not that much into python and kodi scripts (yet), so: can you help me on how to install this ?
Yep thats no problem. Although im going to assume you are using a linux based kodi box, like a pi or something because i couldnt tell you anything about python for windows.

Right well my script is called "/home/user/scripts/playlist_xsp.py" and that would be the first long code block in the post you quoted. In linux you would create the script and make it executable.
I made all the path references in the linux shortcut to home style (~/ rather than /home/user/) so as to not show my username. So you would need to find all lines with ~/ and modify the paths accordingly e.g ~/.kodi/userdata/playlists/video/LAZY_* for your video playlists directory where all the playlists created by the script are called "LAZY_Tv_Show_Name.xsp", also ~/scripts/playlist_fix_special.sh path to a supplementary script (more later) and ~/special:/ which i added to accomodate a specific thing so ill come back to that.
Most of the rest should be generic python so you would just need to make sure you have all the appropriate modules installed (so on linux "pip install requests, json, time, datetime, sys, base64, os, re, sh") and update any system specific stuff.

This part:
Code:
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'

Is for if you have a user name and password and you would need to fix the ip and port depending on your setup too.

Ill quote the line numbers where you need to make the changes and the broad changes ok:

13,14,15 - file paths, and added because on my system after the playlists were being written something was modifying the path value in the xml and breaking the special:// path to the folders in my library. My library is setup by an addon so the library folders are within the addon_data folder and the playlist created for each show points to the specific folder within the addon. If your library isnt in a special folder this might not be required for you. If your system leaves the playlists alone any of these lines wont be required (13,14,15   117,118,119   151-154   168-175)

24-31 - personal settings based on your system, 127.0.0.1 and port 8080 will work fine as local host and default port. You wouldn't be able to run the script from a remote machine as you need write access to the local system for the playlists, unless you set that up yourself. And if you don't use authentication the header would change. Im not sure what happens if you use dummy authentication details when they arent required, it might still work though if you dont hvae a username and password on your kodi setup.

33-71 - this should basically work in any version of python. My script is for python 2.7 so assuming you have the modules installed and the right details for kodi setup at the top these polls of the inprogress shows in your library and processing the json data should just work.  I tried to document the steps in the code.
Although i have just noticed that its flattened the code, so if you are going to use this you need to indent it all properly. I could send you a copy of the script via email or somehting if you want.

72 - today = date.today() if you change this you could filter out the next up unwatched episodes by any other date.
87 - if playcounter == 0 and str(today.strftime("%Y-%m-%d")) <> str(x['firstaired']): when the playcounter is 0 (ie first unwatched episode for a show) and the date is anything but "today" variable a playlist is created. Therefore filtering out shows with an air date today.  if you wanted to do it properly before any date, you would need to make sure the less than date test actually works (they are both strings in mine, in the same date format so match/not match works, less than/greater than might not. Also getting the logic wrong would shorten down your list the wrong way probably.

73-117 - Again should just be python. On 111 its sorting the list of results from kodi for the episodes in reverse date order, however this is probably irrelevant as playlists seem to have no specific order of their own beyond the "order by" bit in xml, which has far fewer options than actual available in kodi metadata.  So you could probably streamline the script by writing the xml files at 89 and save another bulk kodi database query.  But initially when I was writing it I wanted to be able to presort my episodes in reverse air date using json. I probably could have figured some other way to do this by writing results from X at 89 (eg str(x['firstaired']) and str(x['episodeid']))  ) to a collection or a list or something or even a sql query to the database directly, but i didnt.

121-149 Should again just be python. At this stage it is writing the XML files (135-137 + 147-149), so it writes a single playlist file for each inprogress show (xml_str) and then combines these into a playlist (big_xml) which are written to playlist fils (.xsp) in the kodi playlists folder.

126-127 - These lines get the file path from the json data and 127 fixes an issue where the file path where it always came back something like /home/user/special:/addon_data/...  so what you need to do with these lines will depend on your system and how your setup comes back with the file path for the episodes in your library.

So yeah the broad strokes should be reasonable generic, but it just something ive hacked together because i couldnt figure out any better way to do it without learning how to build my own addon. So most of the system specific stuff are workarounds for problems I was having, whether or not they work for you or you have the same problems. I couldnt say.

I suggest you try setting it up (possibly an online code compiler would help with indenting) and comment out all parts where its writing any data to a file and check all the outputs with print statements and exit() and certain points and see what works. Ill comment back if you have any issues.
(2019-05-19, 19:24)henryjfry Wrote: [ -> ]Yep thats no problem. Although im going to assume you are using a linux based kodi box, like a pi or something because i couldnt tell you anything about python for windows.

Right well my script is called "/home/user/scripts/playlist_xsp.py" and that would be the first long code block in the post you quoted. In linux you would create the script and make it executable.
I made all the path references in the linux shortcut to home style (~/ rather than /home/user/) so as to not show my username. So you would need to find all lines with ~/ and modify the paths accordingly e.g ~/.kodi/userdata/playlists/video/LAZY_* for your video playlists directory where all the playlists created by the script are called "LAZY_Tv_Show_Name.xsp", also ~/scripts/playlist_fix_special.sh path to a supplementary script (more later) and ~/special:/ which i added to accomodate a specific thing so ill come back to that.
Most of the rest should be generic python so you would just need to make sure you have all the appropriate modules installed (so on linux "pip install requests, json, time, datetime, sys, base64, os, re, sh") and update any system specific stuff.

This part:
Code:
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'

Is for if you have a user name and password and you would need to fix the ip and port depending on your setup too.

Ill quote the line numbers where you need to make the changes and the broad changes ok:

13,14,15 - file paths, and added because on my system after the playlists were being written something was modifying the path value in the xml and breaking the special:// path to the folders in my library. My library is setup by an addon so the library folders are within the addon_data folder and the playlist created for each show points to the specific folder within the addon. If your library isnt in a special folder this might not be required for you. If your system leaves the playlists alone any of these lines wont be required (13,14,15 117,118,119 151-154 168-175)

24-31 - personal settings based on your system, 127.0.0.1 and port 8080 will work fine as local host and default port. You wouldn't be able to run the script from a remote machine as you need write access to the local system for the playlists, unless you set that up yourself. And if you don't use authentication the header would change. Im not sure what happens if you use dummy authentication details when they arent required, it might still work though if you dont hvae a username and password on your kodi setup.

33-71 - this should basically work in any version of python. My script is for python 2.7 so assuming you have the modules installed and the right details for kodi setup at the top these polls of the inprogress shows in your library and processing the json data should just work. I tried to document the steps in the code.
Although i have just noticed that its flattened the code, so if you are going to use this you need to indent it all properly. I could send you a copy of the script via email or somehting if you want.

72 - today = date.today() if you change this you could filter out the next up unwatched episodes by any other date.
87 - if playcounter == 0 and str(today.strftime("%Y-%m-%d")) <> str(x['firstaired']): when the playcounter is 0 (ie first unwatched episode for a show) and the date is anything but "today" variable a playlist is created. Therefore filtering out shows with an air date today. if you wanted to do it properly before any date, you would need to make sure the less than date test actually works (they are both strings in mine, in the same date format so match/not match works, less than/greater than might not. Also getting the logic wrong would shorten down your list the wrong way probably.

73-117 - Again should just be python. On 111 its sorting the list of results from kodi for the episodes in reverse date order, however this is probably irrelevant as playlists seem to have no specific order of their own beyond the "order by" bit in xml, which has far fewer options than actual available in kodi metadata. So you could probably streamline the script by writing the xml files at 89 and save another bulk kodi database query. But initially when I was writing it I wanted to be able to presort my episodes in reverse air date using json. I probably could have figured some other way to do this by writing results from X at 89 (eg str(x['firstaired']) and str(x['episodeid'])) ) to a collection or a list or something or even a sql query to the database directly, but i didnt.

121-149 Should again just be python. At this stage it is writing the XML files (135-137 + 147-149), so it writes a single playlist file for each inprogress show (xml_str) and then combines these into a playlist (big_xml) which are written to playlist fils (.xsp) in the kodi playlists folder.

126-127 - These lines get the file path from the json data and 127 fixes an issue where the file path where it always came back something like /home/user/special:/addon_data/... so what you need to do with these lines will depend on your system and how your setup comes back with the file path for the episodes in your library.

So yeah the broad strokes should be reasonable generic, but it just something ive hacked together because i couldnt figure out any better way to do it without learning how to build my own addon. So most of the system specific stuff are workarounds for problems I was having, whether or not they work for you or you have the same problems. I couldnt say.

I suggest you try setting it up (possibly an online code compiler would help with indenting) and comment out all parts where its writing any data to a file and check all the outputs with print statements and exit() and certain points and see what works. Ill comment back if you have any issues.
Many thanks for this detailed explanation ! It will take me some time to get through this and I guess it will not be that easy to use for my purposes, especially since I have to use Windows or Android (my Kodi boxes are Android: Shield TV), but I have one Kodi instance running on Windows. Anyway, again many thanks...maybe there is some Kodi coder out there who can actually make this into and addon - this would be the absolute best Smile As I have written in  the other thread, this is one of my main pain points with Kodi, that there is no really quick (at least in my setup) "Next Episode" widget. I'll let you know if I succeed...
Well for Android there is a terminal emulator, termux and it's possible to access the android storage so it might be possible to setup the script on that. But if you have to do it using a remote it could be painful.

Add-ons have access to more internal xbmc functions so I'm sure a proper add-on would be a lot cleaner than my code.
But unfortunately to work it needs to create multiple playlists and Kodi didn't seem to like that much.

I might have a go rewriting it at some stage to be a bit more efficient. Possibly I'll look into adding a library node which might be a way to make it a bit more versatile, while requiring something else to be set-up in Kodi.

But it'll depend on my free time coming up.
So i've tidied up the code a bit and it should be a bit better for non linux based kodi users now.
So ive removed the steps where i was making files read only as this didnt seem to stop kodi modifying the file playlist path anyway.

And ive added in python text replace steps at the end. So all you would need to do is update the "playlist_path" to point to the correct path for whatever system and then modify this line "    path_str = (path_str.replace("/path/to/home/special:/", "special://") + '/')" depending on whether or not the preceding line mangles the file path or not.
So on my system a file path "/home/user/.kodi/userdata/addon_data..." was being changed to "/home/user/special:/....", necessitating a fix.  What it does on other systems I couldnt say, so this part would need to be user specific i'm afraid.

But the remainder should just be pure python and work out of the box. You could probably just run it and print a statement to the log and exit immediately after this line and see what it does?


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

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'

playlist_path = '/path/to/home/.kodi/userdata/playlists/video/'

kodi_params = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "VideoLibrary.GetInProgressTVShows"})
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)

json_object  = json.loads(json_data)

kodi_params = ''
for k in json_object['result']['tvshows']:
    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 + "]"
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
json_object  = json.loads(json_data)

playcounter = 1
kodi_params = ''
today = date.today()

for k in json_object:
    playcounter = 1
    for x in k['result']['episodes']:
        if x['playcount'] < 1:
            playcounter = 0
        elif x['playcount'] > 0:
            playcounter = 1
        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"]}}')
            break

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)

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'])
json_object = sorted(json_object, key=lambda k: (k['result']['episodedetails']['firstaired']), reverse=True)

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>'

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("/path/to/home/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>'
    f = open(playlist_path + "LAZY_" +k['result']['episodedetails']['showtitle']+ ".xsp", "w")
    f.write(xml_str)
    f.close()

    big_xml = big_xml + "\n"+'    <rule field=\"playlist\" operator="is">' +"\n"+'        <value>'+str(k['result']['episodedetails']['showtitle'])+'</value>'+"\n"
    big_xml = big_xml +'    </rule>'

big_xml = big_xml + "\n"+'    <order direction=\"descending\">year</order>' + "\n" +'</smartplaylist>'
f = open(playlist_path + "NEXT_EPISODES_PLAYLIST.xsp", "w")
f.write(big_xml)
f.close()

replacement = """special://"""
for dname, dirs, files in os.walk(playlist_path):
    for fname in files:
        fpath = os.path.join(dname, fname)
        with open(fpath) as f:
            s = f.read()
        s = s.replace("/special:/", replacement)
        with open(fpath, "w") as f:
            f.write(s)

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)

replacement = """special://"""
for dname, dirs, files in os.walk(playlist_path):
    for fname in files:
        fpath = os.path.join(dname, fname)
        with open(fpath) as f:
            s = f.read()
        s = s.replace("/special:/", replacement)
        with open(fpath, "w") as f:
            f.write(s)
 
(2019-05-19, 20:54)henryjfry Wrote: [ -> ]Well for Android there is a terminal emulator, termux and it's possible to access the android storage so it might be possible to setup the script on that. But if you have to do it using a remote it could be painful.

Add-ons have access to more internal xbmc functions so I'm sure a proper add-on would be a lot cleaner than my code.
But unfortunately to work it needs to create multiple playlists and Kodi didn't seem to like that much.

I might have a go rewriting it at some stage to be a bit more efficient. Possibly I'll look into adding a library node which might be a way to make it a bit more versatile, while requiring something else to be set-up in Kodi.

But it'll depend on my free time coming up.
So edit line 77:
path_str = (path_str.replace("/path/to/home/special:/", "special://") + '/')
(2019-05-20, 19:50)henryjfry Wrote: [ -> ]So edit line 77:
path_str = (path_str.replace("/path/to/home/special:/", "special://") + '/')

Ok so further improvements. Basically the same but it should just need configuring at the top of the script.

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

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'

playlist_path = '/home/path/.kodi/userdata/playlists/video/'

kodi_params = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "VideoLibrary.GetInProgressTVShows"})
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)

json_object  = json.loads(json_data)

kodi_params = ''
for k in json_object['result']['tvshows']:
    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 + "]"
kodi_response = requests.post(kodi_url, headers=kodi_header, data=kodi_params)
json_data = json.dumps(kodi_response.json(), indent=4, sort_keys=True)
json_object  = json.loads(json_data)

playcounter = 1
kodi_params = ''
today = date.today()

for k in json_object:
    playcounter = 1
    for x in k['result']['episodes']:
        if x['playcount'] < 1:
            playcounter = 0
        elif x['playcount'] > 0:
            playcounter = 1
        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"]}}')
            break

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)

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'])
json_object = sorted(json_object, key=lambda k: (k['result']['episodedetails']['firstaired']), reverse=True)

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>'

for k in json_object:
    path_str = os.path.dirname(k['result']['episodedetails']['file'])
    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>'
    f = open(playlist_path + "LAZY_" +k['result']['episodedetails']['showtitle']+ ".xsp", "w")
    f.write(xml_str)
    f.close()

    big_xml = big_xml + "\n"+'    <rule field=\"playlist\" operator="is">' +"\n"+'        <value>'+str(k['result']['episodedetails']['showtitle'])+'</value>'+"\n"
    big_xml = big_xml +'    </rule>'

big_xml = big_xml + "\n"+'    <order direction=\"descending\">year</order>' + "\n" +'</smartplaylist>'
f = open(playlist_path + "NEXT_EPISODES_PLAYLIST.xsp", "w")
f.write(big_xml)
f.close()

replacement = """special://"""
for dname, dirs, files in os.walk(playlist_path):
    for fname in files:
        fpath = os.path.join(dname, fname)
        with open(fpath) as f:
            s = f.read()
        s = s.replace("/special:/", replacement)
        with open(fpath, "w") as f:
            f.write(s)

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)

replacement = """special://"""
for dname, dirs, files in os.walk(playlist_path):
    for fname in files:
        fpath = os.path.join(dname, fname)
        with open(fpath) as f:
            s = f.read()
        s = s.replace("/special:/", replacement)
        with open(fpath, "w") as f:
            f.write(s)