Merge pull request #102 from AnonymusRaccoon/feat/worker-spotify

Feat/worker spotify
This commit is contained in:
Zoe Roux
2022-03-05 15:56:03 +01:00
committed by GitHub
10 changed files with 90 additions and 167 deletions
+16 -106
View File
@@ -80,45 +80,15 @@
},
"params": [
{
"name": "artist",
"name": "trackUri",
"type": "string",
"description": {
"en": "Artist of the song to play",
"fr": "Interprète de la musique à jouer"
}
},
{
"name": "track",
"type": "string",
"description": {
"en": "Title of the song to play",
"fr": "Titre de la musique à jouer"
"en": "Url of a track",
"fr": "Url d'une musique'"
}
}
],
"returns": [
{
"name": "URL",
"description": {
"en": "URL of the song",
"fr": "URL de la musique"
}
},
{
"name": "ARTIST",
"description": {
"en": "Artist of the song",
"fr": "Interprête de la musique"
}
},
{
"name": "TRACK",
"description": {
"en": "Title of the song",
"fr": "Titre de la musique"
}
}
]
"returns": []
},
{
"name": "Spotify_Pause",
@@ -145,45 +115,15 @@
},
"params": [
{
"name": "artist",
"name": "trackUri",
"type": "string",
"description": {
"en": "Artist of the song to add",
"fr": "Interprète de la musique à ajouter"
}
},
{
"name": "track",
"type": "string",
"description": {
"en": "Title of the song to add",
"fr": "Titre de la musique à ajouter"
"en": "Url of a track",
"fr": "Url d'une musique'"
}
}
],
"returns": [
{
"name": "URL",
"description": {
"en": "URL of the added song",
"fr": "URL de la musique ajoutée"
}
},
{
"name": "ARTIST",
"description": {
"en": "Artist of the added song",
"fr": "Interprète de la musique ajoutée"
}
},
{
"name": "TRACK",
"description": {
"en": "Title of the added song",
"fr": "Titre de la musique ajoutée"
}
}
]
"returns": []
},
{
"name": "Spotify_AddToPlaylist",
@@ -197,53 +137,23 @@
},
"params": [
{
"name": "artist",
"name": "trackUri",
"type": "string",
"description": {
"en": "Artist of the song to add",
"fr": "Interprète de la musique à ajouter"
"en": "Url of a track",
"fr": "Url d'une musique'"
}
},
{
"name": "track",
"name": "playlistUri",
"type": "string",
"description": {
"en": "Title of the song to add",
"fr": "Titre de la musique à ajouter"
}
},
{
"name": "playlist",
"type": "string",
"description": {
"en": "Name of the playlist",
"fr": "Nom de la playlist dans laquelle ajouter la musique"
"en": "Id of the playlist",
"fr": "Id de la playlist dans laquelle ajouter la musique"
}
}
],
"returns": [
{
"name": "URL",
"description": {
"en": "URL of the added song",
"fr": "URL de la musique ajoutée"
}
},
{
"name": "ARTIST",
"description": {
"en": "Artist of the added song",
"fr": "Interprète de la musique ajoutée"
}
},
{
"name": "TRACK",
"description": {
"en": "Title of the added song",
"fr": "Titre de la musique ajoutée"
}
}
]
"returns": []
}
]
}
}
+2 -1
View File
@@ -62,7 +62,8 @@ urlHandler Spotify (Just r) = do
clientId <- liftIO $ envAsString "SPOTIFY_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_URL" ""
throwError $ err302 { errHeaders =
[("Location", B8.pack $ "https://accounts.spotify.com/authorize?response_type=code&scope=user-library-read&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
[("Location", B8.pack $ "https://accounts.spotify.com/authorize?response_type=code&scope=user-library-read user-library-modify streaming playlist-modify-private playlist-read-collaborative playlist-read-private playlist-modify-public user-modify-playback-state user-read-private&client_id=" ++ clientId ++ "&redirect_uri=" ++ backRedirect ++ "auth/redirect" ++ "&state=" ++ r)] }
urlHandler Github (Just r) = do
clientId <- liftIO $ envAsString "GITHUB_CLIENT_ID" ""
backRedirect <- liftIO $ envAsString "BACK_URL" ""
+2 -1
View File
@@ -30,6 +30,7 @@ import Repository.User (updateTokens, getTokensByUserId, delTokens)
import GHC.Generics (Generic)
import Data.Int (Int64)
import Data.Text (Text)
import Data.Time (UTCTime)
data WorkerUserData = WorkerUserData
@@ -47,7 +48,7 @@ newtype ErrorBody = ErrorBody { error :: Text }
data RefreshBody = RefreshBody
{ accessToken :: Text
, refreshToken :: Text
, expiresIn :: Int64
, expiresAt :: UTCTime
}
$(deriveJSON defaultOptions ''WorkerUserData)
+25 -8
View File
@@ -6,13 +6,14 @@ import qualified Data.ByteString.Char8 as B8
import qualified Data.HashMap.Strict as HM
import App (AppM)
import Core.User (ExternalToken (ExternalToken), Service (Github, Discord, Spotify, Google, Twitter, Anilist))
import Core.User (ExternalToken (ExternalToken, expiresAt), Service (Github, Discord, Spotify, Google, Twitter, Anilist))
import Data.Aeson.Types (Object, Value (String))
import Data.Text (Text, pack, unpack)
import Network.HTTP.Simple (JSONException, addRequestHeader, getResponseBody, httpJSONEither, parseRequest, setRequestMethod, setRequestQueryString, setRequestBodyURLEncoded)
import System.Environment.MrEnv (envAsBool, envAsInt, envAsInteger, envAsString)
import Utils (lookupObjString)
import Utils (lookupObjString, lookupObjInt)
import Data.ByteString.Base64
import Data.Time (getCurrentTime, addUTCTime)
data OAuth2Conf = OAuth2Conf
{ oauthClientId :: String
, oauthClientSecret :: String
@@ -55,11 +56,12 @@ getGithubTokens code = do
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
return $ case (getResponseBody response :: Either JSONException Object) of
Left _ -> Nothing
Right obj -> do
access <- lookupObjString obj "access_token"
Just $ ExternalToken (pack access) "" 0 Github
Just $ ExternalToken (pack access) "" currTime Github
-- DISCORD
getDiscordConfig :: IO OAuth2Conf
@@ -86,12 +88,15 @@ getDiscordTokens code = do
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
return $ case (getResponseBody response :: Either JSONException Object) of
Left _ -> Nothing
Right obj -> do
access <- lookupObjString obj "access_token"
refresh <- lookupObjString obj "refresh_token"
Just $ ExternalToken (pack access) (pack refresh) 0 Discord
expiresIn <- lookupObjInt obj "expires_in"
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
Just $ ExternalToken (pack access) (pack refresh) expiresAt Discord
-- GOOGLE
getGoogleConfig :: IO OAuth2Conf
@@ -118,12 +123,15 @@ getGoogleTokens code = do
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
return $ case (getResponseBody response :: Either JSONException Object) of
Left _ -> Nothing
Right obj -> do
access <- lookupObjString obj "access_token"
refresh <- lookupObjString obj "refresh_token"
Just $ ExternalToken (pack access) (pack refresh) 0 Google
expiresIn <- lookupObjInt obj "expires_in"
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
Just $ ExternalToken (pack access) (pack refresh) expiresAt Google
-- SPOTIFY
getSpotifyConfig :: IO OAuth2Conf
@@ -151,12 +159,15 @@ getSpotifyTokens code = do
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
return $ case (getResponseBody response :: Either JSONException Object) of
Left _ -> Nothing
Right obj -> do
access <- lookupObjString obj "access_token"
refresh <- lookupObjString obj "refresh_token"
Just $ ExternalToken (pack access) (pack refresh) 0 Spotify
expiresIn <- lookupObjInt obj "expires_in"
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
Just $ ExternalToken (pack access) (pack refresh) expiresAt Spotify
-- TWITTER
getTwitterConfig :: IO OAuth2Conf
@@ -184,12 +195,15 @@ getTwitterTokens code = do
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
return $ case (getResponseBody response :: Either JSONException Object) of
Left _ -> Nothing
Right obj -> do
access <- lookupObjString obj "access_token"
refresh <- lookupObjString obj "refresh_token"
Just $ ExternalToken (pack access) (pack refresh) 0 Twitter
expiresIn <- lookupObjInt obj "expires_in"
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
Just $ ExternalToken (pack access) (pack refresh) expiresAt Twitter
-- ANILIST
getAnilistConfig :: IO OAuth2Conf
@@ -216,12 +230,15 @@ getAnilistTokens code = do
]
request'
response <- httpJSONEither request
currTime <- getCurrentTime
return $ case (getResponseBody response :: Either JSONException Object) of
Left _ -> Nothing
Right obj -> do
access <- lookupObjString obj "access_token"
refresh <- lookupObjString obj "refresh_token"
Just $ ExternalToken (pack access) (pack refresh) 0 Anilist
expiresIn <- lookupObjInt obj "expires_in"
let expiresAt = addUTCTime (fromInteger . fromIntegral $ expiresIn) currTime
Just $ ExternalToken (pack access) (pack refresh) expiresAt Anilist
+2 -1
View File
@@ -20,6 +20,7 @@ import Servant (AuthProtect, FromHttpApiData)
import Servant.API (FromHttpApiData (parseUrlPiece))
import Servant.Server.Experimental.Auth (AuthServerData)
import Servant.Auth.JWT (ToJWT, FromJWT)
import Data.Time (UTCTime)
newtype UserId = UserId {toInt64 :: Int64}
deriving newtype (DBEq, DBType, Eq, Show, Num, FromJSON, ToJSON, FromHttpApiData)
@@ -41,7 +42,7 @@ instance FromHttpApiData Service where
data ExternalToken = ExternalToken
{ accessToken :: Text
, refreshToken :: Text
, expiresIn :: Int64
, expiresAt :: UTCTime
, service :: Service
}
deriving (Eq, Show, Generic)
+1 -1
View File
@@ -33,7 +33,7 @@ lookupObjString obj key = case Data.HashMap.Strict.lookup key obj of
lookupObjInt :: Object -> Text -> Maybe Int64
lookupObjInt obj key = case Data.HashMap.Strict.lookup key obj of
Just (Number x) -> toBoundedInteger $ x
Just (Number x) -> toBoundedInteger x
_ -> Nothing
uncurry3 :: (a -> b -> c -> d) -> (a, b, c) -> d
+2 -2
View File
@@ -96,7 +96,7 @@ export class Pipeline {
export class Token {
accessToken: string;
refreshToken: string;
expiresIn: string;
expiresAt: string;
};
export class Reaction {
@@ -132,7 +132,7 @@ export const pipelineFromApi = (data: any): Pipeline => {
{
accessToken: x.accessToken,
refreshToken: x.refreshToken,
expiresIn: x.expiresIn
expiresAt: x.expiresAt
} as Token
])),
};
+1 -1
View File
@@ -27,7 +27,7 @@ export class Runner {
for (let [key, value] of Object.entries(params)) {
let newValue = value;
if (typeof value == "string") {
newValue = value.replace(/{(\w*)(?:@(\d))?}/, (_, name, index) => {
newValue = value.replace(/{(\w*)(?:@(\d))?}/g, (_, name, index) => {
if (index)
return this._history[parseInt(index)][name]
return this._history[this._history.length - 1][name]
+26 -43
View File
@@ -7,12 +7,14 @@ import { Pipeline, PipelineEnv, PipelineType, ReactionType, ServiceType } from "
@service(ServiceType.Spotify)
export class Spotify extends BaseService {
private _pipeline: Pipeline;
private _spotify;
constructor(pipeline: Pipeline) {
super();
if (!("Spotify" in pipeline.userData))
throw new Error("User is not authenticated via Spotify");
this._pipeline = pipeline;
this._spotify = new SpotifyWebApi({
accessToken: pipeline.userData["Spotify"].accessToken,
refreshToken: pipeline.userData["Spotify"].refreshToken,
@@ -22,7 +24,20 @@ export class Spotify extends BaseService {
}
private async _refreshIfNeeded(): Promise<void> {
if (Date.parse(this._pipeline.userData["Spotify"].expiresAt) >= Date.now() + 100_000)
return;
const ret = await this._spotify.refreshAccessToken();
fetch(`${process.env["WORKER_API_URL"]}/spotify/${this._pipeline.userId}?WORKER_API_KEY=${process.env["WORKER_API_KEY"]}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
accessToken: ret.body.access_token,
refreshToken: ret.body.refresh_token,
expiresAt: new Date(Date.now() + ret.body.expires_in),
}),
});
}
@action(PipelineType.OnSpotifyAddToPlaylist, ["playlistId"])
@@ -53,64 +68,32 @@ export class Spotify extends BaseService {
});
}
private async _searchTrack(artistName: string, trackName: string) {
await this._refreshIfNeeded();
let searchResult = await this._spotify.searchTracks(`artist=${artistName}&track=${trackName}`);
if (searchResult.body.tracks.total == 0)
throw new Error(`Spotify API: '${trackName}' by ${artistName}: no such track`);
return searchResult.body.tracks.items[0];
}
private async _searchPlaylist(playlistName: string) {
await this._refreshIfNeeded();
let searchResult = await this._spotify.searchPlaylists(`name=${playlistName}&type=playlist`);
if (searchResult.body.playlists.total == 0)
throw new Error(`Spotify API: '${playlistName}': no such playlist`);
return searchResult.body.playlists.items[0];
}
@reaction(ReactionType.PlayTrack, ['artist', 'track'])
@reaction(ReactionType.PlayTrack, ['trackUri'])
async playTrack(params: any): Promise<PipelineEnv> {
await this._refreshIfNeeded();
let track = await this._searchTrack(params['artist'], params['track']);
await this._spotify.play({uris: [track.uri]});
return {
URL: track.uri,
ARTIST: track.artists?.[0].name,
TRACK: track.name,
};
await this._spotify.play({uris: [params.trackUri]});
return {};
}
@reaction(ReactionType.Pause, [])
async pause(params: any): Promise<PipelineEnv> {
async pause(_: any): Promise<PipelineEnv> {
await this._refreshIfNeeded();
await this._spotify.pause();
return {};
}
@reaction(ReactionType.AddTrackToLibrary, ['artist', 'track'])
@reaction(ReactionType.AddTrackToLibrary, ['trackUri'])
async addTrackToLibrary(params: any): Promise<PipelineEnv> {
await this._refreshIfNeeded();
let track = await this._searchTrack(params['artist'], params['track']);
await this._spotify.addToMySavedTracks([track.id]);
return {
URL: track.uri,
ARTIST: track.artists?.[0].name,
TRACK: track.name,
};
await this._spotify.addToMySavedTracks([params.trackUri]);
return {};
}
@reaction(ReactionType.AddToPlaylist, ['artist', 'track', 'playlist'])
@reaction(ReactionType.AddToPlaylist, ['trackUri', 'playlistUri'])
async addToPlaylist(params: any): Promise<PipelineEnv> {
await this._refreshIfNeeded();
let playlist = await this._searchPlaylist( params['playlist']);
let track = await this._searchTrack(params['artist'], params['track']);
await this._spotify.addTracksToPlaylist(playlist.id, [track.uri]);
return {
URL: track.uri,
ARTIST: track.artists?.[0].name,
TRACK: track.name,
};
await this._spotify.addTracksToPlaylist(params.playlistUri, ["spotify:track:" + params.trackUri]);
return {};
}
}
+13 -3
View File
@@ -22,9 +22,19 @@ export class Youtube extends BaseService {
refresh_token: pipeline.userData["Google"].refreshToken,
access_token: pipeline.userData["Google"].accessToken,
});
// client.on("tokens", x => {
//
// });
client.on("tokens", x => {
fetch(`${process.env["WORKER_API_URL"]}/google/${pipeline.userId}?WORKER_API_KEY=${process.env["WORKER_API_KEY"]}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
accessToken: x.access_token,
refreshToken: x.refresh_token,
expiresAt: x.expiry_date,
}),
});
});
this._youtube = new youtube_v3.Youtube({
auth: client,
});