Skip to content

Update loopyLoop.js to work.#3780

Closed
LukaTheHero wants to merge 1 commit intospicetify:mainfrom
LukaTheHero:patch-1
Closed

Update loopyLoop.js to work.#3780
LukaTheHero wants to merge 1 commit intospicetify:mainfrom
LukaTheHero:patch-1

Conversation

@LukaTheHero
Copy link
Copy Markdown

@LukaTheHero LukaTheHero commented Apr 4, 2026

Summary of Changes

Problems and fixes

Outdated DOM selectors
Added multiple fallback selectors for modern Spotify

addEventListener("onprogress") removed
Replaced with setInterval polling every 200ms

Spicetify.Player.seek(percent) wrong input
Now uses seek(start * duration) (milliseconds)

Context menu styling missing
Added self-contained CSS (no longer relies on Spotify's main-contextMenu-* classes)

position: absolute on context menu
Changed to position: fixed with left/top for reliable positioning

Bar not relatively positioned
Explicitly set bar.style.position = "relative"

It looks good and is futureproof, won't break with an update.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability of progress bar element detection using enhanced fallback strategies
    • Fixed "process bar" wording to "progress bar"
  • Style

    • Enhanced loop marker styling with improved colors, z-index, and positioning
    • Redesigned context menu with better visual appearance and button hover effects
    • Updated loop control labels in the context menu for improved clarity

Summary of Changes

Problems and fixes

Outdated DOM selectors	
Added multiple fallback selectors for modern Spotify

addEventListener("onprogress") removed	
Replaced with setInterval polling every 200ms

Spicetify.Player.seek(percent) wrong input	
Now uses seek(start * duration) (milliseconds)

Context menu styling missing	
Added self-contained CSS (no longer relies on Spotify's main-contextMenu-* classes)

position: absolute on context menu	
Changed to position: fixed with left/top for reliable positioning

Bar not relatively positioned	
Explicitly set bar.style.position = "relative"

It looks good and is futureproof, won't break with an update.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

The loopyLoop extension was refactored to use multi-selector fallback strategies for DOM discovery, replaced deprecated Spicetify event listeners with polling logic, reworked context menu rendering with plain DOM elements, and enhanced CSS styling for loop markers and menu items.

Changes

Cohort / File(s) Summary
Loop Extension Refactoring
Extensions/loopyLoop.js
Updated metadata and version. Replaced single-chain DOM selector with multi-fallback strategy for robustness. Changed readiness gating with updated Spicetify/Player checks. Replaced onprogress event listener with 200ms polling using getProgress()/getDuration()/isPlaying(). Reworked context menu from React-based class dependencies to plain DOM items with position: fixed layout and event listeners. Enhanced CSS for loop markers and menu styling. Updated marker positioning to use percentage-based left values.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A loopy loop hops with glee,
From event-driven to polling spree,
DOM selectors, robust and keen,
Menu styling, refined and clean,
Progress tracked with each 200ms beat! 🎵

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Update loopyLoop.js to work' is vague and generic, using non-descriptive language that does not convey meaningful information about the specific changes made. Provide a more specific title that describes the main change, such as 'Fix loopyLoop.js DOM selectors and replace deprecated progress events with polling' or 'Restore loopyLoop.js functionality with fallback selectors and setInterval polling'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Extensions/loopyLoop.js`:
- Around line 111-129: The current fixed 1000ms debounce (debounceUntil) after
calling Spicetify.Player.seek causes short loops to miss boundary checks;
replace the fixed timeout with a seek-completion guard: when you call
Spicetify.Player.seek(seekTo) set a flag (e.g., isSeeking or record
lastSeekTarget) and in the setInterval loop ignore checks only while the player
is still reaching that seek target, then clear the flag once
Spicetify.Player.getProgress()/percent is within a small epsilon of the seek
target (or when a single interval tick passes), so use that seek-completion
condition instead of debounceUntil = now + 1000; reference debounceUntil,
setInterval, start, end, percent, progress, duration and Spicetify.Player.seek
in your change.
- Around line 149-153: When setting the loop start in the startBtn handler,
clamp start so it can never equal 1 (e.g., start = Math.min(mouseOnBarPercent,
0.9999)) and then handle the ordering: if end === null set end = 0.99, otherwise
if start >= end treat it as the reorder case by swapping start and end instead
of forcing end to 0.99; this prevents start == 1 from causing a seek to duration
and ensures start < end. Reference: startBtn/createMenuItem handler, variables
start, end, and mouseOnBarPercent (and the seek behavior invoked elsewhere).
- Around line 23-29: The initialization guard in LoopyLoop currently only checks
for Spicetify.Player.origin but not Spicetify.Player.origin._state, which allows
the interval to start and later dereference origin._state in
Spicetify.Player.getDuration/getProgress, causing null derefs; update the
startup condition inside LoopyLoop to also require
Spicetify.Player.origin._state (e.g., check Spicetify?.Player?.origin?._state)
before proceeding, preserve the existing document.readyState-based backoff
(setTimeout with 300ms/1000ms) when the state is missing, and only start the
interval/initialization when origin._state is truthy so subsequent calls to
getDuration/getProgress are safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f74c6ca0-7ea2-4225-8012-f6a8c7ceffe4

📥 Commits

Reviewing files that changed from the base of the PR and between 1c77973 and 82070b8.

📒 Files selected for processing (1)
  • Extensions/loopyLoop.js

Comment on lines +23 to +29
if (!(bar && progressContainer && Spicetify?.Player?.origin && Spicetify?.Player?.getDuration)) {
if (document.readyState !== "complete") {
setTimeout(LoopyLoop, 300);
return;
}
setTimeout(LoopyLoop, 1000);
return;
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "spicetifyWrapper.js" | head -5

Repository: spicetify/cli

Length of output: 87


🏁 Script executed:

find . -type f -name "loopyLoop.js" | head -5

Repository: spicetify/cli

Length of output: 82


🏁 Script executed:

find . -type f -name "*.js" | xargs grep -l "getProgress\|getDuration" | head -10

Repository: spicetify/cli

Length of output: 261


🏁 Script executed:

sed -n '1,50p' ./jsHelper/spicetifyWrapper.js

Repository: spicetify/cli

Length of output: 1754


🏁 Script executed:

sed -n '20,40p' ./jsHelper/spicetifyWrapper.js

Repository: spicetify/cli

Length of output: 883


🏁 Script executed:

sed -n '1440,1480p' ./jsHelper/spicetifyWrapper.js

Repository: spicetify/cli

Length of output: 1207


🏁 Script executed:

sed -n '1,80p' ./Extensions/loopyLoop.js

Repository: spicetify/cli

Length of output: 2593


🏁 Script executed:

sed -n '30,150p' ./Extensions/loopyLoop.js | head -100

Repository: spicetify/cli

Length of output: 2821


🏁 Script executed:

grep -n "getDuration\|getProgress" ./Extensions/loopyLoop.js

Repository: spicetify/cli

Length of output: 303


Guard on origin._state before starting the loop machinery.

Line 23 only waits for Spicetify.Player.origin, but getDuration() and getProgress() called at lines 117–118 directly dereference origin._state.duration, origin._state.isPaused, and origin._state.timestamp without checks. The wrapper itself at lines 1450–1475 explicitly gates on origin._state before initializing related APIs. If origin exists but _state is not yet populated, the initialization passes and the interval executes, leaving a null-deref window.

Suggested fix
-    if (!(bar && progressContainer && Spicetify?.Player?.origin && Spicetify?.Player?.getDuration)) {
+    if (!(bar && progressContainer && Spicetify?.Player?.origin?._state && Spicetify?.Player?.getDuration)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!(bar && progressContainer && Spicetify?.Player?.origin && Spicetify?.Player?.getDuration)) {
if (document.readyState !== "complete") {
setTimeout(LoopyLoop, 300);
return;
}
setTimeout(LoopyLoop, 1000);
return;
if (!(bar && progressContainer && Spicetify?.Player?.origin?._state && Spicetify?.Player?.getDuration)) {
if (document.readyState !== "complete") {
setTimeout(LoopyLoop, 300);
return;
}
setTimeout(LoopyLoop, 1000);
return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Extensions/loopyLoop.js` around lines 23 - 29, The initialization guard in
LoopyLoop currently only checks for Spicetify.Player.origin but not
Spicetify.Player.origin._state, which allows the interval to start and later
dereference origin._state in Spicetify.Player.getDuration/getProgress, causing
null derefs; update the startup condition inside LoopyLoop to also require
Spicetify.Player.origin._state (e.g., check Spicetify?.Player?.origin?._state)
before proceeding, preserve the existing document.readyState-based backoff
(setTimeout with 300ms/1000ms) when the state is missing, and only start the
interval/initialization when origin._state is truthy so subsequent calls to
getDuration/getProgress are safe.

Comment on lines +111 to +129
let debounceUntil = 0;
setInterval(() => {
if (start != null && end != null && Spicetify.Player.isPlaying()) {
const now = Date.now();
if (now < debounceUntil) return;

const progress = Spicetify.Player.getProgress(); // ms
const duration = Spicetify.Player.getDuration(); // ms
if (!duration) return;

const percent = progress / duration;

if (percent > end || percent < start - 0.01) {
debounceUntil = now + 1000;
const seekTo = Math.round(start * duration);
Spicetify.Player.seek(seekTo);
}
}
}, 200);
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 | 🟠 Major

The fixed 1s debounce breaks short loops.

Line 124 suppresses boundary checks for a full second after every seek. Any practice loop shorter than that will only wrap once, then playback runs past end unchecked on the next pass. Please replace the fixed 1000 ms mute window with a one-tick or seek-completion guard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Extensions/loopyLoop.js` around lines 111 - 129, The current fixed 1000ms
debounce (debounceUntil) after calling Spicetify.Player.seek causes short loops
to miss boundary checks; replace the fixed timeout with a seek-completion guard:
when you call Spicetify.Player.seek(seekTo) set a flag (e.g., isSeeking or
record lastSeekTarget) and in the setInterval loop ignore checks only while the
player is still reaching that seek target, then clear the flag once
Spicetify.Player.getProgress()/percent is within a small epsilon of the seek
target (or when a single interval tick passes), so use that seek-completion
condition instead of debounceUntil = now + 1000; reference debounceUntil,
setInterval, start, end, percent, progress, duration and Spicetify.Player.seek
in your change.

Comment on lines +149 to +153
const startBtn = createMenuItem("Set loop start", () => {
start = mouseOnBarPercent;
if (end === null || start > end) {
end = 0.99;
}
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

Keep start < end at the 100% boundary.

Line 150 can store start = 1, and Line 152 then sets end = 0.99, leaving an inverted loop window. From there Line 125 seeks to duration instead of a valid loop start. Clamp the start point below 1 and treat start >= end as the reorder case.

Suggested fix
     const startBtn = createMenuItem("Set loop start", () => {
-        start = mouseOnBarPercent;
-        if (end === null || start > end) {
+        start = Math.min(mouseOnBarPercent, 0.98);
+        if (end === null || start >= end) {
             end = 0.99;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Extensions/loopyLoop.js` around lines 149 - 153, When setting the loop start
in the startBtn handler, clamp start so it can never equal 1 (e.g., start =
Math.min(mouseOnBarPercent, 0.9999)) and then handle the ordering: if end ===
null set end = 0.99, otherwise if start >= end treat it as the reorder case by
swapping start and end instead of forcing end to 0.99; this prevents start == 1
from causing a seek to duration and ensures start < end. Reference:
startBtn/createMenuItem handler, variables start, end, and mouseOnBarPercent
(and the seek behavior invoked elsewhere).

@3xyy
Copy link
Copy Markdown
Contributor

3xyy commented Apr 6, 2026

Bruh #3771

@LukaTheHero
Copy link
Copy Markdown
Author

Bruh #3771

I didn't see it. 😂

@rxri rxri closed this Apr 6, 2026
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.

3 participants