Skip to content

API Refactor HTMX/JSON for (almost) all endpoints#163

Closed
contre95 wants to merge 27 commits into
mainfrom
dev
Closed

API Refactor HTMX/JSON for (almost) all endpoints#163
contre95 wants to merge 27 commits into
mainfrom
dev

Conversation

@contre95

@contre95 contre95 commented May 21, 2026

Copy link
Copy Markdown
Owner

wip

Summary by CodeRabbit

  • Documentation

    • Added a comprehensive API reference describing endpoints, response formats, HTMX vs API negotiation, and streaming behavior.
  • New Features

    • Audio streaming endpoint with range support and JSON resource negotiation.
    • In-page audio players for import/queue items (including duplicate comparisons).
  • Improvements

    • Unified routes (removed legacy /ui prefixes) and standardized HTMX/API responses and toasts.
    • Updated templates, polling intervals, forms (hx-put), and client scripts to use new routes and audio controls.

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ed1de9ed-a63b-4901-b21b-6bce298df5fa

📥 Commits

Reviewing files that changed from the base of the PR and between 695d6ac and 0934fa1.

📒 Files selected for processing (2)
  • docs/api.md
  • views/partials/sidebar.html

📝 Walkthrough

Walkthrough

Adds a hosting/respond helper and migrates handlers across features to HTMX-aware respond.* helpers, moves routes off the /ui prefix to root-level grouped paths, updates templates accordingly, adds a streaming feature, and creates docs/api.md documenting endpoint behaviors.

Changes

Respond helpers and feature refactoring

Layer / File(s) Summary
Respond helper package foundation
src/features/hosting/respond/respond.go
New package providing Section, Partial, Text, ToastErr, ToastOk, ToastJob, Resource, and HTMX helpers that conditionally render HTMX templates or return JSON based on the HX-Request header.
Config feature: handlers, routes, and templates
src/features/config/handlers.go, src/features/config/routes.go, views/config/config_form.html, views/sections/settings.html
Config handlers refactored to use respond helpers; RenderSettingsSection uses respond.Section, GetConfigForm uses respond.Partial, DownloadDatabase uses respond.Resource; routes moved from /ui/settings to root-level /settings and /config/form; config form changed from POST /settings/update to PUT /settings.
Downloading feature: handlers, routes, and templates
src/features/downloading/handlers.go, src/features/downloading/routes.go, views/sections/download.html
Downloading handlers refactored to use respond helpers: SearchAlbums/SearchTracks return respond.Partial, download endpoints return respond.ToastJob on success and respond.ToastErr on failure, GetAlbumTracks/GetChartTracks/GetUserInfo return respond.Partial; routes moved from /ui/download to /downloads group; template updated to fetch from /downloads/chart/tracks.
Importing and Jobs features: handlers, routes, and templates
src/features/importing/handlers.go, src/features/importing/routes.go, src/features/jobs/handlers.go, src/features/jobs/routes.go, views/importing/queue_header.html, views/partials/job_status.html, views/sections/jobs.html
Importing handlers refactored to use respond helpers for sections, partials, and toasts; ImportDirectory and ProcessQueueGroup return respond.ToastJob/ToastOk with consolidated HX-Trigger headers; Jobs handlers similarly refactored with HandleStartJob returning respond.ToastJob; routes moved to /import and /jobs groups; templates updated with new endpoints.
Library and Lyrics features: handlers, routes, and templates
src/features/library/handlers.go, src/features/library/routes.go, src/features/lyrics/handlers.go, src/features/lyrics/routes.go, views/library/track_overview_panel.html, views/library/unified_search_list.html, views/lyrics/queue_header.html, views/lyrics/queue_items.html, views/tag/lyrics_buttons.html
Library handlers refactored: RenderLibrarySection uses respond.Section, count endpoints return respond.Text, GetUnifiedSearch returns respond.Partial; Lyrics handlers refactored: GetLyricsProviders replaces RenderLyricsButtons with JSON/partial branching, queue endpoints use respond.Partial/respond.Text; routes moved to root-level /library and /tag groups; templates updated with new artwork and lyrics provider endpoints.
Metadata, Metrics, and Playlists features: handlers, routes, and templates
src/features/metadata/handlers.go, src/features/metadata/routes.go, src/features/metrics/handlers.go, src/features/metrics/routes.go, src/features/playlists/handlers.go, src/features/playlists/routes.go, views/metrics/overview.html, views/partials/main.html, views/playlists/add_to_playlist_modal.html, views/playlists/playlist.html, views/tag/edit_form.html, views/tag/metadata_buttons.html, views/tag/search_results_modal.html
Metadata handlers refactored: RenderTagEditor/FetchFromProvider use respond.Section, ServeArtwork uses respond.Resource, UpdateTags uses respond.ToastErr/ToastOk, StartAcoustIDAnalysis uses respond.ToastJob; Metrics handlers refactored to always use respond.Partial for charts; Playlists handlers refactored: RenderPlaylistsSection uses respond.Section, create/update/delete use respond.ToastErr/ToastOk, ExportM3U uses respond.Resource; routes moved to /tag, /metrics, and /playlists groups; templates updated with new metadata/lyrics/playlist endpoints.
Reorganize and UI features: handlers, routes, and templates
src/features/reorganize/handlers.go, src/features/reorganize/routes.go, src/features/ui/handlers.go, src/features/ui/routes.go, views/cards/quick_actions.html, views/partials/navbar.html, views/partials/sidebar.html, views/sections/analyze.html, views/sections/analyze_files.html, views/sections/analyze_lyrics.html, views/sections/analyze_metadata.html, views/sections/dashboard.html, views/sections/import.html, views/sections/library.html, views/sections/playlists.html, views/sections/tag.html
Reorganize handlers refactored: StartReorganizeAnalysis/RenderFilesReorganizationSection use respond.ToastJob/Section; UI handlers refactored to use respond.Section/Partial; routes moved to root-level /dashboard, /analyze, /analyze/files paths; navbar/sidebar templates and all section templates updated to remove /ui prefix and use new root-level paths for navigation and dynamic content loading.
Streaming feature
src/features/streaming/service.go, src/features/streaming/handlers.go, src/features/streaming/routes.go, src/features/hosting/server.go, src/main.go
New streaming feature: Service.Stream validates and resolves requested paths against configured library/download directories (symlink-aware), maps extensions to audio MIME types, and returns path+MIME; Handler.Stream streams resolved files at GET /stream; server wiring adds streamingService and template helpers.
Templates & client JS: audio players and HTMX endpoints
views/importing/queue_items.html, views/importing/queue_items_grouped_*, views/partials/scripts.html, various navbar/sidebar/section templates
Adds per-item audio players in importing queue templates and client-side audio player helpers; updates HTMX endpoints across templates to remove /ui prefix and target new routes; adjusts polling intervals.
API reference documentation
docs/api.md
New consolidated API reference documenting all endpoints grouped by feature area (UI, Config, Library, Tag/Metadata, Importing, Jobs, Downloading, Lyrics, Playlists, Reorganize, Metrics), including HTTP method, path, HTMX behavior (section/partial/resource/text/toast/job), and response format for both HTMX and API clients.

Sequence Diagram

sequenceDiagram
  participant Client
  participant StreamHandler
  participant StreamingService
  participant Filesystem
  Client->>StreamHandler: GET /stream?path=<encoded>
  StreamHandler->>StreamingService: Stream(decodedPath)
  StreamingService->>Filesystem: resolve & validate path
  Filesystem-->>StreamingService: resolved path + mime
  StreamingService-->>StreamHandler: path + mime
  alt Accept: application/json
    StreamHandler->>Client: { "type", "url" } JSON
  else
    StreamHandler->>Client: audio bytes (stream)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels: new feature

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
views/importing/queue_header.html (1)

17-26: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add type attribute to button elements.

