mirror of
https://github.com/owntone/owntone-server.git
synced 2025-10-30 00:05:05 -04:00
[web] Add Spotify audiobooks #1900
This commit is contained in:
parent
0017c9cace
commit
fd5a34c6b6
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
907
web-src/package-lock.json
generated
907
web-src/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,16 +14,16 @@
|
||||
"dependencies": {
|
||||
"@aacassandra/vue3-progressbar": "^1.0.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@spotify/web-api-ts-sdk": "^1.2.0",
|
||||
"@ts-pro/vue-eternal-loading": "^1.3.1",
|
||||
"axios": "^1.10.0",
|
||||
"axios": "^1.11.0",
|
||||
"bulma": "^1.0.4",
|
||||
"luxon": "^3.6.1",
|
||||
"luxon": "^3.7.1",
|
||||
"mdi-vue": "^3.0.13",
|
||||
"pinia": "^3.0.3",
|
||||
"reconnectingwebsocket": "^1.0.0",
|
||||
"spotify-web-api-js": "^1.5.2",
|
||||
"vue": "^3.5.17",
|
||||
"vue-i18n": "^11.1.6",
|
||||
"vue": "^3.5.18",
|
||||
"vue-i18n": "^11.1.11",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue3-click-away": "^1.2.4",
|
||||
"vue3-lazyload": "^0.3.8",
|
||||
@ -31,12 +31,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@intlify/unplugin-vue-i18n": "^6.0.8",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-plugin-vue": "^10.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"sass": "^1.89.2",
|
||||
"vite": "^6.3.5"
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-vue": "^10.4.0",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.90.0",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { SpotifyApi } from '@spotify/web-api-ts-sdk'
|
||||
import api from '@/api'
|
||||
|
||||
export default {
|
||||
@ -14,6 +15,12 @@ export default {
|
||||
return api.get('./api/spotify-logout')
|
||||
},
|
||||
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,
|
||||
token_type: 'Bearer'
|
||||
})
|
||||
return { api: sdk, configuration }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,10 +41,7 @@ export default {
|
||||
return { settingsStore: useSettingsStore() }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedItem: {},
|
||||
showDetailsModal: false
|
||||
}
|
||||
return { selectedItem: {}, showDetailsModal: false }
|
||||
},
|
||||
methods: {
|
||||
image(item) {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
:index="item.index"
|
||||
:lines="[
|
||||
item.name,
|
||||
item.artists[0]?.name,
|
||||
item.artists.map((item) => item.name).join(', '),
|
||||
$formatters.toDate(item.release_date)
|
||||
]"
|
||||
@open="open(item)"
|
||||
@ -15,6 +15,7 @@
|
||||
/>
|
||||
<loader-list-item :load="load" />
|
||||
<modal-dialog-album-spotify
|
||||
v-if="showDetailsModal"
|
||||
:item="selectedItem"
|
||||
:show="showDetailsModal"
|
||||
@close="showDetailsModal = false"
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:is-item="true"
|
||||
:image="image(item)"
|
||||
:index="item.index"
|
||||
:lines="[item.name]"
|
||||
@open="open(item)"
|
||||
@ -32,6 +33,9 @@ export default {
|
||||
return { selectedItem: {}, showDetailsModal: false }
|
||||
},
|
||||
methods: {
|
||||
image(item) {
|
||||
return { caption: item.name, url: item.images?.[0]?.url ?? '' }
|
||||
},
|
||||
open(item) {
|
||||
this.$router.push({
|
||||
name: 'music-spotify-artist',
|
||||
|
||||
62
web-src/src/components/ListAudiobooksSpotify.vue
Normal file
62
web-src/src/components/ListAudiobooksSpotify.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<list-item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:is-item="item.isItem"
|
||||
:image="image(item)"
|
||||
:index="item.index"
|
||||
:lines="[
|
||||
item.name,
|
||||
item.authors.map((item) => item.name).join(', '),
|
||||
$formatters.toDate(item.release_date)
|
||||
]"
|
||||
@open="open(item)"
|
||||
@open-details="openDetails(item)"
|
||||
/>
|
||||
<loader-list-item :load="load" />
|
||||
<modal-dialog-album-spotify
|
||||
:item="selectedItem"
|
||||
:show="showDetailsModal"
|
||||
@close="showDetailsModal = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItem from '@/components/ListItem.vue'
|
||||
import LoaderListItem from '@/components/LoaderListItem.vue'
|
||||
import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
export default {
|
||||
name: 'ListAlbumsSpotify',
|
||||
components: { ListItem, LoaderListItem, ModalDialogAlbumSpotify },
|
||||
props: {
|
||||
items: { required: true, type: Object },
|
||||
load: { default: null, type: Function }
|
||||
},
|
||||
setup() {
|
||||
return { settingsStore: useSettingsStore() }
|
||||
},
|
||||
data() {
|
||||
return { selectedItem: {}, showDetailsModal: false }
|
||||
},
|
||||
methods: {
|
||||
image(item) {
|
||||
if (this.settingsStore.showCoverArtworkInAlbumLists) {
|
||||
return { caption: item.name, url: item.images?.[0]?.url ?? '' }
|
||||
}
|
||||
return null
|
||||
},
|
||||
open(item) {
|
||||
this.$router.push({
|
||||
name: 'music-spotify-audiobook',
|
||||
params: { id: item.id }
|
||||
})
|
||||
},
|
||||
openDetails(item) {
|
||||
this.selectedItem = item
|
||||
this.showDetailsModal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
64
web-src/src/components/ListChaptersSpotify.vue
Normal file
64
web-src/src/components/ListChaptersSpotify.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<list-item
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:is-playable="item.is_playable"
|
||||
:lines="[
|
||||
item.name,
|
||||
item.album.authors.map((item) => item.name).join(', '),
|
||||
item.album.name
|
||||
]"
|
||||
@open="open(item)"
|
||||
@open-details="openDetails(item)"
|
||||
>
|
||||
<template v-if="!item.is_playable" #reason>
|
||||
(<span v-text="$t('list.spotify.not-playable-track')" />
|
||||
<span
|
||||
v-if="item.restrictions?.reason"
|
||||
v-text="
|
||||
$t('list.spotify.restriction-reason', {
|
||||
reason: item.restrictions.reason
|
||||
})
|
||||
"
|
||||
/>)
|
||||
</template>
|
||||
</list-item>
|
||||
<loader-list-item :load="load" />
|
||||
<modal-dialog-track-spotify
|
||||
v-if="selectedItem"
|
||||
:item="selectedItem"
|
||||
:show="showDetailsModal"
|
||||
@close="showDetailsModal = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListItem from '@/components/ListItem.vue'
|
||||
import LoaderListItem from '@/components/LoaderListItem.vue'
|
||||
import ModalDialogTrackSpotify from '@/components/ModalDialogTrackSpotify.vue'
|
||||
import queue from '@/api/queue'
|
||||
|
||||
export default {
|
||||
name: 'ListChaptersSpotify',
|
||||
components: { ListItem, LoaderListItem, ModalDialogTrackSpotify },
|
||||
props: {
|
||||
contextUri: { default: '', type: String },
|
||||
items: { required: true, type: Object },
|
||||
load: { default: null, type: Function }
|
||||
},
|
||||
data() {
|
||||
return { selectedItem: null, showDetailsModal: false }
|
||||
},
|
||||
methods: {
|
||||
open(item) {
|
||||
if (item.is_playable) {
|
||||
queue.playUri(this.contextUri || item.uri, false, item.position || 0)
|
||||
}
|
||||
},
|
||||
openDetails(item) {
|
||||
this.selectedItem = item
|
||||
this.showDetailsModal = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -3,6 +3,7 @@
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:is-item="item.isItem"
|
||||
:image="image(item)"
|
||||
:index="item.index"
|
||||
:lines="[item.name, item.owner.display_name]"
|
||||
@open="open(item)"
|
||||
@ -20,6 +21,7 @@
|
||||
import ListItem from '@/components/ListItem.vue'
|
||||
import LoaderListItem from '@/components/LoaderListItem.vue'
|
||||
import ModalDialogPlaylistSpotify from '@/components/ModalDialogPlaylistSpotify.vue'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
export default {
|
||||
name: 'ListPlaylistsSpotify',
|
||||
@ -28,12 +30,19 @@ export default {
|
||||
items: { required: true, type: Object },
|
||||
load: { default: null, type: Function }
|
||||
},
|
||||
|
||||
setup() {
|
||||
return { settingsStore: useSettingsStore() }
|
||||
},
|
||||
data() {
|
||||
return { selectedItem: {}, showDetailsModal: false }
|
||||
},
|
||||
|
||||
methods: {
|
||||
image(item) {
|
||||
if (this.settingsStore.showCoverArtworkInAlbumLists) {
|
||||
return { caption: item.name, url: item.images?.[0]?.url ?? '' }
|
||||
}
|
||||
return null
|
||||
},
|
||||
open(item) {
|
||||
this.$router.push({ name: 'playlist-spotify', params: { id: item.id } })
|
||||
},
|
||||
|
||||
@ -3,7 +3,11 @@
|
||||
v-for="item in items"
|
||||
:key="item.id"
|
||||
:is-playable="item.is_playable"
|
||||
:lines="[item.name, item.artists[0].name, item.album.name]"
|
||||
:lines="[
|
||||
item.name,
|
||||
item.artists.map((item) => item.name).join(', '),
|
||||
item.album.name
|
||||
]"
|
||||
@open="open(item)"
|
||||
@open-details="openDetails(item)"
|
||||
>
|
||||
@ -21,6 +25,7 @@
|
||||
</list-item>
|
||||
<loader-list-item :load="load" />
|
||||
<modal-dialog-track-spotify
|
||||
v-if="showDetailsModal"
|
||||
:item="selectedItem"
|
||||
:show="showDetailsModal"
|
||||
@close="showDetailsModal = false"
|
||||
|
||||
@ -17,19 +17,30 @@ export default {
|
||||
computed: {
|
||||
playable() {
|
||||
return {
|
||||
image: this.item?.images?.[0]?.url || '',
|
||||
name: this.item.name || '',
|
||||
image: this.item.images?.[0]?.url || '',
|
||||
name: this.item.name,
|
||||
properties: [
|
||||
{
|
||||
handler: this.openArtist,
|
||||
key: 'property.artist',
|
||||
value: this.item?.artists?.[0]?.name
|
||||
value: this.item.artists?.map((item) => item.name).join(', ')
|
||||
},
|
||||
{
|
||||
key: 'property.author',
|
||||
value: this.item.authors?.map((item) => item.name).join(', ')
|
||||
},
|
||||
{ key: 'property.chapters', value: this.item.total_chapters },
|
||||
{ key: 'property.edition', value: this.item.edition },
|
||||
{
|
||||
key: 'property.narrator',
|
||||
value: this.item.narrators?.map((item) => item.name).join(', ')
|
||||
},
|
||||
{
|
||||
key: 'property.release-date',
|
||||
value: this.$formatters.toDate(this.item.release_date)
|
||||
},
|
||||
{ key: 'property.type', value: this.item.album_type }
|
||||
{ key: 'property.type', value: this.item.album_type },
|
||||
{ key: 'property.path', value: this.item.uri }
|
||||
],
|
||||
uri: this.item.uri
|
||||
}
|
||||
|
||||
@ -39,13 +39,24 @@ export default {
|
||||
computed: {
|
||||
actions() {
|
||||
return [
|
||||
{ handler: this.addToQueue, icon: 'playlist-plus', key: 'actions.add' },
|
||||
{
|
||||
disabled: !this.item.playable,
|
||||
handler: this.addToQueue,
|
||||
icon: 'playlist-plus',
|
||||
key: 'actions.add'
|
||||
},
|
||||
{
|
||||
disabled: !this.item.playable,
|
||||
handler: this.addNextToQueue,
|
||||
icon: 'playlist-play',
|
||||
key: 'actions.add-next'
|
||||
},
|
||||
{ handler: this.play, icon: 'play', key: 'actions.play' }
|
||||
{
|
||||
disabled: !this.item.playable,
|
||||
handler: this.play,
|
||||
icon: 'play',
|
||||
key: 'actions.play'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@ -14,19 +14,15 @@
|
||||
<script>
|
||||
import ListProperties from '@/components/ListProperties.vue'
|
||||
import ModalDialog from '@/components/ModalDialog.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import player from '@/api/player'
|
||||
import queue from '@/api/queue'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
import services from '@/api/services'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogQueueItem',
|
||||
components: { ListProperties, ModalDialog },
|
||||
props: { item: { required: true, type: Object }, show: Boolean },
|
||||
emits: ['close'],
|
||||
setup() {
|
||||
return { servicesStore: useServicesStore() }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
spotifyTrack: {}
|
||||
@ -50,7 +46,7 @@ export default {
|
||||
},
|
||||
{
|
||||
handler: this.openArtist,
|
||||
key: 'property.album-artist',
|
||||
key: 'property.artist',
|
||||
value: this.item.album_artist
|
||||
},
|
||||
{ key: 'property.composer', value: this.item.composer },
|
||||
@ -90,16 +86,17 @@ export default {
|
||||
watch: {
|
||||
item() {
|
||||
if (this.item?.data_kind === 'spotify') {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.servicesStore.spotify.webapi_token)
|
||||
spotifyApi
|
||||
.getTrack(this.item.path.slice(this.item.path.lastIndexOf(':') + 1))
|
||||
.then((response) => {
|
||||
return services.spotify().then(({ api }) => {
|
||||
const trackId = this.item.path.slice(
|
||||
this.item.path.lastIndexOf(':') + 1
|
||||
)
|
||||
return api.tracks.get(trackId).then((response) => {
|
||||
this.spotifyTrack = response
|
||||
})
|
||||
} else {
|
||||
this.spotifyTrack = {}
|
||||
})
|
||||
}
|
||||
this.spotifyTrack = {}
|
||||
return {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@ -28,6 +28,7 @@ export default {
|
||||
playable() {
|
||||
return {
|
||||
name: this.item.title,
|
||||
playable: true,
|
||||
properties: [
|
||||
{
|
||||
handler: this.openAlbum,
|
||||
@ -36,7 +37,7 @@ export default {
|
||||
},
|
||||
{
|
||||
handler: this.openArtist,
|
||||
key: 'property.album-artist',
|
||||
key: 'property.artist',
|
||||
value: this.item.album_artist
|
||||
},
|
||||
{ key: 'property.composer', value: this.item.composer },
|
||||
|
||||
@ -16,11 +16,9 @@ export default {
|
||||
emits: ['close'],
|
||||
computed: {
|
||||
playable() {
|
||||
if (!this.item.artists) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
name: this.item.name,
|
||||
playable: this.item.is_playable,
|
||||
properties: [
|
||||
{
|
||||
handler: this.openAlbum,
|
||||
@ -29,13 +27,22 @@ export default {
|
||||
},
|
||||
{
|
||||
handler: this.openArtist,
|
||||
key: 'property.album-artist',
|
||||
value: this.item.artists[0]?.name
|
||||
key: 'property.artist',
|
||||
value: this.item.artists?.map((item) => item.name).join(', ')
|
||||
},
|
||||
{
|
||||
handler: this.openArtist,
|
||||
key: 'property.author',
|
||||
value: this.item.album.authors?.map((item) => item.name).join(', ')
|
||||
},
|
||||
{
|
||||
key: 'property.release-date',
|
||||
value: this.$formatters.toDate(this.item.album.release_date)
|
||||
},
|
||||
{
|
||||
key: 'property.release-date',
|
||||
value: this.$formatters.toDate(this.item.release_date)
|
||||
},
|
||||
{
|
||||
key: 'property.position',
|
||||
value: [this.item.disc_number, this.item.track_number].join(' / ')
|
||||
|
||||
@ -298,17 +298,20 @@
|
||||
"property": {
|
||||
"added-on": "Hinzugefügt am",
|
||||
"album": "Album",
|
||||
"album-artist": "Album-Künstler",
|
||||
"albums": "Alben",
|
||||
"artist": "Künstler",
|
||||
"artists": "Künstler",
|
||||
"author": "Autor",
|
||||
"chapters": "Kapitel",
|
||||
"comment": "Kommentar",
|
||||
"composer": "Komponist",
|
||||
"duration": "Dauer",
|
||||
"edition": "Ausgabe",
|
||||
"folders": "Ordner",
|
||||
"genre": "Genre",
|
||||
"genres": "Genres",
|
||||
"name": "Name",
|
||||
"narrator": "Sprecher",
|
||||
"owner": "Besitzer",
|
||||
"path": "Pfad",
|
||||
"playlists": "Playlisten",
|
||||
|
||||
@ -298,17 +298,20 @@
|
||||
"property": {
|
||||
"added-on": "Added On",
|
||||
"album": "Album",
|
||||
"album-artist": "Album Artist",
|
||||
"albums": "Albums",
|
||||
"artist": "Artist",
|
||||
"artists": "Artists",
|
||||
"author": "Author",
|
||||
"chapters": "Chapters",
|
||||
"comment": "Comment",
|
||||
"composer": "Composer",
|
||||
"duration": "Duration",
|
||||
"edition": "Edition",
|
||||
"folders": "Folders",
|
||||
"genre": "Genre",
|
||||
"genres": "Genres",
|
||||
"name": "Name",
|
||||
"narrator": "Narrator",
|
||||
"owner": "Owner",
|
||||
"path": "Path",
|
||||
"playlists": "Playlists",
|
||||
|
||||
@ -298,17 +298,20 @@
|
||||
"property": {
|
||||
"added-on": "Ajouté le",
|
||||
"album": "Album",
|
||||
"album-artist": "Artiste de l’album",
|
||||
"albums": "Albums",
|
||||
"artist": "Artiste",
|
||||
"artists": "Artistes",
|
||||
"author": "Auteur",
|
||||
"chapters": "Chapitres",
|
||||
"comment": "Commentaire",
|
||||
"composer": "Compositeur",
|
||||
"duration": "Durée",
|
||||
"edition": "Édition",
|
||||
"folders": "Dossiers",
|
||||
"genre": "Genre",
|
||||
"genres": "Genres",
|
||||
"name": "Nom",
|
||||
"narrator": "Narrateur",
|
||||
"owner": "Propriétaire",
|
||||
"path": "Emplacement",
|
||||
"playlists": "Listes de lecture",
|
||||
|
||||
@ -298,17 +298,20 @@
|
||||
"property": {
|
||||
"added-on": "添加时间",
|
||||
"album": "专辑",
|
||||
"album-artist": "专辑艺人",
|
||||
"albums": "张专辑",
|
||||
"artist": "专辑艺人",
|
||||
"artists": "艺人",
|
||||
"authors": "作者",
|
||||
"chapters": "章节",
|
||||
"comment": "评论",
|
||||
"composer": "作曲家",
|
||||
"duration": "时长",
|
||||
"edition": "版本",
|
||||
"folders": "文件夹",
|
||||
"genre": "流派",
|
||||
"genres": "流派",
|
||||
"name": "名称",
|
||||
"narrator": "叙述者",
|
||||
"owner": "所有者",
|
||||
"path": "路径",
|
||||
"playlists": "播放列表",
|
||||
|
||||
@ -298,17 +298,20 @@
|
||||
"property": {
|
||||
"added-on": "新增時間",
|
||||
"album": "專輯",
|
||||
"album-artist": "專輯藝人",
|
||||
"albums": "張專輯",
|
||||
"artist": "專輯藝人",
|
||||
"artists": "藝人",
|
||||
"author": "作者",
|
||||
"chapters": "章節",
|
||||
"comment": "評論",
|
||||
"composer": "作曲家",
|
||||
"duration": "時長",
|
||||
"edition": "版本",
|
||||
"folders": "檔案夾",
|
||||
"genre": "音樂類型",
|
||||
"genres": "音樂類型",
|
||||
"name": "名稱",
|
||||
"narrator": "敘述者",
|
||||
"owner": "所有者",
|
||||
"path": "路徑",
|
||||
"playlists": "播放列表",
|
||||
|
||||
@ -28,10 +28,8 @@ import ControlImage from '@/components/ControlImage.vue'
|
||||
import ListTracksSpotify from '@/components/ListTracksSpotify.vue'
|
||||
import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue'
|
||||
import PaneHero from '@/components/PaneHero.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import queue from '@/api/queue'
|
||||
import services from '@/api/services'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
|
||||
export default {
|
||||
name: 'PageAlbumSpotify',
|
||||
@ -43,13 +41,9 @@ export default {
|
||||
PaneHero
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
services.spotify().then((data) => {
|
||||
spotifyApi.setAccessToken(data.webapi_token)
|
||||
spotifyApi
|
||||
.getAlbum(to.params.id, {
|
||||
market: useServicesStore().spotify.webapi_country
|
||||
})
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
api.albums
|
||||
.get(to.params.id, configuration.webapi_country)
|
||||
.then((album) => {
|
||||
next((vm) => {
|
||||
vm.album = album
|
||||
@ -57,9 +51,6 @@ export default {
|
||||
})
|
||||
})
|
||||
},
|
||||
setup() {
|
||||
return { servicesStore: useServicesStore() }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
album: { artists: [{}], tracks: {} },
|
||||
@ -75,7 +66,7 @@ export default {
|
||||
],
|
||||
count: this.$t('data.tracks', { count: this.album.tracks.total }),
|
||||
handler: this.openArtist,
|
||||
subtitle: this.album.artists[0].name,
|
||||
subtitle: this.album.artists.map((item) => item.name).join(', '),
|
||||
title: this.album.name
|
||||
}
|
||||
},
|
||||
@ -98,7 +89,6 @@ export default {
|
||||
this.showDetailsModal = true
|
||||
},
|
||||
play() {
|
||||
this.showDetailsModal = false
|
||||
queue.playUri(this.album.uri, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,10 +28,8 @@ import ControlButton from '@/components/ControlButton.vue'
|
||||
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
|
||||
import ModalDialogArtistSpotify from '@/components/ModalDialogArtistSpotify.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import queue from '@/api/queue'
|
||||
import services from '@/api/services'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
@ -45,17 +43,16 @@ export default {
|
||||
PaneTitle
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
services.spotify().then((data) => {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(data.webapi_token)
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
Promise.all([
|
||||
spotifyApi.getArtist(to.params.id),
|
||||
spotifyApi.getArtistAlbums(to.params.id, {
|
||||
include_groups: 'album,single',
|
||||
limit: PAGE_SIZE,
|
||||
market: useServicesStore().spotify.webapi_country,
|
||||
offset: 0
|
||||
})
|
||||
api.artists.get(to.params.id),
|
||||
api.artists.albums(
|
||||
to.params.id,
|
||||
'album,single',
|
||||
configuration.webapi_country,
|
||||
PAGE_SIZE,
|
||||
0
|
||||
)
|
||||
]).then(([artist, albums]) => {
|
||||
next((vm) => {
|
||||
vm.artist = artist
|
||||
@ -66,9 +63,6 @@ export default {
|
||||
})
|
||||
})
|
||||
},
|
||||
setup() {
|
||||
return { servicesStore: useServicesStore() }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
albums: [],
|
||||
@ -93,15 +87,15 @@ export default {
|
||||
this.offset += data.limit
|
||||
},
|
||||
load({ loaded }) {
|
||||
services.spotify().then((data) => {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(data.webapi_token)
|
||||
spotifyApi
|
||||
.getArtistAlbums(this.artist.id, {
|
||||
include_groups: 'album,single',
|
||||
limit: PAGE_SIZE,
|
||||
offset: this.offset
|
||||
})
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
api.artists
|
||||
.albums(
|
||||
this.artist.id,
|
||||
'album,single',
|
||||
configuration.webapi_country,
|
||||
PAGE_SIZE,
|
||||
this.offset
|
||||
)
|
||||
.then((albums) => {
|
||||
this.appendAlbums(albums)
|
||||
loaded(albums.items.length, PAGE_SIZE)
|
||||
@ -112,7 +106,6 @@ export default {
|
||||
this.showDetailsModal = true
|
||||
},
|
||||
play() {
|
||||
this.showDetailsModal = false
|
||||
queue.playUri(this.artist.uri, true)
|
||||
}
|
||||
}
|
||||
|
||||
89
web-src/src/pages/PageAudiobookSpotify.vue
Normal file
89
web-src/src/pages/PageAudiobookSpotify.vue
Normal file
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<content-with-hero>
|
||||
<template #heading>
|
||||
<pane-hero :content="heading" />
|
||||
</template>
|
||||
<template #image>
|
||||
<control-image
|
||||
:url="album.images?.[0]?.url ?? ''"
|
||||
:caption="album.name"
|
||||
class="is-medium"
|
||||
@click="openDetails"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<list-chapters-spotify :items="tracks" :context-uri="album.uri" />
|
||||
</template>
|
||||
</content-with-hero>
|
||||
<modal-dialog-album-spotify
|
||||
:item="album"
|
||||
:show="showDetailsModal"
|
||||
@close="showDetailsModal = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHero from '@/templates/ContentWithHero.vue'
|
||||
import ControlImage from '@/components/ControlImage.vue'
|
||||
import ListChaptersSpotify from '@/components/ListChaptersSpotify.vue'
|
||||
import ModalDialogAlbumSpotify from '@/components/ModalDialogAlbumSpotify.vue'
|
||||
import PaneHero from '@/components/PaneHero.vue'
|
||||
import queue from '@/api/queue'
|
||||
import services from '@/api/services'
|
||||
|
||||
export default {
|
||||
name: 'PageAudiobookSpotify',
|
||||
components: {
|
||||
ContentWithHero,
|
||||
ControlImage,
|
||||
ListChaptersSpotify,
|
||||
ModalDialogAlbumSpotify,
|
||||
PaneHero
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
api.audiobooks
|
||||
.get(to.params.id, configuration.webapi_country)
|
||||
.then((album) => {
|
||||
next((vm) => {
|
||||
vm.album = album
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
album: { authors: [{}], chapters: {} },
|
||||
showDetailsModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
heading() {
|
||||
return {
|
||||
actions: [
|
||||
{ handler: this.play, icon: 'shuffle', key: 'actions.shuffle' },
|
||||
{ handler: this.openDetails, icon: 'dots-horizontal' }
|
||||
],
|
||||
count: this.$t('data.tracks', { count: this.album.chapters.total }),
|
||||
subtitle: this.album.authors.map((item) => item.name).join(', '),
|
||||
title: this.album.name
|
||||
}
|
||||
},
|
||||
tracks() {
|
||||
const { album } = this
|
||||
if (album.chapters.total) {
|
||||
return album.chapters.items.map((track) => ({ ...track, album }))
|
||||
}
|
||||
return []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openDetails() {
|
||||
this.showDetailsModal = true
|
||||
},
|
||||
play() {
|
||||
queue.playUri(this.album.uri, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,84 +1,102 @@
|
||||
<template>
|
||||
<tabs-music />
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title :content="{ title: $t('page.music.recently-added.title') }" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-albums :items="albums" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<router-link
|
||||
class="button is-small is-rounded"
|
||||
:to="{ name: 'music-recently-added' }"
|
||||
>
|
||||
{{ $t('actions.show-more') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title
|
||||
:content="{ title: $t('page.music.recently-played.title') }"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<list-tracks :items="tracks" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<router-link
|
||||
class="button is-small is-rounded"
|
||||
:to="{ name: 'music-recently-played' }"
|
||||
>
|
||||
{{ $t('actions.show-more') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
<content-with-lists :results="results" :types="types" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import ContentWithLists from '@/templates/ContentWithLists.vue'
|
||||
import { GroupedList } from '@/lib/GroupedList'
|
||||
import ListAlbums from '@/components/ListAlbums.vue'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
import library from '@/api/library'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
const PAGE_SIZE = 3
|
||||
const PAGE_SIZE_EXPANDED = 50
|
||||
|
||||
export default {
|
||||
name: 'PageMusic',
|
||||
components: {
|
||||
ContentWithHeading,
|
||||
ListAlbums,
|
||||
ListTracks,
|
||||
PaneTitle,
|
||||
TabsMusic
|
||||
},
|
||||
components: { ContentWithLists },
|
||||
beforeRouteEnter(to, from, next) {
|
||||
Promise.all([
|
||||
library.search({
|
||||
expression:
|
||||
'time_added after 8 weeks ago and media_kind is music having track_count > 3 order by time_added desc',
|
||||
limit: 3,
|
||||
type: 'album'
|
||||
}),
|
||||
library.search({
|
||||
expression:
|
||||
'time_played after 8 weeks ago and media_kind is music order by time_played desc',
|
||||
limit: 3,
|
||||
type: 'track'
|
||||
})
|
||||
]).then(([{ albums }, { tracks }]) => {
|
||||
next((vm) => {
|
||||
vm.albums = new GroupedList(albums)
|
||||
vm.tracks = new GroupedList(tracks)
|
||||
next((vm) => {
|
||||
Object.values(vm.types).forEach(({ handler }) => {
|
||||
handler(PAGE_SIZE)
|
||||
})
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
albums: [],
|
||||
tracks: null
|
||||
results: new Map()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
types() {
|
||||
return {
|
||||
album: {
|
||||
title: 'page.music.recently-added.title',
|
||||
component: ListAlbums,
|
||||
handler: this.recentlyAddedAlbums
|
||||
},
|
||||
track: {
|
||||
title: 'page.music.recently-played.title',
|
||||
component: ListTracks,
|
||||
handler: this.recentlyPlayedTracks
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
recentlyAddedAlbums(limit, expanded, loaded) {
|
||||
library
|
||||
.search({
|
||||
expression:
|
||||
'media_kind is music having track_count > 3 order by time_added desc',
|
||||
limit: limit || useSettingsStore().recentlyAddedLimit,
|
||||
type: 'album'
|
||||
})
|
||||
.then(({ albums }) => {
|
||||
this.storeResults({
|
||||
type: 'album',
|
||||
items: new GroupedList(albums, {
|
||||
criteria: [{ field: 'time_added', order: -1, type: Date }],
|
||||
index: { field: 'time_added', type: Date }
|
||||
}),
|
||||
expanded,
|
||||
loaded
|
||||
})
|
||||
})
|
||||
},
|
||||
recentlyPlayedTracks(limit, expanded, loaded) {
|
||||
library
|
||||
.search({
|
||||
expression: 'media_kind is music order by time_played desc',
|
||||
limit,
|
||||
type: 'track'
|
||||
})
|
||||
.then(({ tracks }) => {
|
||||
this.storeResults({
|
||||
type: 'track',
|
||||
items: new GroupedList(tracks, {
|
||||
criteria: [{ field: 'time_played', order: -1, type: Date }],
|
||||
index: { field: 'time_played', type: Date }
|
||||
}),
|
||||
expanded,
|
||||
loaded
|
||||
})
|
||||
})
|
||||
},
|
||||
storeResults({ type, items, expanded, loaded }) {
|
||||
if (loaded) {
|
||||
const current = this.results.get(type) || []
|
||||
const updated = [...current, ...items.items]
|
||||
this.results.clear()
|
||||
this.results.set(type, new GroupedList(updated))
|
||||
loaded(items.total - updated.length, PAGE_SIZE_EXPANDED)
|
||||
} else {
|
||||
if (expanded) {
|
||||
this.results.clear()
|
||||
}
|
||||
this.results.set(type, items)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,54 +0,0 @@
|
||||
<template>
|
||||
<tabs-music />
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title :content="{ title: $t('page.music.recently-added.title') }" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-albums :items="albums" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import { GroupedList } from '@/lib/GroupedList'
|
||||
import ListAlbums from '@/components/ListAlbums.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
import library from '@/api/library'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
export default {
|
||||
name: 'PageMusicRecentlyAdded',
|
||||
components: { ContentWithHeading, ListAlbums, PaneTitle, TabsMusic },
|
||||
beforeRouteEnter(to, from, next) {
|
||||
const limit = useSettingsStore().recentlyAddedLimit
|
||||
library
|
||||
.search({
|
||||
expression:
|
||||
'media_kind is music having track_count > 3 order by time_added desc',
|
||||
limit,
|
||||
type: 'album'
|
||||
})
|
||||
.then((data) => {
|
||||
next((vm) => {
|
||||
vm.albums = new GroupedList(data.albums, {
|
||||
criteria: [{ field: 'time_added', order: -1, type: Date }],
|
||||
index: { field: 'time_added', type: Date }
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
settingsStore: useSettingsStore()
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
albums: new GroupedList()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<tabs-music />
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title
|
||||
:content="{ title: $t('page.music.recently-played.title') }"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<list-tracks :items="tracks" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import { GroupedList } from '@/lib/GroupedList'
|
||||
import ListTracks from '@/components/ListTracks.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
import library from '@/api/library'
|
||||
|
||||
export default {
|
||||
name: 'PageMusicRecentlyPlayed',
|
||||
components: { ContentWithHeading, ListTracks, PaneTitle, TabsMusic },
|
||||
beforeRouteEnter(to, from, next) {
|
||||
library
|
||||
.search({
|
||||
expression:
|
||||
'time_played after 8 weeks ago and media_kind is music order by time_played desc',
|
||||
limit: 50,
|
||||
type: 'track'
|
||||
})
|
||||
.then((data) => {
|
||||
next((vm) => {
|
||||
vm.tracks = new GroupedList(data.tracks)
|
||||
})
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tracks: new GroupedList()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,84 +1,89 @@
|
||||
<template>
|
||||
<tabs-music />
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title :content="{ title: $t('page.spotify.music.new-releases') }" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-albums-spotify :items="albums" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<router-link
|
||||
:to="{ name: 'music-spotify-new-releases' }"
|
||||
class="button is-small is-rounded"
|
||||
>
|
||||
{{ $t('actions.show-more') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title
|
||||
:content="{ title: $t('page.spotify.music.featured-playlists') }"
|
||||
/>
|
||||
</template>
|
||||
<template #content>
|
||||
<list-playlists-spotify :items="playlists" />
|
||||
</template>
|
||||
<template #footer>
|
||||
<router-link
|
||||
:to="{ name: 'music-spotify-featured-playlists' }"
|
||||
class="button is-small is-rounded"
|
||||
>
|
||||
{{ $t('actions.show-more') }}
|
||||
</router-link>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
<content-with-lists :results="results" :types="types" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import ContentWithLists from '@/templates/ContentWithLists.vue'
|
||||
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
|
||||
import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
import services from '@/api/services'
|
||||
|
||||
const PAGE_SIZE = 3
|
||||
const PAGE_SIZE_EXPANDED = 50
|
||||
|
||||
export default {
|
||||
name: 'PageMusicSpotify',
|
||||
components: {
|
||||
ContentWithHeading,
|
||||
ListAlbumsSpotify,
|
||||
ListPlaylistsSpotify,
|
||||
PaneTitle,
|
||||
TabsMusic
|
||||
},
|
||||
components: { ContentWithLists },
|
||||
beforeRouteEnter(to, from, next) {
|
||||
services.spotify().then((data) => {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(data.webapi_token)
|
||||
Promise.all([
|
||||
spotifyApi.getNewReleases({
|
||||
country: data.webapi_country,
|
||||
limit: 3
|
||||
}),
|
||||
spotifyApi.getFeaturedPlaylists({
|
||||
country: data.webapi_country,
|
||||
limit: 3
|
||||
})
|
||||
]).then((response) => {
|
||||
next((vm) => {
|
||||
vm.albums = response[0].albums.items
|
||||
vm.playlists = response[1].playlists.items
|
||||
})
|
||||
next((vm) => {
|
||||
Object.values(vm.types).forEach(({ handler }) => {
|
||||
handler(PAGE_SIZE)
|
||||
})
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
albums: [],
|
||||
playlists: []
|
||||
results: new Map()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
types() {
|
||||
return {
|
||||
album: {
|
||||
title: 'page.spotify.music.new-releases',
|
||||
component: ListAlbumsSpotify,
|
||||
handler: this.newReleases
|
||||
},
|
||||
playlist: {
|
||||
title: 'page.spotify.music.featured-playlists',
|
||||
component: ListPlaylistsSpotify,
|
||||
handler: this.featuredPlaylists
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
featuredPlaylists(limit, expanded, loaded) {
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
api.browse
|
||||
.getFeaturedPlaylists(configuration.webapi_country, null, null, limit)
|
||||
.then(({ playlists }) => {
|
||||
this.storeResults({
|
||||
type: 'playlist',
|
||||
items: playlists,
|
||||
expanded,
|
||||
loaded
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
newReleases(limit, expanded, loaded) {
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
api.browse
|
||||
.getNewReleases(configuration.webapi_country, limit)
|
||||
.then(({ albums }) => {
|
||||
this.storeResults({
|
||||
type: 'album',
|
||||
items: albums,
|
||||
expanded,
|
||||
loaded
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
storeResults({ type, items, expanded = false, loaded = null }) {
|
||||
if (loaded) {
|
||||
const current = this.results.get(type) || []
|
||||
const updated = [...current, ...items.items]
|
||||
this.results.clear()
|
||||
this.results.set(type, updated)
|
||||
loaded(items.total - updated.length, PAGE_SIZE_EXPANDED)
|
||||
} else {
|
||||
if (expanded) {
|
||||
this.results.clear()
|
||||
}
|
||||
this.results.set(type, items.items)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<tabs-music />
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title :content="heading" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-playlists-spotify :items="playlists" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
import services from '@/api/services'
|
||||
|
||||
export default {
|
||||
name: 'PageMusicSpotifyFeaturedPlaylists',
|
||||
components: {
|
||||
ContentWithHeading,
|
||||
ListPlaylistsSpotify,
|
||||
PaneTitle,
|
||||
TabsMusic
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
services.spotify().then((data) => {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(data.webapi_token)
|
||||
spotifyApi
|
||||
.getFeaturedPlaylists({
|
||||
country: data.webapi_country,
|
||||
limit: 50
|
||||
})
|
||||
.then((response) => {
|
||||
next((vm) => {
|
||||
vm.playlists = response.playlists.items
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
playlists: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
heading() {
|
||||
return { title: this.$t('page.spotify.music.featured-playlists') }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<tabs-music />
|
||||
<content-with-heading>
|
||||
<template #heading>
|
||||
<pane-title :content="heading" />
|
||||
</template>
|
||||
<template #content>
|
||||
<list-albums-spotify :items="albums" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
import services from '@/api/services'
|
||||
|
||||
export default {
|
||||
name: 'PageMusicSpotifyNewReleases',
|
||||
components: {
|
||||
ContentWithHeading,
|
||||
ListAlbumsSpotify,
|
||||
PaneTitle,
|
||||
TabsMusic
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
services.spotify().then((data) => {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(data.webapi_token)
|
||||
spotifyApi
|
||||
.getNewReleases({
|
||||
country: data.webapi_country,
|
||||
limit: 50
|
||||
})
|
||||
.then((response) => {
|
||||
next((vm) => {
|
||||
vm.albums = response.albums.items
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
albums: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
heading() {
|
||||
return { title: this.$t('page.spotify.music.new-releases') }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -37,9 +37,8 @@ import ControlButton from '@/components/ControlButton.vue'
|
||||
import ListTracksSpotify from '@/components/ListTracksSpotify.vue'
|
||||
import ModalDialogPlaylistSpotify from '@/components/ModalDialogPlaylistSpotify.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import queue from '@/api/queue'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
import services from '@/api/services'
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
|
||||
@ -53,28 +52,27 @@ export default {
|
||||
PaneTitle
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(useServicesStore().spotify.webapi_token)
|
||||
Promise.all([
|
||||
spotifyApi.getPlaylist(to.params.id),
|
||||
spotifyApi.getPlaylistTracks(to.params.id, {
|
||||
limit: PAGE_SIZE,
|
||||
market: useServicesStore().$state.spotify.webapi_country,
|
||||
offset: 0
|
||||
})
|
||||
]).then(([playlist, tracks]) => {
|
||||
next((vm) => {
|
||||
vm.playlist = playlist
|
||||
vm.tracks = []
|
||||
vm.total = 0
|
||||
vm.offset = 0
|
||||
vm.appendTracks(tracks)
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
Promise.all([
|
||||
api.playlists.getPlaylist(to.params.id),
|
||||
api.playlists.getPlaylistItems(
|
||||
to.params.id,
|
||||
configuration.webapi_country,
|
||||
null,
|
||||
PAGE_SIZE,
|
||||
0
|
||||
)
|
||||
]).then(([playlist, tracks]) => {
|
||||
next((vm) => {
|
||||
vm.playlist = playlist
|
||||
vm.tracks = []
|
||||
vm.total = 0
|
||||
vm.offset = 0
|
||||
vm.appendTracks(tracks)
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
setup() {
|
||||
return { servicesStore: useServicesStore() }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
offset: 0,
|
||||
@ -88,9 +86,7 @@ export default {
|
||||
heading() {
|
||||
if (this.playlist.name) {
|
||||
return {
|
||||
subtitle: [
|
||||
{ count: this.playlist.tracks.total, key: 'data.playlists' }
|
||||
],
|
||||
subtitle: [{ count: this.playlist.tracks.total, key: 'data.tracks' }],
|
||||
title: this.playlist.name
|
||||
}
|
||||
}
|
||||
@ -118,21 +114,22 @@ export default {
|
||||
this.offset += data.limit
|
||||
},
|
||||
load({ loaded }) {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.servicesStore.spotify.webapi_token)
|
||||
spotifyApi
|
||||
.getPlaylistTracks(this.playlist.id, {
|
||||
limit: PAGE_SIZE,
|
||||
market: this.servicesStore.spotify.webapi_country,
|
||||
offset: this.offset
|
||||
})
|
||||
.then((data) => {
|
||||
this.appendTracks(data)
|
||||
loaded(data.items.length, PAGE_SIZE)
|
||||
})
|
||||
services.spotify().then(({ api, configuration }) => {
|
||||
api.playlists
|
||||
.getPlaylistItems(
|
||||
this.playlist.id,
|
||||
configuration.webapi_country,
|
||||
null,
|
||||
PAGE_SIZE,
|
||||
this.offset
|
||||
)
|
||||
.then((data) => {
|
||||
this.appendTracks(data)
|
||||
loaded(data.items.length, PAGE_SIZE)
|
||||
})
|
||||
})
|
||||
},
|
||||
play() {
|
||||
this.showDetailsModal = false
|
||||
queue.playUri(this.playlist.uri, true)
|
||||
},
|
||||
openDetails() {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
:expanded="expanded"
|
||||
:get-items="getItems"
|
||||
:history="history"
|
||||
:load="(expanded && searchNext) || null"
|
||||
:load="(expanded && load) || null"
|
||||
:results="results"
|
||||
@search="search"
|
||||
@search-library="searchLibrary"
|
||||
@ -18,15 +18,15 @@
|
||||
import ContentWithSearch from '@/templates/ContentWithSearch.vue'
|
||||
import ListAlbumsSpotify from '@/components/ListAlbumsSpotify.vue'
|
||||
import ListArtistsSpotify from '@/components/ListArtistsSpotify.vue'
|
||||
import ListAudiobooksSpotify from '@/components/ListAudiobooksSpotify.vue'
|
||||
import ListPlaylistsSpotify from '@/components/ListPlaylistsSpotify.vue'
|
||||
import ListTracksSpotify from '@/components/ListTracksSpotify.vue'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
import services from '@/api/services'
|
||||
import { useSearchStore } from '@/stores/search'
|
||||
|
||||
const PAGE_SIZE = 3
|
||||
const PAGE_SIZE_EXPANDED = 50
|
||||
const SEARCH_TYPES = ['track', 'artist', 'album', 'playlist']
|
||||
const SEARCH_TYPES = ['track', 'artist', 'album', 'audiobook', 'playlist']
|
||||
|
||||
export default {
|
||||
name: 'PageSearchSpotify',
|
||||
@ -35,6 +35,7 @@ export default {
|
||||
return {
|
||||
components: {
|
||||
album: ListAlbumsSpotify,
|
||||
audiobook: ListAudiobooksSpotify,
|
||||
artist: ListArtistsSpotify,
|
||||
playlist: ListPlaylistsSpotify,
|
||||
track: ListTracksSpotify
|
||||
@ -95,21 +96,22 @@ export default {
|
||||
}
|
||||
},
|
||||
searchItems() {
|
||||
return services.spotify().then((data) => {
|
||||
this.parameters.market = data.webapi_country
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(data.webapi_token)
|
||||
return spotifyApi.search(
|
||||
this.searchStore.query,
|
||||
this.types,
|
||||
this.parameters
|
||||
return services
|
||||
.spotify()
|
||||
.then(({ api, configuration }) =>
|
||||
api.search(
|
||||
this.searchStore.query,
|
||||
this.types,
|
||||
configuration.webapi_country,
|
||||
this.parameters.limit,
|
||||
this.parameters.offset
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
searchLibrary() {
|
||||
this.$router.push({ name: 'search-library' })
|
||||
},
|
||||
searchNext({ loaded }) {
|
||||
load({ loaded }) {
|
||||
const items = this.results.get(this.types[0])
|
||||
this.parameters.limit = PAGE_SIZE_EXPANDED
|
||||
this.searchItems().then((data) => {
|
||||
|
||||
@ -12,6 +12,7 @@ import PageAudiobookAlbums from '@/pages/PageAudiobookAlbums.vue'
|
||||
import PageAudiobookArtist from '@/pages/PageAudiobookArtist.vue'
|
||||
import PageAudiobookArtists from '@/pages/PageAudiobookArtists.vue'
|
||||
import PageAudiobookGenres from '@/pages/PageAudiobookGenres.vue'
|
||||
import PageAudiobookSpotify from '@/pages/PageAudiobookSpotify.vue'
|
||||
import PageComposerAlbums from '@/pages/PageComposerAlbums.vue'
|
||||
import PageComposerTracks from '@/pages/PageComposerTracks.vue'
|
||||
import PageComposers from '@/pages/PageComposers.vue'
|
||||
@ -20,11 +21,7 @@ import PageGenreAlbums from '@/pages/PageGenreAlbums.vue'
|
||||
import PageGenreTracks from '@/pages/PageGenreTracks.vue'
|
||||
import PageGenres from '@/pages/PageGenres.vue'
|
||||
import PageMusic from '@/pages/PageMusic.vue'
|
||||
import PageMusicRecentlyAdded from '@/pages/PageMusicRecentlyAdded.vue'
|
||||
import PageMusicRecentlyPlayed from '@/pages/PageMusicRecentlyPlayed.vue'
|
||||
import PageMusicSpotify from '@/pages/PageMusicSpotify.vue'
|
||||
import PageMusicSpotifyFeaturedPlaylists from '@/pages/PageMusicSpotifyFeaturedPlaylists.vue'
|
||||
import PageMusicSpotifyNewReleases from '@/pages/PageMusicSpotifyNewReleases.vue'
|
||||
import PagePlayer from '@/pages/PagePlayer.vue'
|
||||
import PagePlaylistFolder from '@/pages/PagePlaylistFolder.vue'
|
||||
import PagePlaylistTracks from '@/pages/PagePlaylistTracks.vue'
|
||||
@ -103,6 +100,11 @@ export const router = createRouter({
|
||||
name: 'audiobook-genres',
|
||||
path: '/audiobook/genres'
|
||||
},
|
||||
{
|
||||
component: PageAudiobookSpotify,
|
||||
name: 'music-spotify-audiobook',
|
||||
path: '/music/spotify/audiobooks/:id'
|
||||
},
|
||||
{
|
||||
name: 'audiobooks',
|
||||
path: '/audiobooks',
|
||||
@ -118,31 +120,11 @@ export const router = createRouter({
|
||||
name: 'music-history',
|
||||
path: '/music/history'
|
||||
},
|
||||
{
|
||||
component: PageMusicRecentlyAdded,
|
||||
name: 'music-recently-added',
|
||||
path: '/music/recently-added'
|
||||
},
|
||||
{
|
||||
component: PageMusicRecentlyPlayed,
|
||||
name: 'music-recently-played',
|
||||
path: '/music/recently-played'
|
||||
},
|
||||
{
|
||||
component: PageMusicSpotify,
|
||||
name: 'music-spotify',
|
||||
path: '/music/spotify'
|
||||
},
|
||||
{
|
||||
component: PageMusicSpotifyFeaturedPlaylists,
|
||||
name: 'music-spotify-featured-playlists',
|
||||
path: '/music/spotify/featured-playlists'
|
||||
},
|
||||
{
|
||||
component: PageMusicSpotifyNewReleases,
|
||||
name: 'music-spotify-new-releases',
|
||||
path: '/music/spotify/new-releases'
|
||||
},
|
||||
{
|
||||
component: PageComposerAlbums,
|
||||
name: 'music-composer-albums',
|
||||
|
||||
@ -7,16 +7,19 @@ export const useServicesStore = defineStore('ServicesStore', {
|
||||
this.lastfm = await services.lastfm()
|
||||
},
|
||||
initialiseSpotify() {
|
||||
services.spotify().then((data) => {
|
||||
this.spotify = data
|
||||
services.spotify().then(({ configuration }) => {
|
||||
this.spotify = configuration
|
||||
if (this.spotifyTimerId > 0) {
|
||||
clearTimeout(this.spotifyTimerId)
|
||||
this.spotifyTimerId = 0
|
||||
}
|
||||
if (data.webapi_token_expires_in > 0 && data.webapi_token) {
|
||||
if (
|
||||
configuration.webapi_token_expires_in > 0 &&
|
||||
configuration.webapi_token
|
||||
) {
|
||||
this.spotifyTimerId = setTimeout(
|
||||
() => this.initialiseSpotify(),
|
||||
1000 * data.webapi_token_expires_in
|
||||
1000 * configuration.webapi_token_expires_in
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
52
web-src/src/templates/ContentWithLists.vue
Normal file
52
web-src/src/templates/ContentWithLists.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<tabs-music />
|
||||
<content-with-heading v-for="[type, items] in results" :key="type">
|
||||
<template #heading>
|
||||
<pane-title :content="{ title: $t(types[type].title) }" />
|
||||
</template>
|
||||
<template #content>
|
||||
<component
|
||||
:is="types[type].component"
|
||||
:items="items"
|
||||
:load="(expanded && ((args) => load(type, args))) || null"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="!expanded" #footer>
|
||||
<control-button
|
||||
:button="{
|
||||
handler: () => load(type),
|
||||
title: $t('actions.show-more')
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading.vue'
|
||||
import ControlButton from '@/components/ControlButton.vue'
|
||||
import PaneTitle from '@/components/PaneTitle.vue'
|
||||
import TabsMusic from '@/components/TabsMusic.vue'
|
||||
|
||||
const PAGE_SIZE_EXPANDED = 50
|
||||
|
||||
export default {
|
||||
name: 'ContentWithLists',
|
||||
components: { ContentWithHeading, ControlButton, PaneTitle, TabsMusic },
|
||||
props: {
|
||||
results: { required: true, type: Object },
|
||||
types: { required: true, type: Object }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
load(type, { loaded } = {}) {
|
||||
this.expanded = true
|
||||
this.types[type].handler(PAGE_SIZE_EXPANDED, this.expanded, loaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user