add: profile connections, spotify connection
This commit is contained in:
parent
a5c2356940
commit
33ba576d4a
31 changed files with 931 additions and 19 deletions
|
@ -279,3 +279,298 @@
|
|||
document.getElementById("tokens_dialog").showModal();
|
||||
});
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const self = reg_ns("connections");
|
||||
|
||||
self.define("pkce_verifier", async (_, length) => {
|
||||
let text = "";
|
||||
const possible =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
text += possible.charAt(
|
||||
Math.floor(Math.random() * possible.length),
|
||||
);
|
||||
}
|
||||
|
||||
return text;
|
||||
});
|
||||
|
||||
self.define("pkce_challenge", async (_, verifier) => {
|
||||
const data = new TextEncoder().encode(verifier);
|
||||
const digest = await window.crypto.subtle.digest("SHA-256", data);
|
||||
return btoa(
|
||||
String.fromCharCode.apply(null, [...new Uint8Array(digest)]),
|
||||
)
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
});
|
||||
|
||||
self.define("delete", async (_, connection) => {
|
||||
if (
|
||||
!(await trigger("atto::confirm", [
|
||||
"Are you sure you want to do this?",
|
||||
]))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/v1/auth/user/connections/${connection}`, {
|
||||
method: "DELETE",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
trigger("atto::toast", [
|
||||
res.ok ? "success" : "error",
|
||||
res.message,
|
||||
]);
|
||||
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.define("push_con_data", async (_, connection, data) => {
|
||||
return await (
|
||||
await fetch("/api/v1/auth/user/connections/_data", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
connection,
|
||||
data,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
});
|
||||
|
||||
self.define("push_con_state", async (_, connection, data) => {
|
||||
return await (
|
||||
await fetch("/api/v1/auth/user/connections/_state", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
connection,
|
||||
data,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
});
|
||||
|
||||
self.define("push_con_shown", async (_, connection, shown) => {
|
||||
return await (
|
||||
await fetch("/api/v1/auth/user/connections/_shown", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
connection,
|
||||
shown,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
});
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const self = reg_ns("spotify");
|
||||
|
||||
self.define("create_connection", (_, client_id) => {
|
||||
fetch("/api/v1/auth/user/connections/spotify", {
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(async (res) => {
|
||||
// create challenge and store
|
||||
const verifier = await trigger("connections::pkce_verifier", [
|
||||
128,
|
||||
]);
|
||||
|
||||
const challenge = await trigger("connections::pkce_challenge", [
|
||||
verifier,
|
||||
]);
|
||||
|
||||
await trigger("connections::push_con_data", [
|
||||
"Spotify",
|
||||
{
|
||||
verifier,
|
||||
challenge,
|
||||
},
|
||||
]);
|
||||
|
||||
// ...
|
||||
const params = new URLSearchParams();
|
||||
params.append("client_id", client_id);
|
||||
params.append("response_type", "code");
|
||||
params.append(
|
||||
"redirect_uri",
|
||||
`${window.location.origin}/auth/connections_link/Spotify`,
|
||||
);
|
||||
params.append(
|
||||
"scope",
|
||||
"user-read-recently-played user-modify-playback-state user-read-playback-state user-read-email",
|
||||
);
|
||||
params.append("code_challenge_method", "S256");
|
||||
params.append("code_challenge", challenge);
|
||||
|
||||
window.open(
|
||||
`https://accounts.spotify.com/authorize?${params.toString()}`,
|
||||
);
|
||||
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
self.define("get_token", async (_, client_id, verifier, code) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("client_id", client_id);
|
||||
params.append("grant_type", "authorization_code");
|
||||
params.append("code", code);
|
||||
params.append(
|
||||
"redirect_uri",
|
||||
`${window.location.origin}/auth/connections_link/Spotify`,
|
||||
);
|
||||
params.append("code_verifier", verifier);
|
||||
|
||||
const result = await fetch("https://accounts.spotify.com/api/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params,
|
||||
});
|
||||
|
||||
const { access_token, refresh_token, expires_in } = await result.json();
|
||||
return [access_token, refresh_token, expires_in];
|
||||
});
|
||||
|
||||
self.define("refresh_token", async (_, client_id, token) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("client_id", client_id);
|
||||
params.append("grant_type", "refresh_token");
|
||||
params.append("refresh_token", token);
|
||||
|
||||
const result = await fetch("https://accounts.spotify.com/api/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: params,
|
||||
});
|
||||
|
||||
const { access_token, refresh_token, expires_in } = await result.json();
|
||||
return [access_token, refresh_token, expires_in];
|
||||
});
|
||||
|
||||
self.define("profile", async (_, token) => {
|
||||
return await (
|
||||
await fetch("https://api.spotify.com/v1/me", {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
).json();
|
||||
});
|
||||
|
||||
self.define("get_playing", async (_, token) => {
|
||||
// <https://developer.spotify.com/documentation/web-api/reference/get-information-about-the-users-current-playback>
|
||||
return await (
|
||||
await fetch("https://api.spotify.com/v1/me/player", {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
).json();
|
||||
});
|
||||
|
||||
self.define("publish_playing", async (_, playing) => {
|
||||
if (!playing.is_playing) {
|
||||
return await trigger("connections::push_con_state", [
|
||||
"Spotify",
|
||||
{
|
||||
external_urls: {},
|
||||
data: {},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
if (playing.item.is_local) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
window.localStorage.getItem("atto:connections.spotify/id") ===
|
||||
playing.item.id
|
||||
) {
|
||||
// item already pushed to connection, no need right now
|
||||
return;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(
|
||||
"atto:connections.spotify/id",
|
||||
playing.item.id,
|
||||
);
|
||||
|
||||
return await trigger("connections::push_con_state", [
|
||||
"Spotify",
|
||||
{
|
||||
external_urls: {
|
||||
track: playing.item.external_urls.spotify,
|
||||
artist: playing.item.artists[0].external_urls.spotify,
|
||||
album: playing.item.album.external_urls.spotify,
|
||||
album_img: playing.item.album.images[0].url,
|
||||
},
|
||||
data: {
|
||||
id: playing.item.id,
|
||||
// track
|
||||
track: playing.item.name,
|
||||
artist: playing.item.artists[0].name,
|
||||
album: playing.item.album.name,
|
||||
// image
|
||||
// img_w: playing.item.album.images[0].width.toString(),
|
||||
// img_h: playing.item.album.images[0].width.toString(),
|
||||
// times
|
||||
timestamp: playing.timestamp.toString(),
|
||||
progress_ms: playing.progress_ms.toString(),
|
||||
duration_ms: playing.item.duration_ms.toString(),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
self.define("ms_time_text", (_, ms) => {
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = ((ms % 60000) / 1000).toFixed(0);
|
||||
return `${minutes}:${(seconds < 10 ? "0" : "") + seconds}`;
|
||||
});
|
||||
|
||||
self.define(
|
||||
"timestamp",
|
||||
({ $ }, updated_, progress_ms_, duration_ms_, display = "full") => {
|
||||
const now = new Date().getTime();
|
||||
const updated = Number.parseInt(updated_) + 8000;
|
||||
const elapsed_since_update = now - updated;
|
||||
|
||||
const progress_ms =
|
||||
Number.parseInt(progress_ms_) + elapsed_since_update;
|
||||
|
||||
const duration_ms = Number.parseInt(duration_ms_);
|
||||
|
||||
if (progress_ms > duration_ms) {
|
||||
// song is over
|
||||
return "";
|
||||
}
|
||||
|
||||
if (display === "full") {
|
||||
return `${$.ms_time_text(progress_ms)}/${$.ms_time_text(duration_ms)} <span class="fade">(${Math.floor((progress_ms / duration_ms) * 100)}%)</span>`;
|
||||
}
|
||||
|
||||
if (display === "left") {
|
||||
return $.ms_time_text(progress_ms);
|
||||
}
|
||||
|
||||
return $.ms_time_text(duration_ms);
|
||||
},
|
||||
);
|
||||
})();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue