add: last.fm status integration
This commit is contained in:
parent
3e2bdceb99
commit
0765156697
17 changed files with 397 additions and 3 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -308,6 +308,12 @@ dependencies = [
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base16ct"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base32"
|
name = "base32"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
@ -3247,8 +3253,10 @@ name = "tetratto-core"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
|
"base16ct",
|
||||||
"bb8-postgres",
|
"bb8-postgres",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
|
"md-5",
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"redis",
|
"redis",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|
|
@ -96,6 +96,7 @@ pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml");
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
pub const VENDOR_SPOTIFY_ICON: &str = include_str!("./public/images/vendor/spotify.svg");
|
pub const VENDOR_SPOTIFY_ICON: &str = include_str!("./public/images/vendor/spotify.svg");
|
||||||
|
pub const VENDOR_LAST_FM_ICON: &str = include_str!("./public/images/vendor/last-fm.svg");
|
||||||
|
|
||||||
/// A container for all loaded icons.
|
/// A container for all loaded icons.
|
||||||
pub(crate) static ICONS: LazyLock<RwLock<HashMap<String, String>>> =
|
pub(crate) static ICONS: LazyLock<RwLock<HashMap<String, String>>> =
|
||||||
|
@ -193,6 +194,7 @@ pub(crate) async fn replace_in_html(input: &str, config: &Config) -> String {
|
||||||
/// Set up public directories.
|
/// Set up public directories.
|
||||||
pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
pub(crate) async fn write_assets(config: &Config) -> PathBufD {
|
||||||
vendor_icon!("spotify", VENDOR_SPOTIFY_ICON, config.dirs.icons);
|
vendor_icon!("spotify", VENDOR_SPOTIFY_ICON, config.dirs.icons);
|
||||||
|
vendor_icon!("last_fm", VENDOR_LAST_FM_ICON, config.dirs.icons);
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
let html_path = PathBufD::current().join(&config.dirs.templates);
|
let html_path = PathBufD::current().join(&config.dirs.templates);
|
||||||
|
|
|
@ -37,4 +37,29 @@ config.connections.spotify_client_id %}
|
||||||
`<b>${message}.</b> You can now close this tab.`;
|
`<b>${message}.</b> You can now close this tab.`;
|
||||||
}, 150);
|
}, 150);
|
||||||
</script>
|
</script>
|
||||||
|
{% elif connection_type == "LastFm" and user and user.connections.LastFm and
|
||||||
|
config.connections.last_fm_key %}
|
||||||
|
<script>
|
||||||
|
setTimeout(async () => {
|
||||||
|
const token = new URLSearchParams(window.location.search).get("token");
|
||||||
|
const api_key = "{{ config.connections.last_fm_key }}";
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
alert("Connection failed (did not get token from Last.fm)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await trigger("last_fm::get_session", [token]);
|
||||||
|
const { message } = await trigger("connections::push_con_data", [
|
||||||
|
"LastFm",
|
||||||
|
{
|
||||||
|
session_token: res.session.key,
|
||||||
|
name: res.session.name,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
document.getElementById("status").innerHTML =
|
||||||
|
`<b>${message}.</b> You can now close this tab.`;
|
||||||
|
}, 1000);
|
||||||
|
</script>
|
||||||
{% endif %} {% endblock %}
|
{% endif %} {% endblock %}
|
||||||
|
|
|
@ -847,4 +847,38 @@ state.data %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %} {%- endmacro %} {% macro last_fm_playing(state, size="60px") -%} {%
|
||||||
|
if state and state.data %}
|
||||||
|
<div class="card-nest">
|
||||||
|
<div class="card flex items-center justify-between gap-2 small">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<b>Listening on</b>
|
||||||
|
{{ icon "last_fm" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card secondary flex gap-2">
|
||||||
|
<img
|
||||||
|
src="{{ state.external_urls.track_img }}"
|
||||||
|
alt="Track cover"
|
||||||
|
loading="lazy"
|
||||||
|
class="avatar"
|
||||||
|
style="--size: {{ size }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h5 class="w-full">
|
||||||
|
<a href="{{ state.external_urls.track }}" class="flush"
|
||||||
|
>{{ state.data.track }}</a
|
||||||
|
>
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<span class="fade"
|
||||||
|
><a href="{{ state.external_urls.artist }}" class="flush"
|
||||||
|
>{{ state.data.artist }}</a
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endif %} {%- endmacro %}
|
{% endif %} {%- endmacro %}
|
||||||
|
|
|
@ -109,6 +109,8 @@
|
||||||
<div style="display: contents;">
|
<div style="display: contents;">
|
||||||
{% if profile.connections.Spotify and profile.connections.Spotify[0].data.name and profile.connections.Spotify[0].show_on_profile %}
|
{% if profile.connections.Spotify and profile.connections.Spotify[0].data.name and profile.connections.Spotify[0].show_on_profile %}
|
||||||
{{ components::spotify_playing(state=profile.connections.Spotify[1]) }}
|
{{ components::spotify_playing(state=profile.connections.Spotify[1]) }}
|
||||||
|
{% elif profile.connections.LastFm and profile.connections.LastFm[0].data.name and profile.connections.LastFm[0].show_on_profile %}
|
||||||
|
{{ components::last_fm_playing(state=profile.connections.LastFm[1]) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -515,13 +515,23 @@
|
||||||
{{ icon "spotify" }}
|
{{ icon "spotify" }}
|
||||||
<span>Spotify</span>
|
<span>Spotify</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %} {% if config.connections.last_fm_key and not
|
||||||
|
user.connections.LastFm %}
|
||||||
|
<button
|
||||||
|
class="quaternary"
|
||||||
|
onclick="trigger('last_fm::create_connection', ['{{ config.connections.last_fm_key }}'])"
|
||||||
|
>
|
||||||
|
{{ icon "last_fm" }}
|
||||||
|
<span>Last.fm</span>
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% for key, value in user.connections %}
|
{% for key, value in user.connections %}
|
||||||
<div class="card-nest">
|
<div class="card-nest">
|
||||||
<div class="card small flex items-center gap-2">
|
<div class="card small flex items-center gap-2">
|
||||||
{% if key == "Spotify" %} {{ icon "spotify" }} {% endif %}
|
{% if key == "Spotify" %} {{ icon "spotify" }} {% elif key ==
|
||||||
|
"LastFm" %} {{ icon "last_fm" }} {% endif %}
|
||||||
|
|
||||||
<b>
|
<b>
|
||||||
{% if value[0].data.name %} {{ value[0].data.name }} {% else
|
{% if value[0].data.name %} {{ value[0].data.name }} {% else
|
||||||
|
|
|
@ -381,6 +381,37 @@ macros -%}
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
</script>
|
</script>
|
||||||
|
{% elif user and user.connections.LastFm and
|
||||||
|
config.connections.last_fm_key and
|
||||||
|
user.connections.LastFm[0].data.session_token %}
|
||||||
|
<script>
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (window.last_fm_init) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.last_fm_init = true;
|
||||||
|
const user = "{{ user.connections.LastFm[0].data.name }}";
|
||||||
|
const session_token =
|
||||||
|
"{{ user.connections.LastFm[0].data.session_token }}";
|
||||||
|
|
||||||
|
if (session_token) {
|
||||||
|
// we already have a token
|
||||||
|
const pull_playing = async () => {
|
||||||
|
const playing = await trigger("last_fm::get_playing", [
|
||||||
|
user,
|
||||||
|
session_token,
|
||||||
|
]);
|
||||||
|
await trigger("last_fm::publish_playing", [playing]);
|
||||||
|
};
|
||||||
|
|
||||||
|
await pull_playing();
|
||||||
|
setInterval(pull_playing, 30_000);
|
||||||
|
} else {
|
||||||
|
window.last_fm_needs_token = true;
|
||||||
|
}
|
||||||
|
}, 150);
|
||||||
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
1
crates/app/src/public/images/vendor/last-fm.svg
vendored
Normal file
1
crates/app/src/public/images/vendor/last-fm.svg
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 284.498"><path fill="#D0232B" d="M225.779 253.403l-18.823-51.007s-30.491 34.026-76.188 34.026c-40.526 0-69.224-35.199-69.224-91.497 0-72.096 36.378-97.891 72.097-97.891 51.584 0 67.997 33.411 82.111 76.205l18.823 58.593c18.813 56.894 54.021 102.609 155.399 102.609 72.701 0 122.026-22.309 122.026-80.914 0-47.49-27.007-72.097-77.418-83.898l-37.494-8.191c-25.807-5.887-33.411-16.412-33.411-34.028 0-19.908 15.808-31.713 41.615-31.713 28.186 0 43.39 10.592 45.687 35.813l58.602-7.003C504.877 21.695 468.496 0 408.693 0c-52.811 0-104.404 19.908-104.404 83.907 0 39.904 19.391 65.085 68.005 76.802l39.904 9.4c29.877 7.013 39.894 19.391 39.894 36.391 0 21.695-21.099 30.494-60.993 30.494-59.21 0-83.908-31.108-97.904-73.893l-19.391-58.593C249.297 28.211 209.998.091 132.014.091 45.791.091 0 54.593 0 147.296c0 89.096 45.697 137.202 127.914 137.202 66.201 0 97.892-31.107 97.892-31.107l-.027.009v.003z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -578,3 +578,109 @@
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
const self = reg_ns("last_fm");
|
||||||
|
|
||||||
|
self.define("api", async (_, method, data) => {
|
||||||
|
return JSON.parse(
|
||||||
|
(
|
||||||
|
await (
|
||||||
|
await fetch(
|
||||||
|
"/api/v1/auth/user/connections/last_fm/api_proxy",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).json()
|
||||||
|
).payload,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("create_connection", (_, client_id) => {
|
||||||
|
fetch("/api/v1/auth/user/connections/last_fm", {
|
||||||
|
method: "POST",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(async (_) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("api_key", client_id);
|
||||||
|
params.append(
|
||||||
|
"cb",
|
||||||
|
`${window.location.origin}/auth/connections_link/LastFm`,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.open(`https://last.fm/api/auth?${params.toString()}`);
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("get_session", async ({ $ }, token) => {
|
||||||
|
return await $.api("auth.getSession", {
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("get_playing", async ({ $ }, user, session_token) => {
|
||||||
|
// <https://lastfm-docs.github.io/api-docs/user/getRecentTracks/>
|
||||||
|
return (
|
||||||
|
await $.api("user.getRecentTracks", {
|
||||||
|
user,
|
||||||
|
sk: session_token,
|
||||||
|
limit: "1",
|
||||||
|
extended: "1",
|
||||||
|
})
|
||||||
|
).recenttracks.track[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
self.define("publish_playing", async (_, playing) => {
|
||||||
|
if (!playing || !playing["@attr"] || !playing["@attr"].nowplaying) {
|
||||||
|
return await trigger("connections::push_con_state", [
|
||||||
|
"LastFm",
|
||||||
|
{
|
||||||
|
external_urls: {},
|
||||||
|
data: {},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
window.localStorage.getItem("atto:connections.last_fm/name") ===
|
||||||
|
playing.name
|
||||||
|
) {
|
||||||
|
// item already pushed to connection, no need right now
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.setItem(
|
||||||
|
"atto:connections.last_fm/name",
|
||||||
|
playing.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
return await trigger("connections::push_con_state", [
|
||||||
|
"LastFm",
|
||||||
|
{
|
||||||
|
external_urls: {
|
||||||
|
track: playing.url,
|
||||||
|
artist: playing.artist.url,
|
||||||
|
track_img: playing.image[2]["#text"],
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
id: playing.mbid,
|
||||||
|
// track
|
||||||
|
track: playing.name,
|
||||||
|
artist: playing.artist.name,
|
||||||
|
album: playing.album["#text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
111
crates/app/src/routes/api/v1/auth/connections/last_fm.rs
Normal file
111
crates/app/src/routes/api/v1/auth/connections/last_fm.rs
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use axum::{response::IntoResponse, Extension, Json};
|
||||||
|
use axum_extra::extract::CookieJar;
|
||||||
|
use tetratto_core::{
|
||||||
|
database::connections::last_fm::LastFmConnection,
|
||||||
|
model::{
|
||||||
|
auth::{ConnectionService, ExternalConnectionData},
|
||||||
|
ApiReturn, Error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use crate::{get_user_from_token, State};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct LastFmApiProxy {
|
||||||
|
pub method: String,
|
||||||
|
pub data: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let mut user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let con = (
|
||||||
|
LastFmConnection::connection(),
|
||||||
|
ExternalConnectionData::default(),
|
||||||
|
);
|
||||||
|
|
||||||
|
user.connections
|
||||||
|
.insert(ConnectionService::LastFm, con.clone());
|
||||||
|
|
||||||
|
if let Err(e) = data
|
||||||
|
.update_user_connections(user.id, user.connections)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Connection created".to_string(),
|
||||||
|
payload: Some(con.0.data),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn proxy_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Json(mut req): Json<LastFmApiProxy>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let None = user.connections.get(&ConnectionService::LastFm) {
|
||||||
|
// connection doesn't exist
|
||||||
|
return Json(Error::GeneralNotFound("connection".to_string()).into());
|
||||||
|
};
|
||||||
|
|
||||||
|
req.data.insert("method".to_string(), req.method);
|
||||||
|
req.data.insert("format".to_string(), "json".to_string());
|
||||||
|
req.data.insert(
|
||||||
|
"api_key".to_string(),
|
||||||
|
data.0.connections.last_fm_key.as_ref().unwrap().to_string(),
|
||||||
|
);
|
||||||
|
req.data.insert(
|
||||||
|
"api_sig".to_string(),
|
||||||
|
LastFmConnection::signature(
|
||||||
|
req.data.clone(),
|
||||||
|
data.0
|
||||||
|
.connections
|
||||||
|
.last_fm_secret
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// build url string
|
||||||
|
let mut out: String = String::new();
|
||||||
|
|
||||||
|
for (i, v) in req.data.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
out.push_str(&format!("?{}={}", v.0, v.1));
|
||||||
|
} else {
|
||||||
|
out.push_str(&format!("&{}={}", v.0, v.1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
let res = reqwest::get(format!("https://ws.audioscrobbler.com/2.0/{out}"))
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: String::new(),
|
||||||
|
payload: res,
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod last_fm;
|
||||||
pub mod spotify;
|
pub mod spotify;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
|
@ -254,6 +254,14 @@ pub fn routes() -> Router {
|
||||||
"/auth/user/connections/spotify",
|
"/auth/user/connections/spotify",
|
||||||
post(auth::connections::spotify::create_request),
|
post(auth::connections::spotify::create_request),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/auth/user/connections/last_fm",
|
||||||
|
post(auth::connections::last_fm::create_request),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/auth/user/connections/last_fm/api_proxy",
|
||||||
|
post(auth::connections::last_fm::proxy_request),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
@ -18,6 +18,10 @@ tetratto-l10n = { path = "../l10n" }
|
||||||
serde_json = "1.0.140"
|
serde_json = "1.0.140"
|
||||||
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
|
totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
|
||||||
reqwest = { version = "0.12.15", features = ["json"] }
|
reqwest = { version = "0.12.15", features = ["json"] }
|
||||||
|
bitflags = "2.9.0"
|
||||||
|
async-recursion = "1.1.1"
|
||||||
|
md-5 = "0.10.6"
|
||||||
|
base16ct = { version = "0.2.0", features = ["alloc"] }
|
||||||
|
|
||||||
redis = { version = "0.30.0", optional = true }
|
redis = { version = "0.30.0", optional = true }
|
||||||
|
|
||||||
|
@ -25,5 +29,3 @@ rusqlite = { version = "0.35.0", optional = true }
|
||||||
|
|
||||||
tokio-postgres = { version = "0.7.13", optional = true }
|
tokio-postgres = { version = "0.7.13", optional = true }
|
||||||
bb8-postgres = { version = "0.9.0", optional = true }
|
bb8-postgres = { version = "0.9.0", optional = true }
|
||||||
bitflags = "2.9.0"
|
|
||||||
async-recursion = "1.1.1"
|
|
||||||
|
|
|
@ -155,12 +155,20 @@ pub struct ConnectionsConfig {
|
||||||
/// <https://developer.spotify.com/documentation/web-api>
|
/// <https://developer.spotify.com/documentation/web-api>
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub spotify_client_id: Option<String>,
|
pub spotify_client_id: Option<String>,
|
||||||
|
/// <https://www.last.fm/api/authspec>
|
||||||
|
#[serde(default)]
|
||||||
|
pub last_fm_key: Option<String>,
|
||||||
|
/// <https://www.last.fm/api/authspec>
|
||||||
|
#[serde(default)]
|
||||||
|
pub last_fm_secret: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConnectionsConfig {
|
impl Default for ConnectionsConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
spotify_client_id: None,
|
spotify_client_id: None,
|
||||||
|
last_fm_key: None,
|
||||||
|
last_fm_secret: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
42
crates/core/src/database/connections/last_fm.rs
Normal file
42
crates/core/src/database/connections/last_fm.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
model::auth::{ConnectionType, ExternalConnectionInfo, UserConnections},
|
||||||
|
};
|
||||||
|
use md5::{Md5, Digest};
|
||||||
|
use base16ct::upper::encode_string;
|
||||||
|
|
||||||
|
/// A connection to last.fm.
|
||||||
|
pub struct LastFmConnection(pub UserConnections, pub Config);
|
||||||
|
|
||||||
|
impl LastFmConnection {
|
||||||
|
/// Create a legacy MD5 signature, since last.fm is old.
|
||||||
|
pub fn signature(params: HashMap<String, String>, secret: String) -> String {
|
||||||
|
let mut params_string = String::new();
|
||||||
|
|
||||||
|
let mut params = params.clone();
|
||||||
|
params.remove("format");
|
||||||
|
|
||||||
|
let mut x = params.iter().collect::<Vec<(&String, &String)>>();
|
||||||
|
x.sort_by(|a, b| a.0.cmp(b.0));
|
||||||
|
|
||||||
|
for param in x {
|
||||||
|
params_string.push_str(&format!("{}{}", param.0, param.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hasher = Md5::new();
|
||||||
|
hasher.update(format!("{params_string}{secret}"));
|
||||||
|
|
||||||
|
let hash = hasher.finalize();
|
||||||
|
encode_string(&hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`ExternalConnectionInfo`] for the connection.
|
||||||
|
pub fn connection() -> ExternalConnectionInfo {
|
||||||
|
ExternalConnectionInfo {
|
||||||
|
con_type: ConnectionType::Token,
|
||||||
|
data: HashMap::new(),
|
||||||
|
show_on_profile: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +1,2 @@
|
||||||
|
pub mod last_fm;
|
||||||
pub mod spotify;
|
pub mod spotify;
|
||||||
|
|
|
@ -346,6 +346,8 @@ impl User {
|
||||||
pub enum ConnectionService {
|
pub enum ConnectionService {
|
||||||
/// A connection to a Spotify account.
|
/// A connection to a Spotify account.
|
||||||
Spotify,
|
Spotify,
|
||||||
|
/// A connection to a last.fm account.
|
||||||
|
LastFm,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue