A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
at main 705 lines 26 kB view raw
1use std::sync::{Arc, Mutex}; 2 3use crate::read_payload; 4use crate::types::stats::{ 5 Compatibility, GetCompatibilityParams, GetNeighboursParams, SharedArtist, 6}; 7use crate::types::{ 8 scrobble::{ScrobblesPerDay, ScrobblesPerMonth, ScrobblesPerYear}, 9 stats::{ 10 GetAlbumScrobblesParams, GetArtistScrobblesParams, GetScrobblesPerDayParams, 11 GetScrobblesPerMonthParams, GetScrobblesPerYearParams, GetStatsParams, 12 GetTrackScrobblesParams, Neighbour, 13 }, 14}; 15use actix_web::{web, HttpRequest, HttpResponse}; 16use anyhow::Error; 17use duckdb::Connection; 18use serde_json::json; 19use tokio_stream::StreamExt; 20 21pub async fn get_stats( 22 payload: &mut web::Payload, 23 _req: &HttpRequest, 24 conn: Arc<Mutex<Connection>>, 25) -> Result<HttpResponse, Error> { 26 let body = read_payload!(payload); 27 28 let params = serde_json::from_slice::<GetStatsParams>(&body)?; 29 tracing::info!(user_did = ?params.user_did, "Get stats"); 30 31 let conn = conn.lock().unwrap(); 32 let mut stmt = conn.prepare("SELECT COUNT(DISTINCT s.created_at) FROM scrobbles s LEFT JOIN users u ON s.user_id = u.id WHERE u.did = ? OR u.handle = ?")?; 33 let scrobbles: i64 = stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 34 35 let mut stmt = conn.prepare( 36 r#" 37 SELECT COUNT(*) FROM ( 38 SELECT 39 s.artist_id AS id, 40 ar.name AS artist_name, 41 ar.picture AS picture, 42 ar.sha256 AS sha256, 43 ar.uri AS uri, 44 COUNT(DISTINCT s.created_at) AS play_count, 45 COUNT(DISTINCT s.user_id) AS unique_listeners 46 FROM 47 scrobbles s 48 LEFT JOIN 49 artists ar ON s.artist_id = ar.id 50 LEFT JOIN 51 users u ON s.user_id = u.id 52 WHERE 53 s.artist_id IS NOT NULL AND (u.did = ? OR u.handle = ?) 54 GROUP BY 55 s.artist_id, ar.name, ar.uri, ar.picture, ar.sha256 56 ) 57 "#, 58 )?; 59 let artists: i64 = stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 60 61 let mut stmt = conn.prepare("SELECT COUNT(*) FROM loved_tracks LEFT JOIN users u ON loved_tracks.user_id = u.id WHERE u.did = ? OR u.handle = ?")?; 62 let loved_tracks: i64 = 63 stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 64 65 let mut stmt = conn.prepare(r#"SELECT COUNT(*) FROM ( 66 SELECT 67 s.album_id AS id, 68 a.title AS title, 69 ar.name AS artist, 70 ar.uri AS artist_uri, 71 a.album_art AS album_art, 72 a.release_date, 73 a.year, 74 a.uri, 75 a.sha256, 76 COUNT(DISTINCT s.created_at) AS play_count, 77 COUNT(DISTINCT s.user_id) AS unique_listeners 78 FROM 79 scrobbles s 80 LEFT JOIN 81 albums a ON s.album_id = a.id 82 LEFT JOIN 83 artists ar ON a.artist = ar.name 84 LEFT JOIN 85 users u ON s.user_id = u.id 86 WHERE s.album_id IS NOT NULL AND (u.did = ? OR u.handle = ?) 87 GROUP BY 88 s.album_id, a.title, ar.name, a.release_date, a.year, a.uri, a.album_art, a.sha256, ar.uri 89 )"#)?; 90 let albums: i64 = stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 91 92 let mut stmt = conn.prepare( 93 r#" 94 SELECT COUNT(*) FROM tracks t 95 LEFT JOIN user_tracks ut ON ut.track_id = t.id 96 LEFT JOIN users u ON ut.user_id = u.id 97 WHERE u.did = ? OR u.handle = ? 98 "#, 99 )?; 100 let tracks: i64 = stmt.query_row([&params.user_did, &params.user_did], |row| row.get(0))?; 101 102 Ok(HttpResponse::Ok().json(json!({ 103 "scrobbles": scrobbles, 104 "artists": artists, 105 "loved_tracks": loved_tracks, 106 "albums": albums, 107 "tracks": tracks, 108 }))) 109} 110 111pub async fn get_scrobbles_per_day( 112 payload: &mut web::Payload, 113 _req: &HttpRequest, 114 conn: Arc<Mutex<Connection>>, 115) -> Result<HttpResponse, Error> { 116 let body = read_payload!(payload); 117 let params = serde_json::from_slice::<GetScrobblesPerDayParams>(&body)?; 118 let start = params 119 .start 120 .unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 121 let end = params 122 .end 123 .unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 124 let did = params.user_did; 125 let genre = params.genre; 126 tracing::info!(start = %start, end = %end, user_did = ?did, "Get scrobbles per day"); 127 128 let conn = conn.lock().unwrap(); 129 match (did, genre) { 130 (Some(did), None) => { 131 let mut stmt = conn.prepare( 132 r#" 133 SELECT 134 date_trunc('day', created_at) AS date, 135 COUNT(DISTINCT scrobbles.created_at) AS count 136 FROM 137 scrobbles 138 LEFT JOIN users u ON scrobbles.user_id = u.id 139 WHERE 140 u.did = ? OR u.handle = ? 141 AND created_at BETWEEN ? AND ? 142 GROUP BY 143 date_trunc('day', created_at) 144 ORDER BY 145 date; 146 "#, 147 )?; 148 let scrobbles = stmt.query_map([&did, &did, &start, &end], |row| { 149 Ok(ScrobblesPerDay { 150 date: row.get(0)?, 151 count: row.get(1)?, 152 }) 153 })?; 154 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 155 Ok(HttpResponse::Ok().json(scrobbles?)) 156 } 157 (None, Some(genre)) => { 158 let mut stmt = conn.prepare( 159 r#" 160 SELECT 161 date_trunc('day', created_at) AS date, 162 COUNT(DISTINCT s.created_at) AS count 163 FROM 164 scrobbles s 165 LEFT JOIN users u ON s.user_id = u.id 166 LEFT JOIN artists a ON s.artist_id = a.id 167 WHERE list_contains(a.genres, ?) AND created_at BETWEEN ? AND ? 168 GROUP BY 169 date_trunc('day', created_at) 170 ORDER BY 171 date; 172 "#, 173 )?; 174 let scrobbles = stmt.query_map([&genre, &start, &end], |row| { 175 Ok(ScrobblesPerDay { 176 date: row.get(0)?, 177 count: row.get(1)?, 178 }) 179 })?; 180 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 181 Ok(HttpResponse::Ok().json(scrobbles?)) 182 } 183 _ => { 184 let mut stmt = conn.prepare( 185 r#" 186 SELECT 187 date_trunc('day', s.created_at) AS date, 188 COUNT(DISTINCT (u.did, s.created_at)) AS count 189 FROM scrobbles s 190 JOIN users u ON u.id = s.user_id 191 WHERE s.created_at BETWEEN ? AND ? 192 GROUP BY 1 193 ORDER BY 1; 194 "#, 195 )?; 196 let scrobbles = stmt.query_map([start, end], |row| { 197 Ok(ScrobblesPerDay { 198 date: row.get(0)?, 199 count: row.get(1)?, 200 }) 201 })?; 202 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 203 Ok(HttpResponse::Ok().json(scrobbles?)) 204 } 205 } 206} 207 208pub async fn get_scrobbles_per_month( 209 payload: &mut web::Payload, 210 _req: &HttpRequest, 211 conn: Arc<Mutex<Connection>>, 212) -> Result<HttpResponse, Error> { 213 let body = read_payload!(payload); 214 let params = serde_json::from_slice::<GetScrobblesPerMonthParams>(&body)?; 215 let start = params 216 .start 217 .unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 218 let end = params 219 .end 220 .unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 221 let did = params.user_did; 222 tracing::info!(start = %start, end = %end, user_did = ?did, "Get scrobbles per month"); 223 224 let conn = conn.lock().unwrap(); 225 match did { 226 Some(did) => { 227 let mut stmt = conn.prepare( 228 r#" 229 SELECT 230 EXTRACT(YEAR FROM created_at) || '-' || 231 LPAD(EXTRACT(MONTH FROM created_at)::VARCHAR, 2, '0') AS year_month, 232 COUNT(DISTINCT scrobbles.created_at) AS count 233 FROM 234 scrobbles 235 LEFT JOIN users u ON scrobbles.user_id = u.id 236 WHERE 237 u.did = ? OR u.handle = ? 238 AND created_at BETWEEN ? AND ? 239 GROUP BY 240 EXTRACT(YEAR FROM created_at), 241 EXTRACT(MONTH FROM created_at) 242 ORDER BY 243 year_month; 244 "#, 245 )?; 246 let scrobbles = stmt.query_map([&did, &did, &start, &end], |row| { 247 Ok(ScrobblesPerMonth { 248 year_month: row.get(0)?, 249 count: row.get(1)?, 250 }) 251 })?; 252 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 253 Ok(HttpResponse::Ok().json(scrobbles?)) 254 } 255 None => { 256 let mut stmt = conn.prepare( 257 r#" 258 SELECT 259 EXTRACT(YEAR FROM created_at) || '-' || 260 LPAD(EXTRACT(MONTH FROM created_at)::VARCHAR, 2, '0') AS year_month, 261 COUNT(DISTINCT scrobbles.created_at) AS count 262 FROM 263 scrobbles 264 WHERE 265 created_at BETWEEN ? AND ? 266 GROUP BY 267 EXTRACT(YEAR FROM created_at), 268 EXTRACT(MONTH FROM created_at) 269 ORDER BY 270 year_month; 271 "#, 272 )?; 273 let scrobbles = stmt.query_map([start, end], |row| { 274 Ok(ScrobblesPerMonth { 275 year_month: row.get(0)?, 276 count: row.get(1)?, 277 }) 278 })?; 279 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 280 Ok(HttpResponse::Ok().json(scrobbles?)) 281 } 282 } 283} 284 285pub async fn get_scrobbles_per_year( 286 payload: &mut web::Payload, 287 _req: &HttpRequest, 288 conn: Arc<Mutex<Connection>>, 289) -> Result<HttpResponse, Error> { 290 let body = read_payload!(payload); 291 let params = serde_json::from_slice::<GetScrobblesPerYearParams>(&body)?; 292 let start = params 293 .start 294 .unwrap_or(GetScrobblesPerDayParams::default().start.unwrap()); 295 let end = params 296 .end 297 .unwrap_or(GetScrobblesPerDayParams::default().end.unwrap()); 298 let did = params.user_did; 299 tracing::info!(start = %start, end = %end, user_did = ?did, "Get scrobbles per year"); 300 301 let conn = conn.lock().unwrap(); 302 match did { 303 Some(did) => { 304 let mut stmt = conn.prepare( 305 r#" 306 SELECT 307 EXTRACT(YEAR FROM created_at) AS year, 308 COUNT(DISTINCT scrobbles.created_at) AS count 309 FROM 310 scrobbles 311 LEFT JOIN users u ON scrobbles.user_id = u.id 312 WHERE 313 u.did = ? OR u.handle = ? 314 AND created_at BETWEEN ? AND ? 315 GROUP BY 316 EXTRACT(YEAR FROM created_at) 317 ORDER BY 318 year; 319 "#, 320 )?; 321 let scrobbles = stmt.query_map([&did, &did, &start, &end], |row| { 322 Ok(ScrobblesPerYear { 323 year: row.get(0)?, 324 count: row.get(1)?, 325 }) 326 })?; 327 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 328 Ok(HttpResponse::Ok().json(scrobbles?)) 329 } 330 None => { 331 let mut stmt = conn.prepare( 332 r#" 333 SELECT 334 EXTRACT(YEAR FROM created_at) AS year, 335 COUNT(DISTINCT scrobbles.created_at) AS count 336 FROM 337 scrobbles 338 WHERE 339 created_at BETWEEN ? AND ? 340 GROUP BY 341 EXTRACT(YEAR FROM created_at) 342 ORDER BY 343 year; 344 "#, 345 )?; 346 let scrobbles = stmt.query_map([start, end], |row| { 347 Ok(ScrobblesPerYear { 348 year: row.get(0)?, 349 count: row.get(1)?, 350 }) 351 })?; 352 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 353 Ok(HttpResponse::Ok().json(scrobbles?)) 354 } 355 } 356} 357 358pub async fn get_album_scrobbles( 359 payload: &mut web::Payload, 360 _req: &HttpRequest, 361 conn: Arc<Mutex<Connection>>, 362) -> Result<HttpResponse, Error> { 363 let body = read_payload!(payload); 364 let params = serde_json::from_slice::<GetAlbumScrobblesParams>(&body)?; 365 let start = params 366 .start 367 .unwrap_or(GetAlbumScrobblesParams::default().start.unwrap()); 368 let end = params 369 .end 370 .unwrap_or(GetAlbumScrobblesParams::default().end.unwrap()); 371 let conn = conn.lock().unwrap(); 372 tracing::info!(album_id = %params.album_id, start = %start, end = %end, "Get album scrobbles"); 373 374 let mut stmt = conn.prepare( 375 r#" 376 SELECT 377 date_trunc('day', s.created_at) AS date, 378 COUNT(s.album_id) AS count 379 FROM 380 scrobbles s 381 LEFT JOIN albums a ON s.album_id = a.id 382 WHERE 383 a.id = ? OR a.uri = ? 384 AND s.created_at BETWEEN ? AND ? 385 GROUP BY 386 date_trunc('day', s.created_at) 387 ORDER BY 388 date; 389 "#, 390 )?; 391 let scrobbles = stmt.query_map([&params.album_id, &params.album_id, &start, &end], |row| { 392 Ok(ScrobblesPerDay { 393 date: row.get(0)?, 394 count: row.get(1)?, 395 }) 396 })?; 397 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 398 Ok(HttpResponse::Ok().json(scrobbles?)) 399} 400 401pub async fn get_artist_scrobbles( 402 payload: &mut web::Payload, 403 _req: &HttpRequest, 404 conn: Arc<Mutex<Connection>>, 405) -> Result<HttpResponse, Error> { 406 let body = read_payload!(payload); 407 let params = serde_json::from_slice::<GetArtistScrobblesParams>(&body)?; 408 let start = params 409 .start 410 .unwrap_or(GetArtistScrobblesParams::default().start.unwrap()); 411 let end = params 412 .end 413 .unwrap_or(GetArtistScrobblesParams::default().end.unwrap()); 414 let conn = conn.lock().unwrap(); 415 tracing::info!(artist_id = %params.artist_id, start = %start, end = %end, "Get artist scrobbles"); 416 417 let mut stmt = conn.prepare( 418 r#" 419 SELECT 420 date_trunc('day', s.created_at) AS date, 421 COUNT(s.artist_id) AS count 422 FROM 423 scrobbles s 424 LEFT JOIN artists a ON s.artist_id = a.id 425 WHERE 426 a.id = ? OR a.uri = ? 427 AND s.created_at BETWEEN ? AND ? 428 GROUP BY 429 date_trunc('day', s.created_at) 430 ORDER BY 431 date; 432 "#, 433 )?; 434 435 let scrobbles = stmt.query_map( 436 [&params.artist_id, &params.artist_id, &start, &end], 437 |row| { 438 Ok(ScrobblesPerDay { 439 date: row.get(0)?, 440 count: row.get(1)?, 441 }) 442 }, 443 )?; 444 445 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 446 Ok(HttpResponse::Ok().json(scrobbles?)) 447} 448 449pub async fn get_track_scrobbles( 450 payload: &mut web::Payload, 451 _req: &HttpRequest, 452 conn: Arc<Mutex<Connection>>, 453) -> Result<HttpResponse, Error> { 454 let body = read_payload!(payload); 455 let params = serde_json::from_slice::<GetTrackScrobblesParams>(&body)?; 456 let start = params 457 .start 458 .unwrap_or(GetTrackScrobblesParams::default().start.unwrap()); 459 let end = params 460 .end 461 .unwrap_or(GetTrackScrobblesParams::default().end.unwrap()); 462 let conn = conn.lock().unwrap(); 463 tracing::info!(track_id = %params.track_id, start = %start, end = %end, "Get track scrobbles"); 464 465 let mut stmt = conn.prepare( 466 r#" 467 SELECT 468 date_trunc('day', s.created_at) AS date, 469 COUNT(s.track_id) AS count 470 FROM 471 scrobbles s 472 LEFT JOIN tracks t ON s.track_id = t.id 473 WHERE 474 t.id = ? OR t.uri = ? 475 AND s.created_at BETWEEN ? AND ? 476 GROUP BY 477 date_trunc('day', s.created_at) 478 ORDER BY 479 date; 480 "#, 481 )?; 482 483 let scrobbles = stmt.query_map([&params.track_id, &params.track_id, &start, &end], |row| { 484 Ok(ScrobblesPerDay { 485 date: row.get(0)?, 486 count: row.get(1)?, 487 }) 488 })?; 489 490 let scrobbles: Result<Vec<_>, _> = scrobbles.collect(); 491 Ok(HttpResponse::Ok().json(scrobbles?)) 492} 493 494pub async fn get_neighbours( 495 payload: &mut web::Payload, 496 _req: &HttpRequest, 497 conn: Arc<Mutex<Connection>>, 498) -> Result<HttpResponse, Error> { 499 let body = read_payload!(payload); 500 let params = serde_json::from_slice::<GetNeighboursParams>(&body)?; 501 let conn = conn.lock().unwrap(); 502 tracing::info!(user_id = %params.user_id, "Get neighbours"); 503 504 let mut stmt = conn.prepare( 505 r#" 506 WITH user_top_artists AS ( 507 SELECT 508 user_id, 509 artist_id, 510 COUNT(*) as play_count, 511 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY COUNT(*) DESC) as artist_rank 512 FROM scrobbles s 513 INNER JOIN artists a ON a.id = s.artist_id 514 WHERE s.artist_id IS NOT NULL 515 AND a.name != 'Various Artists' 516 GROUP BY user_id, artist_id 517 ), 518 weighted_similarity AS ( 519 SELECT 520 u1.user_id as target_user, 521 u2.user_id as neighbor_user, 522 SUM(1.0 / (u1.artist_rank + u2.artist_rank)) as similarity_score, 523 COUNT(DISTINCT u1.artist_id) as shared_artists, 524 ARRAY_AGG(DISTINCT u1.artist_id) FILTER (WHERE u1.artist_rank <= 20) as top_shared_artists 525 FROM user_top_artists u1 526 JOIN user_top_artists u2 527 ON u1.artist_id = u2.artist_id 528 AND u1.user_id != u2.user_id 529 WHERE u1.user_id = ? 530 AND u1.artist_rank <= 50 531 AND u2.artist_rank <= 50 532 GROUP BY u1.user_id, u2.user_id 533 HAVING shared_artists >= 3 534 AND top_shared_artists IS NOT NULL 535 ) 536 SELECT 537 ws.neighbor_user, 538 u.display_name, 539 u.handle, 540 u.did, 541 u.avatar, 542 ws.similarity_score, 543 ws.shared_artists, 544 to_json(LIST(a.name ORDER BY array_position(ws.top_shared_artists, a.id))) as top_shared_artist_names, 545 to_json(LIST({'id': a.id, 'name': a.name, 'picture': a.picture, 'uri': a.uri} 546 ORDER BY array_position(ws.top_shared_artists, a.id))) as top_shared_artists_details 547 FROM weighted_similarity ws 548 LEFT JOIN users u ON u.id = ws.neighbor_user 549 INNER JOIN UNNEST(ws.top_shared_artists) AS t(artist_id) ON true 550 INNER JOIN artists a ON a.id = t.artist_id 551 GROUP BY ws.neighbor_user, u.display_name, u.handle, u.did, u.avatar, ws.similarity_score, ws.shared_artists, ws.top_shared_artists 552 ORDER BY ws.similarity_score DESC 553 LIMIT 20 554 "#, 555 )?; 556 557 let neighbours = stmt.query_map([&params.user_id], |row| { 558 let top_shared_artist_names_json: String = row.get(7)?; 559 let top_shared_artists_details_json: String = row.get(8)?; 560 561 let top_shared_artist_names: Vec<String> = 562 serde_json::from_str(&top_shared_artist_names_json).unwrap_or_else(|_| Vec::new()); 563 let top_shared_artists_details: Vec<crate::types::stats::NeighbourArtist> = 564 serde_json::from_str(&top_shared_artists_details_json).unwrap_or_else(|_| Vec::new()); 565 566 Ok(Neighbour { 567 user_id: row.get(0)?, 568 display_name: row.get(1)?, 569 handle: row.get(2)?, 570 did: row.get(3)?, 571 avatar: row.get(4)?, 572 similarity_score: row.get(5)?, 573 shared_artists_count: row.get(6)?, 574 top_shared_artist_names, 575 top_shared_artists_details, 576 }) 577 })?; 578 579 let neighbours: Result<Vec<_>, _> = neighbours.collect(); 580 Ok(HttpResponse::Ok().json(neighbours?)) 581} 582 583pub async fn get_compatibility( 584 payload: &mut web::Payload, 585 _req: &HttpRequest, 586 conn: Arc<Mutex<Connection>>, 587) -> Result<HttpResponse, Error> { 588 let body = read_payload!(payload); 589 let params = serde_json::from_slice::<GetCompatibilityParams>(&body)?; 590 let conn = conn.lock().unwrap(); 591 tracing::info!(user_id_1 = %params.user_id1, user_id_2 = %params.user_id2, "Get compatibility"); 592 593 let mut stmt = conn.prepare( 594 r#" 595 WITH user_top_artists AS ( 596 SELECT 597 user_id, 598 artist_id, 599 COUNT(*) as play_count, 600 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY COUNT(*) DESC) as artist_rank 601 FROM scrobbles s 602 INNER JOIN artists a ON a.id = s.artist_id 603 WHERE s.artist_id IS NOT NULL 604 AND a.name != 'Various Artists' 605 AND user_id IN (?, ?) 606 GROUP BY user_id, artist_id 607 ), 608 user_totals AS ( 609 SELECT 610 user_id, 611 COUNT(DISTINCT artist_id) as total_artists 612 FROM user_top_artists 613 WHERE artist_rank <= 50 614 GROUP BY user_id 615 ), 616 shared_weighted AS ( 617 SELECT 618 u1.artist_id, 619 u1.artist_rank as user1_rank, 620 u2.artist_rank as user2_rank, 621 (1.0 / u1.artist_rank) * (1.0 / u2.artist_rank) as artist_weight, 622 ROW_NUMBER() OVER (ORDER BY (1.0 / u1.artist_rank) * (1.0 / u2.artist_rank) DESC) as weight_rank 623 FROM user_top_artists u1 624 INNER JOIN user_top_artists u2 625 ON u1.artist_id = u2.artist_id 626 AND u1.user_id = ? 627 AND u2.user_id = ? 628 WHERE u1.artist_rank <= 50 629 AND u2.artist_rank <= 50 630 ), 631 compatibility_calc AS ( 632 SELECT 633 SUM(sw.artist_weight) as weighted_overlap, 634 COUNT(*) as shared_count, 635 (SELECT total_artists FROM user_totals WHERE user_id = ?) as user1_total, 636 (SELECT total_artists FROM user_totals WHERE user_id = ?) as user2_total 637 FROM shared_weighted sw 638 ) 639 SELECT 640 ROUND( 641 (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100, 642 1 643 ) as compatibility_percentage, 644 CASE 645 WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 20 THEN 'Low' 646 WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 40 THEN 'Medium' 647 WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 60 THEN 'High' 648 WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 75 THEN 'Very High' 649 WHEN (shared_count * 1.0 / LEAST(user1_total, user2_total)) * 100 < 90 THEN 'Super' 650 ELSE 'ZOMG!1!' 651 END as compatibility_level, 652 shared_count as shared_artists, 653 user1_total as user1_artist_count, 654 user2_total as user2_artist_count, 655 to_json(LIST(a.name ORDER BY sw.artist_weight DESC) FILTER (WHERE sw.weight_rank <= 10)) as top_shared_artists, 656 to_json(LIST({ 657 'id': a.id, 658 'name': a.name, 659 'picture': a.picture, 660 'uri': a.uri, 661 'user1_rank': sw.user1_rank, 662 'user2_rank': sw.user2_rank, 663 'weight': sw.artist_weight 664 } ORDER BY sw.artist_weight DESC) FILTER (WHERE sw.weight_rank <= 10)) as top_shared_detailed_artists 665 FROM compatibility_calc 666 CROSS JOIN shared_weighted sw 667 INNER JOIN artists a ON a.id = sw.artist_id 668 GROUP BY weighted_overlap, shared_count, user1_total, user2_total; 669 "#, 670 )?; 671 672 let compatibility = stmt.query_map( 673 [ 674 &params.user_id1, 675 &params.user_id2, 676 &params.user_id1, 677 &params.user_id2, 678 &params.user_id1, 679 &params.user_id2, 680 ], 681 |row| { 682 let top_shared_artists_json: String = row.get(5)?; 683 let top_shared_artists: Vec<String> = 684 serde_json::from_str(&top_shared_artists_json).unwrap_or_else(|_| Vec::new()); 685 let top_shared_detailed_artists_json: String = row.get(6)?; 686 let top_shared_detailed_artists: Vec<SharedArtist> = 687 serde_json::from_str(&top_shared_detailed_artists_json) 688 .unwrap_or_else(|_| Vec::new()); 689 Ok(Compatibility { 690 compatibility_percentage: row.get(0)?, 691 compatibility_level: row.get(1)?, 692 shared_artists: row.get(2)?, 693 user1_artist_count: row.get(3)?, 694 user2_artist_count: row.get(4)?, 695 top_shared_artists, 696 top_shared_detailed_artists, 697 }) 698 }, 699 )?; 700 701 let compatibility = compatibility.collect::<Result<Vec<_>, _>>()?; 702 let compatibility = compatibility.into_iter().next(); 703 704 Ok(HttpResponse::Ok().json(compatibility)) 705}