@ -2,7 +2,7 @@ package com.github.mgifos.workouts
import java.time.LocalDate
import akka.actor. { Actor , ActorRef , ActorSystem , Props }
import akka.actor. { Actor , ActorRef , ActorSystem , Props }
import akka.http.scaladsl.Http
import akka.http.scaladsl.client.RequestBuilding._
import akka.http.scaladsl.model.ContentTypes._
@ -12,17 +12,17 @@ import akka.http.scaladsl.model.Uri.Query
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.headers._
import akka.pattern.ask
import akka.stream.scaladsl. { Flow , Sink , Source }
import akka.stream. { Materializer , ThrottleMode }
import akka.stream.scaladsl. { Flow , Sink , Source }
import akka.stream. { Materializer , ThrottleMode }
import akka.util.Timeout
import com.github.mgifos.workouts.GarminConnect._
import com.github.mgifos.workouts.model.WorkoutDef
import com.typesafe.scalalogging.Logger
import play.api.libs.json. { JsObject , Json }
import play.api.libs.json. { JsObject , Json }
import scala.collection.immutable. { Map , Seq }
import scala.collection.immutable. { Map , Seq }
import scala.concurrent.duration._
import scala.concurrent. { ExecutionContext , Future }
import scala.concurrent. { ExecutionContext , Future }
import scala.language.implicitConversions
import scala.util.Failure
@ -37,47 +37,46 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
private val log = Logger ( getClass )
/* *
* Creates workout definitions
*
* @param workouts
* @return
*/
* Creates workout definitions
*
* @param workouts
* @return
*/
def createWorkouts ( workouts : Seq [ WorkoutDef ] ) ( implicit session : GarminSession ) : Future [ Seq [ GarminWorkout ] ] = {
log . info ( "\nCreating workouts:" )
val source = Source ( workouts . map { workout =>
val req = Post ( "https://connect.garmin.com/modern/proxy/workout-service/workout" )
. withEntity ( HttpEntity ( `application/json` , workout . json . toString ( ) ) )
. withHeaders ( session . headers
: + Referer ( " https :// connect . garmin . com / modern / workout / create / running " )
: + RawHeader ( " NK " , " NT " ) )
. withHeaders (
session . headers
: + Referer ( " https :// connect . garmin . com / modern / workout / create / running " )
: + RawHeader ( " NK " , " NT " ) )
workout . name -> req
} )
val flow = Flow [ ( String , HttpRequest ) ]
. throttle ( 1 , 1. second , 1 , ThrottleMode . shaping )
. mapAsync ( 1 ) {
case ( workout , req ) =>
Http ( ) . singleRequest ( req ) . flatMap { res =>
if ( res . status == OK ) {
res . body . map { json =>
log . info ( s" $workout " )
GarminWorkout ( workout , Json . parse ( json ) . \ ( "workoutId" ) . as [ Long ] )
}
} else {
log . debug ( s" Creation wo response: $res " )
Future . failed ( new Error ( "Cannot create workout" ) )
val flow = Flow [ ( String , HttpRequest ) ] . throttle ( 1 , 1. second , 1 , ThrottleMode . shaping ) . mapAsync ( 1 ) {
case ( workout , req ) =>
Http ( ) . singleRequest ( req ) . flatMap { res =>
if ( res . status == OK ) {
res . body . map { json =>
log . info ( s" $workout " )
GarminWorkout ( workout , Json . parse ( json ) . \ ( "workoutId" ) . as [ Long ] )
}
} else {
log . debug ( s" Creation wo response: $res " )
Future . failed ( new Error ( "Cannot create workout" ) )
}
}
}
}
source . via ( flow ) . runWith ( Sink . seq )
}
/* *
* Deletes workouts with provided names
*
* @param workouts Workout names
* @return Count of deleted items
*/
* Deletes workouts with provided names
*
* @param workouts Workout names
* @return Count of deleted items
*/
def deleteWorkouts ( workouts : Seq [ String ] ) ( implicit session : GarminSession ) : Future [ Int ] = {
val futureRequests = getWorkoutsMap ( ) . map { wsMap =>
@ -88,23 +87,23 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
if ids . nonEmpty
} yield {
val label = s" $workout -> ${ ids . mkString ( "[" , ", " , "]" ) } "
label -> ids . map ( id => Post ( s" https://connect.garmin.com/modern/proxy/workout-service/workout/ $id " )
. withHeaders ( session . headers
: + Referer ( " https :// connect . garmin . com / modern / workouts " )
: + RawHeader ( " NK " , " NT " )
: + RawHeader ( " X - HTTP - Method - Override " , " DELETE " ) ) )
label -> ids . map (
id =>
Post ( s" https://connect.garmin.com/modern/proxy/workout-service/workout/ $id " ) . withHeaders (
session . headers
: + Referer ( " https :// connect . garmin . com / modern / workouts " )
: + RawHeader ( " NK " , " NT " )
: + RawHeader ( " X - HTTP - Method - Override " , " DELETE " ) ) )
}
}
val source = Source . fromFuture ( futureRequests ) . flatMapConcat ( Source ( _ ) )
. throttle ( 1 , 1. second , 1 , ThrottleMode . shaping )
. mapAsync ( 1 ) {
case ( label , reqs ) =>
val statusesFut = Future . sequence ( reqs . map ( req => Http ( ) . singleRequest ( req ) . withoutBody ) )
statusesFut . map { statuses =>
if ( statuses . forall ( _ . status == NoContent ) ) log . info ( s" $label " )
else log . error ( s" Cannot delete workout: $label " )
}
}
val source = Source . fromFuture ( futureRequests ) . flatMapConcat ( Source ( _ ) ) . throttle ( 1 , 1. second , 1 , ThrottleMode . shaping ) . mapAsync ( 1 ) {
case ( label , reqs ) =>
val statusesFut = Future . sequence ( reqs . map ( req => Http ( ) . singleRequest ( req ) . withoutBody ) )
statusesFut . map { statuses =>
if ( statuses . forall ( _ . status == NoContent ) ) log . info ( s" $label " )
else log . error ( s" Cannot delete workout: $label " )
}
}
source . runWith ( Sink . seq ) . map ( _ . length )
}
@ -127,34 +126,38 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
else log . error ( s" Cannot schedule: $label " )
}
}
. runWith ( Sink . seq ) . map ( _ . length )
. runWith ( Sink . seq )
. map ( _ . length )
}
/* *
* Retrieves workout mapping : name -> Seq [ id ] @ GarminConnect
* @return
*/
* Retrieves workout mapping : name -> Seq [ id ] @ GarminConnect
* @return
*/
private def getWorkoutsMap ( ) ( implicit session : GarminSession ) : Future [ Map [ String , Seq [ Long ] ] ] = {
val req = Get ( "https://connect.garmin.com/modern/proxy/workout-service/workouts?start=1&limit=9999&myWorkoutsOnly=true&sharedWorkoutsOnly=false" )
. withHeaders ( session . headers
: + Referer ( " https :// connect . garmin . com / modern / workouts " )
: + RawHeader ( " NK " , " NT " ) )
val source = Source . fromFuture (
Http ( ) . singleRequest ( req ) . flatMap { res =>
if ( res . status == OK )
res . body . map { json =>
Json . parse ( json ) . asOpt [ Seq [ JsObject ] ] . map { arr =>
. withHeaders (
session . headers
: + Referer ( " https :// connect . garmin . com / modern / workouts " )
: + RawHeader ( " NK " , " NT " ) )
val source = Source . fromFuture ( Http ( ) . singleRequest ( req ) . flatMap { res =>
if ( res . status == OK )
res . body . map { json =>
Json
. parse ( json )
. asOpt [ Seq [ JsObject ] ]
. map { arr =>
arr . map ( x => ( x \ "workoutName" ) . as [ String ] -> ( x \ "workoutId" ) . as [ Long ] )
} . getOrElse ( Seq . empty )
. groupBy { case ( name , _ ) => name }
. map { case ( a , b ) => a -> b . map ( _ . _2 ) }
}
else {
log . debug ( s" Cannot retrieve workout list, response: $res " )
Future . failed ( new Error ( "Cannot retrieve workout list from Garmin Connect" ) )
}
} )
}
. getOrElse ( Seq . empty )
. groupBy { case ( name , _ ) => name }
. map { case ( a , b ) => a -> b . map ( _ . _2 ) }
} else {
log . debug ( s" Cannot retrieve workout list, response: $res " )
Future . failed ( new Error ( "Cannot retrieve workout list from Garmin Connect" ) )
}
} )
source . runWith ( Sink . head )
}
@ -164,8 +167,8 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
ask ( loginActor , Login ( forceNewSession ) ) ( Timeout ( 2. minutes ) ) . mapTo [ Either [ String , GarminSession ] ]
/* *
* Holds and reloads session if neccessary
*/
* Holds and reloads session if neccessary
*/
class LoginActor extends Actor {
private case class NewSession ( session : GarminSession )
@ -246,19 +249,19 @@ class GarminConnect(email: String, password: String)(implicit system: ActorSyste
// "rememberMeChecked" -> "false" ,
// "rememberMeShown" -> "true" ,
"service" -> "https://connect.garmin.com/modern"
// "source" -> "https://connect.garmin.com/en-US/signin" ,
// "webhost" -> "https://connect.garmin.com"
// "source" -> "https://connect.garmin.com/en-US/signin" ,
// "webhost" -> "https://connect.garmin.com"
)
for {
res1 <- Http ( ) . singleRequest ( HttpRequest ( uri = Uri ( "https://sso.garmin.com/sso/login" ) . withQuery ( Query ( params ) ) ) ) . withoutBody
res2 <- Http ( ) . singleRequest (
Http Request(
POST ,
Uri ( "https://sso.garmin.com/sso/login" ) . withQuery ( Query ( params ) ) ,
entity = FormData ( Map (
"username" -> email ,
"password" -> password ,
"embed" -> "false" ) ) . toEntity ) . withHeaders ( extractCookies ( res1 ) ) ) . withoutBody
res2 <- Http ( )
. single Request(
HttpRequest (
POST ,
Uri ( "https://sso.garmin.com/sso/login" ) . withQuery ( Query ( params ) ) ,
entity = FormData ( Map ( "username" -> email , "password" -> password , "embed" -> "false" ) ) . toEntity
) . withHeaders ( extractCookies ( res1 ) ) )
. withoutBody
sessionCookies <- redirectionLoop ( 0 , "https://connect.garmin.com/modern" , extractCookies ( res2 ) )
} yield GarminSession ( sessionCookies )
}