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 { MemoryRouter } from "react-router";
import Routes from "./routes";
import SpotifyWebApi from "spotify-web-api-node";
import { LocalStorage } from "node-localstorage";
import authContext from "./context/authContext";
import playerContext, { CurrentPlaylist, CurrentTrack } from "./context/playerContext";
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 {
credentials = "credentials",
@ -33,29 +34,27 @@ function RootApp() {
{
async KeyRelease(nativeEv) {
try {
if (nativeEv) {
const event = new QKeyEvent(nativeEv);
const eventKey = event.key();
console.log('eventKey:', eventKey)
console.log("eventKey:", eventKey);
if (audioPlayer.isRunning() && currentTrack)
switch (eventKey) {
case 32: //space
await audioPlayer.isPaused() ?
await audioPlayer.play() : await audioPlayer.pause();
(await audioPlayer.isPaused()) ? await audioPlayer.play() : await audioPlayer.pause();
break;
case 16777236: //arrow-right
await audioPlayer.isSeekable() && await audioPlayer.seek(+5);
(await audioPlayer.isSeekable()) && (await audioPlayer.seek(+5));
break;
case 16777234: //arrow-left
await audioPlayer.isSeekable() && await audioPlayer.seek(-5);
(await audioPlayer.isSeekable()) && (await audioPlayer.seek(-5));
break;
default:
break;
}
}
} catch (error) {
console.error("Error in window events: ", error)
console.error("Error in window events: ", error);
}
},
},
@ -68,7 +67,6 @@ function RootApp() {
const [access_token, setAccess_token] = useState<string>("");
const [currentPlaylist, setCurrentPlaylist] = useState<CurrentPlaylist>();
const spotifyApi = new SpotifyWebApi({ redirectUri: redirectURI, ...credentials });
const cachedCredentials = localStorage.getItem(CredentialKeys.credentials);
const setExpireTime = (expirationDuration: number) => setExpires_in(Date.now() + expirationDuration);
@ -89,18 +87,37 @@ function RootApp() {
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(() => {
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) {
setCredentials(JSON.parse(cachedCredentials));
}
@ -110,7 +127,7 @@ function RootApp() {
<Window ref={windowRef} on={windowEvents} windowState={WindowState.WindowMaximized} windowIcon={winIcon} windowTitle="Spotube" minSize={minSize}>
<MemoryRouter>
<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%';`}>
<Routes />
{isLoggedIn && <Player />}

View File

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

View File

@ -9,6 +9,7 @@ import PlayerProgressBar from "./PlayerProgressBar";
import { random as shuffleIcon, play, pause, backward, forward, stop, heartRegular } from "../icons";
import IconButton from "./shared/IconButton";
import authContext from "../context/authContext";
import useSpotifyApi from "../hooks/useSpotifyApi";
export const audioPlayer = new NodeMpv(
{
@ -16,14 +17,15 @@ export const audioPlayer = new NodeMpv(
auto_restart: true,
time_update: 1,
binary: process.env.MPV_EXECUTABLE ?? "/usr/bin/mpv",
debug: true,
verbose: true,
// debug: true,
// verbose: true,
},
["--ytdl-raw-options-set=format=140,http-chunk-size=300000"]
);
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 initVolume = parseFloat(localStorage.getItem("volume") ?? "55");
const [isPaused, setIsPaused] = useState(true);
@ -67,19 +69,18 @@ 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]);
// useEffect(() => {
// (async () => {
// try {
// if (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
useEffect(() => {

View File

@ -2,7 +2,7 @@ import { ScrollArea, Text, View } from "@nodegui/react-nodegui";
import React, { useContext, useEffect, useState } from "react";
import { useLocation, useParams } from "react-router";
import authContext from "../context/authContext";
import playerContext from "../context/playerContext";
import useSpotifyApi from "../hooks/useSpotifyApi";
import BackButton from "./BackButton";
import { PlaylistCard } from "./Home";
@ -11,20 +11,24 @@ function PlaylistGenreView() {
const location = useLocation<{ name: string }>();
const [playlists, setPlaylists] = useState<SpotifyApi.PlaylistObjectSimplified[]>([]);
const { access_token, isLoggedIn } = useContext(authContext);
const { spotifyApi } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
useEffect(() => {
let mounted = true;
(async () => {
try {
if (access_token) {
spotifyApi.setAccessToken(access_token);
const playlistsRes = await spotifyApi.getPlaylistsForCategory(id);
setPlaylists(playlistsRes.body.playlists.items);
mounted && setPlaylists(playlistsRes.body.playlists.items);
}
} catch (error) {
console.error(`Failed to get playlists of category ${name} for: `, error);
}
})();
return () => {
mounted = false;
};
}, [access_token]);
const playlistGenreViewStylesheet = `

View File

@ -1,14 +1,15 @@
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 { 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 authContext from "../context/authContext";
import playerContext from "../context/playerContext";
import IconButton from "./shared/IconButton";
import { heartRegular, play, stop } from "../icons";
import { audioPlayer } from "./Player";
import useSpotifyApi from "../hooks/useSpotifyApi";
export interface PlaylistTrackRes {
name: string;
@ -16,13 +17,10 @@ export interface PlaylistTrackRes {
url: string;
}
interface PlaylistViewProps {
// audioPlayer: any;
}
const PlaylistView: FC<PlaylistViewProps> = () => {
const { isLoggedIn, access_token } = useContext(authContext);
const { spotifyApi, setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
const PlaylistView: FC = () => {
const { isLoggedIn } = useContext(authContext);
const { setCurrentTrack, currentPlaylist, currentTrack, setCurrentPlaylist } = useContext(playerContext);
const spotifyApi = useSpotifyApi();
const params = useParams<{ id: string }>();
const location = useLocation<{ name: string; thumbnail: string }>();
const [tracks, setTracks] = useState<SpotifyApi.PlaylistTrackObject[]>([]);
@ -31,7 +29,6 @@ const PlaylistView: FC<PlaylistViewProps> = () => {
if (isLoggedIn) {
(async () => {
try {
spotifyApi.setAccessToken(access_token);
const tracks = await spotifyApi.getPlaylistTracks(params.id);
setTracks(tracks.body.items);
} catch (error) {

View File

@ -15,16 +15,18 @@ function CachedImage({ src, alt, ...props }: CachedImageProps) {
const [imageProcessError, setImageProcessError] = useState<boolean>(false);
useEffect(() => {
let mounted = true;
(async () => {
try {
setImageBuffer(await getCachedImageBuffer(src, props.maxSize ?? props.size));
mounted && setImageBuffer(await getCachedImageBuffer(src, props.maxSize ?? props.size));
} catch (error) {
setImageProcessError(false);
mounted && setImageProcessError(false);
console.log("Cached Image Error:", error);
}
})();
return () => {
imgRef.current?.close();
mounted = false;
};
}, []);
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
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 SpotifyWebApi from "spotify-web-api-node";
export type CurrentTrack = SpotifyApi.TrackObjectFull;
export type CurrentPlaylist = { tracks: SpotifyApi.PlaylistTrackObject[]; id: string; name: string; thumbnail: string };
export interface PlayerContext {
spotifyApi: SpotifyWebApi;
currentPlaylist?: CurrentPlaylist;
currentTrack?: CurrentTrack;
setCurrentPlaylist: Dispatch<SetStateAction<CurrentPlaylist | 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;

View File

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

View File

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