Skip to content

Streaming feature#165

Merged
contre95 merged 12 commits into
devfrom
feat/play_on_queue
Jun 7, 2026
Merged

Streaming feature#165
contre95 merged 12 commits into
devfrom
feat/play_on_queue

Conversation

@contre95

@contre95 contre95 commented Jun 6, 2026

Copy link
Copy Markdown
Owner

Note

This PR introduces a streaming feature. This feature intends not to turn Soulsolid into a player but to help during the tasks as figuring out if a song is the same as the other for duplicates queue items or playing a song when being preview/edited.

  • Adds a new streaming feature that serves audio files over HTTP (/stream/queue/:id, /stream/library/:id), with path traversal guards
    validating files stay within their allowed directories
  • Adds inline audio players to duplicate queue items (individual, grouped-by-album, grouped-by-artist views) so you can listen to both the new
    (download folder) and existing (library) file before deciding to replace or skip
  • Adds an always-visible audio player and download button to the track tag edit form
  • Slows down badge polling (every 1s → every 15s for import/lyrics queues, every 5s for active jobs) since HTMX events already update badges
    immediately on user action
image

Summary by CodeRabbit

  • New Features

    • Added audio streaming and preview across import queue, grouped views, tag editor, and library track panel.
    • Play/pause, seek, progress display, and reset controls for inline audio players.
  • Performance

    • Reduced HTMX polling frequency for queue and job badges (15s and 5s instead of 1s).
  • UI/UX

    • Import queue badge shows count only.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 07f239f5-b486-408c-a0f5-028e33254e5e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a symlink-safe streaming Service, a Fiber HTTP handler and single /stream route, locator methods on importing/library services, hosting wiring and template helpers, plus frontend audio players, JS controls, and reduced HTMX polling intervals.

Changes

Audio Streaming Feature

Layer / File(s) Summary
Streaming service core
src/features/streaming/streaming.go, src/features/streaming/service.go
Adds Service with containedIn symlink-safe containment checks, Stream(path) validating against configured library/download dirs, and mimeTypeFor (.flacaudio/flac, else audio/mpeg).
HTTP handler and route
src/features/streaming/handlers.go, src/features/streaming/routes.go
Adds Handler and NewHandler; Stream decodes path query, delegates to Service.Stream, returns 400/404 on errors, sets Content-Type and Accept-Ranges: bytes, and streams files via SendFile. Registers GET /stream.
Importing & library locators
src/features/importing/service.go, src/features/library/service.go
Adds GetPendingTrackPath(itemID string) (string, error) and GetLibraryTrackPath(ctx, trackID) (string, error) to expose filesystem paths used by the streaming service.
Server initialization & template helpers
src/features/hosting/server.go, src/main.go
Imports streaming, extends NewServer signature to accept streamingService, registers pathBase and urlEncode template funcs, registers streaming routes during server wiring, and constructs streamingService in main.
Queue & library player templates
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/tag/edit_form.html
Adds inline audio players that source /stream?path=<url-encoded-path>; duplicate queue items render New/Existing toggles showing queue vs existing audio sources; queue badge shows count only.
Client-side audio controls & polling updates
views/partials/scripts.html, views/partials/navbar.html, views/partials/sidebar.html
Adds toggleAudioPlayer, updateAudioProgress, seekAudioProgress, resetAudioPlayer; removes a conditional htmx.process call; reduces HTMX polling intervals (queue/lyrics 1s→15s, jobs 1s→5s).

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Streaming feature' is vague and generic, lacking specifics about what the streaming feature accomplishes or its context. Consider a more descriptive title that conveys the main purpose, such as 'Add audio streaming endpoints with path traversal guards' or 'Implement HTTP audio streaming for queued and library tracks'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

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

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

❤️ Share

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

@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

