Python recipe for auto-selecting the "next" episode
#1
Update: please check the comments in the thread for tips & fixes to the recipe being shared in this first post.

Using a personal add-on, for some series that have lots of episodes, the items list on screen becomes very long and tiring to navigate with the TV box remote.
This is especially so after Kodi starts up, I navigate to that list and need a lot of clicks to reach the specific next episode that I should watch.

I don't think there's a built-in Kodi feature for what I'm looking for ("auto-select the next episode, without playing"), so I wondered if there wasn't a way to use Python to make vanilla Kodi do that. 
The below works (tested with Leia 18.9), and note that it's written for clarity rather than speed, you should rewrite it in your own way:
Python:
def myDirectoryFunction(...)
    # Create and send your ListItem's to Kodi to finish the directory.
    # ...
    xbmcplugin.addDirectoryItems(PLUGIN_ID, allMyItems)
    xbmcplugin.endOfDirectory(PLUGIN_ID)

    # RIGHT AFTER finishing the directory, if your "auto-select" custom setting is on, queue
    # the execution of some custom python code using the RunPlugin command.
    # This command does work with Python add-ons.
    #
    # Note the 'True' for the "block" parameter, to avoid the user interacting with the
    # UI in the meantime.
    if autoSelectNextEpisode:
        xbmc.executebuiltin('RunPlugin(plugin://plugin.video.myplugin/?action=actionAutoSelect)', True)


# The parameter / route from that RunPlugin command should lead to your auto-select
# function, something like the below.
def actionAutoSelect(params):
    # By this point the directory listing is available, so we can query things with xbmc.getInfoLabel().
    totalItems = int(xbmc.getInfoLabel('Container.NumItems'))
    if not totalItems:
        return

    # Get the list control ID (the numbers below are from the Estuary skin XML files, in
    # the Kodi source files).
    viewMode = xbmc.getInfoLabel('Container.ViewMode')
    if viewMode == 'WideList':
        containerID = 55
    elif viewMode == 'InfoWall':
        containerID = 54
    elif viewMode == 'Wall':
        containerID = 500
    else:
        # Unsupported view mode.
        return

    # Go through all listed items, using their PlayCount & PercentPlayed properties as criteria
    # to decide which should be auto-selected.
    # You can avoid having to loop in this way if you keep track of what episodes your add-on
    # plays. Then you don't need this loop since you already know the index of the ListItem.
    for itemIndex in range(totalItems):
        template = 'Container({id}).ListItemAbsolute({index}).'.format(id=containerID, index=itemIndex))
        # PlayCount infolabel:
        # A string that's either empty or a stringified number, the times the item
        # has been completely played ("", "1", "2" etc). This leaves that "watched"
        # checkmark on items that aren't partially played.
        playcount = xbmc.getInfoLabel(template + 'PlayCount')
        if playcount:
            # This ListItem has been completely played.
            pass
        # PercentPlayed infolabel:
        # A stringified number from 0 to 100, (eg "37"), which is how far the video has been played.
        # If the number is non-zero then the video can be resumed (that half-filled circle icon).
        percentPlayed = xbmc.getInfoLabel(template + 'PercentPlayed')
        if percentPlayed != '0':
            # This ListItem has been partially played.
            pass

    # Select the chosen item, whatever your criteria were.
    try:
        xbmc.executebuiltin('SetFocus({id},{selectedIndex},absolute)'.format(
                            id=containerID, selectedIndex=...))
    except:
        pass


Notes:

- This could also be done with you implementing your own windowing with xbmcgui and WindowXML. This way is much more sophisticated since you know exactly when the window is navigated to, what list control has the items etc. but it takes way more coding and time to use. 

- I tried using the xbmcgui functions to query the ListItem data, I even managed to access the list control from the built-in Kodi skin, but for some reason it came in as having no items at all.
Python:
window = xbmcgui.Window(xbmcgui.getCurrentWindowId())
list = window.getControl(55)
size = list.size()
items = [list.getListItem(index) for index in range(size)]
# 'size' is 0, 'items' is empty.
Note that the ID 55 used above is for the main list control on Estuary skin, WideList view mode (source reference).
Reply
#2
https://forum.kodi.tv/showthread.php?tid=322813
Reply
#3
@jepsizofye thanks a lot! So there is a built-in feature. I'll leave the code above because maybe it can be useful for other things.
Thanks again, will try this tonight.
Reply
#4
Okay, so I tried it and the suggested setting doesn't work for Python folder plugins. 
The UI description for it says that it happens in a "episode view", which I think is a type of screen that can't be produced from Python folder plugins.

