更新点歌和弹幕微调
This commit is contained in:
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user