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