Therefore, my code above is still relevant if someone is using a Python plugin.
Reply
#5
@doko-desuka Thanks for your example. I also found this old thing https://raw.githubusercontent.com/marcel...Monitor.py

I did few modifications to your example. Previously it didn't work if '..' parent path was enabled.

Python:

def actionAutoSelect():
# By this point the directory listing is available, so we can query things with xbmc.getInfoLabel().
totalItems = int(xbmc.getInfoLabel('Container.NumAllItems'))
totalUnwatched = int(xbmc.getInfoLabel("Container.TotalUnWatched"))
content = xbmc.getInfoLabel('Container.Content')
sortOrderAsc = xbmc.getCondVisibility('Container.SortDirection(ascending)')

if not totalItems:
return
if totalUnwatched == 0:
return
if content not in ['seasons', 'episodes']:
return

# Get current container ID
window = xbmcgui.Window(xbmcgui.getCurrentWindowId())
containerID = window.getFocusId()

nextItemIndex = 0
# Go through all listed items, using their PlayCount & PercentPlayed properties as criteria
# to decide which should be auto-selected.
# You can avoid having to loop in this way if you keep track of what episodes your add-on
# plays. Then you don't need this loop since you already know the index of the ListItem.
for itemIndex in range(totalItems):
template = 'Container({id}).ListItemAbsolute({index}).'.format(id=containerID, index=itemIndex)
label = xbmc.getInfoLabel(template + 'Label')
# PlayCount infolabel:
# A string that's either empty or a stringified number, the times the item
# has been completely played ("", "1", "2" etc). This leaves that "watched"
# checkmark on items that aren't partially played.
playcount = xbmc.getInfoLabel(template + 'PlayCount')
if playcount:
# This ListItem has been completely played.
pass
# PercentPlayed infolabel:
# A stringified number from 0 to 100, (eg "37"), which is how far the video has been played.
# If the number is non-zero then the video can be resumed (that half-filled circle icon).
percentPlayed = xbmc.getInfoLabel(template + 'PercentPlayed')
if playcount == '' and percentPlayed == '0' and label != '..':
# This ListItem is not watched
nextItemIndex = itemIndex
# Stop looping in first match if sort order is ascending
if sortOrderAsc:
break
if percentPlayed != '0' and label != '..':
# This ListItem has been partially played.
nextItemIndex = itemIndex
# Stop looping in first match if sort order is ascending
if sortOrderAsc:
break

# Select the chosen item, whatever your criteria were.
try:
xbmc.executebuiltin('SetFocus({id},{selectedIndex},absolute)'.format(id=containerID, selectedIndex=nextItemIndex))
except:
pass
Reply
#6
@-Dis thanks a lot for the observations!

I also wanted to add that I found a bug on a rare occasion when I entered an item (a directory of my add-on) from my Favorites screen and it wasn't what I wanted, so I quickly pressed "Back" to step back to the Favorites screen. After a bit, a Python exception occurred.
The reason this happened is that the xbmc.executebuiltin command takes up to a couple of seconds to run, so when I quickly stepped out of that directory, the auto-select code ended up running after the directory was gone, at a moment when there weren't any ListItems. The infolabel for ".NumItems" was an empty string.

All of that to say that any raw calls to int() should be replaced with an attempt:
Python:
    try:
        totalItems = int(xbmc.getInfoLabel('Container.NumAllItems'))
    except:
        #totalItems = 0
        return
I tried editing the code in the OP with this fix but for some reason it's not going through.
Reply
#7
I found problem with this when using Arctic Horizon 2 skin and view Landscape Combined in season page. After scrolling to different season than what is not first unwatched it will jump back to first unwatched after episodes are loaded.

Edit. Workaround for above problem. My add-on (discovery+) uses same function to all content loading. So I added content type here:
Python:

if content_type in ['seasons', 'episodes']:
xbmc.sleep(100)
helper.autoSelect(content_type)
 
and then in autoSelect:
Python:

# If currently displayed content type is different than in loaded data we don't want to autoscroll
# Example when using view Landscape Combined in seasons page on skin Arctic Horizon 2 and changing season
if content != contentType:
return

Edit2. xbmc.sleep(100) can be also in start of autoSelect.
Reply
#8
Hi. After using this auto-select command for some time, I found another fix that's needed.
Most of the time it works great, but on my slow Android device I noticed that the auto-select would consistently not happen when the list being loaded was very long (like 300+ items).
I know that pagination (separating items over several pages) is the recommended solution for these cases, but anyway, I investigated the cause.

