This commit is contained in:
2026-08-06 10:38:07 -07:00
parent ac80cd1db4
commit 97a3f1be48
29 changed files with 1008 additions and 115 deletions
+69
View File
@@ -38,6 +38,12 @@ pub struct SongRequestSettings {
pub font_family: FontFamilyId,
#[serde(default = "default_font_brightness")]
pub font_brightness: u16,
/// Optional text-color overrides. `None` follows the selected theme so a
/// later theme switch can update the palette without stale saved colors.
#[serde(default)]
pub requester_color: Option<String>,
#[serde(default)]
pub song_title_color: Option<String>,
#[serde(default = "default_font_scale")]
pub font_scale: u16,
#[serde(default = "default_scroll_speed")]
@@ -61,6 +67,8 @@ impl Default for SongRequestSettings {
theme_id: OverlayThemeId::default(),
font_family: FontFamilyId::default(),
font_brightness: default_font_brightness(),
requester_color: None,
song_title_color: None,
font_scale: default_font_scale(),
scroll_speed_pixels_per_second: default_scroll_speed(),
edge_pause_seconds: default_edge_pause(),
@@ -75,6 +83,8 @@ impl SongRequestSettings {
pub fn sanitize(mut self) -> Self {
self.font_scale = self.font_scale.clamp(50, 250);
self.font_brightness = sanitize_font_brightness(self.font_brightness);
self.requester_color = sanitize_optional_hex_color(self.requester_color);
self.song_title_color = sanitize_optional_hex_color(self.song_title_color);
self.scroll_speed_pixels_per_second = self.scroll_speed_pixels_per_second.clamp(5, 200);
self.edge_pause_seconds = self.edge_pause_seconds.min(15);
self.max_queue_size = self.max_queue_size.min(10_000);
@@ -84,6 +94,14 @@ impl SongRequestSettings {
}
}
fn sanitize_optional_hex_color(value: Option<String>) -> Option<String> {
let value = value?.trim().to_ascii_uppercase();
(value.len() == 7
&& value.starts_with('#')
&& value[1..].bytes().all(|byte| byte.is_ascii_hexdigit()))
.then_some(value)
}
const fn default_font_scale() -> u16 {
100
}
@@ -465,6 +483,53 @@ impl SongRequestService {
)
}
/// Cancel every active request in one tenant-scoped transaction.
///
/// A single revision represents the whole reset. Publishing one reset
/// event also avoids exposing a half-cleared queue to OBS clients while a
/// long queue is being processed.
pub async fn clear_active(
&self,
component: &ComponentInstance,
room_id: &str,
) -> Result<u64, SongRequestError> {
let mut client = self.db.get().await?;
let transaction = client.transaction().await?;
Db::set_tenant(&transaction, component.owner_id).await?;
lock_state(&transaction, component).await?;
let cancelled_count = transaction
.execute(
"UPDATE song_requests SET status='cancelled',queue_position=0,finished_at=now() \
WHERE owner_user_id=$1 AND component_instance_id=$2 \
AND status IN ('current','queued')",
&[&component.owner_id, &component.id],
)
.await?;
if cancelled_count == 0 {
transaction.commit().await?;
return Ok(0);
}
let revision = bump_revision(&transaction, component.id).await?;
transaction.commit().await?;
self.hub.publish(
component.id,
Arc::new(ComponentMessage::new(
component,
room_id,
"song.queue.changed",
json!({
"revision": revision,
"operation": "cleared",
"itemId": null,
"item": null,
"current": null,
"cancelledCount": cancelled_count,
}),
)),
);
Ok(cancelled_count)
}
async fn request_song(
&self,
component: &ComponentInstance,
@@ -1031,6 +1096,8 @@ mod tests {
let bounded = SongRequestSettings {
font_scale: 999,
font_brightness: u16::MAX,
requester_color: Some(" #a1b2c3 ".into()),
song_title_color: Some("transparent".into()),
scroll_speed_pixels_per_second: 0,
edge_pause_seconds: 200,
max_queue_size: u32::MAX,
@@ -1041,6 +1108,8 @@ mod tests {
.sanitize();
assert_eq!(bounded.font_scale, 250);
assert_eq!(bounded.font_brightness, 180);
assert_eq!(bounded.requester_color.as_deref(), Some("#A1B2C3"));
assert_eq!(bounded.song_title_color, None);
assert_eq!(bounded.scroll_speed_pixels_per_second, 5);
assert_eq!(bounded.edge_pause_seconds, 15);
assert_eq!(bounded.max_queue_size, 10_000);