diff --git a/nix/shell.nix b/nix/shell.nix index 1072b319..d3637ad1 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -9,27 +9,6 @@ rustc = rust; }; - # Trying to fix an issue with `watchexec` on MacOS - # https://github.com/h4llow3En/mac-notification-sys/issues/28 - # Nix uses old Apple SDKs? Not sure, but definitely a Nix issue. - watchexec = rustPlatform.buildRustPackage rec { - pname = "watchexec"; - version = "1.20.4"; - - cargoSha256 = "sha256-YM+Zm3wFp3Lsx5LmyjGwZywV/SZjriL6JMDO1l0tNf4="; - - src = fetchFromGitHub { - owner = pname; - repo = pname; - rev = "cli-v${version}"; - sha256 = "sha256-se3iqz+qjwf71wvHQhCWYryEdUc+kY0Q0ZTg4i1ayNI="; - }; - - nativeBuildInputs = lib.optionals - stdenv.isDarwin - (with darwin.apple_sdk_11_0.frameworks; [ Cocoa Foundation ]); - }; - # Wraps # ----- # Inspired by https://www.tweag.io/blog/2022-06-02-haskell-stack-nix-shell/ diff --git a/src/Applications/Brain/Sources/Processing/Steps.elm b/src/Applications/Brain/Sources/Processing/Steps.elm index 9fb9d94f..71196fa2 100644 --- a/src/Applications/Brain/Sources/Processing/Steps.elm +++ b/src/Applications/Brain/Sources/Processing/Steps.elm @@ -80,7 +80,7 @@ takeFirstStep origin currentTime source = takePrepareStep : Context -> String -> Time.Posix -> Cmd Brain.Msg takePrepareStep context response currentTime = context - |> handlePreparationResponse response + |> handlePreparationResponse response currentTime |> intoPreparationCommands currentTime @@ -149,13 +149,14 @@ prepare context currentTime = makeTree context currentTime -handlePreparationResponse : String -> Context -> Context -handlePreparationResponse response context = +handlePreparationResponse : String -> Time.Posix -> Context -> Context +handlePreparationResponse response currentTime context = let answer = Services.parsePreparationResponse context.source.service response + currentTime context.source.data context.preparationMarker diff --git a/src/Javascript/Brain/artwork.js b/src/Javascript/Brain/artwork.js index de636dab..f5ca6116 100644 --- a/src/Javascript/Brain/artwork.js +++ b/src/Javascript/Brain/artwork.js @@ -10,7 +10,7 @@ import * as processing from "../processing" const REJECT = () => Promise.reject("No artwork found") -export function find(prep) { +export function find(prep, app) { return findUsingTags(prep) .then(a => a ? a : findUsingMusicBrainz(prep)) .then(a => a ? a : findUsingLastFm(prep)) @@ -28,10 +28,11 @@ function decodeCacheKey(cacheKey) { // 1. TAGS -function findUsingTags(prep) { +function findUsingTags(prep, app) { return Promise.all( - [ transformUrl(prep.trackHeadUrl) - , transformUrl(prep.trackGetUrl) + [ + transformUrl(prep.trackHeadUrl, app), + transformUrl(prep.trackGetUrl, app) ] ).then(([ headUrl, getUrl ]) => processing.getTags( @@ -55,8 +56,8 @@ function findUsingTags(prep) { function findUsingMusicBrainz(prep) { const parts = decodeCacheKey(prep.cacheKey).split(" --- ") - const artist = parts[0] - const album = parts[1] || parts[0] + const artist = parts[ 0 ] + const album = parts[ 1 ] || parts[ 0 ] const query = `release:"${album}"` + (prep.variousArtists === "t" ? `` : ` AND artist:"${artist}"`) const encodedQuery = encodeURIComponent(query) @@ -68,7 +69,7 @@ function findUsingMusicBrainz(prep) { function musicBrainzCover(remainingReleases) { - const release = remainingReleases[0] + const release = remainingReleases[ 0 ] if (!release) return null return fetch( @@ -99,12 +100,12 @@ function findUsingLastFm(prep) { function lastFmCover(remainingMatches) { - const album = remainingMatches[0] - const url = album ? album.image[album.image.length - 1]["#text"] : null + const album = remainingMatches[ 0 ] + const url = album ? album.image[ album.image.length - 1 ][ "#text" ] : null return url && url !== "" ? fetch(url) - .then(r => r.blob()) - .catch(_ => lastFmCover(remainingMatches.slice(1))) + .then(r => r.blob()) + .catch(_ => lastFmCover(remainingMatches.slice(1))) : album && lastFmCover(remainingMatches.slice(1)) } diff --git a/src/Javascript/Brain/index.js b/src/Javascript/Brain/index.js index ad58ef47..557e1566 100644 --- a/src/Javascript/Brain/index.js +++ b/src/Javascript/Brain/index.js @@ -26,8 +26,8 @@ const flags = location .substr(1) .split("&") .reduce((acc, flag) => { - const [k, v] = flag.split("=") - return { ...acc, [k]: v } + const [ k, v ] = flag.split("=") + return { ...acc, [ k ]: v } }, {}) @@ -56,9 +56,11 @@ self.onmessage = event => { } -function handleAction(action, data) { switch (action) { - case "DOWNLOAD_ARTWORK": return downloadArtwork(data) -}} +function handleAction(action, data) { + switch (action) { + case "DOWNLOAD_ARTWORK": return downloadArtwork(data) + } +} @@ -67,7 +69,7 @@ function handleAction(action, data) { switch (action) { app.ports.removeCache.subscribe(event => { removeCache(event.tag) - .catch( reportError(app, event) ) + .catch(reportError(app, event)) }) @@ -77,8 +79,8 @@ app.ports.requestCache.subscribe(event => { : event.tag fromCache(key) - .then( sendData(app, event) ) - .catch( reportError(app, event) ) + .then(sendData(app, event)) + .catch(reportError(app, event)) }) @@ -90,10 +92,10 @@ app.ports.toCache.subscribe(event => { toCache(key, event.data.data || event.data) .then( event.tag === "AUTH_ANONYMOUS" - ? storageCallback(app, event) - : identity + ? storageCallback(app, event) + : identity ) - .catch( reportError(app, event) ) + .catch(reportError(app, event)) }) @@ -105,7 +107,7 @@ let artworkQueue = [] function downloadArtwork(list) { - const exe = !artworkQueue[0] + const exe = !artworkQueue[ 0 ] artworkQueue = artworkQueue.concat(list) if (exe) shiftArtworkQueue() } @@ -127,7 +129,7 @@ function shiftArtworkQueue() { app.ports.provideArtworkTrackUrls.subscribe(prep => { artwork - .find(prep) + .find(prep, app) .then(blob => { const url = URL.createObjectURL(blob) @@ -139,10 +141,17 @@ app.ports.provideArtworkTrackUrls.subscribe(prep => { return toCache(`coverCache.${prep.cacheKey}`, blob) }) - .catch(_ => { - // Indicate that we've tried to find artwork, - // so that we don't try to find it each time we launch the app. - return toCache(`coverCache.${prep.cacheKey}`, "TRIED") + .catch(err => { + if (err === "No artwork found") { + // Indicate that we've tried to find artwork, + // so that we don't try to find it each time we launch the app. + return toCache(`coverCache.${prep.cacheKey}`, "TRIED") + + } else { + // Something went wrong + reportError(app, { tag: "REPORT_ERROR" })(err) + + } }) .finally(shiftArtworkQueue) }) @@ -159,7 +168,7 @@ app.ports.removeTracksFromCache.subscribe(trackIds => { ).catch( _ => reportError - ({ tag: "REMOVE_TRACKS_FROM_CACHE" }) + (app, { tag: "REMOVE_TRACKS_FROM_CACHE" }) ("Failed to remove tracks from cache") ) @@ -168,10 +177,11 @@ app.ports.removeTracksFromCache.subscribe(trackIds => { app.ports.storeTracksInCache.subscribe(list => { list.reduce( - (acc, item) => { return acc - .then(_ => fetch(item.url)) - .then(r => r.blob()) - .then(b => db.setInIndex({ key: item.trackId, data: b, store: db.storeNames.tracks })) + (acc, item) => { + return acc + .then(_ => fetch(item.url)) + .then(r => r.blob()) + .then(b => db.setInIndex({ key: item.trackId, data: b, store: db.storeNames.tracks })) }, Promise.resolve() @@ -245,14 +255,14 @@ search.onmessage = event => { // ---- app.ports.requestTags.subscribe(context => { - processing.processContext(context).then(newContext => { + processing.processContext(context, app).then(newContext => { app.ports.receiveTags.send(newContext) }) }) app.ports.syncTags.subscribe(context => { - processing.processContext(context).then(newContext => { + processing.processContext(context, app).then(newContext => { app.ports.replaceTags.send(newContext) }) }) diff --git a/src/Javascript/audio-engine.js b/src/Javascript/audio-engine.js index 92dbece7..0d79dd06 100644 --- a/src/Javascript/audio-engine.js +++ b/src/Javascript/audio-engine.js @@ -15,7 +15,7 @@ import { transformUrl } from "./urls" const IS_SAFARI = !!navigator.platform.match(/iPhone|iPod|iPad/) || - navigator.vendor === "Apple Computer, Inc." + navigator.vendor === "Apple Computer, Inc." @@ -38,9 +38,9 @@ const audioElementsContainer = (() => { let c let styles = [ "height: 0" - , "width: 0" - , "visibility: hidden" - , "pointer-events: none" + , "width: 0" + , "visibility: hidden" + , "pointer-events: none" ] c = document.createElement("div") @@ -117,7 +117,7 @@ export function insertTrack(orchestrion, queueItem, maybeArtwork) { // initial promise const initialPromise = queueItem.isCached ? db.getFromIndex({ key: queueItem.trackId, store: db.storeNames.tracks }).then(blobUrl) - : transformUrl(queueItem.url) + : transformUrl(queueItem.url, orchestrion.app) // find or create audio node let audioNode @@ -215,7 +215,7 @@ export function preloadAudioElement(orchestrion, queueItem) { ) // audio element remains valid for 2 hours - transformUrl(queueItem.url).then(url => { + transformUrl(queueItem.url, orchestrion.app).then(url => { const queueItemWithTransformedUrl = Object.assign({}, queueItem, { url: url }) @@ -285,22 +285,22 @@ function audioErrorEvent(event) { } - function showNetworkErrorNotification() { - if (showedNoNetworkError) return - showedNoNetworkError = true - this.app.ports.showErrorNotification.send( - navigator.onLine - ? "I can't play this track because of a network error. I'll try to reconnect." - : "I can't play this track because we're offline. I'll try to reconnect." - ) - } +function showNetworkErrorNotification() { + if (showedNoNetworkError) return + showedNoNetworkError = true + this.app.ports.showErrorNotification.send( + navigator.onLine + ? "I can't play this track because of a network error. I'll try to reconnect." + : "I can't play this track because we're offline. I'll try to reconnect." + ) +} - function showUnsupportedSrcErrorNotification() { - this.app.ports.showErrorNotification.send( - "__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." - ) - } +function showUnsupportedSrcErrorNotification() { + this.app.ports.showErrorNotification.send( + "__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." + ) +} function audioStalledEvent(event, notifyAppImmediately) { @@ -426,8 +426,8 @@ function isActiveAudioElement(orchestrion, node) { !node || node.getAttribute("data-preload") === "t" ) - ? false - : orchestrion.activeQueueItem.trackId === audioElementTrackId(node) + ? false + : orchestrion.activeQueueItem.trackId === audioElementTrackId(node) } @@ -492,10 +492,10 @@ export function setMediaSessionMetadata(queueItem, maybeArtwork) { let artwork = [] if (maybeArtwork && typeof maybeArtwork !== "string") { - artwork = [{ + artwork = [ { src: URL.createObjectURL(maybeArtwork), type: maybeArtwork.type - }] + } ] } navigator.mediaSession.metadata = new MediaMetadata({ diff --git a/src/Javascript/index.js b/src/Javascript/index.js index 28d5239a..0cf24be9 100644 --- a/src/Javascript/index.js +++ b/src/Javascript/index.js @@ -1,5 +1,4 @@ // -// Elm loader // | (• ◡•)| (❍ᴥ❍ʋ) // // The bit where we launch the Elm app, @@ -17,6 +16,7 @@ import * as audioEngine from "./audio-engine" import * as db from "./indexed-db" import { version } from '../../package.json' import { WEBNATIVE_STAGING_ENV, WEBNATIVE_STAGING_MODE, debounce, fileExtension } from "./common" +import { transformUrl } from "./urls" @@ -35,13 +35,13 @@ if (location.hostname.endsWith("diffuse.sh") && location.protocol === "http:") { location.href = location.href.replace("http://", "https://") failure("Just a moment, redirecting to HTTPS.") -// Not a secure context + // Not a secure context } else if (!self.isSecureContext) { failure(` This app only works on a secure context, HTTPS & localhost, and modern browsers. `) -// Service worker + // Service worker } else if ("serviceWorker" in navigator) { window.addEventListener("load", () => { navigator.serviceWorker @@ -193,10 +193,12 @@ wire.brain = () => { } -function handleAction(action, data, _ports) { switch (action) { - case "DOWNLOAD_TRACKS": return downloadTracks(data) - case "FINISHED_DOWNLOADING_ARTWORK": return finishedDownloadingArtwork() -}} +function handleAction(action, data, _ports) { + switch (action) { + case "DOWNLOAD_TRACKS": return downloadTracks(data) + case "FINISHED_DOWNLOADING_ARTWORK": return finishedDownloadingArtwork() + } +} @@ -257,10 +259,10 @@ function activeQueueItemChanged(item) { // 🎵 if (item) { const coverPrep = { - cacheKey: btoa(unescape(encodeURIComponent(item.trackTags.artist + " --- " + item.trackTags.album))), - trackFilename: item.trackPath.split("/").reverse()[0], - trackPath: item.trackPath, - trackSourceId: item.sourceId, + cacheKey: btoa(unescape(encodeURIComponent(item.trackTags.artist + " --- " + item.trackTags.album))), + trackFilename: item.trackPath.split("/").reverse()[ 0 ], + trackPath: item.trackPath, + trackSourceId: item.sourceId, variousArtists: "f" } @@ -285,7 +287,7 @@ function activeQueueItemChanged(item) { }) }) - // ✋ + // ✋ } else { app.ports.setAudioIsPlaying.send(false) app.ports.setAudioPosition.send(0) @@ -388,9 +390,9 @@ function averageColorOfImage(img) { const color = { r: 0, g: 0, b: 0 } for (let i = 0, l = imageData.data.length; i < l; i += 4) { - color.r += imageData.data[i] - color.g += imageData.data[i + 1] - color.b += imageData.data[i + 2] + color.r += imageData.data[ i ] + color.g += imageData.data[ i + 1 ] + color.b += imageData.data[ i + 2 ] } color.r = Math.floor(color.r / (imageData.data.length / 4)) @@ -432,12 +434,25 @@ wire.broadcastChannel = () => { // --------- wire.clipboard = () => { - app.ports.copyToClipboard.subscribe(copyToClipboard) + app.ports.copyToClipboard.subscribe(text => { + // TODO: Find a better solution for this + const adjustedText = (() => { + if (text.startsWith("dropbox://")) { + return transformUrl(text) + } else if (text.startsWith("google://")) { + return transformUrl(text) + } else { + return text + + } + })() + + copyToClipboard(copyToClipboard) + }) } function copyToClipboard(text) { - // Insert a textarea element const el = document.createElement("textarea") @@ -465,7 +480,6 @@ function copyToClipboard(text) { document.getSelection().removeAllRanges() document.getSelection().addRange(selected) } - } @@ -491,13 +505,13 @@ function gotCachedCover({ key, url }) { const item = orchestrion.activeQueueItem if (item && orchestrion.coverPrep && key === orchestrion.coverPrep.key && url) { - let artwork = [{ src: url }] + let artwork = [ { src: url } ] if (typeof url !== "string") { - artwork = [{ + artwork = [ { src: URL.createObjectURL(url), type: url.type - }] + } ] } navigator.mediaSession.metadata = new MediaMetadata({ @@ -524,10 +538,10 @@ function loadAlbumCoversFromDom({ coverView, list } = {}) { if (!nodes.length) return; const coverPrepList = nodes.map(node => ({ - cacheKey: node.getAttribute("data-key"), - trackFilename: node.getAttribute("data-filename"), - trackPath: node.getAttribute("data-path"), - trackSourceId: node.getAttribute("data-source-id"), + cacheKey: node.getAttribute("data-key"), + trackFilename: node.getAttribute("data-filename"), + trackPath: node.getAttribute("data-path"), + trackSourceId: node.getAttribute("data-source-id"), variousArtists: node.getAttribute("data-various-artists") })) @@ -566,7 +580,7 @@ function cachedCovers(keys) { const cacheKey = key.slice(11) if (blob && typeof blob !== "string") { - cache[cacheKey] = URL.createObjectURL(blob) + cache[ cacheKey ] = URL.createObjectURL(blob) } return cache @@ -618,16 +632,17 @@ function downloadTracks(group) { const folder = zip.folder("Diffuse - " + group.name) return group.tracks.reduce( - (acc, track) => { return acc - .then(_ => fetch(track.url)) - .then(r => { - const mimeType = r.headers.get("content-type") - const fileExt = fileExtension(mimeType) || "unknown" - - return r.blob().then( - b => folder.file(track.filename + "." + fileExt, b) - ) - }) + (acc, track) => { + return acc + .then(_ => fetch(track.url)) + .then(r => { + const mimeType = r.headers.get("content-type") + const fileExt = fileExtension(mimeType) || "unknown" + + return r.blob().then( + b => folder.file(track.filename + "." + fileExt, b) + ) + }) }, Promise.resolve() @@ -863,9 +878,9 @@ function touchPointerEvent(eventType, touchEvent, touch) { // Simulate `pointerenter` and `pointerleave` event for non-touch devices if (!self.PointerEvent) { document.addEventListener("mouseover", event => { - const section = document.body.querySelector("section") - const isDragging = section && section.classList.contains("dragging-something") - const node = isDragging && document.elementFromPoint(event.clientX, event.clientY) + const section = document.body.querySelector("section") + const isDragging = section && section.classList.contains("dragging-something") + const node = isDragging && document.elementFromPoint(event.clientX, event.clientY) if (node && node != enteredElement) { enteredElement && enteredElement.dispatchEvent(mousePointerEvent("pointerleave", event)) @@ -878,10 +893,10 @@ if (!self.PointerEvent) { // Simulate `pointerenter` and `pointerleave` event for touch devices document.body.addEventListener("touchmove", event => { - const section = document.body.querySelector("section") - const isDragging = section && section.classList.contains("dragging-something") + const section = document.body.querySelector("section") + const isDragging = section && section.classList.contains("dragging-something") - let touch = event.touches[0] + let touch = event.touches[ 0 ] let node if (isDragging && touch) { diff --git a/src/Javascript/processing.js b/src/Javascript/processing.js index 9edf26ec..ee74e834 100644 --- a/src/Javascript/processing.js +++ b/src/Javascript/processing.js @@ -15,21 +15,21 @@ import { transformUrl } from "./urls" // Contexts // -------- -export function processContext(context) { +export function processContext(context, app) { const initialPromise = Promise.resolve([]) return context.urlsForTags.reduce((accumulator, urls, idx) => { return accumulator.then(col => { const filename = context - .receivedFilePaths[idx] + .receivedFilePaths[ idx ] .split("/") - .reverse()[0] + .reverse()[ 0 ] return Promise.all([ - transformUrl(urls.headUrl), - transformUrl(urls.getUrl) + transformUrl(urls.headUrl, app), + transformUrl(urls.getUrl, app) - ]).then(([headUrl, getUrl]) => { + ]).then(([ headUrl, getUrl ]) => { return getTags(headUrl, getUrl, filename, { skipCovers: true }) }).then(r => { @@ -63,7 +63,7 @@ const parserConfiguration = Object.assign( export function getTags(headUrl, getUrl, filename, options) { const fileExtMatch = filename.match(/\.(\w+)$/) - const fileExt = fileExtMatch && fileExtMatch[1] + const fileExt = fileExtMatch && fileExtMatch[ 1 ] const overrideContentType = ( getUrl.includes("googleapis.com") || @@ -109,9 +109,9 @@ function pickTags(filename, result) { album: tags.album && tags.album.length ? tags.album : "Unknown", artist: artist || "Unknown", title: title ? title : (artist ? "Unknown" : filename.replace(/\.\w+$/, "")), - genre: (tags.genre && tags.genre[0]) || null, + genre: (tags.genre && tags.genre[ 0 ]) || null, year: tags.year || null, - picture: tags.picture ? tags.picture[0] : null + picture: tags.picture ? tags.picture[ 0 ] : null } } diff --git a/src/Javascript/urls.js b/src/Javascript/urls.js index 1fc54cc3..50aa9c3a 100644 --- a/src/Javascript/urls.js +++ b/src/Javascript/urls.js @@ -5,21 +5,27 @@ // Some URLs are special you know. -export function transformUrl(url) { +const EXPIRED_ACCESS_TOKENS = { + GOOGLE: {} +} + + +export async function transformUrl(url, app) { const parts = url.split("://") - switch (parts[0]) { + switch (parts[ 0 ]) { case "dropbox": { - const dropboxBits = parts[1].split("@") - const accessToken = dropboxBits[0] - const filePath = dropboxBits[1] + const dropboxBits = parts[ 1 ].split("@") + const accessToken = dropboxBits[ 0 ] + const filePath = dropboxBits[ 1 ] return fetch( "https://api.dropboxapi.com/2/files/get_temporary_link", - { method: "POST" - , body: JSON.stringify({ path: filePath }) - , headers: new Headers({ + { + method: "POST" + , body: JSON.stringify({ path: filePath }) + , headers: new Headers({ "Authorization": "Bearer " + accessToken, "Content-Type": "application/json" }) @@ -31,8 +37,73 @@ export function transformUrl(url) { ) } + case "google": { + let finalAccessToken + + const googleBits = parts[ 1 ].split("@") + const [ accessToken, expiresAtString, refreshToken, clientId, clientSecret ] = googleBits[ 0 ].split(":") + const fileId = googleBits[ 1 ] + + // Unix timestamp in milliseconds + const in15minutes = Date.now() + 15 * 60 * 1000 + const expiresAt = parseInt(expiresAtString, 10) + const isAlmostExpired = expiresAt <= in15minutes + + if (EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ]) { + const replacement = EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ] + + if (replacement.newExpiresAt <= in15minutes) { + finalAccessToken = await refreshGoogleAccessToken({ + clientId, clientSecret, refreshToken, oldToken: accessToken + }) + } else { + finalAccessToken = replacement.newToken + } + + } else if (isAlmostExpired) { + finalAccessToken = await refreshGoogleAccessToken({ + clientId, clientSecret, refreshToken, oldToken: accessToken + }) + + } else { + finalAccessToken = accessToken + + } + + return Promise.resolve( + `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(fileId)}?alt=media&bearer_token=${encodeURIComponent(finalAccessToken)}` + ) + } + default: return Promise.resolve(url) } } + + + +// GOOGLE + + +async function refreshGoogleAccessToken({ clientId, clientSecret, oldToken, refreshToken }) { + console.log("🔐 Refreshing Google Drive access token") + + const url = new URL("https://www.googleapis.com/oauth2/v4/token") + + url.searchParams.set("client_id", clientId) + url.searchParams.set("client_secret", clientSecret) + url.searchParams.set("refresh_token", refreshToken) + url.searchParams.set("grant_type", "refresh_token") + + const serverResponse = await fetch(url, { method: "POST" }).then(r => r.json()) + + EXPIRED_ACCESS_TOKENS.GOOGLE[ oldToken ] = { + oldToken, + newToken: serverResponse.access_token, + newExpiresAt: Date.now() + (serverResponse.expires_in * 1000), + refreshToken + } + + return serverResponse.access_token +} diff --git a/src/Library/Sources/Services.elm b/src/Library/Sources/Services.elm index c36b361a..dc2213ae 100644 --- a/src/Library/Sources/Services.elm +++ b/src/Library/Sources/Services.elm @@ -139,7 +139,7 @@ parseErrorResponse service = WebDav.parseErrorResponse -parsePreparationResponse : Service -> String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : Service -> String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse service = case service of AmazonS3 -> diff --git a/src/Library/Sources/Services/AmazonS3.elm b/src/Library/Sources/Services/AmazonS3.elm index a3c74d95..05416d6c 100644 --- a/src/Library/Sources/Services/AmazonS3.elm +++ b/src/Library/Sources/Services/AmazonS3.elm @@ -146,7 +146,7 @@ makeTree srcData marker currentTime resultMsg = {-| Re-export parser functions. -} -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse = noPrep diff --git a/src/Library/Sources/Services/AzureBlob.elm b/src/Library/Sources/Services/AzureBlob.elm index 1da75d20..8e59ab3c 100644 --- a/src/Library/Sources/Services/AzureBlob.elm +++ b/src/Library/Sources/Services/AzureBlob.elm @@ -124,7 +124,7 @@ makeTree srcData marker currentTime resultMsg = {-| Re-export parser functions. -} -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse = noPrep diff --git a/src/Library/Sources/Services/AzureFile.elm b/src/Library/Sources/Services/AzureFile.elm index 56a87dca..37b7a2e4 100644 --- a/src/Library/Sources/Services/AzureFile.elm +++ b/src/Library/Sources/Services/AzureFile.elm @@ -128,7 +128,7 @@ makeTree srcData marker currentTime resultMsg = {-| Re-export parser functions. -} -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse = noPrep diff --git a/src/Library/Sources/Services/Common.elm b/src/Library/Sources/Services/Common.elm index a8487a51..286e3e47 100644 --- a/src/Library/Sources/Services/Common.elm +++ b/src/Library/Sources/Services/Common.elm @@ -3,6 +3,7 @@ module Sources.Services.Common exposing (cleanPath, noPrep) import Sources exposing (..) import Sources.Processing exposing (..) import String.Ext as String +import Time @@ -43,6 +44,6 @@ cleanPath dirtyPath = -- PARSING -noPrep : String -> SourceData -> Marker -> PrepationAnswer Marker -noPrep _ srcData _ = +noPrep : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker +noPrep _ _ srcData _ = { sourceData = srcData, marker = TheEnd } diff --git a/src/Library/Sources/Services/Dropbox.elm b/src/Library/Sources/Services/Dropbox.elm index a130e066..0b98b58c 100644 --- a/src/Library/Sources/Services/Dropbox.elm +++ b/src/Library/Sources/Services/Dropbox.elm @@ -187,7 +187,7 @@ getProperDirectoryPath srcData = {-| Re-export parser functions. -} -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse = noPrep diff --git a/src/Library/Sources/Services/Google.elm b/src/Library/Sources/Services/Google.elm index 78056c00..6773a93e 100644 --- a/src/Library/Sources/Services/Google.elm +++ b/src/Library/Sources/Services/Google.elm @@ -250,7 +250,7 @@ makeTree srcData marker _ resultMsg = {-| Re-export parser functions. -} -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse = Parser.parsePreparationResponse @@ -289,7 +289,7 @@ We need this to play the track. -} makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ srcData _ path = +makeTrackUrl currentTime srcData _ path = let file = String.Path.file path @@ -300,12 +300,23 @@ makeTrackUrl _ srcData _ path = |> List.head |> Maybe.withDefault file - accessToken = - Dict.fetch "accessToken" "" srcData + now = + Time.posixToMillis currentTime + + expiresAt = + Dict.fetch "expiresAt" (String.fromInt now) srcData in String.concat - [ "https://www.googleapis.com/drive/v3/files/" - , Url.percentEncode fileId - , "?alt=media&bearer_token=" - , Url.percentEncode accessToken + [ "google://" + , Dict.fetch "accessToken" "" srcData + , ":" + , expiresAt + , ":" + , Dict.fetch "refreshToken" "" srcData + , ":" + , Dict.fetch "clientId" "" srcData + , ":" + , Dict.fetch "clientSecret" "" srcData + , "@" + , fileId ] diff --git a/src/Library/Sources/Services/Google/Parser.elm b/src/Library/Sources/Services/Google/Parser.elm index d2f0def6..ed2ad27a 100755 --- a/src/Library/Sources/Services/Google/Parser.elm +++ b/src/Library/Sources/Services/Google/Parser.elm @@ -9,20 +9,33 @@ import Sources.Pick import Sources.Processing exposing (Marker(..), PrepationAnswer, TreeAnswer) import Sources.Services.Google.Marker as Marker import String.Path +import Time -- PREPARATION -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker -parsePreparationResponse response srcData _ = +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse response currentTimePosix srcData _ = let newAccessToken = response |> decodeString (field "access_token" string) |> Result.withDefault "" + currentTime = + -- Current time in milliseconds + Time.posixToMillis currentTimePosix + + expiresAt = + -- Unix timestamp in milliseconds + response + |> decodeString (field "expires_in" int) + -- time in seconds + |> Result.withDefault 2500 + |> (\s -> currentTime + s * 1000) + maybeRefreshToken = response |> decodeString (maybe <| field "refresh_token" string) @@ -39,6 +52,7 @@ parsePreparationResponse response srcData _ = in srcData |> Dict.insert "accessToken" newAccessToken + |> Dict.insert "expiresAt" (String.fromInt expiresAt) |> refreshTokenUpdater |> Dict.remove "authCode" |> (\s -> { sourceData = s, marker = TheEnd }) diff --git a/src/Library/Sources/Services/Ipfs.elm b/src/Library/Sources/Services/Ipfs.elm index 8d7a3e0f..aa28d076 100644 --- a/src/Library/Sources/Services/Ipfs.elm +++ b/src/Library/Sources/Services/Ipfs.elm @@ -209,7 +209,7 @@ ipnsResolver response = {-| Re-export parser functions. -} -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse = Parser.parseDnsLookup diff --git a/src/Library/Sources/Services/Ipfs/Parser.elm b/src/Library/Sources/Services/Ipfs/Parser.elm index 29975186..8175f757 100755 --- a/src/Library/Sources/Services/Ipfs/Parser.elm +++ b/src/Library/Sources/Services/Ipfs/Parser.elm @@ -7,14 +7,15 @@ import Sources.Pick exposing (isMusicFile) import Sources.Processing exposing (Marker(..), PrepationAnswer, TreeAnswer) import Sources.Services.Ipfs.Marker as Marker import String.Ext as String +import Time -- PREPARATION -parseDnsLookup : String -> SourceData -> Marker -> PrepationAnswer Marker -parseDnsLookup response srcData _ = +parseDnsLookup : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker +parseDnsLookup response _ srcData _ = case decodeString dnsResultDecoder response of Ok path -> srcData diff --git a/src/Library/Sources/Services/WebDav.elm b/src/Library/Sources/Services/WebDav.elm index 85003d97..045c8f4b 100644 --- a/src/Library/Sources/Services/WebDav.elm +++ b/src/Library/Sources/Services/WebDav.elm @@ -130,7 +130,7 @@ makeTree srcData marker _ resultMsg = {-| Re-export parser functions. -} -parsePreparationResponse : String -> SourceData -> Marker -> PrepationAnswer Marker +parsePreparationResponse : String -> Time.Posix -> SourceData -> Marker -> PrepationAnswer Marker parsePreparationResponse = noPrep -- 2.51.2 From d105f99f80a899529ace32b3d2af20ac1dc74d3d Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 27 Aug 2022 23:07:31 +0200 Subject: [PATCH 2/5] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5982fb95..7a408960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - **Native builds with [Tauri](https://tauri.app/)**. - Fixes playback issue with Google Drive on Safari/iOS. +- Improves Google Drive support, access tokens are now refreshed when needed (before it only refreshed on source processing) ## 3.1.1 -- 2.51.2 From 39e636887680721f2faf0bf4f75d95c98e83cd26 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sun, 28 Aug 2022 19:06:50 +0200 Subject: [PATCH 3/5] Notify Elm apps of access token refreshing --- src/Applications/Brain.elm | 7 ++++ src/Applications/Brain/Other/State.elm | 22 +++++++++++ src/Applications/Brain/Ports.elm | 3 ++ .../Brain/Sources/Processing/Steps.elm | 4 +- src/Applications/Brain/Tracks/State.elm | 3 +- src/Applications/Brain/Types.elm | 1 + src/Applications/Brain/User/State.elm | 34 +++++++++------- src/Applications/UI.elm | 4 ++ src/Applications/UI/Other/State.elm | 5 +++ src/Applications/UI/Ports.elm | 3 ++ src/Applications/UI/Types.elm | 2 + src/Javascript/urls.js | 27 ++++++++----- src/Library/Alien.elm | 2 + src/Library/Queue.elm | 1 + src/Library/Sources/Refresh/AccessToken.elm | 39 +++++++++++++++++++ src/Library/Sources/Services.elm | 2 +- src/Library/Sources/Services/AmazonS3.elm | 4 +- src/Library/Sources/Services/AzureBlob.elm | 4 +- src/Library/Sources/Services/AzureFile.elm | 4 +- src/Library/Sources/Services/Btfs.elm | 4 +- src/Library/Sources/Services/Dropbox.elm | 4 +- src/Library/Sources/Services/Google.elm | 6 ++- src/Library/Sources/Services/Ipfs.elm | 4 +- src/Library/Sources/Services/WebDav.elm | 4 +- 24 files changed, 149 insertions(+), 44 deletions(-) create mode 100644 src/Library/Sources/Refresh/AccessToken.elm diff --git a/src/Applications/Brain.elm b/src/Applications/Brain.elm index 3f10d07e..0e97b74a 100644 --- a/src/Applications/Brain.elm +++ b/src/Applications/Brain.elm @@ -144,6 +144,9 @@ update msg = ----------------------------------------- -- 📭 Other ----------------------------------------- + RefreshedAccessToken a -> + Other.refreshedAccessToken a + SetCurrentTime a -> Other.setCurrentTime a @@ -160,6 +163,7 @@ subscriptions _ = Sub.batch [ Ports.fromAlien alien , Ports.makeArtworkTrackUrls MakeArtworkTrackUrls + , Ports.refreshedAccessToken RefreshedAccessToken , Ports.receiveSearchResults GotSearchResults , Ports.receiveTags (ProcessingMsg << Processing.TagsStep) , Ports.replaceTags ReplaceTrackTags @@ -230,6 +234,9 @@ translateAlienData tag data = Alien.ProcessSources -> ProcessingMsg (Processing.Process data) + Alien.RefreshedAccessToken -> + RefreshedAccessToken data + Alien.RemoveEncryptionKey -> UserMsg User.RemoveEncryptionKey diff --git a/src/Applications/Brain/Other/State.elm b/src/Applications/Brain/Other/State.elm index f7015a3b..54cdc278 100644 --- a/src/Applications/Brain/Other/State.elm +++ b/src/Applications/Brain/Other/State.elm @@ -7,6 +7,8 @@ import Brain.Types exposing (..) import Json.Decode as Json import Return import Return.Ext as Return +import Sources exposing (Service(..)) +import Sources.Refresh.AccessToken import Time @@ -14,6 +16,26 @@ import Time -- 🔱 +refreshedAccessToken : Json.Value -> Manager +refreshedAccessToken value model = + case Json.decodeValue Sources.Refresh.AccessToken.portArgumentsDecoder value of + Ok portArguments -> + case portArguments.service of + Google -> + -- TODO: + -- 1. Find source + -- 2. Add `access token` and `expires at` to source data + -- 3. Replace source in collection + -- 4. Save user data + Return.singleton model + + _ -> + Return.singleton model + + Err err -> + Common.reportUI Alien.ToCache (Json.errorToString err) model + + setCurrentTime : Time.Posix -> Manager setCurrentTime time model = Return.singleton { model | currentTime = time } diff --git a/src/Applications/Brain/Ports.elm b/src/Applications/Brain/Ports.elm index e1b308a0..dd1dba85 100644 --- a/src/Applications/Brain/Ports.elm +++ b/src/Applications/Brain/Ports.elm @@ -99,6 +99,9 @@ port makeArtworkTrackUrls : (Json.Value -> msg) -> Sub msg port receiveSearchResults : (List String -> msg) -> Sub msg +port refreshedAccessToken : (Json.Value -> msg) -> Sub msg + + port receiveTags : (ContextForTags -> msg) -> Sub msg diff --git a/src/Applications/Brain/Sources/Processing/Steps.elm b/src/Applications/Brain/Sources/Processing/Steps.elm index 71196fa2..61f1d55a 100644 --- a/src/Applications/Brain/Sources/Processing/Steps.elm +++ b/src/Applications/Brain/Sources/Processing/Steps.elm @@ -320,8 +320,8 @@ makeTrackUrls currentTime source filePaths = mapFn = \path -> - { getUrl = maker currentTime source.data Get path - , headUrl = maker currentTime source.data Head path + { getUrl = maker currentTime source.id source.data Get path + , headUrl = maker currentTime source.id source.data Head path } in List.map mapFn filePaths diff --git a/src/Applications/Brain/Tracks/State.elm b/src/Applications/Brain/Tracks/State.elm index c2ed2914..a30d7afc 100644 --- a/src/Applications/Brain/Tracks/State.elm +++ b/src/Applications/Brain/Tracks/State.elm @@ -306,6 +306,7 @@ makeTrackUrl timestamp trackPath maybeSource httpMethod = Sources.Services.makeTrackUrl source.service timestamp + source.id source.data httpMethod trackPath @@ -318,7 +319,7 @@ tagUrls : Time.Posix -> String -> Source -> TagUrls tagUrls currentTime path source = let maker = - Sources.Services.makeTrackUrl source.service currentTime source.data + Sources.Services.makeTrackUrl source.service currentTime source.id source.data in { getUrl = maker Get path , headUrl = maker Head path diff --git a/src/Applications/Brain/Types.elm b/src/Applications/Brain/Types.elm index 35227c1d..d15fa3a2 100644 --- a/src/Applications/Brain/Types.elm +++ b/src/Applications/Brain/Types.elm @@ -66,6 +66,7 @@ type Msg ----------------------------------------- -- 📭 Other ----------------------------------------- + | RefreshedAccessToken Json.Value | SetCurrentTime Time.Posix | ToCache Json.Value diff --git a/src/Applications/Brain/User/State.elm b/src/Applications/Brain/User/State.elm index 036277fb..c5b72a94 100644 --- a/src/Applications/Brain/User/State.elm +++ b/src/Applications/Brain/User/State.elm @@ -840,21 +840,6 @@ methodRetrieved json model = terminate NotAuthenticated model -refreshedDropboxTokens : - { currentTime : Int, refreshToken : String } - -> Dropbox.Tokens - -> User.Msg - -> Manager -refreshedDropboxTokens { currentTime, refreshToken } tokens msg model = - { accessToken = tokens.accessToken - , expiresAt = currentTime + tokens.expiresIn - , refreshToken = refreshToken - } - |> Dropbox - |> (\m -> saveMethod m model) - |> andThen (update msg) - - retrieveMethod : Manager retrieveMethod = Alien.AuthMethod @@ -955,3 +940,22 @@ terminate t model = model |> Common.nudgeUI Alien.NotAuthenticated |> andThen (Common.nudgeUI Alien.HideLoadingScreen) + + + +-- 📭 ░░ OTHER + + +refreshedDropboxTokens : + { currentTime : Int, refreshToken : String } + -> Dropbox.Tokens + -> User.Msg + -> Manager +refreshedDropboxTokens { currentTime, refreshToken } tokens msg model = + { accessToken = tokens.accessToken + , expiresAt = currentTime + tokens.expiresIn + , refreshToken = refreshToken + } + |> Dropbox + |> (\m -> saveMethod m model) + |> andThen (update msg) diff --git a/src/Applications/UI.elm b/src/Applications/UI.elm index 66eb9f56..c3e6e0b8 100644 --- a/src/Applications/UI.elm +++ b/src/Applications/UI.elm @@ -529,6 +529,9 @@ update msg = InstallingServiceWorker -> Other.installingServiceWorker + RedirectToBrain a -> + Other.redirectToBrain a + ReloadApp -> Other.reloadApp @@ -603,6 +606,7 @@ subscriptions _ = ----------------------------------------- , Ports.installedNewServiceWorker (\_ -> InstalledServiceWorker) , Ports.installingNewServiceWorker (\_ -> InstallingServiceWorker) + , Ports.refreshedAccessToken (Alien.broadcast Alien.RefreshedAccessToken >> RedirectToBrain) , Ports.setIsOnline SetIsOnline , Ports.webnativeResponse GotWebnativeResponse , Sub.map KeyboardMsg Keyboard.subscriptions diff --git a/src/Applications/UI/Other/State.elm b/src/Applications/UI/Other/State.elm index fbe180cd..aeb0cf0f 100644 --- a/src/Applications/UI/Other/State.elm +++ b/src/Applications/UI/Other/State.elm @@ -31,6 +31,11 @@ installingServiceWorker model = Return.singleton { model | serviceWorkerStatus = InstallingNew } +redirectToBrain : Alien.Event -> Manager +redirectToBrain event model = + return model (Ports.toBrain event) + + reloadApp : Manager reloadApp model = return model (Ports.reloadApp ()) diff --git a/src/Applications/UI/Ports.elm b/src/Applications/UI/Ports.elm index aa186aaa..51951e24 100644 --- a/src/Applications/UI/Ports.elm +++ b/src/Applications/UI/Ports.elm @@ -83,6 +83,9 @@ port installingNewServiceWorker : (() -> msg) -> Sub msg port noteProgress : ({ trackId : String, progress : Float } -> msg) -> Sub msg +port refreshedAccessToken : (Json.Value -> msg) -> Sub msg + + port preferredColorSchemaChanged : ({ dark : Bool } -> msg) -> Sub msg diff --git a/src/Applications/UI/Types.elm b/src/Applications/UI/Types.elm index c3d85612..3cfff2cc 100644 --- a/src/Applications/UI/Types.elm +++ b/src/Applications/UI/Types.elm @@ -1,6 +1,7 @@ module UI.Types exposing (..) import Alfred exposing (Alfred) +import Alien import Browser import Browser.Navigation as Nav import Color exposing (Color) @@ -305,6 +306,7 @@ type Msg ----------------------------------------- | InstalledServiceWorker | InstallingServiceWorker + | RedirectToBrain Alien.Event | ReloadApp | SetCurrentTime Time.Posix | SetCurrentTimeZone Time.Zone diff --git a/src/Javascript/urls.js b/src/Javascript/urls.js index 50aa9c3a..b75afb6e 100644 --- a/src/Javascript/urls.js +++ b/src/Javascript/urls.js @@ -41,20 +41,20 @@ export async function transformUrl(url, app) { let finalAccessToken const googleBits = parts[ 1 ].split("@") - const [ accessToken, expiresAtString, refreshToken, clientId, clientSecret ] = googleBits[ 0 ].split(":") + const [ accessToken, expiresAtString, refreshToken, clientId, clientSecret, srcId ] = googleBits[ 0 ].split(":") const fileId = googleBits[ 1 ] // Unix timestamp in milliseconds - const in15minutes = Date.now() + 15 * 60 * 1000 + const inXminutes = Date.now() + 5 * 60 * 1000 // 5 minutes const expiresAt = parseInt(expiresAtString, 10) - const isAlmostExpired = expiresAt <= in15minutes + const isAlmostExpired = expiresAt <= inXminutes if (EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ]) { const replacement = EXPIRED_ACCESS_TOKENS.GOOGLE[ accessToken ] - if (replacement.newExpiresAt <= in15minutes) { + if (replacement.newExpiresAt <= inXminutes) { finalAccessToken = await refreshGoogleAccessToken({ - clientId, clientSecret, refreshToken, oldToken: accessToken + app, clientId, clientSecret, refreshToken, srcId, oldToken: accessToken }) } else { finalAccessToken = replacement.newToken @@ -62,7 +62,7 @@ export async function transformUrl(url, app) { } else if (isAlmostExpired) { finalAccessToken = await refreshGoogleAccessToken({ - clientId, clientSecret, refreshToken, oldToken: accessToken + app, clientId, clientSecret, refreshToken, srcId, oldToken: accessToken }) } else { @@ -86,7 +86,7 @@ export async function transformUrl(url, app) { // GOOGLE -async function refreshGoogleAccessToken({ clientId, clientSecret, oldToken, refreshToken }) { +async function refreshGoogleAccessToken({ app, clientId, clientSecret, oldToken, refreshToken, srcId }) { console.log("🔐 Refreshing Google Drive access token") const url = new URL("https://www.googleapis.com/oauth2/v4/token") @@ -97,13 +97,22 @@ async function refreshGoogleAccessToken({ clientId, clientSecret, oldToken, refr url.searchParams.set("grant_type", "refresh_token") const serverResponse = await fetch(url, { method: "POST" }).then(r => r.json()) + const newToken = serverResponse.access_token + const newExpiresAt = Date.now() + (serverResponse.expires_in * 1000) EXPIRED_ACCESS_TOKENS.GOOGLE[ oldToken ] = { oldToken, - newToken: serverResponse.access_token, - newExpiresAt: Date.now() + (serverResponse.expires_in * 1000), + newToken, + newExpiresAt, refreshToken } + app.ports.refreshAccessToken.send({ + service: "google", + sourceId: srcId, + accessToken: newToken, + expiresAt: newExpiresAt + }) + return serverResponse.access_token } diff --git a/src/Library/Alien.elm b/src/Library/Alien.elm index 574a98ad..abf64e46 100644 --- a/src/Library/Alien.elm +++ b/src/Library/Alien.elm @@ -40,6 +40,7 @@ type Tag | DownloadTracks | ImportLegacyData | ProcessSources + | RefreshedAccessToken | RemoveEncryptionKey | RemoveTracksBySourceId | RemoveTracksFromCache @@ -98,6 +99,7 @@ enum = , ( "DOWNLOAD_TRACKS", DownloadTracks ) , ( "IMPORT_LEGACY_DATA", ImportLegacyData ) , ( "PROCESS_SOURCES", ProcessSources ) + , ( "REFRESHED_ACCESS_TOKEN", RefreshedAccessToken ) , ( "REMOVE_ENCRYPTION_KEY", RemoveEncryptionKey ) , ( "REMOVE_TRACKS_BY_SOURCE_ID", RemoveTracksBySourceId ) , ( "REMOVE_TRACKS_FROM_CACHE", RemoveTracksFromCache ) diff --git a/src/Library/Queue.elm b/src/Library/Queue.elm index 405810ce..da6008a1 100644 --- a/src/Library/Queue.elm +++ b/src/Library/Queue.elm @@ -70,6 +70,7 @@ makeTrackUrl_ timestamp track source = Sources.Services.makeTrackUrl source.service timestamp + source.id source.data Get track.path diff --git a/src/Library/Sources/Refresh/AccessToken.elm b/src/Library/Sources/Refresh/AccessToken.elm new file mode 100644 index 00000000..2152a5a9 --- /dev/null +++ b/src/Library/Sources/Refresh/AccessToken.elm @@ -0,0 +1,39 @@ +module Sources.Refresh.AccessToken exposing (..) + +import Json.Decode exposing (Decoder) +import Sources exposing (Service) +import Sources.Encoding exposing (serviceDecoder) + + + +-- 🌳 + + +type alias PortArguments = + { service : Service + , sourceId : String + , accessToken : String + + -- Unix timestamp in milliseconds + , expiresAt : Int + } + + + +-- 🛠 + + +portArgumentsDecoder : Decoder PortArguments +portArgumentsDecoder = + Json.Decode.map4 + (\service sourceId accessToken expiresAt -> + { service = service + , sourceId = sourceId + , accessToken = accessToken + , expiresAt = expiresAt + } + ) + (Json.Decode.field "service" serviceDecoder) + (Json.Decode.field "sourceId" Json.Decode.string) + (Json.Decode.field "accessToken" Json.Decode.string) + (Json.Decode.field "expiresAt" Json.Decode.int) diff --git a/src/Library/Sources/Services.elm b/src/Library/Sources/Services.elm index dc2213ae..9bf3d781 100644 --- a/src/Library/Sources/Services.elm +++ b/src/Library/Sources/Services.elm @@ -49,7 +49,7 @@ initialData service = WebDav.initialData -makeTrackUrl : Service -> Time.Posix -> SourceData -> HttpMethod -> String -> String +makeTrackUrl : Service -> Time.Posix -> String -> SourceData -> HttpMethod -> String -> String makeTrackUrl service = case service of AmazonS3 -> diff --git a/src/Library/Sources/Services/AmazonS3.elm b/src/Library/Sources/Services/AmazonS3.elm index 05416d6c..34cc66bc 100644 --- a/src/Library/Sources/Services/AmazonS3.elm +++ b/src/Library/Sources/Services/AmazonS3.elm @@ -185,6 +185,6 @@ We need this to play the track. Creates a presigned url that's valid for 48 hours -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime srcData method pathToFile = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl currentTime _ srcData method pathToFile = presignedUrl method 172800 [] currentTime srcData pathToFile diff --git a/src/Library/Sources/Services/AzureBlob.elm b/src/Library/Sources/Services/AzureBlob.elm index 8e59ab3c..d9ca88f0 100644 --- a/src/Library/Sources/Services/AzureBlob.elm +++ b/src/Library/Sources/Services/AzureBlob.elm @@ -163,6 +163,6 @@ We need this to play the track. (!) Creates a presigned url that's valid for 48 hours -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime srcData _ pathToFile = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl currentTime _ srcData _ pathToFile = presignedUrl Blob Read Get 48 currentTime srcData pathToFile [] diff --git a/src/Library/Sources/Services/AzureFile.elm b/src/Library/Sources/Services/AzureFile.elm index 37b7a2e4..e49a68bb 100644 --- a/src/Library/Sources/Services/AzureFile.elm +++ b/src/Library/Sources/Services/AzureFile.elm @@ -167,6 +167,6 @@ We need this to play the track. (!) Creates a presigned url that's valid for 48 hours -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime srcData _ pathToFile = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl currentTime _ srcData _ pathToFile = presignedUrl File Read Get 48 currentTime srcData pathToFile [] diff --git a/src/Library/Sources/Services/Btfs.elm b/src/Library/Sources/Services/Btfs.elm index 2c6d295e..07a22d5e 100644 --- a/src/Library/Sources/Services/Btfs.elm +++ b/src/Library/Sources/Services/Btfs.elm @@ -116,6 +116,6 @@ postProcessTree = We need this to play the track. -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ srcData _ path = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl _ _ srcData _ path = Ipfs.extractGateway srcData ++ "/btfs/" ++ Ipfs.rootHash srcData ++ "/" ++ Ipfs.encodedPath path diff --git a/src/Library/Sources/Services/Dropbox.elm b/src/Library/Sources/Services/Dropbox.elm index 0b98b58c..130b52d4 100644 --- a/src/Library/Sources/Services/Dropbox.elm +++ b/src/Library/Sources/Services/Dropbox.elm @@ -225,6 +225,6 @@ postProcessTree = We need this to play the track. -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ srcData _ pathToFile = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl _ _ srcData _ pathToFile = "dropbox://" ++ Dict.fetch "accessToken" "" srcData ++ "@" ++ pathToFile diff --git a/src/Library/Sources/Services/Google.elm b/src/Library/Sources/Services/Google.elm index 6773a93e..94a62670 100644 --- a/src/Library/Sources/Services/Google.elm +++ b/src/Library/Sources/Services/Google.elm @@ -288,8 +288,8 @@ postProcessTree = We need this to play the track. -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl currentTime srcData _ path = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl currentTime srcId srcData _ path = let file = String.Path.file path @@ -317,6 +317,8 @@ makeTrackUrl currentTime srcData _ path = , Dict.fetch "clientId" "" srcData , ":" , Dict.fetch "clientSecret" "" srcData + , ":" + , srcId , "@" , fileId ] diff --git a/src/Library/Sources/Services/Ipfs.elm b/src/Library/Sources/Services/Ipfs.elm index aa28d076..fc68d6f0 100644 --- a/src/Library/Sources/Services/Ipfs.elm +++ b/src/Library/Sources/Services/Ipfs.elm @@ -247,8 +247,8 @@ postProcessTree = We need this to play the track. -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ srcData _ path = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl _ _ srcData _ path = if not (String.contains "/" path) && not (String.contains "." path) then -- If it still uses the old way of doing things -- (ie. each path was a cid) diff --git a/src/Library/Sources/Services/WebDav.elm b/src/Library/Sources/Services/WebDav.elm index 045c8f4b..f8280463 100644 --- a/src/Library/Sources/Services/WebDav.elm +++ b/src/Library/Sources/Services/WebDav.elm @@ -168,8 +168,8 @@ postProcessTree = We need this to play the track. -} -makeTrackUrl : Time.Posix -> SourceData -> HttpMethod -> String -> String -makeTrackUrl _ srcData _ filePath = +makeTrackUrl : Time.Posix -> String -> SourceData -> HttpMethod -> String -> String +makeTrackUrl _ _ srcData _ filePath = url { addAuth = True } srcData filePath -- 2.51.2 From e573ea4a1116fc0429c6dfd5c2b5b5d69becee59 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 3 Sep 2022 18:01:33 +0200 Subject: [PATCH 4/5] Update Nix dependencies --- flake.lock | 30 +++++++++++++++--------------- nix/shell.nix | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/flake.lock b/flake.lock index 22657af5..dbfccea6 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "flake-utils": { "locked": { - "lastModified": 1656928814, - "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "owner": "numtide", "repo": "flake-utils", - "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "type": "github" }, "original": { @@ -17,11 +17,11 @@ }, "flake-utils_2": { "locked": { - "lastModified": 1656065134, - "narHash": "sha256-oc6E6ByIw3oJaIyc67maaFcnjYOz1mMcOtHxbEf9NwQ=", + "lastModified": 1656928814, + "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", "owner": "numtide", "repo": "flake-utils", - "rev": "bee6a7250dd1b01844a2de7e02e4df7d8a0a206c", + "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", "type": "github" }, "original": { @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1658380158, - "narHash": "sha256-DBunkegKWlxPZiOcw3/SNIFg93amkdGIy2g0y/jDpHg=", + "lastModified": 1662019588, + "narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a65b5b3f5504b8b89c196aba733bdf2b0bd13c16", + "rev": "2da64a81275b68fdad38af669afeda43d401e94b", "type": "github" }, "original": { @@ -48,11 +48,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1656401090, - "narHash": "sha256-bUS2nfQsvTQW2z8SK7oEFSElbmoBahOPtbXPm0AL3I4=", + "lastModified": 1659102345, + "narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "16de63fcc54e88b9a106a603038dd5dd2feb21eb", + "rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7", "type": "github" }, "original": { @@ -75,11 +75,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1658544517, - "narHash": "sha256-ipu69vA0a6AcWZqnHLzeRhvnVZbURXALHLIsqQWtJe4=", + "lastModified": 1662173844, + "narHash": "sha256-+ZgW98Y8fZkgFSylE+Mzalumw+kw3SVivZznbJqQaj8=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "4bd340885e39e0625fc6dda8a5a9d13c921ebb96", + "rev": "8ac6d40380dc4ec86f1ff591d5c14c8ae1d77a18", "type": "github" }, "original": { diff --git a/nix/shell.nix b/nix/shell.nix index d3637ad1..cb80a9ed 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -15,7 +15,7 @@ stack-wrapped = symlinkJoin { name = "stack"; - paths = [ stack ]; + paths = [ haskellPackages.stack ]; buildInputs = [ makeWrapper ]; postBuild = '' wrapProgram $out/bin/stack \ -- 2.51.2 From 9380ba3192d5653110fbe938648bdd5bed86efe0 Mon Sep 17 00:00:00 2001 From: Steven Vandevelde Date: Sat, 3 Sep 2022 22:36:04 +0200 Subject: [PATCH 5/5] Update source and save collection --- src/Applications/Brain/Other/State.elm | 29 +++++++++++++++++++------- src/Javascript/Workers/service.js | 12 ++++++----- src/Javascript/audio-engine.js | 4 ++-- src/Javascript/urls.js | 4 ++-- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/Applications/Brain/Other/State.elm b/src/Applications/Brain/Other/State.elm index 54cdc278..2950f73e 100644 --- a/src/Applications/Brain/Other/State.elm +++ b/src/Applications/Brain/Other/State.elm @@ -4,10 +4,13 @@ import Alien import Brain.Common.State as Common import Brain.Ports as Ports import Brain.Types exposing (..) +import Dict import Json.Decode as Json -import Return +import List.Extra as List +import Return exposing (return) import Return.Ext as Return import Sources exposing (Service(..)) +import Sources.Encoding import Sources.Refresh.AccessToken import Time @@ -22,12 +25,24 @@ refreshedAccessToken value model = Ok portArguments -> case portArguments.service of Google -> - -- TODO: - -- 1. Find source - -- 2. Add `access token` and `expires at` to source data - -- 3. Replace source in collection - -- 4. Save user data - Return.singleton model + model.hypaethralUserData.sources + |> List.find (.id >> (==) portArguments.sourceId) + |> Maybe.map + (\source -> + source.data + |> Dict.insert "accessToken" portArguments.accessToken + |> Dict.insert "expiresAt" (String.fromInt portArguments.expiresAt) + |> (\newData -> { source | data = newData }) + ) + |> Maybe.map + (\source -> + source + |> Sources.Encoding.encode + |> Alien.broadcast Alien.UpdateSourceData + |> Ports.toUI + ) + |> Maybe.withDefault Cmd.none + |> return model _ -> Return.singleton model diff --git a/src/Javascript/Workers/service.js b/src/Javascript/Workers/service.js index 49947f06..7907495b 100644 --- a/src/Javascript/Workers/service.js +++ b/src/Javascript/Workers/service.js @@ -13,8 +13,8 @@ const KEY = const EXCLUDE = [ "_headers" - , "_redirects" - , "CORS" + , "_redirects" + , "CORS" ] @@ -79,7 +79,7 @@ self.addEventListener("fetch", event => { "Basic " + token ) - // When doing a request with access token in the url, put it in the headers instead + // When doing a request with access token in the url, put it in the headers instead } else if (event.request.url.includes("bearer_token=")) { const url = new URL(event.request.url) const token = url.searchParams.get("bearer_token") @@ -95,7 +95,7 @@ self.addEventListener("fetch", event => { "Bearer " + token ) - // Use cache if internal request and not using native app + // Use cache if internal request and not using native app } else if (isInternal) { event.respondWith( isNativeWrapper @@ -152,7 +152,7 @@ function newRequestWithAuth(event, urlWithoutToken, authToken) { request.headers.entries() ) - newHeaders["authorization"] = authToken + newHeaders[ "authorization" ] = authToken const newRequest = new Request( new Request(urlWithoutToken, event.request), @@ -170,6 +170,8 @@ function newRequestWithAuth(event, urlWithoutToken, authToken) { let retries = 0 + // TODO: When request fails because access token is expired, + // refresh the token, and retry the request. const makeFetch = () => fetch(newRequest).then(r => { if (r.ok) { retries = 0 diff --git a/src/Javascript/audio-engine.js b/src/Javascript/audio-engine.js index 0d79dd06..6aa8f12d 100644 --- a/src/Javascript/audio-engine.js +++ b/src/Javascript/audio-engine.js @@ -214,7 +214,7 @@ export function preloadAudioElement(orchestrion, queueItem) { n => n.parentNode.removeChild(n) ) - // audio element remains valid for 2 hours + // audio element remains valid for 45 minutes transformUrl(queueItem.url, orchestrion.app).then(url => { const queueItemWithTransformedUrl = Object.assign({}, queueItem, { url: url }) @@ -222,7 +222,7 @@ export function preloadAudioElement(orchestrion, queueItem) { createAudioElement( orchestrion, queueItemWithTransformedUrl, - Date.now() + 1000 * 60 * 60 * 2, + Date.now() + 1000 * 60 * 45, true ) }) diff --git a/src/Javascript/urls.js b/src/Javascript/urls.js index b75afb6e..5f9fd85f 100644 --- a/src/Javascript/urls.js +++ b/src/Javascript/urls.js @@ -107,8 +107,8 @@ async function refreshGoogleAccessToken({ app, clientId, clientSecret, oldToken, refreshToken } - app.ports.refreshAccessToken.send({ - service: "google", + app.ports.refreshedAccessToken.send({ + service: "Google", sourceId: srcId, accessToken: newToken, expiresAt: newExpiresAt