mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Works fine with clientCredentialGrant flow of spotify
This commit is contained in:
parent
b97a8edc5b
commit
114bd0838a
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +1,10 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist/
|
dist/
|
||||||
*.log
|
*.log
|
||||||
|
# user specific
|
||||||
cache/
|
cache/
|
||||||
local/
|
local/
|
||||||
|
# deply build binaries
|
||||||
|
deploy/linux/build
|
||||||
|
deploy/win32/build
|
||||||
|
deploy/darwin/build
|
2763
package-lock.json
generated
2763
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -6,7 +6,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack -p",
|
"build": "webpack --mode=production",
|
||||||
"dev": "TSC_WATCHFILE=UseFsEvents webpack --mode=development",
|
"dev": "TSC_WATCHFILE=UseFsEvents webpack --mode=development",
|
||||||
"start": "qode ./dist/index.js",
|
"start": "qode ./dist/index.js",
|
||||||
"debug": "qode --inspect ./dist/index.js"
|
"debug": "qode --inspect ./dist/index.js"
|
||||||
@ -15,14 +15,18 @@
|
|||||||
"@nodegui/nodegui": "^0.27.0",
|
"@nodegui/nodegui": "^0.27.0",
|
||||||
"@nodegui/react-nodegui": "^0.10.0",
|
"@nodegui/react-nodegui": "^0.10.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"base64url": "^3.0.1",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"du": "^1.0.0",
|
"du": "^1.0.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
"is-url": "^1.2.4",
|
"is-url": "^1.2.4",
|
||||||
|
"jimp": "^0.16.1",
|
||||||
"node-localstorage": "^2.1.6",
|
"node-localstorage": "^2.1.6",
|
||||||
"node-mpv": "^2.0.0-beta.1",
|
"node-mpv": "^2.0.0-beta.1",
|
||||||
|
"open": "^7.4.1",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"scrape-yt": "^1.4.7",
|
"scrape-yt": "^1.4.7",
|
||||||
"sharp": "^0.27.1",
|
|
||||||
"spotify-web-api-node": "^5.0.2"
|
"spotify-web-api-node": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -32,12 +36,12 @@
|
|||||||
"@babel/preset-typescript": "^7.10.4",
|
"@babel/preset-typescript": "^7.10.4",
|
||||||
"@nodegui/packer": "^1.4.1",
|
"@nodegui/packer": "^1.4.1",
|
||||||
"@types/du": "^1.0.0",
|
"@types/du": "^1.0.0",
|
||||||
|
"@types/express": "^4.17.11",
|
||||||
"@types/is-url": "^1.2.28",
|
"@types/is-url": "^1.2.28",
|
||||||
"@types/node": "^14.11.1",
|
"@types/node": "^14.11.1",
|
||||||
"@types/node-localstorage": "^1.3.0",
|
"@types/node-localstorage": "^1.3.0",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
"@types/react-router": "^5.1.11",
|
"@types/react-router": "^5.1.11",
|
||||||
"@types/sharp": "^0.27.1",
|
|
||||||
"@types/spotify-web-api-node": "^5.0.0",
|
"@types/spotify-web-api-node": "^5.0.0",
|
||||||
"@types/webpack-env": "^1.15.3",
|
"@types/webpack-env": "^1.15.3",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
|
17
src/app.tsx
17
src/app.tsx
@ -9,9 +9,11 @@ import { LocalStorage } from "node-localstorage";
|
|||||||
import authContext from "./context/authContext";
|
import authContext from "./context/authContext";
|
||||||
import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext";
|
import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext";
|
||||||
import Player, { audioPlayer } from "./components/Player";
|
import Player, { audioPlayer } from "./components/Player";
|
||||||
|
import { redirectURI } from "./conf";
|
||||||
|
|
||||||
export enum CredentialKeys {
|
export enum CredentialKeys {
|
||||||
credentials = "credentials",
|
credentials = "credentials",
|
||||||
|
refresh_token = "refresh_token"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Credentials {
|
export interface Credentials {
|
||||||
@ -26,16 +28,16 @@ global.localStorage = new LocalStorage("./local");
|
|||||||
function RootApp() {
|
function RootApp() {
|
||||||
const windowRef = useRef<QMainWindow>();
|
const windowRef = useRef<QMainWindow>();
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
||||||
const [spotifyAuth, setSpotifyAuth] = useState({ clientId: "", clientSecret: "" });
|
const [credentials, setCredentials] = useState<Credentials>({ clientId: "", clientSecret: "" });
|
||||||
const [access_token, setAccess_token] = useState<string>("");
|
const [access_token, setAccess_token] = useState<string>("");
|
||||||
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
|
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
|
||||||
const [currentTrack, setCurrentTrack] = useState<CurrentTrack>();
|
const [currentTrack, setCurrentTrack] = useState<CurrentTrack>();
|
||||||
|
|
||||||
const spotifyApi = new SpotifyWebApi({ ...spotifyAuth });
|
const spotifyApi = new SpotifyWebApi({ redirectUri: redirectURI, ...credentials });
|
||||||
const credentialStr = localStorage.getItem(CredentialKeys.credentials);
|
const cachedCredentials = localStorage.getItem(CredentialKeys.credentials);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoggedIn(!!credentialStr);
|
setIsLoggedIn(!!cachedCredentials);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -62,12 +64,9 @@ function RootApp() {
|
|||||||
console.error("Spotify Client Credential not granted for: ", error);
|
console.error("Spotify Client Credential not granted for: ", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (cachedCredentials) {
|
||||||
if (!credentialStr) {
|
setCredentials(JSON.parse(cachedCredentials));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const credentials = JSON.parse(credentialStr) as Credentials;
|
|
||||||
setSpotifyAuth(credentials);
|
|
||||||
}, [isLoggedIn]);
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import CachedImage from "./shared/CachedImage";
|
|||||||
import { CursorShape } from "@nodegui/nodegui";
|
import { CursorShape } from "@nodegui/nodegui";
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const { spotifyApi } = useContext(playerContext);
|
const { spotifyApi, currentPlaylist } = useContext(playerContext);
|
||||||
const { isLoggedIn, access_token } = useContext(authContext);
|
const { isLoggedIn, access_token } = useContext(authContext);
|
||||||
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
|
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ function Home() {
|
|||||||
return (
|
return (
|
||||||
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}>
|
<ScrollArea style={`flex-grow: 1; border: none; flex: 1;`}>
|
||||||
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
|
<View style={`flex-direction: 'column'; justify-content: 'center'; flex: 1;`}>
|
||||||
<CategoryCard key={((Math.random() * Date.now()) / Math.random()) * 100} id="current" name="Currently Playing" />
|
{currentPlaylist && <CategoryCard key={((Math.random() * Date.now()) / Math.random()) * 100} id="current" name="Currently Playing" />}
|
||||||
{isLoggedIn &&
|
{isLoggedIn &&
|
||||||
categories.map(({ id, name }, index) => {
|
categories.map(({ id, name }, index) => {
|
||||||
return <CategoryCard key={((index * Date.now()) / Math.random()) * 100} id={id} name={name} />;
|
return <CategoryCard key={((index * Date.now()) / Math.random()) * 100} id={id} name={name} />;
|
||||||
@ -54,20 +54,16 @@ function CategoryCard({ id, name }: CategoryCardProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (access_token) {
|
if (id !== "current") {
|
||||||
if (id === "current") {
|
|
||||||
} else {
|
|
||||||
spotifyApi.setAccessToken(access_token);
|
spotifyApi.setAccessToken(access_token);
|
||||||
|
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id, { limit: 4 });
|
||||||
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id, { limit: 5 });
|
|
||||||
setPlaylists(playlistsRes.body.playlists.items);
|
setPlaylists(playlistsRes.body.playlists.items);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to get playlists of category ${name} for: `, error);
|
console.error(`Failed to get playlists of category ${name} for: `, error);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [access_token]);
|
}, []);
|
||||||
|
|
||||||
function goToGenre() {
|
function goToGenre() {
|
||||||
history.push(`/genre/playlists/${id}`, { name });
|
history.push(`/genre/playlists/${id}`, { name });
|
||||||
@ -102,11 +98,11 @@ function CategoryCard({ id, name }: CategoryCardProps) {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (playlists.length > 0 && id!=="current") {
|
|
||||||
return (
|
return (
|
||||||
<View id="container" styleSheet={categoryStylesheet}>
|
<View id="container" styleSheet={categoryStylesheet}>
|
||||||
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
|
{(playlists.length > 0 || id === "current") && <Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />}
|
||||||
<View id="child-view">
|
<View id="child-view">
|
||||||
|
{id === "current" && currentPlaylist && <PlaylistCard key={(Date.now() / Math.random()) * 100} {...currentPlaylist} />}
|
||||||
{isLoggedIn &&
|
{isLoggedIn &&
|
||||||
playlists.map((playlist, index) => {
|
playlists.map((playlist, index) => {
|
||||||
return <PlaylistCard key={((index * Date.now()) / Math.random()) * 100} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
|
return <PlaylistCard key={((index * Date.now()) / Math.random()) * 100} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
|
||||||
@ -115,18 +111,6 @@ function CategoryCard({ id, name }: CategoryCardProps) {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (id === "current" && currentPlaylist) {
|
|
||||||
return (
|
|
||||||
<View id="container" styleSheet={categoryStylesheet}>
|
|
||||||
<Button id="anchor-heading" cursor={CursorShape.PointingHandCursor} on={{ MouseButtonRelease: goToGenre }} text={name} />
|
|
||||||
<View id="child-view">
|
|
||||||
<PlaylistCard key={(Date.now() / Math.random()) * 100} {...currentPlaylist} />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PlaylistCardProps {
|
interface PlaylistCardProps {
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { LineEdit, Text, Button, BoxView } from "@nodegui/react-nodegui";
|
import { LineEdit, Text, Button, View } from "@nodegui/react-nodegui";
|
||||||
import authContext from "../context/authContext";
|
import authContext from "../context/authContext";
|
||||||
import { CredentialKeys, Credentials } from "../app";
|
import { CredentialKeys, Credentials } from "../app";
|
||||||
import { Direction } from "@nodegui/nodegui";
|
|
||||||
|
|
||||||
function Login() {
|
function Login() {
|
||||||
const { setIsLoggedIn } = useContext(authContext);
|
const { setIsLoggedIn } = useContext(authContext);
|
||||||
@ -29,7 +28,7 @@ function Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxView direction={Direction.TopToBottom}>
|
<View style={`flex: 1; flex-direction: 'column';`}>
|
||||||
<Text>{`<center><h1>Add Spotify & Youtube credentials to get started</h1></center>`}</Text>
|
<Text>{`<center><h1>Add Spotify & Youtube credentials to get started</h1></center>`}</Text>
|
||||||
<Text>{`<center><p>Don't worry any of the credentials won't be collected or used for abuses</p></center>`}</Text>
|
<Text>{`<center><p>Don't worry any of the credentials won't be collected or used for abuses</p></center>`}</Text>
|
||||||
<LineEdit
|
<LineEdit
|
||||||
@ -67,7 +66,7 @@ function Login() {
|
|||||||
}}
|
}}
|
||||||
text="Add"
|
text="Add"
|
||||||
/>
|
/>
|
||||||
</BoxView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import { getYoutubeTrack } from "../helpers/getYoutubeTrack";
|
|||||||
import PlayerProgressBar from "./PlayerProgressBar";
|
import PlayerProgressBar from "./PlayerProgressBar";
|
||||||
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular } from "../icons";
|
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular } from "../icons";
|
||||||
import IconButton from "./shared/IconButton";
|
import IconButton from "./shared/IconButton";
|
||||||
|
import authContext from "../context/authContext";
|
||||||
|
|
||||||
export const audioPlayer = new NodeMpv(
|
export const audioPlayer = new NodeMpv(
|
||||||
{
|
{
|
||||||
@ -22,8 +23,10 @@ export const audioPlayer = new NodeMpv(
|
|||||||
);
|
);
|
||||||
|
|
||||||
function Player(): ReactElement {
|
function Player(): ReactElement {
|
||||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
|
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist, spotifyApi } = useContext(playerContext);
|
||||||
const [volume, setVolume] = useState<number>(parseFloat(localStorage.getItem("volume") ?? "55"));
|
const { access_token } = useContext(authContext);
|
||||||
|
const initVolume = parseFloat(localStorage.getItem("volume") ?? "55");
|
||||||
|
const [volume, setVolume] = useState<number>(initVolume);
|
||||||
const [totalDuration, setTotalDuration] = useState(0);
|
const [totalDuration, setTotalDuration] = useState(0);
|
||||||
const [shuffle, setShuffle] = useState<boolean>(false);
|
const [shuffle, setShuffle] = useState<boolean>(false);
|
||||||
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
const [realPlaylist, setRealPlaylist] = useState<CurrentPlaylist["tracks"]>([]);
|
||||||
@ -36,7 +39,7 @@ function Player(): ReactElement {
|
|||||||
},
|
},
|
||||||
sliderReleased: () => {
|
sliderReleased: () => {
|
||||||
localStorage.setItem("volume", volume.toString());
|
localStorage.setItem("volume", volume.toString());
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
@ -49,6 +52,7 @@ function Player(): ReactElement {
|
|||||||
try {
|
try {
|
||||||
if (!playerRunning) {
|
if (!playerRunning) {
|
||||||
await audioPlayer.start();
|
await audioPlayer.start();
|
||||||
|
await audioPlayer.volume(initVolume);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to start audio player", error);
|
console.error("Failed to start audio player", error);
|
||||||
@ -62,9 +66,22 @@ function Player(): ReactElement {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
if (access_token) {
|
||||||
|
spotifyApi.setAccessToken(access_token);
|
||||||
|
const userSavedTrack = await spotifyApi.getMySavedTracks();
|
||||||
|
console.log("userSavedTrack:", userSavedTrack);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get spotify user saved tracks: ", error);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [access_token]);
|
||||||
|
|
||||||
// track change effect
|
// track change effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// titleRef.current?.setAlignment(AlignmentFlag.AlignLeft);
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
if (currentTrack && playerRunning) {
|
if (currentTrack && playerRunning) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Direction } from "@nodegui/nodegui";
|
import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
||||||
import { BoxView, ScrollArea, Text, View } from "@nodegui/react-nodegui";
|
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { useLocation, useParams } from "react-router";
|
import { useLocation, useParams } from "react-router";
|
||||||
import authContext from "../context/authContext";
|
import authContext from "../context/authContext";
|
||||||
@ -29,10 +28,15 @@ function PlaylistGenreView() {
|
|||||||
}, [access_token]);
|
}, [access_token]);
|
||||||
|
|
||||||
const playlistGenreViewStylesheet = `
|
const playlistGenreViewStylesheet = `
|
||||||
|
#genre-container{
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
#heading {
|
#heading {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
#scroll-view{
|
#scroll-view{
|
||||||
|
flex: 1;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@ -46,7 +50,7 @@ function PlaylistGenreView() {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxView direction={Direction.TopToBottom} styleSheet={playlistGenreViewStylesheet}>
|
<View id="genre-container" styleSheet={playlistGenreViewStylesheet}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<Text id="heading">{`<h2>${location.state.name}</h2>`}</Text>
|
<Text id="heading">{`<h2>${location.state.name}</h2>`}</Text>
|
||||||
<ScrollArea id="scroll-view">
|
<ScrollArea id="scroll-view">
|
||||||
@ -57,7 +61,7 @@ function PlaylistGenreView() {
|
|||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</BoxView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
src/conf.ts
Normal file
6
src/conf.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import dotenv from "dotenv"
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
const env = dotenv.config({path: join(process.cwd(), ".env")}).parsed as any
|
||||||
|
export const clientId = "";
|
||||||
|
export const redirectURI = "http%3A%2F%2F/localhost:4304/auth/spotify/callback/"
|
@ -1,16 +1,29 @@
|
|||||||
import React, { Dispatch, SetStateAction } from "react";
|
import React, { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
|
||||||
export interface AuthContext {
|
export interface AuthContext {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
setIsLoggedIn: Dispatch<SetStateAction<boolean>>;
|
setIsLoggedIn: Dispatch<SetStateAction<boolean>>;
|
||||||
access_token: string
|
access_token: string;
|
||||||
|
/**
|
||||||
|
* the time when the current access token will expire \
|
||||||
|
* always update this with `Date.now() + expires_in`
|
||||||
|
*/
|
||||||
|
expires_in: number;
|
||||||
|
/**
|
||||||
|
* sets the time when the current access token will expire \
|
||||||
|
* always update this with `Date.now() + expires_in`
|
||||||
|
*/
|
||||||
|
setExpiresIn: Dispatch<SetStateAction<number>>;
|
||||||
|
setAccess_token: Dispatch<SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authContext = React.createContext<AuthContext>({
|
const authContext = React.createContext<AuthContext>({
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
setIsLoggedIn() {},
|
setIsLoggedIn() {},
|
||||||
access_token: ""
|
access_token: "",
|
||||||
|
expires_in: 0,
|
||||||
|
setExpiresIn() {},
|
||||||
|
setAccess_token() {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default authContext;
|
export default authContext;
|
||||||
|
35
src/helpers/authorizationCodePKCEGrant.ts
Normal file
35
src/helpers/authorizationCodePKCEGrant.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
import qs from "querystring";
|
||||||
|
import { redirectURI } from "../conf";
|
||||||
|
|
||||||
|
export interface AuthorizationResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
scope: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authorizationCodePKCEGrant({ client_id, code, code_verifier }: { code: string; code_verifier: string; client_id: string }): Promise<AxiosResponse<AuthorizationResponse>> {
|
||||||
|
const body = {
|
||||||
|
client_id,
|
||||||
|
code,
|
||||||
|
code_verifier,
|
||||||
|
redirect_uri: redirectURI,
|
||||||
|
grant_type: "authorization_code",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await axios.post<AuthorizationResponse>("https://accounts.spotify.com/api/token", qs.stringify(body), {
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.152 Safari/537.36",
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default authorizationCodePKCEGrant;
|
13
src/helpers/generateCodeChallenge.ts
Normal file
13
src/helpers/generateCodeChallenge.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import base64url from "base64url";
|
||||||
|
|
||||||
|
export function generateCodeChallenge() {
|
||||||
|
try {
|
||||||
|
const code_verifier = crypto.randomBytes(64).toString("hex");
|
||||||
|
const base64Digest = crypto.createHash("sha256").update(code_verifier).digest("base64");
|
||||||
|
const code_challenge = base64url.fromBase64(base64Digest);
|
||||||
|
return {code_challenge, code_verifier};
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
@ -4,9 +4,10 @@ import * as fs from "fs"
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Stream } from "stream";
|
import { Stream } from "stream";
|
||||||
import { streamToBuffer } from "./streamToBuffer";
|
import { streamToBuffer } from "./streamToBuffer";
|
||||||
import sharp from "sharp";
|
import Jimp from "jimp";
|
||||||
import du from "du";
|
import du from "du";
|
||||||
|
|
||||||
|
|
||||||
interface ImageDimensions {
|
interface ImageDimensions {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
@ -28,7 +29,7 @@ export async function getCachedImageBuffer(name: string, dims?: ImageDimensions)
|
|||||||
fs.rmSync(cacheFolder, { recursive: true, force: true });
|
fs.rmSync(cacheFolder, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
const cachedImg = await fsm.readFile(cachePath);
|
const cachedImg = await fsm.readFile(cachePath);
|
||||||
const cachedImgMeta = await sharp(cachedImg).metadata();
|
const cachedImgMeta = (await Jimp.read(cachedImg)).bitmap;
|
||||||
|
|
||||||
// if the dimensions are changed then the previously cached
|
// if the dimensions are changed then the previously cached
|
||||||
// images are removed and replaced with a new one
|
// images are removed and replaced with a new one
|
||||||
@ -59,7 +60,8 @@ export async function getCachedImageBuffer(name: string, dims?: ImageDimensions)
|
|||||||
async function imageResizeAndWrite(img: Buffer, { cacheFolder, cacheName, dims }: { dims: ImageDimensions; cacheFolder: string; cacheName: string }): Promise<Buffer> {
|
async function imageResizeAndWrite(img: Buffer, { cacheFolder, cacheName, dims }: { dims: ImageDimensions; cacheFolder: string; cacheName: string }): Promise<Buffer> {
|
||||||
// caching the images by resizing if the max/fixed (Width/Height)
|
// caching the images by resizing if the max/fixed (Width/Height)
|
||||||
// is available in the args
|
// is available in the args
|
||||||
const resizedImg = await sharp(img).resize(dims.width, dims.height).toBuffer();
|
const resizedImg = (await Jimp.read(img)).resize(dims.width, dims.height)
|
||||||
await fsm.writeFile(path.join(cacheFolder, cacheName), resizedImg);
|
const resizedImgBuffer = await resizedImg.getBufferAsync(resizedImg._originalMime);
|
||||||
return resizedImg;
|
await fsm.writeFile(path.join(cacheFolder, cacheName), resizedImgBuffer);
|
||||||
|
return resizedImgBuffer;
|
||||||
}
|
}
|
||||||
|
30
src/hooks/useAuth.ts
Normal file
30
src/hooks/useAuth.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { DependencyList, useContext, useEffect } from "react";
|
||||||
|
import authContext from "../context/authContext";
|
||||||
|
import { CredentialKeys } from "../app";
|
||||||
|
import playerContext from "../context/playerContext";
|
||||||
|
|
||||||
|
interface UseAuthResult {
|
||||||
|
access_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (deps: DependencyList = []): UseAuthResult => {
|
||||||
|
const { access_token, expires_in, isLoggedIn, setExpiresIn, setAccess_token } = useContext(authContext);
|
||||||
|
const { spotifyApi } = useContext(playerContext);
|
||||||
|
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isExpiredToken = Date.now() > expires_in;
|
||||||
|
if (isLoggedIn && isExpiredToken && refreshToken) {
|
||||||
|
spotifyApi.setRefreshToken(refreshToken);
|
||||||
|
spotifyApi
|
||||||
|
.refreshAccessToken()
|
||||||
|
.then(({ body: { access_token, expires_in } }) => {
|
||||||
|
setAccess_token(access_token);
|
||||||
|
setExpiresIn(Date.now() + expires_in);
|
||||||
|
})
|
||||||
|
.catch();
|
||||||
|
}
|
||||||
|
}, deps);
|
||||||
|
|
||||||
|
return { access_token };
|
||||||
|
};
|
@ -8,9 +8,6 @@ module.exports = (env, argv) => {
|
|||||||
mode: "production",
|
mode: "production",
|
||||||
entry: ["./src/index.tsx"],
|
entry: ["./src/index.tsx"],
|
||||||
target: "node",
|
target: "node",
|
||||||
externals: {
|
|
||||||
'sharp': 'commonjs sharp',
|
|
||||||
},
|
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, "dist"),
|
path: path.resolve(__dirname, "dist"),
|
||||||
filename: "index.js",
|
filename: "index.js",
|
||||||
|
Loading…
Reference in New Issue
Block a user