feat: add recent plays history modal
- GET /api/me/recent endpoint returning last 50 play events with track and artist info - RecentPlays modal component with time-ago display - "Recent plays" button in user dropdown menu - Clicking a track in history starts playback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,38 @@ pub async fn record_play_event(
|
||||
Ok(result.rows_affected() > 0)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, sqlx::FromRow)]
|
||||
pub struct RecentPlay {
|
||||
pub track_slug: String,
|
||||
pub track_title: String,
|
||||
pub artist_name: String,
|
||||
pub album_slug: Option<String>,
|
||||
pub played_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
pub async fn recent_plays(
|
||||
pool: &PgPool,
|
||||
user_id: &str,
|
||||
limit: i32,
|
||||
) -> Result<Vec<RecentPlay>, sqlx::Error> {
|
||||
sqlx::query_as::<_, RecentPlay>(
|
||||
r#"SELECT t.slug AS track_slug, t.title AS track_title,
|
||||
ar.name AS artist_name, al.slug AS album_slug,
|
||||
pe.played_at
|
||||
FROM play_events pe
|
||||
JOIN tracks t ON pe.track_id = t.id
|
||||
JOIN artists ar ON t.artist_id = ar.id
|
||||
LEFT JOIN albums al ON t.album_id = al.id
|
||||
WHERE pe.user_id = $1
|
||||
ORDER BY pe.played_at DESC
|
||||
LIMIT $2"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(limit)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
// --- Queries ---
|
||||
|
||||
pub async fn list_artists(pool: &PgPool) -> Result<Vec<ArtistListItem>, sqlx::Error> {
|
||||
|
||||
@@ -295,6 +295,16 @@ pub async fn search(State(state): State<S>, Query(q): Query<SearchQuery>) -> imp
|
||||
|
||||
// --- Play tracking ---
|
||||
|
||||
pub async fn recent_plays(
|
||||
State(state): State<S>,
|
||||
Extension(user): Extension<AuthUser>,
|
||||
) -> impl IntoResponse {
|
||||
match db::recent_plays(&state.pool, &user.id, 50).await {
|
||||
Ok(plays) => (StatusCode::OK, Json(serde_json::to_value(plays).unwrap())).into_response(),
|
||||
Err(e) => error_json(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn record_play(
|
||||
State(state): State<S>,
|
||||
Path(slug): Path<String>,
|
||||
|
||||
@@ -30,7 +30,8 @@ pub fn build_router(state: Arc<AppState>) -> Router {
|
||||
.route("/tracks/:slug/cover", get(api::track_cover))
|
||||
.route("/stream/:slug", get(api::stream_track))
|
||||
.route("/search", get(api::search))
|
||||
.route("/tracks/:slug/play", post(api::record_play));
|
||||
.route("/tracks/:slug/play", post(api::record_play))
|
||||
.route("/me/recent", get(api::recent_plays));
|
||||
|
||||
let api = Router::new()
|
||||
.nest("/api", library);
|
||||
|
||||
Reference in New Issue
Block a user