Skip to content

Commit

Permalink
Show message when jumping (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz authored Sep 2, 2021
1 parent 4348b3c commit f6356a9
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 41 deletions.
3 changes: 2 additions & 1 deletion src/main/scala/com/kubukoz/next/Program.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ object Program {
}

def makeSpotify[F[_]: UserOutput: Concurrent](client: Client[F]): F[Spotify[F]] = {
given Spotify.DeviceInfo[F] = Spotify.DeviceInfo.instance(client)
given Client[F] = client
given Spotify.DeviceInfo[F] = Spotify.DeviceInfo.instance
given Spotify.SonosInfo[F] = Spotify.SonosInfo.instance(sonos.baseUri, client)

SpotifyChoice
Expand Down
73 changes: 38 additions & 35 deletions src/main/scala/com/kubukoz/next/Spotify.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.kubukoz.next

import cats.FlatMap
import cats.data.Kleisli
import cats.data.OptionT
import cats.effect.Concurrent
import cats.effect.Ref
Expand All @@ -22,6 +21,7 @@ import org.http4s.Uri
import org.http4s.circe.CirceEntityCodec.*
import org.http4s.client.Client
import scala.concurrent.duration.*
import scala.util.chaining.*

trait Spotify[F[_]] {
def skipTrack: F[Unit]
Expand Down Expand Up @@ -66,19 +66,18 @@ object Spotify {
Playback[F].nextTrack

val dropTrack: F[Unit] =
methods.player[F].run(client).flatMap(requirePlaylist(_)).flatMap(requireTrack).flatMap { player =>
methods.player[F].flatMap(requirePlaylist(_)).flatMap(requireTrack).flatMap { player =>
val trackUri = player.item.uri
val playlistId = player.context.uri.playlist

UserOutput[F].print(UserMessage.RemovingCurrentTrack(player)) *>
skipTrack *>
methods.removeTrack[F](trackUri, playlistId).run(client)
methods.removeTrack[F](trackUri, playlistId)
}

def fastForward(percentage: Int): F[Unit] =
methods
.player[F]
.run(client)
.flatMap(requireTrack)
.fproduct { player =>
val currentLength = player.progress_ms
Expand All @@ -98,25 +97,35 @@ object Spotify {

def jumpSection: F[Unit] = methods
.player[F]
.flatMapF(requireTrack)
.flatMap(requireTrack)
.flatMap { player =>
val track = player.item

val currentLength = player.progress_ms.millis

methods
.audioAnalysis[F](track.uri)
.flatMapF {
_.sections
.map(_.start.seconds)
.find(_ > currentLength)
.toOptionT
.getOrElseF(UserOutput[F].print(UserMessage.TooCloseToEnd).as(Duration.Zero))
.map(_.toMillis.toInt)
.flatMap { analysis =>
analysis
.sections
.zipWithIndex
.find { case (section, _) => section.start.seconds > currentLength }
.traverse { case (section, index) =>
val percentage = (section.start.seconds * 100 / track.duration_ms.millis).toInt

UserOutput[F].print(
UserMessage.Jumping(
sectionNumber = index + 1,
sectionsTotal = analysis.sections.length,
percentTotal = percentage
)
) *>
Playback[F].seek(section.start.seconds.toMillis.toInt)
}
.pipe(OptionT(_))
.getOrElseF(UserOutput[F].print(UserMessage.TooCloseToEnd) *> Playback[F].seek(0))
}
}
.flatMapF(Playback[F].seek)
.run(client)
.void

}
Expand Down Expand Up @@ -167,8 +176,8 @@ object Spotify {
object DeviceInfo {
def apply[F[_]](using F: DeviceInfo[F]): DeviceInfo[F] = F

def instance[F[_]: Concurrent](client: Client[F]): DeviceInfo[F] = new DeviceInfo[F] {
val isRestricted: F[Boolean] = methods.player[F].run(client).map(_.device.is_restricted)
def instance[F[_]: Concurrent: Client]: DeviceInfo[F] = new DeviceInfo[F] {
val isRestricted: F[Boolean] = methods.player[F].map(_.device.is_restricted)
}

}
Expand Down Expand Up @@ -199,30 +208,24 @@ object Spotify {

val SpotifyApi = uri"https://api.spotify.com"

type Method[F[_], A] = Kleisli[F, Client[F], A]

def player[F[_]: Concurrent]: Method[F, Player[Option[PlayerContext], Option[Item]]] =
Kleisli {
_.expectOr(SpotifyApi / "v1" / "me" / "player") {
case response if response.status === Status.NoContent => NotPlaying.pure[F].widen
case response => InvalidStatus(response.status).pure[F].widen
}
def player[F[_]: Concurrent: Client]: F[Player[Option[PlayerContext], Option[Item]]] =
summon[Client[F]].expectOr(SpotifyApi / "v1" / "me" / "player") {
case response if response.status === Status.NoContent => NotPlaying.pure[F].widen
case response => InvalidStatus(response.status).pure[F].widen
}

def removeTrack[F[_]: Concurrent](trackUri: TrackUri, playlistId: String): Method[F, Unit] =
Kleisli {
_.expect[api.spotify.Anything](
def removeTrack[F[_]: Concurrent: Client](trackUri: TrackUri, playlistId: String): F[Unit] =
summon[Client[F]]
.expect[api.spotify.Anything](
Request[F](DELETE, SpotifyApi / "v1" / "playlists" / playlistId / "tracks")
.withEntity(Map("tracks" := List(Map("uri" := trackUri.toFullUri))).asJson)
).void
}

def audioAnalysis[F[_]: Concurrent](trackUri: TrackUri): Method[F, AudioAnalysis] =
Kleisli {
_.expect(
SpotifyApi / "v1" / "audio-analysis" / trackUri.id
)
}
.void

def audioAnalysis[F[_]: Concurrent: Client](trackUri: TrackUri): F[AudioAnalysis] =
summon[Client[F]].expect(
SpotifyApi / "v1" / "audio-analysis" / trackUri.id
)

}

Expand Down
13 changes: 8 additions & 5 deletions src/main/scala/com/kubukoz/next/UserMessage.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum UserMessage {
case SwitchingToNext
case RemovingCurrentTrack(player: Player[PlayerContext.playlist, Item.track])
case TooCloseToEnd
case Jumping(sectionNumber: Int, sectionsTotal: Int, percentTotal: Int)
case Seeking(desiredProgressPercent: Int)

// sonos
Expand Down Expand Up @@ -56,11 +57,13 @@ object UserOutput {
show"""Removing track "${player.item.name}" (${player.item.uri.toFullUri}) 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 => show"Checking if Sonos API is available at $sonosBaseUrl..."
case SonosNotFound => "Sonos not found, using fallback"
case SonosFound(zones, roomName) => show"Found ${zones.zones.size} zone(s), will use room $roomName"
case DeviceRestricted => "Device restricted, trying to switch to Sonos API control..."
case DirectControl => "Switching to direct Spotify API control..."
case Jumping(sectionNumber, sectionsTotal, percentTotal) =>
show"Jumping to section $sectionNumber/$sectionsTotal ($percentTotal%)"
case CheckingSonos => show"Checking if Sonos API is available at $sonosBaseUrl..."
case SonosNotFound => "Sonos not found, using fallback"
case SonosFound(zones, roomName) => show"Found ${zones.zones.size} zone(s), will use room $roomName"
case DeviceRestricted => "Device restricted, trying to switch to Sonos API control..."
case DirectControl => "Switching to direct Spotify API control..."
}

msg => std.Console[F].println(stringify(msg))
Expand Down

0 comments on commit f6356a9

Please sign in to comment.