From eb6485c0ec731db971a1c72387a5181467fc3344 Mon Sep 17 00:00:00 2001 From: Vynull App Date: Thu, 15 Oct 2026 22:56:28 +0000 Subject: [PATCH 1/2] dbserver: give TRACK INFO rows their category IDs, fixing info jumps On a CDJ-2000NXS2, TRACK INFO's ARTIST and ALBUM jumps opened an empty menu. The deck jumps by the ID carried in each Display Song Info (0x2002) row, and ours carried the track's ID in the artist and album rows and 0 in the genre and label rows, so the deck asked for a category entry that doesn't exist. BPM and KEY worked because their rows carried real values. The rows now carry the category's ID, as rekordbox's do: artist, album, genre, label, key, original artist and remixer, with rekordbox's own IDs for an imported library and the IDs the category menus list otherwise. The label row also carries the label's name, as rekordbox's does. The track list's original-artist detail column looked up the ID under "original_artist" while the ORIGINAL ARTIST menu and the import use "origartist", so its ID didn't match the menu's; it uses "origartist" now. --- dbserver/menuitem.go | 2 +- dbserver/songinfo_display_test.go | 53 +++++++++++++++++++++++++++++++ dbserver/track.go | 52 +++++++++++++++++++++++------- 3 files changed, 95 insertions(+), 12 deletions(-) create mode 100644 dbserver/songinfo_display_test.go diff --git a/dbserver/menuitem.go b/dbserver/menuitem.go index 858e8b8..dc701de 100644 --- a/dbserver/menuitem.go +++ b/dbserver/menuitem.go @@ -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()) diff --git a/dbserver/songinfo_display_test.go b/dbserver/songinfo_display_test.go new file mode 100644 index 0000000..71bee86 --- /dev/null +++ b/dbserver/songinfo_display_test.go @@ -0,0 +1,53 @@ +// 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)) + } +} diff --git a/dbserver/track.go b/dbserver/track.go index e7f4882..8f441cb 100644 --- a/dbserver/track.go +++ b/dbserver/track.go @@ -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 { @@ -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 } } @@ -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") @@ -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 @@ -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 @@ -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) From c9241f87ea546a03a5bce482be6d9386d2155474 Mon Sep 17 00:00:00 2001 From: Vynull App Date: Thu, 15 Oct 2026 23:36:18 +0000 Subject: [PATCH 2/2] dbserver: select the jumped-to track after a TRACK INFO jump TRACK INFO jumps opened the right list but left the cursor on its first row. After opening the list the deck asks 0x3100 for the loaded track's position in it, and we only ever looked IDs up in the root menu, so the answer was always 0. 0x3100 is rekordbox's CMD_GET_OFFSET_CONTENTID (GetListBufOffset): the position of an ID in the list loaded at the request's menu location. It now looks there; location 7 is still the root menu, which the SHORT CUT buttons use (a rekordbox capture shows their requests at location 7). An ID not in the list still answers 0. --- dbserver/songinfo_display_test.go | 21 +++++++++++++++++ dbserver/track.go | 39 +++++++++++++++---------------- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/dbserver/songinfo_display_test.go b/dbserver/songinfo_display_test.go index 71bee86..b3ab620 100644 --- a/dbserver/songinfo_display_test.go +++ b/dbserver/songinfo_display_test.go @@ -51,3 +51,24 @@ func TestDisplaySongInfoCategoryIDs(t *testing.T) { 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) + } +} diff --git a/dbserver/track.go b/dbserver/track.go index 8f441cb..184467f 100644 --- a/dbserver/track.go +++ b/dbserver/track.go @@ -881,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,