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 last.fm integration

+467 -92
+2 -1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 - ## 2.3.2 3 + ## 2.4.0 4 4 5 + - **Adds Last.fm integration** 5 6 - Fixes Amazon S3 processing issue in Chrome (max call stack) 6 7 7 8 ## 2.3.1
+3 -1
elm.json
··· 43 43 "rtfeldman/elm-hex": "1.0.0", 44 44 "ryannhg/date-format": "2.3.0", 45 45 "truqu/elm-base64": "2.0.4", 46 + "truqu/elm-md5": "1.1.0", 46 47 "wernerdegroot/listzipper": "4.0.0", 47 48 "ymtszw/elm-xml-decode": "3.1.0" 48 49 }, ··· 50 51 "elm/parser": "1.1.0", 51 52 "fredcy/elm-parseint": "2.0.1", 52 53 "jinjor/elm-xml-parser": "2.0.0", 53 - "pzp1997/assoc-list": "1.0.0" 54 + "pzp1997/assoc-list": "1.0.0", 55 + "zwilias/elm-utf-tools": "2.0.1" 54 56 } 55 57 }, 56 58 "test-dependencies": {
+78 -2
src/Applications/UI.elm
··· 28 28 import Json.Decode 29 29 import Json.Encode 30 30 import Keyboard 31 + import LastFm 31 32 import List.Ext as List 32 33 import List.Extra as List 33 34 import Maybe.Extra as Maybe ··· 42 43 import Sources.Encoding as Sources 43 44 import Sources.Services.Dropbox 44 45 import Sources.Services.Google 46 + import String.Ext as String 45 47 import Task 46 48 import Time 47 49 import Tracks ··· 123 125 , isOnline : Bool 124 126 , isTouchDevice : Bool 125 127 , isUpgrading : Bool 128 + , lastFm : LastFm.Model 126 129 , navKey : Nav.Key 127 130 , notifications : UI.Notifications.Model 128 131 , page : Page ··· 181 184 , isOnline = flags.isOnline 182 185 , isTouchDevice = False 183 186 , isUpgrading = flags.upgrade 187 + , lastFm = LastFm.initialModel url 184 188 , navKey = key 185 189 , notifications = [] 186 190 , page = page ··· 231 235 ) 232 236 |> addCommand 233 237 (Task.perform SetCurrentTime Time.now) 238 + |> addCommand 239 + (LastFm.authenticationCommand GotLastFmSession url) 234 240 235 241 236 242 ··· 293 299 ----------------------------------------- 294 300 | Import File 295 301 | ImportJson String 302 + ----------------------------------------- 303 + -- Last.fm 304 + ----------------------------------------- 305 + | GotLastFmSession (Result Http.Error String) 306 + | Scrobble { duration : Float, timestamp : Int, trackId : String } 296 307 ----------------------------------------- 297 308 -- Page Transitions 298 309 ----------------------------------------- ··· 777 788 -- Save all the imported data 778 789 ----------------------------- 779 790 |> saveAllHypaethralData 791 + 792 + ----------------------------------------- 793 + -- Last.fm 794 + ----------------------------------------- 795 + GotLastFmSession (Ok sessionKey) -> 796 + { model | lastFm = LastFm.gotSessionKey sessionKey model.lastFm } 797 + |> showNotification 798 + (Notifications.success "Connected successfully with Last.fm") 799 + |> andThen 800 + (translateReply SaveSettings) 801 + 802 + GotLastFmSession (Err _) -> 803 + showNotification 804 + (Notifications.stickyError "Could not connect with Last.fm") 805 + { model | lastFm = LastFm.failedToAuthenticate model.lastFm } 806 + 807 + Scrobble { duration, timestamp, trackId } -> 808 + case model.tracks.nowPlaying of 809 + Just ( _, track ) -> 810 + if trackId == track.id then 811 + ( model 812 + , LastFm.scrobble model.lastFm duration timestamp track Bypass 813 + ) 814 + 815 + else 816 + return model 817 + 818 + Nothing -> 819 + return model 780 820 781 821 ----------------------------------------- 782 822 -- Page Transitions ··· 1129 1169 return { model | contextMenu = Just (Tracks.viewMenu model.tracks.cachedOnly maybeGrouping coordinates) } 1130 1170 1131 1171 ----------------------------------------- 1172 + -- Last.fm 1173 + ----------------------------------------- 1174 + ConnectLastFm -> 1175 + model.url 1176 + |> Common.urlOrigin 1177 + |> String.addSuffix "?action=authenticate/lastfm" 1178 + |> Url.percentEncode 1179 + |> String.append "&cb=" 1180 + |> String.append 1181 + (String.append 1182 + "http://www.last.fm/api/auth/?api_key=" 1183 + LastFm.apiKey 1184 + ) 1185 + |> Nav.load 1186 + |> returnWithModel model 1187 + 1188 + DisconnectLastFm -> 1189 + translateReply 1190 + SaveSettings 1191 + { model | lastFm = LastFm.disconnect model.lastFm } 1192 + 1193 + ----------------------------------------- 1132 1194 -- Notifications 1133 1195 ----------------------------------------- 1134 1196 DismissNotification options -> ··· 1330 1392 model 1331 1393 |> update (TracksMsg <| Tracks.SetNowPlaying nowPlaying) 1332 1394 |> addCommand portCmd 1395 + |> (case nowPlaying of 1396 + Just identifiedTrack -> 1397 + addCommand (LastFm.nowPlaying model.lastFm identifiedTrack Bypass) 1398 + 1399 + Nothing -> 1400 + identity 1401 + ) 1333 1402 1334 1403 AddToQueue { inFront, tracks } -> 1335 1404 (if inFront then ··· 1880 1949 , Ports.downloadTracksFinished (\_ -> DownloadTracksFinished) 1881 1950 , Ports.indicateTouchDevice (\_ -> SetIsTouchDevice True) 1882 1951 , Ports.preferredColorSchemaChanged PreferredColorSchemaChanged 1883 - , Ports.showErrorNotification (Notifications.error >> ShowNotification) 1952 + , Ports.scrobble Scrobble 1884 1953 , Ports.setAverageBackgroundColor (Backdrop.BackgroundColor >> BackdropMsg) 1885 1954 , Ports.setIsOnline SetIsOnline 1955 + , Ports.showErrorNotification (Notifications.error >> ShowNotification) 1886 1956 1887 1957 -- 1888 1958 , Sub.map KeyboardMsg Keyboard.subscriptions ··· 2172 2242 { authenticationMethod = Authentication.extractMethod model.authentication 2173 2243 , chosenBackgroundImage = model.backdrop.chosen 2174 2244 , hideDuplicateTracks = model.tracks.hideDuplicates 2245 + , lastFm = model.lastFm 2175 2246 , processAutomatically = model.processAutomatically 2176 2247 , rememberProgress = model.rememberProgress 2177 2248 } ··· 2330 2401 2331 2402 2332 2403 gatherSettings : Model -> Settings.Settings 2333 - gatherSettings { backdrop, processAutomatically, rememberProgress, tracks } = 2404 + gatherSettings { backdrop, lastFm, processAutomatically, rememberProgress, tracks } = 2334 2405 { backgroundImage = backdrop.chosen 2335 2406 , hideDuplicates = tracks.hideDuplicates 2407 + , lastFm = lastFm.sessionKey 2336 2408 , processAutomatically = processAutomatically 2337 2409 , rememberProgress = rememberProgress 2338 2410 } ··· 2366 2438 2367 2439 ( tracksModel, tracksCmd, tracksReplies ) = 2368 2440 Tracks.importHypaethral model.tracks data selectedPlaylist 2441 + 2442 + lastFmModel = 2443 + model.lastFm 2369 2444 in 2370 2445 ( { model 2371 2446 | backdrop = backdropModel ··· 2375 2450 , tracks = tracksModel 2376 2451 2377 2452 -- 2453 + , lastFm = { lastFmModel | sessionKey = Maybe.andThen .lastFm data.settings } 2378 2454 , processAutomatically = Maybe.unwrap True .processAutomatically data.settings 2379 2455 , rememberProgress = Maybe.unwrap True .rememberProgress data.settings 2380 2456 }
+4 -1
src/Applications/UI/Ports.elm
··· 73 73 port requestStop : (() -> msg) -> Sub msg 74 74 75 75 76 - port showErrorNotification : (String -> msg) -> Sub msg 76 + port scrobble : ({ duration : Float, timestamp : Int, trackId : String } -> msg) -> Sub msg 77 77 78 78 79 79 port setAudioPosition : (Float -> msg) -> Sub msg ··· 95 95 96 96 97 97 port setIsOnline : (Bool -> msg) -> Sub msg 98 + 99 + 100 + port showErrorNotification : (String -> msg) -> Sub msg 98 101 99 102 100 103
+5
src/Applications/UI/Reply.elm
··· 49 49 | ShowTracksContextMenu Coordinates { alt : Bool } (List IdentifiedTrack) 50 50 | ShowTracksViewMenu Coordinates (Maybe Tracks.Grouping) 51 51 ----------------------------------------- 52 + -- Last.fm 53 + ----------------------------------------- 54 + | ConnectLastFm 55 + | DisconnectLastFm 56 + ----------------------------------------- 52 57 -- Notifications 53 58 ----------------------------------------- 54 59 | DismissNotification { id : Int }
+151 -84
src/Applications/UI/Settings.elm
··· 4 4 import Conditional exposing (ifThenElse) 5 5 import Css.Classes as C 6 6 import Html exposing (Html, text) 7 - import Html.Attributes exposing (style) 8 - import Html.Events exposing (onClick) 7 + import Html.Attributes exposing (..) 8 + import Html.Events exposing (onClick, onInput) 9 9 import Html.Lazy 10 + import LastFm 10 11 import Material.Icons as Icons 11 12 import Material.Icons.Types exposing (Coloring(..)) 13 + import Maybe.Extra as Maybe 12 14 import Settings 13 15 import UI.Backdrop as Backdrop exposing (backgroundPositioning) 14 16 import UI.Kit ··· 28 30 { authenticationMethod : Maybe User.Layer.Method 29 31 , chosenBackgroundImage : Maybe String 30 32 , hideDuplicateTracks : Bool 33 + , lastFm : LastFm.Model 31 34 , processAutomatically : Bool 32 35 , rememberProgress : Bool 33 36 } ··· 70 73 ----------------------------------------- 71 74 -- Content 72 75 ----------------------------------------- 73 - , UI.Kit.canister 74 - [ UI.Kit.h1 "Settings" 75 - , [ text "Changes are saved automatically." 76 - , lineBreak 77 - , text "You're storing the data for this application " 78 - , case deps.authenticationMethod of 79 - Just Blockstack -> 80 - text "on Blockstack." 76 + , deps 77 + |> content 78 + |> chunk [ C.pb_4 ] 79 + |> List.singleton 80 + |> UI.Kit.canister 81 + ] 81 82 82 - Just (Dropbox _) -> 83 - text "on Dropbox." 84 83 85 - Just (Ipfs _) -> 86 - text "on IPFS." 84 + content : Dependencies -> List (Html Reply) 85 + content deps = 86 + [ ----------------------------------------- 87 + -- Title 88 + ----------------------------------------- 89 + UI.Kit.h1 "Settings" 87 90 88 - Just Local -> 89 - text "in this browser." 91 + ----------------------------------------- 92 + -- Intro 93 + ----------------------------------------- 94 + , [ text "Changes are saved automatically." 95 + , lineBreak 96 + , text "You're storing the data for this application " 97 + , case deps.authenticationMethod of 98 + Just Blockstack -> 99 + text "on Blockstack." 90 100 91 - Just (RemoteStorage _) -> 92 - text "on a RemoteStorage server." 101 + Just (Dropbox _) -> 102 + text "on Dropbox." 93 103 94 - Just (Textile _) -> 95 - text "on Textile." 104 + Just (Ipfs _) -> 105 + text "on IPFS." 96 106 97 - Nothing -> 98 - text "on nothing, wtf?" 107 + Just Local -> 108 + text "in this browser." 99 109 100 - -- Change passphrase (if applicable) 101 - , case deps.authenticationMethod of 102 - Just Blockstack -> 103 - nothing 110 + Just (RemoteStorage _) -> 111 + text "on a RemoteStorage server." 104 112 105 - Just (Dropbox d) -> 106 - changePassphrase (Dropbox d) 113 + Just (Textile _) -> 114 + text "on Textile." 107 115 108 - Just (Ipfs i) -> 109 - changePassphrase (Ipfs i) 116 + Nothing -> 117 + text "on nothing, wtf?" 110 118 111 - Just Local -> 112 - changePassphrase Local 119 + -- Change passphrase (if applicable) 120 + , case deps.authenticationMethod of 121 + Just Blockstack -> 122 + nothing 123 + 124 + Just (Dropbox d) -> 125 + changePassphrase (Dropbox d) 126 + 127 + Just (Ipfs i) -> 128 + changePassphrase (Ipfs i) 129 + 130 + Just Local -> 131 + changePassphrase Local 132 + 133 + Just (RemoteStorage r) -> 134 + changePassphrase (RemoteStorage r) 135 + 136 + Just (Textile _) -> 137 + nothing 113 138 114 - Just (RemoteStorage r) -> 115 - changePassphrase (RemoteStorage r) 139 + Nothing -> 140 + nothing 141 + ] 142 + |> raw 143 + |> UI.Kit.intro 116 144 117 - Just (Textile _) -> 118 - nothing 145 + ----------------------------------------- 146 + -- Background 147 + ----------------------------------------- 148 + , chunk 149 + [ C.mt_8 ] 150 + [ label "Background Image" 151 + , Html.Lazy.lazy backgroundImage deps.chosenBackgroundImage 152 + ] 119 153 120 - Nothing -> 121 - nothing 122 - ] 123 - |> raw 124 - |> UI.Kit.intro 154 + ----------------------------------------- 155 + -- Row 1 156 + ----------------------------------------- 157 + , chunk 158 + [ C.flex, C.flex_wrap, C.pt_2 ] 159 + [ chunk 160 + [ C.w_full, C.md__w_half ] 161 + [ label "Downloaded tracks" 162 + , UI.Kit.buttonWithColor 163 + UI.Kit.Gray 164 + UI.Kit.Normal 165 + ClearTracksCache 166 + (text "Clear cache") 167 + ] 125 168 126 - -- Clear cache 127 - -------------- 169 + -- Last.fm 170 + ---------- 128 171 , chunk 129 - [ C.flex, C.flex_wrap ] 130 - [ chunk 131 - [ C.w_full, C.md__w_half ] 132 - [ label "Downloaded tracks" 133 - , UI.Kit.buttonWithColor 134 - UI.Kit.Gray 135 - UI.Kit.Normal 136 - ClearTracksCache 137 - (text "Clear cache") 138 - ] 139 - , chunk 140 - [ C.w_full, C.md__w_half ] 141 - [ label "Hide Duplicates" 142 - , UI.Kit.checkbox 143 - { checked = deps.hideDuplicateTracks 144 - , toggleMsg = ToggleHideDuplicates 145 - } 146 - ] 172 + [ C.w_half ] 173 + [ label "Last.fm scrobbling" 174 + 175 + -- 176 + , case ( deps.lastFm.authenticating, deps.lastFm.sessionKey ) of 177 + ( _, Just _ ) -> 178 + UI.Kit.checkbox 179 + { checked = True 180 + , toggleMsg = DisconnectLastFm 181 + } 182 + 183 + ( True, Nothing ) -> 184 + UI.Kit.buttonWithColor 185 + UI.Kit.Gray 186 + UI.Kit.Normal 187 + Shunt 188 + (text "Connecting") 189 + 190 + ( False, Nothing ) -> 191 + UI.Kit.buttonWithColor 192 + UI.Kit.Gray 193 + UI.Kit.Normal 194 + ConnectLastFm 195 + (text "Connect") 147 196 ] 197 + ] 148 198 149 - -- Check it 150 - ----------- 199 + ----------------------------------------- 200 + -- Row 2 201 + ----------------------------------------- 202 + , chunk 203 + [ C.flex, C.flex_wrap ] 204 + [ chunk 205 + [ C.w_full, C.md__w_half ] 206 + [ label "Hide Duplicates" 207 + , UI.Kit.checkbox 208 + { checked = deps.hideDuplicateTracks 209 + , toggleMsg = ToggleHideDuplicates 210 + } 211 + ] 151 212 , chunk 152 - [ C.flex, C.flex_wrap ] 153 - [ chunk 154 - [ C.w_full, C.md__w_half ] 155 - [ label "Process sources automatically" 156 - , UI.Kit.checkbox 157 - { checked = deps.processAutomatically 158 - , toggleMsg = ToggleProcessAutomatically 159 - } 160 - ] 161 - , chunk 162 - [ C.w_full, C.md__w_half ] 163 - [ label "Remember position on long tracks" 164 - , UI.Kit.checkbox 165 - { checked = deps.rememberProgress 166 - , toggleMsg = ToggleRememberProgress 167 - } 168 - ] 213 + [ C.w_full, C.md__w_half ] 214 + [ label "Process sources automatically" 215 + , UI.Kit.checkbox 216 + { checked = deps.processAutomatically 217 + , toggleMsg = ToggleProcessAutomatically 218 + } 169 219 ] 220 + ] 170 221 171 - -- Background image 172 - ------------------- 173 - , label "Background Image" 174 - , Html.Lazy.lazy backgroundImage deps.chosenBackgroundImage 222 + ----------------------------------------- 223 + -- Row 3 224 + ----------------------------------------- 225 + , chunk 226 + [ C.flex, C.flex_wrap ] 227 + [ chunk 228 + [ C.w_full, C.md__w_half ] 229 + [ label "Remember position on long tracks" 230 + , UI.Kit.checkbox 231 + { checked = deps.rememberProgress 232 + , toggleMsg = ToggleRememberProgress 233 + } 234 + ] 175 235 ] 176 236 ] 177 237 ··· 181 241 chunk 182 242 [ C.mb_3, C.mt_6, C.pb_px ] 183 243 [ UI.Kit.label [] l ] 244 + 245 + 246 + textField : List (Html.Attribute Reply) -> Html Reply 247 + textField attributes = 248 + chunk 249 + [ C.max_w_xs ] 250 + [ UI.Kit.textField attributes ] 184 251 185 252 186 253
+19 -2
src/Javascript/audio-engine.js
··· 155 155 // reset 156 156 orchestrion.app.ports.setAudioHasStalled.send(false) 157 157 orchestrion.app.ports.setAudioPosition.send(0) 158 + clearTimeout(orchestrion.scrobbleTimeout) 158 159 clearTimeout(orchestrion.unstallTimeout) 159 160 timesStalled = 1 160 161 ··· 409 410 } 410 411 411 412 412 - function audioPlayEvent(_event) { 413 + function audioPlayEvent(event) { 413 414 this.app.ports.setAudioIsPlaying.send(true) 414 - if (navigator.mediaSession) navigator.mediaSession.playbackState = "playing" 415 + 416 + if (navigator.mediaSession) { 417 + navigator.mediaSession.playbackState = "playing" 418 + } 419 + 420 + if (event.target.duration && event.target.duration >= 30) { 421 + const timestamp = Math.floor( Date.now() / 1000 ) 422 + const scrobbleTimeoutDuration = Math.min(240 + 0.5, event.target.duration / 1.95) 423 + 424 + this.scrobbleTimeout = setTimeout(_ => { 425 + this.app.ports.scrobble.send({ 426 + duration: event.target.duration, 427 + timestamp: timestamp, 428 + trackId: event.target.getAttribute("rel") 429 + }) 430 + }, scrobbleTimeoutDuration * 1000) 431 + } 415 432 } 416 433 417 434
+2
src/Javascript/index.js
··· 84 84 orchestrion.activeQueueItem = item 85 85 orchestrion.audio = null 86 86 87 + clearTimeout(orchestrion.scrobbleTimeout) 88 + 87 89 audioEngine.usesSingleAudioNode() 88 90 ? false 89 91 : audioEngine.removeOlderAudioElements(timestampInMilliseconds)
+193
src/Library/LastFm.elm
··· 1 + module LastFm exposing (..) 2 + 3 + import Common 4 + import Http 5 + import Json.Decode as Json 6 + import List.Ext as List 7 + import MD5 8 + import String.Ext as String 9 + import Tracks exposing (IdentifiedTrack, Track) 10 + import Tuple.Ext as Tuple 11 + import Url exposing (Url) 12 + import Url.Ext as Url 13 + 14 + 15 + 16 + -- 🏔 17 + 18 + 19 + apiKey = 20 + "4f0fe85b67baef8bb7d008a8754a95e5" 21 + 22 + 23 + apiUrl = 24 + "http://ws.audioscrobbler.com/2.0" 25 + 26 + 27 + notSoSecret = 28 + "0cec3ca0f58e04a5082f1131aba1e0d3" 29 + 30 + 31 + 32 + -- 🌳 33 + 34 + 35 + type alias Model = 36 + { authenticating : Bool 37 + , sessionKey : Maybe String 38 + } 39 + 40 + 41 + initialModel : Url -> Model 42 + initialModel url = 43 + case Url.action url of 44 + [ "authenticate", "lastfm" ] -> 45 + { authenticating = True 46 + , sessionKey = Nothing 47 + } 48 + 49 + _ -> 50 + { authenticating = False 51 + , sessionKey = Nothing 52 + } 53 + 54 + 55 + authenticationCommand : (Result Http.Error String -> msg) -> Url -> Cmd msg 56 + authenticationCommand msg url = 57 + case Url.action url of 58 + [ "authenticate", "lastfm" ] -> 59 + case Url.extractQueryParam "token" url of 60 + Just token -> 61 + Http.get 62 + { url = 63 + authenticatedUrl 64 + [ ( "method", "auth.getSession" ) 65 + , ( "token", token ) 66 + ] 67 + , expect = 68 + Json.string 69 + |> Json.at [ "session", "key" ] 70 + |> Http.expectJson msg 71 + } 72 + 73 + Nothing -> 74 + Cmd.none 75 + 76 + _ -> 77 + Cmd.none 78 + 79 + 80 + 81 + -- 📣 82 + 83 + 84 + disconnect : Model -> Model 85 + disconnect model = 86 + { model | sessionKey = Nothing } 87 + 88 + 89 + failedToAuthenticate : Model -> Model 90 + failedToAuthenticate model = 91 + { model | authenticating = False } 92 + 93 + 94 + gotSessionKey : String -> Model -> Model 95 + gotSessionKey sessionKey model = 96 + { model | sessionKey = Just sessionKey } 97 + 98 + 99 + 100 + -- 🎵 101 + 102 + 103 + nowPlaying : Model -> IdentifiedTrack -> msg -> Cmd msg 104 + nowPlaying model ( _, track ) msg = 105 + case model.sessionKey of 106 + Just sessionKey -> 107 + Http.post 108 + { url = 109 + apiUrl 110 + , body = 111 + authenticatedBody 112 + [ ( "artist", track.tags.artist ) 113 + , ( "track", track.tags.title ) 114 + , ( "album", track.tags.album ) 115 + , ( "trackNumber", String.fromInt track.tags.nr ) 116 + 117 + -- 118 + , ( "method", "track.updateNowPlaying" ) 119 + , ( "sk", sessionKey ) 120 + ] 121 + , expect = 122 + Http.expectWhatever (always msg) 123 + } 124 + 125 + Nothing -> 126 + Cmd.none 127 + 128 + 129 + scrobble : Model -> Float -> Int -> Track -> msg -> Cmd msg 130 + scrobble model duration timestamp track msg = 131 + case model.sessionKey of 132 + Just sessionKey -> 133 + Http.post 134 + { url = 135 + apiUrl 136 + , body = 137 + authenticatedBody 138 + [ ( "artist", track.tags.artist ) 139 + , ( "track", track.tags.title ) 140 + , ( "album", track.tags.album ) 141 + , ( "trackNumber", String.fromInt track.tags.nr ) 142 + 143 + -- 144 + , ( "duration", String.fromInt <| round duration ) 145 + , ( "method", "track.scrobble" ) 146 + , ( "sk", sessionKey ) 147 + , ( "timestamp", String.fromInt timestamp ) 148 + ] 149 + , expect = 150 + Http.expectWhatever (always msg) 151 + } 152 + 153 + Nothing -> 154 + Cmd.none 155 + 156 + 157 + 158 + -- 🔱 159 + 160 + 161 + authenticatedBody : List ( String, String ) -> Http.Body 162 + authenticatedBody params = 163 + params 164 + |> authenticatedParams 165 + |> Common.queryString 166 + |> String.dropLeft 1 167 + |> Http.stringBody "application/x-www-form-urlencoded" 168 + 169 + 170 + authenticatedUrl : List ( String, String ) -> String 171 + authenticatedUrl params = 172 + params 173 + |> authenticatedParams 174 + |> Common.queryString 175 + |> String.append apiUrl 176 + 177 + 178 + authenticatedParams : List ( String, String ) -> List ( String, String ) 179 + authenticatedParams params = 180 + let 181 + extendedParams = 182 + ( "api_key", apiKey ) :: params 183 + in 184 + extendedParams 185 + |> List.sortBy Tuple.first 186 + |> List.map (Tuple.uncurry String.append) 187 + |> String.concat 188 + |> String.addSuffix notSoSecret 189 + |> MD5.hex 190 + |> Tuple.pair "api_sig" 191 + |> List.addTo extendedParams 192 + |> (::) ( "format", "json" ) 193 + |> List.sortBy Tuple.first
+10 -1
src/Library/Settings.elm
··· 13 13 type alias Settings = 14 14 { backgroundImage : Maybe String 15 15 , hideDuplicates : Bool 16 + , lastFm : Maybe String 16 17 , processAutomatically : Bool 17 18 , rememberProgress : Bool 18 19 } 19 20 20 21 21 22 22 - -- 🔱 23 + -- ENCODING 23 24 24 25 25 26 encode : Settings -> Json.Value ··· 30 31 ) 31 32 , ( "hideDuplicates" 32 33 , Json.Encode.bool settings.hideDuplicates 34 + ) 35 + , ( "lastFm" 36 + , Maybe.unwrap Json.Encode.null Json.Encode.string settings.lastFm 33 37 ) 34 38 , ( "processAutomatically" 35 39 , Json.Encode.bool settings.processAutomatically ··· 40 44 ] 41 45 42 46 47 + 48 + -- DECODING 49 + 50 + 43 51 decoder : Json.Decoder Settings 44 52 decoder = 45 53 Json.succeed Settings 46 54 |> optional "backgroundImage" (Json.maybe Json.string) Nothing 47 55 |> optional "hideDuplicates" Json.bool False 56 + |> optional "lastFm" (Json.maybe Json.string) Nothing 48 57 |> optional "processAutomatically" Json.bool True 49 58 |> optional "rememberProgress" Json.bool True