🤖 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/streaming/service.go`:
- Around line 31-32: Cache the cleaned path once and reuse it instead of calling
filepath.Clean(path) twice: compute cleanPath := filepath.Clean(path) and
replace both occurrences in the validation if (the Prefix check and the equality
check) with cleanPath; do the same refactor inside LibraryTrackStream (the
validation block around lines 44-47) to avoid redundant filepath.Clean calls and
improve efficiency.
- Around line 52-57: The mimeTypeFor function currently uses suffix matching and
misclassifies names like "file.flac.backup"; change mimeTypeFor to use
filepath.Ext to extract the actual extension, normalize it with strings.ToLower,
then switch/map on the extension (e.g., ".flac" -> "audio/flac", ".mp3" ->
"audio/mpeg") and keep a sensible default (audio/mpeg). Update references in
mimeTypeFor to import/path-qualify filepath and remove reliance on
strings.HasSuffix to make detection robust for future formats.
- Around line 30-34: The path containment check in
QueueTrackStream/LibraryTrackStream currently only uses filepath.Clean and then
passes the original path to c.SendFile, allowing symlink escapes; change the
validation to call filepath.Clean once, then call filepath.EvalSymlinks on both
the cleaned candidate path and the cleaned base (DownloadPath/LibraryPath) and
perform the containment check against the resolved base (e.g., ensure
resolvedCandidate has the resolvedBase+separator prefix or equals resolvedBase);
also eliminate repeated filepath.Clean(path) calls by reusing the cleaned value,
and tighten the MIME/extension detection logic used after validation by relying
on evaluated file extension/mime (e.g., use filepath.Ext and
mime.TypeByExtension or an explicit extension whitelist) rather than naive
HasSuffix(".flac") checks so renamed files cannot be misclassified.

In `@views/importing/queue_items_grouped_album.html`:
- Line 144: The audio progress range inputs (the <input type="range"> elements
that call seekAudioProgress(this) and use class "flex-1 h-1 rounded-full
cursor-pointer accent-[`#463371`] mx-1") need accessible labels: add an aria-label
(e.g., aria-label="Audio progress") to each of those range inputs (apply same
change to the other occurrences referenced in the comment) so screen readers can
announce the control; ensure the aria-label text describes the control (or make
it dynamic if it represents a specific track).

