Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions script.service.playbackresumer/addon.xml
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.service.playbackresumer" name="Kodi Playback Resumer" version="2.0.7" provider-name="bossanova808, bradvido88">
<addon id="script.service.playbackresumer" name="Kodi Playback Resumer" version="2.0.8" provider-name="bossanova808, bradvido88">
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.bossanova808" version="1.0.0"/>
</requires>
<extension point="xbmc.service" library="default.py" />
<extension point="xbmc.addon.metadata">
<summary lang="en_GB">Periodically sets the resume point of videos and can automatically resume last played video if Kodi crashes.</summary>
<summary lang="sv_SE">Ställer in återuppspelningspunkten för videor med jämna mellanrum och kan automatiskt återuppspela den senast spelade videon om Kodi kraschar.</summary>
<description lang="en_GB">
Runs as a service and will periodically update the resume point while videos are playing, so you can re-start from where you were in the event of a crash. It can also automatically resume a video if Kodi was shutdown while playing it. See setting to configure how often the resume point is set, and whether to automatically resume.
</description>
<description lang="sv_SE">
Körs som en tjänst och uppdaterar regelbundet återuppspelningspunkten vid spelning medan videor spelas upp, så att du kan starta om från där du var i händelse av ett krascher. Den kan också automatiskt återuppspela en video om Kodi stängdes av vid spelning. Se inställningarna för att konfigurera hur ofta återuppspelningspunkten ska ställas in och om den ska återuppspelas automatiskt.
</description>
<platform>all</platform>
<license>GPL-3.0-only</license>
<website>https://github.com/bossanova808/script.service.playbackresumer</website>
<source>https://github.com/bossanova808/script.service.playbackresumer</source>
<forum>https://forum.kodi.tv/showthread.php?tid=355383</forum>
<email>[email protected]</email>
<news>v2.0.7
- Remove old common code, use new module
<news>v2.0.8
- Minor changes for Piers
</news>
<assets>
<icon>icon.png</icon>
Expand All @@ -26,3 +30,4 @@
</addon>



7 changes: 6 additions & 1 deletion script.service.playbackresumer/changelog.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
v2.0.6
v2.0.8
- Minor changes for Piers

v2.0.7
- Remove old common code, use new module

v2.0.6
- Add support for non library videos

v2.0.5
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# XBMC Media Center Swedish language file
# Addon Name: XBMC Playback Resumer
# Addon id: script.service.playbackresumer
# Addon version: 1.2.0
# Addon Provider: bradvido88,bossanova808
msgid ""
msgstr ""
"Project-Id-Version: XBMC-Addons\n"
"Report-Msgid-Bugs-To: [email protected]\n"
"POT-Creation-Date: 2014-01-12 02:29+0000\n"
"PO-Revision-Date: 2025-10-01 11:42+0200\n"
"Last-Translator: Daniel Nylander <[email protected]>\n"
"Language-Team: sv\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.7\n"

msgctxt "Addon Summary"
msgid "Periodically sets the resume point while videos are playing and optionally automatically resumes last played video if XBMC crashes or is shutdown during playback."
msgstr "Ställer regelbundet in återupptagningspunkten medan videor spelas upp och återupptar automatiskt den senast spelade videon om XBMC kraschar eller stängs av under uppspelning."

msgctxt "Addon Description"
msgid "This addon runs in the background as a service and will periodically update the resume point while videos are playing. It will also automatically resume a video if XBMC was shutdown while playing it. See setting to configure how often the resume point is set and whether to automatically resume."
msgstr "Detta tillägg körs i bakgrunden som en tjänst och kommer regelbundet att uppdatera återuppspelningspunkten medan videor spelas upp. Det kommer också automatiskt att återuppta en video om XBMC stängdes av medan den spelades. Se Inställning för att konfigurera hur ofta återuppspelningspunkten ska ställas in och om den ska återupptas automatiskt."

msgctxt "#32000"
msgid "Resumer"
msgstr "Återuppspelare"

msgctxt "#32001"
msgid "Save resume point every X seconds"
msgstr "Spara återuppspelningspunkt var X:e sekund"

msgctxt "#32002"
msgid "Auto-Resume playback at startup"
msgstr "Återuppta uppspelningen automatiskt vid start"

msgctxt "#32003"
msgid "Auto-Play a random video if nothing is playing"
msgstr "Spela automatiskt upp en slumpmässig video om inget spelas"

msgctxt "#32030"
msgid "Exclude"
msgstr "Exkludera"

msgctxt "#32031"
msgid "Exclude Live TV"
msgstr "Exkludera direktsänd TV"

msgctxt "#32032"
msgid "Exclude HTTP sources"
msgstr "Exkludera HTTP-källor"

msgctxt "#32033"
msgid "Exclude path"
msgstr "Exkludera sökväg"