The original code only tries to read the "container viewmode" infolabel once and assumes that it will work, but when you have a long list or a slow add-on, it's possible that the infolabel hasn't yet been set when the code tries to do that.

So the solution is to poll the infolabel until a valid value is found (or the amount of attempts or time limit are exhausted).
The same happens when getting the amount of items of the UI container, the infolabel for this usually returns "0" (a stringified zero) for some time until the list is "ready" and the actual item count is known.

I also noticed that you don't need xbmc.executebuiltin() to run this auto-select operation, you can just add some code right after submitting your directory. This resulted in a performance increase, by the way.

So the new steps become:
Python:
   # Get the plugin name (the value is something like "plugin.video.myplugin").
    pluginName = xbmc.getInfoLabel('Container.PluginName')

    # Create and send your ListItem's to Kodi to finish the directory.
    # ...
    xbmcplugin.addDirectoryItems(PLUGIN_ID, allMyItems)
    xbmcplugin.endOfDirectory(PLUGIN_ID)

    # Right after finishing the directory, if your "auto-select" custom setting is on, run
    # the auto-select code. Rather pack it in a function for cleanliness.
    if autoSelectNextEpisode:
        autoSelectExec(pluginName)

The polling function for "slow" infolabels:
Python:
from time import sleep

# pollInfoLabel: returns a string with the infolabel value, or None if it couldn't be read.
# Parameters:
#     infolabel: the name of the infolabel to query. See the list of infotags / infolabels in here:
#     https://xbmc.github.io/docs.kodi.tv/mast...tions.html
#
#     maxSeconds: maximum number of seconds to poll the label before giving up.
#
#     pluginName: video plugin name, obtained from the "Container.PluginName" infolabel.
#                 The value is something like "plugin.video.myplugin".
#
#     defaultValue: optional default value that the infolabel is expected to have, indicating it's
#                   not yet resolved and the polling should continue. For example, "Container.NumItems"
#                   is "0" by default.
def pollInfoLabel(infolabel, maxSeconds, pluginName, defaultValue=None):
    SLEEP_MS = 500
    SLEEP_S = SLEEP_MS / 1000.0
    maxTries = (maxSeconds * 1000) // SLEEP_MS
    for x in range(maxTries):
        # Test if the user has navigated outside of the video plugin (like going
        # back to the Favorites screen, for example).
        if xbmc.getInfoLabel('Container.PluginName') != pluginName:
            return None
        # Poll the infolabel in question, waiting for it to be a non-empty
        # string and also different than the default value parameter.
        stringValue = xbmc.getInfoLabel(infolabel)
        if stringValue and stringValue != defaultValue:
            return stringValue
        sleep(SLEEP_S)
    return None

The viewmode and item counting parts of the auto-select function become like this, using the poll function:
Python:
def autoSelectExec(pluginName):
    # Need to get the viewmode so we have a container ID to use
    # with SetFocus() at the end.
    # The container IDs below are for the Estuary skin. Please check the XMLs of your preferred skin for
    # their container IDs.
    viewMode = pollInfoLabel('Container.ViewMode', 4, pluginName)    
    if viewMode == 'WideList':
        # ID 55 is for the main item list in the WideList.xml layout:
        # https://github.com/xbmc/xbmc/blob/Leia/a...ist.xml#L8
        containerID = 55
    elif viewMode == 'InfoWall':
        # https://github.com/xbmc/xbmc/blob/Leia/a...30-L369C32
        containerID = 54
    elif viewMode == 'Wall':
        # https://github.com/xbmc/xbmc/blob/Leia/a...ll.xml#L10
        containerID = 500
    else:
        # Unsupported view mode.
        return

    template = 'Container(%d).' % containerID

    # Poll the number of items in the main container. Note the use of the 'defaultValue'
    # parameter, returned by ".NumItems" before it's resolved.
    totalItems = pollInfoLabel(template + 'NumItems', 4, pluginName, defaultValue='0')
    if totalItems and totalItems.isdigit():
        totalItems = int(totalItems)
    else:
        return

    # (. . .)
The rest of the function stays the same, it was only those two infolabels that needed to be polled. After that, the directory is fully loaded.
Reply
#9
(2023-08-07, 06:29)doko-desuka Wrote: The viewmode and item counting parts of the auto-select function become like this, using the poll function:

From testing and coding I have done with view modes they aren't consistent across all Kodi skins.  I've had to account for that in my addon and it can get complex.  I am not sure if that will be a problem for you or not ?


Thanks,

Jeff
Running with the Mezzmo Kodi addon.  The easier way to share your media with multiple Kodi clients.
Service.autostop , CBC Sports, Kodi Selective Cleaner and Mezzmo Kodi addon author.
Reply
#10
@jbinkley60 thanks for the heads up, and for pointing to that cool script.

I personally only use Estuary, so I've got the container IDs and view modes hard-coded for that, in that function.
Reply
#11
(2023-08-07, 11:16)doko-desuka Wrote: @jbinkley60 thanks for the heads up, and for pointing to that cool script.

I personally only use Estuary, so I've got the container IDs and view modes hard-coded for that, in that function.
That will work.  I wasn't sure if the target was for all skins in which case this issue would arise.  I've often wondered if anyone has taken the time to map these across all the common skins.  I did a few and stopped.


Thanks,

Jeff
Running with the Mezzmo Kodi addon.  The easier way to share your media with multiple Kodi clients.
Service.autostop , CBC Sports, Kodi Selective Cleaner and Mezzmo Kodi addon author.
Reply
#12
There's no need for hard coding view modes. See https://github.com/Dis90/plugin.video.di...er.py#L293
Reply
#13
(2023-08-07, 19:58)-Dis Wrote: There's no need for hard coding view modes. See https://github.com/Dis90/plugin.video.di...er.py#L293

In my case it is because I am forcing the view mode based upon a user selected addon setting. I am not trying to detect the current view mode.


Thanks,

Jeff
Running with the Mezzmo Kodi addon.  The easier way to share your media with multiple Kodi clients.
Service.autostop , CBC Sports, Kodi Selective Cleaner and Mezzmo Kodi addon author.
Reply
#14
(2023-08-07, 19:58)-Dis Wrote: There's no need for hard coding view modes. See https://github.com/Dis90/plugin.video.di...er.py#L293
Very interesting, taking the container ID using the xbmcgui API. From that link: "containerID = window.getFocusId()"
In my slow device I'm not sure what value this window.getfocusId() would return before the UI with the long list is fully loaded, and the main container focused.

Based on the source code, the ID would be -1 (nothing) and it would throw an exception: 
https://github.com/xbmc/xbmc/blob/Leia/x...w.cpp#L529

So to poll that, you'd put it inside a try: except: block.
Edit: or, doing it like what you're doing, using the generic infolabel with xbmc.getInfoLabel('Container.NumAllItems'). If you poll that infolabel with pollInfoLabel('Container.NumAllItems', 4, pluginName, '0'), then as soon as the polling is successful and the items exist, I think by that point the UI is loaded and it should be okay to get the container ID with .getFocusId().
Reply
#15
(2023-08-09, 04:52)doko-desuka Wrote: Very interesting, taking the container ID using the xbmcgui API.
In my slow device I'm not sure what value this window.getfocusId() would return before the UI with the long list is fully loaded, and the main container focused.

Based on the source code, the ID would be -1 (nothing) and it would throw an exception: 
https://github.com/xbmc/xbmc/blob/Leia/x...w.cpp#L529

So to poll that, you'd put it inside a try: except: block.
Edit: or, doing it like what you're doing, using the generic infolabel with xbmc.getInfoLabel('Container.NumAllItems'). If you poll that infolabel with pollInfoLabel('Container.NumAllItems', 4, pluginName, '0'), then as soon as the polling is successful and the items exist, I think by that point the UI is loaded and it should be okay to get the container ID with .getFocusId().

Interesting approach and could work.  However, I would still have the issue that I give the user the option for the view mapping by container type and skin in the addon settings.  Here's the AeonNox 5/ Silvo skin view settings options) I suppose I could make the settings generic and the same across all skins and if a view mode isn't supported by a particular skin log an error but I wouldn't be able to let the user know at the time they make the addon setting selections.  I do have a default for all other skins that I don't enumerate.  That is similar to your suggested approach for setting the view. 

If I was doing a user popup menu for the user then your suggestion would be perfect for populating the available view types, similar to what Kodi offers in the GUI today.  I'll give your suggestion some more thought to making it a context menu item vs. an addon setting.  it would be simpler to maintain.


Thanks,

Jeff
Running with the Mezzmo Kodi addon.  The easier way to share your media with multiple Kodi clients.
Service.autostop , CBC Sports, Kodi Selective Cleaner and Mezzmo Kodi addon author.
Reply

Logout Mark Read Team Forum Stats Members Help
Python recipe for auto-selecting the "next" episode0