Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dbserver/menuitem.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ func (h *Handler) applyTrackDetail(m *menuItem, t *library.Track) {
m.ParentID = h.catID("remixer", t.Remixer)
m.Label2 = t.Remixer
case "original_artist":
m.ParentID = h.catID("original_artist", t.OriginalArtist)
m.ParentID = h.catID("origartist", t.OriginalArtist) // the ORIGINAL ARTIST menu's IDs
m.Label2 = t.OriginalArtist
case "time":
m.ParentID = uint32(t.Duration.Seconds())
Expand Down
74 changes: 74 additions & 0 deletions dbserver/songinfo_display_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package dbserver

import (
"testing"

"github.com/vynulldev/vynull/library"
"github.com/vynulldev/vynull/proto"
)

// Display Song Info's category rows carry the category's ID, as rekordbox's
// do: the deck's TRACK INFO jump opens ARTIST, ALBUM and the rest by it. They
// carried the track ID, so the jump opened an empty menu.
func TestDisplaySongInfoCategoryIDs(t *testing.T) {
lib := library.New()
lib.AddTrack(&library.Track{ID: 10001, Title: "Alpha One", Artist: "Alpha Artist", Album: "Album One",
Genre: "Fixture House", Label: "Fixture Label One", Key: "Am", Remixer: "Fixture Remixer",
OriginalArtist: "Fixture Original"})
for kind, v := range map[string]struct {
name string
id uint32
}{
"artist": {"Alpha Artist", 1001}, "album": {"Album One", 2001}, "genre": {"Fixture House", 3001},
"label": {"Fixture Label One", 4001}, "key": {"Am", 5001}, "remixer": {"Fixture Remixer", 1003},
"origartist": {"Fixture Original", 1004},
} {
lib.RegisterCategoryID(kind, v.name, v.id)
}
h := &Handler{lib: lib}
h.Handle(&proto.DBMessage{TxID: 1, Type: proto.DBMsgGetMetadata, Args: []proto.DBArg{proto.ArgI32(0x01020401), proto.ArgI32(10001)}})

want := map[uint32]struct {
id uint32
label string
}{
0x07: {1001, "Alpha Artist"}, 0x02: {2001, "Album One"}, 0x06: {3001, "Fixture House"},
0x0e: {4001, "Fixture Label One"}, 0x0f: {5001, "Am"}, 0x29: {1003, "Fixture Remixer"},
0x28: {1004, "Fixture Original"},
}
seen := 0
for _, it := range h.pendingItems {
if w, ok := want[it.ItemType]; ok {
seen++
if it.ID != w.id || it.Label1 != w.label {
t.Errorf("row %#x = %d %q, want %d %q", it.ItemType, it.ID, it.Label1, w.id, w.label)
}
}
}
if seen != len(want) {
t.Errorf("found %d of %d category rows", seen, len(want))
}
}

// 0x3100 returns an ID's position in the list loaded at the request's menu
// location, so a TRACK INFO jump lands on the loaded track; it answered 0
// (the top) for anything but the root menu.
func TestListOffsetForInfoJump(t *testing.T) {
h := &Handler{}
var items []*menuItem
for _, id := range []uint32{11, 22, 1648969, 44} {
items = append(items, &menuItem{ID: id, ItemType: 0x0004})
}
h.setPendingAll(&proto.DBMessage{TxID: 1, Type: 0x1004, Args: []proto.DBArg{proto.ArgI32(0x01010401)}}, items)
offset := func(id uint32) uint32 {
return h.Handle(&proto.DBMessage{TxID: 2, Type: 0x3100, Args: []proto.DBArg{proto.ArgI32(0x01010401), proto.ArgI32(id)}})[0].Args[1].Int()
}
if got := offset(1648969); got != 2 {
t.Errorf("offset of the loaded track = %d, want 2", got)
}
if got := offset(999); got != 0 {
t.Errorf("offset of an ID not in the list = %d, want 0", got)
}
}
91 changes: 60 additions & 31 deletions dbserver/track.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ func (h *Handler) handleGetMetadata(msg *proto.DBMessage) []*proto.DBMessage {
go h.lazyAnalyze(trackID)

// Look up from PDB first, then library.
var title, artist, album, genre, key, comment, dateAdded string
var title, artist, album, genre, key, comment, dateAdded, label string
var tID, artworkID, keyID, duration uint32
// Category rows carry the category's ID, as rekordbox's do: the deck's
// information jump (TRACK INFO -> ARTIST/ALBUM/...) opens that menu entry
// by this ID.
var artistID, albumID, genreID, labelID uint32
if h.pdb != nil {
pt := h.pdb.TrackByID(trackID)
if pt != nil {
Expand All @@ -53,6 +57,7 @@ func (h *Handler) handleGetMetadata(msg *proto.DBMessage) []*proto.DBMessage {
dateAdded = pt.DateAdded
artworkID = pt.ArtworkID
keyID = pt.KeyID
artistID, albumID, genreID, labelID = pt.ArtistID, pt.AlbumID, pt.GenreID, pt.LabelID
duration = uint32(pt.Duration) * 1000 // seconds to ms
}
}
Expand All @@ -66,6 +71,7 @@ func (h *Handler) handleGetMetadata(msg *proto.DBMessage) []*proto.DBMessage {
genre = track.Genre
key = track.Key
comment = track.Comment
label = track.Label
artworkID = track.ArtID
if !track.DateAdded.IsZero() {
dateAdded = track.DateAdded.Format("2006-01-02")
Expand Down Expand Up @@ -119,6 +125,30 @@ func (h *Handler) handleGetMetadata(msg *proto.DBMessage) []*proto.DBMessage {
}
}

// Library tracks: the same IDs the category menus list (rekordbox's own
// IDs for an imported library, else the hashed name IDs).
catID := func(kind, name string) uint32 {
if name == "" {
return 0
}
return h.catID(kind, name)
}
if artistID == 0 {
artistID = catID("artist", artist)
}
if albumID == 0 {
albumID = catID("album", album)
}
if genreID == 0 {
genreID = catID("genre", genre)
}
if labelID == 0 {
labelID = catID("label", label)
}
if keyID == 0 {
keyID = catID("key", key)
}

// Use 0x7FFFFFFF as sentinel for unknown key.
if keyID == 0 && key == "" {
keyID = 0x7FFFFFFF
Expand All @@ -130,8 +160,8 @@ func (h *Handler) handleGetMetadata(msg *proto.DBMessage) []*proto.DBMessage {
// ParentID values: some items have parent=1, others parent=0.
metaItems := []*menuItem{
{ID: tID, Label1: title, ArtID: artworkID, ItemType: 0x0004, ParentID: 1, FileType: fileType}, // title
{ID: tID, Label1: artist, ItemType: 0x0007, ParentID: 1}, // artist
{ID: tID, Label1: album, ItemType: 0x0002, ParentID: 1}, // album
{ID: artistID, Label1: artist, ItemType: 0x0007, ParentID: 1}, // artist
{ID: albumID, Label1: album, ItemType: 0x0002, ParentID: 1}, // album
{ID: duration / 1000, Label1: "", ItemType: 0x000b}, // duration (parent=0)
{ID: tempo, Label1: "", ItemType: 0x000d, ParentID: 1}, // BPM (* 100)
{ID: keyID, Label1: key, ItemType: 0x000f, ParentID: 1}, // key
Expand All @@ -145,14 +175,14 @@ func (h *Handler) handleGetMetadata(msg *proto.DBMessage) []*proto.DBMessage {
// its own palette.
{ID: colorID, Label1: trackColorName(uint8(colorID)),
ItemType: 0x0013},
{ID: 0, Label1: genre, ItemType: 0x0006, ParentID: 1}, // genre
{ID: tID, Label1: dateAdded, ItemType: 0x002e, ParentID: 1}, // date added
{ID: tID, Label1: comment, ItemType: 0x0023}, // comment (parent=0)
{ID: 0, Label1: "", ItemType: 0x000e, ParentID: 1}, // label
{ID: bitrate, Label1: "", ItemType: 0x0010}, // bitrate (parent=0)
{ID: year, Label1: "", ItemType: 0x0011, ParentID: 1}, // year
{ID: 0, Label1: originalArtist, ItemType: 0x0028}, // original artist (parent=0)
{ID: 0, Label1: remixer, ItemType: 0x0029}, // remixer (parent=0)
{ID: genreID, Label1: genre, ItemType: 0x0006, ParentID: 1}, // genre
{ID: tID, Label1: dateAdded, ItemType: 0x002e, ParentID: 1}, // date added
{ID: tID, Label1: comment, ItemType: 0x0023}, // comment (parent=0)
{ID: labelID, Label1: label, ItemType: 0x000e, ParentID: 1}, // label
{ID: bitrate, Label1: "", ItemType: 0x0010}, // bitrate (parent=0)
{ID: year, Label1: "", ItemType: 0x0011, ParentID: 1}, // year
{ID: catID("origartist", originalArtist), Label1: originalArtist, ItemType: 0x0028}, // original artist (parent=0)
{ID: catID("remixer", remixer), Label1: remixer, ItemType: 0x0029}, // remixer (parent=0)
}
h.setPendingAll(msg, metaItems)

Expand Down Expand Up @@ -851,33 +881,32 @@ func (h *Handler) handleGetNXS2Cues(msg *proto.DBMessage) []*proto.DBMessage {
}

func (h *Handler) handleMountInfo(msg *proto.DBMessage) []*proto.DBMessage {
// 0x3100 is the SHORT CUT preview query. Before a SHORT CUT button
// (TRACK / PLAYLIST / SEARCH) opens its list, the deck asks which
// root-menu slot that shortcut lives in: arg[1] is the target
// category's item ID and the response value is that item's *index*
// within the root menu. The deck then renders a 1-item preview at
// that offset (0x3000, count=1) for the top-bar label and opens the
// list there. Returning a fixed 0 makes every shortcut resolve to
// root-menu slot 0 (ARTIST), so TRACK/PLAYLIST both open ARTIST.
//
// Look the requested ID up in the actual root menu so the offset
// stays correct regardless of the configured menu order. Unknown or
// absent IDs fall back to 0 (the old ack behaviour) — e.g. plain
// mount notifications that carry no category ID.
// 0x3100 is rekordbox's CMD_GET_OFFSET_CONTENTID (GetListBufOffset):
// [context, id] asks where id sits in the list loaded at the context's
// menu location, and the reply is that index. Decks use it to put the
// cursor on an item: the SHORT CUT buttons ask for a category in the root
// menu (location 7), and TRACK INFO's jumps ask for the loaded track in
// the list they just opened, so it comes up selected. An ID not in the
// list answers 0 (the top), as plain mount notifications expect.
offset := 0
wantID := uint32(0)
menu := dmstMenu(msg)
if len(msg.Args) >= 2 {
wantID = msg.Args[1].Int()
if wantID != 0 {
for i, item := range h.rootMenu() {
if item.ID == wantID {
offset = i
break
}
}
if wantID != 0 {
list := h.getPending(menu)
if menu == 7 || list == nil {
list = h.rootMenu()
}
for i, item := range list {
if item.ID == wantID {
offset = i
break
}
}
}
dlog.Debugf("dbserver: mount info (0x3100) shortcut id=0x%x -> root offset %d", wantID, offset)
dlog.Debugf("dbserver: list offset (0x3100) menu=%d id=0x%x -> offset %d", menu, wantID, offset)
return []*proto.DBMessage{{
TxID: msg.TxID,
Type: proto.DBMsgSuccess,
Expand Down
Loading