mirror of
https://github.com/koel/koel
synced 2024-09-20 06:11:53 +00:00
feat(test): add songStore tests
This commit is contained in:
parent
5a6ddb226e
commit
9789933991
12 changed files with 343 additions and 107 deletions
|
@ -1,7 +1,8 @@
|
|||
import factory from 'factoria'
|
||||
import artistFactory, { states as artistStates } from '@/__tests__/factory/artistFactory'
|
||||
import songFactory, { states as songStates } from '@/__tests__/factory/songFactory'
|
||||
import albumFactory, { states as albumStates } from './albumFactory'
|
||||
import albumFactory, { states as albumStates } from '@/__tests__/factory/albumFactory'
|
||||
import interactionFactory from '@/__tests__/factory/interactionFactory'
|
||||
import smartPlaylistRuleFactory from '@/__tests__/factory/smartPlaylistRuleFactory'
|
||||
import smartPlaylistRuleGroupFactory from '@/__tests__/factory/smartPlaylistRuleGroupFactory'
|
||||
import playlistFactory, { states as playlistStates } from '@/__tests__/factory/playlistFactory'
|
||||
|
@ -18,6 +19,7 @@ export default factory
|
|||
.define('album-track', faker => albumTrackFactory(faker))
|
||||
.define('album-info', faker => albumInfoFactory(faker))
|
||||
.define('song', faker => songFactory(faker), songStates)
|
||||
.define('interaction', faker => interactionFactory(faker))
|
||||
.define('video', faker => youTubeVideoFactory(faker))
|
||||
.define('smart-playlist-rule', faker => smartPlaylistRuleFactory(faker))
|
||||
.define('smart-playlist-rule-group', faker => smartPlaylistRuleGroupFactory(faker))
|
||||
|
|
10
resources/assets/js/__tests__/factory/interactionFactory.ts
Normal file
10
resources/assets/js/__tests__/factory/interactionFactory.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import crypto from 'crypto-random-string'
|
||||
import { Faker } from '@faker-js/faker'
|
||||
|
||||
export default (faker: Faker): Interaction => ({
|
||||
type: 'interactions',
|
||||
id: faker.datatype.number({ min: 1 }),
|
||||
song_id: crypto(32),
|
||||
liked: faker.datatype.boolean(),
|
||||
play_count: faker.datatype.number({ min: 1 })
|
||||
})
|
|
@ -13,3 +13,5 @@ global.ResizeObserver = global.ResizeObserver ||
|
|||
window.HTMLMediaElement.prototype.load = vi.fn()
|
||||
window.HTMLMediaElement.prototype.play = vi.fn()
|
||||
window.HTMLMediaElement.prototype.pause = vi.fn()
|
||||
|
||||
window.BASE_URL = 'https://koel.test/'
|
||||
|
|
|
@ -13,7 +13,7 @@ new class extends UnitTestCase {
|
|||
commonStore.state.song_count = 420
|
||||
commonStore.state.song_length = 123_456
|
||||
songStore.state.songs = factory<Song[]>('song', 20)
|
||||
const fetchMock = this.mock(songStore, 'fetch').mockResolvedValue(2)
|
||||
const fetchMock = this.mock(songStore, 'paginate').mockResolvedValue(2)
|
||||
|
||||
const rendered = this.render(AllSongsScreen, {
|
||||
global: {
|
||||
|
|
|
@ -84,7 +84,7 @@ const fetchSongs = async () => {
|
|||
if (!moreSongsAvailable.value || loading) return
|
||||
|
||||
loading = true
|
||||
page.value = await songStore.fetch(sortField, sortOrder, page.value!)
|
||||
page.value = await songStore.paginate(sortField, sortOrder, page.value!)
|
||||
loading = false
|
||||
}
|
||||
|
||||
|
|
|
@ -173,22 +173,12 @@
|
|||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { isEqual } from 'lodash'
|
||||
import { alerts, defaultCover, pluralize, requireInjection } from '@/utils'
|
||||
import { songStore } from '@/stores'
|
||||
import { songStore, SongUpdateData } from '@/stores'
|
||||
import { EditSongFormInitialTabKey, SongsKey } from '@/symbols'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
import SoundBar from '@/components/ui/SoundBar.vue'
|
||||
|
||||
type EditFormData = {
|
||||
title?: string
|
||||
artist_name?: string
|
||||
album_name?: string
|
||||
album_artist_name?: string
|
||||
track?: number | null
|
||||
disc?: number | null
|
||||
lyrics?: string
|
||||
}
|
||||
|
||||
const [initialTab] = requireInjection(EditSongFormInitialTabKey)
|
||||
const [songs] = requireInjection(SongsKey)
|
||||
|
||||
|
@ -200,7 +190,7 @@ const mutatedSongs = computed(() => songs.value)
|
|||
/**
|
||||
* In order not to mess up the original songs, we manually assign and manipulate their attributes.
|
||||
*/
|
||||
const formData = reactive<EditFormData>({
|
||||
const formData = reactive<SongUpdateData>({
|
||||
title: '',
|
||||
album_name: '',
|
||||
artist_name: '',
|
||||
|
@ -223,7 +213,7 @@ const coverUrl = computed(() => allSongsAreInSameAlbum.value
|
|||
: defaultCover
|
||||
)
|
||||
|
||||
const allSongsShareSameValue = (key: keyof EditFormData) => {
|
||||
const allSongsShareSameValue = (key: keyof SongUpdateData) => {
|
||||
if (editingOnlyOneSong.value) return true
|
||||
return new Set(mutatedSongs.value.map(song => song[key])).size === 1
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { reactive } from 'vue'
|
||||
import { reactive, UnwrapNestedRefs } from 'vue'
|
||||
import { differenceBy, merge, orderBy, take, unionBy } from 'lodash'
|
||||
import { Cache, httpService } from '@/services'
|
||||
import { arrayify } from '@/utils'
|
||||
import { arrayify, logger } from '@/utils'
|
||||
import { songStore } from '@/stores'
|
||||
|
||||
const UNKNOWN_ALBUM_ID = 1
|
||||
|
||||
export const albumStore = {
|
||||
vault: new Map<number, Album>(),
|
||||
vault: new Map<number, UnwrapNestedRefs<Album>>(),
|
||||
|
||||
state: reactive({
|
||||
albums: []
|
||||
|
@ -64,8 +64,13 @@ export const albumStore = {
|
|||
let album = this.byId(id)
|
||||
|
||||
if (!album) {
|
||||
album = await Cache.resolve<Album>(['album', id], async () => await httpService.get<Album>(`albums/${id}`))
|
||||
this.syncWithVault(album)
|
||||
try {
|
||||
album = this.syncWithVault(
|
||||
await Cache.resolve<Album>(['album', id], async () => await httpService.get<Album>(`albums/${id}`))
|
||||
)[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return album
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { reactive } from 'vue'
|
||||
import { reactive, UnwrapNestedRefs } from 'vue'
|
||||
import { differenceBy, orderBy, take, unionBy } from 'lodash'
|
||||
import { Cache, httpService } from '@/services'
|
||||
import { arrayify } from '@/utils'
|
||||
import { arrayify, logger } from '@/utils'
|
||||
|
||||
const UNKNOWN_ARTIST_ID = 1
|
||||
const VARIOUS_ARTISTS_ID = 2
|
||||
|
||||
export const artistStore = {
|
||||
vault: new Map<number, Artist>(),
|
||||
vault: new Map<number, UnwrapNestedRefs<Artist>>(),
|
||||
|
||||
state: reactive({
|
||||
artists: []
|
||||
|
@ -57,8 +57,13 @@ export const artistStore = {
|
|||
let artist = this.byId(id)
|
||||
|
||||
if (!artist) {
|
||||
artist = await Cache.resolve<Artist>(['artist', id], async () => await httpService.get<Artist>(`artists/${id}`))
|
||||
this.syncWithVault(artist)
|
||||
try {
|
||||
artist = this.syncWithVault(
|
||||
await Cache.resolve<Artist>(['artist', id], async () => await httpService.get<Artist>(`artists/${id}`))
|
||||
)[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return artist
|
||||
|
|
|
@ -1,27 +1,280 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { expect, it } from 'vitest'
|
||||
import { songStore } from '@/stores/songStore'
|
||||
import factory from '@/__tests__/factory'
|
||||
import { authService, httpService } from '@/services'
|
||||
import {
|
||||
albumStore,
|
||||
artistStore,
|
||||
commonStore,
|
||||
overviewStore,
|
||||
preferenceStore,
|
||||
songStore,
|
||||
SongUpdateResult
|
||||
} from '@/stores'
|
||||
import { eventBus } from '@/utils'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
new class extends UnitTestCase {
|
||||
protected afterEach () {
|
||||
super.afterEach(() => {
|
||||
isMobile.any = false
|
||||
preferenceStore.transcodeOnMobile = false
|
||||
})
|
||||
}
|
||||
|
||||
protected test () {
|
||||
it('gets a song by ID', () => {
|
||||
expect(songStore.byId('e6d3977f3ffa147801ca5d1fdf6fa55e')!.title).toBe('Like a rolling stone')
|
||||
const song = reactive(factory<Song>('song', { id: 'foo' }))
|
||||
songStore.vault.set('foo', reactive(song))
|
||||
songStore.vault.set('bar', reactive(factory<Song>('song', { id: 'bar' })))
|
||||
|
||||
expect(songStore.byId('foo')).toBe(song)
|
||||
})
|
||||
|
||||
it('gets multiple songs by IDs', () => {
|
||||
const songs = songStore.byIds(['e6d3977f3ffa147801ca5d1fdf6fa55e', 'aa16bbef6a9710eb9a0f41ecc534fad5'])
|
||||
expect(songs[0].title).toBe('Like a rolling stone')
|
||||
expect(songs[1].title).toBe('Knockin\' on heaven\'s door')
|
||||
it('gets songs by IDs', () => {
|
||||
const foo = reactive(factory<Song>('song', { id: 'foo' }))
|
||||
const bar = reactive(factory<Song>('song', { id: 'bar' }))
|
||||
songStore.vault.set('foo', foo)
|
||||
songStore.vault.set('bar', bar)
|
||||
songStore.vault.set('baz', reactive(factory<Song>('song', { id: 'baz' })))
|
||||
|
||||
expect(songStore.byIds(['foo', 'bar'])).toEqual([foo, bar])
|
||||
})
|
||||
|
||||
it('sets interaction status', () => {
|
||||
const song = songStore.byId('cb7edeac1f097143e65b1b2cde102482')!
|
||||
expect(song.liked).toBe(true)
|
||||
expect(song.play_count).toBe(3)
|
||||
it('gets formatted length', () => {
|
||||
expect(songStore.getFormattedLength(factory<Song>('song', { length: 123 }))).toBe('02:03')
|
||||
expect(songStore.getFormattedLength([
|
||||
factory<Song>('song', { length: 122 }),
|
||||
factory<Song>('song', { length: 123 })
|
||||
])).toBe('04:05')
|
||||
})
|
||||
|
||||
it('guesses a song', () => {
|
||||
throw 'Unimplemented'
|
||||
it('gets songs by album', () => {
|
||||
const songs = reactive(factory<Song>('song', 2, { album_id: 3 }))
|
||||
songStore.vault.set(songs[0].id, songs[0])
|
||||
songStore.vault.set(songs[1].id, songs[1])
|
||||
const album = factory<Album>('album', { id: 3 })
|
||||
|
||||
expect(songStore.byAlbum(album)).toEqual(songs)
|
||||
})
|
||||
|
||||
it('resolves a song', async () => {
|
||||
const song = factory<Song>('song')
|
||||
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce(song)
|
||||
|
||||
expect(await songStore.resolve(song.id)).toEqual(song)
|
||||
expect(getMock).toHaveBeenCalledWith(`songs/${song.id}`)
|
||||
|
||||
// next call shouldn't make another request
|
||||
expect(await songStore.resolve(song.id)).toEqual(song)
|
||||
expect(getMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('matches a song', () => {
|
||||
const song = factory<Song>('song', { title: 'An amazing song' })
|
||||
const songs = [song, ...factory<Song[]>('song', 3)]
|
||||
|
||||
expect(songStore.match('An amazing song', songs)).toEqual(song)
|
||||
expect(songStore.match('An Amazing Song', songs)).toEqual(song)
|
||||
})
|
||||
|
||||
it('registers a play', async () => {
|
||||
const song = factory<Song>('song', { play_count: 42 })
|
||||
|
||||
const postMock = this.mock(httpService, 'post').mockResolvedValueOnce(factory<Interaction>('interaction', {
|
||||
song_id: song.id,
|
||||
play_count: 50
|
||||
}))
|
||||
|
||||
await songStore.registerPlay(song)
|
||||
expect(postMock).toHaveBeenCalledWith('interaction/play', { song: song.id })
|
||||
expect(song.play_count).toBe(50)
|
||||
})
|
||||
|
||||
it('scrobbles', async () => {
|
||||
const song = factory<Song>('song')
|
||||
song.play_start_time = 123456789
|
||||
const postMock = this.mock(httpService, 'post')
|
||||
|
||||
await songStore.scrobble(song)
|
||||
|
||||
expect(postMock).toHaveBeenCalledWith(`songs/${song.id}/scrobble`, { timestamp: 123456789 })
|
||||
})
|
||||
|
||||
it('updates songs', async () => {
|
||||
const songs = factory<Song[]>('song', 3)
|
||||
|
||||
const result: SongUpdateResult = {
|
||||
songs: factory<Song[]>('song', 3),
|
||||
albums: factory<Album[]>('album', 2),
|
||||
artists: factory<Artist[]>('artist', 2),
|
||||
removed: {
|
||||
albums: [{
|
||||
id: 10,
|
||||
artist_id: 3,
|
||||
name: 'Removed Album',
|
||||
cover: 'https://example.com/removed-album.jpg',
|
||||
created_at: '2020-01-01'
|
||||
}],
|
||||
artists: [{
|
||||
id: 42,
|
||||
name: 'Removed Artist',
|
||||
image: 'https://example.com/removed-artist.jpg',
|
||||
created_at: '2020-01-01'
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
const syncSongsMock = this.mock(songStore, 'syncWithVault')
|
||||
const syncAlbumsMock = this.mock(albumStore, 'syncWithVault')
|
||||
const syncArtistsMock = this.mock(artistStore, 'syncWithVault')
|
||||
const removeAlbumsMock = this.mock(albumStore, 'removeByIds')
|
||||
const removeArtistsMock = this.mock(artistStore, 'removeByIds')
|
||||
const emitMock = this.mock(eventBus, 'emit')
|
||||
const refreshMock = this.mock(overviewStore, 'refresh')
|
||||
const putMock = this.mock(httpService, 'put').mockResolvedValueOnce(result)
|
||||
|
||||
await songStore.update(songs, {
|
||||
album_name: 'Updated Album',
|
||||
artist_name: 'Updated Artist'
|
||||
})
|
||||
|
||||
expect(putMock).toHaveBeenCalledWith('songs', {
|
||||
data: {
|
||||
album_name: 'Updated Album',
|
||||
artist_name: 'Updated Artist'
|
||||
},
|
||||
songs: songs.map(song => song.id)
|
||||
})
|
||||
|
||||
expect(syncSongsMock).toHaveBeenCalledWith(result.songs)
|
||||
expect(syncAlbumsMock).toHaveBeenCalledWith(result.albums)
|
||||
expect(syncArtistsMock).toHaveBeenCalledWith(result.artists)
|
||||
expect(removeAlbumsMock).toHaveBeenCalledWith([10])
|
||||
expect(removeArtistsMock).toHaveBeenCalledWith([42])
|
||||
expect(emitMock).toHaveBeenCalledWith('SONGS_UPDATED')
|
||||
expect(refreshMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('gets source URL', () => {
|
||||
commonStore.state.cdn_url = 'http://localhost/'
|
||||
const song = factory<Song>('song', { id: 'foo' })
|
||||
this.mock(authService, 'getToken', 'hadouken')
|
||||
|
||||
expect(songStore.getSourceUrl(song)).toBe('http://localhost/play/foo?api_token=hadouken')
|
||||
|
||||
isMobile.any = true
|
||||
preferenceStore.transcodeOnMobile = true
|
||||
expect(songStore.getSourceUrl(song)).toBe('http://localhost/play/foo/1/128?api_token=hadouken')
|
||||
})
|
||||
|
||||
it('gets shareable URL', () => {
|
||||
const song = factory<Song>('song', { id: 'foo' })
|
||||
expect(songStore.getShareableUrl(song)).toBe('https://koel.test/#!/song/foo')
|
||||
})
|
||||
|
||||
it('syncs with the vault', () => {
|
||||
const song = factory<Song>('song', {
|
||||
playback_state: null
|
||||
})
|
||||
|
||||
const trackPlayCountMock = this.mock(songStore, 'trackPlayCount')
|
||||
|
||||
expect(songStore.syncWithVault(song)).toEqual([reactive(song)])
|
||||
expect(songStore.vault.has(song.id)).toBe(true)
|
||||
expect(trackPlayCountMock).toHaveBeenCalledOnce()
|
||||
|
||||
expect(songStore.syncWithVault(song)).toEqual([reactive(song)])
|
||||
expect(songStore.vault.has(song.id)).toBe(true)
|
||||
// second call shouldn't set up play count tracking again
|
||||
expect(trackPlayCountMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('tracks play count', async () => {
|
||||
const refreshMock = this.mock(overviewStore, 'refresh')
|
||||
const artist = reactive(factory<Artist>('artist', { id: 42, play_count: 100 }))
|
||||
const album = reactive(factory<Album>('album', { id: 10, play_count: 120 }))
|
||||
const albumArtist = reactive(factory<Artist>('artist', { id: 43, play_count: 130 }))
|
||||
|
||||
artistStore.vault.set(42, artist)
|
||||
artistStore.vault.set(43, albumArtist)
|
||||
albumStore.vault.set(10, album)
|
||||
|
||||
const song = reactive(factory<Song>('song', {
|
||||
album_id: 10,
|
||||
artist_id: 42,
|
||||
album_artist_id: 43,
|
||||
play_count: 98
|
||||
}))
|
||||
|
||||
songStore.trackPlayCount(song)
|
||||
song.play_count = 100
|
||||
|
||||
await this.tick()
|
||||
|
||||
expect(artist.play_count).toBe(102)
|
||||
expect(album.play_count).toBe(122)
|
||||
expect(albumArtist.play_count).toBe(132)
|
||||
expect(refreshMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fetches for album', async () => {
|
||||
const songs = factory<Song[]>('song', 3)
|
||||
const album = factory<Album>('album', { id: 42 })
|
||||
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce(songs)
|
||||
const syncMock = this.mock(songStore, 'syncWithVault')
|
||||
|
||||
await songStore.fetchForAlbum(album)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('albums/42/songs')
|
||||
expect(syncMock).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
||||
it('fetches for artist', async () => {
|
||||
const songs = factory<Song[]>('song', 3)
|
||||
const artist = factory<Artist>('artist', { id: 42 })
|
||||
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce(songs)
|
||||
const syncMock = this.mock(songStore, 'syncWithVault')
|
||||
|
||||
await songStore.fetchForArtist(artist)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('artists/42/songs')
|
||||
expect(syncMock).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
||||
it('fetches for playlist', async () => {
|
||||
const songs = factory<Song[]>('song', 3)
|
||||
const playlist = factory<Playlist>('playlist', { id: 42 })
|
||||
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce(songs)
|
||||
const syncMock = this.mock(songStore, 'syncWithVault')
|
||||
|
||||
await songStore.fetchForPlaylist(playlist)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('playlists/42/songs')
|
||||
expect(syncMock).toHaveBeenCalledWith(songs)
|
||||
})
|
||||
|
||||
it('paginates', async () => {
|
||||
const songs = factory<Song[]>('song', 3)
|
||||
|
||||
const getMock = this.mock(httpService, 'get').mockResolvedValueOnce({
|
||||
data: songs,
|
||||
links: {
|
||||
next: 'http://localhost/api/v1/songs?page=3'
|
||||
},
|
||||
meta: {
|
||||
current_page: 2
|
||||
}
|
||||
})
|
||||
|
||||
const syncMock = this.mock(songStore, 'syncWithVault', reactive(songs))
|
||||
|
||||
expect(await songStore.paginate('title', 'desc', 2)).toBe(3)
|
||||
|
||||
expect(getMock).toHaveBeenCalledWith('songs?page=2&sort=title&order=desc')
|
||||
expect(syncMock).toHaveBeenCalledWith(songs)
|
||||
expect(songStore.state.songs).toEqual(reactive(songs))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,22 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import slugify from 'slugify'
|
||||
import { orderBy, take, union, unionBy } from 'lodash'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { arrayify, eventBus, secondsToHis, use } from '@/utils'
|
||||
import { merge, orderBy, sumBy, take, unionBy } from 'lodash'
|
||||
import { reactive, UnwrapNestedRefs, watch } from 'vue'
|
||||
import { arrayify, eventBus, logger, secondsToHis, use } from '@/utils'
|
||||
import { authService, Cache, httpService } from '@/services'
|
||||
import { albumStore, artistStore, commonStore, overviewStore, preferenceStore } from '@/stores'
|
||||
|
||||
interface BroadcastSongData {
|
||||
song: {
|
||||
id: string
|
||||
title: string
|
||||
liked: boolean
|
||||
playbackState: PlaybackState
|
||||
album: {
|
||||
id: number
|
||||
name: string
|
||||
cover: string
|
||||
}
|
||||
artist: {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
}
|
||||
export type SongUpdateData = {
|
||||
title?: string
|
||||
artist_name?: string
|
||||
album_name?: string
|
||||
album_artist_name?: string
|
||||
track?: number | null
|
||||
disc?: number | null
|
||||
lyrics?: string
|
||||
}
|
||||
|
||||
interface SongUpdateResult {
|
||||
export interface SongUpdateResult {
|
||||
songs: Song[]
|
||||
artists: Artist[]
|
||||
albums: Album[]
|
||||
|
@ -35,27 +27,13 @@ interface SongUpdateResult {
|
|||
}
|
||||
|
||||
export const songStore = {
|
||||
vault: new Map<string, Song>(),
|
||||
vault: new Map<string, UnwrapNestedRefs<Song>>(),
|
||||
|
||||
state: reactive({
|
||||
songs: [] as Song[]
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get the total duration of some songs.
|
||||
*
|
||||
* @param songs
|
||||
* @param {Boolean} formatted Whether to convert the duration into H:i:s format
|
||||
*/
|
||||
getLength: (songs: Song[], formatted: boolean = false) => {
|
||||
const duration = songs.reduce((length, song) => length + song.length, 0)
|
||||
|
||||
return formatted ? secondsToHis(duration) : duration
|
||||
},
|
||||
|
||||
getFormattedLength (songs: Song[]) {
|
||||
return String(this.getLength(songs, true))
|
||||
},
|
||||
getFormattedLength: (songs: Song | Song[]) => secondsToHis(sumBy(arrayify(songs), 'length')),
|
||||
|
||||
byId (id: string) {
|
||||
return this.vault.get(id)
|
||||
|
@ -72,15 +50,17 @@ export const songStore = {
|
|||
},
|
||||
|
||||
async resolve (id: string) {
|
||||
if (this.byId(id)) {
|
||||
return this.byId(id)
|
||||
let song = this.byId(id)
|
||||
|
||||
if (!song) {
|
||||
try {
|
||||
song = this.syncWithVault(await httpService.get<Song>(`songs/${id}`))[0]
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return this.syncWithVault(await httpService.get<Song>(`songs/${id}`))[0]
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
return song
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -109,9 +89,11 @@ export const songStore = {
|
|||
song.play_count = interaction.play_count
|
||||
},
|
||||
|
||||
scrobble: async (song: Song) => await httpService.post(`${song.id}/scrobble`, { timestamp: song.play_start_time }),
|
||||
scrobble: async (song: Song) => await httpService.post(`songs/${song.id}/scrobble`, {
|
||||
timestamp: song.play_start_time
|
||||
}),
|
||||
|
||||
async update (songsToUpdate: Song[], data: any) {
|
||||
async update (songsToUpdate: Song[], data: SongUpdateData) {
|
||||
const { songs, artists, albums, removed } = await httpService.put<SongUpdateResult>('songs', {
|
||||
data,
|
||||
songs: songsToUpdate.map(song => song.id)
|
||||
|
@ -138,42 +120,24 @@ export const songStore = {
|
|||
|
||||
getShareableUrl: (song: Song) => `${window.BASE_URL}#!/song/${song.id}`,
|
||||
|
||||
generateDataToBroadcast: (song: Song): BroadcastSongData => ({
|
||||
song: {
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
liked: song.liked,
|
||||
playbackState: song.playback_state || 'Stopped',
|
||||
album: {
|
||||
id: song.album_id,
|
||||
name: song.album_name,
|
||||
cover: song.album_cover
|
||||
},
|
||||
artist: {
|
||||
id: song.artist_id,
|
||||
name: song.artist_name
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
syncWithVault (songs: Song | Song[]) {
|
||||
return arrayify(songs).map(song => {
|
||||
let local = this.byId(song.id)
|
||||
|
||||
if (local) {
|
||||
Object.assign(local, song)
|
||||
merge(local, song)
|
||||
} else {
|
||||
song.playback_state = 'Stopped'
|
||||
local = reactive(song)
|
||||
this.trackPlayCount(local!)
|
||||
local.playback_state = 'Stopped'
|
||||
this.trackPlayCount(local)
|
||||
this.vault.set(local.id, local)
|
||||
}
|
||||
|
||||
this.vault.set(song.id, local)
|
||||
return local
|
||||
})
|
||||
},
|
||||
|
||||
trackPlayCount: (song: Song) => {
|
||||
trackPlayCount: (song: UnwrapNestedRefs<Song>) => {
|
||||
watch(() => song.play_count, (newCount, oldCount) => {
|
||||
const album = albumStore.byId(song.album_id)
|
||||
album && (album.play_count += (newCount - oldCount))
|
||||
|
@ -211,7 +175,7 @@ export const songStore = {
|
|||
)
|
||||
},
|
||||
|
||||
async fetch (sortField: SongListSortField, sortOrder: SortOrder, page: number) {
|
||||
async paginate (sortField: SongListSortField, sortOrder: SortOrder, page: number) {
|
||||
const resource = await httpService.get<PaginatorResource>(
|
||||
`songs?page=${page}&sort=${sortField}&order=${sortOrder}`
|
||||
)
|
||||
|
|
1
resources/assets/js/types.d.ts
vendored
1
resources/assets/js/types.d.ts
vendored
|
@ -268,6 +268,7 @@ interface Settings {
|
|||
}
|
||||
|
||||
interface Interaction {
|
||||
type: 'interactions'
|
||||
readonly id: number
|
||||
readonly song_id: string
|
||||
liked: boolean
|
||||
|
|
|
@ -55,7 +55,11 @@ Route::prefix('api')->middleware('api')->group(static function (): void {
|
|||
|
||||
Route::put('settings', [SettingController::class, 'update']);
|
||||
|
||||
/**
|
||||
* @deprecated Use songs/{song}/scrobble instead
|
||||
*/
|
||||
Route::post('{song}/scrobble', [ScrobbleController::class, 'store']);
|
||||
Route::post('songs/{song}/scrobble', [ScrobbleController::class, 'store']);
|
||||
Route::put('songs', [SongController::class, 'update']);
|
||||
|
||||
Route::post('upload', UploadController::class);
|
||||
|
|
Loading…
Reference in a new issue