[web] Switch to Spotify Web SDK

This commit is contained in:
Alain Nussbaumer 2025-08-17 12:28:32 +10:00
parent b612e12aca
commit 978a9b6a96
12 changed files with 397 additions and 424 deletions

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"@aacassandra/vue3-progressbar": "^1.0.3", "@aacassandra/vue3-progressbar": "^1.0.3",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@spotify/web-api-ts-sdk": "^1.2.0",
"@ts-pro/vue-eternal-loading": "^1.3.1", "@ts-pro/vue-eternal-loading": "^1.3.1",
"axios": "^1.11.0", "axios": "^1.11.0",
"bulma": "^1.0.4", "bulma": "^1.0.4",
@ -21,7 +22,6 @@
"mdi-vue": "^3.0.13", "mdi-vue": "^3.0.13",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"reconnectingwebsocket": "^1.0.0", "reconnectingwebsocket": "^1.0.0",
"spotify-web-api-js": "^1.5.2",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-i18n": "^11.1.11", "vue-i18n": "^11.1.11",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
@ -31,12 +31,12 @@
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^6.0.8", "@intlify/unplugin-vue-i18n": "^6.0.8",
"@vitejs/plugin-vue": "^6.0.0", "@vitejs/plugin-vue": "^6.0.1",
"eslint": "^9.32.0", "eslint": "^9.33.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-vue": "^10.3.0", "eslint-plugin-vue": "^10.4.0",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"sass": "^1.89.2", "sass": "^1.90.0",
"vite": "^7.0.6" "vite": "^7.1.2"
} }
} }

View File

@ -1,3 +1,4 @@
import { SpotifyApi } from '@spotify/web-api-ts-sdk'
import api from '@/api' import api from '@/api'
export default { export default {
@ -14,6 +15,11 @@ export default {
return api.get('./api/spotify-logout') return api.get('./api/spotify-logout')
}, },
spotify() { spotify() {
return api.get('./api/spotify') return api.get('./api/spotify').then((configuration) => {
const sdk = SpotifyApi.withAccessToken(configuration.webapi_client_id, {
access_token: configuration.webapi_token
})
return { api: sdk, configuration }
})
} }
} }

View File

