From b04ead6056e23adcb25338e532bacd82cc020185 Mon Sep 17 00:00:00 2001 From: ayamdobhal Date: Tue, 24 Dec 2024 20:05:14 +0530 Subject: [PATCH] feat: initial scrobbling impl --- .gitignore | 2 +- ...65cef00c13270c488699e45e0940b94333d71.json | 12 + Cargo.lock | 354 +++++++++++++++++- Cargo.toml | 1 + migrations/20241224102046_user.sql | 5 + src/commands/fmlogin.rs | 19 + src/commands/mod.rs | 1 + src/commands/music.rs | 1 + src/commands/utils.rs | 86 ++++- src/events/track_queue_event.rs | 48 ++- src/main.rs | 6 + src/persistence/entities/mod.rs | 2 +- src/persistence/entities/user.rs | 3 +- src/persistence/mod.rs | 27 +- src/queue/mod.rs | 5 +- src/scrobbler/mod.rs | 53 +++ src/state/mod.rs | 5 +- 17 files changed, 573 insertions(+), 57 deletions(-) create mode 100644 .sqlx/query-0a34d97893d306f87cd2bb00fc865cef00c13270c488699e45e0940b94333d71.json create mode 100644 migrations/20241224102046_user.sql create mode 100644 src/commands/fmlogin.rs create mode 100644 src/scrobbler/mod.rs diff --git a/.gitignore b/.gitignore index 0996431..3ff47ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target -db.sqlite +*.sqlite .env \ No newline at end of file diff --git a/.sqlx/query-0a34d97893d306f87cd2bb00fc865cef00c13270c488699e45e0940b94333d71.json b/.sqlx/query-0a34d97893d306f87cd2bb00fc865cef00c13270c488699e45e0940b94333d71.json new file mode 100644 index 0000000..51b6962 --- /dev/null +++ b/.sqlx/query-0a34d97893d306f87cd2bb00fc865cef00c13270c488699e45e0940b94333d71.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO user (id, token) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "0a34d97893d306f87cd2bb00fc865cef00c13270c488699e45e0940b94333d71" +} diff --git a/Cargo.lock b/Cargo.lock index 246fb7c..7b9b6fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + [[package]] name = "base64" version = "0.13.1" @@ -324,7 +330,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.23", "serde", "serde_json", ] @@ -359,6 +365,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + [[package]] name = "cipher" version = "0.4.4" @@ -444,12 +456,45 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_fn" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" +dependencies = [ + "percent-encoding", + "time 0.2.27", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" +dependencies = [ + "cookie", + "idna 0.2.3", + "log", + "publicsuffix", + "serde", + "serde_json", + "time 0.2.27", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -705,7 +750,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.1", "syn 2.0.87", ] @@ -721,6 +766,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "discortp" version = "0.6.0" @@ -1565,6 +1616,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "1.0.3" @@ -1735,6 +1797,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "matchit" version = "0.7.3" @@ -1764,6 +1832,7 @@ dependencies = [ "poise", "reqwest 0.11.27", "rspotify", + "rustfm-scrobble", "serde", "serde_derive", "serde_json", @@ -1786,6 +1855,12 @@ dependencies = [ "digest", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.4" @@ -2284,6 +2359,12 @@ dependencies = [ "num-integer", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.89" @@ -2325,6 +2406,16 @@ dependencies = [ "prost", ] +[[package]] +name = "publicsuffix" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b4ce31ff0a27d93c8de1849cf58162283752f065a90d508f1105fa6c9a213f" +dependencies = [ + "idna 0.2.3", + "url", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -2336,6 +2427,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + [[package]] name = "quote" version = "1.0.37" @@ -2660,13 +2760,22 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver", + "semver 1.0.23", ] [[package]] @@ -2684,6 +2793,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "rustfm-scrobble" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c46a75fb6409a528f7e0d8e99826684f88461d1b0d0edeec60d82e3f554dad5" +dependencies = [ + "md5", + "serde", + "serde_json", + "ureq", + "wrapped-vec", +] + [[package]] name = "rustix" version = "0.38.39" @@ -2697,6 +2819,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64 0.13.1", + "log", + "ring 0.16.20", + "sct 0.6.1", + "webpki 0.21.4", +] + [[package]] name = "rustls" version = "0.20.9" @@ -2705,8 +2840,8 @@ checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" dependencies = [ "log", "ring 0.16.20", - "sct", - "webpki", + "sct 0.7.1", + "webpki 0.22.4", ] [[package]] @@ -2718,7 +2853,7 @@ dependencies = [ "log", "ring 0.17.8", "rustls-webpki 0.101.7", - "sct", + "sct 0.7.1", ] [[package]] @@ -2856,6 +2991,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "sct" version = "0.7.1" @@ -2899,6 +3044,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.23" @@ -2908,6 +3062,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.215" @@ -3020,7 +3180,7 @@ dependencies = [ "serde_cow", "serde_json", "static_assertions", - "time", + "time 0.3.36", "tokio", "tokio-tungstenite 0.21.0", "tracing", @@ -3043,6 +3203,15 @@ dependencies = [ "serde_repr", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3054,6 +3223,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -3358,7 +3533,7 @@ dependencies = [ "rand", "rsa", "serde", - "sha1", + "sha1 0.10.6", "sha2", "smallvec", "sqlx-core", @@ -3444,12 +3619,70 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + [[package]] name = "stream_lib" version = "0.4.2" @@ -3857,6 +4090,21 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros 0.1.1", + "version_check", + "winapi", +] + [[package]] name = "time" version = "0.3.36" @@ -3869,7 +4117,7 @@ dependencies = [ "powerfmt", "serde", "time-core", - "time-macros", + "time-macros 0.2.18", ] [[package]] @@ -3878,6 +4126,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + [[package]] name = "time-macros" version = "0.2.18" @@ -3888,6 +4146,19 @@ dependencies = [ "time-core", ] +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.109", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -3961,7 +4232,7 @@ checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls 0.20.9", "tokio", - "webpki", + "webpki 0.22.4", ] [[package]] @@ -4021,7 +4292,7 @@ dependencies = [ "tokio", "tokio-rustls 0.23.4", "tungstenite 0.18.0", - "webpki", + "webpki 0.22.4", ] [[package]] @@ -4237,11 +4508,11 @@ dependencies = [ "log", "rand", "rustls 0.20.9", - "sha1", + "sha1 0.10.6", "thiserror", "url", "utf-8", - "webpki", + "webpki 0.22.4", ] [[package]] @@ -4259,7 +4530,7 @@ dependencies = [ "rand", "rustls 0.22.4", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror", "url", "utf-8", @@ -4305,7 +4576,7 @@ dependencies = [ "serde", "serde-value", "serde_repr", - "time", + "time 0.3.36", ] [[package]] @@ -4333,7 +4604,7 @@ dependencies = [ "parking_lot", "secrecy", "serde_json", - "time", + "time 0.3.36", "typesize-derive", "url", ] @@ -4416,6 +4687,25 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8b063c2d59218ae09f22b53c42eaad0d53516457905f5235ca4bc9e99daa71" +dependencies = [ + "base64 0.13.1", + "chunked_transfer", + "cookie", + "cookie_store", + "log", + "once_cell", + "qstring", + "rustls 0.19.1", + "url", + "webpki 0.21.4", + "webpki-roots 0.21.1", +] + [[package]] name = "url" version = "2.5.3" @@ -4423,7 +4713,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" dependencies = [ "form_urlencoded", - "idna", + "idna 1.0.3", "percent-encoding", "serde", ] @@ -4600,6 +4890,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "webpki" version = "0.22.4" @@ -4610,6 +4910,15 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki 0.21.4", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -4872,6 +5181,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wrapped-vec" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85e08702c1e919669e1e90213c9c75ea4bb689d0f3970347e2b37c04600b4e5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index b507dcb..314eb43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,3 +22,4 @@ async-trait = "0.1.83" dotenv = "0.15.0" anyhow = "1.0.95" sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] } +rustfm-scrobble = "1.1.1" diff --git a/migrations/20241224102046_user.sql b/migrations/20241224102046_user.sql new file mode 100644 index 0000000..4004053 --- /dev/null +++ b/migrations/20241224102046_user.sql @@ -0,0 +1,5 @@ +-- Add migration script here +CREATE TABLE user ( + id INTEGER NOT NULL, + token TEXT NOT NULL +) \ No newline at end of file diff --git a/src/commands/fmlogin.rs b/src/commands/fmlogin.rs new file mode 100644 index 0000000..66f3dfd --- /dev/null +++ b/src/commands/fmlogin.rs @@ -0,0 +1,19 @@ +use crate::commands::utils::Error; +use crate::persistence::entities::user::User; +use crate::scrobbler::Scrobbler; + +use super::utils::Context; + +#[poise::command(prefix_command, aliases("login"), dm_only)] +pub async fn fmlogin(ctx: Context<'_>, username: String, password: String) -> Result<(), Error> { + let api_key = std::env::var("LASTFM_API_KEY").expect("missing LASTFM_API_KEY"); + let api_secret = std::env::var("LASTFM_API_SECRET").expect("missing LASTFM_API_SECRET"); + let token = Scrobbler::new(api_key, api_secret) + .get_user_token(&username, &password) + .await?; + let user = User::new(ctx.author().id.get() as i64, token); + user.save(&ctx.data().sql_conn.sql_conn).await?; + + ctx.reply("Saved LastFM Token").await?; + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ecc905d..cf6964e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,5 @@ pub mod clear; +pub mod fmlogin; pub mod music; pub mod now; pub mod pause; diff --git a/src/commands/music.rs b/src/commands/music.rs index ed7a898..f09b586 100644 --- a/src/commands/music.rs +++ b/src/commands/music.rs @@ -66,6 +66,7 @@ pub async fn music(ctx: Context<'_>, song_name: Vec) -> Result<(), Error guild_id, text_channel_id: ctx.channel_id(), context: ctx.serenity_context().clone(), + sql_conn: ctx.data().sql_conn.clone(), }, &k, ); diff --git a/src/commands/utils.rs b/src/commands/utils.rs index 3b57c1b..358de42 100644 --- a/src/commands/utils.rs +++ b/src/commands/utils.rs @@ -1,6 +1,13 @@ use std::time::Duration; -use crate::state::Data; +use futures::future::join_all; +use serenity::all::{ChannelId, Colour, Context as SerenityContext, CreateEmbed, CreateMessage}; + +use crate::{ + persistence::SqlConn, + scrobbler::Scrobbler, + state::{Data, Track}, +}; pub type Error = Box; pub type Context<'a> = poise::Context<'a, Data, Error>; @@ -41,3 +48,80 @@ pub fn time_to_duration(time: &String) -> Duration { } Duration::new(secs, 0) } + +pub async fn handle_playing( + ctx: SerenityContext, + text_channel_id: ChannelId, + track: &Track, + channel_id: ChannelId, + sql_conn: &SqlConn, +) { + let embed = CreateEmbed::new() + .title("**⏯️ Now Playing**") + .field( + track.artist.to_string(), + format!("{} [{}]", track.name, track.duration), + true, + ) + .image(track.thumbnail.to_string()) + .color(Colour::from_rgb(0, 255, 0)); + text_channel_id + .send_message(&ctx, CreateMessage::new().add_embed(embed)) + .await + .expect("Failed to send message"); + + let channel = channel_id.to_channel(&ctx).await.unwrap(); + let category = channel.category().unwrap(); + let members = category.members(&ctx).unwrap(); + + let users_future: Vec<_> = members + .iter() + .map(|member| async { sql_conn.get_user(member.user.id.get() as i64).await }) + .collect(); + + let users = join_all(users_future).await; + for user in users { + if user.is_some() { + let api_key = std::env::var("LASTFM_API_KEY").expect("missing LASTFM_API_KEY"); + let api_secret = std::env::var("LASTFM_API_SECRET").expect("missing LASTFM_API_SECRET"); + let mut scrobbler = Scrobbler::new(api_key, api_secret); + let song = scrobbler + .track_to_scrobble(&track.artist, &track.name, &"".to_string()) + .await; + tokio::spawn(async move { + scrobbler.now_playing(&song, user.unwrap()).await; + }); + } + } +} + +pub async fn scrobble( + ctx: SerenityContext, + track: &Track, + channel_id: ChannelId, + sql_conn: &SqlConn, +) { + let channel = channel_id.to_channel(&ctx).await.unwrap(); + let category = channel.category().unwrap(); + let members = category.members(&ctx).unwrap(); + + let users_future: Vec<_> = members + .iter() + .map(|member| async { sql_conn.get_user(member.user.id.get() as i64).await }) + .collect(); + + let users = join_all(users_future).await; + for user in users { + if user.is_some() { + let api_key = std::env::var("LASTFM_API_KEY").expect("missing LASTFM_API_KEY"); + let api_secret = std::env::var("LASTFM_API_SECRET").expect("missing LASTFM_API_SECRET"); + let mut scrobbler = Scrobbler::new(api_key, api_secret); + let song = scrobbler + .track_to_scrobble(&track.artist, &track.name, &"".to_string()) + .await; + tokio::spawn(async move { + scrobbler.scrobble(&song, user.unwrap()).await; + }); + } + } +} diff --git a/src/events/track_queue_event.rs b/src/events/track_queue_event.rs index 6783c6e..f7c19ed 100644 --- a/src/events/track_queue_event.rs +++ b/src/events/track_queue_event.rs @@ -2,6 +2,8 @@ use async_trait::async_trait; use serenity::all::{ChannelId, Colour, Context, CreateEmbed, CreateMessage, GuildId}; use crate::{ + commands::utils::{handle_playing, scrobble}, + persistence::SqlConn, queue::{EventfulQueueKey, QueueEventHandler, QueueEvents}, state::Track, }; @@ -12,6 +14,7 @@ pub struct QueueEvent { pub guild_id: GuildId, pub text_channel_id: ChannelId, pub context: Context, + pub sql_conn: SqlConn, } #[async_trait] @@ -31,19 +34,14 @@ impl QueueEventHandler for QueueEvent { let track = queue.back().unwrap(); match len { 1 => { - let embed = CreateEmbed::new() - .title("**⏯️ Now Playing**") - .field( - track.artist.to_string(), - format!("{} [{}]", track.name, track.duration), - true, - ) - .image(track.thumbnail.to_string()) - .color(Colour::from_rgb(0, 255, 0)); - self.text_channel_id - .send_message(&self.context, CreateMessage::new().add_embed(embed)) - .await - .expect("Failed to send message"); + handle_playing( + self.context.clone(), + self.text_channel_id, + track, + self.channel_id, + &self.sql_conn, + ) + .await } v => { let embed = CreateEmbed::new() @@ -63,7 +61,7 @@ impl QueueEventHandler for QueueEvent { } } } - QueueEvents::TrackPopped(k, queue) => { + QueueEvents::TrackPopped(k, queue, v) => { let key = EventfulQueueKey { guild_id: self.guild_id, channel_id: self.channel_id, @@ -82,21 +80,17 @@ impl QueueEventHandler for QueueEvent { } _ => { let track = queue.front().unwrap(); - let embed = CreateEmbed::new() - .title("**⏯️ Now Playing**") - .field( - track.artist.to_string(), - format!("{} [{}]", track.name, track.duration), - true, - ) - .image(track.thumbnail.to_string()) - .color(Colour::from_rgb(0, 255, 0)); - self.text_channel_id - .send_message(&self.context, CreateMessage::new().add_embed(embed)) - .await - .expect("Failed to send message"); + handle_playing( + self.context.clone(), + self.text_channel_id, + track, + self.channel_id, + &self.sql_conn, + ) + .await; } } + scrobble(self.context.clone(), v, self.channel_id, &self.sql_conn).await; } } QueueEvents::QueueCleared(k) => { diff --git a/src/main.rs b/src/main.rs index 7f40f3a..0401c4f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use std::error::Error; +use commands::fmlogin::fmlogin; use commands::music::music; use commands::now::now; use commands::pause::pause; @@ -11,6 +12,7 @@ use commands::skip::skip; use commands::{clear::clear, repeat::repeat}; use dotenv::dotenv; +use persistence::SqlConn; use poise::{serenity_prelude as serenity, PrefixFrameworkOptions}; use reqwest::Client as HttpClient; use songbird::SerenityInit; @@ -21,6 +23,7 @@ mod events; mod models; mod persistence; mod queue; +mod scrobbler; mod state; #[tokio::main] @@ -28,6 +31,7 @@ async fn main() -> Result<(), Box> { let _ = dotenv(); tracing_subscriber::fmt().init(); let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN"); + let sql_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL"); let intents = serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; @@ -44,6 +48,7 @@ async fn main() -> Result<(), Box> { pause(), seek(), remove(), + fmlogin(), ], event_handler: |ctx, event, _, _| match event { serenity::FullEvent::VoiceStateUpdate { new, .. } => Box::pin(async move { @@ -82,6 +87,7 @@ async fn main() -> Result<(), Box> { Ok(Data { hc: HttpClient::new(), queue: Default::default(), + sql_conn: SqlConn::new(sql_url).await, }) }) }) diff --git a/src/persistence/entities/mod.rs b/src/persistence/entities/mod.rs index 0eba110..22d12a3 100644 --- a/src/persistence/entities/mod.rs +++ b/src/persistence/entities/mod.rs @@ -1 +1 @@ -mod user; +pub mod user; diff --git a/src/persistence/entities/user.rs b/src/persistence/entities/user.rs index 50d64fe..ba134a4 100644 --- a/src/persistence/entities/user.rs +++ b/src/persistence/entities/user.rs @@ -1,7 +1,6 @@ -use anyhow::{Ok, Error}; +use anyhow::{Error, Ok}; use sqlx::SqlitePool; - #[derive(sqlx::FromRow)] pub struct User { pub id: i64, diff --git a/src/persistence/mod.rs b/src/persistence/mod.rs index c3e677d..248a17b 100644 --- a/src/persistence/mod.rs +++ b/src/persistence/mod.rs @@ -1,9 +1,28 @@ pub mod entities; -use anyhow::Result; +use entities::user::User; use sqlx::SqlitePool; -pub async fn connect() -> Result { - let conn_uri = std::env::var("DATABASE_URL").expect("missing DATABASE_URL"); - Ok(SqlitePool::connect(&conn_uri).await?) +#[derive(Debug, Clone)] +pub struct SqlConn { + pub sql_conn: SqlitePool, +} + +impl SqlConn { + pub async fn new(sql_url: String) -> Self { + Self { + sql_conn: SqlitePool::connect(&sql_url) + .await + .expect("Failed to connect to db"), + } + } + + pub async fn get_user(&self, id: i64) -> Option { + Some( + sqlx::query_as!(User, "SELECT * FROM user where id=(?) LIMIT 1", id) + .fetch_one(&self.sql_conn) + .await + .expect("User not found"), + ) + } } diff --git a/src/queue/mod.rs b/src/queue/mod.rs index d22ca6f..785035a 100644 --- a/src/queue/mod.rs +++ b/src/queue/mod.rs @@ -13,7 +13,7 @@ pub struct EventfulQueueKey { #[non_exhaustive] pub enum QueueEvents<'a, T> { TrackPushed(EventfulQueueKey, &'a VecDeque), - TrackPopped(EventfulQueueKey, &'a VecDeque), + TrackPopped(EventfulQueueKey, &'a VecDeque, &'a T), QueueCreated(EventfulQueueKey), QueueCleared(EventfulQueueKey), } @@ -86,13 +86,14 @@ impl EventfulQueue { pub async fn pop(&mut self, key: &EventfulQueueKey) -> Option { let track = self.data.get_mut(&key)?.pop_front(); - if let Some(_) = track { + if let Some(ref v) = track { self.handlers .get(&key) .unwrap() .on_event(&QueueEvents::TrackPopped( key.clone(), self.data.get(&key).unwrap(), + v, )) .await; } diff --git a/src/scrobbler/mod.rs b/src/scrobbler/mod.rs new file mode 100644 index 0000000..2c05dba --- /dev/null +++ b/src/scrobbler/mod.rs @@ -0,0 +1,53 @@ +use anyhow::Error; +use rustfm_scrobble::{Scrobble, Scrobbler as RustFmScrobbler}; + +use crate::persistence::entities::user::User; +use std::fmt::Debug; + +#[derive(Debug, Clone)] +pub struct Scrobbler { + api_key: String, + api_secret: String, +} + +impl Scrobbler { + pub fn new(api_key: String, api_secret: String) -> Self { + Scrobbler { + api_key, + api_secret, + } + } + + pub async fn track_to_scrobble( + &self, + artist: &String, + track: &String, + album: &String, + ) -> Scrobble { + Scrobble::new(artist, track, album) + } + + pub async fn scrobble(&mut self, song: &Scrobble, user: User) { + let mut scrobbler = RustFmScrobbler::new(&self.api_key, &self.api_secret); + scrobbler.authenticate_with_session_key(&user.token); + scrobbler.scrobble(song).expect("Scrobble failed"); + } + + pub async fn now_playing(&mut self, song: &Scrobble, user: User) { + let mut scrobbler = RustFmScrobbler::new(&self.api_key, &self.api_secret); + scrobbler.authenticate_with_session_key(&user.token); + scrobbler.scrobble(song).expect("Now Playing failed"); + } + + pub async fn get_user_token( + &mut self, + username: &String, + password: &String, + ) -> Result { + let mut scrobbler = RustFmScrobbler::new(&self.api_key, &self.api_secret); + let res = scrobbler + .authenticate_with_password(username, password) + .expect("Invalid creds"); + Ok(res.key) + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs index 69bd3bd..45586a1 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use reqwest::Client as HttpClient; use tokio::sync::RwLock; -use crate::queue::EventfulQueue; +use crate::{persistence::SqlConn, queue::EventfulQueue}; #[derive(Debug, Clone, Default)] pub struct Track { @@ -14,10 +14,11 @@ pub struct Track { pub thumbnail: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Data { pub hc: HttpClient, pub queue: Arc>>, + pub sql_conn: SqlConn, } // pub struct Track {