msgctxt "#32034"
msgid "Folder's path (and subfolders)"
msgstr "Mappens sökväg (och undermappar)"
8 changes: 6 additions & 2 deletions script.service.playbackresumer/resources/lib/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ def __init__(self, *args, **kwargs):
Logger.debug('KodiEventMonitor __init__')

def onSettingsChanged(self):
"""
Handle Kodi settings changes by reloading the add-on configuration from settings.

Invoked when Kodi reports settings have changed; calls the Store to reload configuration so runtime state reflects updated settings.
"""
Logger.info('onSettingsChanged - reload them.')
Store.load_config_from_settings()

def onAbortRequested(self):
Logger.debug('onAbortRequested')

23 changes: 15 additions & 8 deletions script.service.playbackresumer/resources/lib/playback_resumer.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
from bossanova808.utilities import *
# noinspection PyPackages
from .store import Store
import xbmc
# noinspection PyPackages
from .monitor import KodiEventMonitor
# noinspection PyPackages
from .player import KodiPlayer
# noinspection PyPackages
from .store import Store

from bossanova808.logger import Logger


def run():
"""
This is 'main'

:return:
Start the addon: initialize logging and global state, configure Kodi monitor and player, attempt to resume or start playback, then run the main event loop until an abort is requested.

This function:
- Starts the logger and creates the global Store.
- Instantiates and stores Kodi event monitor and player objects.
- Attempts to resume previous playback; if nothing resumed and no video is playing, triggers autoplay when enabled.
- Enters a loop that waits for an abort request and exits when one is detected.
- Stops the logger before returning.
"""
footprints()
Logger.start()
# load settings and create the store for our globals
Store()
Store.kodi_event_monitor = KodiEventMonitor(xbmc.Monitor)
Expand All @@ -26,7 +32,8 @@ def run():

while not Store.kodi_event_monitor.abortRequested():
if Store.kodi_event_monitor.waitForAbort(1):
Logger.debug('onAbortRequested')
# Abort was requested while waiting. We should exit
break

footprints(False)
Logger.stop()
91 changes: 67 additions & 24 deletions script.service.playbackresumer/resources/lib/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from bossanova808.logger import Logger
from bossanova808.notify import Notify
from bossanova808.utilities import *
from bossanova808.utilities import send_kodi_json

# noinspection PyPackages
from .store import Store
Expand All @@ -17,7 +17,14 @@ class KodiPlayer(xbmc.Player):
This class represents/monitors the Kodi video player
"""

def __init__(self, *args):
# noinspection PyUnusedLocal
def __init__(self, *_args):
"""
Initialize the KodiPlayer instance and bind it to xbmc.Player.

Parameters:
*_args: Optional positional arguments accepted for compatibility; any values passed are ignored.
"""
xbmc.Player.__init__(self)
Logger.debug('KodiPlayer __init__')

Expand All @@ -32,17 +39,32 @@ def onPlayBackEnded(self): # video ended normally (user didn't stop it)
self.autoplay_random_if_enabled()

def onPlayBackStopped(self):
"""
Handle the playback-stopped event and mark the current resume point as managed by Kodi.

When playback stops, record a sentinel resume value indicating that Kodi should retain or handle the resume point (internal sentinel -2).
"""
Logger.info("onPlayBackStopped")
self.update_resume_point(-2)

def onPlayBackSeek(self, time, seekOffset):
Logger.info(f'onPlayBackSeek time {time}, seekOffset {seekOffset}')
def onPlayBackSeek(self, time_to_seek, seek_offset):
"""
Handle a user-initiated seek during playback and update the stored resume point.

When a seek occurs, attempt to record the current playback time as the resume point.
If reading the current playback time raises a RuntimeError (e.g., seeked past the end),
clear the stored resume point.

Parameters:
time_to_seek (float): The target time position of the seek (seconds).
seek_offset (float): The relative offset of the seek from the previous position (seconds).
"""
Logger.info(f'onPlayBackSeek time {time_to_seek}, seekOffset {seek_offset}')
try:
self.update_resume_point(self.getTime())
except RuntimeError:
Logger.warning("Could not get playing time - seeked past end? Clearing resume point.")
self.update_resume_point(0)
pass

def onPlayBackSeekChapter(self, chapter):
Logger.info(f'onPlayBackSeekChapter chapter: {chapter}')
Expand All @@ -51,7 +73,6 @@ def onPlayBackSeekChapter(self, chapter):
except RuntimeError:
Logger.warning("Could not get playing time - seeked past end? Clearing resume point.")
self.update_resume_point(0)
pass

def onAVStarted(self):
Logger.info("onAVStarted")
Expand Down Expand Up @@ -86,7 +107,11 @@ def update_resume_point(self, seconds):
"""
This is where the work is done - stores a new resume point in the Kodi library for the currently playing file

