mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Fixed bug of infinite spotify category fetching
This commit is contained in:
parent
67bb01526e
commit
5a0486bc4f
93
src/app.tsx
93
src/app.tsx
@ -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",
|
||||
@ -31,32 +32,30 @@ function RootApp() {
|
||||
|
||||
const windowEvents = useEventHandler<QMainWindowSignals>(
|
||||
{
|
||||
async KeyRelease(nativeEv) {
|
||||
try {
|
||||
|
||||
if (nativeEv) {
|
||||
const event = new QKeyEvent(nativeEv);
|
||||
const eventKey = event.key();
|
||||
console.log('eventKey:', eventKey)
|
||||
if(audioPlayer.isRunning() && currentTrack)
|
||||
switch (eventKey) {
|
||||
case 32: //space
|
||||
await audioPlayer.isPaused() ?
|
||||
await audioPlayer.play() : await audioPlayer.pause();
|
||||
break;
|
||||
case 16777236: //arrow-right
|
||||
await audioPlayer.isSeekable() && await audioPlayer.seek(+5);
|
||||
break;
|
||||
case 16777234: //arrow-left
|
||||
await audioPlayer.isSeekable() && await audioPlayer.seek(-5);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
async KeyRelease(nativeEv) {
|
||||
try {
|
||||
if (nativeEv) {
|
||||
const event = new QKeyEvent(nativeEv);
|
||||
const eventKey = event.key();
|
||||
console.log("eventKey:", eventKey);
|
||||
if (audioPlayer.isRunning() && currentTrack)
|
||||
switch (eventKey) {
|
||||
case 32: //space
|
||||
(await audioPlayer.isPaused()) ? await audioPlayer.play() : await audioPlayer.pause();
|
||||
break;
|
||||
case 16777236: //arrow-right
|
||||
(await audioPlayer.isSeekable()) && (await audioPlayer.seek(+5));
|
||||
break;
|
||||
case 16777234: //arrow-left
|
||||
(await audioPlayer.isSeekable()) && (await audioPlayer.seek(-5));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in window events: ", error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in window events: ", error)
|
||||
}
|
||||
},
|
||||
},
|
||||
[currentTrack]
|
||||
@ -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 />}
|
||||
|
@ -5,34 +5,34 @@ 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(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (access_token) {
|
||||
spotifyApi.setAccessToken(access_token);
|
||||
if (access_token) {
|
||||
(async () => {
|
||||
try {
|
||||
const categoriesReceived = await spotifyApi.getCategories({ country: "US" });
|
||||
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]);
|
||||
|
||||
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,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} />}
|
||||
<View id="child-view">
|
||||
{id === "current" && currentPlaylist && <PlaylistCard key={(Date.now() / Math.random()) * 100} {...currentPlaylist} />}
|
||||
{isLoggedIn &&
|
||||
playlists.map((playlist, index) => {
|
||||
return <PlaylistCard key={((index * Date.now()) / Math.random()) * 100} id={playlist.id} name={playlist.name} thumbnail={playlist.images[0].url} />;
|
||||
})}
|
||||
{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>
|
||||
);
|
||||
|
@ -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(() => {
|
||||
|
@ -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 = `
|
||||
|
@ -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) {
|
||||
|
@ -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 ? (
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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,9 +13,20 @@ 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;
|
||||
}
|
||||
|
||||
export default useSpotifyApi;
|
||||
export default useSpotifyApi;
|
||||
|
Loading…
Reference in New Issue
Block a user