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 }