Skip to content

Commit

Permalink
Display albums & songs on /artist
Browse files Browse the repository at this point in the history
  • Loading branch information
fsktom committed Oct 14, 2024
1 parent be71f70 commit 8e358db
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 58 deletions.
4 changes: 2 additions & 2 deletions endsong_ui/src/summarize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ struct ArtistSummary {
name: Arc<str>,
/// Number of top songs/albums to be displayed
top: usize,
/// Array of top song names with their playcount
/// List of top song names with their playcount sorted by the playcount descending
songs: Vec<(Arc<str>, usize)>,
/// Array of top album names with their playcount
/// List of top album names with their playcount sorted by the playcount descending
albums: Vec<(Arc<str>, usize)>,
/// Count of this artist's plays
plays: usize,
Expand Down
268 changes: 246 additions & 22 deletions endsong_web/src/artist.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
//! Contains templates for `/artist` route
//! Contains templates for `/artist` routes
#![allow(clippy::module_name_repetitions, reason = "looks nicer")]

use crate::{not_found, AppState};
use crate::{encode_url, not_found, AppState};

use std::sync::Arc;

use axum::{
extract::{Path, Query, State},
response::{IntoResponse, Response},
Form,
};
use endsong::prelude::*;
use itertools::Itertools;
use plotly::{Layout, Plot, Scatter};
use rinja_axum::Template;
use serde::Deserialize;
Expand All @@ -21,8 +23,9 @@ use tracing::debug;
#[derive(Deserialize)]
pub struct ArtistQuery {
/// The artist's index in the [`Vec`] returned by [`find::artist`]
id: usize,
artist_id: Option<usize>,
}

/// [`Template`] for if there are multiple artist with different
/// capitalization in [`base`]
#[derive(Template)]
Expand Down Expand Up @@ -51,8 +54,16 @@ struct ArtistTemplate<'a> {
last_listen: DateTime<Local>,
/// This artist's ranking compared to other artists (playcount)
position: usize,
/// Link to albums
link_albums: String,
/// Link to songs
link_songs: String,
/// Link to absolute plot
link_absolute: String,
/// Link to relative plot
link_relative: String,
}
/// GET `/artist/:artist_name(?id=usize)`
/// GET `/artist/[:artist_name][?artist_id=usize]`
///
/// Artist page
///
Expand All @@ -69,12 +80,12 @@ struct ArtistTemplate<'a> {
pub async fn base(
State(state): State<Arc<AppState>>,
Path(artist_name): Path<String>,
options: Option<Query<ArtistQuery>>,
Query(options): Query<ArtistQuery>,
) -> Response {
debug!(
artist_name = artist_name,
query = options.is_some(),
"GET /artist/:artist_name(?query)"
artist_id = options.artist_id,
"GET /artist/[:artist_name][?artist_id=usize]"
);

let entries = &state.entries;
Expand All @@ -85,8 +96,8 @@ pub async fn base(

let artist = if artists.len() == 1 {
artists.first()
} else if let Some(Query(options)) = options {
artists.get(options.id)
} else if let Some(artist_id) = options.artist_id {
artists.get(artist_id)
} else {
None
};
Expand All @@ -96,7 +107,7 @@ pub async fn base(
return ArtistSelectionTemplate { artists }.into_response();
};

let (plays, position) = *state.artists.get(artist).unwrap();
let (plays, position) = *state.artist_ranking.get(artist).unwrap();
let percentage_of_plays = format!(
"{:.2}",
(plays as f64 / gather::all_plays(entries) as f64) * 100.0
Expand All @@ -115,30 +126,52 @@ pub async fn base(
.unwrap()
.timestamp;

let encoded_artist = encode_url(&artist.name);
let (link_albums, link_songs, link_absolute, link_relative) =
if let Some(artist_id) = options.artist_id {
(
format!("/artist/{encoded_artist}/albums?artist_id={artist_id}"),
format!("/artist/{encoded_artist}/songs?artist_id={artist_id}"),
format!("/artist/{encoded_artist}/absolute_plot?artist_id={artist_id}"),
format!("/artist/{encoded_artist}/relative_plot?artist_id={artist_id}"),
)
} else {
(
format!("/artist/{encoded_artist}/albums"),
format!("/artist/{encoded_artist}/songs"),
format!("/artist/{encoded_artist}/absolute_plot"),
format!("/artist/{encoded_artist}/relative_plot"),
)
};

ArtistTemplate {
plays,
position,
percentage_of_plays,
time_played: gather::listening_time(entries, artist),
first_listen,
last_listen,
link_albums,
link_songs,
link_absolute,
link_relative,
artist,
}
.into_response()
}

/// GET `/artist/:artist_name(?id=usize)/absolute_plot`
/// GET `/artist/[:artist_name]/absolute_lot[?artist_id=usize]`
///
/// Has to be in-lined in another base.html-derived template
pub async fn absolute_plot(
State(state): State<Arc<AppState>>,
Path(artist_name): Path<String>,
options: Option<Query<ArtistQuery>>,
Query(options): Query<ArtistQuery>,
) -> Response {
debug!(
artist_name = artist_name,
query = options.is_some(),
"GET /artist/:artist_name(?query)/absolute_plot"
artist_id = options.artist_id,
"GET /artist/[:artist_name]/absolute_lot[?artist_id=usize]"
);

let entries = &state.entries;
Expand All @@ -149,8 +182,8 @@ pub async fn absolute_plot(

let artist = if artists.len() == 1 {
artists.first()
} else if let Some(Query(options)) = options {
artists.get(options.id)
} else if let Some(artist_id) = options.artist_id {
artists.get(artist_id)
} else {
None
};
Expand Down Expand Up @@ -188,18 +221,18 @@ pub async fn absolute_plot(
axum_extra::response::Html(plot_html).into_response()
}

/// GET `/artist/:artist_name(?id=usize)/relative_plot`
/// GET `/artist/[:artist_name]/relative_plot[?artist_id=usize]`
///
/// Has to be in-lined in another base.html-derived template
pub async fn relative_plot(
State(state): State<Arc<AppState>>,
Path(artist_name): Path<String>,
options: Option<Query<ArtistQuery>>,
Query(options): Query<ArtistQuery>,
) -> Response {
debug!(
artist_name = artist_name,
query = options.is_some(),
"GET /artist/:artist_name(?query)/relative_plot"
artist_id = options.artist_id,
"GET /artist/[:artist_name]/relative_plot[?artist_id=usize]"
);

let entries = &state.entries;
Expand All @@ -210,8 +243,8 @@ pub async fn relative_plot(

let artist = if artists.len() == 1 {
artists.first()
} else if let Some(Query(options)) = options {
artists.get(options.id)
} else if let Some(artist_id) = options.artist_id {
artists.get(artist_id)
} else {
None
};
Expand Down Expand Up @@ -261,3 +294,194 @@ pub async fn relative_plot(

axum_extra::response::Html(plot_html).into_response()
}

/// Form for getting albums/songs
#[derive(Deserialize)]
pub struct ArtistForm {
/// Amount of top albums/songs to return
top: Option<usize>,
/// Whether to sum song plays across albums
///
/// Client sends either "on" or empty,
/// so `Some(_) => true` and `None => false`
sum_across_albums: Option<String>,
}

/// [`Template`] for [`albums`]
///
/// Has to be in-lined in another base.html-derived template
///
/// ```rinja
/// {% for (link, album, plays) in albums -%}
/// <li class="ml-7">
/// <a href="{{ link }}"
/// >{{ album.name }} | {{ plays }} plays</a
/// >
/// </li>
/// {% endfor %}
/// ```
#[derive(Template)]
#[template(in_doc = true, ext = "html", print = "none")]
struct AlbumsTemplate {
/// List of the artist's albums sorted by the playcount descending
///
/// Elements: link to album page, [`Album`] instance, plays
albums: Vec<(String, Album, usize)>,
}
/// POST `/artist/[:artist_name]/albums[&artist_id=usize][?top=usize]`
///
/// Lists of top albums
///
/// Has to be in-lined in another base.html-derived template
pub async fn albums(
State(state): State<Arc<AppState>>,
Path(artist_name): Path<String>,
Query(options): Query<ArtistQuery>,
Form(form): Form<ArtistForm>,
) -> Response {
debug!(
artist_name = artist_name,
artist_id = options.artist_id,
top = form.top,
"POST /artist/[:artist_name]/albums[&artist_id=usize][?top=usize]"
);

let entries = &state.entries;

let Some(artists) = entries.find().artist(&artist_name) else {
return not_found().await.into_response();
};

let artist = if artists.len() == 1 {
artists.first()
} else if let Some(artist_id) = options.artist_id {
artists.get(artist_id)
} else {
None
};

let Some(artist) = artist else {
// query if multiple artists with different capitalization
return ArtistSelectionTemplate { artists }.into_response();
};

let top = form.top.unwrap_or(1000);

let get_album_link = |album: &Album| {
if let Some(artist_id) = options.artist_id {
format!(
"/album/{}/{}?artist_id={artist_id}",
encode_url(&album.artist.name),
encode_url(&album.name)
)
} else {
format!(
"/album/{}/{}",
encode_url(&album.artist.name),
encode_url(&album.name)
)
}
};

let album_map = gather::albums_from_artist(entries, artist);
let albums = album_map
.into_iter()
.sorted_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)))
.take(top)
.map(|(album, plays)| (get_album_link(&album), album, plays))
.collect();

AlbumsTemplate { albums }.into_response()
}

/// [`Template`] for [`songs`]
///
/// Has to be in-lined in another base.html-derived template
///
/// ```rinja
/// {% for (link, song, plays) in songs -%}
/// <li class="ml-7">
/// <a href="{{ link }}"
/// >{{ song.name }} | {{ plays }} plays</a
/// >
/// </li>
/// {% endfor %}
/// ```
#[derive(Template)]
#[template(in_doc = true, ext = "html", print = "none")]
struct SongsTemplate {
/// List of the artist's songs sorted by the playcount descending
///
/// Elements: link to song page, [`Song`] instance, plays
songs: Vec<(String, Song, usize)>,
}
/// POST `/artist/[:artist_name]/songs[&artist_id=usize][?top=usize][&sum_across_albums=String]`
///
/// Lists of top albums
///
/// Has to be in-lined in another base.html-derived template
pub async fn songs(
State(state): State<Arc<AppState>>,
Path(artist_name): Path<String>,
Query(options): Query<ArtistQuery>,
Form(form): Form<ArtistForm>,
) -> Response {
debug!(
artist_name = artist_name,
artist_id = options.artist_id,
top = form.top,
sum_across_albums = form.sum_across_albums,
"POST /artist/[:artist_name]/songs[&artist_id=usize][?top=usize][&sum_across_albums=String]"
);

let entries = &state.entries;

let Some(artists) = entries.find().artist(&artist_name) else {
return not_found().await.into_response();
};

let artist = if artists.len() == 1 {
artists.first()
} else if let Some(artist_id) = options.artist_id {
artists.get(artist_id)
} else {
None
};

let Some(artist) = artist else {
// query if multiple artists with different capitalization
return ArtistSelectionTemplate { artists }.into_response();
};

let top = form.top.unwrap_or(1000);

let get_song_link = |song: &Song| {
if let Some(artist_id) = options.artist_id {
format!(
"/song/{}/{}?artist_id={artist_id}",
encode_url(&song.album.artist.name),
encode_url(&song.name)
)
} else {
format!(
"/song/{}/{}",
encode_url(&song.album.artist.name),
encode_url(&song.name)
)
}
};

let song_map = if form.sum_across_albums.is_some() {
gather::songs_from_artist_summed_across_albums(entries, artist)
} else {
gather::songs_from(entries, artist)
};
let songs = song_map
.into_iter()
.sorted_unstable_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)))
.take(top)
.map(|(song, plays)| (get_song_link(&song), song, plays))
.collect();

SongsTemplate { songs }.into_response()
}
Loading

0 comments on commit 8e358db

Please sign in to comment.