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
20 changes: 20 additions & 0 deletions api/tagstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,26 @@ func (ts *TagStore) GetTagsForTrack(trackID uint32) []TagInfo {
return out
}

// TrackTagIDs returns the IDs of the tags assigned to a track.
func (ts *TagStore) TrackTagIDs(trackID uint32) []uint32 {
ts.mu.RLock()
defer ts.mu.RUnlock()
return append([]uint32(nil), ts.trackTags[trackID]...)
}

// TagTrackCounts returns how many tracks carry each tag.
func (ts *TagStore) TagTrackCounts() map[uint32]int {
ts.mu.RLock()
defer ts.mu.RUnlock()
counts := make(map[uint32]int)
for _, ids := range ts.trackTags {
for _, id := range ids {
counts[id]++
}
}
return counts
}

func (ts *TagStore) SetTagsForTrack(trackID uint32, tagIDs []uint32) {
ts.mu.Lock()
defer ts.mu.Unlock()
Expand Down
1 change: 1 addition & 0 deletions dbserver/categories.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func (h *Handler) handleGetTracks(msg *proto.DBMessage) []*proto.DBMessage {
orderByTitle(h.pendingItems) // sorts keep title order on ties
}
h.sortTracks(h.pendingItems, order)
h.pendingItems = h.applyTrackFilter(msg, h.pendingItems)
dlog.Debugf("dbserver: get tracks returning %d items (sort=%d)", len(h.pendingItems), order)
return []*proto.DBMessage{h.successWithCount(msg)}
}
Expand Down
37 changes: 15 additions & 22 deletions dbserver/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import (

// Handler processes individual dbserver messages and returns responses.
type Handler struct {
mytags MyTagSource // TRACK FILTER MyTag lists (nil: none)
// filters holds the players' TRACK FILTER settings, shared by the
// server's sessions (trackfilter.go).
filters *trackFilters
// ctxTrackType is the context track type of the browse request being
// served (the low byte of its first argument). rekordbox echoes it in
// argument 7 of hierarchy track rows (see markContextType).
Expand Down Expand Up @@ -219,8 +223,18 @@ func (h *Handler) Handle(msg *proto.DBMessage) []*proto.DBMessage {
switch msg.Type {
case proto.DBMsgSetup:
return h.handleSetup(msg)
case 0x3007: // set filter on/off (CMD_SET_FILTER_ONOFF)
case 0x3007: // TRACK FILTER on/off (CMD_SET_FILTER_ONOFF)
return h.handleSetFilterOnOff(msg)
case 0x3107: // TRACK FILTER conditions (CMD_GET_FILTER_CONDITION_PROPERTY)
return h.handleGetFilterConditions(msg)
case 0x3207: // set a TRACK FILTER condition
return h.handleSetFilterCondition(msg)
case 0x3307: // TRACK FILTER MyTag condition on/off
return h.handleSetFilterMyTag(msg)
case 0x3407: // add/remove a TRACK FILTER MyTag
return h.handleAddFilterMyTag(msg)
case 0x1015: // TRACK FILTER MyTag columns, or a column's tags
return h.handleGetFilterMyTags(msg)
case proto.DBMsgRootMenu: // 0x1000
return h.handleRootMenu(msg)
case 0x3e03: // NXS2 extension
Expand Down Expand Up @@ -510,27 +524,6 @@ func (h *Handler) handleSetup(msg *proto.DBMessage) []*proto.DBMessage {
}}
}

// handleSetFilterOnOff handles 0x3007, which decks send before each root
// menu request. It is the first of rekordbox's filter commands (0x3007 to
// 0x3407 in 0x100 steps, dispatched by PSvDBMain::OnFilterCmd), and XDJ-RR
// firmware sends it from dbcl_SetFilterOnOff (link-spec's static analysis
// in docs/LINK_EXPORT_REQUEST_VOCABULARY.md and
// data/static-analysis/xdj-rr-client-navigation.json). We keep no filter
// state, so it is only acknowledged. rekordbox's reply has not been
// captured; this plain success is what decks have always accepted from us.
func (h *Handler) handleSetFilterOnOff(msg *proto.DBMessage) []*proto.DBMessage {
argVals := make([]string, len(msg.Args))
for i, a := range msg.Args {
argVals[i] = fmt.Sprintf("0x%08x", a.Int())
}
dlog.Debugf("dbserver: set filter on/off (0x3007) args=%v", argVals)
return []*proto.DBMessage{{
TxID: msg.TxID,
Type: proto.DBMsgSuccess,
Args: []proto.DBArg{proto.ArgI32(0x3007), proto.ArgI32(0)},
}}
}

func (h *Handler) handleNXS2Extension(msg *proto.DBMessage) []*proto.DBMessage {
dlog.Debugf("dbserver: NXS2 extension (0x3e03)")
// Respond: type 0x4b02 with [echo_type, 0, 2, ""] — matches the CDJ
Expand Down
5 changes: 4 additions & 1 deletion dbserver/menuitem.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type menuItem struct {
FileType string // "mp3", "m4a", "flac", "wav", "aiff" — for title items
TrackInfo bool // true for track-info title items (no special flags)
ColorID uint32 // track color (1=pink, 2=red, ..., 8=purple) for arg[11]
rating uint32 // track rating (0-5 stars), for the TRACK FILTER

// Sort metadata (for tracks).
sortArtist string
Expand Down Expand Up @@ -135,6 +136,7 @@ func (h *Handler) pdbTrackToStdItem(t *pdb.Track) *menuItem {
ArtID: t.ArtworkID,
FileType: h.resolveFileType(t.ID),
ColorID: uint32(t.ColorID),
rating: uint32(t.Rating),
// USB exports carry hot-cue autoload; the compatibility bit and
// new-key index fall out of the codec string and Camelot key. The PDB
// row has no FileTypeID/sample-rate, so those use the string fallback.
Expand All @@ -158,9 +160,10 @@ func (h *Handler) trackToStdItem(t *library.Track) *menuItem {
ArtID: t.ArtID,
FileType: t.FileType,
ColorID: uint32(t.ColorID),
rating: uint32(t.Rating),
sortArtist: t.Artist,
sortAlbum: t.Album,
sortBPM: uint32(t.BPM * 100),
sortBPM: uint32(math.Round(t.BPM * 100)), // exact for range checks (TRACK FILTER)
sortKey: t.Key,
hasSongTag: h.lib != nil && h.lib.HasSongTag(t.ID),
autoLoadHotCue: t.AutoLoadHotCue,
Expand Down
27 changes: 26 additions & 1 deletion dbserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,22 @@ type MenuSource interface {
TrackDetail() string
}

// MyTagSource gives the dbserver the MyTag catalog and assignments for the
// TRACK FILTER's MyTag condition: the columns (categories) in order, a
// column's tags in order, and a track's tag IDs. Tags that no track carries
// are left out of a column's list, as rekordbox does.
type MyTagSource interface {
MyTagColumns() []MyTagEntry
MyTagsIn(columnID uint32) []MyTagEntry
TrackMyTags(trackID uint32) []uint32
}

// MyTagEntry is one MyTag column or tag.
type MyTagEntry struct {
ID uint32
Name string
}

// PlaylistEntry is the minimal shape the dbserver needs to render the
// CDJ's PLAYLIST menu. Implemented by api.PlaylistStore (and any future
// playlist source) so the dbserver doesn't depend on the api package.
Expand Down Expand Up @@ -118,6 +134,10 @@ type SmartPlaylistSource interface {

// Server implements the Pro DJ Link database server protocol.
type Server struct {
// MyTags serves the TRACK FILTER's MyTag lists and matching (nil: none).
MyTags MyTagSource
// filters holds every player's TRACK FILTER settings across sessions.
filters trackFilters
Library *library.Library
PDB *pdb.Database
DeviceNumber uint8
Expand Down Expand Up @@ -433,6 +453,8 @@ func (s *Server) handleSession(ctx context.Context, conn net.Conn) {
menu: s.Menu,
cues: s.Cues,
settings: s.Settings,
filters: &s.filters,
mytags: s.MyTags,
}

for {
Expand Down Expand Up @@ -669,8 +691,11 @@ func readMessage(conn net.Conn) (*proto.DBMessage, error) {
// 0x2805 = tagged analysis-section read — sends a ~150-byte binary spec
// blob (e.g. "PVB2"/"EXT"). This was the cause of the deck
// dropping the dbserver link mid-session.
// 0x3207 = TRACK FILTER condition — the condition blob (on, kind,
// count, values). Skipping it dropped the session the moment a
// filter was ticked, and the deck unticked it.
actualArgCount := argCount
sendsTrailingBinary := msgType == 0x2705 || msgType == 0x2805
sendsTrailingBinary := msgType == 0x2705 || msgType == 0x2805 || msgType == 0x3207
if !sendsTrailingBinary {
// A tag list shorter than the argument count (malformed) has no tag
// for the missing positions; treat those as sent, so the read below
Expand Down
Loading
Loading