In `@views/importing/queue_items_grouped_artist.html`:
- Line 142: Add accessible labels to the audio progress range inputs by adding
an aria-label attribute (e.g., aria-label="Audio progress" or a more specific
label like "Track progress for {trackTitle}") to the <input type="range">
elements that call seekAudioProgress(this) (the elements with class "flex-1 h-1
rounded-full cursor-pointer accent-[`#463371`] mx-1"); apply the same aria-label
pattern to the other two range inputs referenced in the review so screen readers
can identify the sliders.

In `@views/importing/queue_items.html`:
- Line 64: Add accessible labels to the audio progress range inputs by adding an
aria-label attribute (e.g., aria-label="Audio progress" or a more specific label
like "Playback position") to each <input type="range"> that uses the
seekAudioProgress handler; update the inputs referenced (the element with
oninput="seekAudioProgress(this)" and the similar inputs at the other
occurrences) so screen readers can announce the control purpose while preserving
existing attributes (value, min, max, step, class, oninput).

In `@views/tag/edit_form.html`:
- Around line 128-131: The range input for audio progress in edit_form.html is
missing an accessible label; add an aria-label (e.g., aria-label="Audio
progress" or a localized string) to the <input type="range"> so screen readers
can identify it, keeping the existing attributes and
oninput="seekAudioProgress(this)"; ensure the label accurately describes the
control and update any related docs or tests if they rely on query selectors for
unlabeled inputs.
- Line 119: The download attribute currently uses the full path via
{{.Track.Path}} which can yield ugly absolute paths; update the anchor element
(the link with href "/stream/library/{{.Track.ID}}") to use only the base
filename for download by replacing {{.Track.Path}} with a basename extractor
(e.g. use an existing template func like {{basename .Track.Path}} or register a
helper that calls path.Base and then use {{pathBase .Track.Path}}); if no helper
exists, add one to the template funcs and use that here so the download
attribute contains only the filename (e.g. "track.flac") instead of the full
path.
🪄 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: 2c2c850f-ffa2-42a2-bd24-647bc5cb9bf2

📥 Commits

Reviewing files that changed from the base of the PR and between 4661eb5 and 37c4ebc.

📒 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/features/streaming/streaming.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/partials/navbar.html
  • views/partials/scripts.html
  • views/partials/sidebar.html
  • views/tag/edit_form.html

Comment thread src/features/streaming/service.go Outdated
Comment thread src/features/streaming/service.go Outdated
Comment thread src/features/streaming/service.go Outdated
Comment thread views/importing/queue_items_grouped_album.html
<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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add aria-label attributes to audio progress range inputs for accessibility.

The progress sliders lack accessible labels for screen reader users.

♿ Suggested accessibility fix
-<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" oninput="seekAudioProgress(this)" aria-label="Audio progress" class="flex-1 h-1 rounded-full cursor-pointer accent-[`#463371`] mx-1" value="0" min="0" max="100" step="0.1">

Apply the same pattern to lines 150 and 165.

Also applies to: 150-150, 165-165

🧰 Tools
🪛 HTMLHint (1.9.2)

[warning] 142-142: 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_artist.html` at line 142, Add accessible
labels to the audio progress range inputs by adding an aria-label attribute
(e.g., aria-label="Audio progress" or a more specific label like "Track progress
for {trackTitle}") to the <input type="range"> elements that call
seekAudioProgress(this) (the elements with class "flex-1 h-1 rounded-full
cursor-pointer accent-[`#463371`] mx-1"); apply the same aria-label pattern to the
other two range inputs referenced in the review so screen readers can identify
the sliders.

Comment thread views/importing/queue_items.html
Comment thread views/tag/edit_form.html Outdated
Comment thread views/tag/edit_form.html
@contre95 contre95 added the new feature New feature request label Jun 6, 2026
@contre95 contre95 changed the title Feat/play on queue Streaming feature Jun 6, 2026

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

🤖 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 `@views/library/track_overview_panel.html`:
- Around line 33-35: The audio element in the template lacks an error handler so
stream load failures provide no user feedback; add an onerror attribute (e.g.
onerror="handleAudioError(this, event)") to the <audio> tag and implement a
corresponding handler function (handleAudioError) that inspects the event,
displays a user-facing message or hides/cleans up the player, and logs the
error; hook this handler alongside the existing updateAudioProgress(this) and
resetAudioPlayer(this) usage so failures are handled consistently with those
functions.
- Line 29: The range input in track_overview_panel.html (the <input type="range"
min="0" max="100" value="0" step="0.1"> element) is missing an accessible label;
add an aria-label attribute to that input (for example aria-label="Track level"
or an appropriate description like "Track volume" or "Track gain") so screen
readers can announce the control's purpose, e.g. update the input element to
include aria-label="..." matching the UI semantics.
🪄 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: 4147cc03-1ad1-4610-ad3c-ef13530f51c4

📥 Commits

Reviewing files that changed from the base of the PR and between 0b0992b and 4158e2b.

📒 Files selected for processing (2)
  • views/library/track_overview_panel.html
  • views/partials/scripts.html
💤 Files with no reviewable changes (1)
  • views/partials/scripts.html

Comment thread views/library/track_overview_panel.html
Comment thread views/library/track_overview_panel.html Outdated
Comment on lines +33 to +35
<audio src="/stream/library/{{.Track.ID}}" 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

Add error handler for audio loading failures.

The <audio> element has no onerror handler. If the stream fails to load (network error, file not found, unsupported format), users receive no feedback. Consider adding an onerror handler to provide visual feedback or gracefully hide the player.

🛡️ Proposed fix
       <audio src="/stream/library/{{.Track.ID}}" preload="none"
              ontimeupdate="updateAudioProgress(this)"
-             onended="resetAudioPlayer(this)"></audio>
+             onended="resetAudioPlayer(this)"
+             onerror="this.closest('.audio-player').style.display='none'"></audio>
📝 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 src="/stream/library/{{.Track.ID}}" preload="none"
ontimeupdate="updateAudioProgress(this)"
onended="resetAudioPlayer(this)"></audio>
<audio src="/stream/library/{{.Track.ID}}" preload="none"
ontimeupdate="updateAudioProgress(this)"
onended="resetAudioPlayer(this)"
onerror="this.closest('.audio-player').style.display='none'"></audio>
🤖 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 33 - 35, The audio
element in the template lacks an error handler so stream load failures provide
no user feedback; add an onerror attribute (e.g. onerror="handleAudioError(this,
event)") to the <audio> tag and implement a corresponding handler function
(handleAudioError) that inspects the event, displays a user-facing message or
hides/cleans up the player, and logs the error; hook this handler alongside the
existing updateAudioProgress(this) and resetAudioPlayer(this) usage so failures
are handled consistently with those functions.

Comment thread src/features/library/service.go
Comment thread src/features/streaming/handlers.go Outdated
}

// StreamQueueItem streams a pending track from the download folder.
func (h *Handler) StreamQueueItem(c *fiber.Ctx) error {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

why queue item, just stream a son.Either from path or from library

Comment thread src/features/streaming/service.go Outdated
Comment thread src/features/streaming/service.go Outdated
// Service handles audio streaming from both the download folder and the library.
type Service struct {
queue QueueLocator
library LibraryLocator

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

why locator? shouldn't this be on the domain? or referenced from it ?

Comment thread src/features/streaming/service.go Outdated
}

// QueueTrackStream returns the validated file path and MIME type for a pending queue item.
func (s *Service) QueueTrackStream(itemID string) (string, string, error) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same comment as above. it shouls either be downñpad path or library. Queue items are just a consequence of that use case that should be handled by the handlers

Comment thread src/features/streaming/streaming.go Outdated
@@ -0,0 +1,13 @@
package streaming

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This entire file is probably not needed. We can already get the file from the library service or the importing service

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

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)

139-151: ⚠️ Potential issue | 🟠 Major

Fix server lifecycle: Start() is blocking, so the signal/shutdown wiring is currently unreachable and Start() is effectively started twice

server.Start() delegates to Fiber’s app.Listen(...) (blocking). The synchronous call at src/main.go lines ~139-142 prevents the code from ever reaching signal.Notify(...), the select { ... }, and server.Shutdown() (lines ~143-166). The goroutine server.Start() (lines ~146-151) only runs if the first Start() returns immediately with an error, and can then cause redundant/double bind attempts.

🤖 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 139 - 151, The current code calls server.Start()
synchronously then again in a goroutine, causing blocking and double-start
issues; remove the initial synchronous call and start the server exactly once
inside the goroutine that writes errors to serverErr (keep the serverErr channel
and quit channel created), call signal.Notify(quit, os.Interrupt) before
launching the goroutine, then use a select to wait on quit or serverErr and call
server.Shutdown() on shutdown path; update logging to reflect single startup via
slog.Info in the goroutine and ensure any error returned from server.Start() is
forwarded to serverErr for the select to handle.
🤖 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/streaming/handlers.go`:
- Around line 22-24: The code double-decodes the query param by calling
url.QueryUnescape on c.Query("path"); remove the url.QueryUnescape call and the
net/url import, use the value returned by c.Query("path") directly (e.g.,
replace rawPath/path logic with a single value from c.Query("path")), and delete
the now-unnecessary err handling around QueryUnescape so streaming lookups use
the original decoded query string.

In `@src/features/streaming/service.go`:
- Around line 40-49: The Stream method currently allows any file under allowed
directories and relies on mimeTypeFor to default unknown extensions to
audio/mpeg; change Stream (Service.Stream) to enforce an explicit audio
extension allowlist (e.g., .mp3, .wav, .flac, .aac, .m4a, etc.) after resolving
the path with containedIn, and return an error if the file extension is not in
the allowlist; update callers or mimeTypeFor usage so mimeTypeFor is only
invoked for allowed extensions and do not treat unknown extensions as
audio/mpeg.

---

Outside diff comments:
In `@src/main.go`:
- Around line 139-151: The current code calls server.Start() synchronously then
again in a goroutine, causing blocking and double-start issues; remove the
initial synchronous call and start the server exactly once inside the goroutine
that writes errors to serverErr (keep the serverErr channel and quit channel
created), call signal.Notify(quit, os.Interrupt) before launching the goroutine,
then use a select to wait on quit or serverErr and call server.Shutdown() on
shutdown path; update logging to reflect single startup via slog.Info in the
goroutine and ensure any error returned from server.Start() is forwarded to
serverErr for the select to handle.
🪄 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: 44a9f0a1-85a2-4d95-8d6e-24e61a74097d

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6c664 and ce20247.

📒 Files selected for processing (11)
  • src/features/hosting/server.go
  • src/features/streaming/handlers.go
  • src/features/streaming/routes.go
  • src/features/streaming/service.go
  • src/features/streaming/streaming.go
  • src/main.go
  • 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/tag/edit_form.html
💤 Files with no reviewable changes (1)
  • src/features/streaming/streaming.go

Comment thread src/features/streaming/handlers.go
Comment thread src/features/streaming/service.go
@contre95 contre95 changed the base branch from main to dev June 7, 2026 11:52
@contre95 contre95 merged commit 695d6ac into dev Jun 7, 2026
1 check passed
@contre95 contre95 deleted the feat/play_on_queue branch June 7, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant