Python recipe for clearing the watched status of multiple items



Tested on v18.9 Leia.

In a custom video plugin (AKA a folder / directory plugin), I needed to clear the watched status of the entire list of video items.
I solved it with this code below, which opens the Kodi video database and deletes some specific elements related to the watch status of the items I wanted to “unwatch”, and that’s the playcount (fully watched items) as well as bookmarks (partially played items).

The end result should be the same as manually going through each item and using the “Mark as unwatched” context menu, or JSON RPC with “File.SetFileDetails” on each item, but it’s done directly in the database.

The input is the list of Kodi URLs for the items that you want to clear the watched status of — these URLs are strings, and are not the ListItem paths that you set with .setPath(), but rather the URLs that you give to xbmcplugin when creating the directory listing that has the playable watched items. These URLs are used with addDirectoryItem() or addDirectoryItems() (usually these URLs point back to your own plugin so it’ll be called and resolve the media URL for playing with setResolvedUrl()).

Don’t copy-paste into your add-ons, as it’s just a reference. Please rewrite this in your own way:

Python:
# Written for Kodi v18.9 Leia
# listOfURLs: string list with the Kodi URLs of all playable watched items that you wish to
#             clear the watched status of.
#             Example: "plugin://plugin.video.myvideoplugin/?param1=bli&param2=666"
def actionClearPlaycounts(listOfURLs):
    if not xbmcgui.Dialog().yesno(
        'My Video Plugin',
        'This will update the Kodi database to clear the playcount of ' \
        'all items in this list and this list alone, making these items seem ' \
        'unwatched.\nProceed?',
        nolabel="Cancel",
        yeslabel="Ok"
    ):
        return

    if not listOfURLs:
        xbmcgui.Dialog().notification('My Video Plugin',
                                      'No Kodi URLs provided',
                                      xbmcgui.NOTIFICATION_INFO, 1000, False)
        return

    import xbmcvfs
    try:
        import sqlite3
    except:
        xbmcgui.Dialog().notification('My Video Plugin',
                                      'sqlite3 not found',
                                      xbmcgui.NOTIFICATION_WARNING, 2000, True)
        return

    # Find the 'MyVideos###.db' file.
    dirs, files = xbmcvfs.listdir('special://database')
    for file in files:
        if 'MyVideos' in file and file.endswith('.db'):
            path = xbmc.translatePath('special://database/' + file)
            break
    else:
        xbmcgui.Dialog().notification('My Video Plugin',
                                      'MyVideos database file not found',
                                      xbmcgui.NOTIFICATION_WARNING, 2000, True)
        return

    # Update the database.

    try:
        connection = sqlite3.connect(path)
    except Exception as e:
        xbmcLog(e)
        xbmcgui.Dialog().notification('My Video Plugin',
                                      'Unable to connect to MyVideos database',
                                      xbmcgui.NOTIFICATION_WARNING, 3000, True)
        return

    getCursor = connection.cursor()
    itemIDFiles = []

    itemURLParams = []
    for itemKodiURL in listOfURLs:
        # Make it a sequence (even if 1-tuple) to be used with SQL placeholders.
        itemURLParam = (itemKodiURL,)
        itemURLParams.append(itemURLParam)
        # Get the 'idFile' of each input item from the database, used for clearing
        # any bookmarks (partially played items).
        getCursor.execute('SELECT idFile FROM files WHERE strFilename=?', itemURLParam)
        result = getCursor.fetchone()
        if result:
            itemIDFiles.append(result)

    # Clear the playcount (fully watched status) of the input items.
    # No errors are produced if the item hasn't been watched yet, SQL will just
    # ignore the item.
    updateCursor = connection.cursor()
    updateCursor.executemany('UPDATE files SET playCount=NULL WHERE strFilename=?',
                             itemURLParams)
    totalUpdates = updateCursor.rowcount or 0
    # Also delete any unfinished / partially watched items from the "bookmark" table.
    updateCursor.executemany('DELETE FROM bookmark WHERE idFile=?', itemIDFiles)

    try:
        if totalUpdates:
            connection.commit()
        connection.close()
    except:
        xbmcgui.Dialog().notification('My Video Plugin',
                                      'Unable to update the database ' \
                                      '(file permission error?)',
                                      xbmcgui.NOTIFICATION_WARNING, 2000, True)
        return

    if totalUpdates:
        xbmcgui.Dialog().ok('My Video Plugin',
                            'Database update complete ' \
                            '([COLOR lime]%i[/COLOR] items updated).' \
                            '\nReloading this list...' % totalUpdates)
        # Optionally reload the container (if the input was the items in the current
        # directory, then they'll be re-listed and all will appear unwatched).
        xbmc.executebuiltin('Container.Refresh()')
    else:
        xbmcgui.Dialog().ok('My Video Plugin',
                            'Finished. No updates needed (0 items updated).')