Skip to content

Initial implentation on jumpback on seek - #5

Closed
bossanova808 wants to merge 1 commit into
mainfrom
jumpback-on-seek-and-extended-pause
Closed

bossanova808 wants to merge 1 commit into
mainfrom
jumpback-on-seek-and-extended-pause

Conversation

@bossanova808

@bossanova808 bossanova808 commented Feb 16, 2026 •

Copy link
Copy Markdown
Owner

@coderabbitai please review

Summary by CodeRabbit

  • New Features

    • Implemented manual seek jumpback with configurable offset. When users manually seek during playback, content now jumps back by a customizable amount (0–60 seconds) to prevent missing context.
  • Bug Fixes

    • Corrected translation string formatting.

@coderabbitai

coderabbitai Bot commented Feb 16, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request adds a new seek-based jumpback feature to the unpause jumpback addon. It includes translation strings for the feature, a new settings configuration for jumpback seconds after manual seeks, and implements seek event handling with state tracking to prevent loops and throttle rapid seeks.

Changes

Cohort / File(s) Summary
Translation updates
resources/language/resource.language.en_gb/strings.po
Updated hyphenated string "Jump back on un-pause" to "Jump back on unpause" and added new translation entry 32044 for "Jump back after manual seek".
Seek jumpback implementation
resources/lib/unpause_jumpback.py
Added new onPlayBackSeek handler for user-initiated seek events with throttling, state checking, and seek computation logic. Introduced three state attributes (jump_back_secs_after_seek, performing_addon_seek, last_seek_time) to track seek state and prevent loops. Modified four existing methods (load_settings, onPlayBackResumed, onAVStarted, onPlayBackSpeedChanged) to guard internal seeks with try/finally blocks and the performing_addon_seek flag. Added error handling for RuntimeError during seek operations.
Settings configuration
resources/settings.xml
Added new integer setting "jumpbacksecsafterseek" with slider control, range 0–60 seconds, and default value 0 under group 1.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Initial implentation on jumpback on seek' contains a typo ('implentation' should be 'implementation') and is vague with 'Initial'. However, it does refer to the main feature added: jumpback functionality triggered by user-initiated seeks. Consider revising to 'Implement jumpback on user-initiated seek' or similar, correcting the typo and removing the vague 'Initial' qualifier for clarity.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jumpback-on-seek-and-extended-pause

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bossanova808

Copy link
Copy Markdown
Owner Author