@ -14,9 +14,9 @@
<script> <script>
import ListProperties from '@/components/ListProperties.vue' import ListProperties from '@/components/ListProperties.vue'
import ModalDialog from '@/components/ModalDialog.vue' import ModalDialog from '@/components/ModalDialog.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import player from '@/api/player' import player from '@/api/player'
import queue from '@/api/queue' import queue from '@/api/queue'
import services from '@/api/services'
import { useServicesStore } from '@/stores/services' import { useServicesStore } from '@/stores/services'
export default { export default {
@ -90,16 +90,17 @@ export default {
watch: { watch: {
item() { item() {
if (this.item?.data_kind === 'spotify') { if (this.item?.data_kind === 'spotify') {
const spotifyApi = new SpotifyWebApi() return services.spotify().then(({ api }) => {
spotifyApi.setAccessToken(this.servicesStore.spotify.webapi_token) const trackId = this.item.path.slice(
spotifyApi this.item.path.lastIndexOf(':') + 1
.getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1)) )
.then((response) => { return api.tracks.get(trackId).then((response) => {
this.spotifyTrack = response this.spotifyTrack = response
}) })
} else { })
this.spotifyTrack = {}
} }
this.spotifyTrack = {}
return {}
} }
}, },
methods: { methods: {

View File

@ -28,7 +28,6 @@ import ControlImage from '@/components/ControlImage.vue'
import ListTracksSpotify from '@/components/ListTracksSpotify.vue' import ListTracksSpotify from '@/components/ListTracksSpotify.vue'
import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue' import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue'
import PaneHero from '@/components/PaneHero.vue' import PaneHero from '@/components/PaneHero.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import queue from '@/api/queue' import queue from '@/api/queue'
import services from '@/api/services' import services from '@/api/services'
import { useServicesStore } from '@/stores/services' import { useServicesStore } from '@/stores/services'
@ -43,13 +42,9 @@ export default {
PaneHero PaneHero
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
const spotifyApi = new SpotifyWebApi() services.spotify().then(({ api, configuration }) => {
services.spotify().then((data) => { api.albums
spotifyApi.setAccessToken(data.webapi_token) .get(to.params.id, configuration.webapi_country)
spotifyApi
.getAlbum(to.params.id, {
market: useServicesStore().spotify.webapi_country
})
.then((album) => { .then((album) => {
next((vm) => { next((vm) => {
vm.album = album vm.album = album

View File

@ -28,10 +28,8 @@ import ControlButton from '@/components/ControlButton.vue'
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue' import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
import ModalDialogArtistSpotify from '@/components/ModalDialogArtistSpotify.vue' import ModalDialogArtistSpotify from '@/components/ModalDialogArtistSpotify.vue'
import PaneTitle from '@/components/PaneTitle.vue' import PaneTitle from '@/components/PaneTitle.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import queue from '@/api/queue' import queue from '@/api/queue'
import services from '@/api/services' import services from '@/api/services'
import { useServicesStore } from '@/stores/services'
const PAGE_SIZE = 50 const PAGE_SIZE = 50
@ -45,17 +43,16 @@ export default {
PaneTitle PaneTitle
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
services.spotify().then((data) => { services.spotify().then(({ api, configuration }) => {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(data.webapi_token)
Promise.all([ Promise.all([
spotifyApi.getArtist(to.params.id), api.artists.get(to.params.id),
spotifyApi.getArtistAlbums(to.params.id, { api.artists.albums(
include_groups: 'album,single', to.params.id,
limit: PAGE_SIZE, 'album,single',
market: useServicesStore().spotify.webapi_country, configuration.webapi_country,
offset: 0 PAGE_SIZE,
}) 0
)
]).then(([artist, albums]) => { ]).then(([artist, albums]) => {
next((vm) => { next((vm) => {
vm.artist = artist vm.artist = artist
@ -66,9 +63,6 @@ export default {
}) })
}) })
}, },
setup() {
return { servicesStore: useServicesStore() }
},
data() { data() {
return { return {
albums: [], albums: [],
@ -93,15 +87,15 @@ export default {
this.offset += data.limit this.offset += data.limit
}, },
load({ loaded }) { load({ loaded }) {
services.spotify().then((data) => { services.spotify().then(({ api, configuration }) => {
const spotifyApi = new SpotifyWebApi() api.artists
spotifyApi.setAccessToken(data.webapi_token) .albums(
spotifyApi this.artist.id,
.getArtistAlbums(this.artist.id, { 'album,single',
include_groups: 'album,single', configuration.webapi_country,
limit: PAGE_SIZE, PAGE_SIZE,
offset: this.offset this.offset
}) )
.then((albums) => { .then((albums) => {
this.appendAlbums(albums) this.appendAlbums(albums)
loaded(albums.items.length, PAGE_SIZE) loaded(albums.items.length, PAGE_SIZE)

View File

@ -41,7 +41,6 @@ import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue' import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue' import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue'
import PaneTitle from '@/components/PaneTitle.vue' import PaneTitle from '@/components/PaneTitle.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import TabsMusic from '@/components/TabsMusic.vue' import TabsMusic from '@/components/TabsMusic.vue'
import services from '@/api/services' import services from '@/api/services'
@ -55,18 +54,15 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
services.spotify().then((data) => { services.spotify().then(({ api, configuration }) => {
const spotifyApi = new SpotifyWebApi()
spotifyApi.setAccessToken(data.webapi_token)
Promise.all([ Promise.all([
spotifyApi.getNewReleases({ api.browse.getNewReleases(configuration.webapi_country, 3),
country: data.webapi_country, api.browse.getFeaturedPlaylists(
limit: 3 configuration.webapi_country,
}), null,
spotifyApi.getFeaturedPlaylists({ null,
country: data.webapi_country, 3
limit: 3 )
})
]).then((response) => { ]).then((response) => {
next((vm) => { next((vm) => {
vm.albums = response[0].albums.items vm.albums = response[0].albums.items

View File

@ -14,7 +14,6 @@
import ContentWithHeading from '@/templates/ContentWithHeading.vue' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue' import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue'
import PaneTitle from '@/components/PaneTitle.vue' import PaneTitle from '@/components/PaneTitle.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import TabsMusic from '@/components/TabsMusic.vue' import TabsMusic from '@/components/TabsMusic.vue'
import services from '@/api/services' import services from '@/api/services'
@ -27,14 +26,9 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
services.spotify().then((data) => { services.spotify().then(({ api, configuration }) => {
const spotifyApi = new SpotifyWebApi() api.browse
spotifyApi.setAccessToken(data.webapi_token) .getFeaturedPlaylists(configuration.webapi_country, null, null, 50)
spotifyApi
.getFeaturedPlaylists({
country: data.webapi_country,
limit: 50
})
.then((response) => { .then((response) => {
next((vm) => { next((vm) => {
vm.playlists = response.playlists.items vm.playlists = response.playlists.items

View File

@ -14,7 +14,6 @@
import ContentWithHeading from '@/templates/ContentWithHeading.vue' import ContentWithHeading from '@/templates/ContentWithHeading.vue'
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue' import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
import PaneTitle from '@/components/PaneTitle.vue' import PaneTitle from '@/components/PaneTitle.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import TabsMusic from '@/components/TabsMusic.vue' import TabsMusic from '@/components/TabsMusic.vue'
import services from '@/api/services' import services from '@/api/services'
@ -27,14 +26,9 @@ export default {
TabsMusic TabsMusic
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
services.spotify().then((data) => { services.spotify().then(({ api, configuration }) => {
const spotifyApi = new SpotifyWebApi() api.browse
spotifyApi.setAccessToken(data.webapi_token) .getNewReleases(configuration.webapi_country, 50)
spotifyApi
.getNewReleases({
country: data.webapi_country,
limit: 50
})
.then((response) => { .then((response) => {
next((vm) => { next((vm) => {
vm.albums = response.albums.items vm.albums = response.albums.items

View File

@ -37,9 +37,8 @@ import ControlButton from '@/components/ControlButton.vue'
import ListTracksSpotify from '@/components/ListTracksSpotify.vue' import ListTracksSpotify from '@/components/ListTracksSpotify.vue'
import ModalDialogPlaylistSpotify from '@/components/ModalDialogPlaylistSpotify.vue' import ModalDialogPlaylistSpotify from '@/components/ModalDialogPlaylistSpotify.vue'
import PaneTitle from '@/components/PaneTitle.vue' import PaneTitle from '@/components/PaneTitle.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import queue from '@/api/queue' import queue from '@/api/queue'
import { useServicesStore } from '@/stores/services' import services from '@/api/services'
const PAGE_SIZE = 50 const PAGE_SIZE = 50
@ -53,28 +52,27 @@ export default {
PaneTitle PaneTitle
}, },
beforeRouteEnter(to, from, next) { beforeRouteEnter(to, from, next) {
const spotifyApi = new SpotifyWebApi() services.spotify().then(({ api, configuration }) => {
spotifyApi.setAccessToken(useServicesStore().spotify.webapi_token) Promise.all([
Promise.all([ api.playlists.getPlaylist(to.params.id),
spotifyApi.getPlaylist(to.params.id), api.playlists.getPlaylistItems(
spotifyApi.getPlaylistTracks(to.params.id, { to.params.id,
limit: PAGE_SIZE, configuration.webapi_country,
market: useServicesStore().$state.spotify.webapi_country, null,
offset: 0 PAGE_SIZE,
}) 0
]).then(([playlist, tracks]) => { )
next((vm) => { ]).then(([playlist, tracks]) => {
vm.playlist = playlist next((vm) => {
vm.tracks = [] vm.playlist = playlist
vm.total = 0 vm.tracks = []
vm.offset = 0 vm.total = 0
vm.appendTracks(tracks) vm.offset = 0
vm.appendTracks(tracks)
})
}) })
}) })
}, },
setup() {
return { servicesStore: useServicesStore() }
},
data() { data() {
return { return {
offset: 0, offset: 0,
@ -118,18 +116,20 @@ export default {
this.offset += data.limit this.offset += data.limit
}, },
load({ loaded }) { load({ loaded }) {
const spotifyApi = new SpotifyWebApi() services.spotify().then(({ api, configuration }) => {
spotifyApi.setAccessToken(this.servicesStore.spotify.webapi_token) api.playlists
spotifyApi .getPlaylistItems(
.getPlaylistTracks(this.playlist.id, { this.playlist.id,
limit: PAGE_SIZE, configuration.webapi_country,
market: this.servicesStore.spotify.webapi_country, null,
offset: this.offset PAGE_SIZE,
}) this.offset
.then((data) => { )
this.appendTracks(data) .then((data) => {
loaded(data.items.length, PAGE_SIZE) this.appendTracks(data)
}) loaded(data.items.length, PAGE_SIZE)
})
})
}, },
play() { play() {
this.showDetailsModal = false this.showDetailsModal = false

View File

@ -20,7 +20,6 @@ import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
import ListArtistsSpotify from '@/components/ListArtistsSpotify.vue' import ListArtistsSpotify from '@/components/ListArtistsSpotify.vue'
import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue' import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue'
import ListTracksSpotify from '@/components/ListTracksSpotify.vue' import ListTracksSpotify from '@/components/ListTracksSpotify.vue'
import SpotifyWebApi from 'spotify-web-api-js'
import services from '@/api/services' import services from '@/api/services'
import { useSearchStore } from '@/stores/search' import { useSearchStore } from '@/stores/search'
@ -95,16 +94,17 @@ export default {
} }
}, },
searchItems() { searchItems() {
return services.spotify().then((data) => { return services
this.parameters.market = data.webapi_country .spotify()
const spotifyApi = new SpotifyWebApi() .then(({ api, configuration }) =>
spotifyApi.setAccessToken(data.webapi_token) api.search(
return spotifyApi.search( this.searchStore.query,
this.searchStore.query, this.types,
this.types, configuration.webapi_country,
this.parameters this.parameters.limit,
this.parameters.offset
)
) )
})
}, },
searchLibrary() { searchLibrary() {
this.$router.push({ name: 'search-library' }) this.$router.push({ name: 'search-library' })

View File

@ -7,18 +7,8 @@ export const useServicesStore = defineStore('ServicesStore', {
this.lastfm = await services.lastfm() this.lastfm = await services.lastfm()
}, },
initialiseSpotify() { initialiseSpotify() {
services.spotify().then((data) => { services.spotify().then(({ configuration }) => {
this.spotify = data this.spotify = configuration
if (this.spotifyTimerId > 0) {
clearTimeout(this.spotifyTimerId)
this.spotifyTimerId = 0
}
if (data.webapi_token_expires_in > 0 && data.webapi_token) {
this.spotifyTimerId = setTimeout(
() => this.initialiseSpotify(),
1000 * data.webapi_token_expires_in
)
}
}) })
} }
}, },
@ -41,5 +31,5 @@ export const useServicesStore = defineStore('ServicesStore', {
requiredSpotifyScopes: (state) => requiredSpotifyScopes: (state) =>
state.spotify.webapi_required_scope?.split(' ') ?? [] state.spotify.webapi_required_scope?.split(' ') ?? []
}, },
state: () => ({ lastfm: {}, spotify: {}, spotifyTimerId: 0 }) state: () => ({ lastfm: {}, spotify: {} })
}) })