refactor: simplify and improve playlist provider and matcher code

Eliminate redundant work and minor issues found during code review:
- Replace manual PlaylistTrack construction in syncPlaylist with the
  existing Playlist.AddMediaFiles helper, removing duplicated logic
- Pre-sanitize track fields once per artist batch in the matcher's
  fuzzy matching loop, avoiding redundant sanitization in both
  findBestMatch and computeSpecificityLevel on every iteration
- Cache resolved usernames in discoverAndSync to avoid N+1 DB lookups
  when multiple playlists share the same owner
- Use the local loadedPlugin variable instead of reading m.plugins[p.ID]
  after releasing the lock in loadPluginWithConfig
- Fix misleading uint32 comparison (<=0 to ==0) in durationProximity
- Update stale comment on checkTracksEditable to mention plugin playlists
This commit is contained in:
Deluan 2026-03-05 17:55:59 -05:00
parent 9053a4ffe9
commit 73203eeef0
3 changed files with 15 additions and 20 deletions

View File

@ -212,7 +212,7 @@ func (s *playlists) checkWritable(ctx context.Context, id string) (*model.Playli
return pls, nil return pls, nil
} }
// checkTracksEditable verifies the user can modify tracks (ownership + not smart playlist). // checkTracksEditable verifies the user can modify tracks (ownership + not smart/plugin playlist).
func (s *playlists) checkTracksEditable(ctx context.Context, playlistID string) (*model.Playlist, error) { func (s *playlists) checkTracksEditable(ctx context.Context, playlistID string) (*model.Playlist, error) {
pls, err := s.checkWritable(ctx, playlistID) pls, err := s.checkWritable(ctx, playlistID)
if err != nil { if err != nil {

View File

@ -390,7 +390,7 @@ func (m *Manager) loadPluginWithConfig(p *model.Plugin) error {
m.mu.Unlock() m.mu.Unlock()
// Call plugin init function // Call plugin init function
callPluginInit(ctx, m.plugins[p.ID]) callPluginInit(ctx, loadedPlugin)
// Start PlaylistProvider syncer if capability is detected // Start PlaylistProvider syncer if capability is detected
if hasCapability(loadedPlugin.capabilities, CapabilityPlaylistProvider) { if hasCapability(loadedPlugin.capabilities, CapabilityPlaylistProvider) {

View File

@ -2,7 +2,6 @@ package plugins
import ( import (
"context" "context"
"fmt"
"slices" "slices"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -125,15 +124,20 @@ func (o *playlistSyncer) discoverAndSync() {
o.retryInterval.Store(int64(time.Duration(resp.RetryInterval) * time.Second)) o.retryInterval.Store(int64(time.Duration(resp.RetryInterval) * time.Second))
} }
resolvedUsers := map[string]string{} // username -> userID cache
for _, info := range resp.Playlists { for _, info := range resp.Playlists {
// Resolve username to user ID // Resolve username to user ID (cached)
ownerID, ok := resolvedUsers[info.OwnerUsername]
if !ok {
user, err := o.ds.User(adminContext(ctx)).FindByUsername(info.OwnerUsername) user, err := o.ds.User(adminContext(ctx)).FindByUsername(info.OwnerUsername)
if err != nil { if err != nil {
log.Error(ctx, "Failed to resolve playlist owner", "plugin", o.pluginName, log.Error(ctx, "Failed to resolve playlist owner", "plugin", o.pluginName,
"playlistID", info.ID, "username", info.OwnerUsername, err) "playlistID", info.ID, "username", info.OwnerUsername, err)
continue continue
} }
ownerID := user.ID ownerID = user.ID
resolvedUsers[info.OwnerUsername] = ownerID
}
// Validate that the plugin is permitted to create playlists for this user // Validate that the plugin is permitted to create playlists for this user
if !o.plugin.allUsers && !slices.Contains(o.plugin.allowedUserIDs, ownerID) { if !o.plugin.allUsers && !slices.Contains(o.plugin.allowedUserIDs, ownerID) {
@ -198,16 +202,7 @@ func (o *playlistSyncer) syncPlaylist(info capabilities.PlaylistInfo, dbID strin
} }
// Set tracks from matched media files // Set tracks from matched media files
tracks := make(model.PlaylistTracks, len(matched)) pls.AddMediaFiles(matched)
for i, mf := range matched {
tracks[i] = model.PlaylistTrack{
ID: fmt.Sprintf("%d", i+1),
MediaFileID: mf.ID,
PlaylistID: dbID,
MediaFile: mf,
}
}
pls.SetTracks(tracks)
// Upsert via repository // Upsert via repository
plsRepo := o.ds.Playlist(ctx) plsRepo := o.ds.Playlist(ctx)