quintodrome/ui/src/common/playbackActions.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

77 lines
2.1 KiB
JavaScript
Raw Normal View History

feat: add similar songs functionality in agents, and Instant Mix (song-based) to UI (#4919) * refactor: rename ArtistRadio to SimilarSongs for clarity and consistency Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement GetSimilarSongsByTrack and related functionality for song similarity retrieval Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance GetSimilarSongsByTrack to include artist and album details and update tests Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance song matching by implementing title and artist filtering in loadTracksByTitleAndArtist Signed-off-by: Deluan <deluan@navidrome.org> * test: add unit tests for song matching functionality in provider Signed-off-by: Deluan <deluan@navidrome.org> * refactor: extract song matching functionality into its own file Signed-off-by: Deluan <deluan@navidrome.org> * docs: clarify similarSongsFallback function description in provider.go Signed-off-by: Deluan <deluan@navidrome.org> * refactor: initialize result slice for songs with capacity based on response length Signed-off-by: Deluan <deluan@navidrome.org> * refactor: simplify agent method calls for retrieving images and similar songs Signed-off-by: Deluan <deluan@navidrome.org> * refactor: simplify agent method calls for retrieving images and similar songs Signed-off-by: Deluan <deluan@navidrome.org> * refactor: remove outdated comments in GetSimilarSongs methods Signed-off-by: Deluan <deluan@navidrome.org> * fix: use composite key for song matches to handle duplicates by title and artist Signed-off-by: Deluan <deluan@navidrome.org> * refactor: consolidate expectations setup for similar songs tests Signed-off-by: Deluan <deluan@navidrome.org> * feat: add instant mix action to song context menu and update translations Signed-off-by: Deluan <deluan@navidrome.org> * fix(provider): handle unknown entity types in GetSimilarSongs Signed-off-by: Deluan <deluan@navidrome.org> * refactor: move playSimilar action to playbackActions and streamline song processing Signed-off-by: Deluan <deluan@navidrome.org> * format Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance instant mix functionality with loading notification and shuffle option Signed-off-by: Deluan <deluan@navidrome.org> * feat: implement fuzzy matching for similar songs based on configurable threshold Signed-off-by: Deluan <deluan@navidrome.org> * refactor: implement track matching with multiple specificity levels Signed-off-by: Deluan <deluan@navidrome.org> * refactor: enhance track matching by implementing unified scoring with specificity levels Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance deezer top tracks result with album Signed-off-by: Deluan <deluan@navidrome.org> * feat: enhance track matching with fuzzy album similarity for improved scoring Signed-off-by: Deluan <deluan@navidrome.org> * docs: document multi-phase song matching algorithm with detailed scoring and prioritization Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-25 12:16:43 -09:00
import subsonic from '../subsonic/index.js'
import { playTracks } from '../actions/index.js'
const shuffleArray = (array) => {
const shuffled = [...array]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
}
return shuffled
}
const mapReplayGain = (song) => {
const { replayGain: rg } = song
if (!rg) {
return song
}
return {
...song,
...(rg.albumGain !== undefined && { rgAlbumGain: rg.albumGain }),
...(rg.albumPeak !== undefined && { rgAlbumPeak: rg.albumPeak }),
...(rg.trackGain !== undefined && { rgTrackGain: rg.trackGain }),
...(rg.trackPeak !== undefined && { rgTrackPeak: rg.trackPeak }),
}
}
export const processSongsForPlayback = (songs) => {
const songData = {}
const ids = []
songs.forEach((s) => {
const song = mapReplayGain(s)
songData[song.id] = song
ids.push(song.id)
})
return { songData, ids }
}
export const playSimilar = async (dispatch, notify, id, options = {}) => {
const { seedRecord = null, shuffle = false } = options
const res = await subsonic.getSimilarSongs2(id, 100)
const data = res.json['subsonic-response']
if (data.status !== 'ok') {
throw new Error(
`Error fetching similar songs: ${data.error?.message || 'Unknown error'} (Code: ${data.error?.code || 'unknown'})`,
)
}
let songs = data.similarSongs2?.song || []
// Randomize similar songs if requested
if (shuffle) {
songs = shuffleArray(songs)
}
// If no similar songs found and no seed, show warning
if (!songs.length && !seedRecord) {
notify('message.noSimilarSongsFound', 'warning')
return
}
const { songData, ids } = processSongsForPlayback(songs)
// Prepend seed song if provided
if (seedRecord) {
const seedId = seedRecord.mediaFileId || seedRecord.id
// Remove seed from similar songs if it appears there
const filteredIds = ids.filter((songId) => songId !== seedId)
songData[seedId] = mapReplayGain(seedRecord)
dispatch(playTracks(songData, [seedId, ...filteredIds]))
} else {
dispatch(playTracks(songData, ids))
}
}