Fixed bug of infinite spotify category fetching

This commit is contained in:
KRTirtho 2021-02-23 12:56:29 +06:00
parent 67bb01526e
commit 5a0486bc4f
10 changed files with 133 additions and 101 deletions

View File

@ -4,12 +4,13 @@ import { QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, Wi
import nodeguiIcon from "../assets/nodegui.jpg"; import nodeguiIcon from "../assets/nodegui.jpg";
import { MemoryRouter } from "react-router"; import { MemoryRouter } from "react-router";
import Routes from "./routes"; import Routes from "./routes";
import SpotifyWebApi from "spotify-web-api-node";
import { LocalStorage } from "node-localstorage"; 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"; import express from "express";
import open from "open";
import spotifyApi from "./initializations/spotifyApi";
export enum CredentialKeys { export enum CredentialKeys {
credentials = "credentials", credentials = "credentials",
@ -31,32 +32,30 @@ function RootApp() {
const windowEvents = useEventHandler<QMainWindowSignals>( const windowEvents = useEventHandler<QMainWindowSignals>(
{ {
async KeyRelease(nativeEv) { async KeyRelease(nativeEv) {
try { try {
if (nativeEv) {
if (nativeEv) { const event = new QKeyEvent(nativeEv);
const event = new QKeyEvent(nativeEv); const eventKey = event.key();
const eventKey = event.key(); console.log("eventKey:", eventKey);
console.log('eventKey:', eventKey) if (audioPlayer.isRunning() && currentTrack)
if(audioPlayer.isRunning() && currentTrack) switch (eventKey) {
switch (eventKey) { case 32: //space
case 32: //space (await audioPlayer.isPaused()) ? await audioPlayer.play() : await audioPlayer.pause();
await audioPlayer.isPaused() ? break;
await audioPlayer.play() : await audioPlayer.pause(); case 16777236: //arrow-right
break; (await audioPlayer.isSeekable()) && (await audioPlayer.seek(+5));
case 16777236: //arrow-right break;
await audioPlayer.isSeekable() && await audioPlayer.seek(+5); case 16777234: //arrow-left
break; (await audioPlayer.isSeekable()) && (await audioPlayer.seek(-5));
case 16777234: //arrow-left break;
await audioPlayer.isSeekable() && await audioPlayer.seek(-5); default:
break; break;
default: }
break;
} }
} catch (error) {
console.error("Error in window events: ", error);
} }
} catch (error) {
console.error("Error in window events: ", error)
}
}, },
}, },
[currentTrack] [currentTrack]
@ -68,7 +67,6 @@ function RootApp() {
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 spotifyApi = new SpotifyWebApi({ redirectUri: redirectURI, ...credentials });
const cachedCredentials = localStorage.getItem(CredentialKeys.credentials); const cachedCredentials = localStorage.getItem(CredentialKeys.credentials);
const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration); const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration);
@ -89,18 +87,37 @@ function RootApp() {
windowRef.current?.removeEventListener(WidgetEventTypes.Close, onWindowClose); windowRef.current?.removeEventListener(WidgetEventTypes.Close, onWindowClose);
}; };
}); });
// for user code login
useEffect(() => {
if (isLoggedIn && credentials && !localStorage.getItem(CredentialKeys.refresh_token)) {
const app = express();
app.use(express.json());
app.get<null, null, null, { code: string }>("/auth/spotify/callback", async (req, res) => {
try {
spotifyApi.setClientId(credentials.clientId);
spotifyApi.setClientSecret(credentials.clientSecret);
const { body: authRes } = await spotifyApi.authorizationCodeGrant(req.query.code);
setAccess_token(authRes.access_token);
setExpireTime(authRes.expires_in);
localStorage.setItem(CredentialKeys.refresh_token, authRes.refresh_token);
return res.end();
} catch (error) {
console.error("Failed to fullfil code grant flow: ", error);
}
});
const server = app.listen(4304, () => {
console.log("Server is running");
open(spotifyApi.createAuthorizeURL(["user-library-read", "user-library-modify"], "xxxyyysssddd")).catch((e) => console.error("Opening IPC connection with browser failed: ", e));
});
return () => {
server.close(() => console.log("Closed server"));
};
}
}, [isLoggedIn, credentials]);
useEffect(() => { useEffect(() => {
if (isLoggedIn) {
spotifyApi
.clientCredentialsGrant()
.then(({ body: { access_token } }) => {
setAccess_token(access_token);
})
.catch((error) => {
console.error("Spotify Client Credential not granted for: ", error);
});
}
if (cachedCredentials) { if (cachedCredentials) {
setCredentials(JSON.parse(cachedCredentials)); setCredentials(JSON.parse(cachedCredentials));
} }
@ -110,7 +127,7 @@ function RootApp() {
<Window ref={windowRef} on={windowEvents} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}> <Window ref={windowRef} on={windowEvents} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}>
<MemoryRouter> <MemoryRouter>
<authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}> <authContext.Provider value={{ isLoggedIn, setIsLoggedIn, access_token, expires_in, setAccess_token, setExpires_in: setExpireTime, ...credentials }}>
<playerContext.Provider value={{ spotifyApi, currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}> <playerContext.Provider value={{ currentPlaylist, currentTrack, setCurrentPlaylist, setCurrentTrack }}>
<View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}> <View style={`flex: 1; flex-direction: 'column'; justify-content: 'center'; align-items: 'stretch'; height: '100%';`}>
<Routes /> <Routes />
{isLoggedIn && <Player />} {isLoggedIn && <Player />}

View File

@ -5,34 +5,34 @@ import authContext from "../context/authContext";
import { useHistory } from "react-router"; import { useHistory } from "react-router";
import CachedImage from "./shared/CachedImage"; import CachedImage from "./shared/CachedImage";
import { CursorShape } from "@nodegui/nodegui"; import { CursorShape } from "@nodegui/nodegui";
import useSpotifyApi from "../hooks/useSpotifyApi";
function Home() { function Home() {
const { spotifyApi, currentPlaylist } = useContext(playerContext); const { currentPlaylist } = useContext(playerContext);
const { isLoggedIn, access_token } = useContext(authContext); const spotifyApi = useSpotifyApi();
const { access_token } = useContext(authContext);
const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]); const [categories, setCategories] = useState<SpotifyApi.CategoryObject[]>([]);
useEffect(() => { useEffect(() => {
(async () => { if (access_token) {
try { (async () => {
if (access_token) { try {
spotifyApi.setAccessToken(access_token);
const categoriesReceived = await spotifyApi.getCategories({ country: "US" }); const categoriesReceived = await spotifyApi.getCategories({ country: "US" });
setCategories(categoriesReceived.body.categories.items); setCategories(categoriesReceived.body.categories.items);
} catch (error) {
console.error("Spotify featured playlist loading failed: ", error);
} }
} catch (error) { })();
console.error("Spotify featured playlist loading failed: ", error); }
}
})();
}, [access_token]); }, [access_token]);
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;`}>
{currentPlaylist && <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 && {categories.map((category, index) => {
categories.map(({ id, name }, index) => { return <CategoryCard key={((index * Date.now()) / Math.random()) * 100} {...category} />;
return <CategoryCard key={((index * Date.now()) / Math.random()) * 100} id={id} name={name} />; })}
})}
</View> </View>
</ScrollArea> </ScrollArea>
); );
@ -48,21 +48,25 @@ interface CategoryCardProps {
function CategoryCard({ id, name }: CategoryCardProps) { function CategoryCard({ id, name }: CategoryCardProps) {
const history = useHistory(); const history = useHistory();
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]); const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
const { access_token, isLoggedIn } = useContext(authContext); const { currentPlaylist } = useContext(playerContext);
const { spotifyApi, currentPlaylist } = useContext(playerContext); const spotifyApi = useSpotifyApi();
useEffect(() => { useEffect(() => {
let mounted = true;
(async () => { (async () => {
try { try {
if (id !== "current") { if (id !== "current") {
spotifyApi.setAccessToken(access_token);
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id, { limit: 4 }); const playlistsRes = await spotifyApi.getPlaylistsForCategory(id, { limit: 4 });
setPlaylists(playlistsRes.body.playlists.items); mounted && 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);
} }
})(); })();
return () => {
mounted = false;
};
}, []); }, []);
function goToGenre() { function goToGenre() {
@ -103,10 +107,9 @@ function CategoryCard({ id, name }: CategoryCardProps) {
{(playlists.length > 0 || id === "current") && <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} />} {id === "current" && currentPlaylist && <PlaylistCard key={(Date.now() / Math.random()) * 100} {...currentPlaylist} />}
{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} />; })}
})}
</View> </View>
</View> </View>
); );

View File

@ -9,6 +9,7 @@ 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"; import authContext from "../context/authContext";
import useSpotifyApi from "../hooks/useSpotifyApi";
export const audioPlayer = new NodeMpv( export const audioPlayer = new NodeMpv(
{ {
@ -16,14 +17,15 @@ export const audioPlayer = new NodeMpv(
auto_restart: true, auto_restart: true,
time_update: 1, time_update: 1,
binary: process.env.MPV_EXECUTABLE ?? "/usr/bin/mpv", binary: process.env.MPV_EXECUTABLE ?? "/usr/bin/mpv",
debug: true, // debug: true,
verbose: true, // verbose: true,
}, },
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"] ["--ytdl-raw-options-set=format=140,http-chunk-size=300000"]
); );
function Player(): ReactElement { function Player(): ReactElement {
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist, spotifyApi } = useContext(playerContext); const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const { access_token } = useContext(authContext); const { access_token } = useContext(authContext);
const initVolume = parseFloat(localStorage.getItem("volume") ?? "55"); const initVolume = parseFloat(localStorage.getItem("volume") ?? "55");
const [isPaused, setIsPaused] = useState(true); const [isPaused, setIsPaused] = useState(true);
@ -67,19 +69,18 @@ function Player(): ReactElement {
}; };
}, []); }, []);
useEffect(() => { // useEffect(() => {
(async () => { // (async () => {
try { // try {
if (access_token) { // if (access_token) {
spotifyApi.setAccessToken(access_token); // const userSavedTrack = await spotifyApi.getMySavedTracks();
const userSavedTrack = await spotifyApi.getMySavedTracks(); // console.log("userSavedTrack:", userSavedTrack);
console.log("userSavedTrack:", userSavedTrack); // }
} // } catch (error) {
} catch (error) { // console.error("Failed to get spotify user saved tracks: ", error);
console.error("Failed to get spotify user saved tracks: ", error); // }
} // })();
})(); // }, [access_token]);
}, [access_token]);
// track change effect // track change effect
useEffect(() => { useEffect(() => {

View File

@ -2,7 +2,7 @@ import { 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";
import playerContext from "../context/playerContext"; import useSpotifyApi from "../hooks/useSpotifyApi";
import BackButton from "./BackButton"; import BackButton from "./BackButton";
import { PlaylistCard } from "./Home"; import { PlaylistCard } from "./Home";
@ -11,20 +11,24 @@ function PlaylistGenreView() {
const location = useLocation<{ name: string }>(); const location = useLocation<{ name: string }>();
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]); const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
const { access_token, isLoggedIn } = useContext(authContext); const { access_token, isLoggedIn } = useContext(authContext);
const { spotifyApi } = useContext(playerContext); const spotifyApi = useSpotifyApi();
useEffect(() => { useEffect(() => {
let mounted = true;
(async () => { (async () => {
try { try {
if (access_token) { if (access_token) {
spotifyApi.setAccessToken(access_token);
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id); const playlistsRes = await spotifyApi.getPlaylistsForCategory(id);
setPlaylists(playlistsRes.body.playlists.items); mounted && 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);
} }
})(); })();
return () => {
mounted = false;
};
}, [access_token]); }, [access_token]);
const playlistGenreViewStylesheet = ` const playlistGenreViewStylesheet = `

View File

@ -1,14 +1,15 @@
import React, { FC, useContext, useEffect, useState } from "react"; import React, { FC, useContext, useEffect, useState } from "react";
import { BoxView, Button, GridView, ScrollArea, Text, View } from "@nodegui/react-nodegui"; import { Button, ScrollArea, Text, View } from "@nodegui/react-nodegui";
import BackButton from "./BackButton"; import BackButton from "./BackButton";
import { useLocation, useParams } from "react-router"; import { useLocation, useParams } from "react-router";
import { Direction, QAbstractButtonSignals, QIcon } from "@nodegui/nodegui"; import { QAbstractButtonSignals, QIcon } from "@nodegui/nodegui";
import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView"; import { WidgetEventListeners } from "@nodegui/react-nodegui/dist/components/View/RNView";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import playerContext from "../context/playerContext"; import playerContext from "../context/playerContext";
import IconButton from "./shared/IconButton"; import IconButton from "./shared/IconButton";
import { heartRegular, play, stop } from "../icons"; import { heartRegular, play, stop } from "../icons";
import { audioPlayer } from "./Player"; import { audioPlayer } from "./Player";
import useSpotifyApi from "../hooks/useSpotifyApi";
export interface PlaylistTrackRes { export interface PlaylistTrackRes {
name: string; name: string;
@ -16,13 +17,10 @@ export interface PlaylistTrackRes {
url: string; url: string;
} }
interface PlaylistViewProps { const PlaylistView: FC = () => {
// audioPlayer: any; const { isLoggedIn } = useContext(authContext);
} const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const PlaylistView: FC<PlaylistViewProps> = () => {
const { isLoggedIn, access_token } = useContext(authContext);
const { spotifyApi, setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
const params = useParams<{ id: string }>(); const params = useParams<{ id: string }>();
const location = useLocation<{ name: string; thumbnail: string }>(); const location = useLocation<{ name: string; thumbnail: string }>();
const [tracks, setTracks] = useState<SpotifyApi.PlaylistTrackObject[]>([]); const [tracks, setTracks] = useState<SpotifyApi.PlaylistTrackObject[]>([]);
@ -31,7 +29,6 @@ const PlaylistView: FC<PlaylistViewProps> = () => {
if (isLoggedIn) { if (isLoggedIn) {
(async () => { (async () => {
try { try {
spotifyApi.setAccessToken(access_token);
const tracks = await spotifyApi.getPlaylistTracks(params.id); const tracks = await spotifyApi.getPlaylistTracks(params.id);
setTracks(tracks.body.items); setTracks(tracks.body.items);
} catch (error) { } catch (error) {

View File

@ -15,16 +15,18 @@ function CachedImage({ src, alt, ...props }: CachedImageProps) {
const [imageProcessError, setImageProcessError] = useState<boolean>(false); const [imageProcessError, setImageProcessError] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
let mounted = true;
(async () => { (async () => {
try { try {
setImageBuffer(await getCachedImageBuffer(src, props.maxSize ?? props.size)); mounted && setImageBuffer(await getCachedImageBuffer(src, props.maxSize ?? props.size));
} catch (error) { } catch (error) {
setImageProcessError(false); mounted && setImageProcessError(false);
console.log("Cached Image Error:", error); console.log("Cached Image Error:", error);
} }
})(); })();
return () => { return () => {
imgRef.current?.close(); imgRef.current?.close();
mounted = false;
}; };
}, []); }, []);
return !imageProcessError && imageBuffer ? ( return !imageProcessError && imageBuffer ? (

View File

@ -3,4 +3,4 @@ import { join } from "path";
const env = dotenv.config({path: join(process.cwd(), ".env")}).parsed as any const env = dotenv.config({path: join(process.cwd(), ".env")}).parsed as any
export const clientId = ""; export const clientId = "";
export const redirectURI = "http%3A%2F%2F/localhost:4304/auth/spotify/callback/" export const redirectURI = "http://localhost:4304/auth/spotify/callback"

View File

@ -1,18 +1,16 @@
import React, { Dispatch, SetStateAction } from "react"; import React, { Dispatch, SetStateAction } from "react";
import SpotifyWebApi from "spotify-web-api-node";
export type CurrentTrack = SpotifyApi.TrackObjectFull; export type CurrentTrack = SpotifyApi.TrackObjectFull;
export type CurrentPlaylist = { tracks: SpotifyApi.PlaylistTrackObject[]; id: string; name: string; thumbnail: string }; export type CurrentPlaylist = { tracks: SpotifyApi.PlaylistTrackObject[]; id: string; name: string; thumbnail: string };
export interface PlayerContext { export interface PlayerContext {
spotifyApi: SpotifyWebApi;
currentPlaylist?: CurrentPlaylist; currentPlaylist?: CurrentPlaylist;
currentTrack?: CurrentTrack; currentTrack?: CurrentTrack;
setCurrentPlaylist: Dispatch<SetStateAction<CurrentPlaylist | undefined>>; setCurrentPlaylist: Dispatch<SetStateAction<CurrentPlaylist | undefined>>;
setCurrentTrack: Dispatch<SetStateAction<CurrentTrack | undefined>>; setCurrentTrack: Dispatch<SetStateAction<CurrentTrack | undefined>>;
} }
const playerContext = React.createContext<PlayerContext>({ spotifyApi: new SpotifyWebApi(), setCurrentPlaylist() {}, setCurrentTrack() {} }); const playerContext = React.createContext<PlayerContext>({ setCurrentPlaylist() {}, setCurrentTrack() {} });
export default playerContext; export default playerContext;

View File

@ -1,7 +1,6 @@
import { DependencyList, useContext, useEffect } from "react"; import { DependencyList, useContext, useEffect } from "react";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import { CredentialKeys } from "../app"; import { CredentialKeys } from "../app";
import playerContext from "../context/playerContext";
import SpotifyWebApi from "spotify-web-api-node"; import SpotifyWebApi from "spotify-web-api-node";
interface UseAccessTokenResult { interface UseAccessTokenResult {

View File

@ -1,11 +1,11 @@
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { CredentialKeys } from "../app";
import authContext from "../context/authContext"; import authContext from "../context/authContext";
import spotifyApi from "../initializations/spotifyApi"; import spotifyApi from "../initializations/spotifyApi";
import useAccessToken from "./useAccessToken";
function useSpotifyApi() { function useSpotifyApi() {
const { isLoggedIn, clientId, clientSecret } = useContext(authContext); const { access_token, clientId, clientSecret, expires_in, isLoggedIn, setExpires_in, setAccess_token } = useContext(authContext);
const { access_token } = useAccessToken(spotifyApi, [clientId, clientSecret]); const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
useEffect(() => { useEffect(() => {
if (isLoggedIn && clientId && clientSecret) { if (isLoggedIn && clientId && clientSecret) {
@ -13,6 +13,17 @@ function useSpotifyApi() {
spotifyApi.setClientSecret(clientSecret); spotifyApi.setClientSecret(clientSecret);
spotifyApi.setAccessToken(access_token); spotifyApi.setAccessToken(access_token);
} }
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);
setExpires_in(expires_in);
})
.catch();
}
}, [access_token, clientId, clientSecret]); }, [access_token, clientId, clientSecret]);
return spotifyApi; return spotifyApi;