A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve syncing algorithm

+182 -79
+4 -1
src/Applications/Brain/User/State.elm
··· 437 437 one part at a time. 438 438 -} 439 439 saveHypaethralDataBits : List HypaethralBit -> Manager 440 - saveHypaethralDataBits bits model = 440 + saveHypaethralDataBits bitsWithoutModifiedAt model = 441 441 let 442 + bits = 443 + ModifiedAt :: bitsWithoutModifiedAt 444 + 442 445 userData = 443 446 model.hypaethralUserData 444 447
+6
src/Applications/UI/User/State/Export.elm
··· 76 76 |> return model 77 77 78 78 79 + saveModifiedAt : Manager 80 + saveModifiedAt = 81 + -- Handled by 🧠 82 + Return.singleton 83 + 84 + 79 85 savePlaylists : Manager 80 86 savePlaylists model = 81 87 model.playlists
+3
src/Applications/UI/User/State/Import.elm
··· 177 177 Favourites -> 178 178 andThen User.saveFavourites 179 179 180 + ModifiedAt -> 181 + andThen User.saveModifiedAt 182 + 180 183 Playlists -> 181 184 andThen User.savePlaylists 182 185
+3
src/Javascript/Brain/common.ts
··· 83 83 } else if (data.startsWith("{") || data.startsWith("[")) { 84 84 return Promise.resolve(data) 85 85 86 + } else if (data.length < 15 && Number.isInteger(parseInt(data, 10))) { 87 + return Promise.resolve(data) 88 + 86 89 } else { 87 90 return data 88 91 ? getSecretKey().then(secretKey => {
+115 -46
src/Library/Syncing.elm
··· 1 - module Syncing exposing (..) 1 + module Syncing exposing (LocalConfig, RemoteConfig, task) 2 2 3 3 import Json.Decode as Decode 4 4 import Json.Encode as Json ··· 6 6 import Task exposing (Task) 7 7 import Task.Extra as Task 8 8 import Time 9 + import Time.Ext as Time 9 10 import User.Layer as User exposing (..) 10 11 11 12 13 + 14 + -- 🌳 15 + 16 + 17 + type alias LocalConfig = 18 + { localData : HypaethralData 19 + , saveLocal : HypaethralBit -> Decode.Value -> Task String () 20 + } 21 + 22 + 23 + type alias RemoteConfig = 24 + { retrieve : HypaethralBit -> Task String (Maybe Decode.Value) 25 + , save : HypaethralBit -> Decode.Value -> Task String () 26 + } 27 + 28 + 29 + 30 + -- 🛠 31 + 32 + 12 33 {-| Syncs all hypaethral data. 34 + 35 + Returns `Nothing` if the local data is preferred. 36 + 37 + 🏝️ LOCAL 38 + 🛰️ REMOTE 39 + 40 + 1. Try to pull remote `modified.json` timestamp 41 + a. If newer, continue (#2) 42 + b. If same, do nothing 43 + c. If older, or not present, prefer local data 🏝️ (stop & push) 44 + 2. Try to download all remote data 45 + a. If any remote data, continue (#3) 46 + b. If none, prefer local data 🏝️ (stop & push) 47 + 3. Decode remote data and compare timestamps 48 + a. If newer, use remote data 🛰️ 49 + b. If same, do nothing 50 + c. If older, prefer local data 🏝️ (stop & push) 51 + d. If no timestamps, if local data, prefer local 🏝️ (stop & push), otherwise remote 🛰️ 52 + 13 53 -} 14 54 task : 15 55 Task String a 16 - -> 17 - { localData : HypaethralData 18 - , saveLocal : HypaethralBit -> Decode.Value -> Task String () 19 - } 20 - -> 21 - { retrieve : HypaethralBit -> Task String (Maybe Decode.Value) 22 - , save : HypaethralBit -> Decode.Value -> Task String () 23 - } 56 + -> LocalConfig 57 + -> RemoteConfig 58 + -> Task String (Maybe HypaethralData) 59 + task initialTask localConfig remoteConfig = 60 + initialTask 61 + |> Task.andThen 62 + (\_ -> 63 + remoteConfig.retrieve ModifiedAt 64 + ) 65 + |> Task.andThen 66 + (\maybeModifiedAt -> 67 + let 68 + maybeRemoteModifiedAt = 69 + Maybe.andThen 70 + (Decode.decodeValue Time.decoder >> Result.toMaybe) 71 + maybeModifiedAt 72 + in 73 + case ( maybeRemoteModifiedAt, localConfig.localData.modifiedAt ) of 74 + ( Just remoteModifiedAt, Just localModifiedAt ) -> 75 + if Time.posixToMillis remoteModifiedAt == Time.posixToMillis localModifiedAt then 76 + -- 🏝️ 77 + Task.succeed Nothing 78 + 79 + else if Time.posixToMillis remoteModifiedAt > Time.posixToMillis localModifiedAt then 80 + -- 🛰️ 81 + fetchRemote localConfig remoteConfig 82 + 83 + else 84 + -- 🏝️ → 🛰️ 85 + pushLocalToRemote localConfig remoteConfig { return = Nothing } 86 + 87 + ( Just _, Nothing ) -> 88 + -- 🛰️ 89 + fetchRemote localConfig remoteConfig 90 + 91 + ( Nothing, _ ) -> 92 + -- 🛰️ 93 + fetchRemote localConfig remoteConfig 94 + ) 95 + 96 + 97 + fetchRemote : 98 + LocalConfig 99 + -> RemoteConfig 24 100 -> Task String (Maybe HypaethralData) 25 - task initialTask { localData, saveLocal } { retrieve, save } = 26 - -- 1. Check if any existing data is present on the service to sync with. 27 - -- 2. If not, copy over all current data (in memory) to that service. 28 - -- If so: 👇 29 - -- 3. If no data is present locally then just load the remote data (ie. service data) 30 - -- No data = no sources, favourites & playlists 31 - -- If so: 👇 32 - -- 4. Compare modifiedAt timestamps 33 - -- (if no remote timestamp is available try to calculate it based on the data, progress → favourites → playlists → tracks → sources) 34 - -- If remote is newer: Load remote data 35 - -- Otherwise: 👇 36 - -- 5. Load remote data and run merge function for each type of data (sources, tracks, etc.) 37 - -- 6. Store merged data into memory 38 - -- 7. Overwrite remote data 39 - -- 40 - -- 🏝️ LOCAL 41 - -- 🛰️ REMOTE 101 + fetchRemote localConfig remoteConfig = 42 102 let 43 - noLocalData = 44 - List.isEmpty localData.sources 45 - && List.isEmpty localData.favourites 46 - && List.isEmpty localData.playlists 103 + { localData, saveLocal } = 104 + localConfig 47 105 48 - pushLocalToRemote { return } = 49 - localData 50 - |> User.encodedHypaethralDataList 51 - |> List.map (\( bit, data ) -> save bit data) 52 - |> Task.sequence 53 - |> Task.map (\_ -> return) 106 + { retrieve } = 107 + remoteConfig 54 108 55 109 saveLocally data = 56 110 data 57 111 |> User.saveHypaethralData saveLocal 58 112 |> Task.map (\_ -> Just data) 113 + 114 + noLocalData = 115 + List.isEmpty localData.sources 116 + && List.isEmpty localData.favourites 117 + && List.isEmpty localData.playlists 59 118 in 60 - initialTask 61 - |> Task.andThen 62 - (\_ -> 63 - User.retrieveHypaethralData retrieve 64 - ) 119 + retrieve 120 + |> User.retrieveHypaethralData 65 121 |> Task.andThen 66 122 (\list -> 67 123 let ··· 74 130 75 131 else 76 132 -- 🏝️ → 🛰️ 77 - pushLocalToRemote { return = list } 133 + pushLocalToRemote localConfig remoteConfig { return = list } 78 134 ) 79 135 |> Task.andThen 80 136 (\list -> ··· 100 156 saveLocally remoteData 101 157 102 158 else 103 - -- 🏝️ 104 - pushLocalToRemote { return = Nothing } 159 + -- 🏝️ → 🛰️ 160 + pushLocalToRemote localConfig remoteConfig { return = Nothing } 105 161 106 162 ( Just _, Nothing ) -> 107 163 -- 🛰️ 108 164 saveLocally remoteData 109 165 110 166 ( Nothing, Just _ ) -> 111 - -- 🏝️ 112 - pushLocalToRemote { return = Nothing } 167 + -- 🏝️ → 🛰️ 168 + pushLocalToRemote localConfig remoteConfig { return = Nothing } 113 169 114 170 _ -> 115 171 if noLocalData then ··· 120 176 -- 🏝️ 121 177 Task.succeed Nothing 122 178 ) 179 + 180 + 181 + 182 + -- ㊙️ 183 + 184 + 185 + pushLocalToRemote : LocalConfig -> RemoteConfig -> { return : a } -> Task String a 186 + pushLocalToRemote localConfig remoteConfig { return } = 187 + localConfig.localData 188 + |> User.encodedHypaethralDataList 189 + |> List.map (\( bit, data ) -> remoteConfig.save bit data) 190 + |> Task.sequence 191 + |> Task.map (\_ -> return)
+51 -32
src/Library/User/Layer.elm
··· 98 98 | Settings 99 99 | Sources 100 100 | Tracks 101 + -- 102 + | ModifiedAt 101 103 102 104 103 105 type HypaethralBaggage ··· 316 318 317 319 encodeHypaethralBit : HypaethralBit -> HypaethralData -> Json.Value 318 320 encodeHypaethralBit bit { favourites, playlists, progress, settings, sources, tracks, modifiedAt } = 319 - Json.Encode.object 320 - [ ( "data" 321 - , case bit of 322 - Favourites -> 323 - Json.Encode.list Tracks.encodeFavourite favourites 321 + case bit of 322 + ModifiedAt -> 323 + Maybe.unwrap Json.Encode.null Time.encode modifiedAt 324 + 325 + _ -> 326 + Json.Encode.object 327 + [ ( "data" 328 + , case bit of 329 + Favourites -> 330 + Json.Encode.list Tracks.encodeFavourite favourites 331 + 332 + ModifiedAt -> 333 + Maybe.unwrap Json.Encode.null Time.encode modifiedAt 324 334 325 - Playlists -> 326 - Json.Encode.list Playlists.encode playlists 335 + Playlists -> 336 + Json.Encode.list Playlists.encode playlists 327 337 328 - Progress -> 329 - Json.Encode.dict identity Json.Encode.float progress 338 + Progress -> 339 + Json.Encode.dict identity Json.Encode.float progress 330 340 331 - Settings -> 332 - Maybe.unwrap Json.Encode.null Settings.encode settings 341 + Settings -> 342 + Maybe.unwrap Json.Encode.null Settings.encode settings 333 343 334 - Sources -> 335 - Json.Encode.list Sources.encode sources 344 + Sources -> 345 + Json.Encode.list Sources.encode sources 336 346 337 - Tracks -> 338 - Json.Encode.list Tracks.encodeTrack tracks 339 - ) 340 - , ( "modifiedAt" 341 - , Maybe.unwrap Json.Encode.null Time.encode modifiedAt 342 - ) 343 - ] 347 + Tracks -> 348 + Json.Encode.list Tracks.encodeTrack tracks 349 + ) 350 + , ( "modifiedAt" 351 + , Maybe.unwrap Json.Encode.null Time.encode modifiedAt 352 + ) 353 + ] 344 354 345 355 346 356 encodeHypaethralData : HypaethralData -> Json.Value ··· 375 385 case bit of 376 386 Favourites -> 377 387 "favourites" 388 + 389 + ModifiedAt -> 390 + "modified" 378 391 379 392 Playlists -> 380 393 "playlists" ··· 402 415 { data = def, modifiedAt = Nothing } 403 416 a 404 417 in 405 - (\fav pla pro set sor tra -> 418 + (\fav pla pro set sor tra mod -> 406 419 { favourites = fav.data 407 420 , playlists = pla.data 408 421 , progress = pro.data ··· 412 425 413 426 -- 414 427 , modifiedAt = 415 - [ fav.modifiedAt 416 - , pla.modifiedAt 417 - , pro.modifiedAt 418 - , set.modifiedAt 419 - , sor.modifiedAt 420 - , tra.modifiedAt 421 - ] 422 - |> List.filterMap (Maybe.map Time.posixToMillis) 423 - |> List.sort 424 - |> List.last 425 - |> Maybe.map Time.millisToPosix 428 + case mod of 429 + Just m -> 430 + Just (Time.millisToPosix m) 431 + 432 + Nothing -> 433 + [ fav.modifiedAt 434 + , pla.modifiedAt 435 + , pro.modifiedAt 436 + , set.modifiedAt 437 + , sor.modifiedAt 438 + , tra.modifiedAt 439 + ] 440 + |> List.filterMap (Maybe.map Time.posixToMillis) 441 + |> List.sort 442 + |> List.last 443 + |> Maybe.map Time.millisToPosix 426 444 } 427 445 ) 428 446 |> Json.succeed ··· 432 450 |> optionalWithPossiblyData Settings (Json.maybe Settings.decoder) Nothing 433 451 |> optionalWithPossiblyData Sources (Json.listIgnore Sources.decoder) [] 434 452 |> optionalWithPossiblyData Tracks (Json.listIgnore Tracks.trackDecoder) [] 453 + |> optional (hypaethralBitKey ModifiedAt) (Json.maybe Json.int) Nothing 435 454 436 455 437 456