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.

Merge pull request #355 from icidasset/access-token-management

Access token management

authored by

Steven Vandevelde and committed by
GitHub
b9377241 d73c03fb

+447 -220
+1
CHANGELOG.md
··· 4 4 5 5 - **Native builds with [Tauri](https://tauri.app/)**. 6 6 - Fixes playback issue with Google Drive on Safari/iOS. 7 + - Improves Google Drive support, access tokens are now refreshed when needed (before it only refreshed on source processing) 7 8 8 9 9 10 ## 3.1.1
+15 -15
flake.lock
··· 2 2 "nodes": { 3 3 "flake-utils": { 4 4 "locked": { 5 - "lastModified": 1656928814, 6 - "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", 5 + "lastModified": 1659877975, 6 + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", 7 7 "owner": "numtide", 8 8 "repo": "flake-utils", 9 - "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", 9 + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", 10 10 "type": "github" 11 11 }, 12 12 "original": { ··· 17 17 }, 18 18 "flake-utils_2": { 19 19 "locked": { 20 - "lastModified": 1656065134, 21 - "narHash": "sha256-oc6E6ByIw3oJaIyc67maaFcnjYOz1mMcOtHxbEf9NwQ=", 20 + "lastModified": 1656928814, 21 + "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", 22 22 "owner": "numtide", 23 23 "repo": "flake-utils", 24 - "rev": "bee6a7250dd1b01844a2de7e02e4df7d8a0a206c", 24 + "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", 25 25 "type": "github" 26 26 }, 27 27 "original": { ··· 32 32 }, 33 33 "nixpkgs": { 34 34 "locked": { 35 - "lastModified": 1658380158, 36 - "narHash": "sha256-DBunkegKWlxPZiOcw3/SNIFg93amkdGIy2g0y/jDpHg=", 35 + "lastModified": 1662019588, 36 + "narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=", 37 37 "owner": "NixOS", 38 38 "repo": "nixpkgs", 39 - "rev": "a65b5b3f5504b8b89c196aba733bdf2b0bd13c16", 39 + "rev": "2da64a81275b68fdad38af669afeda43d401e94b", 40 40 "type": "github" 41 41 }, 42 42 "original": { ··· 48 48 }, 49 49 "nixpkgs_2": { 50 50 "locked": { 51 - "lastModified": 1656401090, 52 - "narHash": "sha256-bUS2nfQsvTQW2z8SK7oEFSElbmoBahOPtbXPm0AL3I4=", 51 + "lastModified": 1659102345, 52 + "narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=", 53 53 "owner": "NixOS", 54 54 "repo": "nixpkgs", 55 - "rev": "16de63fcc54e88b9a106a603038dd5dd2feb21eb", 55 + "rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7", 56 56 "type": "github" 57 57 }, 58 58 "original": { ··· 75 75 "nixpkgs": "nixpkgs_2" 76 76 }, 77 77 "locked": { 78 - "lastModified": 1658544517, 79 - "narHash": "sha256-ipu69vA0a6AcWZqnHLzeRhvnVZbURXALHLIsqQWtJe4=", 78 + "lastModified": 1662173844, 79 + "narHash": "sha256-+ZgW98Y8fZkgFSylE+Mzalumw+kw3SVivZznbJqQaj8=", 80 80 "owner": "oxalica", 81 81 "repo": "rust-overlay", 82 - "rev": "4bd340885e39e0625fc6dda8a5a9d13c921ebb96", 82 + "rev": "8ac6d40380dc4ec86f1ff591d5c14c8ae1d77a18", 83 83 "type": "github" 84 84 }, 85 85 "original": {
+1 -22
nix/shell.nix
··· 9 9 rustc = rust; 10 10 }; 11 11 12 - # Trying to fix an issue with `watchexec` on MacOS 13 - # https://github.com/h4llow3En/mac-notification-sys/issues/28 14 - # Nix uses old Apple SDKs? Not sure, but definitely a Nix issue. 15 - watchexec = rustPlatform.buildRustPackage rec { 16 - pname = "watchexec"; 17 - version = "1.20.4"; 18 - 19 - cargoSha256 = "sha256-YM+Zm3wFp3Lsx5LmyjGwZywV/SZjriL6JMDO1l0tNf4="; 20 - 21 - src = fetchFromGitHub { 22 - owner = pname; 23 - repo = pname; 24 - rev = "cli-v${version}"; 25 - sha256 = "sha256-se3iqz+qjwf71wvHQhCWYryEdUc+kY0Q0ZTg4i1ayNI="; 26 - }; 27 - 28 - nativeBuildInputs = lib.optionals 29 - stdenv.isDarwin 30 - (with darwin.apple_sdk_11_0.frameworks; [ Cocoa Foundation ]); 31 - }; 32 - 33 12 # Wraps 34 13 # ----- 35 14 # Inspired by https://www.tweag.io/blog/2022-06-02-haskell-stack-nix-shell/ 36 15 37 16 stack-wrapped = symlinkJoin { 38 17 name = "stack"; 39 - paths = [ stack ]; 18 + paths = [ haskellPackages.stack ]; 40 19 buildInputs = [ makeWrapper ]; 41 20 postBuild = '' 42 21 wrapProgram $out/bin/stack \
+7
src/Applications/Brain.elm
··· 144 144 ----------------------------------------- 145 145 -- 📭 Other 146 146 ----------------------------------------- 147 + RefreshedAccessToken a -> 148 + Other.refreshedAccessToken a 149 + 147 150 SetCurrentTime a -> 148 151 Other.setCurrentTime a 149 152 ··· 160 163 Sub.batch 161 164 [ Ports.fromAlien alien 162 165 , Ports.makeArtworkTrackUrls MakeArtworkTrackUrls 166 + , Ports.refreshedAccessToken RefreshedAccessToken 163 167 , Ports.receiveSearchResults GotSearchResults 164 168 , Ports.receiveTags (ProcessingMsg << Processing.TagsStep) 165 169 , Ports.replaceTags ReplaceTrackTags ··· 229 233 230 234 Alien.ProcessSources -> 231 235 ProcessingMsg (Processing.Process data) 236 + 237 + Alien.RefreshedAccessToken -> 238 + RefreshedAccessToken data 232 239 233 240 Alien.RemoveEncryptionKey -> 234 241 UserMsg User.RemoveEncryptionKey
+38 -1
src/Applications/Brain/Other/State.elm
··· 4 4 import Brain.Common.State as Common 5 5 import Brain.Ports as Ports 6 6 import Brain.Types exposing (..) 7 + import Dict 7 8 import Json.Decode as Json 8 - import Return 9 + import List.Extra as List 10 + import Return exposing (return) 9 11 import Return.Ext as Return 12 + import Sources exposing (Service(..)) 13 + import Sources.Encoding 14 + import Sources.Refresh.AccessToken 10 15 import Time 11 16 12 17 13 18 14 19 -- 🔱 20 + 21 + 22 + refreshedAccessToken : Json.Value -> Manager 23 + refreshedAccessToken value model = 24 + case Json.decodeValue Sources.Refresh.AccessToken.portArgumentsDecoder value of 25 + Ok portArguments -> 26 + case portArguments.service of 27 + Google -> 28 + model.hypaethralUserData.sources 29 + |> List.find (.id >> (==) portArguments.sourceId) 30 + |> Maybe.map 31 + (\source -> 32 + source.data 33 + |> Dict.insert "accessToken" portArguments.accessToken 34 + |> Dict.insert "expiresAt" (String.fromInt portArguments.expiresAt) 35 + |> (\newData -> { source | data = newData }) 36 + ) 37 + |> Maybe.map 38 + (\source -> 39 + source 40 + |> Sources.Encoding.encode 41 + |> Alien.broadcast Alien.UpdateSourceData 42 + |> Ports.toUI 43 + ) 44 + |> Maybe.withDefault Cmd.none 45 + |> return model 46 + 47 + _ -> 48 + Return.singleton model 49 + 50 + Err err -> 51 + Common.reportUI Alien.ToCache (Json.errorToString err) model 15 52 16 53 17 54 setCurrentTime : Time.Posix -> Manager
+3
src/Applications/Brain/Ports.elm
··· 99 99 port receiveSearchResults : (List String -> msg) -> Sub msg 100 100 101 101 102 + port refreshedAccessToken : (Json.Value -> msg) -> Sub msg 103 + 104 + 102 105 port receiveTags : (ContextForTags -> msg) -> Sub msg 103 106 104 107
+6 -5
src/Applications/Brain/Sources/Processing/Steps.elm
··· 80 80 takePrepareStep : Context -> String -> Time.Posix -> Cmd Brain.Msg 81 81 takePrepareStep context response currentTime = 82 82 context 83 - |> handlePreparationResponse response 83 + |> handlePreparationResponse response currentTime 84 84 |> intoPreparationCommands currentTime 85 85 86 86 ··· 149 149 makeTree context currentTime 150 150 151 151 152 - handlePreparationResponse : String -> Context -> Context 153 - handlePreparationResponse response context = 152 + handlePreparationResponse : String -> Time.Posix -> Context -> Context 153 + handlePreparationResponse response currentTime context = 154 154 let 155 155 answer = 156 156 Services.parsePreparationResponse 157 157 context.source.service 158 158 response 159 + currentTime 159 160 context.source.data 160 161 context.preparationMarker 161 162 ··· 319 320 320 321 mapFn = 321 322 \path -> 322 - { getUrl = maker currentTime source.data Get path 323 - , headUrl = maker currentTime source.data Head path 323 + { getUrl = maker currentTime source.id source.data Get path 324 + , headUrl = maker currentTime source.id source.data Head path 324 325 } 325 326 in 326 327 List.map mapFn filePaths
+2 -1
src/Applications/Brain/Tracks/State.elm
··· 306 306 Sources.Services.makeTrackUrl 307 307 source.service 308 308 timestamp 309 + source.id 309 310 source.data 310 311 httpMethod 311 312 trackPath ··· 318 319 tagUrls currentTime path source = 319 320 let 320 321 maker = 321 - Sources.Services.makeTrackUrl source.service currentTime source.data 322 + Sources.Services.makeTrackUrl source.service currentTime source.id source.data 322 323 in 323 324 { getUrl = maker Get path 324 325 , headUrl = maker Head path
+1
src/Applications/Brain/Types.elm
··· 66 66 ----------------------------------------- 67 67 -- 📭 Other 68 68 ----------------------------------------- 69 + | RefreshedAccessToken Json.Value 69 70 | SetCurrentTime Time.Posix 70 71 | ToCache Json.Value 71 72
+19 -15
src/Applications/Brain/User/State.elm
··· 840 840 terminate NotAuthenticated model 841 841 842 842 843 - refreshedDropboxTokens : 844 - { currentTime : Int, refreshToken : String } 845 - -> Dropbox.Tokens 846 - -> User.Msg 847 - -> Manager 848 - refreshedDropboxTokens { currentTime, refreshToken } tokens msg model = 849 - { accessToken = tokens.accessToken 850 - , expiresAt = currentTime + tokens.expiresIn 851 - , refreshToken = refreshToken 852 - } 853 - |> Dropbox 854 - |> (\m -> saveMethod m model) 855 - |> andThen (update msg) 856 - 857 - 858 843 retrieveMethod : Manager 859 844 retrieveMethod = 860 845 Alien.AuthMethod ··· 955 940 model 956 941 |> Common.nudgeUI Alien.NotAuthenticated 957 942 |> andThen (Common.nudgeUI Alien.HideLoadingScreen) 943 + 944 + 945 + 946 + -- 📭 ░░ OTHER 947 + 948 + 949 + refreshedDropboxTokens : 950 + { currentTime : Int, refreshToken : String } 951 + -> Dropbox.Tokens 952 + -> User.Msg 953 + -> Manager 954 + refreshedDropboxTokens { currentTime, refreshToken } tokens msg model = 955 + { accessToken = tokens.accessToken 956 + , expiresAt = currentTime + tokens.expiresIn 957 + , refreshToken = refreshToken 958 + } 959 + |> Dropbox 960 + |> (\m -> saveMethod m model) 961 + |> andThen (update msg)
+4
src/Applications/UI.elm
··· 529 529 InstallingServiceWorker -> 530 530 Other.installingServiceWorker 531 531 532 + RedirectToBrain a -> 533 + Other.redirectToBrain a 534 + 532 535 ReloadApp -> 533 536 Other.reloadApp 534 537 ··· 603 606 ----------------------------------------- 604 607 , Ports.installedNewServiceWorker (\_ -> InstalledServiceWorker) 605 608 , Ports.installingNewServiceWorker (\_ -> InstallingServiceWorker) 609 + , Ports.refreshedAccessToken (Alien.broadcast Alien.RefreshedAccessToken >> RedirectToBrain) 606 610 , Ports.setIsOnline SetIsOnline 607 611 , Ports.webnativeResponse GotWebnativeResponse 608 612 , Sub.map KeyboardMsg Keyboard.subscriptions
+5
src/Applications/UI/Other/State.elm
··· 31 31 Return.singleton { model | serviceWorkerStatus = InstallingNew } 32 32 33 33 34 + redirectToBrain : Alien.Event -> Manager 35 + redirectToBrain event model = 36 + return model (Ports.toBrain event) 37 + 38 + 34 39 reloadApp : Manager 35 40 reloadApp model = 36 41 return model (Ports.reloadApp ())
+3
src/Applications/UI/Ports.elm
··· 83 83 port noteProgress : ({ trackId : String, progress : Float } -> msg) -> Sub msg 84 84 85 85 86 + port refreshedAccessToken : (Json.Value -> msg) -> Sub msg 87 + 88 + 86 89 port preferredColorSchemaChanged : ({ dark : Bool } -> msg) -> Sub msg 87 90 88 91
+2
src/Applications/UI/Types.elm
··· 1 1 module UI.Types exposing (..) 2 2 3 3 import Alfred exposing (Alfred) 4 + import Alien 4 5 import Browser 5 6 import Browser.Navigation as Nav 6 7 import Color exposing (Color) ··· 305 306 ----------------------------------------- 306 307 | InstalledServiceWorker 307 308 | InstallingServiceWorker 309 + | RedirectToBrain Alien.Event 308 310 | ReloadApp 309 311 | SetCurrentTime Time.Posix 310 312 | SetCurrentTimeZone Time.Zone
+12 -11
src/Javascript/Brain/artwork.js
··· 10 10 const REJECT = () => Promise.reject("No artwork found") 11 11 12 12 13 - export function find(prep) { 13 + export function find(prep, app) { 14 14 return findUsingTags(prep) 15 15 .then(a => a ? a : findUsingMusicBrainz(prep)) 16 16 .then(a => a ? a : findUsingLastFm(prep)) ··· 28 28 // 1. TAGS 29 29 30 30 31 - function findUsingTags(prep) { 31 + function findUsingTags(prep, app) { 32 32 return Promise.all( 33 - [ transformUrl(prep.trackHeadUrl) 34 - , transformUrl(prep.trackGetUrl) 33 + [ 34 + transformUrl(prep.trackHeadUrl, app), 35 + transformUrl(prep.trackGetUrl, app) 35 36 ] 36 37 37 38 ).then(([ headUrl, getUrl ]) => processing.getTags( ··· 55 56 56 57 function findUsingMusicBrainz(prep) { 57 58 const parts = decodeCacheKey(prep.cacheKey).split(" --- ") 58 - const artist = parts[0] 59 - const album = parts[1] || parts[0] 59 + const artist = parts[ 0 ] 60 + const album = parts[ 1 ] || parts[ 0 ] 60 61 61 62 const query = `release:"${album}"` + (prep.variousArtists === "t" ? `` : ` AND artist:"${artist}"`) 62 63 const encodedQuery = encodeURIComponent(query) ··· 68 69 69 70 70 71 function musicBrainzCover(remainingReleases) { 71 - const release = remainingReleases[0] 72 + const release = remainingReleases[ 0 ] 72 73 if (!release) return null 73 74 74 75 return fetch( ··· 99 100 100 101 101 102 function lastFmCover(remainingMatches) { 102 - const album = remainingMatches[0] 103 - const url = album ? album.image[album.image.length - 1]["#text"] : null 103 + const album = remainingMatches[ 0 ] 104 + const url = album ? album.image[ album.image.length - 1 ][ "#text" ] : null 104 105 105 106 return url && url !== "" 106 107 ? fetch(url) 107 - .then(r => r.blob()) 108 - .catch(_ => lastFmCover(remainingMatches.slice(1))) 108 + .then(r => r.blob()) 109 + .catch(_ => lastFmCover(remainingMatches.slice(1))) 109 110 : album && lastFmCover(remainingMatches.slice(1)) 110 111 }
+34 -24
src/Javascript/Brain/index.js
··· 26 26 .substr(1) 27 27 .split("&") 28 28 .reduce((acc, flag) => { 29 - const [k, v] = flag.split("=") 30 - return { ...acc, [k]: v } 29 + const [ k, v ] = flag.split("=") 30 + return { ...acc, [ k ]: v } 31 31 }, {}) 32 32 33 33 ··· 56 56 } 57 57 58 58 59 - function handleAction(action, data) { switch (action) { 60 - case "DOWNLOAD_ARTWORK": return downloadArtwork(data) 61 - }} 59 + function handleAction(action, data) { 60 + switch (action) { 61 + case "DOWNLOAD_ARTWORK": return downloadArtwork(data) 62 + } 63 + } 62 64 63 65 64 66 ··· 67 69 68 70 app.ports.removeCache.subscribe(event => { 69 71 removeCache(event.tag) 70 - .catch( reportError(app, event) ) 72 + .catch(reportError(app, event)) 71 73 }) 72 74 73 75 ··· 77 79 : event.tag 78 80 79 81 fromCache(key) 80 - .then( sendData(app, event) ) 81 - .catch( reportError(app, event) ) 82 + .then(sendData(app, event)) 83 + .catch(reportError(app, event)) 82 84 }) 83 85 84 86 ··· 90 92 toCache(key, event.data.data || event.data) 91 93 .then( 92 94 event.tag === "AUTH_ANONYMOUS" 93 - ? storageCallback(app, event) 94 - : identity 95 + ? storageCallback(app, event) 96 + : identity 95 97 ) 96 - .catch( reportError(app, event) ) 98 + .catch(reportError(app, event)) 97 99 }) 98 100 99 101 ··· 105 107 106 108 107 109 function downloadArtwork(list) { 108 - const exe = !artworkQueue[0] 110 + const exe = !artworkQueue[ 0 ] 109 111 artworkQueue = artworkQueue.concat(list) 110 112 if (exe) shiftArtworkQueue() 111 113 } ··· 127 129 128 130 app.ports.provideArtworkTrackUrls.subscribe(prep => { 129 131 artwork 130 - .find(prep) 132 + .find(prep, app) 131 133 .then(blob => { 132 134 const url = URL.createObjectURL(blob) 133 135 ··· 139 141 140 142 return toCache(`coverCache.${prep.cacheKey}`, blob) 141 143 }) 142 - .catch(_ => { 143 - // Indicate that we've tried to find artwork, 144 - // so that we don't try to find it each time we launch the app. 145 - return toCache(`coverCache.${prep.cacheKey}`, "TRIED") 144 + .catch(err => { 145 + if (err === "No artwork found") { 146 + // Indicate that we've tried to find artwork, 147 + // so that we don't try to find it each time we launch the app. 148 + return toCache(`coverCache.${prep.cacheKey}`, "TRIED") 149 + 150 + } else { 151 + // Something went wrong 152 + reportError(app, { tag: "REPORT_ERROR" })(err) 153 + 154 + } 146 155 }) 147 156 .finally(shiftArtworkQueue) 148 157 }) ··· 159 168 160 169 ).catch( 161 170 _ => reportError 162 - ({ tag: "REMOVE_TRACKS_FROM_CACHE" }) 171 + (app, { tag: "REMOVE_TRACKS_FROM_CACHE" }) 163 172 ("Failed to remove tracks from cache") 164 173 165 174 ) ··· 168 177 169 178 app.ports.storeTracksInCache.subscribe(list => { 170 179 list.reduce( 171 - (acc, item) => { return acc 172 - .then(_ => fetch(item.url)) 173 - .then(r => r.blob()) 174 - .then(b => db.setInIndex({ key: item.trackId, data: b, store: db.storeNames.tracks })) 180 + (acc, item) => { 181 + return acc 182 + .then(_ => fetch(item.url)) 183 + .then(r => r.blob()) 184 + .then(b => db.setInIndex({ key: item.trackId, data: b, store: db.storeNames.tracks })) 175 185 }, 176 186 Promise.resolve() 177 187 ··· 245 255 // ---- 246 256 247 257 app.ports.requestTags.subscribe(context => { 248 - processing.processContext(context).then(newContext => { 258 + processing.processContext(context, app).then(newContext => { 249 259 app.ports.receiveTags.send(newContext) 250 260 }) 251 261 }) 252 262 253 263 254 264 app.ports.syncTags.subscribe(context => { 255 - processing.processContext(context).then(newContext => { 265 + processing.processContext(context, app).then(newContext => { 256 266 app.ports.replaceTags.send(newContext) 257 267 }) 258 268 })
+7 -5
src/Javascript/Workers/service.js
··· 13 13 14 14 const EXCLUDE = 15 15 [ "_headers" 16 - , "_redirects" 17 - , "CORS" 16 + , "_redirects" 17 + , "CORS" 18 18 ] 19 19 20 20 ··· 79 79 "Basic " + token 80 80 ) 81 81 82 - // When doing a request with access token in the url, put it in the headers instead 82 + // When doing a request with access token in the url, put it in the headers instead 83 83 } else if (event.request.url.includes("bearer_token=")) { 84 84 const url = new URL(event.request.url) 85 85 const token = url.searchParams.get("bearer_token") ··· 95 95 "Bearer " + token 96 96 ) 97 97 98 - // Use cache if internal request and not using native app 98 + // Use cache if internal request and not using native app 99 99 } else if (isInternal) { 100 100 event.respondWith( 101 101 isNativeWrapper ··· 152 152 request.headers.entries() 153 153 ) 154 154 155 - newHeaders["authorization"] = authToken 155 + newHeaders[ "authorization" ] = authToken 156 156 157 157 const newRequest = new Request( 158 158 new Request(urlWithoutToken, event.request), ··· 170 170 171 171 let retries = 0 172 172 173 + // TODO: When request fails because access token is expired, 174 + // refresh the token, and retry the request. 173 175 const makeFetch = () => fetch(newRequest).then(r => { 174 176 if (r.ok) { 175 177 retries = 0
+26 -26
src/Javascript/audio-engine.js
··· 15 15 16 16 17 17 const IS_SAFARI = !!navigator.platform.match(/iPhone|iPod|iPad/) || 18 - navigator.vendor === "Apple Computer, Inc." 18 + navigator.vendor === "Apple Computer, Inc." 19 19 20 20 21 21 ··· 38 38 let c 39 39 let styles = 40 40 [ "height: 0" 41 - , "width: 0" 42 - , "visibility: hidden" 43 - , "pointer-events: none" 41 + , "width: 0" 42 + , "visibility: hidden" 43 + , "pointer-events: none" 44 44 ] 45 45 46 46 c = document.createElement("div") ··· 117 117 // initial promise 118 118 const initialPromise = queueItem.isCached 119 119 ? db.getFromIndex({ key: queueItem.trackId, store: db.storeNames.tracks }).then(blobUrl) 120 - : transformUrl(queueItem.url) 120 + : transformUrl(queueItem.url, orchestrion.app) 121 121 122 122 // find or create audio node 123 123 let audioNode ··· 214 214 n => n.parentNode.removeChild(n) 215 215 ) 216 216 217 - // audio element remains valid for 2 hours 218 - transformUrl(queueItem.url).then(url => { 217 + // audio element remains valid for 45 minutes 218 + transformUrl(queueItem.url, orchestrion.app).then(url => { 219 219 const queueItemWithTransformedUrl = 220 220 Object.assign({}, queueItem, { url: url }) 221 221 222 222 createAudioElement( 223 223 orchestrion, 224 224 queueItemWithTransformedUrl, 225 - Date.now() + 1000 * 60 * 60 * 2, 225 + Date.now() + 1000 * 60 * 45, 226 226 true 227 227 ) 228 228 }) ··· 285 285 } 286 286 287 287 288 - function showNetworkErrorNotification() { 289 - if (showedNoNetworkError) return 290 - showedNoNetworkError = true 291 - this.app.ports.showErrorNotification.send( 292 - navigator.onLine 293 - ? "I can't play this track because of a network error. I'll try to reconnect." 294 - : "I can't play this track because we're offline. I'll try to reconnect." 295 - ) 296 - } 288 + function showNetworkErrorNotification() { 289 + if (showedNoNetworkError) return 290 + showedNoNetworkError = true 291 + this.app.ports.showErrorNotification.send( 292 + navigator.onLine 293 + ? "I can't play this track because of a network error. I'll try to reconnect." 294 + : "I can't play this track because we're offline. I'll try to reconnect." 295 + ) 296 + } 297 297 298 298 299 - function showUnsupportedSrcErrorNotification() { 300 - this.app.ports.showErrorNotification.send( 301 - "__I can't play this track because your browser didn't recognize it.__ Try checking your developer console for a warning to find out why." 302 - ) 303 - } 299 + function showUnsupportedSrcErrorNotification() { 300 + this.app.ports.showErrorNotification.send( 301 + "__I can't play this track because your browser didn't recognize it.__ Try checking your developer console for a warning to find out why." 302 + ) 303 + } 304 304 305 305 306 306 function audioStalledEvent(event, notifyAppImmediately) { ··· 426 426 !node || 427 427 node.getAttribute("data-preload") === "t" 428 428 ) 429 - ? false 430 - : orchestrion.activeQueueItem.trackId === audioElementTrackId(node) 429 + ? false 430 + : orchestrion.activeQueueItem.trackId === audioElementTrackId(node) 431 431 } 432 432 433 433 ··· 492 492 let artwork = [] 493 493 494 494 if (maybeArtwork && typeof maybeArtwork !== "string") { 495 - artwork = [{ 495 + artwork = [ { 496 496 src: URL.createObjectURL(maybeArtwork), 497 497 type: maybeArtwork.type 498 - }] 498 + } ] 499 499 } 500 500 501 501 navigator.mediaSession.metadata = new MediaMetadata({
+56 -41
src/Javascript/index.js
··· 1 1 // 2 - // Elm loader 3 2 // | (• ◡•)| (❍ᴥ❍ʋ) 4 3 // 5 4 // The bit where we launch the Elm app, ··· 17 16 import * as db from "./indexed-db" 18 17 import { version } from '../../package.json' 19 18 import { WEBNATIVE_STAGING_ENV, WEBNATIVE_STAGING_MODE, debounce, fileExtension } from "./common" 19 + import { transformUrl } from "./urls" 20 20 21 21 22 22 ··· 35 35 location.href = location.href.replace("http://", "https://") 36 36 failure("Just a moment, redirecting to HTTPS.") 37 37 38 - // Not a secure context 38 + // Not a secure context 39 39 } else if (!self.isSecureContext) { 40 40 failure(` 41 41 This app only works on a <a class="underline" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#When_is_a_context_considered_secure">secure context</a>, HTTPS & localhost, and modern browsers. 42 42 `) 43 43 44 - // Service worker 44 + // Service worker 45 45 } else if ("serviceWorker" in navigator) { 46 46 window.addEventListener("load", () => { 47 47 navigator.serviceWorker ··· 193 193 } 194 194 195 195 196 - function handleAction(action, data, _ports) { switch (action) { 197 - case "DOWNLOAD_TRACKS": return downloadTracks(data) 198 - case "FINISHED_DOWNLOADING_ARTWORK": return finishedDownloadingArtwork() 199 - }} 196 + function handleAction(action, data, _ports) { 197 + switch (action) { 198 + case "DOWNLOAD_TRACKS": return downloadTracks(data) 199 + case "FINISHED_DOWNLOADING_ARTWORK": return finishedDownloadingArtwork() 200 + } 201 + } 200 202 201 203 202 204 ··· 257 259 // 🎵 258 260 if (item) { 259 261 const coverPrep = { 260 - cacheKey: btoa(unescape(encodeURIComponent(item.trackTags.artist + " --- " + item.trackTags.album))), 261 - trackFilename: item.trackPath.split("/").reverse()[0], 262 - trackPath: item.trackPath, 263 - trackSourceId: item.sourceId, 262 + cacheKey: btoa(unescape(encodeURIComponent(item.trackTags.artist + " --- " + item.trackTags.album))), 263 + trackFilename: item.trackPath.split("/").reverse()[ 0 ], 264 + trackPath: item.trackPath, 265 + trackSourceId: item.sourceId, 264 266 variousArtists: "f" 265 267 } 266 268 ··· 285 287 }) 286 288 }) 287 289 288 - // ✋ 290 + // ✋ 289 291 } else { 290 292 app.ports.setAudioIsPlaying.send(false) 291 293 app.ports.setAudioPosition.send(0) ··· 388 390 const color = { r: 0, g: 0, b: 0 } 389 391 390 392 for (let i = 0, l = imageData.data.length; i < l; i += 4) { 391 - color.r += imageData.data[i] 392 - color.g += imageData.data[i + 1] 393 - color.b += imageData.data[i + 2] 393 + color.r += imageData.data[ i ] 394 + color.g += imageData.data[ i + 1 ] 395 + color.b += imageData.data[ i + 2 ] 394 396 } 395 397 396 398 color.r = Math.floor(color.r / (imageData.data.length / 4)) ··· 432 434 // --------- 433 435 434 436 wire.clipboard = () => { 435 - app.ports.copyToClipboard.subscribe(copyToClipboard) 437 + app.ports.copyToClipboard.subscribe(text => { 438 + // TODO: Find a better solution for this 439 + const adjustedText = (() => { 440 + if (text.startsWith("dropbox://")) { 441 + return transformUrl(text) 442 + } else if (text.startsWith("google://")) { 443 + return transformUrl(text) 444 + } else { 445 + return text 446 + 447 + } 448 + })() 449 + 450 + copyToClipboard(copyToClipboard) 451 + }) 436 452 } 437 453 438 454 439 455 function copyToClipboard(text) { 440 - 441 456 // Insert a textarea element 442 457 const el = document.createElement("textarea") 443 458 ··· 465 480 document.getSelection().removeAllRanges() 466 481 document.getSelection().addRange(selected) 467 482 } 468 - 469 483 } 470 484 471 485 ··· 491 505 const item = orchestrion.activeQueueItem 492 506 493 507 if (item && orchestrion.coverPrep && key === orchestrion.coverPrep.key && url) { 494 - let artwork = [{ src: url }] 508 + let artwork = [ { src: url } ] 495 509 496 510 if (typeof url !== "string") { 497 - artwork = [{ 511 + artwork = [ { 498 512 src: URL.createObjectURL(url), 499 513 type: url.type 500 - }] 514 + } ] 501 515 } 502 516 503 517 navigator.mediaSession.metadata = new MediaMetadata({ ··· 524 538 if (!nodes.length) return; 525 539 526 540 const coverPrepList = nodes.map(node => ({ 527 - cacheKey: node.getAttribute("data-key"), 528 - trackFilename: node.getAttribute("data-filename"), 529 - trackPath: node.getAttribute("data-path"), 530 - trackSourceId: node.getAttribute("data-source-id"), 541 + cacheKey: node.getAttribute("data-key"), 542 + trackFilename: node.getAttribute("data-filename"), 543 + trackPath: node.getAttribute("data-path"), 544 + trackSourceId: node.getAttribute("data-source-id"), 531 545 variousArtists: node.getAttribute("data-various-artists") 532 546 })) 533 547 ··· 566 580 const cacheKey = key.slice(11) 567 581 568 582 if (blob && typeof blob !== "string") { 569 - cache[cacheKey] = URL.createObjectURL(blob) 583 + cache[ cacheKey ] = URL.createObjectURL(blob) 570 584 } 571 585 572 586 return cache ··· 618 632 const folder = zip.folder("Diffuse - " + group.name) 619 633 620 634 return group.tracks.reduce( 621 - (acc, track) => { return acc 622 - .then(_ => fetch(track.url)) 623 - .then(r => { 624 - const mimeType = r.headers.get("content-type") 625 - const fileExt = fileExtension(mimeType) || "unknown" 635 + (acc, track) => { 636 + return acc 637 + .then(_ => fetch(track.url)) 638 + .then(r => { 639 + const mimeType = r.headers.get("content-type") 640 + const fileExt = fileExtension(mimeType) || "unknown" 626 641 627 - return r.blob().then( 628 - b => folder.file(track.filename + "." + fileExt, b) 629 - ) 630 - }) 642 + return r.blob().then( 643 + b => folder.file(track.filename + "." + fileExt, b) 644 + ) 645 + }) 631 646 }, 632 647 Promise.resolve() 633 648 ··· 863 878 // Simulate `pointerenter` and `pointerleave` event for non-touch devices 864 879 if (!self.PointerEvent) { 865 880 document.addEventListener("mouseover", event => { 866 - const section = document.body.querySelector("section") 867 - const isDragging = section && section.classList.contains("dragging-something") 868 - const node = isDragging && document.elementFromPoint(event.clientX, event.clientY) 881 + const section = document.body.querySelector("section") 882 + const isDragging = section && section.classList.contains("dragging-something") 883 + const node = isDragging && document.elementFromPoint(event.clientX, event.clientY) 869 884 870 885 if (node && node != enteredElement) { 871 886 enteredElement && enteredElement.dispatchEvent(mousePointerEvent("pointerleave", event)) ··· 878 893 879 894 // Simulate `pointerenter` and `pointerleave` event for touch devices 880 895 document.body.addEventListener("touchmove", event => { 881 - const section = document.body.querySelector("section") 882 - const isDragging = section && section.classList.contains("dragging-something") 896 + const section = document.body.querySelector("section") 897 + const isDragging = section && section.classList.contains("dragging-something") 883 898 884 - let touch = event.touches[0] 899 + let touch = event.touches[ 0 ] 885 900 let node 886 901 887 902 if (isDragging && touch) {
+9 -9
src/Javascript/processing.js
··· 15 15 // Contexts 16 16 // -------- 17 17 18 - export function processContext(context) { 18 + export function processContext(context, app) { 19 19 const initialPromise = Promise.resolve([]) 20 20 21 21 return context.urlsForTags.reduce((accumulator, urls, idx) => { 22 22 return accumulator.then(col => { 23 23 const filename = context 24 - .receivedFilePaths[idx] 24 + .receivedFilePaths[ idx ] 25 25 .split("/") 26 - .reverse()[0] 26 + .reverse()[ 0 ] 27 27 28 28 return Promise.all([ 29 - transformUrl(urls.headUrl), 30 - transformUrl(urls.getUrl) 29 + transformUrl(urls.headUrl, app), 30 + transformUrl(urls.getUrl, app) 31 31 32 - ]).then(([headUrl, getUrl]) => { 32 + ]).then(([ headUrl, getUrl ]) => { 33 33 return getTags(headUrl, getUrl, filename, { skipCovers: true }) 34 34 35 35 }).then(r => { ··· 63 63 64 64 export function getTags(headUrl, getUrl, filename, options) { 65 65 const fileExtMatch = filename.match(/\.(\w+)$/) 66 - const fileExt = fileExtMatch && fileExtMatch[1] 66 + const fileExt = fileExtMatch && fileExtMatch[ 1 ] 67 67 68 68 const overrideContentType = ( 69 69 getUrl.includes("googleapis.com") || ··· 109 109 album: tags.album && tags.album.length ? tags.album : "Unknown", 110 110 artist: artist || "Unknown", 111 111 title: title ? title : (artist ? "Unknown" : filename.replace(/\.\w+$/, "")), 112 - genre: (tags.genre && tags.genre[0]) || null, 112 + genre: (tags.genre && tags.genre[ 0 ]) || null, 113 113 year: tags.year || null, 114 - picture: tags.picture ? tags.picture[0] : null 114 + picture: tags.picture ? tags.picture[ 0 ] : null 115 115 } 116 116 } 117 117
+88 -8
src/Javascript/urls.js
··· 5 5 // Some URLs are special you know. 6 6 7 7 8 - export function transformUrl(url) { 8 + const EXPIRED_ACCESS_TOKENS = { 9 + GOOGLE: {} 10 + } 11 + 12 + 13 + export async function transformUrl(url, app) { 9 14 const parts = url.split("://") 10 15 11 - switch (parts[0]) { 16 + switch (parts[ 0 ]) { 12 17 13 18 case "dropbox": { 14 - const dropboxBits = parts[1].split("@") 15 - const accessToken = dropboxBits[0] 16 - const filePath = dropboxBits[1] 19 + const dropboxBits = parts[ 1 ].split("@") 20 + const accessToken = dropboxBits[ 0 ] 21 + const filePath = dropboxBits[ 1 ] 17 22 18 23 return fetch( 19 24 "https://api.dropboxapi.com/2/files/get_temporary_link", 20 - { method: "POST" 21 - , body: JSON.stringify({ path: filePath }) 22 - , headers: new Headers({ 25 + { 26 + method: "POST" 27 + , body: JSON.stringify({ path: filePath }) 28 + , headers: new Headers({ 23 29 "Authorization": "Bearer " + accessToken, 24 30 "Content-Type": "application/json" 25 31 }) ··· 31 37 ) 32 38 } 33 39 40 + case "google": { 41 + let finalAccessToken 42 + 43 + const googleBits = parts[ 1 ].split("@") 44 + const [ accessToken, expiresAtString, refreshToken, clientId, clientSecret, srcId ] = googleBits[ 0 ].split(":") 45 + const fileId = googleBits[ 1 ] 46 + 47 + // Unix timestamp in milliseconds 48 + const inXminutes = Date.now() + 5 * 60 * 1000 // 5 minutes 49 + const expiresAt = parseInt(expiresAtString, 10) 50 + const isAlmostExpired = expiresAt <= inXminutes 51 + 52 + if (EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ]) { 53 + const replacement = EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ] 54 + 55 + if (replacement.newExpiresAt <= inXminutes) { 56 + finalAccessToken = await refreshGoogleAccessToken({ 57 + app, clientId, clientSecret, refreshToken, srcId, oldToken: accessToken 58 + }) 59 + } else { 60 + finalAccessToken = replacement.newToken 61 + } 62 + 63 + } else if (isAlmostExpired) { 64 + finalAccessToken = await refreshGoogleAccessToken({ 65 + app, clientId, clientSecret, refreshToken, srcId, oldToken: accessToken 66 + }) 67 + 68 + } else { 69 + finalAccessToken = accessToken 70 + 71 + } 72 + 73 + return Promise.resolve( 74 + `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media&bearer_token=${encodeURIComponent(finalAccessToken)}` 75 + ) 76 + } 77 + 34 78 default: 35 79 return Promise.resolve(url) 36 80 37 81 } 38 82 } 83 + 84 + 85 + 86 + // GOOGLE 87 + 88 + 89 + async function refreshGoogleAccessToken({ app, clientId, clientSecret, oldToken, refreshToken, srcId }) { 90 + console.log("🔐 Refreshing Google Drive access token") 91 + 92 + const url = new URL("https://www.googleapis.com/oauth2/v4/token") 93 + 94 + url.searchParams.set("client_id", clientId) 95 + url.searchParams.set("client_secret", clientSecret) 96 + url.searchParams.set("refresh_token", refreshToken) 97 + url.searchParams.set("grant_type", "refresh_token") 98 + 99 + const serverResponse = await fetch(url, { method: "POST" }).then(r => r.json()) 100 + const newToken = serverResponse.access_token 101 + const newExpiresAt = Date.now() + (serverResponse.expires_in * 1000) 102 + 103 + EXPIRED_ACCESS_TOKENS.GOOGLE[ oldToken ] = { 104 + oldToken, 105 + newToken, 106 + newExpiresAt, 107 + refreshToken 108 + } 109 + 110 + app.ports.refreshedAccessToken.send({ 111 + service: "Google", 112 + sourceId: srcId, 113 + accessToken: newToken, 114 + expiresAt: newExpiresAt 115 + }) 116 + 117 + return serverResponse.access_token 118 + }
+2
src/Library/Alien.elm
··· 40 40 | DownloadTracks 41 41 | ImportLegacyData 42 42 | ProcessSources 43 + | RefreshedAccessToken 43 44 | RemoveEncryptionKey 44 45 | RemoveTracksBySourceId 45 46 | RemoveTracksFromCache ··· 98 99 , ( "DOWNLOAD_TRACKS", DownloadTracks ) 99 100 , ( "IMPORT_LEGACY_DATA", ImportLegacyData ) 100 101 , ( "PROCESS_SOURCES", ProcessSources ) 102 + , ( "REFRESHED_ACCESS_TOKEN", RefreshedAccessToken ) 101 103 , ( "REMOVE_ENCRYPTION_KEY", RemoveEncryptionKey ) 102 104 , ( "REMOVE_TRACKS_BY_SOURCE_ID", RemoveTracksBySourceId ) 103 105 , ( "REMOVE_TRACKS_FROM_CACHE", RemoveTracksFromCache )
+1
src/Library/Queue.elm
··· 70 70 Sources.Services.makeTrackUrl 71 71 source.service 72 72 timestamp 73 + source.id 73 74 source.data 74 75 Get 75 76 track.path
+39
src/Library/Sources/Refresh/AccessToken.elm
··· 1 + module Sources.Refresh.AccessToken exposing (..) 2 + 3 + import Json.Decode exposing (Decoder) 4 + import Sources exposing (Service) 5 + import Sources.Encoding exposing (serviceDecoder) 6 + 7 + 8 + 9 + -- 🌳 10 + 11 + 12 + type alias PortArguments = 13 + { service : Service 14 + , sourceId : String 15 + , accessToken : String 16 + 17 + -- Unix timestamp in milliseconds 18 + , expiresAt : Int 19 + } 20 + 21 + 22 + 23 + -- 🛠 24 + 25 + 26 + portArgumentsDecoder : Decoder PortArguments 27 + portArgumentsDecoder = 28 + Json.Decode.map4 29 + (\service sourceId accessToken expiresAt -> 30 + { service = service 31 + , sourceId = sourceId 32 + , accessToken = accessToken 33 + , expiresAt = expiresAt 34 + } 35 + ) 36 + (Json.Decode.field "service" serviceDecoder) 37 + (Json.Decode.field "sourceId" Json.Decode.string) 38 + (Json.Decode.field "accessToken" Json.Decode.string) 39 + (Json.Decode.field "expiresAt" Json.Decode.int)
+2 -2
src/Library/Sources/Services.elm
··· 49 49 WebDav.initialData 50 50 51 51 52 - makeTrackUrl : Service -> Time.Posix -> SourceData -> HttpMethod -> String -> String 52 + makeTrackUrl : Service -> Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 53 53 makeTrackUrl service = 54 54 case service of 55 55 AmazonS3 -> ··· 139 139 WebDav.parseErrorResponse 140 140 141 141 142 - parsePreparationResponse : Service -> String -> SourceData -> Marker -> PrepationAnswer Marker 142 + parsePreparationResponse : Service -> String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 143 143 parsePreparationResponse service = 144 144 case service of 145 145 AmazonS3 ->
+3 -3
src/Library/Sources/Services/AmazonS3.elm
··· 146 146 147 147 {-| Re-export parser functions. 148 148 -} 149 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 149 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 150 150 parsePreparationResponse = 151 151 noPrep 152 152 ··· 185 185 Creates a presigned url that's valid for 48 hours 186 186 187 187 -} 188 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 189 - makeTrackUrl currentTime srcData method pathToFile = 188 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 189 + makeTrackUrl currentTime _ srcData method pathToFile = 190 190 presignedUrl method 172800 [] currentTime srcData pathToFile
+3 -3
src/Library/Sources/Services/AzureBlob.elm
··· 124 124 125 125 {-| Re-export parser functions. 126 126 -} 127 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 127 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 128 128 parsePreparationResponse = 129 129 noPrep 130 130 ··· 163 163 (!) Creates a presigned url that's valid for 48 hours 164 164 165 165 -} 166 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 167 - makeTrackUrl currentTime srcData _ pathToFile = 166 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 167 + makeTrackUrl currentTime _ srcData _ pathToFile = 168 168 presignedUrl Blob Read Get 48 currentTime srcData pathToFile []
+3 -3
src/Library/Sources/Services/AzureFile.elm
··· 128 128 129 129 {-| Re-export parser functions. 130 130 -} 131 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 131 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 132 132 parsePreparationResponse = 133 133 noPrep 134 134 ··· 167 167 (!) Creates a presigned url that's valid for 48 hours 168 168 169 169 -} 170 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 171 - makeTrackUrl currentTime srcData _ pathToFile = 170 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 171 + makeTrackUrl currentTime _ srcData _ pathToFile = 172 172 presignedUrl File Read Get 48 currentTime srcData pathToFile []
+2 -2
src/Library/Sources/Services/Btfs.elm
··· 116 116 We need this to play the track. 117 117 118 118 -} 119 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 120 - makeTrackUrl _ srcData _ path = 119 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 120 + makeTrackUrl _ _ srcData _ path = 121 121 Ipfs.extractGateway srcData ++ "/btfs/" ++ Ipfs.rootHash srcData ++ "/" ++ Ipfs.encodedPath path
+3 -2
src/Library/Sources/Services/Common.elm
··· 3 3 import Sources exposing (..) 4 4 import Sources.Processing exposing (..) 5 5 import String.Ext as String 6 + import Time 6 7 7 8 8 9 ··· 43 44 -- PARSING 44 45 45 46 46 - noPrep : String -> SourceData -> Marker -> PrepationAnswer Marker 47 - noPrep _ srcData _ = 47 + noPrep : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 48 + noPrep _ _ srcData _ = 48 49 { sourceData = srcData, marker = TheEnd }
+3 -3
src/Library/Sources/Services/Dropbox.elm
··· 187 187 188 188 {-| Re-export parser functions. 189 189 -} 190 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 190 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 191 191 parsePreparationResponse = 192 192 noPrep 193 193 ··· 225 225 We need this to play the track. 226 226 227 227 -} 228 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 229 - makeTrackUrl _ srcData _ pathToFile = 228 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 229 + makeTrackUrl _ _ srcData _ pathToFile = 230 230 "dropbox://" ++ Dict.fetch "accessToken" "" srcData ++ "@" ++ pathToFile
+22 -9
src/Library/Sources/Services/Google.elm
··· 250 250 251 251 {-| Re-export parser functions. 252 252 -} 253 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 253 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 254 254 parsePreparationResponse = 255 255 Parser.parsePreparationResponse 256 256 ··· 288 288 We need this to play the track. 289 289 290 290 -} 291 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 292 - makeTrackUrl _ srcData _ path = 291 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 292 + makeTrackUrl currentTime srcId srcData _ path = 293 293 let 294 294 file = 295 295 String.Path.file path ··· 300 300 |> List.head 301 301 |> Maybe.withDefault file 302 302 303 - accessToken = 304 - Dict.fetch "accessToken" "" srcData 303 + now = 304 + Time.posixToMillis currentTime 305 + 306 + expiresAt = 307 + Dict.fetch "expiresAt" (String.fromInt now) srcData 305 308 in 306 309 String.concat 307 - [ "https://www.googleapis.com/drive/v3/files/" 308 - , Url.percentEncode fileId 309 - , "?alt=media&bearer_token=" 310 - , Url.percentEncode accessToken 310 + [ "google://" 311 + , Dict.fetch "accessToken" "" srcData 312 + , ":" 313 + , expiresAt 314 + , ":" 315 + , Dict.fetch "refreshToken" "" srcData 316 + , ":" 317 + , Dict.fetch "clientId" "" srcData 318 + , ":" 319 + , Dict.fetch "clientSecret" "" srcData 320 + , ":" 321 + , srcId 322 + , "@" 323 + , fileId 311 324 ]
+16 -2
src/Library/Sources/Services/Google/Parser.elm
··· 9 9 import Sources.Processing exposing (Marker(..), PrepationAnswer, TreeAnswer) 10 10 import Sources.Services.Google.Marker as Marker 11 11 import String.Path 12 + import Time 12 13 13 14 14 15 15 16 -- PREPARATION 16 17 17 18 18 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 19 - parsePreparationResponse response srcData _ = 19 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 20 + parsePreparationResponse response currentTimePosix srcData _ = 20 21 let 21 22 newAccessToken = 22 23 response 23 24 |> decodeString (field "access_token" string) 24 25 |> Result.withDefault "" 25 26 27 + currentTime = 28 + -- Current time in milliseconds 29 + Time.posixToMillis currentTimePosix 30 + 31 + expiresAt = 32 + -- Unix timestamp in milliseconds 33 + response 34 + |> decodeString (field "expires_in" int) 35 + -- time in seconds 36 + |> Result.withDefault 2500 37 + |> (\s -> currentTime + s * 1000) 38 + 26 39 maybeRefreshToken = 27 40 response 28 41 |> decodeString (maybe <| field "refresh_token" string) ··· 39 52 in 40 53 srcData 41 54 |> Dict.insert "accessToken" newAccessToken 55 + |> Dict.insert "expiresAt" (String.fromInt expiresAt) 42 56 |> refreshTokenUpdater 43 57 |> Dict.remove "authCode" 44 58 |> (\s -> { sourceData = s, marker = TheEnd })
+3 -3
src/Library/Sources/Services/Ipfs.elm
··· 209 209 210 210 {-| Re-export parser functions. 211 211 -} 212 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 212 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 213 213 parsePreparationResponse = 214 214 Parser.parseDnsLookup 215 215 ··· 247 247 We need this to play the track. 248 248 249 249 -} 250 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 251 - makeTrackUrl _ srcData _ path = 250 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 251 + makeTrackUrl _ _ srcData _ path = 252 252 if not (String.contains "/" path) && not (String.contains "." path) then 253 253 -- If it still uses the old way of doing things 254 254 -- (ie. each path was a cid)
+3 -2
src/Library/Sources/Services/Ipfs/Parser.elm
··· 7 7 import Sources.Processing exposing (Marker(..), PrepationAnswer, TreeAnswer) 8 8 import Sources.Services.Ipfs.Marker as Marker 9 9 import String.Ext as String 10 + import Time 10 11 11 12 12 13 13 14 -- PREPARATION 14 15 15 16 16 - parseDnsLookup : String -> SourceData -> Marker -> PrepationAnswer Marker 17 - parseDnsLookup response srcData _ = 17 + parseDnsLookup : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 18 + parseDnsLookup response _ srcData _ = 18 19 case decodeString dnsResultDecoder response of 19 20 Ok path -> 20 21 srcData
+3 -3
src/Library/Sources/Services/WebDav.elm
··· 130 130 131 131 {-| Re-export parser functions. 132 132 -} 133 - parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker 133 + parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker 134 134 parsePreparationResponse = 135 135 noPrep 136 136 ··· 168 168 We need this to play the track. 169 169 170 170 -} 171 - makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String 172 - makeTrackUrl _ srcData _ filePath = 171 + makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String 172 + makeTrackUrl _ _ srcData _ filePath = 173 173 url { addAuth = True } srcData filePath 174 174 175 175