A music player that connects to your cloud/distributed storage.
at main 15 kB view raw
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