:param: seconds: the time to update the resume point to. @todo add notes on -1, -2 etc here!
:param seconds: target resume time in seconds.
Special values:
-2 -> stopped normally, let Kodi persist native resume (no-op here)
-1 -> end-of-file, clear resume point (sends 0)
0 -> explicit clear resume point
:param: Store.library_id: the Kodi library id of the currently playing file
:return: None
"""
Expand Down Expand Up @@ -139,11 +164,13 @@ def update_resume_point(self, seconds):
seconds = 0

# if current time > Kodi's ignorepercentatend setting
percent_played = int((seconds * 100) / Store.length_of_currently_playing_file)
if percent_played > (100 - Store.ignore_percent_at_end):
Logger.info(f'Not updating resume point as current percent played ({percent_played}) is above Kodi\'s ignorepercentatend'
f' setting of {Store.ignore_percent_at_end}')
return
# if current time > Kodi's ignorepercentatend setting
total = Store.length_of_currently_playing_file
if total:
percent_played = int((seconds * 100) / total)
if percent_played > (100 - Store.ignore_percent_at_end):
Logger.info(f"Not updating resume point as current percent played ({percent_played}) is above Kodi's ignorepercentatend setting of {Store.ignore_percent_at_end}")
return

# OK, BELOW HERE, we're probably going to set a resume point

Expand Down Expand Up @@ -228,9 +255,12 @@ def update_resume_point(self, seconds):

def resume_if_was_playing(self):
"""
Automatically resume a video after a crash, if one was playing...

:return:
Attempt to resume playback after a previous shutdown if resuming is enabled and saved resume data exist.

If configured and valid resume data are present, the player will start the saved file and seek to the stored resume time; on any failure or if no resume data are applicable, no playback is resumed.

Returns:
True if playback was resumed and seeked to the saved position, False otherwise.
"""

if Store.resume_on_startup \
Expand All @@ -242,7 +272,7 @@ def resume_if_was_playing(self):
resume_point = float(f.read())
except Exception:
Logger.error("Error reading resume point from file, therefore not resuming.")
return
return False

# neg 1 means the video wasn't playing when Kodi ended
if resume_point < 0:
Expand All @@ -252,13 +282,17 @@ def resume_if_was_playing(self):
with open(Store.file_to_store_last_played, 'r') as f:
full_path = f.read()

str_timestamp = '%d:%02d' % (resume_point / 60, resume_point % 60)
Logger.info(f'Will resume playback at {str_timestamp} of {full_path}')
if not full_path:
Logger.info("No last-played file found; skipping resume.")
return False

mins, secs = divmod(int(resume_point), 60)
str_timestamp = f'{mins}:{secs:02d}'

self.play(full_path)

# wait up to 10 secs for the video to start playing before we try to seek
for i in range(0, 1000):
for _ in range(100):
if not self.isPlayingVideo() and not Store.kodi_event_monitor.abortRequested():
xbmc.sleep(100)
else:
Expand All @@ -270,19 +304,25 @@ def resume_if_was_playing(self):

def get_random_library_video(self):
"""
Get a random video from the library for playback

:return:
Selects a random video file path from the Kodi library.

Chooses among episodes, movies, and music videos and returns the file path of a randomly selected item if one exists. Updates Store.video_types_in_library to reflect whether a given type is present. If the library contains no eligible videos, no selection is made.

Returns:
str: File path of the selected video.
False: If no episodes, movies, or music videos exist in the library.
"""

# Short circuit if library is empty
if not Store.video_types_in_library['episodes'] \
and not Store.video_types_in_library['movies'] \
and not Store.video_types_in_library['musicvideos']:
Logger.warning('No episodes, movies, or music videos exist in the Kodi library. Cannot autoplay a random video.')
return
return False

random_int = randint(0, 2)
result_type = None
method = None
if random_int == 0:
result_type = 'episodes'
method = "GetEpisodes"
Expand Down Expand Up @@ -347,7 +387,10 @@ def autoplay_random_if_enabled(self):
if not self.isPlayingVideo() \
and (video_playlist.getposition() == -1 or video_playlist.getposition() == video_playlist.size()):
full_path = self.get_random_library_video()
Logger.info("Auto-playing next random video because nothing is playing and playlist is empty: " + full_path)
if not full_path:
Logger.info("No random video available to autoplay.")
return
Logger.info(f"Auto-playing next random video because nothing is playing and playlist is empty: {full_path}")
self.play(full_path)
Notify.info(f'Auto-playing random video: {full_path}')
else:
Expand Down
7 changes: 5 additions & 2 deletions script.service.playbackresumer/resources/lib/store.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from bossanova808.utilities import *
from bossanova808.logger import Logger
import os
import json
import xml.etree.ElementTree as ElementTree

import xbmc
import xbmcvfs

from bossanova808.constants import PROFILE, ADDON
from bossanova808.logger import Logger
from bossanova808.utilities import get_setting, get_setting_as_bool


class Store:
Expand Down