mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Common authentication mechanism with server-to-server flow works..
This commit is contained in:
parent
114bd0838a
commit
67bb01526e
1
deploy/config.json
Normal file
1
deploy/config.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"appName":"spotube"}
|
8
deploy/linux/spotube/default.desktop
Normal file
8
deploy/linux/spotube/default.desktop
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Application
|
||||||
|
Exec=AppRun %F
|
||||||
|
Icon=default
|
||||||
|
Comment=Edit this default file
|
||||||
|
Terminal=true
|
||||||
|
Categories=Utility;
|
0
deploy/linux/spotube/default.png
Normal file
0
deploy/linux/spotube/default.png
Normal file
6
deploy/linux/spotube/index.js
Normal file
6
deploy/linux/spotube/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
const path = require("path");
|
||||||
|
// Fix so that linux resources are found correctly
|
||||||
|
// since webpack will bundle them such that the expected path is /dist from cwd
|
||||||
|
process.chdir(path.resolve(path.dirname(process.execPath)));
|
||||||
|
// Now start loading the actual bundle
|
||||||
|
require("./dist");
|
3
deploy/linux/spotube/qode.json
Normal file
3
deploy/linux/spotube/qode.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"distPath": "./index.js"
|
||||||
|
}
|
49
src/app.tsx
49
src/app.tsx
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { Window, hot, View } from "@nodegui/react-nodegui";
|
import { Window, hot, View, useEventHandler } from "@nodegui/react-nodegui";
|
||||||
import { QIcon, QMainWindow, WidgetEventTypes, WindowState } from "@nodegui/nodegui";
|
import { QIcon, QKeyEvent, QMainWindow, QMainWindowSignals, WidgetEventTypes, WindowState } from "@nodegui/nodegui";
|
||||||
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";
|
||||||
@ -13,7 +13,7 @@ import { redirectURI } from "./conf";
|
|||||||
|
|
||||||
export enum CredentialKeys {
|
export enum CredentialKeys {
|
||||||
credentials = "credentials",
|
credentials = "credentials",
|
||||||
refresh_token = "refresh_token"
|
refresh_token = "refresh_token",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Credentials {
|
export interface Credentials {
|
||||||
@ -27,15 +27,52 @@ global.localStorage = new LocalStorage("./local");
|
|||||||
|
|
||||||
function RootApp() {
|
function RootApp() {
|
||||||
const windowRef = useRef<QMainWindow>();
|
const windowRef = useRef<QMainWindow>();
|
||||||
|
const [currentTrack, setCurrentTrack] = useState<CurrentTrack>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in window events: ", error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[currentTrack]
|
||||||
|
);
|
||||||
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
const [isLoggedIn, setIsLoggedIn] = useState<boolean>(false);
|
||||||
const [credentials, setCredentials] = useState<Credentials>({ clientId: "", clientSecret: "" });
|
const [credentials, setCredentials] = useState<Credentials>({ clientId: "", clientSecret: "" });
|
||||||
|
const [expires_in, setExpires_in] = useState<number>(0);
|
||||||
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 spotifyApi = new SpotifyWebApi({ redirectUri: redirectURI, ...credentials });
|
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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoggedIn(!!cachedCredentials);
|
setIsLoggedIn(!!cachedCredentials);
|
||||||
}, []);
|
}, []);
|
||||||
@ -70,9 +107,9 @@ function RootApp() {
|
|||||||
}, [isLoggedIn]);
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Window ref={windowRef} 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 }}>
|
<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={{ spotifyApi, 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 />
|
||||||
|
@ -26,6 +26,7 @@ function Player(): ReactElement {
|
|||||||
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist, spotifyApi } = useContext(playerContext);
|
const { currentTrack, currentPlaylist, setCurrentTrack, setCurrentPlaylist, spotifyApi } = useContext(playerContext);
|
||||||
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 [volume, setVolume] = useState<number>(initVolume);
|
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);
|
||||||
@ -88,11 +89,13 @@ function Player(): ReactElement {
|
|||||||
const youtubeTrack = await getYoutubeTrack(currentTrack);
|
const youtubeTrack = await getYoutubeTrack(currentTrack);
|
||||||
await audioPlayer.load(youtubeTrack.youtube_uri, "replace");
|
await audioPlayer.load(youtubeTrack.youtube_uri, "replace");
|
||||||
await audioPlayer.play();
|
await audioPlayer.play();
|
||||||
|
setIsPaused(false);
|
||||||
}
|
}
|
||||||
setIsStopped(false);
|
setIsStopped(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.errcode !== 5) {
|
if (error.errcode !== 5) {
|
||||||
setIsStopped(true);
|
setIsStopped(true);
|
||||||
|
setIsPaused(true);
|
||||||
}
|
}
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -128,17 +131,28 @@ function Player(): ReactElement {
|
|||||||
};
|
};
|
||||||
const stopListener = () => {
|
const stopListener = () => {
|
||||||
setIsStopped(true);
|
setIsStopped(true);
|
||||||
|
setIsPaused(true);
|
||||||
// go to next track
|
// go to next track
|
||||||
if (currentTrack && playlistTracksIds && currentPlaylist?.tracks.length !== 0) {
|
if (currentTrack && playlistTracksIds && currentPlaylist?.tracks.length !== 0) {
|
||||||
const index = playlistTracksIds?.indexOf(currentTrack.id) + 1;
|
const index = playlistTracksIds?.indexOf(currentTrack.id) + 1;
|
||||||
setCurrentTrack(currentPlaylist?.tracks[index > playlistTracksIds.length - 1 ? 0 : index].track);
|
setCurrentTrack(currentPlaylist?.tracks[index > playlistTracksIds.length - 1 ? 0 : index].track);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const pauseListener = () => {
|
||||||
|
setIsPaused(true);
|
||||||
|
}
|
||||||
|
const resumeListener = () => {
|
||||||
|
setIsPaused(false);
|
||||||
|
};
|
||||||
audioPlayer.on("status", statusListener);
|
audioPlayer.on("status", statusListener);
|
||||||
audioPlayer.on("stopped", stopListener);
|
audioPlayer.on("stopped", stopListener);
|
||||||
|
audioPlayer.on("paused", pauseListener)
|
||||||
|
audioPlayer.on("resumed", resumeListener)
|
||||||
return () => {
|
return () => {
|
||||||
audioPlayer.off("status", statusListener);
|
audioPlayer.off("status", statusListener);
|
||||||
audioPlayer.off("stopped", stopListener);
|
audioPlayer.off("stopped", stopListener);
|
||||||
|
audioPlayer.off("paused", pauseListener);
|
||||||
|
audioPlayer.off("resumed", resumeListener);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -148,9 +162,11 @@ function Player(): ReactElement {
|
|||||||
if ((await audioPlayer.isPaused()) && playerRunning) {
|
if ((await audioPlayer.isPaused()) && playerRunning) {
|
||||||
await audioPlayer.play();
|
await audioPlayer.play();
|
||||||
setIsStopped(false);
|
setIsStopped(false);
|
||||||
|
setIsPaused(false);
|
||||||
} else {
|
} else {
|
||||||
await audioPlayer.pause();
|
await audioPlayer.pause();
|
||||||
setIsStopped(true);
|
setIsStopped(true);
|
||||||
|
setIsPaused(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -195,7 +211,7 @@ function Player(): ReactElement {
|
|||||||
<BoxView direction={Direction.LeftToRight}>
|
<BoxView direction={Direction.LeftToRight}>
|
||||||
<IconButton style={`background-color: ${shuffle ? "orange" : "rgba(255, 255, 255, 0.055)"}`} on={{ clicked: () => setShuffle(!shuffle) }} icon={new QIcon(shuffleIcon)} />
|
<IconButton style={`background-color: ${shuffle ? "orange" : "rgba(255, 255, 255, 0.055)"}`} on={{ clicked: () => setShuffle(!shuffle) }} icon={new QIcon(shuffleIcon)} />
|
||||||
<IconButton on={{ clicked: () => prevOrNext(-1) }} icon={new QIcon(backward)} />
|
<IconButton on={{ clicked: () => prevOrNext(-1) }} icon={new QIcon(backward)} />
|
||||||
<IconButton on={{ clicked: handlePlayPause }} icon={new QIcon(isStopped || !currentTrack ? play : pause)} />
|
<IconButton on={{ clicked: handlePlayPause }} icon={new QIcon(isStopped || isPaused || !currentTrack ? play : pause)} />
|
||||||
<IconButton on={{ clicked: () => prevOrNext(1) }} icon={new QIcon(forward)} />
|
<IconButton on={{ clicked: () => prevOrNext(1) }} icon={new QIcon(forward)} />
|
||||||
<IconButton icon={new QIcon(stop)} on={{ clicked: stopPlayback }} />
|
<IconButton icon={new QIcon(stop)} on={{ clicked: stopPlayback }} />
|
||||||
</BoxView>
|
</BoxView>
|
||||||
|
@ -3,6 +3,8 @@ import React, { Dispatch, SetStateAction } from "react";
|
|||||||
export interface AuthContext {
|
export interface AuthContext {
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
setIsLoggedIn: Dispatch<SetStateAction<boolean>>;
|
setIsLoggedIn: Dispatch<SetStateAction<boolean>>;
|
||||||
|
clientId: string;
|
||||||
|
clientSecret: string;
|
||||||
access_token: string;
|
access_token: string;
|
||||||
/**
|
/**
|
||||||
* the time when the current access token will expire \
|
* the time when the current access token will expire \
|
||||||
@ -13,7 +15,7 @@ export interface AuthContext {
|
|||||||
* sets the time when the current access token will expire \
|
* sets the time when the current access token will expire \
|
||||||
* always update this with `Date.now() + expires_in`
|
* always update this with `Date.now() + expires_in`
|
||||||
*/
|
*/
|
||||||
setExpiresIn: Dispatch<SetStateAction<number>>;
|
setExpires_in: (arg: number)=>void;
|
||||||
setAccess_token: Dispatch<SetStateAction<string>>;
|
setAccess_token: Dispatch<SetStateAction<string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +24,9 @@ const authContext = React.createContext<AuthContext>({
|
|||||||
setIsLoggedIn() {},
|
setIsLoggedIn() {},
|
||||||
access_token: "",
|
access_token: "",
|
||||||
expires_in: 0,
|
expires_in: 0,
|
||||||
setExpiresIn() {},
|
clientId: "",
|
||||||
|
clientSecret: "",
|
||||||
|
setExpires_in() {},
|
||||||
setAccess_token() {},
|
setAccess_token() {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,14 +2,14 @@ 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 playerContext from "../context/playerContext";
|
||||||
|
import SpotifyWebApi from "spotify-web-api-node";
|
||||||
|
|
||||||
interface UseAuthResult {
|
interface UseAccessTokenResult {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (deps: DependencyList = []): UseAuthResult => {
|
export default (spotifyApi: SpotifyWebApi, deps: DependencyList = []): UseAccessTokenResult => {
|
||||||
const { access_token, expires_in, isLoggedIn, setExpiresIn, setAccess_token } = useContext(authContext);
|
const { access_token, expires_in, isLoggedIn, setExpires_in, setAccess_token } = useContext(authContext);
|
||||||
const { spotifyApi } = useContext(playerContext);
|
|
||||||
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
|
const refreshToken = localStorage.getItem(CredentialKeys.refresh_token);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -20,7 +20,7 @@ export default (deps: DependencyList = []): UseAuthResult => {
|
|||||||
.refreshAccessToken()
|
.refreshAccessToken()
|
||||||
.then(({ body: { access_token, expires_in } }) => {
|
.then(({ body: { access_token, expires_in } }) => {
|
||||||
setAccess_token(access_token);
|
setAccess_token(access_token);
|
||||||
setExpiresIn(Date.now() + expires_in);
|
setExpires_in(expires_in);
|
||||||
})
|
})
|
||||||
.catch();
|
.catch();
|
||||||
}
|
}
|
21
src/hooks/useSpotifyApi.ts
Normal file
21
src/hooks/useSpotifyApi.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
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]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn && clientId && clientSecret) {
|
||||||
|
spotifyApi.setClientId(clientId);
|
||||||
|
spotifyApi.setClientSecret(clientSecret);
|
||||||
|
spotifyApi.setAccessToken(access_token);
|
||||||
|
}
|
||||||
|
}, [access_token, clientId, clientSecret]);
|
||||||
|
|
||||||
|
return spotifyApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSpotifyApi;
|
6
src/initializations/spotifyApi.ts
Normal file
6
src/initializations/spotifyApi.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import SpotifyWebApi from "spotify-web-api-node";
|
||||||
|
import { redirectURI } from "../conf";
|
||||||
|
|
||||||
|
const spotifyApi = new SpotifyWebApi({ redirectUri: redirectURI });
|
||||||
|
|
||||||
|
export default spotifyApi;
|
Loading…
Reference in New Issue
Block a user