feat: add Queue Repeat extension#1148
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds a new "Queue Repeat" Spicetify extension: manifest, README, and a single-file IIFE that injects a toggle button into the player UI, captures current + queued tracks, polls the queue for newly added tracks, and re-queues finished tracks to form an infinite loop. ChangesQueue Repeat Extension
Sequence DiagramsequenceDiagram
participant User
participant UI as QR Button
participant Ext as QueueRepeat Ext
participant Player as Spicetify PlayerAPI
participant Queue as Queue/NextTracks
User->>UI: Click toggle
UI->>Ext: toggleQueueRepeat()
alt Enable
Ext->>Player: getAllQueueTracks()
Player-->>Ext: queue URIs
Ext->>Ext: build repeatList & set previousTrackUri
Ext->>Ext: start polling (2s)
Ext-->>UI: set active visual
Ext-->>User: notify enabled
else Disable
Ext->>Ext: clear repeatList, stop polling
Ext-->>UI: set inactive visual
Ext-->>User: notify disabled
end
loop while enabled
Ext->>Player: pollForNewQueueTracks()
Player-->>Ext: newly queued URIs
Ext->>Ext: append deduped URIs & notify
end
Player->>Ext: songchange event
Ext->>Ext: onSongChange()
alt previousTrackUri in repeatList
Ext->>Player: addToQueue(previousTrackUri)
end
Ext->>Ext: update previousTrackUri
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/extensions/queue-repeat/queue-repeat.js (1)
41-41: ⚡ Quick winRemove debug log before merging.
log("Queue raw: " + JSON.stringify(q).slice(0, 200))will spam the browser console for every poll cycle in production (every 2 seconds while the watcher is active).📝 Proposed fix
- log("Queue raw: " + JSON.stringify(q).slice(0, 200)); - const queued = q?.queued ?? [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/extensions/queue-repeat/queue-repeat.js` at line 41, Remove the noisy debug statement log("Queue raw: " + JSON.stringify(q).slice(0, 200)) that runs every poll cycle; either delete this line from the polling/watcher code in queue-repeat.js or wrap it behind a debug/config flag (e.g., only call it when DEBUG or verbose logging is enabled) so production polling no longer spams the console.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/extensions/queue-repeat/queue-repeat.js`:
- Line 95: The forEach callbacks on repeatList implicitly return the result of
log(...) which Biome flags; replace the forEach usage (repeatList.forEach(...))
with a for...of iteration that does not return a value — e.g., iterate over
repeatList.entries() to get index and uri and call log(...) inside the loop;
apply the same change for the other identical forEach occurrence that also calls
log.
- Around line 118-139: pollForNewQueueTracks can run concurrently because
setInterval may call it again before the previous async work (getAllQueueTracks)
finishes, causing duplicate URIs to be pushed into repeatList; add an in-flight
guard (e.g., a module-scoped boolean like isPolling) checked at the top of
pollForNewQueueTracks (return early if true), set to true immediately when
starting the async work and ensure it is reset to false in a finally block so
overlapping executions are prevented; update references to isActive,
getAllQueueTracks, repeatList and repeatSet inside pollForNewQueueTracks only
after acquiring the guard so duplicates cannot be added.
In `@src/extensions/queue-repeat/README.md`:
- Line 1: Update the README.md markdown: add descriptive alt text to the image
reference by replacing the bare `` with an alt text string (e.g.,
``), and add a language specifier of `text`
(or `plaintext`) to the two fenced code blocks that list file paths so they
become ```text fenced blocks; look for the code fences that show
`%APPDATA%\spicetify\Extensions\queue-repeat.js` and the `Extensions/ └──
queue-repeat.js` directory listing and add the `text` language tag to each.
- Around line 69-72: The uninstall instruction in README.md is wrong — update
the uninstall command string "spicetify config extensions queue-repeat.js" to
append a trailing dash so it reads "spicetify config extensions
queue-repeat.js-" (leave the following "spicetify apply" line as-is) so the
extension is actually removed when following the README's uninstall steps.
---
Nitpick comments:
In `@src/extensions/queue-repeat/queue-repeat.js`:
- Line 41: Remove the noisy debug statement log("Queue raw: " +
JSON.stringify(q).slice(0, 200)) that runs every poll cycle; either delete this
line from the polling/watcher code in queue-repeat.js or wrap it behind a
debug/config flag (e.g., only call it when DEBUG or verbose logging is enabled)
so production polling no longer spams the console.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 85a07300-a0d6-4312-914c-96884ab744e2
⛔ Files ignored due to path filters (1)
src/extensions/queue-repeat/Foto.pngis excluded by!**/*.png
📒 Files selected for processing (3)
src/extensions/queue-repeat/README.mdsrc/extensions/queue-repeat/manifest.jsonsrc/extensions/queue-repeat/queue-repeat.js
Add isPolling flag to prevent multiple polling executions.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/extensions/queue-repeat/queue-repeat.js (1)
42-42: 💤 Low valueRemove or gate production debug logging.
getAllQueueTracksis called during activation and every 2 s while active, so this line dumps up to 200 characters of raw queue JSON to the console on every poll cycle. Consider removing it or wrapping it in aconst DEBUG = falseguard.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/extensions/queue-repeat/queue-repeat.js` at line 42, The debug log in getAllQueueTracks ("Queue raw: " + JSON.stringify(q).slice(0, 200)) floods the console on every poll; either remove that statement or wrap it behind a disable-able guard—e.g., add a module-level constant like DEBUG = false and change the call to only log when DEBUG is true (or use an existing extension debug/config flag); update the log use in getAllQueueTracks so production runs omit the JSON dump while keeping the single DEBUG gate for easy toggling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/extensions/queue-repeat/queue-repeat.js`:
- Around line 391-396: When re-injecting the button after DOM removal, the
visual state isn't re-applied; ensure that after calling injectButton() (or
inside injectButton()) you call updateButtonVisual(isActive) so the recreated
element reflects the current isActive state. Locate injectButton(),
makeButton(), and the re-injection block that sets buttonElement = null and
setTimeout(...); update that flow to invoke updateButtonVisual(isActive) once
the new buttonElement exists (or have makeButton initialize from isActive) so
the button doesn't appear "off" when isActive is true.
- Around line 178-180: Replace the noisy call to
Spicetify.Platform.PlayerAPI.addToQueue with the silent API Spicetify.addToQueue
so re-queuing in queue-repeat does not produce "Added to queue" toasts; locate
the call to Spicetify.Platform.PlayerAPI.addToQueue (the try block that awaits
it and then calls log("Re-queued.")) and invoke Spicetify.addToQueue with the
same track identifier(s) (previousTrackUri) instead, keeping the surrounding
error handling and log("Re-queued.") intact.
- Around line 263-272: The toggle button created in makeButton lacks
aria-pressed so screen readers can't detect its on/off state; set an initial
aria-pressed ("false" or based on current repeat state) on the btn created in
makeButton and update that attribute whenever toggleQueueRepeat runs (e.g.,
after awaiting toggleQueueRepeat(), set btn.setAttribute("aria-pressed",
newState) or compute the new state and update the attribute and title
accordingly). Locate makeButton and the click handler that calls
toggleQueueRepeat to add the initial aria-pressed and the post-toggle update so
the DOM always reflects the current state for assistive tech.
- Around line 141-148: The function pollForNewQueueTracks is missing its closing
brace causing subsequent declarations (startQueueWatcher, stopQueueWatcher,
init, and the final IIFE closure) to be nested incorrectly; fix by adding the
missing closing brace and ensuring the finally block is closed properly so
pollForNewQueueTracks ends before the startQueueWatcher function declaration,
then verify the IIFE closure remains balanced (check pollForNewQueueTracks,
startQueueWatcher, stopQueueWatcher, init and the trailing })() are all at
top-level scope).
---
Nitpick comments:
In `@src/extensions/queue-repeat/queue-repeat.js`:
- Line 42: The debug log in getAllQueueTracks ("Queue raw: " +
JSON.stringify(q).slice(0, 200)) floods the console on every poll; either remove
that statement or wrap it behind a disable-able guard—e.g., add a module-level
constant like DEBUG = false and change the call to only log when DEBUG is true
(or use an existing extension debug/config flag); update the log use in
getAllQueueTracks so production runs omit the JSON dump while keeping the single
DEBUG gate for easy toggling.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 4be6b2d1-e85f-4c75-800f-25f60f964be9
📒 Files selected for processing (1)
src/extensions/queue-repeat/queue-repeat.js
| if (buttonElement && !document.contains(buttonElement)) { | ||
| log("Button removed from DOM, re-injecting..."); | ||
| buttonElement = null; | ||
| setTimeout(() => { | ||
| if (!buttonElement) injectButton(); | ||
| }, 500); |
There was a problem hiding this comment.
Re-injected button loses its active visual state.
After the MutationObserver removes and reinserts the button, makeButton() always initialises the button in the "off" state. updateButtonVisual(isActive) is never called post-injection, so if isActive is true at that moment the re-injected button will appear as "off" until the user clicks it.
🐛 Proposed fix
setTimeout(() => {
- if (!buttonElement) injectButton();
+ if (!buttonElement) {
+ injectButton();
+ updateButtonVisual(isActive);
+ }
}, 500);📝 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.
| if (buttonElement && !document.contains(buttonElement)) { | |
| log("Button removed from DOM, re-injecting..."); | |
| buttonElement = null; | |
| setTimeout(() => { | |
| if (!buttonElement) injectButton(); | |
| }, 500); | |
| if (buttonElement && !document.contains(buttonElement)) { | |
| log("Button removed from DOM, re-injecting..."); | |
| buttonElement = null; | |
| setTimeout(() => { | |
| if (!buttonElement) { | |
| injectButton(); | |
| updateButtonVisual(isActive); | |
| } | |
| }, 500); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/extensions/queue-repeat/queue-repeat.js` around lines 391 - 396, When
re-injecting the button after DOM removal, the visual state isn't re-applied;
ensure that after calling injectButton() (or inside injectButton()) you call
updateButtonVisual(isActive) so the recreated element reflects the current
isActive state. Locate injectButton(), makeButton(), and the re-injection block
that sets buttonElement = null and setTimeout(...); update that flow to invoke
updateButtonVisual(isActive) once the new buttonElement exists (or have
makeButton initialize from isActive) so the button doesn't appear "off" when
isActive is true.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/extensions/queue-repeat/queue-repeat.js`:
- Around line 121-141: In pollForNewQueueTracks(), after awaiting
getAllQueueTracks() add a second guard that checks isActive (and optionally
isPolling/session state) before mutating repeatList or showing notifications; if
isActive is false, set isPolling back to false and return early to avoid stale
in-flight polls updating state — update the logic around isActive/isPolling and
repeatList so the await result is ignored when the extension was disabled during
the network call.
- Around line 22-24: The readiness gate is checking
Spicetify.Platform.PlayerAPI.addToQueue but the runtime call uses the global
Spicetify.addToQueue, so the gate can hang if only the global API exists; update
the readiness check to test the same API you call at runtime
(Spicetify.addToQueue) or check both places. Locate the readiness predicate that
references Spicetify?.Platform?.PlayerAPI?.addToQueue (and
Spicetify?.Platform?.PlayerAPI?.getQueue / Spicetify?.Player?.addEventListener)
and replace or augment the check to include Spicetify?.addToQueue (or prefer the
global Spicetify.addToQueue exclusively) so the gate passes when the actual
runtime API is present.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 48737630-8654-4fa8-9f6d-eacdee2a7d5c
📒 Files selected for processing (1)
src/extensions/queue-repeat/queue-repeat.js
| Spicetify?.Platform?.PlayerAPI?.getQueue && | ||
| Spicetify?.Platform?.PlayerAPI?.addToQueue && | ||
| Spicetify?.Player?.addEventListener && |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
types_file="$(fd -i 'spicetify.d.ts' | head -n1)"
if [[ -z "${types_file}" ]]; then
echo "spicetify.d.ts not found"
exit 1
fi
echo "Using types file: ${types_file}"
rg -nC3 'namespace Spicetify|interface PlayerAPI|addToQueue' "${types_file}"Repository: spicetify/marketplace
Length of output: 379
🏁 Script executed:
rg -n 'interface PlayerAPI|class PlayerAPI' src/types/spicetify.d.ts -A 15Repository: spicetify/marketplace
Length of output: 47
🏁 Script executed:
cat -n src/extensions/queue-repeat/queue-repeat.js | sed -n '15,30p'Repository: spicetify/marketplace
Length of output: 760
🏁 Script executed:
cat -n src/extensions/queue-repeat/queue-repeat.js | sed -n '175,185p'Repository: spicetify/marketplace
Length of output: 604
🏁 Script executed:
rg -n 'PlayerAPI' src/types/spicetify.d.ts -B 2 -A 10Repository: spicetify/marketplace
Length of output: 47
🏁 Script executed:
rg -n 'Platform' src/types/spicetify.d.ts -B 1 -A 3Repository: spicetify/marketplace
Length of output: 229
🏁 Script executed:
rg -n 'addToQueue' src/types/spicetify.d.ts -B 3 -A 3Repository: spicetify/marketplace
Length of output: 252
Readiness gate checks a different queue API than the one used at runtime.
Line 23 checks for Spicetify.Platform.PlayerAPI.addToQueue, but the code at line 179 calls Spicetify.addToQueue (the global API). The types file defines only the global addToQueue() and does not type PlayerAPI. If only the global API is available, the readiness check will hang indefinitely. Gate on the API you actually call:
Proposed fix
if (
Spicetify?.Platform?.PlayerAPI?.getQueue &&
- Spicetify?.Platform?.PlayerAPI?.addToQueue &&
+ Spicetify?.addToQueue &&
Spicetify?.Player?.addEventListener &&
Spicetify?.Player?.data !== undefined
) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/extensions/queue-repeat/queue-repeat.js` around lines 22 - 24, The
readiness gate is checking Spicetify.Platform.PlayerAPI.addToQueue but the
runtime call uses the global Spicetify.addToQueue, so the gate can hang if only
the global API exists; update the readiness check to test the same API you call
at runtime (Spicetify.addToQueue) or check both places. Locate the readiness
predicate that references Spicetify?.Platform?.PlayerAPI?.addToQueue (and
Spicetify?.Platform?.PlayerAPI?.getQueue / Spicetify?.Player?.addEventListener)
and replace or augment the check to include Spicetify?.addToQueue (or prefer the
global Spicetify.addToQueue exclusively) so the gate passes when the actual
runtime API is present.
| async function pollForNewQueueTracks() { | ||
| if (!isActive || isPolling) return; | ||
|
|
||
| isPolling = true; | ||
| try { | ||
| const currentQueueUris = await getAllQueueTracks(); | ||
| const repeatSet = new Set(repeatList); | ||
| const newUris = currentQueueUris.filter(uri => !repeatSet.has(uri)); | ||
|
|
||
| if (newUris.length > 0) { | ||
| repeatList.push(...newUris); | ||
| log(`${newUris.length} new track(s) added to repeat list.`); | ||
| for (const uri of newUris) { | ||
| log(` + ${uri}`); | ||
| } | ||
| Spicetify.showNotification( | ||
| `Queue Repeat: ${newUris.length} new track(s) added`, | ||
| false, | ||
| 1800 | ||
| ); | ||
| } |
There was a problem hiding this comment.
Stale in-flight poll can update state after Queue Repeat is turned off.
If disable happens while Line 126 is awaiting getAllQueueTracks(), this invocation still reaches Lines 131-140 and can repopulate repeatList/show notifications after deactivation. Add a second active/session check right after the await and before mutating state.
Proposed fix
async function pollForNewQueueTracks() {
if (!isActive || isPolling) return;
isPolling = true;
try {
const currentQueueUris = await getAllQueueTracks();
+ if (!isActive) return;
+
const repeatSet = new Set(repeatList);
const newUris = currentQueueUris.filter(uri => !repeatSet.has(uri));📝 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.
| async function pollForNewQueueTracks() { | |
| if (!isActive || isPolling) return; | |
| isPolling = true; | |
| try { | |
| const currentQueueUris = await getAllQueueTracks(); | |
| const repeatSet = new Set(repeatList); | |
| const newUris = currentQueueUris.filter(uri => !repeatSet.has(uri)); | |
| if (newUris.length > 0) { | |
| repeatList.push(...newUris); | |
| log(`${newUris.length} new track(s) added to repeat list.`); | |
| for (const uri of newUris) { | |
| log(` + ${uri}`); | |
| } | |
| Spicetify.showNotification( | |
| `Queue Repeat: ${newUris.length} new track(s) added`, | |
| false, | |
| 1800 | |
| ); | |
| } | |
| async function pollForNewQueueTracks() { | |
| if (!isActive || isPolling) return; | |
| isPolling = true; | |
| try { | |
| const currentQueueUris = await getAllQueueTracks(); | |
| if (!isActive) return; | |
| const repeatSet = new Set(repeatList); | |
| const newUris = currentQueueUris.filter(uri => !repeatSet.has(uri)); | |
| if (newUris.length > 0) { | |
| repeatList.push(...newUris); | |
| log(`${newUris.length} new track(s) added to repeat list.`); | |
| for (const uri of newUris) { | |
| log(` + ${uri}`); | |
| } | |
| Spicetify.showNotification( | |
| `Queue Repeat: ${newUris.length} new track(s) added`, | |
| false, | |
| 1800 | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/extensions/queue-repeat/queue-repeat.js` around lines 121 - 141, In
pollForNewQueueTracks(), after awaiting getAllQueueTracks() add a second guard
that checks isActive (and optionally isPolling/session state) before mutating
repeatList or showing notifications; if isActive is false, set isPolling back to
false and return early to avoid stale in-flight polls updating state — update
the logic around isActive/isPolling and repeatList so the await result is
ignored when the extension was disabled during the network call.
|
This repository is not used to host user creations. See https://github.com/spicetify/marketplace/wiki/Publishing-to-Marketplace |
Add a new extension called Queue Repeat. This extension allows users to loop tracks that are added manually to the "Next Up" queue. It includes a toggle button in the player bar and handles dynamic queue updates.
Summary by CodeRabbit
New Features
Documentation
Chores