Solves #4

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@resources/lib/unpause_jumpback.py`:
- Around line 354-424: The onPlayBackSeek handler improperly uses an f-string
with no placeholders, contains a redundant self.performing_addon_seek reset in
the outer except, and doesn't distinguish small step forwards/backwards from
user scrubs; to fix, change the literal log call in onPlayBackSeek (remove the
unnecessary f-prefix) where the message has no interpolations, remove the
redundant self.performing_addon_seek = False in the outer except (the inner
finally already resets it), and add a small filter at the top of onPlayBackSeek
using the incoming seek_offset parameter (which is in milliseconds) to ignore
tiny step operations (e.g. if abs(seek_offset) < YOUR_STEP_THRESHOLD_MS: log and
return) or add a clarifying comment if steps should be handled—refer to the
onPlayBackSeek function, the self.performing_addon_seek flag,
self.last_seek_time, and the seek_offset parameter to locate and apply these
changes.
🧹 Nitpick comments (1)
resources/lib/unpause_jumpback.py (1)

162-165: The log messages don't mention the new seek jumpback setting.

When settings are loaded, the log only reports the pause-based jumpback mode. Consider also logging jump_back_secs_after_seek so operators can confirm the seek jumpback is active without digging into Kodi's settings UI.

Suggestion
         if self.jump_back_on_resume:
             Logger.info(f'Settings loaded, jump back mode is: On Resume, with a jump back of {self.jump_back_secs_after_pause} seconds')
         else:
             Logger.info(f'Settings loaded, jump back mode is: On Pause, with a jump back of {self.jump_back_secs_after_pause} seconds')
+
+        if self.jump_back_secs_after_seek > 0:
+            Logger.info(f'Seek jumpback enabled: {self.jump_back_secs_after_seek} seconds after manual seek')

Comment on lines +354 to +424
def onPlayBackSeek(self, seek_time, seek_offset):
"""
Handle manual seek operations.

Called when the user manually seeks to a different position in the video.
Performs a jumpback after the seek to help the user catch context, but
only for user-initiated seeks to prevent infinite loops.

The addon sets a flag (performing_addon_seek) before performing its own
seeks to distinguish them from user-initiated seeks.

Args:
seek_time (int): The time in milliseconds to which playback was seeked
seek_offset (int): The offset in milliseconds from the current position
"""
# Ignore seeks performed by this addon itself to prevent loops
if self.performing_addon_seek:
Logger.info(f'onPlayBackSeek: Ignoring addon-initiated seek to {seek_time}ms')
return

Logger.info(f'onPlayBackSeek: User seek detected - time: {seek_time}ms, offset: {seek_offset}ms')

# Only proceed if seek jumpback is configured
if self.jump_back_secs_after_seek == 0:
Logger.debug('Seek jumpback disabled (set to 0 seconds)')
return

try:
_filename = self.getPlayingFile()
except RuntimeError:
Logger.info('No file is playing, could not getPlayingFile(), stopping seek jumpback')
return

if self.is_excluded(_filename):
Logger.info(f"Seek jumpback ignored because '{_filename}' is in exclusion settings.")
return

try:
current_time = self.getTime()

# Don't jump back if we're near the beginning
if current_time <= self.jump_back_secs_after_seek:
Logger.info(f'Current time {current_time}s too close to start, skipping seek jumpback')
return

# Throttle: ignore rapid successive seeks (within 1 second)
current_timestamp = time.time()
if self.last_seek_time > 0 and (current_timestamp - self.last_seek_time) < 1.0:
Logger.info(f'Ignoring rapid seek (within 1 second of last seek)')
self.last_seek_time = current_timestamp
return

self.last_seek_time = current_timestamp

# Perform the jumpback
resume_time = current_time - self.jump_back_secs_after_seek
resume_time = max(0, resume_time) # Don't go below 0

Logger.info(f'Performing seek jumpback: {self.jump_back_secs_after_seek}s back to {resume_time}s')

# Set flag before seeking to prevent loop
self.performing_addon_seek = True
try:
self.seekTime(resume_time)
finally:
# Always reset the flag, even if seek fails
self.performing_addon_seek = False

except RuntimeError as e:
Logger.error(f'Error during seek jumpback: {e}')
self.performing_addon_seek = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Several small issues in onPlayBackSeek.

  1. Line 402 — f-string without placeholders (flagged by Ruff F541): Remove the f prefix since there are no interpolated expressions.

  2. Line 424 — Redundant performing_addon_seek = False: The inner finally block (line 418–420) already guarantees the flag is reset regardless of whether seekTime raises. The outer except reset on line 424 is dead code.

  3. Step forward/back also triggers this callback: Based on learnings, onPlayBackSeek fires for step operations too (not just scrubbing). This means a user pressing "step forward 30s" will immediately get jumped back. If that's unintended, you could filter by seek_offset magnitude or direction. If intentional, a brief comment would help future readers.

Proposed diff for items 1 & 2
             # Throttle: ignore rapid successive seeks (within 1 second)
             current_timestamp = time.time()
             if self.last_seek_time > 0 and (current_timestamp - self.last_seek_time) < 1.0:
-                Logger.info(f'Ignoring rapid seek (within 1 second of last seek)')
+                Logger.info('Ignoring rapid seek (within 1 second of last seek)')
                 self.last_seek_time = current_timestamp
                 return
         except RuntimeError as e:
             Logger.error(f'Error during seek jumpback: {e}')
-            self.performing_addon_seek = False

,

Based on learnings: "In Kodi, the onPlayBackSeek callback does fire for step forward/back operations (not just scrubbing/seeking via progress bar), with the seekOffset parameter indicating the direction and magnitude of the step."

🧰 Tools
🪛 Ruff (0.15.0)

[error] 402-402: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In `@resources/lib/unpause_jumpback.py` around lines 354 - 424, The onPlayBackSeek
handler improperly uses an f-string with no placeholders, contains a redundant
self.performing_addon_seek reset in the outer except, and doesn't distinguish
small step forwards/backwards from user scrubs; to fix, change the literal log
call in onPlayBackSeek (remove the unnecessary f-prefix) where the message has
no interpolations, remove the redundant self.performing_addon_seek = False in
the outer except (the inner finally already resets it), and add a small filter
at the top of onPlayBackSeek using the incoming seek_offset parameter (which is
in milliseconds) to ignore tiny step operations (e.g. if abs(seek_offset) <
YOUR_STEP_THRESHOLD_MS: log and return) or add a clarifying comment if steps
should be handled—refer to the onPlayBackSeek function, the
self.performing_addon_seek flag, self.last_seek_time, and the seek_offset
parameter to locate and apply these changes.

@bossanova808

Copy link
Copy Markdown
Owner Author

@coderabbitai currently there are two options for the jumpback on pause - either at pause, or at resume. I'd like to also offer a third option, that does the jumpback after a configurable delay (still doing the jumpback at resume if the delay has not been reached when the user resumes). Can you help with this.

Also - a reminder, NO DIFFS!! Only before/after blocks

@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@bossanova808, I can help you implement a delayed jumpback option for pause! Let me first read the current implementation to understand how the pause/resume jumpback modes work.


🏁 Script executed:

#!/bin/bash
# Read the main implementation file to understand current jumpback logic
cat resources/lib/unpause_jumpback.py

Length of output: 22363


Implementation Plan: Third Jumpback Mode with Delayed Timer and Resume Fallback

I'll implement a third mode that starts a timer when paused, performs the jumpback after the delay expires (while still paused), but if the user resumes before the delay expires, it performs the jumpback at resume instead.

Changes needed:

  1. Change jump_back_on_resume from boolean to integer (0, 1, 2) for three modes
  2. Add tracking variable to prevent double jumpback
  3. Update settings.xml with dropdown for mode selection
  4. Update strings.po with new labels
  5. Modify onPlayBackPaused() to handle new mode
  6. Modify onPlayBackResumed() to handle new mode
  7. Update load_settings() to handle new mode
  8. Update the AlarmClock callback to mark when jumpback was performed during pause

🧠 Learnings used
Learnt from: bossanova808
Repo: bossanova808/script.xbmc.unpausejumpback PR: 0
File: :0-0
Timestamp: 2025-09-24T06:13:39.469Z
Learning: User bossanova808 prefers before/after code blocks over diffs for code changes, explicitly stating "I never want to see diffs!!!" - always present code changes in clean before/after format for easy copy-pasting.

Learnt from: bossanova808
Repo: bossanova808/script.xbmc.unpausejumpback PR: 0
File: :0-0
Timestamp: 2025-09-24T05:08:29.830Z
Learning: For the repository script.xbmc.unpausejumpback, ignore the file addon-checker.yaml in reviews unless the user bossanova808 specifically requests otherwise.

💡 Use @coderabbitai plan to create a plan for code changes

@bossanova808

Copy link
Copy Markdown
Owner Author

@coderabbitai please generate those changes

@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@bossanova808, I'll generate those changes now!

Task: Implement Third Jumpback Mode with Delayed Timer and Resume Fallback

This will add a third mode that starts a timer when paused, performs the jumpback after the delay expires (while still paused), but if the user resumes before the delay expires, it performs the jumpback at resume instead.

Changes overview:

  • Change jumpback mode from boolean to integer (0, 1, 2) for three modes
  • Add tracking to prevent double jumpback
  • Update settings.xml with dropdown for mode selection
  • Update strings.po with new labels
  • Modify onPlayBackPaused() to handle new mode
  • Modify onPlayBackResumed() to handle new mode
  • Update load_settings() to handle new mode

🧠 Learnings used
Learnt from: bossanova808
Repo: bossanova808/script.xbmc.unpausejumpback PR: 0
File: :0-0
Timestamp: 2025-09-24T05:08:29.830Z
Learning: For the repository script.xbmc.unpausejumpback, ignore the file addon-checker.yaml in reviews unless the user bossanova808 specifically requests otherwise.

Learnt from: bossanova808
Repo: bossanova808/script.xbmc.unpausejumpback PR: 0
File: :0-0
Timestamp: 2025-09-24T06:13:39.469Z
Learning: User bossanova808 prefers before/after code blocks over diffs for code changes, explicitly stating "I never want to see diffs!!!" - always present code changes in clean before/after format for easy copy-pasting.

💡 Use @coderabbitai plan to create a plan for code changes

@bossanova808

Copy link
Copy Markdown
Owner Author

@coderabbitai you just re-presented the plan, not the actual code changes. Please generate the requited code changed and present them here as before/after blocks.

@coderabbitai

coderabbitai Bot commented Feb 16, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@bossanova808, you're right - let me read the current files and provide the actual complete code changes in before/after format!


🏁 Script executed:

#!/bin/bash
# Read the three files that need modifications
echo "=== strings.po ==="
cat resources/language/resource.language.en_gb/strings.po

echo -e "\n=== settings.xml ==="
cat resources/settings.xml

echo -e "\n=== unpause_jumpback.py (relevant sections) ==="
# Get the __init__ method
echo "--- __init__ method ---"
sed -n '/def __init__/,/def [a-z]/p' resources/lib/unpause_jumpback.py | head -n -1

echo -e "\n--- load_settings method ---"
sed -n '/def load_settings/,/def [a-z]/p' resources/lib/unpause_jumpback.py | head -n -1

echo -e "\n--- onPlayBackPaused method ---"
sed -n '/def onPlayBackPaused/,/def [a-z]/p' resources/lib/unpause_jumpback.py | head -n -1

echo -e "\n--- onPlayBackResumed method ---"
sed -n '/def onPlayBackResumed/,/def [a-z]/p' resources/lib/unpause_jumpback.py | head -n -1

Length of output: 19551


@bossanova808, here are the complete code changes in before/after format!


Change 1: Update strings.po

Add new string labels for the jumpback mode selector.

File: resources/language/resource.language.en_gb/strings.po

BEFORE (lines 14-20):

msgctxt "#32000"
msgid "Jump back on unpause"
msgstr ""

msgctxt "#32001"
msgid "Jump back how many seconds?"
msgstr ""

AFTER:

msgctxt "#32000"
msgid "Jump back on unpause"
msgstr ""

msgctxt "#32001"
msgid "Jumpback Mode"
msgstr ""

msgctxt "#32002"
msgid "On Resume (default)"
msgstr ""

msgctxt "#32003"
msgid "During Pause (low-power systems)"
msgstr ""

msgctxt "#32004"
msgid "Delayed with Resume Fallback"
msgstr ""

msgctxt "#32005"
msgid "Jump back how many seconds?"
msgstr ""

Note: The old string 32001 "Jump back how many seconds?" becomes 32005, and we insert the new mode-related strings at 32001-32004.


Change 2: Update settings.xml

Replace the boolean jumpbackonresume setting with an integer dropdown jumpbackmode setting. Also update the label reference for jumpbacksecs from 32001 to 32005.

File: resources/settings.xml

BEFORE (lines 7-29):

			<group id="1">
				<setting help="" id="jumpbacksecs" label="32001" type="integer">
					<level>0</level>
					<default>4</default>
					<constraints>
						<minimum>0</minimum>
						<maximum>60</maximum>
					</constraints>
					<control format="integer" type="slider">
						<popup>false</popup>
					</control>
				</setting>

AFTER:

			<group id="1">
				<setting help="" id="jumpbacksecs" label="32005" type="integer">
					<level>0</level>
					<default>4</default>
					<constraints>
						<minimum>0</minimum>
						<maximum>60</maximum>
					</constraints>
					<control format="integer" type="slider">
						<popup>false</popup>
					</control>
				</setting>

BEFORE (lines 51-57 in group 3):

			<group id="3" label="32042">
				<setting help="" id="jumpbackonresume" label="32040" type="boolean">
					<level>0</level>
					<default>true</default>
					<control type="toggle"/>
				</setting>
			</group>

AFTER:

			<group id="3" label="32042">
				<setting help="" id="jumpbackmode" label="32001" type="integer">
					<level>0</level>
					<default>0</default>
					<constraints>
						<options>
							<option label="32002">0</option>
							<option label="32003">1</option>
							<option label="32004">2</option>
						</options>
					</constraints>
					<control type="spinner"/>
				</setting>
			</group>

Change 3: Update unpause_jumpback.py - init() method

Replace jump_back_on_resume boolean with jumpback_mode integer and add tracking variable.

File: resources/lib/unpause_jumpback.py

BEFORE (lines 118-125):

        # Jumpback behavior settings
        self.jump_back_on_resume = False
        self.jump_back_on_playback_started = False
        self.paused_time = 0
        self.jump_back_secs_after_pause = 0
        self.last_playback_speed = 0
        self.wait_for_jumpback = 0

AFTER:

        # Jumpback behavior settings
        self.jumpback_mode = 0  # 0=on resume, 1=during pause, 2=delayed with fallback
        self.jump_back_on_playback_started = False
        self.paused_time = 0
        self.jump_back_secs_after_pause = 0
        self.jumpback_performed_during_pause = False  # Track if jumpback already done during pause
        self.last_playback_speed = 0
        self.wait_for_jumpback = 0

Change 4: Update unpause_jumpback.py - load_settings() method

Update to load the new mode setting and log it appropriately.

File: resources/lib/unpause_jumpback.py

BEFORE (lines 175-200):

    def load_settings(self):
        """
        Load and cache settings from Kodi's add-on settings.

        Retrieves all jumpback configuration options, exclusion settings,
        and timing parameters from the add-on settings and caches them
        for performance during playback events.

        Logs the current jumpback mode after loading settings.
        """
        self.jump_back_on_resume = get_setting_as_bool('jumpbackonresume')
        self.jump_back_on_playback_started = get_setting_as_bool('jumpbackonplaybackstarted')
        self.jump_back_secs_after_pause = _get_int_setting("jumpbacksecs", default=0)
        self.jump_back_secs_after_fwd_x2 = _get_int_setting("jumpbacksecsfwdx2", default=0)
        self.jump_back_secs_after_fwd_x4 = _get_int_setting("jumpbacksecsfwdx4", default=0)
        self.jump_back_secs_after_fwd_x8 = _get_int_setting("jumpbacksecsfwdx8", default=0)
        self.jump_back_secs_after_fwd_x16 = _get_int_setting("jumpbacksecsfwdx16", default=0)
        self.jump_back_secs_after_fwd_x32 = _get_int_setting("jumpbacksecsfwdx32", default=0)
        self.jump_back_secs_after_rwd_x2 = _get_int_setting("jumpbacksecsrwdx2", default=0)
        self.jump_back_secs_after_rwd_x4 = _get_int_setting("jumpbacksecsrwdx4", default=0)
        self.jump_back_secs_after_rwd_x8 = _get_int_setting("jumpbacksecsrwdx8", default=0)
        self.jump_back_secs_after_rwd_x16 = _get_int_setting("jumpbacksecsrwdx16", default=0)
        self.jump_back_secs_after_rwd_x32 = _get_int_setting("jumpbacksecsrwdx32", default=0)
        self.jump_back_secs_after_seek = _get_int_setting("jumpbacksecsafterseek", default=0)
        self.wait_for_jumpback = _get_int_setting("waitforjumpback", default=0)
        self.exclude_live_tv = get_setting_as_bool('ExcludeLiveTV')
        self.exclude_http = get_setting_as_bool('ExcludeHTTP')
        self.excluded_path_1_enabled = get_setting_as_bool('ExcludePathOption')
        self.excluded_path_2_enabled = get_setting_as_bool('ExcludePathOption2')
        self.excluded_path_3_enabled = get_setting_as_bool('ExcludePathOption3')
        self.excluded_path_1 = get_setting('ExcludePath')
        self.excluded_path_2 = get_setting('ExcludePath2')
        self.excluded_path_3 = get_setting('ExcludePath3')

        if self.jump_back_on_resume:
            Logger.info(f'Settings loaded, jump back mode is: On Resume, with a jump back of {self.jump_back_secs_after_pause} seconds')
        else:
            Logger.info(f'Settings loaded, jump back mode is: On Pause, with a jump back of {self.jump_back_secs_after_pause} seconds')

AFTER:

    def load_settings(self):
        """
        Load and cache settings from Kodi's add-on settings.

        Retrieves all jumpback configuration options, exclusion settings,
        and timing parameters from the add-on settings and caches them
        for performance during playback events.

        Logs the current jumpback mode after loading settings.
        """
        self.jumpback_mode = _get_int_setting('jumpbackmode', default=0)
        self.jump_back_on_playback_started = get_setting_as_bool('jumpbackonplaybackstarted')
        self.jump_back_secs_after_pause = _get_int_setting("jumpbacksecs", default=0)
        self.jump_back_secs_after_fwd_x2 = _get_int_setting("jumpbacksecsfwdx2", default=0)
        self.jump_back_secs_after_fwd_x4 = _get_int_setting("jumpbacksecsfwdx4", default=0)
        self.jump_back_secs_after_fwd_x8 = _get_int_setting("jumpbacksecsfwdx8", default=0)
        self.jump_back_secs_after_fwd_x16 = _get_int_setting("jumpbacksecsfwdx16", default=0)
        self.jump_back_secs_after_fwd_x32 = _get_int_setting("jumpbacksecsfwdx32", default=0)
        self.jump_back_secs_after_rwd_x2 = _get_int_setting("jumpbacksecsrwdx2", default=0)
        self.jump_back_secs_after_rwd_x4 = _get_int_setting("jumpbacksecsrwdx4", default=0)
        self.jump_back_secs_after_rwd_x8 = _get_int_setting("jumpbacksecsrwdx8", default=0)
        self.jump_back_secs_after_rwd_x16 = _get_int_setting("jumpbacksecsrwdx16", default=0)
        self.jump_back_secs_after_rwd_x32 = _get_int_setting("jumpbacksecsrwdx32", default=0)
        self.jump_back_secs_after_seek = _get_int_setting("jumpbacksecsafterseek", default=0)
        self.wait_for_jumpback = _get_int_setting("waitforjumpback", default=0)
        self.exclude_live_tv = get_setting_as_bool('ExcludeLiveTV')
        self.exclude_http = get_setting_as_bool('ExcludeHTTP')
        self.excluded_path_1_enabled = get_setting_as_bool('ExcludePathOption')
        self.excluded_path_2_enabled = get_setting_as_bool('ExcludePathOption2')
        self.excluded_path_3_enabled = get_setting_as_bool('ExcludePathOption3')
        self.excluded_path_1 = get_setting('ExcludePath')
        self.excluded_path_2 = get_setting('ExcludePath2')
        self.excluded_path_3 = get_setting('ExcludePath3')

        mode_names = {
            0: "On Resume (default)",
            1: "During Pause (low-power systems)",
            2: "Delayed with Resume Fallback"
        }
        mode_name = mode_names.get(self.jumpback_mode, "Unknown")
        Logger.info(f'Settings loaded, jump back mode is: {mode_name}, with a jump back of {self.jump_back_secs_after_pause} seconds')

Change 5: Update unpause_jumpback.py - onPlayBackPaused() method

Handle all three modes including the new delayed fallback mode.

File: resources/lib/unpause_jumpback.py

BEFORE (entire method):

    def onPlayBackPaused(self):
        """
        Handle playback pause events (alternative jumpback mode for low-power systems).

        Called when playback is paused. Records the pause time and optionally
        sets up alarm-based jumpback during the pause period.

        For low-power systems, the add-on can be configured to perform the jumpback
        during the pause period, which prevents jankiness on resume but has the
        disadvantage that the paused image also jumps back.

        Checks exclusion settings and sets up delayed jumpback via Kodi's AlarmClock
        if jump_back_on_resume is disabled.
        """
        # Record when the pause was done
        self.paused_time = time.time()
        Logger.info(f'onPlayBackPaused. Time: {self.paused_time}')

        try:
            _filename = self.getPlayingFile()
        except RuntimeError:
            Logger.info('No file is playing, could not getPlayingFile(), stopping UnpauseJumpBack')
            xbmc.executebuiltin('CancelAlarm(JumpbackPaused, true)')
            return

        if self.is_excluded(_filename):
            Logger.info(f'Playback paused - ignoring because [{_filename}] is in exclusion settings.')
            return

        # For low power systems, perform jumpback during pause period
        # Prevents janky resume experience but paused image also jumps back
        if not self.jump_back_on_resume and self.isPlayingVideo() and 0 < self.jump_back_secs_after_pause < self.getTime():
            jump_back_point = self.getTime() - self.jump_back_secs_after_pause
            Logger.info(f'Playback paused - jumping back {self.jump_back_secs_after_pause}s to: {int(jump_back_point)} seconds')
            xbmc.executebuiltin(
                    f'AlarmClock(JumpbackPaused, Seek(-{self.jump_back_secs_after_pause}), 00:00:{int(self.wait_for_jumpback):02d}, silent), silent)')

AFTER:

    def onPlayBackPaused(self):
        """
        Handle playback pause events.

        Called when playback is paused. Records the pause time and handles jumpback
        based on the configured mode:
        
        - Mode 0 (On Resume): Just records pause time, jumpback happens on resume
        - Mode 1 (During Pause): Sets alarm to perform jumpback during pause period
        - Mode 2 (Delayed Fallback): Sets alarm to perform jumpback during pause,
          but if user resumes early, jumpback will happen at resume instead

        Checks exclusion settings before setting up any jumpback operations.
        """
        # Record when the pause was done and reset jumpback tracking
        self.paused_time = time.time()
        self.jumpback_performed_during_pause = False
        Logger.info(f'onPlayBackPaused. Time: {self.paused_time}, Mode: {self.jumpback_mode}')

        try:
            _filename = self.getPlayingFile()
        except RuntimeError:
            Logger.info('No file is playing, could not getPlayingFile(), stopping UnpauseJumpBack')
            xbmc.executebuiltin('CancelAlarm(JumpbackPaused, true)')
            return

        if self.is_excluded(_filename):
            Logger.info(f'Playback paused - ignoring because [{_filename}] is in exclusion settings.')
            return

        # Mode 0: On Resume - just record the pause time, jumpback happens on resume
        if self.jumpback_mode == 0:
            Logger.info('Mode 0 (On Resume): Waiting for resume to perform jumpback')
            return

        # Mode 1 (During Pause) or Mode 2 (Delayed Fallback): Set alarm for jumpback during pause
        if (self.jumpback_mode in [1, 2] and self.isPlayingVideo() 
                and 0 < self.jump_back_secs_after_pause < self.getTime()):
            jump_back_point = self.getTime() - self.jump_back_secs_after_pause
            
            if self.jumpback_mode == 1:
                Logger.info(f'Mode 1 (During Pause): Setting alarm to jump back {self.jump_back_secs_after_pause}s '
                           f'to {int(jump_back_point)}s after {self.wait_for_jumpback}s delay')
            else:  # Mode 2
                Logger.info(f'Mode 2 (Delayed Fallback): Setting alarm to jump back {self.jump_back_secs_after_pause}s '
                           f'to {int(jump_back_point)}s after {self.wait_for_jumpback}s delay (will fallback to resume if resumed early)')
            
            # Use SetProperty to mark that jumpback was performed during pause
            xbmc.executebuiltin(
                f'AlarmClock(JumpbackPaused, '
                f'Seek(-{self.jump_back_secs_after_pause})||SetProperty(jumpback_done,true,home), '
                f'00:00:{int(self.wait_for_jumpback):02d}, silent)')

Change 6: Update unpause_jumpback.py - onPlayBackResumed() method

Handle all three modes with proper checking for jumpback already performed during pause.

File: resources/lib/unpause_jumpback.py

BEFORE (entire method):

    def onPlayBackResumed(self):
        """
        Handle playback resume events (default jumpback mode).

        Called when playback is resumed after being paused. This is the default
        behavior where the pause position remains where the user actually paused,
        which is usually the desired behavior.

        When jump_back_on_resume is enabled:
        - Checks exclusion settings for the current file
        - Performs jumpback if conditions are met (sufficient pause time, etc.)
        - Resets the paused_time tracking variable

        When jump_back_on_resume is disabled:
        - Cancels any pending alarm-based jumpback operations
        """
        Logger.info(f'onPlayBackResumed with jump_back_on_resume: {self.jump_back_on_resume}')

        if self.jump_back_on_resume:

            if self.paused_time > 0:
                Logger.info(f'Was paused for {int(time.time() - self.paused_time)} seconds.')

            # Check for exclusion
            try:
                _filename = self.getPlayingFile()
            except RuntimeError:
                Logger.info('No file is playing, could not getPlayingFile(), stopping UnpauseJumpBack')
                xbmc.executebuiltin('CancelAlarm(JumpbackPaused, true)')
                return

            if self.is_excluded(_filename):
                Logger.info(f"Ignored because '{_filename}' is in exclusion settings.")
                return

            else:
                # Handle jump back after pause
                current_time = self.getTime()
                if (self.jump_back_secs_after_pause != 0
                        and self.isPlayingVideo()
                        and current_time > self.jump_back_secs_after_pause
                        and self.paused_time > 0
                        and (time.time() - self.paused_time) > self.wait_for_jumpback):
                    resume_time = current_time - self.jump_back_secs_after_pause
                    self.performing_addon_seek = True
                    try:
                        self.seekTime(resume_time)
                        Logger.info(f'Resumed, with {int(self.jump_back_secs_after_pause)}s jump back')
                    finally:
                        self.performing_addon_seek = False

                self.paused_time = 0

        # If we're not jumping back on resume, cancel any alarm set for manual resume
        else:
            Logger.info('Cancelling alarm - playback either resumed or stopped by the user.')
            xbmc.executebuiltin('CancelAlarm(JumpbackPaused, true)')

AFTER:

    def onPlayBackResumed(self):
        """
        Handle playback resume events.

        Called when playback is resumed after being paused. Behavior depends on mode:
        
        - Mode 0 (On Resume): Performs jumpback when resuming (default behavior)
        - Mode 1 (During Pause): Cancels any pending alarm (jumpback already done or cancelled)
        - Mode 2 (Delayed Fallback): Checks if jumpback was done during pause via alarm;
          if not (user resumed before delay), performs jumpback now at resume

        Checks exclusion settings and validates conditions before performing jumpback.
        """
        pause_duration = int(time.time() - self.paused_time) if self.paused_time > 0 else 0
        Logger.info(f'onPlayBackResumed. Mode: {self.jumpback_mode}, Pause duration: {pause_duration}s')

        # Check if jumpback was performed during pause (by the alarm)
        jumpback_done_during_pause = xbmc.getInfoLabel('Window(Home).Property(jumpback_done)') == 'true'
        if jumpback_done_during_pause:
            Logger.info('Jumpback was already performed during pause by alarm, clearing flag')
            xbmc.executebuiltin('ClearProperty(jumpback_done,home)')
            self.jumpback_performed_during_pause = True

        # Always cancel any pending alarm on resume
        xbmc.executebuiltin('CancelAlarm(JumpbackPaused, true)')

        # Check for exclusion
        try:
            _filename = self.getPlayingFile()
        except RuntimeError:
            Logger.info('No file is playing, could not getPlayingFile(), stopping UnpauseJumpBack')
            self.paused_time = 0
            return

        if self.is_excluded(_filename):
            Logger.info(f"Ignored because '{_filename}' is in exclusion settings.")
            self.paused_time = 0
            return

        # Mode 0: On Resume - always perform jumpback at resume
        if self.jumpback_mode == 0:
            current_time = self.getTime()
            if (self.jump_back_secs_after_pause != 0
                    and self.isPlayingVideo()
                    and current_time > self.jump_back_secs_after_pause
                    and self.paused_time > 0
                    and pause_duration > self.wait_for_jumpback):
                resume_time = current_time - self.jump_back_secs_after_pause
                self.performing_addon_seek = True
                try:
                    self.seekTime(resume_time)
                    Logger.info(f'Mode 0 (On Resume): Performed {int(self.jump_back_secs_after_pause)}s jump back to {int(resume_time)}s')
                finally:
                    self.performing_addon_seek = False

        # Mode 1: During Pause - jumpback already done or cancelled, just log
        elif self.jumpback_mode == 1:
            Logger.info('Mode 1 (During Pause): Resume detected, alarm cancelled')

        # Mode 2: Delayed Fallback - perform jumpback if it wasn't done during pause
        elif self.jumpback_mode == 2:
            if self.jumpback_performed_during_pause:
                Logger.info('Mode 2 (Delayed Fallback): Jumpback was already done during pause, skipping')
            else:
                # User resumed before the delay - perform jumpback now
                current_time = self.getTime()
                if (self.jump_back_secs_after_pause != 0
                        and self.isPlayingVideo()
                        and current_time > self.jump_back_secs_after_pause
                        and self.paused_time > 0):
                    resume_time = current_time - self.jump_back_secs_after_pause
                    self.performing_addon_seek = True
                    try:
                        self.seekTime(resume_time)
                        Logger.info(f'Mode 2 (Delayed Fallback): User resumed before delay, '
                                   f'performed {int(self.jump_back_secs_after_pause)}s jump back at resume to {int(resume_time)}s')
                    finally:
                        self.performing_addon_seek = False

        self.paused_time = 0
        self.jumpback_performed_during_pause = False

That's all the changes! These implement the third jumpback mode with delayed timer and resume fallback as requested.


🧠 Learnings used
Learnt from: bossanova808
Repo: bossanova808/script.xbmc.unpausejumpback PR: 0
File: :0-0
Timestamp: 2025-09-24T05:08:29.830Z
Learning: For the repository script.xbmc.unpausejumpback, ignore the file addon-checker.yaml in reviews unless the user bossanova808 specifically requests otherwise.

Learnt from: bossanova808
Repo: bossanova808/script.xbmc.unpausejumpback PR: 0
File: :0-0
Timestamp: 2025-09-24T06:13:39.469Z
Learning: User bossanova808 prefers before/after code blocks over diffs for code changes, explicitly stating "I never want to see diffs!!!" - always present code changes in clean before/after format for easy copy-pasting.

@bossanova808

Copy link
Copy Markdown
Owner Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant