Skip to content

Commit

Permalink
Remove Sonos support (not needed anymore)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz committed Apr 21, 2021
1 parent f28b742 commit 100f123
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 127 deletions.
1 change: 0 additions & 1 deletion src/main/scala/com/kubukoz/next/ConfigLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import java.nio.file.NoSuchFileException
import com.kubukoz.next.util.ConsoleRead
import cats.effect._
import cats.implicits._
import cats.Show
import cats.Applicative
import cats.effect.std.Console
import fs2.io.file.Files
Expand Down
35 changes: 35 additions & 0 deletions src/main/scala/com/kubukoz/next/LoginProcess.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.kubukoz.next

import cats.implicits._
import com.kubukoz.next.util.Config
import cats.Monad

trait LoginProcess[F[_]] {
def login: F[Unit]
def refreshUserToken(refreshToken: Config.RefreshToken): F[Unit]
}

object LoginProcess {
def apply[F[_]](implicit F: LoginProcess[F]): LoginProcess[F] = F

def instance[F[_]: UserOutput: Login: ConfigLoader: Monad]: LoginProcess[F] = new LoginProcess[F] {

def login: F[Unit] = for {
tokens <- Login[F].server
config <- ConfigLoader[F].loadConfig
newConfig = config.copy(token = tokens.access.some, refreshToken = tokens.refresh.some)
_ <- ConfigLoader[F].saveConfig(newConfig)
_ <- UserOutput[F].print(UserMessage.SavedToken)
} yield ()

def refreshUserToken(refreshToken: Config.RefreshToken): F[Unit] = for {
newToken <- Login[F].refreshToken(refreshToken)
config <- ConfigLoader[F].loadConfig
newConfig = config.copy(token = newToken.some)
_ <- ConfigLoader[F].saveConfig(newConfig)
_ <- UserOutput[F].print(UserMessage.RefreshedToken)
} yield ()

}

}
23 changes: 8 additions & 15 deletions src/main/scala/com/kubukoz/next/Main.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.kubukoz.next

import cats.Monad
import cats.data.NonEmptyList
import cats.effect.ExitCode
import cats.effect.IO
Expand Down Expand Up @@ -47,35 +46,29 @@ object Main extends CommandIOApp(name = "spotify-next", header = "spotify-next:

import Program._

def runApp[F[_]: Spotify: ConfigLoader: Login: UserOutput: Monad]: Choice => F[Unit] = {
case Choice.Login => loginUser[F]
case Choice.SkipTrack => Spotify[F].skipTrack
case Choice.DropTrack => Spotify[F].dropTrack
case Choice.FastForward(p) => Spotify[F].fastForward(p)
}

def makeProgram[F[_]: Async: Console]: Resource[F, Choice => F[Unit]] = {
def makeProgram[F[_]: Async: Console]: Resource[F, Runner[F]] = {
implicit val userOutput: UserOutput[F] = UserOutput.toConsole

Resource
.eval(makeLoader[F])
.flatMap { implicit loader =>
implicit val configAsk: Config.Ask[F] = loader.configAsk

makeBasicClient[F].evalMap { rawClient =>
makeBasicClient[F].map { rawClient =>
implicit val login: Login[F] = Login.blaze[F](rawClient)
implicit val loginProcess: LoginProcess[F] = LoginProcess.instance

implicit val spotify = makeSpotify(apiClient[F].apply(rawClient))

makeSpotify(apiClient[F].apply(rawClient)).map { implicit spotify =>
runApp[F]
}
Runner.instance[F]
}
}
}

val mainOpts: Opts[IO[Unit]] = Choice
.opts
.map { choice =>
makeProgram[IO].use(_.apply(choice))
makeProgram[IO].use(_.run(choice))
}

val runRepl: IO[Unit] = {
Expand All @@ -99,7 +92,7 @@ object Main extends CommandIOApp(name = "spotify-next", header = "spotify-next:
.Stream
.resource(makeProgram[IO])
.evalTap(_ => IO.println("Welcome to the spotify-next REPL! Type in a command to begin"))
.map(Command("", "")(Choice.opts).map(_))
.map(runner => Command("", "")(Choice.opts).map(runner.run))
.flatMap { command =>
input
.map(command.parse(_, sys.env).leftMap(_.toString))
Expand Down
39 changes: 6 additions & 33 deletions src/main/scala/com/kubukoz/next/Program.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.kubukoz.next

import cats.Monad
import cats.MonadError
import cats.effect.Concurrent
import cats.effect.MonadCancelThrow
import cats.effect.Resource
Expand All @@ -10,7 +8,6 @@ import cats.effect.kernel.Ref
import cats.effect.std.Console
import cats.implicits._
import com.kubukoz.next.util.Config
import com.kubukoz.next.util.Config.RefreshToken
import com.kubukoz.next.util.Config.Token
import com.kubukoz.next.util.middlewares
import fs2.io.file.Files
Expand Down Expand Up @@ -43,7 +40,7 @@ object Program {
.map(RequestLogger(logHeaders = true, logBody = true))
.map(ResponseLogger(logHeaders = true, logBody = true))

def apiClient[F[_]: UserOutput: Console: ConfigLoader: Login: MonadCancelThrow](
def apiClient[F[_]: Console: ConfigLoader: LoginProcess: MonadCancelThrow](
implicit SC: fs2.Compiler[F, F]
): Client[F] => Client[F] = {
implicit val configAsk: Config.Ask[F] = ConfigLoader[F].configAsk
Expand All @@ -54,8 +51,8 @@ object Program {
.ask[F]
.map(_.refreshToken)
.flatMap {
case None => loginUser
case Some(refreshToken) => refreshUserToken(refreshToken)
case None => LoginProcess[F].login
case Some(refreshToken) => LoginProcess[F].refreshUserToken(refreshToken)
}

middlewares
Expand All @@ -64,35 +61,11 @@ object Program {
.compose(middlewares.withToken[F])
}

// Do NOT move this into Spotify, it'll vastly increase the range of its responsibilities!
def loginUser[F[_]: UserOutput: Login: ConfigLoader: Monad]: F[Unit] =
for {
tokens <- Login[F].server
config <- ConfigLoader[F].loadConfig
newConfig = config.copy(token = tokens.access.some, refreshToken = tokens.refresh.some)
_ <- ConfigLoader[F].saveConfig(newConfig)
_ <- UserOutput[F].print(UserMessage.SavedToken)
} yield ()

def refreshUserToken[F[_]: UserOutput: Login: ConfigLoader: MonadError[*[_], Throwable]](
refreshToken: RefreshToken
): F[Unit] =
for {
newToken <- Login[F].refreshToken(refreshToken)
config <- ConfigLoader[F].loadConfig
newConfig = config.copy(token = newToken.some)
_ <- ConfigLoader[F].saveConfig(newConfig)
_ <- UserOutput[F].print(UserMessage.RefreshedToken)
} yield ()

def makeSpotify[F[_]: UserOutput: Concurrent](client: Client[F]): F[Spotify[F]] = {
def makeSpotify[F[_]: UserOutput: Concurrent](client: Client[F]): Spotify[F] = {
implicit val theClient = client
implicit val playback = Spotify.Playback.spotify[F](client)

import org.http4s.syntax.all._

Spotify.Playback.build[F](uri"http://localhost:5005", client).map { implicit playback =>
Spotify.instance[F]
}
Spotify.instance[F]
}

}
17 changes: 17 additions & 0 deletions src/main/scala/com/kubukoz/next/Runner.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.kubukoz.next

trait Runner[F[_]] {
def run(choice: Choice): F[Unit]
}

object Runner {
def apply[F[_]](implicit F: Runner[F]): Runner[F] = F

def instance[F[_]: Spotify: LoginProcess]: Runner[F] = {
case Choice.Login => LoginProcess[F].login
case Choice.SkipTrack => Spotify[F].skipTrack
case Choice.DropTrack => Spotify[F].dropTrack
case Choice.FastForward(p) => Spotify[F].fastForward(p)
}

}
36 changes: 0 additions & 36 deletions src/main/scala/com/kubukoz/next/Spotify.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package com.kubukoz.next

import java.time.Duration

import cats.data.Kleisli
import cats.effect.Concurrent
import cats.implicits._
import com.kubukoz.next.api.sonos
import com.kubukoz.next.api.spotify.Item
import com.kubukoz.next.api.spotify.Player
import com.kubukoz.next.api.spotify.PlayerContext
Expand All @@ -15,7 +12,6 @@ import org.http4s.Method.POST
import org.http4s.Method.PUT
import org.http4s.Request
import org.http4s.Status
import org.http4s.Uri
import org.http4s.circe.CirceEntityCodec._
import org.http4s.client.Client

Expand Down Expand Up @@ -110,38 +106,6 @@ object Spotify {

}

def localSonos[F[_]: Concurrent](baseUrl: Uri, room: String, client: Client[F]): Playback[F] = new Playback[F] {
val nextTrack: F[Unit] =
client.expect[api.spotify.Anything](baseUrl / room / "next").void

def seek(ms: Int): F[Unit] = {
val seconds = Duration.ofMillis(ms.toLong).toSeconds().toString

client.expect[api.spotify.Anything](baseUrl / room / "timeseek" / seconds).void
}

}

def build[F[_]: UserOutput: Concurrent](sonosBaseUrl: Uri, client: Client[F]): F[Playback[F]] =
UserOutput[F].print(UserMessage.CheckingSonos(sonosBaseUrl)) *>
client
.get(sonosBaseUrl / "zones") {
case response if response.status.isSuccess => response.as[sonos.SonosZones].map(_.some)
case _ => none[sonos.SonosZones].pure[F]
}
.handleError(_ => None)
.flatMap {
case None =>
UserOutput[F].print(UserMessage.SonosNotFound).as(spotify(client))

case Some(zones) =>
val roomName = zones.zones.head.coordinator.roomName

UserOutput[F]
.print(UserMessage.SonosFound(zones, roomName))
.as(localSonos(sonosBaseUrl, roomName, client))
}

}

private object methods {
Expand Down
9 changes: 0 additions & 9 deletions src/main/scala/com/kubukoz/next/UserMessage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import cats.Show
import cats.effect.std
import cats.implicits._
import cats.~>
import com.kubukoz.next.api.sonos.SonosZones
import com.kubukoz.next.api.spotify.Item
import com.kubukoz.next.api.spotify.Player
import com.kubukoz.next.api.spotify.PlayerContext
Expand All @@ -26,11 +25,6 @@ object UserMessage {
final case class RemovingCurrentTrack(player: Player[PlayerContext.playlist, Item.track]) extends UserMessage
case object TooCloseToEnd extends UserMessage
final case class Seeking(desiredProgressPercent: Int) extends UserMessage

// setup
final case class CheckingSonos(url: Uri) extends UserMessage
case object SonosNotFound extends UserMessage
final case class SonosFound(zones: SonosZones, roomName: String) extends UserMessage
}

trait UserOutput[F[_]] {
Expand All @@ -57,9 +51,6 @@ object UserOutput {
show"""Removing track "${player.item.name}" (${player.item.uri}) from playlist ${player.context.uri.playlist}"""
case TooCloseToEnd => "Too close to song's ending, rewinding to beginning"
case Seeking(desiredProgressPercent) => show"Seeking to $desiredProgressPercent%"
case CheckingSonos(url) => show"Checking if Sonos API is available at $url..."
case SonosNotFound => "Sonos not found, will access Spotify API directly"
case SonosFound(zones, roomName) => show"Found ${zones.zones.size} zone(s), will use room $roomName"
}

msg => std.Console[F].println(stringify(msg))
Expand Down
33 changes: 0 additions & 33 deletions src/main/scala/com/kubukoz/next/api/sonos.scala

This file was deleted.

0 comments on commit 100f123

Please sign in to comment.