HTML5 requires the type attribute on <button> elements. Without it, buttons default to type="submit", which can trigger unintended form submissions. Add type="button" to all action buttons that should not submit forms.

🔧 Proposed fix
       <button
+        type="button"
         hx-get="/import/queue/items"

Apply the same fix to all other button elements in this file (lines 27, 43, 57, 69).

Also applies to: 27-36, 43-55, 57-68, 69-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/importing/queue_header.html` around lines 17 - 26, Add an explicit type
attribute to each plain action button so they don't default to type="submit":
locate the button element with attributes hx-get="/import/queue/items" and
title="Refresh" (inner text "Refresh") and add type="button"; repeat the same
change for the other action buttons in this template (any <button> elements used
for actions/refreshes) so all non-form-submit buttons include type="button".
src/features/downloading/handlers.go (2)

206-227: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fire the job-list refresh trigger after starting downloads.

Unlike the other job-start handlers in this PR, these endpoints return respond.ToastJob without setting HX-Trigger. The download succeeds, but the jobs UI will stay stale until its next refresh after Lines 227, 249, 271, 298, and 331.

Also applies to: 230-250, 253-272, 275-299, 302-332

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/downloading/handlers.go` around lines 206 - 227, After
successfully starting a job in DownloadTrack (after h.service.DownloadTrack
returns jobID), set the HX-Trigger header on the Fiber context so the jobs UI is
refreshed (for example set c.Set("HX-Trigger", `{"job-list-refresh": null}`) or
equivalent) before returning respond.ToastJob; apply the same fix to the other
job-start handlers in this file that call h.service.* and return
respond.ToastJob so each response includes the HX-Trigger for
"job-list-refresh".

334-369: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't swap raw JSON into the album-tracks target on failure.

This handler now renders downloading/album_tracks on success, but Lines 340 and 347 still return JSON errors. In the HTMX flow, that will insert raw JSON into the panel instead of showing a toast or an HTML error state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/downloading/handlers.go` around lines 334 - 369, The handler
GetAlbumTracks currently returns JSON error responses when albumID is missing
and when h.service.GetAlbumTracks fails, which will insert raw JSON into the
HTMX panel; replace those two c.Status(...).JSON(...) returns (the albumID check
and the err != nil branch after h.service.GetAlbumTracks) with calls to
respond.Partial (or the app's HTML error partial) so the HTMX flow gets an HTML
fragment—invoke respond.Partial with the same HTTP status and pass a simple map
like {"Error": errMsg} (preserve context like the downloader if useful) so the
"downloading/album_tracks" panel shows a proper HTML error instead of raw JSON.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/api.md`:
- Around line 25-206: The API docs still list legacy routes using the `/ui`
prefix (e.g. GET `/ui`, `/ui/dashboard`, `/ui/library`, `/ui/analyze/lyrics`,
`/ui/jobs/count`) while the updated templates and handlers now use non-`/ui`
paths (e.g. `/dashboard`, `/library`, `/jobs/count`, `/analyze/lyrics`); update
the route tables by replacing `/ui/...` entries with the new top-level routes,
ensure the examples in the
Config/Library/Tag/Importing/Jobs/Download/Lyrics/Playlists/Reorganize/Metrics
sections match the new routes and HTTP responses shown in the diff, and add a
short note/redirect entry where appropriate for any legacy `/ui` → new path
redirects so consumers aren’t misled.
- Around line 3-5: The statement claiming "Every endpoint supports dual content
negotiation via the `HX-Request` header" is too broad; update the wording to
restrict the claim to only those endpoints that actually perform content
negotiation (e.g., endpoints that return Resource representations and those that
explicitly support HTMX). Replace "Every endpoint" with a qualified phrase like
"Endpoints that return Resource responses" or "Most endpoints that support both
HTML and API clients" and add a short note that some endpoints are always JSON
or HTMX-only and instead use Accept header negotiation for Resource responses;
reference the `HX-Request` header, "Resource responses", and "Accept"
negotiation in the edit so readers know which cases use which mechanism.

In `@src/features/lyrics/handlers.go`:
- Around line 69-90: GetLyricsProviders currently returns JSON on success for
non-HTMX clients but returns plain text/HTML on errors; update error branches
(e.g., the empty track ID check, the metadataService.GetTrackFileTags error
path, and any other branches inside GetLyricsProviders and similar handlers
between lines 94-117) to return structured JSON error responses when the request
is not HTMX (check c.Get("HX-Request") != "true") instead of
c.Status(...).SendString or respond.Partial; use c.Status(...).JSON(...) with a
consistent error shape (message and optional code) so both success and failure
for non-HTMX callers are JSON, while still using respond.Partial for HTMX flows
like respond.Partial(c, "tag/lyrics_buttons", ...).

In `@src/features/metadata/handlers.go`:
- Around line 605-627: The GetMetadataProviders handler returns plain strings
for errors when HX-Request != "true", which breaks JSON consumers; update
GetMetadataProviders so that when c.Get("HX-Request") != "true" all responses
(including error cases) use JSON payloads with consistent shape (e.g.,
{"error":"message"} or {"error":"message","details":...}); specifically change
the two plain-string error returns (the empty trackID check and the
GetTrackFileTags error path) to return c.Status(...).JSON(...) and keep the HTMX
branch using respond.Partial for template rendering; reference the
GetMetadataProviders function and the calls to
h.service.GetEnabledMetadataProviders and h.service.GetTrackFileTags to locate
where to replace SendString with JSON responses.

In `@src/features/playlists/handlers.go`:
- Around line 296-299: The response sets the M3U content type inconsistently:
respond.Resource is called with "audio/x-mpegurl" but the inner callback
overrides it to "text/plain"; update the callback in handlers.go (the anonymous
function passed to respond.Resource in the playlist export handler) to not
override the Content-Type or to set it to "audio/x-mpegurl" instead of
"text/plain" (leave the Content-Disposition filename logic intact) so clients
receive the correct MIME type; ensure the change is made where
c.Set("Content-Type", ...) and c.Set("Content-Disposition", ...) are invoked.

In `@src/features/ui/routes.go`:
- Around line 9-12: Routes were refactored to remove the /ui prefix but other
code and docs still point to /ui paths; update all references so they match the
new routes. Replace occurrences of '/ui/library/tag/edit/'+track.ID and any
other '/ui/...' links in src/features/lyrics/lyrics_job.go (manual_fix HTML),
change documented endpoints in docs/api.md to their new paths (remove /ui prefix
and update redirect notes), and update examples in FEATURE_SPEC_KIT.md
(hx-get/redirect examples) to use the new root-based routes (e.g.,
/library/tag/edit/:id, /dashboard, /analyze, /dashboard/quick-actions) so code
and docs match handler.RenderDashboard, handler.RenderAnalyzeSection and
handler.GetQuickActionsCard routing.

In `@views/playlists/playlist.html`:
- Around line 4-5: The HX action buttons are missing an explicit type which
causes them to default to submit; update both button elements (the one with
hx-get="/playlists" and the other "Browse Library" HX button) to include
type="button" so they won't trigger form submission and to satisfy the HTML lint
rule; locate the <button> elements that have hx-get/hx-post attributes and add
the type="button" attribute to each.

In `@views/sections/library.html`:
- Around line 5-6: The refresh button element identified by attributes
hx-get="/library" and hx-target="`#contenido`" lacks a type attribute and may
default to type="submit" inside forms; update that <button> (the element with
hx-get="/library" hx-swap="outerHTML" hx-target="`#contenido`") to explicitly
include type="button" to prevent accidental form submissions.

---

