diff --git a/endsong_ui/src/summarize.rs b/endsong_ui/src/summarize.rs index f98d2ba..c766aa8 100644 --- a/endsong_ui/src/summarize.rs +++ b/endsong_ui/src/summarize.rs @@ -52,7 +52,10 @@ pub fn artist(entries: &SongEntries, artist: &Artist) { let albums = get_sorted_playcount_list(album_map, top); let plays = gather::plays(entries, artist); - let percentage_of_plays = format!("{:.2}", (plays as f64 / entries.len() as f64) * 100.0); + let percentage_of_plays = format!( + "{:.2}", + (plays as f64 / gather::all_plays(entries) as f64) * 100.0 + ); let time_played = gather::listening_time(entries, artist); diff --git a/endsong_web/src/artist.rs b/endsong_web/src/artist.rs index 850d8d0..72cc481 100644 --- a/endsong_web/src/artist.rs +++ b/endsong_web/src/artist.rs @@ -40,8 +40,16 @@ struct ArtistTemplate<'a> { artist: &'a Artist, /// This artist's playcount plays: usize, + /// Percentage of this artist's plays to the total playcount + percentage_of_plays: String, /// Time spent listening to this artist time_played: TimeDelta, + /// Date of first artist entry + first_listen: DateTime, + /// Date of most recent artist entry + last_listen: DateTime, + /// This artist's ranking compared to other artists (playcount) + position: usize, } /// GET `/artist/:artist_name(?id=usize)` /// @@ -52,6 +60,11 @@ struct ArtistTemplate<'a> { /// multiple artists with this name /// but different capitalization, /// and [`not_found`] if it's not in the dataset +#[expect(clippy::cast_precision_loss, reason = "necessary for % calc")] +#[expect( + clippy::missing_panics_doc, + reason = "unwraps which should never panic" +)] pub async fn base( State(state): State>, Path(artist_name): Path, @@ -63,7 +76,7 @@ pub async fn base( "GET /artist/:artist_name(?query)" ); - let entries = state.entries.read().await; + let entries = &state.entries; let Some(artists) = entries.find().artist(&artist_name) else { return not_found().await.into_response(); @@ -82,9 +95,32 @@ pub async fn base( return ArtistSelectionTemplate { artists }.into_response(); }; + let (plays, position) = *state.artists.get(artist).unwrap(); + let percentage_of_plays = format!( + "{:.2}", + (plays as f64 / gather::all_plays(entries) as f64) * 100.0 + ); + + // unwrap ok bc already made sure artist exists earlier + let first_listen = entries + .iter() + .find(|entry| artist.is_entry(entry)) + .unwrap() + .timestamp; + let last_listen = entries + .iter() + .rev() + .find(|entry| artist.is_entry(entry)) + .unwrap() + .timestamp; + ArtistTemplate { - plays: gather::plays(&entries, artist), - time_played: gather::listening_time(&entries, artist), + plays, + position, + percentage_of_plays, + time_played: gather::listening_time(entries, artist), + first_listen, + last_listen, artist, } .into_response() diff --git a/endsong_web/src/artists.rs b/endsong_web/src/artists.rs index bb0cd2f..0ff8a5a 100644 --- a/endsong_web/src/artists.rs +++ b/endsong_web/src/artists.rs @@ -54,7 +54,7 @@ pub async fn elements( ) -> impl IntoResponse { debug!(search = form.search, "POST /artists"); - let artists = state.artists.read().await; + let artists = &state.artist_names; let lowercase_search = form.search.to_lowercase(); diff --git a/endsong_web/src/lib.rs b/endsong_web/src/lib.rs index 377b0b0..240aac5 100644 --- a/endsong_web/src/lib.rs +++ b/endsong_web/src/lib.rs @@ -22,29 +22,41 @@ pub mod artist; pub mod artists; pub mod r#static; +use std::collections::HashMap; use std::sync::Arc; use axum::{extract::State, http::StatusCode, response::IntoResponse}; use endsong::prelude::*; +use itertools::Itertools; use rinja::Template; -use tokio::sync::RwLock; use tracing::debug; /// State shared across all handlers #[derive(Clone)] pub struct AppState { /// Reference to the [`SongEntries`] instance used - pub entries: Arc>, - /// Sorted list of all artist names in the dataset - pub artists: Arc>>>, + pub entries: Arc, + /// Sorted (ascending alphabetically) list of all artist names in the dataset + pub artist_names: Arc>>, + /// Map of artists with their playcount (.0) and their position/ranking descending (.1) + pub artists: Arc>, } impl AppState { /// Creates a new [`AppState`] within an [`Arc`] #[must_use] pub fn new(entries: SongEntries) -> Arc { + let artists: HashMap = gather::artists(&entries) + .into_iter() + .sorted_unstable_by_key(|(art, plays)| (std::cmp::Reverse(*plays), art.clone())) + .enumerate() + // bc enumeration starts with 0 :P + .map(|(position, (art, plays))| (art, (plays, position + 1))) + .collect(); + Arc::new(Self { - artists: Arc::new(RwLock::new(entries.artists())), - entries: Arc::new(RwLock::new(entries)), + artists: Arc::new(artists), + artist_names: Arc::new(entries.artists()), + entries: Arc::new(entries), }) } } @@ -73,10 +85,10 @@ struct IndexTemplate { pub async fn index(State(state): State>) -> impl IntoResponse { debug!("GET /"); - let entries = state.entries.read().await; + let entries = &state.entries; IndexTemplate { - total_listened: gather::total_listening_time(&entries), - playcount: gather::all_plays(&entries), + total_listened: gather::total_listening_time(entries), + playcount: gather::all_plays(entries), } } diff --git a/endsong_web/static/tailwind_style.css b/endsong_web/static/tailwind_style.css index 18bd46f..c6578f4 100644 --- a/endsong_web/static/tailwind_style.css +++ b/endsong_web/static/tailwind_style.css @@ -554,6 +554,10 @@ video { display: none; } +.ml-4 { + margin-left: 1rem; +} + .block { display: block; } @@ -562,6 +566,10 @@ video { display: flex; } +.list-item { + display: list-item; +} + .w-full { width: 100%; } @@ -570,6 +578,10 @@ video { list-style-type: none; } +.list-disc { + list-style-type: disc; +} + .flex-col { flex-direction: column; } @@ -693,6 +705,10 @@ video { font-weight: 700; } +.font-semibold { + font-weight: 600; +} + .text-gray-900 { --tw-text-opacity: 1; color: rgb(17 24 39 / var(--tw-text-opacity)); @@ -715,19 +731,9 @@ https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/ } @media (prefers-color-scheme: dark) { - .dark\:border-gray-700 { - --tw-border-opacity: 1; - border-color: rgb(55 65 81 / var(--tw-border-opacity)); - } - - .dark\:border-gray-800 { - --tw-border-opacity: 1; - border-color: rgb(31 41 55 / var(--tw-border-opacity)); - } - - .dark\:bg-slate-800 { + .dark\:bg-gray-950 { --tw-bg-opacity: 1; - background-color: rgb(30 41 59 / var(--tw-bg-opacity)); + background-color: rgb(3 7 18 / var(--tw-bg-opacity)); } .dark\:bg-slate-700 { @@ -735,9 +741,14 @@ https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/ background-color: rgb(51 65 85 / var(--tw-bg-opacity)); } - .dark\:bg-gray-950 { + .dark\:bg-slate-800 { --tw-bg-opacity: 1; - background-color: rgb(3 7 18 / var(--tw-bg-opacity)); + background-color: rgb(30 41 59 / var(--tw-bg-opacity)); + } + + .dark\:text-gray-100 { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); } .dark\:text-white { @@ -745,9 +756,10 @@ https://www.joshwcomeau.com/css/interactive-guide-to-flexbox/ color: rgb(255 255 255 / var(--tw-text-opacity)); } - .dark\:text-gray-100 { - --tw-text-opacity: 1; - color: rgb(243 244 246 / var(--tw-text-opacity)); + .dark\:shadow-none { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .dark\:shadow-white { diff --git a/endsong_web/templates/artist.html b/endsong_web/templates/artist.html index 878f3fd..911299e 100644 --- a/endsong_web/templates/artist.html +++ b/endsong_web/templates/artist.html @@ -3,18 +3,49 @@ {% block title %} {{ artist.name }} {% endblock %} {% block content %} -

{{ artist.name }}

+

{{ artist.name }}

-

General info

+

General info

  • Playcount: {{ plays }}
  • Time spent listening: - +
      + {% let minutes = time_played.num_minutes() %} {% let hours = + time_played.num_hours() %} {% let days = time_played.num_days() %} +
    • + +
    • + {% if hours != 0 %} +
    • + +
    • + {% endif %} {% if days != 0 %} +
    • + +
    • + {% endif %} +
  • +
  • + First listen: + +
  • +
  • + Last listen: + +
  • +
  • % of total plays: {{ percentage_of_plays }}%
  • +
  • #{{ position }} artist of all time
diff --git a/endsong_web/templates/artists.html b/endsong_web/templates/artists.html index b23664f..58206a0 100644 --- a/endsong_web/templates/artists.html +++ b/endsong_web/templates/artists.html @@ -17,6 +17,7 @@ hx-post="/artists" hx-trigger="input changed, search, load" hx-target="#search-results" + autofocus />