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.

Add ability to resync tags

+254 -51
+2 -1
CHANGELOG.md
··· 5 5 - **Adds album-covers view** (switch to and from list view with icon in nav bar) 6 6 - Added new background images and replaced two older ones 7 7 - Adds a keyboard shortcut `L` to quickly select a playlist 8 + - Adds ability to sync tags for individual tracks 8 9 - Adds keyboard shortcuts to switch screens 9 10 - Adds the ability to migrate between data-storage methods 10 11 - Dark mode for the about page 11 12 - Fixes issue with shuffle algorithm 12 13 - Fixes playback issues (eg. clicking same track multiple times) 13 14 - Fixes various issues with add-to-playlist overlay 14 - - Only generated playlists for enabled sources 15 + - Only show generated playlists for enabled sources 15 16 - Removes support for Blockstack & Textile 16 17 17 18 ## 2.5.3
+10
src/Applications/Brain.elm
··· 120 120 RemoveTracksFromCache a -> 121 121 Tracks.removeFromCache a 122 122 123 + ReplaceTrackTags a -> 124 + Tracks.replaceTags a 125 + 123 126 Search a -> 124 127 Tracks.search a 125 128 126 129 StoreTracksInCache a -> 127 130 Tracks.storeInCache a 131 + 132 + SyncTrackTags a -> 133 + Tracks.syncTrackTags a 128 134 129 135 UpdateSearchIndex a -> 130 136 Tracks.updateSearchIndex a ··· 159 165 , Ports.makeArtworkTrackUrls MakeArtworkTrackUrls 160 166 , Ports.receiveSearchResults GotSearchResults 161 167 , Ports.receiveTags (ProcessingMsg << Processing.TagsStep) 168 + , Ports.replaceTags ReplaceTrackTags 162 169 , Ports.savedHypaethralBit (\_ -> UserMsg User.SaveNextHypaethralBit) 163 170 164 171 -- ··· 263 270 264 271 Alien.StoreTracksInCache -> 265 272 StoreTracksInCache data 273 + 274 + Alien.SyncTrackTags -> 275 + SyncTrackTags data 266 276 267 277 Alien.ToCache -> 268 278 ToCache data
+7 -1
src/Applications/Brain/Ports.elm
··· 2 2 3 3 import Alien 4 4 import Json.Encode as Json 5 - import Sources.Processing exposing (ContextForTags) 5 + import Sources.Processing exposing (ContextForTags, ContextForTagsSync) 6 6 7 7 8 8 ··· 31 31 32 32 33 33 port storeTracksInCache : Json.Value -> Cmd msg 34 + 35 + 36 + port syncTags : ContextForTagsSync -> Cmd msg 34 37 35 38 36 39 port toCache : Alien.Event -> Cmd msg ··· 87 90 88 91 89 92 port receiveTags : (ContextForTags -> msg) -> Sub msg 93 + 94 + 95 + port replaceTags : (ContextForTagsSync -> msg) -> Sub msg 90 96 91 97 92 98 port savedHypaethralBit : (Json.Value -> msg) -> Sub msg
+120 -1
src/Applications/Brain/Tracks/State.elm
··· 14 14 import Return exposing (andThen, return) 15 15 import Return.Ext as Return 16 16 import Sources exposing (Source) 17 - import Sources.Processing exposing (HttpMethod(..)) 17 + import Sources.Processing exposing (ContextForTagsSync, HttpMethod(..), TagUrls) 18 18 import Sources.Services 19 19 import Time 20 20 import Tracks exposing (Track) ··· 160 160 Return.communicate (Ports.removeTracksFromCache data) 161 161 162 162 163 + replaceTags : ContextForTagsSync -> Manager 164 + replaceTags context model = 165 + model.hypaethralUserData.tracks 166 + |> List.foldr 167 + (\track ( acc, trackIds, tags ) -> 168 + case List.elemIndex track.id trackIds of 169 + Just idx -> 170 + let 171 + newTags = 172 + tags 173 + |> List.getAt idx 174 + |> Maybe.andThen identity 175 + |> Maybe.withDefault track.tags 176 + in 177 + ( { track | tags = newTags } :: acc 178 + , List.removeAt idx trackIds 179 + , List.removeAt idx tags 180 + ) 181 + 182 + Nothing -> 183 + ( track :: acc 184 + , trackIds 185 + , tags 186 + ) 187 + ) 188 + ( [] 189 + , context.trackIds 190 + , context.receivedTags 191 + ) 192 + |> (\( a, _, _ ) -> 193 + User.saveTracksAndUpdateSearchIndex a model 194 + ) 195 + |> andThen 196 + (\m -> 197 + m.hypaethralUserData.tracks 198 + |> Json.Encode.list Tracks.Encoding.encodeTrack 199 + |> (\data -> Common.giveUI Alien.ReloadTracks data m) 200 + ) 201 + 202 + 163 203 search : Json.Value -> Manager 164 204 search encodedSearchTerm = 165 205 encodedSearchTerm ··· 174 214 Return.communicate (Ports.storeTracksInCache data) 175 215 176 216 217 + syncTrackTags : Json.Value -> Manager 218 + syncTrackTags data model = 219 + let 220 + result = 221 + Json.decodeValue 222 + (Json.list <| 223 + Json.map3 224 + (\path sourceId trackId -> 225 + { path = path 226 + , sourceId = sourceId 227 + , trackId = trackId 228 + } 229 + ) 230 + (Json.field "path" Json.string) 231 + (Json.field "sourceId" Json.string) 232 + (Json.field "trackId" Json.string) 233 + ) 234 + data 235 + 236 + ( sources, _ ) = 237 + result 238 + |> Result.withDefault [] 239 + |> List.foldl 240 + (\{ path, sourceId } ( dict, acc ) -> 241 + if List.member sourceId acc then 242 + ( dict, acc ) 243 + 244 + else 245 + case List.find (.id >> (==) sourceId) model.hypaethralUserData.sources of 246 + Just source -> 247 + ( Dict.insert sourceId source dict, sourceId :: acc ) 248 + 249 + Nothing -> 250 + ( dict, sourceId :: acc ) 251 + ) 252 + ( Dict.empty, [] ) 253 + in 254 + case result of 255 + Ok list -> 256 + list 257 + |> List.foldr 258 + (\{ path, sourceId, trackId } ( accPaths, accUrls, accIds ) -> 259 + sources 260 + |> Dict.get sourceId 261 + |> Maybe.map 262 + (tagUrls model.currentTime path) 263 + |> Maybe.map 264 + (\urls -> 265 + ( path :: accPaths, urls :: accUrls, trackId :: accIds ) 266 + ) 267 + |> Maybe.withDefault 268 + ( accPaths, accUrls, accIds ) 269 + ) 270 + ( [], [], [] ) 271 + |> (\( accPaths, accUrls, accIds ) -> 272 + Ports.syncTags 273 + { receivedFilePaths = accPaths 274 + , receivedTags = [] 275 + , trackIds = accIds 276 + , urlsForTags = accUrls 277 + } 278 + ) 279 + |> return model 280 + 281 + Err _ -> 282 + Return.singleton model 283 + 284 + 177 285 updateSearchIndex : Json.Value -> Manager 178 286 updateSearchIndex data = 179 287 Return.communicate (Ports.updateSearchIndex data) ··· 204 312 205 313 Nothing -> 206 314 "<missing-source>" 315 + 316 + 317 + tagUrls : Time.Posix -> String -> Source -> TagUrls 318 + tagUrls currentTime path source = 319 + let 320 + maker = 321 + Sources.Services.makeTrackUrl source.service currentTime source.data 322 + in 323 + { getUrl = maker Get path 324 + , headUrl = maker Head path 325 + }
+2
src/Applications/Brain/Types.elm
··· 53 53 | MakeArtworkTrackUrls Json.Value 54 54 | RemoveTracksBySourceId Json.Value 55 55 | RemoveTracksFromCache Json.Value 56 + | ReplaceTrackTags Processing.ContextForTagsSync 56 57 | Search Json.Value 57 58 | StoreTracksInCache Json.Value 59 + | SyncTrackTags Json.Value 58 60 | UpdateSearchIndex Json.Value 59 61 ----------------------------------------- 60 62 -- 🦉 Nested
+3
src/Applications/UI.elm
··· 622 622 Alien.NotAuthenticated -> 623 623 AuthenticationMsg Authentication.NotAuthenticated 624 624 625 + Alien.ReloadTracks -> 626 + TracksMsg (Tracks.Reload data) 627 + 625 628 Alien.RemoveTracksByPath -> 626 629 TracksMsg (Tracks.RemoveByPaths data) 627 630
+1 -1
src/Applications/UI/Common/State.elm
··· 84 84 showNotification notification model = 85 85 model.notifications 86 86 |> UI.Notifications.show notification 87 - |> Return.map (\n -> { model | isLoading = False, notifications = n }) 87 + |> Return.map (\n -> { model | notifications = n }) 88 88 89 89 90 90 showNotificationWithModel : UI.Model -> Notification Msg -> ( UI.Model, Cmd UI.Msg )
+29 -21
src/Applications/UI/Tracks/ContextMenu.elm
··· 33 33 -> ContextMenu Msg 34 34 trackMenu { cached, cachingInProgress, currentTime, selectedPlaylist, lastModifiedPlaylistName, showAlternativeMenu, sources } tracks = 35 35 if showAlternativeMenu then 36 - [ temporaryUrlActions 36 + [ alternativeMenuActions 37 37 currentTime 38 38 sources 39 39 tracks ··· 62 62 ] 63 63 |> List.concat 64 64 |> ContextMenu 65 + 66 + 67 + alternativeMenuActions : 68 + Time.Posix 69 + -> List Source 70 + -> List IdentifiedTrack 71 + -> List (ContextMenu.Item Msg) 72 + alternativeMenuActions timestamp sources tracks = 73 + case tracks of 74 + [ ( i, t ) ] -> 75 + [ Item 76 + { icon = Icons.link 77 + , label = "Copy temporary url" 78 + , msg = CopyToClipboard (Queue.makeTrackUrl timestamp sources t) 79 + , active = False 80 + } 81 + 82 + -- 83 + , Item 84 + { icon = Icons.sync 85 + , label = "Sync tags" 86 + , msg = TracksMsg (Tracks.SyncTags [ t ]) 87 + , active = False 88 + } 89 + ] 90 + 91 + _ -> 92 + [] 65 93 66 94 67 95 cacheAction : ··· 222 250 , active = False 223 251 } 224 252 ] 225 - 226 - 227 - temporaryUrlActions : 228 - Time.Posix 229 - -> List Source 230 - -> List IdentifiedTrack 231 - -> List (ContextMenu.Item Msg) 232 - temporaryUrlActions timestamp sources tracks = 233 - case tracks of 234 - [ ( i, t ) ] -> 235 - [ Item 236 - { icon = Icons.link 237 - , label = "Copy temporary url" 238 - , msg = CopyToClipboard (Queue.makeTrackUrl timestamp sources t) 239 - , active = False 240 - } 241 - ] 242 - 243 - _ -> 244 - [] 245 253 246 254 247 255
+48 -24
src/Applications/UI/Tracks/State.elm
··· 63 63 ScrollToNowPlaying -> 64 64 scrollToNowPlaying 65 65 66 + SyncTags a -> 67 + syncTags a 68 + 66 69 ToggleCachedOnly -> 67 70 toggleCachedOnly 68 71 ··· 101 104 ----------------------------------------- 102 105 Add a -> 103 106 add a 107 + 108 + Reload a -> 109 + reload a 104 110 105 111 RemoveByPaths a -> 106 112 removeByPaths a ··· 178 184 179 185 add : Json.Value -> Manager 180 186 add encodedTracks model = 181 - model 182 - |> reviseCollection 183 - (encodedTracks 184 - |> Json.decodeValue (Json.list Encoding.trackDecoder) 185 - |> Result.withDefault [] 186 - |> Collection.add 187 - ) 188 - |> andThen search 187 + reviseCollection 188 + (encodedTracks 189 + |> Json.decodeValue (Json.list Encoding.trackDecoder) 190 + |> Result.withDefault [] 191 + |> Collection.add 192 + ) 193 + model 189 194 190 195 191 196 changeScene : Scene -> Manager ··· 406 411 [ indexInList ] 407 412 in 408 413 Return.singleton { model | selectedTrackIndexes = selection } 414 + 415 + 416 + reload : Json.Value -> Manager 417 + reload encodedTracks model = 418 + reviseCollection 419 + (encodedTracks 420 + |> Json.decodeValue (Json.list Encoding.trackDecoder) 421 + |> Result.withDefault model.tracks.untouched 422 + |> Collection.replace 423 + ) 424 + model 409 425 410 426 411 427 removeByPaths : Json.Value -> Manager ··· 724 740 |> Common.showNotification 725 741 726 742 743 + syncTags : List Track -> Manager 744 + syncTags tracks = 745 + tracks 746 + |> Json.Encode.list 747 + (\track -> 748 + Json.Encode.object 749 + [ ( "path", Json.Encode.string track.path ) 750 + , ( "sourceId", Json.Encode.string track.sourceId ) 751 + , ( "trackId", Json.Encode.string track.id ) 752 + ] 753 + ) 754 + |> Alien.broadcast Alien.SyncTrackTags 755 + |> Ports.toBrain 756 + |> Return.communicate 757 + 758 + 727 759 toggleCachedOnly : Manager 728 760 toggleCachedOnly model = 729 761 { model | cachedTracksOnly = not model.cachedTracksOnly } ··· 905 937 906 938 907 939 whenCollectionChanges = 908 - andThen Common.generateDirectoryPlaylists >> whenArrangementChanges 940 + andThen search >> andThen Common.generateDirectoryPlaylists >> whenArrangementChanges 909 941 910 942 911 943 scrollContext : Model -> String ··· 922 954 923 955 importHypaethral : HypaethralData -> Maybe Playlist -> Manager 924 956 importHypaethral data selectedPlaylist model = 925 - let 926 - adjustedModel = 927 - { model 928 - | favourites = data.favourites 929 - , hideDuplicates = Maybe.unwrap False .hideDuplicates data.settings 930 - , selectedPlaylist = selectedPlaylist 931 - , tracks = { emptyCollection | untouched = data.tracks } 932 - } 933 - in 934 - adjustedModel 935 - |> resolveParcel 936 - (adjustedModel 937 - |> makeParcel 938 - |> Collection.identify 939 - ) 957 + { model 958 + | favourites = data.favourites 959 + , hideDuplicates = Maybe.unwrap False .hideDuplicates data.settings 960 + , selectedPlaylist = selectedPlaylist 961 + , tracks = { emptyCollection | untouched = data.tracks } 962 + } 963 + |> reviseCollection Collection.identify 940 964 |> andThen search 941 965 |> (case model.searchTerm of 942 966 Just _ ->
+2
src/Applications/UI/Tracks/Types.elm
··· 17 17 | Harvest 18 18 | MarkAsSelected Int { shiftKey : Bool } 19 19 | ScrollToNowPlaying 20 + | SyncTags (List Track) 20 21 | ToggleCachedOnly 21 22 | ToggleFavouritesOnly 22 23 | ToggleHideDuplicates ··· 36 37 -- Collection 37 38 ----------------------------------------- 38 39 | Add Json.Value 40 + | Reload Json.Value 39 41 | RemoveByPaths Json.Value 40 42 | RemoveBySourceId String 41 43 | SortBy SortBy
+8
src/Javascript/Brain/index.js
··· 236 236 app.ports.receiveTags.send(newContext) 237 237 }) 238 238 }) 239 + 240 + 241 + app.ports.syncTags.subscribe(context => { 242 + processing.processContext(context).then(newContext => { 243 + console.log(newContext) 244 + app.ports.replaceTags.send(newContext) 245 + }) 246 + })
+4
src/Library/Alien.elm
··· 54 54 | StopProcessing 55 55 | StoreTracksInCache 56 56 | SyncHypaethralData 57 + | SyncTrackTags 57 58 | ToCache 58 59 | UpdateEncryptionKey 59 60 ----------------------------------------- ··· 68 69 | LoadHypaethralUserData 69 70 | MissingSecretKey 70 71 | NotAuthenticated 72 + | ReloadTracks 71 73 | RemoveTracksByPath 72 74 | ReportProcessingError 73 75 | ReportProcessingProgress ··· 108 110 , ( "STOP_PROCESSING", StopProcessing ) 109 111 , ( "STORE_TRACKS_IN_CACHE", StoreTracksInCache ) 110 112 , ( "SYNC_HYPAETHRAL_DATA", SyncHypaethralData ) 113 + , ( "SYNC_TRACK_TAGS", SyncTrackTags ) 111 114 , ( "TO_CACHE", ToCache ) 112 115 , ( "UPDATE_ENCRYPTION_KEY", UpdateEncryptionKey ) 113 116 ··· 123 126 , ( "LOAD_HYPAETHRAL_USER_DATA", LoadHypaethralUserData ) 124 127 , ( "MISSING_SECRET_KEY", MissingSecretKey ) 125 128 , ( "NOT_AUTHENTICATED", NotAuthenticated ) 129 + , ( "RELOAD_TRACKS", ReloadTracks ) 126 130 , ( "REMOVE_TRACKS_BY_PATH", RemoveTracksByPath ) 127 131 , ( "REPORT_PROCESSING_ERROR", ReportProcessingError ) 128 132 , ( "REPORT_PROCESSING_PROGRESS", ReportProcessingProgress )
+9 -1
src/Library/Sources/Processing.elm
··· 1 - module Sources.Processing exposing (Arguments, Context, ContextForTags, HttpMethod(..), Marker(..), PrepationAnswer, Status(..), TagUrls, TreeAnswer, httpMethod) 1 + module Sources.Processing exposing (..) 2 2 3 3 import Sources exposing (Source, SourceData) 4 4 import Tracks exposing (Tags, Track) ··· 60 60 , receivedFilePaths : List String 61 61 , receivedTags : List (Maybe Tags) 62 62 , sourceId : String 63 + , urlsForTags : List TagUrls 64 + } 65 + 66 + 67 + type alias ContextForTagsSync = 68 + { receivedFilePaths : List String 69 + , receivedTags : List (Maybe Tags) 70 + , trackIds : List String 63 71 , urlsForTags : List TagUrls 64 72 } 65 73
+9 -1
src/Library/Tracks/Collection.elm
··· 1 - module Tracks.Collection exposing (add, arrange, harvest, identifiedTracksChanged, identify, map, tracksChanged) 1 + module Tracks.Collection exposing (add, arrange, harvest, identifiedTracksChanged, identify, map, replace, tracksChanged) 2 2 3 3 import Tracks exposing (IdentifiedTrack, Parcel, Track, emptyCollection) 4 4 import Tracks.Collection.Internal as Internal ··· 43 43 identify 44 44 ( deps 45 45 , { emptyCollection | untouched = untouched ++ tracks } 46 + ) 47 + 48 + 49 + replace : List Track -> Parcel -> Parcel 50 + replace tracks ( deps, { untouched } ) = 51 + identify 52 + ( deps 53 + , { emptyCollection | untouched = tracks } 46 54 ) 47 55 48 56