Skip to content

Commit

Permalink
Improve Artist page
Browse files Browse the repository at this point in the history
  • Loading branch information
fsktom committed Oct 10, 2024
1 parent eb20737 commit e8d2594
Show file tree
Hide file tree
Showing 17 changed files with 179 additions and 78 deletions.
2 changes: 1 addition & 1 deletion benches/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fn lol(c: &mut Criterion) {
let end = parse_date("2021-01-01").unwrap();
c.bench_function("listening_time", |c| {
c.iter(|| {
black_box(gather::listening_time(entries.between(&start, &end)));
black_box(gather::total_listening_time(entries.between(&start, &end)));
})
});
}
Expand Down
6 changes: 3 additions & 3 deletions endsong_ui/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,16 @@ fn test(entries: &SongEntries) {
print::aspect_date(entries, &final_solution, &start_date, &end_date);

assert_eq!(
gather::listening_time(entries),
gather::listening_time(entries.between(&entries.first_date(), &entries.last_date()))
gather::total_listening_time(entries),
gather::total_listening_time(entries.between(&entries.first_date(), &entries.last_date()))
);

let (time, start, end) = entries.max_listening_time(TimeDelta::try_weeks(26 * 9).unwrap());
dbg!(time.num_minutes(), start.date_naive(), end.date_naive());

dbg!(gather::all_plays(entries.between(&start, &end)));
print::time_played_date(entries, &start, &end);
dbg!(gather::listening_time(entries.between(&start, &end)).num_minutes());
dbg!(gather::total_listening_time(entries.between(&start, &end)).num_minutes());

print::aspect(entries, &Album::new("Built To Last", "HammerFall"));
}
Expand Down
2 changes: 1 addition & 1 deletion endsong_ui/src/print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ pub fn time_played(entries: &SongEntries) {
#[expect(clippy::cast_possible_wrap, reason = "casting usize to i64")]
pub fn time_played_date(entries: &SongEntries, start: &DateTime<Local>, end: &DateTime<Local>) {
assert!(start <= end, "Start date is after end date!");
let duration = gather::listening_time(entries.between(start, end));
let duration = gather::total_listening_time(entries.between(start, end));
let (start, end) = normalize_dates(entries, start, end);
let period = *end - *start;

Expand Down
6 changes: 1 addition & 5 deletions endsong_ui/src/summarize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,7 @@ pub fn artist(entries: &SongEntries, artist: &Artist) {
let plays = gather::plays(entries, artist);
let percentage_of_plays = format!("{:.2}", (plays as f64 / entries.len() as f64) * 100.0);

let time_played = entries
.iter()
.filter(|e| artist.is_entry(e))
.map(|e| e.time_played)
.sum();
let time_played = gather::listening_time(entries, artist);

let first_listen = entries
.iter()
Expand Down
6 changes: 3 additions & 3 deletions endsong_ui/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,11 @@ impl ShellHelper {
self.completer_list = string_vec(&["artist", "album", "song"]);
}

/// Changes tab-complete to the given list of valid inputs - list should be unsorted
/// because it will be sorted here anyway
/// Changes tab-complete to the given list of valid inputs
fn complete_list(&mut self, completer_list: Vec<Arc<str>>) {
self.completer_list = completer_list;
self.completer_list.sort_unstable();
// sort instead of sort_unstable in case it's already sorted
self.completer_list.sort();
}
}
impl Highlighter for ShellHelper {
Expand Down
4 changes: 2 additions & 2 deletions endsong_ui/templates/artist.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ <h2 class="self-center text-xl font-bold">General info</h2>
<li>
First listen:
<time datetime="{{ first_listen }}"
>{{ first_listen|pretty_date}}</time
>{{ first_listen|pretty_date }}</time
>
</li>
<li>
Last listen:
<time datetime="{{ last_listen }}"
>{{ last_listen|pretty_date}}</time
>{{ last_listen|pretty_date }}</time
>
</li>
<li>% of total plays: {{ percentage_of_plays }}%</li>
Expand Down
55 changes: 43 additions & 12 deletions endsong_web/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions endsong_web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ rinja_axum = "0.3"
tokio = { version = "1.40", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
axum-extra = "0.9"
urlencoding = "2.1"
serde = "1.0"
111 changes: 76 additions & 35 deletions endsong_web/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
use std::sync::Arc;

use axum::{
extract::{Path, State},
http::{header::CONTENT_TYPE, HeaderMap, HeaderValue, StatusCode},
extract::{Path, Query, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::get,
Router,
};
use endsong::prelude::*;
use itertools::Itertools;
use rinja_axum::Template;
use tokio::{fs::File, io::AsyncReadExt, sync::RwLock};
use tokio::sync::RwLock;
use tower_http::compression::CompressionLayer;
use tracing::debug;
use tracing_subscriber::filter::{EnvFilter, LevelFilter};

/// Tailwind-generated CSS used on this web page
const STYLING: &str = include_str!("../templates/tailwind_style.css");

#[derive(Clone)]
struct AppState {
entries: Arc<RwLock<SongEntries>>,
Expand Down Expand Up @@ -74,6 +76,7 @@ struct NotFound;
/// 404
async fn not_found() -> impl IntoResponse {
debug!("404");

(StatusCode::NOT_FOUND, NotFound {})
}

Expand All @@ -83,15 +86,8 @@ async fn not_found() -> impl IntoResponse {
/// it isn't requested on each load in full? idk
async fn styles() -> impl IntoResponse {
debug!("GET /styles");
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_str("text/css").unwrap());

let mut file = File::open("templates/tailwind_style.css").await.unwrap();

let mut contents = String::new();
file.read_to_string(&mut contents).await.unwrap();

(headers, contents)
axum_extra::response::Css(STYLING)
}

/// [`Template`] for [`index`]
Expand All @@ -104,9 +100,11 @@ struct Index {
/// GET `/`
async fn index(State(state): State<Arc<AppState>>) -> impl IntoResponse {
debug!("GET /");

let entries = state.entries.read().await;

Index {
total_listened: gather::listening_time(&entries),
total_listened: gather::total_listening_time(&entries),
playcount: gather::all_plays(&entries),
}
}
Expand All @@ -122,45 +120,88 @@ struct Artists {
/// List of artists
async fn artists(State(state): State<Arc<AppState>>) -> impl IntoResponse {
debug!("GET /artists");

let entries = state.entries.read().await;

let artist_names = entries.artists().into_iter().sorted_unstable().collect();
let artist_names = entries.artists();

Artists { artist_names }
}

/// To choose an artist if there are multiple with same capitalization
/// (in my dataset tia)
#[derive(serde::Deserialize)]
struct ArtistQuery {
id: usize,
}
/// [`Template`] for if there are multiple artist with different
/// capitalization in [`artist`]
#[derive(Template)]
#[template(path = "artist_selection.html", print = "none")]
struct ArtistSelection {
artists: Vec<Artist>,
}
/// [`Template`] for [`artist`]
#[derive(Template)]
#[template(path = "artist.html", print = "none")]
struct ArtistPage {
artist: Artist,
struct ArtistPage<'a> {
artist: &'a Artist,
plays: usize,
time_played: TimeDelta,
}
/// GET `/artist/:artist_name`
/// GET `/artist/:artist_name(?id=usize)`
///
/// Artist page
///
/// Returns an [`ArtistPage`] with a valid `artist_name`
/// Returns an [`ArtistPage`] with a valid `artist_name`,
/// an [`ArtistSelection`] if there are multiple artists with this name
/// but different capitalization,
/// and [`not_found`] if it's not in the dataset
async fn artist(State(state): State<Arc<AppState>>, Path(artist_name): Path<String>) -> Response {
debug!("GET /artist/{}", artist_name);
async fn artist(
State(state): State<Arc<AppState>>,
Path(artist_name): Path<String>,
options: Option<Query<ArtistQuery>>,
) -> Response {
debug!(
artist_name = artist_name,
query = options.is_some(),
"GET /artist/:artist_name(?query)"
);

let entries = state.entries.read().await;

match entries.find().artist(&artist_name) {
Some(artist) => {
let artist = artist[0].clone();
ArtistPage {
plays: gather::plays(&entries, &artist),
time_played: entries
.iter()
.filter(|e| artist.is_entry(e))
.map(|e| e.time_played)
.sum(),
artist,
}
.into_response()
}
None => not_found().await.into_response(),
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(Query(options)) = options {
artists.get(options.id)
} else {
None
};

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

ArtistPage {
plays: gather::plays(&entries, artist),
time_played: gather::listening_time(&entries, artist),
artist,
}
.into_response()
}

mod filters {
use urlencoding::encode;

pub fn encodeurl(name: &str) -> rinja::Result<String> {
// bc of artists like AC/DC
Ok(encode(name).to_string())
}
}
Loading

0 comments on commit e8d2594

Please sign in to comment.