Skip to content

Commit

Permalink
Add basic /album page
Browse files Browse the repository at this point in the history
  • Loading branch information
fsktom committed Oct 15, 2024
1 parent 04c0a98 commit 64fde40
Show file tree
Hide file tree
Showing 7 changed files with 299 additions and 11 deletions.
199 changes: 199 additions & 0 deletions endsong_web/src/album.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//! Contains template for /album routes
#![allow(clippy::module_name_repetitions, reason = "looks nicer")]

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

use std::sync::Arc;

use axum::{
extract::{Path, Query, State},
response::{IntoResponse, Redirect, Response},
};
use endsong::prelude::*;
use rinja_axum::Template;
use serde::Deserialize;
use tracing::debug;

/// To choose an artist and album if there are multiple with same capitalization
#[derive(Deserialize)]
pub struct AlbumQuery {
/// The artist's index in the [`Vec`] returned by [`find::artist`]
artist_id: Option<usize>,
/// The albums's index in the [`Vec`] returned by [`find::album`]
album_id: Option<usize>,
}

/// [`Template`] for if there are multiple artist with different
/// capitalization in [`base`]
#[derive(Template)]
#[template(path = "album_selection.html", print = "none")]
struct AlbumSelectionTemplate {
/// Albums with same name, but different capitalization
///
/// Will only happen if you didn't do [`SongEntries::sum_different_capitalization`]
///
/// See [`find::album`]
albums: Vec<Album>,
/// Link to the album page (without `album_id`)
link_base_album: String,
}
/// [`Template`] for [`base`]
#[derive(Template)]
#[template(path = "album.html", print = "none")]
struct AlbumTemplate<'a> {
/// Reference to the given Album
album: &'a Album,
/// This album's playcount
plays: usize,
/// Percentage of this album's plays to the total playcount
percentage_of_plays: String,
/// Percentage of this album's plays to the artist playcount
percentage_of_artist_plays: String,
/// Time spent listening to this artist
time_played: TimeDelta,
/// Date of first artist entry
first_listen: DateTime<Local>,
/// Date of most recent artist entry
last_listen: DateTime<Local>,
/// Link to artist page
link_artist: String,
}
/// GET `/album/[:artist_name]/[:album_name][?artist_id=usize][?album_id=usize]`
///
/// Artist page
///
/// Returns an [`AlbumTemplate`] with a valid `artist_name` and `album_name`,
/// an [`ArtistSelectionTemplate`] if there are
/// multiple artists with this name
/// but different capitalization,
/// an [`AlbumSelectionTemplate`] if there are
/// multiple artists with this name
/// but different capitalization,
/// and [`not_found`] if the artist or album is 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<Arc<AppState>>,
Path((artist_name, album_name)): Path<(String, String)>,
Query(options): Query<AlbumQuery>,
) -> Response {
debug!(
artist_name = artist_name,
album_name = album_name,
artist_id = options.artist_id,
album_id = options.album_id,
"/album/[:artist_name]/[:album_name][?artist_id=usize][?album_id=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 {
link_base_artist: format!(
"/album/{}/{}",
encode_url(&artist_name),
encode_url(&album_name)
),
artists,
}
.into_response();
};

let Some(albums) = entries.find().album(&album_name, &artist_name) else {
return not_found().await.into_response();
};

let album = if albums.len() == 1 {
albums.first()
} else if let Some(album_id) = options.album_id {
albums.get(album_id)
} else {
None
};

let encoded_artist = encode_url(&artist.name);

let Some(album) = album else {
let encoded_album = encode_url(&albums.first().unwrap().name);

let link_base_album = if let Some(artist_id) = options.artist_id {
format!("/album/{encoded_artist}/{encoded_album}?artist_id={artist_id}")
} else {
format!("/album/{encoded_artist}/{encoded_album}")
};

return AlbumSelectionTemplate {
albums,
link_base_album,
}
.into_response();
};

// http://localhost:3000/album/TiA/%E6%B5%81%E6%98%9F
// (i.e. could only happen on manual link entry)
if &album.artist != artist {
return Redirect::permanent(&format!(
"/artist/{encoded_artist}?artist_id={}",
options.artist_id.unwrap()
))
.into_response();
}

let plays = gather::plays(entries, album);
let percentage_of_plays = format!(
"{:.2}",
(plays as f64 / gather::all_plays(entries) as f64) * 100.0
);
let percentage_of_artist_plays = format!(
"{:.2}",
(plays as f64 / gather::plays(entries, artist) as f64) * 100.0
);

// unwrap ok bc already made sure artist exists earlier
let first_listen = entries
.iter()
.find(|entry| album.is_entry(entry))
.unwrap()
.timestamp;
let last_listen = entries
.iter()
.rev()
.find(|entry| album.is_entry(entry))
.unwrap()
.timestamp;

let link_artist = if let Some(artist_id) = options.artist_id {
format!("/artist/{encoded_artist}?artist_id={artist_id}")
} else {
format!("/artist/{encoded_artist}")
};

