A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz

[webscrobbler] initialize webscrobler webhook server

+181
+21
Cargo.lock
··· 5673 5673 ] 5674 5674 5675 5675 [[package]] 5676 + name = "webscrobbler" 5677 + version = "0.1.0" 5678 + dependencies = [ 5679 + "actix-web", 5680 + "anyhow", 5681 + "chrono", 5682 + "dotenv", 5683 + "hex", 5684 + "jsonwebtoken", 5685 + "md5", 5686 + "owo-colors", 5687 + "quick-xml 0.37.4", 5688 + "redis", 5689 + "reqwest", 5690 + "serde", 5691 + "serde_json", 5692 + "sqlx", 5693 + "tokio", 5694 + ] 5695 + 5696 + [[package]] 5676 5697 name = "whoami" 5677 5698 version = "1.5.2" 5678 5699 source = "registry+https://github.com/rust-lang/crates.io-index"
+1
crates/webscrobbler/.gitignore
··· 1 + .env
+39
crates/webscrobbler/Cargo.toml
··· 1 + [package] 2 + name = "webscrobbler" 3 + version = "0.1.0" 4 + authors.workspace = true 5 + edition.workspace = true 6 + license.workspace = true 7 + repository.workspace = true 8 + 9 + [[bin]] 10 + name = "webscrobbler" 11 + path = "src/main.rs" 12 + 13 + [dependencies] 14 + serde = { version = "1.0.217", features = ["derive"] } 15 + serde_json = "1.0.139" 16 + sqlx = { version = "0.8.3", features = [ 17 + "runtime-tokio", 18 + "tls-rustls", 19 + "postgres", 20 + "chrono", 21 + "derive", 22 + "macros", 23 + ] } 24 + tokio = { version = "1.43.0", features = ["full"] } 25 + owo-colors = "4.1.0" 26 + dotenv = "0.15.0" 27 + anyhow = "1.0.96" 28 + actix-web = "4.9.0" 29 + redis = "0.29.0" 30 + hex = "0.4.3" 31 + jsonwebtoken = "9.3.1" 32 + md5 = "0.7.0" 33 + reqwest = { version = "0.12.12", features = [ 34 + "rustls-tls", 35 + "json", 36 + "multipart", 37 + ], default-features = false } 38 + quick-xml = { version = "0.37.4", features = ["serialize"] } 39 + chrono = { version = "= 0.4.39", features = ["serde"] }
+47
crates/webscrobbler/src/cache.rs
··· 1 + use anyhow::Error; 2 + use redis::Client; 3 + use std::env; 4 + 5 + #[derive(Clone)] 6 + pub struct Cache { 7 + pub client: Client, 8 + } 9 + 10 + impl Cache { 11 + pub fn new() -> Result<Self, Error> { 12 + let client = 13 + redis::Client::open(env::var("REDIS_URL").unwrap_or("redis://127.0.0.1".into()))?; 14 + Ok(Cache { client }) 15 + } 16 + 17 + pub fn get(&self, key: &str) -> Result<Option<String>, Error> { 18 + let mut con = self.client.get_connection()?; 19 + let result: Option<String> = redis::cmd("GET").arg(key).query(&mut con)?; 20 + Ok(result) 21 + } 22 + 23 + pub fn set(&self, key: &str, value: &str) -> Result<(), Error> { 24 + let mut con = self.client.get_connection()?; 25 + redis::cmd("SET") 26 + .arg(key) 27 + .arg(value) 28 + .query::<()>(&mut con)?; 29 + Ok(()) 30 + } 31 + 32 + pub fn setex(&self, key: &str, value: &str, seconds: usize) -> Result<(), Error> { 33 + let mut con = self.client.get_connection()?; 34 + redis::cmd("SETEX") 35 + .arg(key) 36 + .arg(seconds) 37 + .arg(value) 38 + .query::<()>(&mut con)?; 39 + Ok(()) 40 + } 41 + 42 + pub fn exists(&self, key: &str) -> Result<bool, Error> { 43 + let mut con = self.client.get_connection()?; 44 + let result: bool = redis::cmd("EXISTS").arg(key).query(&mut con)?; 45 + Ok(result) 46 + } 47 + }
+9
crates/webscrobbler/src/handlers.rs
··· 1 + use actix_web::{get, HttpResponse, Responder}; 2 + 3 + use crate::BANNER; 4 + 5 + 6 + #[get("/")] 7 + pub async fn index() -> impl Responder { 8 + HttpResponse::Ok().body(BANNER) 9 + }
+64
crates/webscrobbler/src/main.rs
··· 1 + use std::{env, sync::Arc}; 2 + 3 + use actix_web::{web::Data, App, HttpServer}; 4 + use anyhow::Error; 5 + use cache::Cache; 6 + use dotenv::dotenv; 7 + use owo_colors::OwoColorize; 8 + use sqlx::postgres::PgPoolOptions; 9 + 10 + pub mod cache; 11 + pub mod handlers; 12 + 13 + pub const BANNER: &str = r#" 14 + _ __ __ _____ __ __ __ 15 + | | / /__ / /_ / ___/______________ / /_ / /_ / /__ _____ 16 + | | /| / / _ \/ __ \\__ \/ ___/ ___/ __ \/ __ \/ __ \/ / _ \/ ___/ 17 + | |/ |/ / __/ /_/ /__/ / /__/ / / /_/ / /_/ / /_/ / / __/ / 18 + |__/|__/\___/_.___/____/\___/_/ \____/_.___/_.___/_/\___/_/ 19 + 20 + 21 + This is the Rocksky WebScrobbler Webhook API compatible with webscrobbler extension. 22 + "#; 23 + 24 + #[tokio::main] 25 + async fn main() -> Result<(), Error> { 26 + dotenv().ok(); 27 + 28 + println!("{}", BANNER.magenta()); 29 + 30 + let cache = Cache::new()?; 31 + 32 + 33 + 34 + let pool = PgPoolOptions::new() 35 + .max_connections(5) 36 + .connect(&env::var("XATA_POSTGRES_URL")?) 37 + .await?; 38 + 39 + let conn = Arc::new(pool); 40 + 41 + 42 + let host = env::var("WEBSCROBBLER_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 43 + let port = env::var("WEBSCROBBLER_PORT") 44 + .unwrap_or_else(|_| "7883".to_string()) 45 + .parse::<u16>() 46 + .unwrap_or(7883); 47 + 48 + println!( 49 + "Starting WebScrobbler WebHook @ {}", 50 + format!("{}:{}", host, port).green() 51 + ); 52 + 53 + HttpServer::new(move || { 54 + App::new() 55 + .app_data(Data::new(conn.clone())) 56 + .app_data(Data::new(cache.clone())) 57 + .service(handlers::index) 58 + }) 59 + .bind((host, port))? 60 + .run() 61 + .await?; 62 + 63 + Ok(()) 64 + }