Merge pull request 'Implement history' (#98) from 41-implement-history into main
Reviewed-on: LibreTunes/LibreTunes#98
This commit is contained in:
commit
b9551e5bee
162
Cargo.lock
generated
162
Cargo.lock
generated
@ -308,7 +308,7 @@ version = "0.18.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f"
|
checksum = "c878c71c2821aa2058722038a59a67583a4240524687c6028571c9b395ded61f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.14.4",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
@ -377,6 +377,15 @@ dependencies = [
|
|||||||
"half",
|
"half",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codee"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d3ad3122b0001c7f140cf4d605ef9a9e2c24d96ab0b4fb4347b76de2425f445"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collection_literals"
|
name = "collection_literals"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
@ -499,8 +508,18 @@ version = "0.14.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.14.4",
|
||||||
"darling_macro",
|
"darling_macro 0.14.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.10",
|
||||||
|
"darling_macro 0.20.10",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -513,21 +532,46 @@ dependencies = [
|
|||||||
"ident_case",
|
"ident_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim 0.10.0",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim 0.11.1",
|
||||||
|
"syn 2.0.58",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.14.4"
|
version = "0.14.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.14.4",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.10",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.58",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@ -541,6 +585,18 @@ dependencies = [
|
|||||||
"parking_lot_core",
|
"parking_lot_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "default-struct-builder"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8fa90da96b8fd491f5754d1f7a731f73921e3b7aa0ce333c821a0e43666ac14"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.20.10",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.58",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.3.11"
|
||||||
@ -620,6 +676,17 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.58",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenv"
|
name = "dotenv"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
@ -874,6 +941,18 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gloo-timers"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gloo-utils"
|
name = "gloo-utils"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -1305,9 +1384,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.69"
|
version = "0.3.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@ -1338,6 +1417,29 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-use"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ab8914bd0ff8ab5029521540a6e15292dcc05d0f1a791a3aa8cc31a94436bfb"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"codee",
|
||||||
|
"cookie",
|
||||||
|
"default-struct-builder",
|
||||||
|
"futures-util",
|
||||||
|
"gloo-timers",
|
||||||
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"leptos",
|
||||||
|
"paste",
|
||||||
|
"thiserror",
|
||||||
|
"unic-langid",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_axum"
|
name = "leptos_axum"
|
||||||
version = "0.6.10"
|
version = "0.6.10"
|
||||||
@ -1587,6 +1689,7 @@ dependencies = [
|
|||||||
"icondata",
|
"icondata",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
"leptos-use",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_icons",
|
"leptos_icons",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
@ -2477,6 +2580,12 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -2580,18 +2689,18 @@ checksum = "384595c11a4e2969895cad5a8c4029115f5ab956a9e5ef4de79d11a426e5f20c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.58"
|
version = "1.0.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.58"
|
version = "1.0.63"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -2629,6 +2738,15 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@ -2951,6 +3069,24 @@ version = "1.17.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid-impl"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5"
|
||||||
|
dependencies = [
|
||||||
|
"tinystr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.7.0"
|
version = "2.7.0"
|
||||||
@ -3141,9 +3277,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.69"
|
version = "0.3.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
|
@ -40,6 +40,7 @@ multer = { version = "3.0.0", optional = true }
|
|||||||
log = { version = "0.4.21", optional = true }
|
log = { version = "0.4.21", optional = true }
|
||||||
flexi_logger = { version = "0.28.0", optional = true, default-features = false }
|
flexi_logger = { version = "0.28.0", optional = true, default-features = false }
|
||||||
web-sys = "0.3.69"
|
web-sys = "0.3.69"
|
||||||
|
leptos-use = "0.13.5"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" }
|
gloo-net = { git = "https://github.com/rustwasm/gloo.git", rev = "a823fab7ecc4068e9a28bd669da5eaf3f0a56380" }
|
||||||
@ -74,6 +75,7 @@ ssr = [
|
|||||||
"multer",
|
"multer",
|
||||||
"log",
|
"log",
|
||||||
"flexi_logger",
|
"flexi_logger",
|
||||||
|
"leptos-use/ssr",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Defines a size-optimized profile for the WASM bundle in release mode
|
# Defines a size-optimized profile for the WASM bundle in release mode
|
||||||
|
2
migrations/2024-05-19-163229_add_song_history/down.sql
Normal file
2
migrations/2024-05-19-163229_add_song_history/down.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
DROP INDEX song_history_user_id_idx;
|
||||||
|
DROP TABLE song_history;
|
8
migrations/2024-05-19-163229_add_song_history/up.sql
Normal file
8
migrations/2024-05-19-163229_add_song_history/up.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
CREATE TABLE song_history (
|
||||||
|
id SERIAL PRIMARY KEY UNIQUE NOT NULL,
|
||||||
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
song_id INTEGER REFERENCES songs(id) ON DELETE CASCADE NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX song_history_user_id_idx ON song_history(user_id);
|
44
src/api/history.rs
Normal file
44
src/api/history.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
use leptos::*;
|
||||||
|
use crate::models::HistoryEntry;
|
||||||
|
use crate::models::Song;
|
||||||
|
|
||||||
|
use cfg_if::cfg_if;
|
||||||
|
|
||||||
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "ssr")] {
|
||||||
|
use leptos::server_fn::error::NoCustomError;
|
||||||
|
use crate::database::get_db_conn;
|
||||||
|
use crate::auth::get_user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the history of the current user.
|
||||||
|
#[server(endpoint = "history/get")]
|
||||||
|
pub async fn get_history(limit: Option<i64>) -> Result<Vec<HistoryEntry>, ServerFnError> {
|
||||||
|
let user = get_user().await?;
|
||||||
|
let db_con = &mut get_db_conn();
|
||||||
|
let history = user.get_history(limit, db_con)
|
||||||
|
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting history: {}", e)))?;
|
||||||
|
Ok(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the listen dates and songs of the current user.
|
||||||
|
#[server(endpoint = "history/get_songs")]
|
||||||
|
pub async fn get_history_songs(limit: Option<i64>) -> Result<Vec<(SystemTime, Song)>, ServerFnError> {
|
||||||
|
let user = get_user().await?;
|
||||||
|
let db_con = &mut get_db_conn();
|
||||||
|
let songs = user.get_history_songs(limit, db_con)
|
||||||
|
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error getting history songs: {}", e)))?;
|
||||||
|
Ok(songs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a song to the history of the current user.
|
||||||
|
#[server(endpoint = "history/add")]
|
||||||
|
pub async fn add_history(song_id: i32) -> Result<(), ServerFnError> {
|
||||||
|
let user = get_user().await?;
|
||||||
|
let db_con = &mut get_db_conn();
|
||||||
|
user.add_history(song_id, db_con)
|
||||||
|
.map_err(|e| ServerFnError::<NoCustomError>::ServerError(format!("Error adding history: {}", e)))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
|
pub mod history;
|
||||||
pub mod songs;
|
pub mod songs;
|
||||||
|
157
src/models.rs
157
src/models.rs
@ -46,6 +46,146 @@ pub struct User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
|
/// Get the history of songs listened to by this user from the database
|
||||||
|
///
|
||||||
|
/// The returned history will be ordered by date in descending order,
|
||||||
|
/// and a limit of N will select the N most recent entries.
|
||||||
|
/// The `id` field of this user must be present (Some) to get history
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `limit` - An optional limit on the number of history entries to return
|
||||||
|
/// * `conn` - A mutable reference to a database connection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Result<Vec<HistoryEntry>, Box<dyn Error>>` -
|
||||||
|
/// A result indicating success with a vector of history entries, or an error
|
||||||
|
///
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub fn get_history(self: &Self, limit: Option<i64>, conn: &mut PgPooledConn) ->
|
||||||
|
Result<Vec<HistoryEntry>, Box<dyn Error>> {
|
||||||
|
use crate::schema::song_history::dsl::*;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("Artist id must be present (Some) to get history")?;
|
||||||
|
|
||||||
|
let my_history =
|
||||||
|
if let Some(limit) = limit {
|
||||||
|
song_history
|
||||||
|
.filter(user_id.eq(my_id))
|
||||||
|
.order(date.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.load(conn)?
|
||||||
|
} else {
|
||||||
|
song_history
|
||||||
|
.filter(user_id.eq(my_id))
|
||||||
|
.load(conn)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(my_history)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the history of songs listened to by this user from the database
|
||||||
|
///
|
||||||
|
/// The returned history will be ordered by date in descending order,
|
||||||
|
/// and a limit of N will select the N most recent entries.
|
||||||
|
/// The `id` field of this user must be present (Some) to get history
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `limit` - An optional limit on the number of history entries to return
|
||||||
|
/// * `conn` - A mutable reference to a database connection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Result<Vec<(SystemTime, Song)>, Box<dyn Error>>` -
|
||||||
|
/// A result indicating success with a vector of listen dates and songs, or an error
|
||||||
|
///
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub fn get_history_songs(self: &Self, limit: Option<i64>, conn: &mut PgPooledConn) ->
|
||||||
|
Result<Vec<(SystemTime, Song)>, Box<dyn Error>> {
|
||||||
|
use crate::schema::songs::dsl::*;
|
||||||
|
use crate::schema::song_history::dsl::*;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("Artist id must be present (Some) to get history")?;
|
||||||
|
|
||||||
|
let my_history =
|
||||||
|
if let Some(limit) = limit {
|
||||||
|
song_history
|
||||||
|
.inner_join(songs)
|
||||||
|
.filter(user_id.eq(my_id))
|
||||||
|
.order(date.desc())
|
||||||
|
.limit(limit)
|
||||||
|
.select((date, songs::all_columns()))
|
||||||
|
.load(conn)?
|
||||||
|
} else {
|
||||||
|
song_history
|
||||||
|
.inner_join(songs)
|
||||||
|
.filter(user_id.eq(my_id))
|
||||||
|
.order(date.desc())
|
||||||
|
.select((date, songs::all_columns()))
|
||||||
|
.load(conn)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(my_history)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a song to this user's history in the database
|
||||||
|
///
|
||||||
|
/// The date of the history entry will be the current time
|
||||||
|
/// The `id` field of this user must be present (Some) to add history
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `song_id` - The id of the song to add to this user's history
|
||||||
|
/// * `conn` - A mutable reference to a database connection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Result<(), Box<dyn Error>>` - A result indicating success with an empty value, or an error
|
||||||
|
///
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub fn add_history(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result<(), Box<dyn Error>> {
|
||||||
|
use crate::schema::song_history;
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("Artist id must be present (Some) to add history")?;
|
||||||
|
|
||||||
|
diesel::insert_into(song_history::table)
|
||||||
|
.values((song_history::user_id.eq(my_id), song_history::song_id.eq(song_id)))
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this user has listened to a song
|
||||||
|
///
|
||||||
|
/// The `id` field of this user must be present (Some) to check history
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `song_id` - The id of the song to check if this user has listened to
|
||||||
|
/// * `conn` - A mutable reference to a database connection
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// * `Result<bool, Box<dyn Error>>` - A result indicating success with a boolean value, or an error
|
||||||
|
///
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub fn has_listened_to(self: &Self, song_id: i32, conn: &mut PgPooledConn) -> Result<bool, Box<dyn Error>> {
|
||||||
|
use crate::schema::song_history::{self, user_id};
|
||||||
|
|
||||||
|
let my_id = self.id.ok_or("Artist id must be present (Some) to check history")?;
|
||||||
|
|
||||||
|
let has_listened = song_history::table
|
||||||
|
.filter(user_id.eq(my_id))
|
||||||
|
.filter(song_history::song_id.eq(song_id))
|
||||||
|
.first::<HistoryEntry>(conn)
|
||||||
|
.optional()?
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
Ok(has_listened)
|
||||||
|
}
|
||||||
|
|
||||||
/// Like or unlike a song for this user
|
/// Like or unlike a song for this user
|
||||||
/// If likeing a song, remove dislike if it exists
|
/// If likeing a song, remove dislike if it exists
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
@ -469,3 +609,20 @@ impl Song {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Model for a history entry
|
||||||
|
#[cfg_attr(feature = "ssr", derive(Queryable, Selectable, Insertable))]
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(table_name = crate::schema::song_history))]
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(check_for_backend(diesel::pg::Pg)))]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct HistoryEntry {
|
||||||
|
/// A unique id for the history entry
|
||||||
|
#[cfg_attr(feature = "ssr", diesel(deserialize_as = i32))]
|
||||||
|
pub id: Option<i32>,
|
||||||
|
/// The id of the user who listened to the song
|
||||||
|
pub user_id: i32,
|
||||||
|
/// The date the song was listened to
|
||||||
|
pub date: SystemTime,
|
||||||
|
/// The id of the song that was listened to
|
||||||
|
pub song_id: i32,
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ use leptos::html::{Audio, Div};
|
|||||||
use leptos::leptos_dom::*;
|
use leptos::leptos_dom::*;
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_icons::*;
|
use leptos_icons::*;
|
||||||
|
use leptos_use::{utils::Pausable, use_interval_fn};
|
||||||
|
|
||||||
/// Width and height of the forward/backward skip buttons
|
/// Width and height of the forward/backward skip buttons
|
||||||
const SKIP_BTN_SIZE: &str = "3.5em";
|
const SKIP_BTN_SIZE: &str = "3.5em";
|
||||||
@ -22,6 +23,9 @@ const MIN_SKIP_BACK_TIME: f64 = 5.0;
|
|||||||
/// How many seconds to skip forward/backward when the user presses the arrow keys
|
/// How many seconds to skip forward/backward when the user presses the arrow keys
|
||||||
const ARROW_KEY_SKIP_TIME: f64 = 5.0;
|
const ARROW_KEY_SKIP_TIME: f64 = 5.0;
|
||||||
|
|
||||||
|
/// Threshold in seconds for considering when the user has listened to a song, for adding it to the history
|
||||||
|
const HISTORY_LISTEN_THRESHOLD: u64 = MIN_SKIP_BACK_TIME as u64;
|
||||||
|
|
||||||
// TODO Handle errors better, when getting audio HTML element and when playing/pausing audio
|
// TODO Handle errors better, when getting audio HTML element and when playing/pausing audio
|
||||||
|
|
||||||
/// Get the current time and duration of the current song, if available
|
/// Get the current time and duration of the current song, if available
|
||||||
@ -532,6 +536,39 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let current_song_id = create_memo(move |_| {
|
||||||
|
status.with(|status| {
|
||||||
|
status.queue.front().map(|song| song.id)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track the last song that was added to the history to prevent duplicates
|
||||||
|
let last_history_song_id = create_rw_signal(None);
|
||||||
|
|
||||||
|
let Pausable {
|
||||||
|
is_active: hist_timeout_pending,
|
||||||
|
resume: resume_hist_timeout,
|
||||||
|
pause: pause_hist_timeout,
|
||||||
|
..
|
||||||
|
} = use_interval_fn(move || {
|
||||||
|
if last_history_song_id.get_untracked() == current_song_id.get_untracked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(current_song_id) = current_song_id.get_untracked() {
|
||||||
|
last_history_song_id.set(Some(current_song_id));
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
if let Err(e) = crate::api::history::add_history(current_song_id).await {
|
||||||
|
error!("Error adding song {} to history: {}", current_song_id, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, HISTORY_LISTEN_THRESHOLD * 1000);
|
||||||
|
|
||||||
|
// Initially pause the timeout, since the audio starts off paused
|
||||||
|
pause_hist_timeout();
|
||||||
|
|
||||||
let on_play = move |_| {
|
let on_play = move |_| {
|
||||||
log!("Audio playing");
|
log!("Audio playing");
|
||||||
status.update(|status| status.playing = true);
|
status.update(|status| status.playing = true);
|
||||||
@ -540,6 +577,7 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
let on_pause = move |_| {
|
let on_pause = move |_| {
|
||||||
log!("Audio paused");
|
log!("Audio paused");
|
||||||
status.update(|status| status.playing = false);
|
status.update(|status| status.playing = false);
|
||||||
|
pause_hist_timeout();
|
||||||
};
|
};
|
||||||
|
|
||||||
let on_time_update = move |_| {
|
let on_time_update = move |_| {
|
||||||
@ -557,6 +595,11 @@ pub fn PlayBar(status: RwSignal<PlayStatus>) -> impl IntoView {
|
|||||||
error!("Unable to update time: Audio element not available");
|
error!("Unable to update time: Audio element not available");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If time is updated, audio is playing, so make sure the history timeout is running
|
||||||
|
if !hist_timeout_pending.get_untracked() {
|
||||||
|
resume_hist_timeout();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let on_end = move |_| {
|
let on_end = move |_| {
|
||||||
|
@ -53,6 +53,15 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
song_history (id) {
|
||||||
|
id -> Int4,
|
||||||
|
user_id -> Int4,
|
||||||
|
date -> Timestamp,
|
||||||
|
song_id -> Int4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
song_likes (song_id, user_id) {
|
song_likes (song_id, user_id) {
|
||||||
song_id -> Int4,
|
song_id -> Int4,
|
||||||
@ -90,6 +99,8 @@ diesel::joinable!(song_artists -> artists (artist_id));
|
|||||||
diesel::joinable!(song_artists -> songs (song_id));
|
diesel::joinable!(song_artists -> songs (song_id));
|
||||||
diesel::joinable!(song_dislikes -> songs (song_id));
|
diesel::joinable!(song_dislikes -> songs (song_id));
|
||||||
diesel::joinable!(song_dislikes -> users (user_id));
|
diesel::joinable!(song_dislikes -> users (user_id));
|
||||||
|
diesel::joinable!(song_history -> songs (song_id));
|
||||||
|
diesel::joinable!(song_history -> users (user_id));
|
||||||
diesel::joinable!(song_likes -> songs (song_id));
|
diesel::joinable!(song_likes -> songs (song_id));
|
||||||
diesel::joinable!(song_likes -> users (user_id));
|
diesel::joinable!(song_likes -> users (user_id));
|
||||||
diesel::joinable!(songs -> albums (album_id));
|
diesel::joinable!(songs -> albums (album_id));
|
||||||
@ -102,6 +113,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
|||||||
friendships,
|
friendships,
|
||||||
song_artists,
|
song_artists,
|
||||||
song_dislikes,
|
song_dislikes,
|
||||||
|
song_history,
|
||||||
song_likes,
|
song_likes,
|
||||||
songs,
|
songs,
|
||||||
users,
|
users,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user