更新点歌和弹幕微调

This commit is contained in:
2026-08-19 12:30:30 -07:00
parent a36d511d39
commit 845b0f5900
43 changed files with 1903 additions and 766 deletions
+39 -115
View File
@@ -128,34 +128,20 @@ pub enum SongCommand {
title: String,
normalized_title: String,
},
Rate(u8),
}
/// Parse only explicit commands separated from their argument by whitespace.
/// This avoids treating ordinary words such as “点歌姬” as queue mutations.
/// Parse a request prefix followed immediately by a title or by whitespace and
/// a title. Whitespace inside the title is normalized before deduplication.
pub fn parse_command(text: &str) -> Option<SongCommand> {
let canonical = collapse_whitespace(text);
let (command, argument) = canonical.split_once(' ')?;
match command {
"点歌" => {
if argument.is_empty() || argument.chars().count() > 80 {
return None;
}
Some(SongCommand::Request {
title: argument.to_owned(),
normalized_title: argument.to_lowercase(),
})
}
"打分" => match argument {
"1" => Some(SongCommand::Rate(1)),
"2" => Some(SongCommand::Rate(2)),
"3" => Some(SongCommand::Rate(3)),
"4" => Some(SongCommand::Rate(4)),
"5" => Some(SongCommand::Rate(5)),
_ => None,
},
_ => None,
let argument = canonical.strip_prefix("点歌")?.trim_start();
if argument.is_empty() || argument.chars().count() > 80 {
return None;
}
Some(SongCommand::Request {
title: argument.to_owned(),
normalized_title: argument.to_lowercase(),
})
}
fn collapse_whitespace(value: &str) -> String {
@@ -180,8 +166,6 @@ pub struct SongRequestItem {
pub requested_at: DateTime<Utc>,
pub started_at: Option<DateTime<Utc>>,
pub finished_at: Option<DateTime<Utc>>,
pub average_score: Option<f64>,
pub rating_count: i64,
}
#[derive(Clone, Debug, Default, Serialize)]
@@ -191,7 +175,6 @@ pub struct SongQueueSummary {
pub queued_count: i64,
pub completed_count: i64,
pub cancelled_count: i64,
pub rating_count: i64,
}
#[derive(Clone, Debug, Serialize)]
@@ -241,25 +224,20 @@ impl SongRequestService {
};
let settings: SongRequestSettings = serde_json::from_value(component.settings.clone())
.map_err(|error| SongRequestError::Invalid(error.to_string()))?;
let result = match command {
SongCommand::Request {
title,
normalized_title,
} => {
self.request_song(
component,
&danmaku.viewer,
&title,
&normalized_title,
event.id,
&settings,
)
.await?
}
SongCommand::Rate(score) => {
self.rate_current(component, &danmaku.viewer, score).await?
}
};
let SongCommand::Request {
title,
normalized_title,
} = command;
let result = self
.request_song(
component,
&danmaku.viewer,
&title,
&normalized_title,
event.id,
&settings,
)
.await?;
if let Some(change) = result {
self.publish_change(component, &event.room_id, change)?;
}
@@ -299,7 +277,7 @@ impl SongRequestService {
};
let sql = format!(
"{} WHERE r.component_instance_id=$1 AND {status_filter} \
GROUP BY r.id ORDER BY {order} OFFSET $2 LIMIT $3",
ORDER BY {order} OFFSET $2 LIMIT $3",
item_select()
);
let rows = transaction
@@ -342,7 +320,7 @@ impl SongRequestService {
.query(
&format!(
"{} WHERE r.component_instance_id=$1 AND r.status='queued' \
GROUP BY r.id ORDER BY r.queue_position ASC",
ORDER BY r.queue_position ASC",
item_select()
),
&[&component.id],
@@ -671,59 +649,6 @@ impl SongRequestService {
}))
}
async fn rate_current(
&self,
component: &ComponentInstance,
viewer: &PlatformViewer,
score: u8,
) -> Result<Option<SongQueueChange>, SongRequestError> {
let viewer_uid = bounded_identity(&viewer.uid, 64)?;
let viewer_name = bounded_identity(&viewer.name, 80)?;
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 Some(row) = transaction
.query_opt(
"SELECT id FROM song_requests WHERE component_instance_id=$1 AND status='current' FOR UPDATE",
&[&component.id],
)
.await?
else {
return Ok(None);
};
let request_id: Uuid = row.get(0);
let score = i16::from(score);
transaction
.execute(
"INSERT INTO song_ratings \
(id,owner_user_id,component_instance_id,song_request_id,viewer_uid,viewer_name,score) \
VALUES($1,$2,$3,$4,$5,$6,$7) \
ON CONFLICT(song_request_id,viewer_uid) DO UPDATE \
SET score=EXCLUDED.score,viewer_name=EXCLUDED.viewer_name,updated_at=now()",
&[
&Uuid::new_v4(),
&component.owner_id,
&component.id,
&request_id,
&viewer_uid,
&viewer_name,
&score,
],
)
.await?;
let revision = bump_revision(&transaction, component.id).await?;
let current = current_item(&transaction, component.id).await?;
transaction.commit().await?;
Ok(Some(SongQueueChange {
revision,
operation: "rating-updated",
item_id: request_id,
item: current.clone(),
current,
}))
}
fn publish_change(
&self,
component: &ComponentInstance,
@@ -846,8 +771,7 @@ async fn promote_next(
fn item_select() -> &'static str {
"SELECT r.id,r.song_title,r.requester_uid,r.requester_name,r.status,r.queue_position,\
r.requested_at,r.started_at,r.finished_at,avg(v.score)::double precision,count(v.id)::BIGINT \
FROM song_requests r LEFT JOIN song_ratings v ON v.song_request_id=r.id"
r.requested_at,r.started_at,r.finished_at FROM song_requests r"
}
fn item_from_row(row: &Row) -> SongRequestItem {
@@ -863,8 +787,6 @@ fn item_from_row(row: &Row) -> SongRequestItem {
requested_at: row.get(6),
started_at: row.get(7),
finished_at: row.get(8),
average_score: row.get(9),
rating_count: row.get(10),
}
}
@@ -873,10 +795,7 @@ async fn request_item(
request_id: Uuid,
) -> Result<Option<SongRequestItem>, SongRequestError> {
Ok(transaction
.query_opt(
&format!("{} WHERE r.id=$1 GROUP BY r.id", item_select()),
&[&request_id],
)
.query_opt(&format!("{} WHERE r.id=$1", item_select()), &[&request_id])
.await?
.map(|row| item_from_row(&row)))
}
@@ -888,7 +807,7 @@ async fn current_item(
Ok(transaction
.query_opt(
&format!(
"{} WHERE r.component_instance_id=$1 AND r.status='current' GROUP BY r.id",
"{} WHERE r.component_instance_id=$1 AND r.status='current'",
item_select()
),
&[&component_id],
@@ -906,8 +825,7 @@ async fn queue_summary(
"SELECT count(*) FILTER (WHERE status IN ('current','queued'))::BIGINT,\
count(*) FILTER (WHERE status='queued')::BIGINT,\
count(*) FILTER (WHERE status='completed')::BIGINT,\
count(*) FILTER (WHERE status='cancelled')::BIGINT,\
(SELECT count(*) FROM song_ratings WHERE component_instance_id=$1)::BIGINT \
count(*) FILTER (WHERE status='cancelled')::BIGINT \
FROM song_requests WHERE component_instance_id=$1",
&[&component_id],
)
@@ -917,7 +835,6 @@ async fn queue_summary(
queued_count: row.get(1),
completed_count: row.get(2),
cancelled_count: row.get(3),
rating_count: row.get(4),
})
}
@@ -1079,7 +996,7 @@ mod tests {
use super::*;
#[test]
fn parses_requests_and_scores_with_normalized_whitespace() {
fn parses_only_requests_with_normalized_whitespace() {
assert_eq!(
parse_command(" 点歌 My Song "),
Some(SongCommand::Request {
@@ -1087,11 +1004,18 @@ mod tests {
normalized_title: "my song".into(),
})
);
assert_eq!(parse_command("打分 5"), Some(SongCommand::Rate(5)));
assert_eq!(
parse_command("点歌夜曲"),
Some(SongCommand::Request {
title: "夜曲".into(),
normalized_title: "夜曲".into(),
})
);
assert_eq!(parse_command("打分 5"), None);
assert_eq!(parse_command("打分 0"), None);
assert_eq!(parse_command("点歌姬"), None);
assert_eq!(parse_command("点歌"), None);
assert_eq!(parse_command(&format!("点歌 {}", "歌".repeat(81))), None);
assert_eq!(parse_command(&format!("点歌{}", "歌".repeat(81))), None);
}
#[test]