Outside diff comments:
In `@src/features/downloading/handlers.go`:
- Around line 206-227: After successfully starting a job in DownloadTrack (after
h.service.DownloadTrack returns jobID), set the HX-Trigger header on the Fiber
context so the jobs UI is refreshed (for example set c.Set("HX-Trigger",
`{"job-list-refresh": null}`) or equivalent) before returning respond.ToastJob;
apply the same fix to the other job-start handlers in this file that call
h.service.* and return respond.ToastJob so each response includes the HX-Trigger
for "job-list-refresh".
- Around line 334-369: The handler GetAlbumTracks currently returns JSON error
responses when albumID is missing and when h.service.GetAlbumTracks fails, which
will insert raw JSON into the HTMX panel; replace those two
c.Status(...).JSON(...) returns (the albumID check and the err != nil branch
after h.service.GetAlbumTracks) with calls to respond.Partial (or the app's HTML
error partial) so the HTMX flow gets an HTML fragment—invoke respond.Partial
with the same HTTP status and pass a simple map like {"Error": errMsg} (preserve
context like the downloader if useful) so the "downloading/album_tracks" panel
shows a proper HTML error instead of raw JSON.

In `@views/importing/queue_header.html`:
- Around line 17-26: Add an explicit type attribute to each plain action button
so they don't default to type="submit": locate the button element with
attributes hx-get="/import/queue/items" and title="Refresh" (inner text
"Refresh") and add type="button"; repeat the same change for the other action
buttons in this template (any <button> elements used for actions/refreshes) so
all non-form-submit buttons include type="button".
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 6ee921c6-8fcd-4e5e-b8b4-62584a006db0

📥 Commits

Reviewing files that changed from the base of the PR and between 4661eb5 and 58ebdd5.

📒 Files selected for processing (54)
  • docs/api.md
  • src/features/config/handlers.go
  • src/features/config/routes.go
  • src/features/downloading/handlers.go
  • src/features/downloading/routes.go
  • src/features/hosting/respond/respond.go
  • src/features/importing/handlers.go
  • src/features/importing/routes.go
  • src/features/jobs/handlers.go
  • src/features/jobs/routes.go
  • src/features/library/handlers.go
  • src/features/library/routes.go
  • src/features/lyrics/handlers.go
  • src/features/lyrics/routes.go
  • src/features/metadata/handlers.go
  • src/features/metadata/routes.go
  • src/features/metrics/handlers.go
  • src/features/metrics/routes.go
  • src/features/playlists/handlers.go
  • src/features/playlists/routes.go
  • src/features/reorganize/handlers.go
  • src/features/reorganize/routes.go
  • src/features/ui/handlers.go
  • src/features/ui/routes.go
  • views/cards/quick_actions.html
  • views/config/config_form.html
  • views/importing/queue_header.html
  • views/library/track_overview_panel.html
  • views/library/unified_search_list.html
  • views/lyrics/queue_header.html
  • views/lyrics/queue_items.html
  • views/metrics/overview.html
  • views/partials/job_status.html
  • views/partials/main.html
  • views/partials/navbar.html
  • views/partials/sidebar.html
  • views/playlists/add_to_playlist_modal.html
  • views/playlists/playlist.html
  • views/sections/analyze.html
  • views/sections/analyze_files.html
  • views/sections/analyze_lyrics.html
  • views/sections/analyze_metadata.html
  • views/sections/dashboard.html
  • views/sections/download.html
  • views/sections/import.html
  • views/sections/jobs.html
  • views/sections/library.html
  • views/sections/playlists.html
  • views/sections/settings.html
  • views/sections/tag.html
  • views/tag/edit_form.html
  • views/tag/lyrics_buttons.html
  • views/tag/metadata_buttons.html
  • views/tag/search_results_modal.html

