Skip to content

Commit 3e7879a

Browse files
committed
refactor: code
* separate some code to new modules * make new artist redirect match case-insensitive (last.fm ignores cases) * use LazyLock to store a global reqwest client * use Display over ToString * make listenbrainz's `ListenType` an enum * remove some unneeded spotify json variables * remove some unneeded clones * update the colored dependency
1 parent b8eadc1 commit 3e7879a

File tree

8 files changed

+109
-115
lines changed

8 files changed

+109
-115
lines changed

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ edition = "2021"
55

66
[dependencies]
77
anyhow = "1"
8-
colored = "2"
8+
colored = "3"
99
md5 = "0.7"
1010
reqwest = { version = "0.12", features = ["blocking", "json"] }
1111
serde = { version = "1", features = ["derive"] }

config.example.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
user_id = 8834263 # Your osu! user ID
33
mode = "osu" # (optional) The osu! mode to scrobble. Possible values: "osu", "taiko", "fruits", "mania"
44
use_original_metadata = true # (optional) Whether to use original metadata instead of romanized metadata
5-
use_spotify_metadata = false # Whether to use metadata matching local Spotify, if exists. This option ignores the option above
5+
use_spotify_metadata = false # Whether to use metadata matching local Spotify, if exists. If set to true, the option above is ignored
66
min_beatmap_length_secs = 60 # (optional) The minimum beatmap length to scrobble (in seconds)
77
log_scrobbles = false # (optional) Whether to log scrobbles to a file
88
artist_redirects = [ # (optional) An array of tuples that contain the old and new artist name to replace with before scrobbling
@@ -18,4 +18,4 @@ api_secret = "api_secret" # Your Last.fm API secret
1818

1919
# This section can be omitted if you do not use ListenBrainz
2020
[listenbrainz]
21-
user_token = "user_token" # Your ListenBrainz user token
21+
user_token = "user_token" # Your ListenBrainz user token

src/scrobbler/last_fm/mod.rs

+5-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
mod queries;
22

3-
use crate::{exit, logger::log_success};
3+
use crate::{exit, logger::log_success, scrobbler::REQWEST};
44
use anyhow::{bail, Result};
55
use colored::Colorize;
66
use queries::LastfmQuery;
7-
use reqwest::{blocking::Client, StatusCode};
7+
use reqwest::StatusCode;
88
use serde::Deserialize;
99
use std::time::{SystemTime, UNIX_EPOCH};
1010

1111
const API_BASE_URL: &str = "https://ws.audioscrobbler.com/2.0/";
1212

1313
pub struct LastfmScrobbler {
14-
pub client: Client,
1514
pub api_key: String,
1615
pub api_secret: String,
1716
pub session_key: String,
@@ -30,9 +29,7 @@ struct LastfmSessionData {
3029

3130
impl LastfmScrobbler {
3231
pub fn new(username: String, password: String, api_key: String, api_secret: String) -> Self {
33-
let client = Client::new();
34-
35-
let response = client
32+
let response = REQWEST
3633
.post(API_BASE_URL)
3734
.header("content-length", "0")
3835
.query(
@@ -49,12 +46,11 @@ impl LastfmScrobbler {
4946
let Ok(session) = response else { exit!("Last.fm", "Invalid credentials provided.") };
5047
log_success("Last.fm", format!("Successfully authenticated with username {}.", session.session.name.bright_blue()));
5148

52-
Self { client, api_key, api_secret, session_key: session.session.key }
49+
Self { api_key, api_secret, session_key: session.session.key }
5350
}
5451

5552
pub fn scrobble(&self, artist: &str, title: &str, album: Option<&str>, total_length: u32) -> Result<()> {
56-
let status = self
57-
.client
53+
let status = REQWEST
5854
.post(API_BASE_URL)
5955
.header("content-length", "0")
6056
.query(

src/scrobbler/listenbrainz/mod.rs

+7-12
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
mod payloads;
22

3-
use crate::{exit, logger::log_success};
3+
use crate::{exit, logger::log_success, scrobbler::REQWEST};
44
use anyhow::{bail, Result};
55
use colored::Colorize;
6-
use payloads::{Listen, Listens};
7-
use reqwest::{blocking::Client, StatusCode};
6+
use payloads::{Listen, ListenType, Listens};
7+
use reqwest::StatusCode;
88
use serde::Deserialize;
99

1010
const API_BASE_URL: &str = "https://api.listenbrainz.org/1";
1111

1212
pub struct ListenBrainzScrobbler {
13-
pub client: Client,
1413
pub user_token: String,
1514
}
1615

@@ -22,10 +21,7 @@ struct ListenBrainzToken {
2221
impl ListenBrainzScrobbler {
2322
pub fn new(user_token: String) -> Self {
2423
let user_token = format!("Token {user_token}");
25-
26-
let client = Client::new();
27-
28-
let response = client
24+
let response = REQWEST
2925
.get(format!("{API_BASE_URL}/validate-token"))
3026
.header("authorization", &user_token)
3127
.send()
@@ -34,15 +30,14 @@ impl ListenBrainzScrobbler {
3430
let Ok(token) = response else { exit!("ListenBrainz", "Invalid user token provided.") };
3531
log_success("ListenBrainz", format!("Successfully authenticated with username {}.", token.user_name.bright_blue()));
3632

37-
Self { client, user_token }
33+
Self { user_token }
3834
}
3935

4036
pub fn scrobble(&self, artist: &str, title: &str, album: Option<&str>, total_length: u32) -> Result<()> {
41-
let status = self
42-
.client
37+
let status = REQWEST
4338
.post(format!("{API_BASE_URL}/submit-listens"))
4439
.header("authorization", &self.user_token)
45-
.json(&Listens::new("single", vec![Listen::new(artist, title, album, total_length)]))
40+
.json(&Listens::new(ListenType::Single, vec![Listen::new(artist, title, album, total_length)]))
4641
.send()?
4742
.status();
4843

src/scrobbler/listenbrainz/payloads.rs

+10-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,19 @@ use std::{
66

77
#[derive(Serialize)]
88
pub struct Listens {
9-
pub listen_type: String,
9+
pub listen_type: ListenType,
1010
pub payload: Vec<Listen>,
1111
}
1212

13+
#[derive(Serialize)]
14+
#[serde(rename_all = "lowercase")]
15+
pub enum ListenType {
16+
Single,
17+
}
18+
1319
impl Listens {
14-
pub fn new<T: ToString>(listen_type: T, listens: Vec<Listen>) -> Self {
15-
Self { listen_type: listen_type.to_string(), payload: listens }
20+
pub fn new(listen_type: ListenType, listens: Vec<Listen>) -> Self {
21+
Self { listen_type, payload: listens }
1622
}
1723
}
1824

@@ -77,7 +83,7 @@ pub struct TrackAdditionalInfo {
7783
}
7884

7985
impl TrackAdditionalInfo {
80-
pub fn new<T: ToString, U: ToString, V: ToString>(
86+
pub fn new<T: Display, U: Display, V: Display>(
8187
media_player: T,
8288
submission_client: U,
8389
submission_client_version: V,

src/scrobbler/mod.rs

+12-10
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use crate::{
1010
spotify::Spotify,
1111
};
1212
use colored::Colorize;
13-
use std::{thread::sleep, time::Duration};
13+
use reqwest::blocking::Client;
14+
use std::{sync::LazyLock, thread::sleep, time::Duration};
15+
16+
static REQWEST: LazyLock<Client> = LazyLock::new(Client::new);
1417

1518
pub struct Scrobbler {
1619
config: ScrobblerConfig,
@@ -103,22 +106,21 @@ impl Scrobbler {
103106
.as_ref()
104107
.and_then(|artist_redirects| {
105108
artist_redirects.iter().find(|(old, new)| {
106-
[artist_original.to_lowercase(), artist_romanized.to_lowercase()].contains(&old.to_lowercase()) && new != &artist
109+
[artist_original.to_lowercase(), artist_romanized.to_lowercase()].contains(&old.to_lowercase())
110+
&& new.to_lowercase() != artist.to_lowercase()
107111
})
108112
})
109-
.map_or_else(
110-
|| "".into(),
111-
|(old, new)| {
112-
artist = new.clone();
113-
format!(" (redirected from {})", old.bright_blue())
114-
},
115-
);
113+
.map(|(old, new)| {
114+
artist = new.clone();
115+
format!(" (redirected from {})", old.bright_blue())
116+
});
116117

117118
log_success(
118119
"Scrobbler",
119120
format!(
120-
"New score found: {}{redirected_text} - {} ({})",
121+
"New score found: {}{} - {} ({})",
121122
artist.bright_blue(),
123+
redirected_text.as_deref().unwrap_or(""),
122124
title.bright_blue(),
123125
album.as_deref().unwrap_or("Unknown Album").bright_blue(),
124126
),
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
mod payloads;
2+
13
use crate::scores::Beatmapset;
24
use anyhow::{bail, Context, Result};
5+
use payloads::{SpotifyArtist, SpotifyData, SpotifySearchResult, SpotifyToken, SpotifyTrack};
36
use reqwest::blocking::Client;
4-
use serde::Deserialize;
7+
58
use serde_json::json;
69
use std::time::{SystemTime, UNIX_EPOCH};
710

@@ -45,18 +48,13 @@ impl Spotify {
4548
"searchTerm": format!("{} - {}", beatmapset.artist_unicode, beatmapset.title_unicode),
4649
"offset": 0,
4750
"limit": 20,
48-
"numberOfTopResults": 20,
49-
"includeAudiobooks": false,
50-
"includePreReleases": false,
5151
});
52-
5352
let extensions = json!({
5453
"persistedQuery": {
5554
"version": 1,
5655
"sha256Hash": "220d098228a4eaf216b39e8c147865244959c4cc6fd82d394d88afda0b710929",
5756
},
5857
});
59-
6058
let json = self
6159
.client
6260
.get("https://api-partner.spotify.com/pathfinder/v1/query")
@@ -79,80 +77,12 @@ impl Spotify {
7977
}
8078

8179
return Ok(SpotifyTrack {
82-
artist: artist.profile.name.clone(),
83-
title: track.item.data.name.clone(),
84-
album: track.item.data.album_of_track.name.clone(),
80+
artist: artist.profile.name,
81+
title: track.item.data.name,
82+
album: track.item.data.album_of_track.name,
8583
});
8684
}
8785

8886
bail!("Could not find track.");
8987
}
9088
}
91-
92-
#[derive(Deserialize)]
93-
#[serde(rename_all = "camelCase")]
94-
pub struct SpotifyToken {
95-
pub access_token: String,
96-
pub access_token_expiration_timestamp_ms: u128,
97-
}
98-
99-
#[derive(Deserialize)]
100-
pub struct SpotifyTrack {
101-
pub artist: String,
102-
pub title: String,
103-
pub album: String,
104-
}
105-
106-
// Generics
107-
#[derive(Deserialize)]
108-
pub struct SpotifyData<T> {
109-
data: T,
110-
}
111-
112-
#[derive(Deserialize)]
113-
pub struct SpotifyItems<T> {
114-
items: Vec<T>,
115-
}
116-
117-
#[derive(Deserialize)]
118-
pub struct SpotifyItem<T> {
119-
item: T,
120-
}
121-
122-
// Search
123-
#[derive(Deserialize)]
124-
#[serde(rename_all = "camelCase")]
125-
pub struct SpotifySearchResult {
126-
search_v2: SpotifySearchV2,
127-
}
128-
129-
#[derive(Deserialize)]
130-
#[serde(rename_all = "camelCase")]
131-
pub struct SpotifySearchV2 {
132-
tracks_v2: SpotifyItems<SpotifyItem<SpotifyData<SpotifyTrackItem>>>,
133-
}
134-
135-
#[derive(Deserialize)]
136-
#[serde(rename_all = "camelCase")]
137-
pub struct SpotifyTrackItem {
138-
name: String,
139-
album_of_track: SpotifyAlbum,
140-
artists: SpotifyItems<SpotifyArtist>,
141-
}
142-
143-
// Album
144-
#[derive(Deserialize)]
145-
pub struct SpotifyAlbum {
146-
name: String,
147-
}
148-
149-
// Artist
150-
#[derive(Deserialize)]
151-
pub struct SpotifyArtist {
152-
profile: SpotifyArtistProfile,
153-
}
154-
155-
#[derive(Deserialize)]
156-
pub struct SpotifyArtistProfile {
157-
name: String,
158-
}

src/spotify/payloads.rs

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use serde::Deserialize;
2+
3+
#[derive(Deserialize)]
4+
#[serde(rename_all = "camelCase")]
5+
pub struct SpotifyToken {
6+
pub access_token: String,
7+
pub access_token_expiration_timestamp_ms: u128,
8+
}
9+
10+
#[derive(Deserialize)]
11+
pub struct SpotifyTrack {
12+
pub artist: String,
13+
pub title: String,
14+
pub album: String,
15+
}
16+
17+
#[derive(Deserialize)]
18+
pub struct SpotifyData<T> {
19+
pub data: T,
20+
}
21+
22+
#[derive(Deserialize)]
23+
pub struct SpotifyItems<T> {
24+
pub items: Vec<T>,
25+
}
26+
27+
#[derive(Deserialize)]
28+
pub struct SpotifyItem<T> {
29+
pub item: T,
30+
}
31+
32+
#[derive(Deserialize)]
33+
#[serde(rename_all = "camelCase")]
34+
pub struct SpotifySearchResult {
35+
pub search_v2: SpotifySearchV2,
36+
}
37+
38+
#[derive(Deserialize)]
39+
#[serde(rename_all = "camelCase")]
40+
pub struct SpotifySearchV2 {
41+
pub tracks_v2: SpotifyItems<SpotifyItem<SpotifyData<SpotifyTrackItem>>>,
42+
}
43+
44+
#[derive(Deserialize)]
45+
#[serde(rename_all = "camelCase")]
46+
pub struct SpotifyTrackItem {
47+
pub name: String,
48+
pub album_of_track: SpotifyAlbum,
49+
pub artists: SpotifyItems<SpotifyArtist>,
50+
}
51+
52+
#[derive(Deserialize)]
53+
pub struct SpotifyAlbum {
54+
pub name: String,
55+
}
56+
57+
#[derive(Deserialize)]
58+
pub struct SpotifyArtist {
59+
pub profile: SpotifyArtistProfile,
60+
}
61+
62+
#[derive(Deserialize)]
63+
pub struct SpotifyArtistProfile {
64+
pub name: String,
65+
}

0 commit comments

Comments
 (0)