1module User.Layer exposing (..)
2
3{-| User Layer.
4
5This concerns data that relates to the app,
6controlled by the user and stored by the user.
7
8**Enclosed** data is data like, the enable-shuffle setting,
9equalizer settings, or the currently-active-search term.
10Which is stored in the browser.
11
12**Hypaethral** data is data like, the user's favourites,
13processed tracks, or the user's sources.
14Which is stored in the location chosen by the user.
15
16-}
17
18import Dict exposing (Dict)
19import Enum exposing (Enum)
20import Equalizer
21import Json.Decode as Json
22import Json.Decode.Ext as Json
23import Json.Decode.Pipeline exposing (optional)
24import Json.Encode
25import List.Extra as List
26import Maybe.Extra as Maybe
27import Playlists
28import Playlists.Encoding as Playlists
29import Settings
30import Sources
31import Sources.Encoding as Sources
32import Task exposing (Task)
33import Theme
34import Time
35import Time.Ext as Time
36import Tracks
37import Tracks.Encoding as Tracks
38
39
40
41-- 🌳
42
43
44type Method
45 = Dropbox { accessToken : String, expiresAt : Int, refreshToken : String }
46 | Ipfs { apiOrigin : String }
47 | RemoteStorage { userAddress : String, token : String }
48
49
50dropboxMethod : Method
51dropboxMethod =
52 Dropbox { accessToken = "", expiresAt = 0, refreshToken = "" }
53
54
55ipfsMethod : Method
56ipfsMethod =
57 Ipfs { apiOrigin = "https://ipfs.io" }
58
59
60remoteStorageMethod : Method
61remoteStorageMethod =
62 RemoteStorage { userAddress = "", token = "" }
63
64
65
66-- 🌳 ░░ ENCLOSED
67
68
69type alias EnclosedData =
70 { cachedTracks : List String
71 , equalizerSettings : Equalizer.Settings
72 , grouping : Maybe Tracks.Grouping
73 , onlyShowCachedTracks : Bool
74 , onlyShowFavourites : Bool
75 , repeat : Bool
76 , scene : Tracks.Scene
77 , searchTerm : Maybe String
78 , selectedPlaylist : Maybe String
79 , shuffle : Bool
80 , sortBy : Tracks.SortBy
81 , sortDirection : Tracks.SortDirection
82 , theme : Maybe Theme.Id
83 }
84
85
86
87-- 🌳 ░░ HYPAETHRAL
88
89
90type HypaethralBit
91 = Favourites
92 | Playlists
93 | Progress
94 | Settings
95 | Sources
96 | Tracks
97 --
98 | ModifiedAt
99
100
101type HypaethralBaggage
102 = BaggageClaimed
103 --
104 | PlaylistsBaggage PlaylistsBaggageAttributes
105
106
107type alias HypaethralData =
108 { favourites : List Tracks.Favourite
109 , playlists : List Playlists.Playlist
110 , progress : Dict String Float
111 , settings : Maybe Settings.Settings
112 , sources : List Sources.Source
113 , tracks : List Tracks.Track
114
115 --
116 , modifiedAt : Maybe Time.Posix
117 }
118
119
120type alias PlaylistsBaggageAttributes =
121 { publicPlaylistsRead : List Json.Value
122 , publicPlaylistsTodo : List String
123 , privatePlaylistsRead : List Json.Value
124 , privatePlaylistsTodo : List String
125 }
126
127
128
129-- 🔱 ░░ METHOD
130
131
132decodeMethod : Json.Value -> Maybe Method
133decodeMethod =
134 Json.decodeValue (Json.map methodFromString Json.string) >> Result.toMaybe >> Maybe.join
135
136
137encodeMethod : Method -> Json.Value
138encodeMethod =
139 methodToString >> Json.Encode.string
140
141
142methodName : Method -> String
143methodName method =
144 case method of
145 Dropbox _ ->
146 "Dropbox"
147
148 Ipfs _ ->
149 "IPFS (using MFS)"
150
151 RemoteStorage _ ->
152 "Remote Storage"
153
154
155methodFromString : String -> Maybe Method
156methodFromString string =
157 case String.split methodSeparator string of
158 [ "DROPBOX", a, e, r ] ->
159 Just
160 (Dropbox
161 { accessToken = a
162 , expiresAt = Maybe.withDefault 0 (String.toInt e)
163 , refreshToken = r
164 }
165 )
166
167 [ "IPFS", a ] ->
168 Just (Ipfs { apiOrigin = a })
169
170 [ "REMOTE_STORAGE", u, t ] ->
171 Just (RemoteStorage { userAddress = u, token = t })
172
173 _ ->
174 Nothing
175
176
177methodToString : Method -> String
178methodToString method =
179 case method of
180 Dropbox { accessToken, expiresAt, refreshToken } ->
181 String.join
182 methodSeparator
183 [ "DROPBOX"
184 , accessToken
185 , String.fromInt expiresAt
186 , refreshToken
187 ]
188
189 Ipfs { apiOrigin } ->
190 String.join
191 methodSeparator
192 [ "IPFS"
193 , apiOrigin
194 ]
195
196 RemoteStorage { userAddress, token } ->
197 String.join
198 methodSeparator
199 [ "REMOTE_STORAGE"
200 , userAddress
201 , token
202 ]
203
204
205methodSeparator : String
206methodSeparator =
207 "___"
208
209
210methodSupportsPublicData : Method -> Bool
211methodSupportsPublicData method =
212 case method of
213 Dropbox _ ->
214 False
215
216 Ipfs _ ->
217 False
218
219 RemoteStorage _ ->
220 False
221
222
223
224-- 🔱 ░░ ENCLOSED
225
226
227decodeEnclosedData : Json.Value -> Result Json.Error EnclosedData
228decodeEnclosedData =
229 Json.decodeValue enclosedDataDecoder
230
231
232enclosedDataDecoder : Json.Decoder EnclosedData
233enclosedDataDecoder =
234 Json.succeed EnclosedData
235 |> optional "cachedTracks" (Json.list Json.string) []
236 |> optional "equalizerSettings" Equalizer.settingsDecoder Equalizer.defaultSettings
237 |> optional "grouping" (Json.maybe Tracks.groupingDecoder) Nothing
238 |> optional "onlyShowCachedTracks" Json.bool False
239 |> optional "onlyShowFavourites" Json.bool False
240 |> optional "repeat" Json.bool False
241 |> optional "scene" Tracks.sceneDecoder Tracks.Covers
242 |> optional "searchTerm" (Json.maybe Json.string) Nothing
243 |> optional "selectedPlaylist" (Json.maybe Json.string) Nothing
244 |> optional "shuffle" Json.bool False
245 |> optional "sortBy" Tracks.sortByDecoder Tracks.Album
246 |> optional "sortDirection" Tracks.sortDirectionDecoder Tracks.Asc
247 |> optional "theme" (Json.maybe Theme.idDecoder) Nothing
248
249
250encodeEnclosedData : EnclosedData -> Json.Value
251encodeEnclosedData { cachedTracks, equalizerSettings, grouping, onlyShowCachedTracks, onlyShowFavourites, repeat, scene, searchTerm, selectedPlaylist, shuffle, sortBy, sortDirection, theme } =
252 Json.Encode.object
253 [ ( "cachedTracks", Json.Encode.list Json.Encode.string cachedTracks )
254 , ( "equalizerSettings", Equalizer.encodeSettings equalizerSettings )
255 , ( "grouping", Maybe.unwrap Json.Encode.null Tracks.encodeGrouping grouping )
256 , ( "onlyShowCachedTracks", Json.Encode.bool onlyShowCachedTracks )
257 , ( "onlyShowFavourites", Json.Encode.bool onlyShowFavourites )
258 , ( "repeat", Json.Encode.bool repeat )
259 , ( "scene", Tracks.encodeScene scene )
260 , ( "searchTerm", Maybe.unwrap Json.Encode.null Json.Encode.string searchTerm )
261 , ( "selectedPlaylist", Maybe.unwrap Json.Encode.null Json.Encode.string selectedPlaylist )
262 , ( "shuffle", Json.Encode.bool shuffle )
263 , ( "sortBy", Tracks.encodeSortBy sortBy )
264 , ( "sortDirection", Tracks.encodeSortDirection sortDirection )
265 , ( "theme", Maybe.unwrap Json.Encode.null Theme.encodeId theme )
266 ]
267
268
269
270-- 🔱 ░░ HYPAETHRAL
271
272
273allHypaethralBits : List HypaethralBit
274allHypaethralBits =
275 [ Favourites
276 , Playlists
277 , Progress
278 , Settings
279 , Sources
280 , Tracks
281 ]
282
283
284decodeHypaethralData : Json.Value -> Result Json.Error HypaethralData
285decodeHypaethralData =
286 Json.decodeValue hypaethralDataDecoder
287
288
289emptyHypaethralData : HypaethralData
290emptyHypaethralData =
291 { favourites = []
292 , playlists = []
293 , progress = Dict.empty
294 , settings = Nothing
295 , sources = []
296 , tracks = []
297
298 --
299 , modifiedAt = Nothing
300 }
301
302
303encodeHypaethralBit : HypaethralBit -> HypaethralData -> Json.Value
304encodeHypaethralBit bit { favourites, playlists, progress, settings, sources, tracks, modifiedAt } =
305 case bit of
306 ModifiedAt ->
307 Maybe.unwrap Json.Encode.null Time.encode modifiedAt
308
309 _ ->
310 Json.Encode.object
311 [ ( "data"
312 , case bit of
313 Favourites ->
314 Json.Encode.list Tracks.encodeFavourite favourites
315
316 ModifiedAt ->
317 Maybe.unwrap Json.Encode.null Time.encode modifiedAt
318
319 Playlists ->
320 Json.Encode.list Playlists.encode playlists
321
322 Progress ->
323 Json.Encode.dict identity Json.Encode.float progress
324
325 Settings ->
326 Maybe.unwrap Json.Encode.null Settings.encode settings
327
328 Sources ->
329 Json.Encode.list Sources.encode sources
330
331 Tracks ->
332 Json.Encode.list Tracks.encodeTrack tracks
333 )
334 , ( "modifiedAt"
335 , Maybe.unwrap Json.Encode.null Time.encode modifiedAt
336 )
337 ]
338
339
340encodeHypaethralData : HypaethralData -> Json.Value
341encodeHypaethralData data =
342 data
343 |> encodedHypaethralDataList
344 |> List.map (Tuple.mapFirst hypaethralBitKey)
345 |> Json.Encode.object
346
347
348encodedHypaethralDataList : HypaethralData -> List ( HypaethralBit, Json.Value )
349encodedHypaethralDataList data =
350 List.map
351 (\bit -> ( bit, encodeHypaethralBit bit data ))
352 allHypaethralBits
353
354
355hypaethralBit : Enum HypaethralBit
356hypaethralBit =
357 allHypaethralBits
358 |> List.map (\bit -> ( hypaethralBitKey bit, bit ))
359 |> Enum.create
360
361
362hypaethralBitFileName : HypaethralBit -> String
363hypaethralBitFileName bit =
364 hypaethralBitKey bit ++ ".json"
365
366
367hypaethralBitKey : HypaethralBit -> String
368hypaethralBitKey bit =
369 case bit of
370 Favourites ->
371 "favourites"
372
373 ModifiedAt ->
374 "modified"
375
376 Playlists ->
377 "playlists"
378
379 Progress ->
380 "progress"
381
382 Settings ->
383 "settings"
384
385 Sources ->
386 "sources"
387
388 Tracks ->
389 "tracks"
390
391
392hypaethralDataDecoder : Json.Decoder HypaethralData
393hypaethralDataDecoder =
394 let
395 optionalWithPossiblyData key dec def a =
396 optional
397 (hypaethralBitKey key)
398 (Json.oneOf [ modifiedAtDecoder dec, noModifiedAt dec ])
399 { data = def, modifiedAt = Nothing }
400 a
401 in
402 (\fav pla pro set sor tra mod ->
403 { favourites = fav.data
404 , playlists = pla.data
405 , progress = pro.data
406 , settings = set.data
407 , sources = sor.data
408 , tracks = tra.data
409
410 --
411 , modifiedAt =
412 case mod of
413 Just m ->
414 Just (Time.millisToPosix m)
415
416 Nothing ->
417 [ fav.modifiedAt
418 , pla.modifiedAt
419 , pro.modifiedAt
420 , set.modifiedAt
421 , sor.modifiedAt
422 , tra.modifiedAt
423 ]
424 |> List.filterMap (Maybe.map Time.posixToMillis)
425 |> List.sort
426 |> List.last
427 |> Maybe.map Time.millisToPosix
428 }
429 )
430 |> Json.succeed
431 |> optionalWithPossiblyData Favourites (Json.listIgnore Tracks.favouriteDecoder) []
432 |> optionalWithPossiblyData Playlists (Json.listIgnore Playlists.decoder) []
433 |> optionalWithPossiblyData Progress (Json.dict Json.float) Dict.empty
434 |> optionalWithPossiblyData Settings (Json.maybe Settings.decoder) Nothing
435 |> optionalWithPossiblyData Sources (Json.listIgnore Sources.decoder) []
436 |> optionalWithPossiblyData Tracks (Json.listIgnore Tracks.trackDecoder) []
437 |> optional (hypaethralBitKey ModifiedAt) (Json.maybe Json.int) Nothing
438
439
440
441-- merge : HypaethralData -> HypaethralData -> HypaethralData
442-- merge a b =
443-- { favourites = List.unique (a.favourites ++ b.favourites)
444-- , playlists = List.unique (a.playlists ++ b.playlists)
445-- , progress = List.unique (a.progress ++ b.progress)
446-- , settings = List.unique (a.settings ++ b.settings)
447-- , sources = List.unique (a.sources ++ b.sources)
448-- , tracks = List.unique (a.tracks ++ b.tracks)
449-- --
450-- , modifiedAt =
451-- case ( a.modifiedAt, b.modifiedAt ) of
452-- ( Just am, Just bm ) ->
453-- if Time.posixToMillis am > Time.posixToMillis bm then
454-- Just am
455-- else
456-- Just bm
457-- ( Just am, Nothing ) ->
458-- Just am
459-- ( Nothing, Just bm ) ->
460-- Just bm
461-- ( Nothing, Nothing ) ->
462-- Nothing
463-- }
464
465
466modifiedAtDecoder : Json.Decoder a -> Json.Decoder { data : a, modifiedAt : Maybe Time.Posix }
467modifiedAtDecoder decoder =
468 Json.map2
469 (\d m -> { data = d, modifiedAt = m })
470 (Json.field "data" decoder)
471 (Json.maybe <| Json.field "modifiedAt" Time.decoder)
472
473
474noModifiedAt : Json.Decoder a -> Json.Decoder { data : a, modifiedAt : Maybe Time.Posix }
475noModifiedAt =
476 Json.map
477 (\data ->
478 { data = data
479 , modifiedAt = Nothing
480 }
481 )
482
483
484putHypaethralJsonBitsTogether : List ( HypaethralBit, Json.Value, HypaethralBaggage ) -> Json.Value
485putHypaethralJsonBitsTogether bits =
486 bits
487 |> List.map (\( a, b, _ ) -> ( hypaethralBitKey a, b ))
488 |> Json.Encode.object
489
490
491retrieveHypaethralData : (HypaethralBit -> Task x (Maybe Json.Value)) -> Task x (List ( HypaethralBit, Maybe Json.Encode.Value ))
492retrieveHypaethralData retrievalFn =
493 hypaethralBit.list
494 |> List.map
495 (\( _, bit ) ->
496 bit
497 |> retrievalFn
498 |> Task.map (\value -> ( bit, value ))
499 )
500 |> Task.sequence
501
502
503saveHypaethralData : (HypaethralBit -> Json.Value -> Task x ()) -> HypaethralData -> Task x ()
504saveHypaethralData saveFn data =
505 hypaethralBit.list
506 |> List.map
507 (\( _, bit ) ->
508 data
509 |> encodeHypaethralBit bit
510 |> saveFn bit
511 )
512 |> Task.sequence
513 |> Task.map (always ())
514
515
516
517-- 🔱 ░░ BAGGAGE
518
519
520mapPlaylistsBaggage : (PlaylistsBaggageAttributes -> PlaylistsBaggageAttributes) -> HypaethralBaggage -> HypaethralBaggage
521mapPlaylistsBaggage fn baggage =
522 case baggage of
523 PlaylistsBaggage p ->
524 PlaylistsBaggage (fn p)
525
526 b ->
527 b