From aa1926d3b7d4ab7171acdde1c64b8b1914de0a4b Mon Sep 17 00:00:00 2001 From: "bossanova808@gmail.com" Date: Sat, 27 Sep 2025 22:34:00 +1000 Subject: [PATCH 1/4] (WIP) Begin Piers work --- resources/lib/monitor.py | 3 +-- resources/lib/playback_resumer.py | 12 +++++++----- resources/lib/player.py | 13 ++++++++----- resources/lib/store.py | 7 +++++-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/resources/lib/monitor.py b/resources/lib/monitor.py index 3ceb335..3182428 100644 --- a/resources/lib/monitor.py +++ b/resources/lib/monitor.py @@ -14,5 +14,4 @@ def onSettingsChanged(self): Logger.info('onSettingsChanged - reload them.') Store.load_config_from_settings() - def onAbortRequested(self): - Logger.debug('onAbortRequested') + diff --git a/resources/lib/playback_resumer.py b/resources/lib/playback_resumer.py index 6d633c0..83e739a 100644 --- a/resources/lib/playback_resumer.py +++ b/resources/lib/playback_resumer.py @@ -1,11 +1,12 @@ -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(): @@ -14,7 +15,7 @@ def run(): :return: """ - footprints() + Logger.start() # load settings and create the store for our globals Store() Store.kodi_event_monitor = KodiEventMonitor(xbmc.Monitor) @@ -26,7 +27,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() diff --git a/resources/lib/player.py b/resources/lib/player.py index 1138f2e..b605c2c 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -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 @@ -17,6 +17,7 @@ class KodiPlayer(xbmc.Player): This class represents/monitors the Kodi video player """ + # noinspection PyUnusedLocal def __init__(self, *args): xbmc.Player.__init__(self) Logger.debug('KodiPlayer __init__') @@ -35,8 +36,8 @@ def onPlayBackStopped(self): 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): + Logger.info(f'onPlayBackSeek time {time_to_seek}, seekOffset {seek_offset}') try: self.update_resume_point(self.getTime()) except RuntimeError: @@ -242,7 +243,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: @@ -280,9 +281,11 @@ def get_random_library_video(self): 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" diff --git a/resources/lib/store.py b/resources/lib/store.py index 76730f8..1c92453 100644 --- a/resources/lib/store.py +++ b/resources/lib/store.py @@ -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: From 986050e4a27eca23daa38f1537fbd00491b3614c Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:39:20 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`pre?= =?UTF-8?q?-for-piers-and-use-playback-class`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @bossanova808. * https://github.com/bossanova808/script.service.playbackresumer/pull/9#issuecomment-3341626055 The following files were modified: * `resources/lib/monitor.py` * `resources/lib/playback_resumer.py` * `resources/lib/player.py` --- resources/lib/monitor.py | 5 ++++ resources/lib/playback_resumer.py | 11 ++++++--- resources/lib/player.py | 41 ++++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/resources/lib/monitor.py b/resources/lib/monitor.py index 3182428..9e24b2e 100644 --- a/resources/lib/monitor.py +++ b/resources/lib/monitor.py @@ -11,6 +11,11 @@ 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() diff --git a/resources/lib/playback_resumer.py b/resources/lib/playback_resumer.py index 83e739a..a979cad 100644 --- a/resources/lib/playback_resumer.py +++ b/resources/lib/playback_resumer.py @@ -11,9 +11,14 @@ 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. """ Logger.start() # load settings and create the store for our globals diff --git a/resources/lib/player.py b/resources/lib/player.py index b605c2c..eabba4e 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -19,6 +19,12 @@ class KodiPlayer(xbmc.Player): # 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__') @@ -33,10 +39,26 @@ 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_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()) @@ -229,9 +251,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 \ @@ -271,9 +296,13 @@ 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 From 622b82f9893ad327ef3ba10e1216e0dcaff9ff4f Mon Sep 17 00:00:00 2001 From: "bossanova808@gmail.com" Date: Sun, 28 Sep 2025 09:57:04 +1000 Subject: [PATCH 3/4] CR --- resources/lib/player.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/resources/lib/player.py b/resources/lib/player.py index eabba4e..81cedb9 100644 --- a/resources/lib/player.py +++ b/resources/lib/player.py @@ -18,12 +18,12 @@ class KodiPlayer(xbmc.Player): """ # noinspection PyUnusedLocal - def __init__(self, *args): + 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. + *_args: Optional positional arguments accepted for compatibility; any values passed are ignored. """ xbmc.Player.__init__(self) Logger.debug('KodiPlayer __init__') @@ -65,7 +65,6 @@ def onPlayBackSeek(self, time_to_seek, seek_offset): 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}') @@ -74,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") @@ -109,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 """ @@ -162,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 @@ -278,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: @@ -379,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: From fac34dee0bae9ad884e657f4b27db84a3fcfeb51 Mon Sep 17 00:00:00 2001 From: "bossanova808@gmail.com" Date: Sun, 28 Sep 2025 15:40:24 +1000 Subject: [PATCH 4/4] Update .gitattributes --- .gitattributes | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 6fda518..c30b52f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,8 @@ -* text=auto eol=lf -# These files are blocked from the release.zip creation -.gitignore export-ignore -.gitattributes export-ignore -.github export-ignore -*.pyc export-ignore -*.pyo export-ignore +* text=auto eol=lf +# These files are blocked from the release.zip creation +.gitignore export-ignore +.gitattributes export-ignore +.github export-ignore +.coderabbit.yaml export-ignore +*.pyc export-ignore +*.pyo export-ignore