A music player that connects to your cloud/distributed storage.
at main 11 kB view raw
1module UI.Audio.State exposing (..) 2 3import Base64 4import Common exposing (boolToString) 5import Debouncer.Basic as Debouncer 6import Dict 7import LastFm 8import List.Extra as List 9import MediaSession 10import Return exposing (return) 11import Return.Ext as Return exposing (communicate) 12import Tracks 13import UI.Audio.Types exposing (..) 14import UI.Common.State as Common 15import UI.Common.Types exposing (DebounceManager) 16import UI.Ports as Ports 17import UI.Queue.State as Queue 18import UI.Types as UI exposing (Manager, Msg(..)) 19import UI.User.State.Export as User 20 21 22 23-- 📣 ░░ EVENTS 24 25 26durationChange : DurationChangeEvent -> Manager 27durationChange { trackId, duration } = 28 onlyIfMatchesNowPlaying 29 { trackId = trackId } 30 (\nowPlaying -> 31 if nowPlaying.duration == Nothing || nowPlaying.duration /= Just duration then 32 durationChange_ { trackId = trackId, duration = duration } nowPlaying 33 34 else 35 -- Ignore repeating events 36 Return.singleton 37 ) 38 39 40durationChange_ : DurationChangeEvent -> NowPlaying -> Manager 41durationChange_ { trackId, duration } nowPlaying model = 42 let 43 ( identifiers, track ) = 44 nowPlaying.item.identifiedTrack 45 46 maybeCover = 47 List.find 48 (\c -> List.member trackId c.trackIds) 49 model.covers.arranged 50 51 coverPrep = 52 Maybe.map 53 (\cover -> 54 { cacheKey = Base64.encode (Tracks.coverKey cover.variousArtists track) 55 , trackFilename = identifiers.filename 56 , trackPath = track.path 57 , trackSourceId = track.sourceId 58 , variousArtists = boolToString cover.variousArtists 59 } 60 ) 61 maybeCover 62 63 coverLoaded = 64 case ( maybeCover, model.cachedCovers ) of 65 ( Just cover, Just cachedCovers ) -> 66 let 67 key = 68 Base64.encode (Tracks.coverKey cover.variousArtists track) 69 in 70 Dict.member key cachedCovers 71 72 _ -> 73 False 74 75 metadata = 76 { album = track.tags.album 77 , artist = track.tags.artist 78 , title = track.tags.title 79 80 -- 81 , coverPrep = coverPrep 82 } 83 in 84 model 85 |> replaceNowPlaying { nowPlaying | coverLoaded = coverLoaded, duration = Just duration } 86 |> Return.command (Ports.setMediaSessionMetadata metadata) 87 |> Return.command (Ports.resetScrobbleTimer { duration = duration, trackId = trackId }) 88 |> Return.andThen (notifyScrobblersOfTrackPlaying { duration = duration }) 89 90 91error : ErrorAudioEvent -> Manager 92error { trackId, code } = 93 onlyIfMatchesNowPlaying 94 { trackId = trackId } 95 (\nowPlaying -> 96 replaceNowPlaying 97 (case code of 98 2 -> 99 { nowPlaying | loadingState = NetworkError } 100 101 3 -> 102 { nowPlaying | loadingState = DecodeError } 103 104 4 -> 105 { nowPlaying | loadingState = NotSupportedError } 106 107 _ -> 108 nowPlaying 109 ) 110 ) 111 112 113ended : GenericAudioEvent -> Manager 114ended { trackId } = 115 onlyIfMatchesNowPlaying 116 { trackId = trackId } 117 (\nowPlaying model -> 118 if model.repeat then 119 Return.command 120 (case nowPlaying.duration of 121 Just duration -> 122 Ports.resetScrobbleTimer { duration = duration, trackId = trackId } 123 124 Nothing -> 125 Cmd.none 126 ) 127 (play model) 128 129 else 130 Return.andThen 131 (if Maybe.map (\d -> Tracks.shouldNoteProgress { duration = d }) nowPlaying.duration == Just True then 132 noteProgress { trackId = trackId, progress = 1.0 } 133 134 else 135 Return.singleton 136 ) 137 (Queue.shift model) 138 ) 139 140 141hasLoaded : GenericAudioEvent -> Manager 142hasLoaded { trackId } = 143 onlyIfMatchesNowPlaying 144 { trackId = trackId } 145 (\nowPlaying -> 146 replaceNowPlaying { nowPlaying | loadingState = Loaded } 147 ) 148 149 150isLoading : GenericAudioEvent -> Manager 151isLoading { trackId } = 152 onlyIfMatchesNowPlaying 153 { trackId = trackId } 154 (\nowPlaying -> 155 replaceNowPlaying { nowPlaying | loadingState = Loading } 156 ) 157 158 159playbackStateChanged : PlaybackStateEvent -> Manager 160playbackStateChanged { trackId, isPlaying } = 161 onlyIfMatchesNowPlaying 162 { trackId = trackId } 163 (\nowPlaying model -> 164 { model | nowPlaying = Just { nowPlaying | isPlaying = isPlaying } } 165 |> Return.singleton 166 |> Return.command 167 (if isPlaying then 168 Ports.startScrobbleTimer () 169 170 else 171 Ports.pauseScrobbleTimer () 172 ) 173 |> Return.command 174 (Ports.setMediaSessionPlaybackState 175 (if isPlaying then 176 MediaSession.states.playing 177 178 else 179 MediaSession.states.paused 180 ) 181 ) 182 ) 183 184 185timeUpdated : TimeUpdatedEvent -> Manager 186timeUpdated { trackId, currentTime, duration } = 187 onlyIfMatchesNowPlaying 188 { trackId = trackId } 189 (\nowPlaying model -> 190 let 191 dur = 192 Maybe.withDefault 0 duration 193 in 194 { model | nowPlaying = Just { nowPlaying | duration = duration, playbackPosition = currentTime } } 195 |> (if Tracks.shouldNoteProgress { duration = dur } then 196 { trackId = trackId 197 , progress = currentTime / dur 198 } 199 |> NoteProgress 200 |> Debouncer.provideInput 201 |> NoteProgressDebounce 202 |> Return.task 203 |> Return.communicate 204 205 else 206 Return.singleton 207 ) 208 |> Return.command 209 (case duration of 210 Just d -> 211 Ports.setMediaSessionPositionState 212 { currentTime = currentTime 213 , duration = d 214 } 215 216 Nothing -> 217 Cmd.none 218 ) 219 ) 220 221 222 223-- 📣 ░░ COMMANDS 224 225 226pause : Manager 227pause model = 228 case model.nowPlaying of 229 Just { item } -> 230 communicate 231 (Ports.pause 232 { trackId = (Tuple.second item.identifiedTrack).id 233 } 234 ) 235 model 236 237 Nothing -> 238 Return.singleton model 239 240 241playPause : Manager 242playPause model = 243 case model.nowPlaying of 244 Just { isPlaying } -> 245 if isPlaying then 246 pause model 247 248 else 249 play model 250 251 Nothing -> 252 play model 253 254 255play : Manager 256play model = 257 case model.nowPlaying of 258 Just { item } -> 259 communicate 260 (Ports.play 261 { trackId = (Tuple.second item.identifiedTrack).id 262 , volume = model.eqSettings.volume 263 } 264 ) 265 model 266 267 Nothing -> 268 Queue.shift model 269 270 271seek : { trackId : String, progress : Float } -> Manager 272seek { trackId, progress } = 273 { percentage = progress, trackId = trackId } 274 |> Ports.seek 275 |> Return.communicate 276 277 278stop : Manager 279stop model = 280 model.audioElements 281 |> List.filter (.isPreload >> (==) True) 282 |> (\a -> { model | audioElements = a }) 283 |> Queue.changeActiveItem Nothing 284 |> Return.effect_ 285 (\m -> 286 Ports.renderAudioElements 287 { items = m.audioElements 288 , play = Nothing 289 , volume = m.eqSettings.volume 290 } 291 ) 292 293 294 295-- 📣 296 297 298noteProgress : { trackId : String, progress : Float } -> Manager 299noteProgress { trackId, progress } model = 300 let 301 updatedProgressTable = 302 if not model.rememberProgress then 303 model.progress 304 305 else if progress > 0.975 then 306 Dict.remove trackId model.progress 307 308 else 309 Dict.insert trackId progress model.progress 310 in 311 if model.rememberProgress then 312 User.saveProgress { model | progress = updatedProgressTable } 313 314 else 315 Return.singleton model 316 317 318noteProgressDebounce : DebounceManager 319noteProgressDebounce = 320 Common.debounce 321 .progressDebouncer 322 (\d m -> { m | progressDebouncer = d }) 323 UI.NoteProgressDebounce 324 325 326notifyScrobblersOfTrackPlaying : { duration : Float } -> Manager 327notifyScrobblersOfTrackPlaying { duration } model = 328 case model.nowPlaying of 329 Just { item } -> 330 { duration = round duration 331 , msg = UI.Bypass 332 , track = Tuple.second item.identifiedTrack 333 } 334 |> LastFm.nowPlaying model.lastFm 335 |> return model 336 337 Nothing -> 338 Return.singleton model 339 340 341preloadDebounce : DebounceManager 342preloadDebounce = 343 Common.debounce 344 .preloadDebouncer 345 (\d m -> { m | preloadDebouncer = d }) 346 UI.AudioPreloadDebounce 347 348 349toggleRememberProgress : Manager 350toggleRememberProgress model = 351 User.saveSettings { model | rememberProgress = not model.rememberProgress } 352 353 354 355-- 🛠️ 356 357 358onlyIfMatchesNowPlaying : { trackId : String } -> (NowPlaying -> Manager) -> Manager 359onlyIfMatchesNowPlaying { trackId } fn model = 360 case model.nowPlaying of 361 Just ({ item } as nowPlaying) -> 362 if trackId == (Tuple.second item.identifiedTrack).id then 363 fn nowPlaying model 364 365 else 366 Return.singleton model 367 368 Nothing -> 369 Return.singleton model 370 371 372replaceNowPlaying : NowPlaying -> Manager 373replaceNowPlaying np model = 374 Return.singleton { model | nowPlaying = Just np }