Comment thread docs/api.md Outdated
Comment thread docs/api.md Outdated
Comment thread src/features/lyrics/handlers.go
Comment on lines +605 to 627
// GetMetadataProviders returns metadata provider buttons for HTMX or provider list as JSON.
func (h *Handler) GetMetadataProviders(c *fiber.Ctx) error {
trackID := c.Params("trackId")
if trackID == "" {
return c.Status(fiber.StatusBadRequest).SendString("Track ID is required")
}

// Get track data for button context
providers := h.service.GetEnabledMetadataProviders()

if c.Get("HX-Request") != "true" {
return c.JSON(providers)
}

track, err := h.service.GetTrackFileTags(c.Context(), trackID)
if err != nil {
slog.Error("Failed to get track for buttons", "error", err, "trackId", trackID)
slog.Error("Failed to get track for metadata providers", "error", err, "trackId", trackID)
return c.Status(fiber.StatusInternalServerError).SendString("Failed to load track data")
}

return c.Render("tag/metadata_buttons", fiber.Map{
return respond.Partial(c, "tag/metadata_buttons", fiber.Map{
"Track": track,
"EnabledProviders": h.service.GetEnabledMetadataProviders(),
"EnabledProviders": providers,
})

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 | ⚡ Quick win

Return JSON errors from GetMetadataProviders for non-HTMX requests.

The success path is JSON when HX-Request != true, but Lines 609 and 621 still use plain-string responses. API callers cannot rely on a stable payload shape once this endpoint fails.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/metadata/handlers.go` around lines 605 - 627, The
GetMetadataProviders handler returns plain strings for errors when HX-Request !=
"true", which breaks JSON consumers; update GetMetadataProviders so that when
c.Get("HX-Request") != "true" all responses (including error cases) use JSON
payloads with consistent shape (e.g., {"error":"message"} or
{"error":"message","details":...}); specifically change the two plain-string
error returns (the empty trackID check and the GetTrackFileTags error path) to
return c.Status(...).JSON(...) and keep the HTMX branch using respond.Partial
for template rendering; reference the GetMetadataProviders function and the
calls to h.service.GetEnabledMetadataProviders and h.service.GetTrackFileTags to
locate where to replace SendString with JSON responses.

Comment on lines +296 to +299
return respond.Resource(c, "audio/x-mpegurl", fmt.Sprintf("%s/playlists/%s/export", c.BaseURL(), playlistID), func() error {
c.Set("Content-Type", "text/plain")
c.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
return c.SendString(m3uContent)

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 | ⚡ Quick win

Keep the exported M3U content type consistent.

respond.Resource declares audio/x-mpegurl, but the callback overrides it to text/plain. This can cause clients to treat playlist exports incorrectly.

Proposed fix
 return respond.Resource(c, "audio/x-mpegurl", fmt.Sprintf("%s/playlists/%s/export", c.BaseURL(), playlistID), func() error {
-	c.Set("Content-Type", "text/plain")
+	c.Set("Content-Type", "audio/x-mpegurl")
 	c.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
 	return c.SendString(m3uContent)
 })
📝 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
return respond.Resource(c, "audio/x-mpegurl", fmt.Sprintf("%s/playlists/%s/export", c.BaseURL(), playlistID), func() error {
c.Set("Content-Type", "text/plain")
c.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
return c.SendString(m3uContent)
return respond.Resource(c, "audio/x-mpegurl", fmt.Sprintf("%s/playlists/%s/export", c.BaseURL(), playlistID), func() error {
c.Set("Content-Type", "audio/x-mpegurl")
c.Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filename))
return c.SendString(m3uContent)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/playlists/handlers.go` around lines 296 - 299, The response sets
the M3U content type inconsistently: respond.Resource is called with
"audio/x-mpegurl" but the inner callback overrides it to "text/plain"; update
the callback in handlers.go (the anonymous function passed to respond.Resource
in the playlist export handler) to not override the Content-Type or to set it to
"audio/x-mpegurl" instead of "text/plain" (leave the Content-Disposition
filename logic intact) so clients receive the correct MIME type; ensure the
change is made where c.Set("Content-Type", ...) and c.Set("Content-Disposition",
...) are invoked.

Comment thread src/features/ui/routes.go
Comment thread views/playlists/playlist.html
Comment thread views/sections/library.html
@contre95 contre95 changed the title Merge pull request #162 from contre95/fix/api_htmx feat(docs): New API API Refactor May 21, 2026
@contre95 contre95 changed the title API Refactor API Refactor HTMX/JSON May 21, 2026
@contre95 contre95 changed the title API Refactor HTMX/JSON API Refactor HTMX/JSON for (almost) all endpoints May 21, 2026
@contre95 contre95 marked this pull request as draft June 6, 2026 14:05
@contre95 contre95 marked this pull request as ready for review June 6, 2026 14:30

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
FEATURE_SPEC_KIT.md (2)

87-93: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unresolved merge-conflict markers.

Line 87 through Line 93 still contains conflict markers, which leaves the spec in a broken state and can mislead contributors.

Suggested fix
-<<<<<<< Updated upstream
-**Error Response Pattern**: 
-- HTMX errors: `src/features/downloading/handlers.go:43-46`
-- API errors: `src/features/downloading/handlers.go:47-50`
-
-=======
->>>>>>> Stashed changes
+**Error Response Pattern**:
+- HTMX errors: `src/features/downloading/handlers.go:43-46`
+- API errors: `src/features/downloading/handlers.go:47-50`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FEATURE_SPEC_KIT.md` around lines 87 - 93, Remove the leftover merge-conflict
markers surrounding the "Error Response Pattern" section so the spec is valid:
delete the "<<<<<<< Updated upstream", "=======" and ">>>>>>> Stashed changes"
markers and reconcile the final content under the "Error Response Pattern"
heading (keep the intended bullet list referencing HTMX errors
`src/features/downloading/handlers.go:43-46` and API errors
`src/features/downloading/handlers.go:47-50`, or merge them into one clear
block). Ensure only the resolved, clean text remains and no conflict markers are
left anywhere in the "Error Response Pattern" section.

99-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update legacy /ui routing guidance to the new root-level pattern.

Line 99 still documents /ui-grouped UI routes, but this refactor moves UI navigation to root-level grouped paths. Keeping this line as-is will cause new features to be scaffolded with stale routes.

Suggested fix
-- UI routes under `/ui` group for HTMX partials
+- UI routes grouped at root-level feature paths (e.g., `/dashboard`, `/library`, `/analyze/...`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FEATURE_SPEC_KIT.md` around lines 99 - 100, Replace the legacy guidance that
says "UI routes under `/ui` group for HTMX partials" with the new root-level
routing pattern so scaffolds generate root-level grouped UI paths (e.g., use "UI
routes at root-level grouped paths for HTMX partials" instead of `/ui`), and
keep the accompanying note that "Route registration should only pass the
feature's own service to handlers" unchanged; update any example text in
FEATURE_SPEC_KIT.md that references `/ui` to instead reference the root-level
grouping convention so new features scaffold correct routes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/lyrics/lyrics_job.go`:
- Line 144: The log builds a manual_fix URL by concatenating track.ID directly,
which can break for reserved characters; update the code that calls
job.Logger.Error (the statement referencing "manual_fix") to path-escape
track.ID using url.PathEscape(track.ID) before embedding it in the href so the
generated link is safe and valid (keep the rest of the log fields intact and use
the escaped value for the manual_fix string).

---

Outside diff comments:
In `@FEATURE_SPEC_KIT.md`:
- Around line 87-93: Remove the leftover merge-conflict markers surrounding the
"Error Response Pattern" section so the spec is valid: delete the "<<<<<<<
Updated upstream", "=======" and ">>>>>>> Stashed changes" markers and reconcile
the final content under the "Error Response Pattern" heading (keep the intended
bullet list referencing HTMX errors `src/features/downloading/handlers.go:43-46`
and API errors `src/features/downloading/handlers.go:47-50`, or merge them into
one clear block). Ensure only the resolved, clean text remains and no conflict
markers are left anywhere in the "Error Response Pattern" section.
- Around line 99-100: Replace the legacy guidance that says "UI routes under
`/ui` group for HTMX partials" with the new root-level routing pattern so
scaffolds generate root-level grouped UI paths (e.g., use "UI routes at
root-level grouped paths for HTMX partials" instead of `/ui`), and keep the
accompanying note that "Route registration should only pass the feature's own
service to handlers" unchanged; update any example text in FEATURE_SPEC_KIT.md
that references `/ui` to instead reference the root-level grouping convention so
new features scaffold correct routes.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: ee05c793-6f50-44ea-b24d-2757412fb618

📥 Commits

Reviewing files that changed from the base of the PR and between 58ebdd5 and 870d041.

📒 Files selected for processing (3)
  • FEATURE_SPEC_KIT.md
  • docs/api.md
  • src/features/lyrics/lyrics_job.go

result, err := t.service.AddLyrics(ctx, track.ID, provider, overrideNoQueue)
if err != nil {
job.Logger.Error("Failed to add lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "error", err.Error(), "manual_fix", "<a href='/ui/library/tag/edit/"+track.ID+"' target='_blank'>track</a>")
job.Logger.Error("Failed to add lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "error", err.Error(), "manual_fix", "<a href='/tag/"+track.ID+"' target='_blank'>track</a>")

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 | ⚡ Quick win

Path-escape track.ID before embedding it in manual_fix URL.

Line 144 builds the URL with raw concatenation. If track.ID contains reserved characters, the link can break. Use url.PathEscape(track.ID) when composing the path.

Suggested fix
 import (
 	"context"
 	"fmt"
 	"log/slog"
+	"net/url"
@@
-				job.Logger.Error("Failed to add lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "error", err.Error(), "manual_fix", "<a href='/tag/"+track.ID+"' target='_blank'>track</a>")
+				job.Logger.Error("Failed to add lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "error", err.Error(), "manual_fix", "<a href='/tag/"+url.PathEscape(track.ID)+"' target='_blank'>track</a>")
📝 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
job.Logger.Error("Failed to add lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "error", err.Error(), "manual_fix", "<a href='/tag/"+track.ID+"' target='_blank'>track</a>")
import (
"context"
"fmt"
"log/slog"
"net/url"
)
// ... other code ...
job.Logger.Error("Failed to add lyrics for track", "trackID", track.ID, "title", track.Title, "provider", provider, "error", err.Error(), "manual_fix", "<a href='/tag/"+url.PathEscape(track.ID)+"' target='_blank'>track</a>")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/lyrics/lyrics_job.go` at line 144, The log builds a manual_fix
URL by concatenating track.ID directly, which can break for reserved characters;
update the code that calls job.Logger.Error (the statement referencing
"manual_fix") to path-escape track.ID using url.PathEscape(track.ID) before
embedding it in the href so the generated link is safe and valid (keep the rest
of the log fields intact and use the escaped value for the manual_fix string).

@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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main.go (1)

137-150: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Start the Fiber server only once, after wiring signal handling.

This block starts the same server twice. The first server.Start() blocks and bypasses the intended graceful-shutdown path; the later goroutine start is redundant and can fail with address-in-use errors.

Suggested fix
 streamingService := streaming.NewService(cfgManager)
 server := hosting.NewServer(cfgManager, importingService, libraryService, playlistsService, downloadingService, jobService, tagService, lyricsService, metricsService, reorganizeService, streamingService)
-slog.Info("Starting server", "port", cfgManager.Get().Server.Port)
-if err := server.Start(); err != nil {
-  slog.Error("server stopped: %v", "error", err)
-}
 quit := make(chan os.Signal, 1)
 signal.Notify(quit, os.Interrupt)
 serverErr := make(chan error, 1)
 go func() {
   slog.Info("Starting server", "port", cfgManager.Get().Server.Port)
   if err := server.Start(); err != nil {
     serverErr <- err
   }
 }()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.go` around lines 137 - 150, The code currently calls server.Start()
twice causing a blocking start and potential address-in-use errors; remove the
first blocking call to server.Start() (the top-level call before signal
handling) and instead set up signal handling (quit channel and signal.Notify)
and serverErr channel first, then start the Fiber server exactly once inside the
goroutine (where server.Start() is already invoked), using the existing
serverErr and quit channels to handle shutdown and errors; ensure you still log
the startup via slog.Info and perform graceful shutdown (server.Shutdown or
equivalent) when quit or an error is received.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/library/service.go`:
- Around line 397-403: The current error handling maps any error from
s.library.GetTrack(ctx, trackID) to "track not found", masking real storage/DB
errors; change it so that if err != nil you return the wrapped err (e.g.,
fmt.Errorf("failed to get track: %w", err)) and only return the "track not
found" error when track == nil; update the block around the GetTrack call
(referencing s.library.GetTrack, trackID and track) to propagate real lookup
errors and reserve the not-found message solely for a nil track.

In `@src/features/streaming/handlers.go`:
- Around line 22-31: The handler currently calls h.service.Stream even when the
"path" query is empty, causing a 404 instead of a 400; update the code that
reads rawPath/path to validate that rawPath (or the decoded path) is non-empty
and if missing return c.Status(fiber.StatusBadRequest).SendString("missing
path") (or similar) before calling h.service.Stream so malformed requests are
rejected with 400; reference the variables rawPath/path and the h.service.Stream
call to locate where to insert the check.

In `@views/importing/queue_items_grouped_album.html`:
- Around line 131-169: The play/toggle controls are missing explicit button
types and the range sliders are unlabeled for accessibility; add type="button"
to every control <button> (both the toggle buttons using the hyperscript
attributes like the ones targeting "`#pa-queue-`{{.ID}}" and "`#pa-lib-`{{.ID}}" and
the play buttons that call toggleAudioPlayer(this) and have class "play-btn")
and add accessible labels to the <input type="range"> elements (e.g.,
aria-label="Playback position" or aria-labelledby pointing to a visible label,
and include aria-valuemin/aria-valuemax if appropriate) so seekAudioProgress and
updateAudioProgress work with labeled controls.

In `@views/importing/queue_items_grouped_artist.html`:
- Around line 129-134: Add explicit type="button" attributes to the audio
control buttons to prevent accidental form submission: update each <button>
element that uses the stimulus-like "_=" click toggles (e.g., the buttons
toggling .hidden on `#pg-queue-`{{.ID}} and `#pg-lib-`{{.ID}} and the other similar
buttons at the other noted locations) to include type="button". Locate the
button elements that have attributes like '_="on click toggle .hidden on
`#pg-queue-`{{.ID}}"' and add the type attribute to each so they don't act as
submit buttons when this fragment is inside a form.
- Around line 144-145: The template is exposing server filesystem paths via
Track.Path in the audio src (src="/stream?path={{urlEncode .Track.Path}}");
replace these with an opaque identifier (e.g. .Track.ID or .QueueItem.ID) in all
occurrences (lines around the existing audio tag and the other reported spots)
and change the client URL to use that id (e.g. /stream?id=...). Then update the
server handler (the Service.Stream call / stream route) to accept the opaque id,
perform a lookup by Track.ID or QueueItem.ID to resolve the filesystem path
server-side, and stream the file from the resolved path; ensure urlEncode usage
is applied to the id if needed and remove direct exposure of .Track.Path from
the template.

In `@views/importing/queue_items.html`:
- Around line 51-90: Add explicit type="button" to every <button> used for the
audio controls (both the toggle buttons that target `#player-queue-`{{.ID}} and
`#player-lib-`{{.ID}} and the play buttons that call toggleAudioPlayer(this)) to
prevent accidental form submission, and give each range input (those with
oninput="seekAudioProgress(this)") an accessible name by adding an aria-label or
aria-labelledby (e.g., "Seek playback" or a unique label per player using
{{.ID}}) so screen readers can identify the slider.

In `@views/library/track_overview_panel.html`:
- Around line 29-31: The range input used for audio seeking lacks an accessible
label; add an explicit label for the slider by giving the input a unique id
(e.g., seek-slider) and either add a visible <label for="seek-slider">Seek
audio</label> or an aria-label/aria-labelledby attribute (e.g., aria-label="Seek
audio") so screen readers can identify the control referenced by the
seekAudioProgress handler; ensure the label text clearly describes the control
and that the input retains its existing attributes and
oninput="seekAudioProgress(this)".

In `@views/partials/scripts.html`:
- Around line 155-157: The UI sets icon.className to the pause icon immediately
after calling audio.play(), which leaves the UI inconsistent if playback is
rejected; change the flow so audio.play()'s returned Promise is handled: call
audio.play().then(() => set icon.className = 'fas fa-pause fa-xs') and in
.catch(() => reset icon.className to the play icon (and optionally surface/log
the error)), ensuring the icon is updated only on successful play and restored
on failure to avoid unhandled promise rejections.

In `@views/partials/sidebar.html`:
- Around line 167-168: The sidebar element with id "active-jobs-badge" is still
polling the old endpoint via its hx-get attribute; update the hx-get value from
"/ui/jobs/count?filter=active" to the migrated path "/jobs/count?filter=active"
in views/partials/sidebar.html (preserve the existing hx-trigger, hx-push-url,
and hx-target attributes).

In `@views/tag/edit_form.html`:
- Around line 128-131: The range input used for audio seeking is missing an
accessible name, so update the input element (the range slider tied to the
seekAudioProgress function) to provide one — either add an aria-label like "Seek
audio" or add aria-labelledby pointing to a nearby visible label/span that
describes the control (e.g., the time-display span or a new label element);
ensure the label text clearly indicates it's the audio seek slider so screen
readers announce context when the slider is focused.

---

Outside diff comments:
In `@src/main.go`:
- Around line 137-150: The code currently calls server.Start() twice causing a
blocking start and potential address-in-use errors; remove the first blocking
call to server.Start() (the top-level call before signal handling) and instead
set up signal handling (quit channel and signal.Notify) and serverErr channel
first, then start the Fiber server exactly once inside the goroutine (where
server.Start() is already invoked), using the existing serverErr and quit
channels to handle shutdown and errors; ensure you still log the startup via
slog.Info and perform graceful shutdown (server.Shutdown or equivalent) when
quit or an error is received.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: f6e19a4c-4998-43f0-adeb-7b1818d533f8

📥 Commits

Reviewing files that changed from the base of the PR and between 870d041 and 695d6ac.

📒 Files selected for processing (16)
  • src/features/hosting/server.go
  • src/features/importing/service.go
  • src/features/library/service.go
  • src/features/streaming/handlers.go
  • src/features/streaming/routes.go
  • src/features/streaming/service.go
  • src/main.go
  • views/importing/queue_header.html
  • views/importing/queue_items.html
  • views/importing/queue_items_grouped_album.html
  • views/importing/queue_items_grouped_artist.html
  • views/library/track_overview_panel.html
  • views/partials/navbar.html
  • views/partials/scripts.html
  • views/partials/sidebar.html
  • views/tag/edit_form.html

Comment on lines +397 to +403
track, err := s.library.GetTrack(ctx, trackID)
if err != nil {
return "", fmt.Errorf("track not found: %w", err)
}
if track == nil {
return "", fmt.Errorf("track not found")
}

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 | ⚡ Quick win

Do not map every GetTrack failure to track not found.

This masks real storage/DB failures as not-found and can cause incorrect downstream 404 behavior. Keep not found for nil track only; propagate real lookup errors.

💡 Proposed fix
 func (s *Service) GetLibraryTrackPath(ctx context.Context, trackID string) (string, error) {
 	track, err := s.library.GetTrack(ctx, trackID)
 	if err != nil {
-		return "", fmt.Errorf("track not found: %w", err)
+		return "", fmt.Errorf("failed to load track %s: %w", trackID, err)
 	}
 	if track == nil {
-		return "", fmt.Errorf("track not found")
+		return "", fmt.Errorf("track not found: %s", trackID)
 	}
 	return track.Path, nil
 }
📝 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
track, err := s.library.GetTrack(ctx, trackID)
if err != nil {
return "", fmt.Errorf("track not found: %w", err)
}
if track == nil {
return "", fmt.Errorf("track not found")
}
func (s *Service) GetLibraryTrackPath(ctx context.Context, trackID string) (string, error) {
track, err := s.library.GetTrack(ctx, trackID)
if err != nil {
return "", fmt.Errorf("failed to load track %s: %w", trackID, err)
}
if track == nil {
return "", fmt.Errorf("track not found: %s", trackID)
}
return track.Path, nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/library/service.go` around lines 397 - 403, The current error
handling maps any error from s.library.GetTrack(ctx, trackID) to "track not
found", masking real storage/DB errors; change it so that if err != nil you
return the wrapped err (e.g., fmt.Errorf("failed to get track: %w", err)) and
only return the "track not found" error when track == nil; update the block
around the GetTrack call (referencing s.library.GetTrack, trackID and track) to
propagate real lookup errors and reserve the not-found message solely for a nil
track.

Comment on lines +22 to +31
rawPath := c.Query("path")
path, err := url.QueryUnescape(rawPath)
if err != nil {
return c.Status(fiber.StatusBadRequest).SendString("invalid path")
}
resolved, mimeType, err := h.service.Stream(path)
if err != nil {
slog.Error("Stream: rejected path", "path", path, "error", err)
return c.Status(fiber.StatusNotFound).SendString("track not found")
}

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 | ⚡ Quick win

Return 400 when path is missing before calling the service.

An empty/missing query currently falls through and is reported as 404 track not found, which misclassifies a malformed request.

💡 Proposed fix
 func (h *Handler) Stream(c *fiber.Ctx) error {
 	rawPath := c.Query("path")
+	if rawPath == "" {
+		return c.Status(fiber.StatusBadRequest).SendString("missing path")
+	}
 	path, err := url.QueryUnescape(rawPath)
 	if err != nil {
 		return c.Status(fiber.StatusBadRequest).SendString("invalid path")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/streaming/handlers.go` around lines 22 - 31, The handler
currently calls h.service.Stream even when the "path" query is empty, causing a
404 instead of a 400; update the code that reads rawPath/path to validate that
rawPath (or the decoded path) is non-empty and if missing return
c.Status(fiber.StatusBadRequest).SendString("missing path") (or similar) before
calling h.service.Stream so malformed requests are rejected with 400; reference
the variables rawPath/path and the h.service.Stream call to locate where to
insert the check.

Comment on lines +131 to +169
<button _="on click toggle .hidden on #pa-queue-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-violet-500/10 border border-violet-400/30 text-violet-600 dark:text-violet-300 hover:bg-violet-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> New
</button>
<button _="on click toggle .hidden on #pa-lib-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-blue-500/10 border border-blue-400/30 text-blue-600 dark:text-blue-300 hover:bg-blue-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Existing
</button>
</div>
<div id="pa-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2 bg-violet-500/5 border border-violet-400/20 rounded-lg px-2.5 py-1.5 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-5 h-5 flex items-center justify-center rounded-full bg-violet-500/20 hover:bg-violet-500/40 text-violet-600 dark:text-violet-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[#463371] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
<div id="pa-lib-{{.ID}}" class="audio-player hidden flex items-center gap-2 bg-blue-500/5 border border-blue-400/20 rounded-lg px-2.5 py-1.5 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-5 h-5 flex items-center justify-center rounded-full bg-blue-500/20 hover:bg-blue-500/40 text-blue-600 dark:text-blue-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[#7BAADF] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode (index .ItemMetadata "duplicate_path")}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
{{else}}
<div class="flex items-center gap-1.5">
<button _="on click toggle .hidden on #pa-queue-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-gray-500/10 border border-gray-400/30 text-gray-600 dark:text-gray-300 hover:bg-gray-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Play
</button>
</div>
<div id="pa-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2 bg-gray-500/5 border border-gray-400/20 rounded-lg px-2.5 py-1.5 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-5 h-5 flex items-center justify-center rounded-full bg-gray-500/20 hover:bg-gray-500/40 text-gray-600 dark:text-gray-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>

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 | ⚡ Quick win

Apply explicit button types and slider labels in grouped-album players.

The new player controls have the same semantic/accessibility gaps: missing type="button" on control buttons and unlabeled range inputs.

💡 Proposed fix
-                        <button _="on click toggle .hidden on `#pa-queue-`{{.ID}}"
+                        <button type="button" _="on click toggle .hidden on `#pa-queue-`{{.ID}}"
                             class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-violet-500/10 border border-violet-400/30 text-violet-600 dark:text-violet-300 hover:bg-violet-500/20 transition-colors">
@@
-                        <button _="on click toggle .hidden on `#pa-lib-`{{.ID}}"
+                        <button type="button" _="on click toggle .hidden on `#pa-lib-`{{.ID}}"
                             class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-blue-500/10 border border-blue-400/30 text-blue-600 dark:text-blue-300 hover:bg-blue-500/20 transition-colors">
@@
-                        <input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#463371`] mx-1" value="0" min="0" max="100" step="0.1">
+                        <input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#463371`] mx-1" value="0" min="0" max="100" step="0.1">
@@
-                        <input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#7BAADF`] mx-1" value="0" min="0" max="100" step="0.1">
+                        <input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#7BAADF`] mx-1" value="0" min="0" max="100" step="0.1">
@@
-                        <button _="on click toggle .hidden on `#pa-queue-`{{.ID}}"
+                        <button type="button" _="on click toggle .hidden on `#pa-queue-`{{.ID}}"
                             class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-gray-500/10 border border-gray-400/30 text-gray-600 dark:text-gray-300 hover:bg-gray-500/20 transition-colors">
@@
-                        <input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
+                        <input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
🧰 Tools
🪛 HTMLHint (1.9.2)

[warning] 131-131: The type attribute must be present on elements.

(button-type-require)


[warning] 135-135: The type attribute must be present on

elements.

(button-type-require)


[warning] 141-141: The type attribute must be present on

elements.

(button-type-require)


[warning] 149-149: The type attribute must be present on

elements.

(button-type-require)


[error] 154-154: Special characters must be escaped : [ < ].

(spec-char-escape)


[error] 154-154: Special characters must be escaped : [ > ].

(spec-char-escape)


[error] 154-154: Tag must be paired, no start tag: [ ]

(tag-pair)


[warning] 158-158: The type attribute must be present on

elements.

(button-type-require)


[error] 163-163: The id value [ pa-queue-{{.ID}} ] must be unique.

(id-unique)


[warning] 164-164: The type attribute must be present on

elements.

(button-type-require)


[warning] 144-144: No matching [ label ] tag found.

(input-requires-label)


[warning] 152-152: No matching [ label ] tag found.

(input-requires-label)


[warning] 167-167: No matching [ label ] tag found.

(input-requires-label)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/importing/queue_items_grouped_album.html` around lines 131 - 169, The
play/toggle controls are missing explicit button types and the range sliders are
unlabeled for accessibility; add type="button" to every control <button> (both
the toggle buttons using the hyperscript attributes like the ones targeting
"`#pa-queue-`{{.ID}}" and "`#pa-lib-`{{.ID}}" and the play buttons that call
toggleAudioPlayer(this) and have class "play-btn") and add accessible labels to
the <input type="range"> elements (e.g., aria-label="Playback position" or
aria-labelledby pointing to a visible label, and include
aria-valuemin/aria-valuemax if appropriate) so seekAudioProgress and
updateAudioProgress work with labeled controls.

Source: Linters/SAST tools

Comment on lines +129 to +134
<button _="on click toggle .hidden on #pg-queue-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-violet-500/10 border border-violet-400/30 text-violet-600 dark:text-violet-300 hover:bg-violet-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> New
</button>
<button _="on click toggle .hidden on #pg-lib-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded text-xs font-medium bg-blue-500/10 border border-blue-400/30 text-blue-600 dark:text-blue-300 hover:bg-blue-500/20 transition-colors">

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 | ⚡ Quick win

Add explicit button types for the new audio controls.

These buttons should use type="button" to avoid accidental form submission behavior when this fragment is composed inside form contexts.

Suggested fix
-<button _="on click toggle .hidden on `#pg-queue-`{{.ID}}"
+<button type="button" _="on click toggle .hidden on `#pg-queue-`{{.ID}}"
 ...
-<button _="on click toggle .hidden on `#pg-lib-`{{.ID}}"
+<button type="button" _="on click toggle .hidden on `#pg-lib-`{{.ID}}"
 ...
-<button onclick="toggleAudioPlayer(this)" class="play-btn ...
+<button type="button" onclick="toggleAudioPlayer(this)" class="play-btn ...
 ...
-<button onclick="toggleAudioPlayer(this)" class="play-btn ...
+<button type="button" onclick="toggleAudioPlayer(this)" class="play-btn ...
 ...
-<button _="on click toggle .hidden on `#pg-queue-`{{.ID}}"
+<button type="button" _="on click toggle .hidden on `#pg-queue-`{{.ID}}"
 ...
-<button onclick="toggleAudioPlayer(this)" class="play-btn ...
+<button type="button" onclick="toggleAudioPlayer(this)" class="play-btn ...

Also applies to: 139-139, 147-147, 156-156, 162-162

🧰 Tools
🪛 HTMLHint (1.9.2)

[warning] 129-129: The type attribute must be present on elements.

(button-type-require)


[warning] 133-133: The type attribute must be present on

elements.

(button-type-require)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/importing/queue_items_grouped_artist.html` around lines 129 - 134, Add
explicit type="button" attributes to the audio control buttons to prevent
accidental form submission: update each <button> element that uses the
stimulus-like "_=" click toggles (e.g., the buttons toggling .hidden on
`#pg-queue-`{{.ID}} and `#pg-lib-`{{.ID}} and the other similar buttons at the other
noted locations) to include type="button". Locate the button elements that have
attributes like '_="on click toggle .hidden on `#pg-queue-`{{.ID}}"' and add the
type attribute to each so they don't act as submit buttons when this fragment is
inside a form.

Source: Linters/SAST tools

Comment on lines +144 to +145
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>

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 | 🏗️ Heavy lift

Avoid exposing filesystem paths in client-side stream URLs.

Embedding absolute track paths in src="/stream?path=..." leaks server filesystem structure to the browser, network logs, and intermediaries. Use opaque IDs in the UI and resolve paths server-side (queue item ID / track ID lookup) before calling Service.Stream.

Also applies to: 152-153, 167-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/importing/queue_items_grouped_artist.html` around lines 144 - 145, The
template is exposing server filesystem paths via Track.Path in the audio src
(src="/stream?path={{urlEncode .Track.Path}}"); replace these with an opaque
identifier (e.g. .Track.ID or .QueueItem.ID) in all occurrences (lines around
the existing audio tag and the other reported spots) and change the client URL
to use that id (e.g. /stream?id=...). Then update the server handler (the
Service.Stream call / stream route) to accept the opaque id, perform a lookup by
Track.ID or QueueItem.ID to resolve the filesystem path server-side, and stream
the file from the resolved path; ensure urlEncode usage is applied to the id if
needed and remove direct exposure of .Track.Path from the template.

Comment on lines +51 to +90
<button _="on click toggle .hidden on #player-queue-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-violet-500/10 border border-violet-400/30 text-violet-600 dark:text-violet-300 hover:bg-violet-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> New
</button>
<button _="on click toggle .hidden on #player-lib-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-blue-500/10 border border-blue-400/30 text-blue-600 dark:text-blue-300 hover:bg-blue-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Existing
</button>
</div>
<div id="player-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-violet-500/5 border border-violet-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-violet-500/20 hover:bg-violet-500/40 text-violet-600 dark:text-violet-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[#463371] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
<div id="player-lib-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-blue-500/5 border border-blue-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-blue-500/20 hover:bg-blue-500/40 text-blue-600 dark:text-blue-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[#7BAADF] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode (index .ItemMetadata "duplicate_path")}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
{{else}}
<div class="flex items-center gap-2">
<button _="on click toggle .hidden on #player-queue-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-gray-500/10 border border-gray-400/30 text-gray-600 dark:text-gray-300 hover:bg-gray-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Play
</button>
</div>
<div id="player-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-gray-500/5 border border-gray-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-gray-500/20 hover:bg-gray-500/40 text-gray-600 dark:text-gray-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>

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 | ⚡ Quick win

Set explicit button types and label the audio sliders.

The new control buttons should be type="button" to avoid accidental form submission, and each range input needs an accessible name.

💡 Proposed fix
-            <button _="on click toggle .hidden on `#player-queue-`{{.ID}}"
+            <button type="button" _="on click toggle .hidden on `#player-queue-`{{.ID}}"
                 class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-violet-500/10 border border-violet-400/30 text-violet-600 dark:text-violet-300 hover:bg-violet-500/20 transition-colors">
@@
-            <button _="on click toggle .hidden on `#player-lib-`{{.ID}}"
+            <button type="button" _="on click toggle .hidden on `#player-lib-`{{.ID}}"
                 class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-blue-500/10 border border-blue-400/30 text-blue-600 dark:text-blue-300 hover:bg-blue-500/20 transition-colors">
@@
-             <input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#463371`] mx-1" value="0" min="0" max="100" step="0.1">
+             <input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#463371`] mx-1" value="0" min="0" max="100" step="0.1">
@@
-             <input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#7BAADF`] mx-1" value="0" min="0" max="100" step="0.1">
+             <input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#7BAADF`] mx-1" value="0" min="0" max="100" step="0.1">
@@
-             <button _="on click toggle .hidden on `#player-queue-`{{.ID}}"
+             <button type="button" _="on click toggle .hidden on `#player-queue-`{{.ID}}"
                 class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-gray-500/10 border border-gray-400/30 text-gray-600 dark:text-gray-300 hover:bg-gray-500/20 transition-colors">
@@
-             <input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
+             <input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
📝 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
<button _="on click toggle .hidden on #player-queue-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-violet-500/10 border border-violet-400/30 text-violet-600 dark:text-violet-300 hover:bg-violet-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> New
</button>
<button _="on click toggle .hidden on #player-lib-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-blue-500/10 border border-blue-400/30 text-blue-600 dark:text-blue-300 hover:bg-blue-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Existing
</button>
</div>
<div id="player-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-violet-500/5 border border-violet-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-violet-500/20 hover:bg-violet-500/40 text-violet-600 dark:text-violet-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[#463371] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
<div id="player-lib-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-blue-500/5 border border-blue-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-blue-500/20 hover:bg-blue-500/40 text-blue-600 dark:text-blue-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[#7BAADF] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode (index .ItemMetadata "duplicate_path")}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
{{else}}
<div class="flex items-center gap-2">
<button _="on click toggle .hidden on #player-queue-{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-gray-500/10 border border-gray-400/30 text-gray-600 dark:text-gray-300 hover:bg-gray-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Play
</button>
</div>
<div id="player-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-gray-500/5 border border-gray-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-gray-500/20 hover:bg-gray-500/40 text-gray-600 dark:text-gray-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
<button type="button" _="on click toggle .hidden on `#player-queue-`{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-violet-500/10 border border-violet-400/30 text-violet-600 dark:text-violet-300 hover:bg-violet-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> New
</button>
<button type="button" _="on click toggle .hidden on `#player-lib-`{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-blue-500/10 border border-blue-400/30 text-blue-600 dark:text-blue-300 hover:bg-blue-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Existing
</button>
</div>
<div id="player-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-violet-500/5 border border-violet-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-violet-500/20 hover:bg-violet-500/40 text-violet-600 dark:text-violet-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#463371`] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
<div id="player-lib-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-blue-500/5 border border-blue-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-blue-500/20 hover:bg-blue-500/40 text-blue-600 dark:text-blue-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#7BAADF`] mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode (index .ItemMetadata "duplicate_path")}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
{{else}}
<div class="flex items-center gap-2">
<button type="button" _="on click toggle .hidden on `#player-queue-`{{.ID}}"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium bg-gray-500/10 border border-gray-400/30 text-gray-600 dark:text-gray-300 hover:bg-gray-500/20 transition-colors">
<i class="fas fa-music fa-xs"></i> Play
</button>
</div>
<div id="player-queue-{{.ID}}" class="audio-player hidden flex items-center gap-2.5 bg-gray-500/5 border border-gray-400/20 rounded-lg px-3 py-2 w-full">
<button onclick="toggleAudioPlayer(this)" class="play-btn flex-shrink-0 w-6 h-6 flex items-center justify-center rounded-full bg-gray-500/20 hover:bg-gray-500/40 text-gray-600 dark:text-gray-300 transition-colors">
<i class="fas fa-play fa-xs"></i>
</button>
<input type="range" aria-label="Seek audio position" oninput="seekAudioProgress(this)" class="flex-1 h-1 rounded-full cursor-pointer accent-gray-500 mx-1" value="0" min="0" max="100" step="0.1">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 tabular-nums w-8 text-right flex-shrink-0">0:00</span>
<audio src="/stream?path={{urlEncode .Track.Path}}" preload="none" ontimeupdate="updateAudioProgress(this)" onended="resetAudioPlayer(this)"></audio>
</div>
🧰 Tools
🪛 HTMLHint (1.9.2)

[warning] 51-51: The type attribute must be present on elements.

(button-type-require)


[warning] 55-55: The type attribute must be present on

elements.

(button-type-require)


[warning] 61-61: The type attribute must be present on

elements.

(button-type-require)


[warning] 69-69: The type attribute must be present on

elements.

(button-type-require)


[error] 74-74: Special characters must be escaped : [ < ].

(spec-char-escape)


[error] 74-74: Special characters must be escaped : [ > ].

(spec-char-escape)


[error] 74-74: Tag must be paired, no start tag: [ ]

(tag-pair)


[warning] 78-78: The type attribute must be present on

elements.

(button-type-require)


[error] 83-83: The id value [ player-queue-{{.ID}} ] must be unique.

(id-unique)


[warning] 84-84: The type attribute must be present on

elements.

(button-type-require)


[warning] 64-64: No matching [ label ] tag found.

(input-requires-label)


[warning] 72-72: No matching [ label ] tag found.

(input-requires-label)


[warning] 87-87: No matching [ label ] tag found.

(input-requires-label)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/importing/queue_items.html` around lines 51 - 90, Add explicit
type="button" to every <button> used for the audio controls (both the toggle
buttons that target `#player-queue-`{{.ID}} and `#player-lib-`{{.ID}} and the play
buttons that call toggleAudioPlayer(this)) to prevent accidental form
submission, and give each range input (those with
oninput="seekAudioProgress(this)") an accessible name by adding an aria-label or
aria-labelledby (e.g., "Seek playback" or a unique label per player using
{{.ID}}) so screen readers can identify the slider.

Source: Linters/SAST tools

Comment on lines +29 to +31
<input type="range" min="0" max="100" value="0" step="0.1"
class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
oninput="seekAudioProgress(this)">

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 | ⚡ Quick win

Add an accessible label to the audio seek slider.

Line 29 introduces a range input without an associated label, which breaks accessible form semantics.

Suggested fix
-      <input type="range" min="0" max="100" value="0" step="0.1"
+      <input type="range" min="0" max="100" value="0" step="0.1"
+             aria-label="Audio playback position"
              class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
              oninput="seekAudioProgress(this)">
📝 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
<input type="range" min="0" max="100" value="0" step="0.1"
class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
oninput="seekAudioProgress(this)">
<input type="range" min="0" max="100" value="0" step="0.1"
aria-label="Audio playback position"
class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
oninput="seekAudioProgress(this)">
🧰 Tools
🪛 HTMLHint (1.9.2)

[warning] 29-29: No matching [ label ] tag found.

(input-requires-label)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/library/track_overview_panel.html` around lines 29 - 31, The range
input used for audio seeking lacks an accessible label; add an explicit label
for the slider by giving the input a unique id (e.g., seek-slider) and either
add a visible <label for="seek-slider">Seek audio</label> or an
aria-label/aria-labelledby attribute (e.g., aria-label="Seek audio") so screen
readers can identify the control referenced by the seekAudioProgress handler;
ensure the label text clearly describes the control and that the input retains
its existing attributes and oninput="seekAudioProgress(this)".

Source: Linters/SAST tools

Comment on lines +155 to +157
audio.play();
icon.className = 'fas fa-pause fa-xs';
} else {

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 | ⚡ Quick win

Handle audio.play() rejection to keep UI state consistent.

If playback fails, the icon stays on pause even though nothing is playing. Update icon only on successful play and reset on catch.

Suggested fix
-      audio.play();
-      icon.className = 'fas fa-pause fa-xs';
+      audio.play()
+        .then(function() {
+          icon.className = 'fas fa-pause fa-xs';
+        })
+        .catch(function() {
+          icon.className = 'fas fa-play fa-xs';
+        });
📝 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
audio.play();
icon.className = 'fas fa-pause fa-xs';
} else {
audio.play()
.then(function() {
icon.className = 'fas fa-pause fa-xs';
})
.catch(function() {
icon.className = 'fas fa-play fa-xs';
});
} else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/partials/scripts.html` around lines 155 - 157, The UI sets
icon.className to the pause icon immediately after calling audio.play(), which
leaves the UI inconsistent if playback is rejected; change the flow so
audio.play()'s returned Promise is handled: call audio.play().then(() => set
icon.className = 'fas fa-pause fa-xs') and in .catch(() => reset icon.className
to the play icon (and optionally surface/log the error)), ensuring the icon is
updated only on successful play and restored on failure to avoid unhandled
promise rejections.

Comment thread views/partials/sidebar.html Outdated
Comment thread views/tag/edit_form.html
Comment on lines +128 to +131
<input type="range" min="0" max="100" value="0" step="0.1"
class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
oninput="seekAudioProgress(this)">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 w-8 text-right tabular-nums">0:00</span>

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 | ⚡ Quick win

Add an accessible name to the seek slider.

The range input is unlabeled, so assistive tech announces it without context.

💡 Proposed fix
-            <input type="range" min="0" max="100" value="0" step="0.1"
+            <input type="range" min="0" max="100" value="0" step="0.1"
+                   aria-label="Seek audio position"
                    class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
                    oninput="seekAudioProgress(this)">
📝 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
<input type="range" min="0" max="100" value="0" step="0.1"
class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
oninput="seekAudioProgress(this)">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 w-8 text-right tabular-nums">0:00</span>
<input type="range" min="0" max="100" value="0" step="0.1"
aria-label="Seek audio position"
class="flex-1 h-1 mx-1 accent-gray-500 cursor-pointer"
oninput="seekAudioProgress(this)">
<span class="time-display text-xs text-gray-500 dark:text-gray-400 w-8 text-right tabular-nums">0:00</span>
🧰 Tools
🪛 HTMLHint (1.9.2)

[warning] 128-128: No matching [ label ] tag found.

(input-requires-label)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/tag/edit_form.html` around lines 128 - 131, The range input used for
audio seeking is missing an accessible name, so update the input element (the
range slider tied to the seekAudioProgress function) to provide one — either add
an aria-label like "Seek audio" or add aria-labelledby pointing to a nearby
visible label/span that describes the control (e.g., the time-display span or a
new label element); ensure the label text clearly indicates it's the audio seek
slider so screen readers announce context when the slider is focused.

Source: Linters/SAST tools

@contre95 contre95 closed this Jun 7, 2026
@contre95 contre95 deleted the dev branch June 7, 2026 14:22
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