AlbumTemplate {
plays,
percentage_of_plays,
percentage_of_artist_plays,
time_played: gather::listening_time(entries, album),
first_listen,
last_listen,
link_artist,
album,
}
.into_response()
}
27 changes: 20 additions & 7 deletions endsong_web/src/artist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,24 @@ pub struct ArtistQuery {
/// capitalization in [`base`]
#[derive(Template)]
#[template(path = "artist_selection.html", print = "none")]
struct ArtistSelectionTemplate {
pub struct ArtistSelectionTemplate {
/// Artists with same name, but different capitalization
///
/// See [`find::artist`]
artists: Vec<Artist>,
pub artists: Vec<Artist>,
/// Link to the artist page (without `artist_id`)
pub link_base_artist: String,
}
impl ArtistSelectionTemplate {
/// Creates a new [`ArtistSelectionTemplate`] with generated `link_base_artist`
#[expect(clippy::missing_panics_doc, reason = "unwrap will never panic")]
#[must_use]
pub fn new(artists: Vec<Artist>) -> Self {
Self {
link_base_artist: format!("/artist/{}", encode_url(&artists.first().unwrap().name)),
artists,
}
}
}
/// [`Template`] for [`base`]
#[derive(Template)]
Expand Down Expand Up @@ -104,7 +117,7 @@ pub async fn base(

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

let (plays, position) = *state.artist_ranking.get(artist).unwrap();
Expand Down Expand Up @@ -190,7 +203,7 @@ pub async fn absolute_plot(

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

// see endsong_ui::trace::absolute
Expand Down Expand Up @@ -251,7 +264,7 @@ pub async fn relative_plot(

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

// see endsong_ui::trace::relative_to_all
Expand Down Expand Up @@ -362,7 +375,7 @@ pub async fn albums(

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

let top = form.top.unwrap_or(1000);
Expand Down Expand Up @@ -450,7 +463,7 @@ pub async fn songs(

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

let top = form.top.unwrap_or(1000);
Expand Down
1 change: 1 addition & 0 deletions endsong_web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#![warn(clippy::allow_attributes)]
#![allow(clippy::unused_async, reason = "axum handlers must be async")]

pub mod album;
pub mod artist;
pub mod artists;
pub mod r#static;
Expand Down
7 changes: 4 additions & 3 deletions endsong_web/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
#![warn(clippy::allow_attributes_without_reason)]
#![warn(clippy::allow_attributes)]

use endsong_web::{artist, artists, index, not_found, r#static, AppState};
use endsong_web::{album, artist, artists, index, not_found, r#static, AppState};

use axum::{routing::get, routing::post, Router};
use endsong::prelude::*;
Expand All @@ -38,14 +38,14 @@ async fn main() {
"macos" => "/Users/filip/Other/Endsong/",
_ => "/mnt/c/temp/Endsong/",
};
let last: u8 = 0;
let last: u8 = 9;
let paths: Vec<String> = (0..=last)
.map(|i| format!("{root}endsong_{i}.json"))
.collect();

let entries = SongEntries::new(&paths)
.unwrap_or_else(|e| panic!("{e}"))
.sum_different_capitalization()
// .sum_different_capitalization()
.filter(30, TimeDelta::try_seconds(10).unwrap());

let state = AppState::new(entries);
Expand All @@ -70,6 +70,7 @@ async fn main() {
"/artist/:artist_name/relative_plot",
get(artist::relative_plot),
)
.route("/album/:artist_name/:album_name", get(album::base))
.with_state(state)
.fallback(not_found)
.layer(compression);
Expand Down
54 changes: 54 additions & 0 deletions endsong_web/templates/album.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{% extends "base.html" %}
<!-- -->
{% block title %} {{ album.artist.name }} - {{ album.name }}{% endblock %}
<!-- -->
{% block content %}
<h2 class="text-2xl font-bold">
<a href="{{ link_artist }}">{{ album.artist.name }}</a> - {{ album.name }}
</h2>
<section class="flex">
<article
class="flex w-full flex-col gap-3 rounded-lg border border-gray-200 p-7 shadow dark:shadow-none"
>
<h2 class="self-center text-xl font-semibold">General info</h2>
<ul class="list-none">
<li>Playcount: {{ plays }}</li>
<li>
Time spent listening:
<ul class="ml-4 list-disc">
{% let minutes = time_played.num_minutes() %} {% let hours =
time_played.num_hours() %} {% let days = time_played.num_days() %}
<li>
<time datetime="{{ time_played }}"
>{{ minutes }} minute{{ minutes|pluralize }}</time
>
</li>
{% if hours != 0 %}
<li>
<time datetime="{{ time_played }}"
>or {{ hours }} hour{{ hours|pluralize }}</time
>
</li>
{% endif %} {% if days != 0 %}
<li>
<time datetime="{{ time_played }}"
>or {{ days }} day{{ days|pluralize }}</time
>
</li>
{% endif %}
</ul>
</li>
<li>
First listen:
<time datetime="{{ first_listen }}">{{ first_listen }}</time>
</li>
<li>
Last listen:
<time datetime="{{ last_listen }}">{{ last_listen }}</time>
</li>
<li>% of total plays: {{ percentage_of_plays }}%</li>
<li>% of {{ album.artist }} plays: {{ percentage_of_artist_plays }}%</li>
</ul>
</article>
</section>
{% endblock %}
16 changes: 16 additions & 0 deletions endsong_web/templates/album_selection.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% extends "base.html" %}
<!-- -->
{% block title %} Album Selection {% endblock %}
<!-- -->
{% block content %}
<h2 class="text-2xl">Which album do you mean?</h2>
<ul class="list-none">
{% for album in albums %}
<li>
<a href="{{ link_base_album }}?album_id={{ loop.index0 }}"
>{{ album.name }}</a
>
</li>
{% endfor %}
</ul>
{% endblock %}
6 changes: 5 additions & 1 deletion endsong_web/templates/artist_selection.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
<h2 class="text-2xl">Which artist do you mean?</h2>
<ul class="list-none">
{% for artist in artists %}
<li><a href="/artist/{{ artist.name }}?artist_id={{ loop.index0 }}">{{ artist.name }}</a></li>
<li>
<a href="{{ link_base_artist }}?artist_id={{ loop.index0 }}"
>{{ artist.name }}</a
>
</li>
{% endfor %}
</ul>
{% endblock %}

0 comments on commit 64fde40

Please sign in to comment.