mirror of
https://github.com/zoriya/Aeris.git
synced 2026-06-05 19:46:02 +00:00
Merge pull request #102 from AnonymusRaccoon/feat/worker-spotify
Feat/worker spotify
This commit is contained in:
+16
-106
@@ -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
@@ -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" ""
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
])),
|
||||
};
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user