@@ -13,6 +13,7 @@ import (
1313 "time"
1414
1515 librespot "github.com/devgianlu/go-librespot"
16+ "github.com/devgianlu/go-librespot/audio"
1617 "github.com/devgianlu/go-librespot/mpris"
1718 "github.com/devgianlu/go-librespot/player"
1819 connectpb "github.com/devgianlu/go-librespot/proto/spotify/connectstate"
@@ -290,14 +291,35 @@ func (p *AppPlayer) loadContext(ctx context.Context, spotCtx *connectpb.Context,
290291 p .state .player .NextTracks = ctxTracks .NextTracks (ctx , nil )
291292 p .state .player .Index = ctxTracks .Index ()
292293
293- // load current track into stream
294- if err := p .loadCurrentTrack (ctx , paused , drop ); err != nil {
294+ // load current track into stream — skip forward if it (or a run of tracks) is unplayable.
295+ if err := p .loadCurrentTrackOrSkip (ctx , paused , drop ); err != nil {
295296 return fmt .Errorf ("failed loading current track (load context): %w" , err )
296297 }
297298
298299 return nil
299300}
300301
302+ // loadCurrentTrackOrSkip loads the current track; if it is unplayable (restricted/unsupported,
303+ // or Spotify refused its audio key), it advances forward to the first playable track instead of
304+ // returning the error — so a transfer/cast/context-load that lands on a refused track does not
305+ // freeze the player. advanceNext walks through a run of unplayable tracks (bounded). Non-
306+ // skippable failures and "ran out of tracks" are returned as-is.
307+ func (p * AppPlayer ) loadCurrentTrackOrSkip (ctx context.Context , paused , drop bool ) error {
308+ err := p .loadCurrentTrack (ctx , paused , drop )
309+ if err == nil {
310+ return nil
311+ }
312+ var keyErr * audio.KeyProviderError
313+ if errors .Is (err , librespot .ErrMediaRestricted ) || errors .Is (err , librespot .ErrNoSupportedFormats ) || errors .As (err , & keyErr ) {
314+ p .app .log .WithError (err ).Warnf ("current track unplayable, skipping forward: %s" , p .state .player .Track .Uri )
315+ if _ , aerr := p .advanceNext (ctx , true , drop ); aerr != nil {
316+ return fmt .Errorf ("failed advancing past unplayable track: %w" , aerr )
317+ }
318+ return nil
319+ }
320+ return err
321+ }
322+
301323func (p * AppPlayer ) loadCurrentTrack (ctx context.Context , paused , drop bool ) error {
302324 if p .primaryStream != nil {
303325 p .sess .Events ().OnPrimaryStreamUnload (p .primaryStream , p .player .PositionMs ())
@@ -620,6 +642,10 @@ func (p *AppPlayer) skipNext(ctx context.Context, track *connectpb.ContextTrack)
620642 }
621643}
622644
645+ // maxConsecutiveUnplayableSkips caps how many refused/restricted tracks advanceNext will skip
646+ // past in a row before stopping, so a fully-gated context can't loop forever.
647+ const maxConsecutiveUnplayableSkips = 50
648+
623649func (p * AppPlayer ) advanceNext (ctx context.Context , forceNext , drop bool ) (bool , error ) {
624650 var uri string
625651 var hasNextTrack bool
@@ -696,19 +722,37 @@ func (p *AppPlayer) advanceNext(ctx context.Context, forceNext, drop bool) (bool
696722 p .state .player .IsBuffering = false
697723 }
698724
699- // load current track into stream
700- if err := p .loadCurrentTrack (ctx , ! hasNextTrack , drop ); errors .Is (err , librespot .ErrMediaRestricted ) || errors .Is (err , librespot .ErrNoSupportedFormats ) {
701- p .app .log .WithError (err ).Infof ("skipping unplayable media: %s" , uri )
702- if forceNext {
703- // we failed in finding another track to play, just stop
704- return false , err
725+ // load current track into stream.
726+ //
727+ // BAND-AID: Spotify makes a per-track, context-dependent decision on granting the legacy
728+ // AES audio key. License-gated tracks are refused (AesKeyError, e.g. code 1) in ordinary
729+ // playlist playback — even though they play on official clients, which establish a licensed
730+ // context. We cannot decrypt a refused track, so skip it instead of freezing the player.
731+ // Remove once proper key licensing (PlayPlay) is implemented — tracked separately.
732+ var keyErr * audio.KeyProviderError
733+ if err := p .loadCurrentTrack (ctx , ! hasNextTrack , drop ); errors .Is (err , librespot .ErrMediaRestricted ) || errors .Is (err , librespot .ErrNoSupportedFormats ) || errors .As (err , & keyErr ) {
734+ if keyErr != nil {
735+ p .app .log .WithError (err ).Warnf ("skipping track: Spotify refused the audio key (code %d) for this playback context: %s" , keyErr .Code , uri )
736+ } else {
737+ p .app .log .WithError (err ).Infof ("skipping unplayable media: %s" , uri )
705738 }
706739
740+ // Walk forward through a run of unplayable tracks (a context whose first — or several —
741+ // tracks are refused), bounded so a fully gated or RepeatingContext context advances to
742+ // the first playable track instead of freezing, and can never recurse forever.
743+ p .consecutiveUnplayableSkips ++
744+ if p .consecutiveUnplayableSkips > maxConsecutiveUnplayableSkips {
745+ p .app .log .WithError (err ).Warnf ("stopping after %d consecutive unplayable tracks" , p .consecutiveUnplayableSkips )
746+ p .consecutiveUnplayableSkips = 0
747+ return false , err
748+ }
707749 return p .advanceNext (ctx , true , drop )
708750 } else if err != nil {
751+ p .consecutiveUnplayableSkips = 0
709752 return false , fmt .Errorf ("failed loading current track (advance to %s): %w" , uri , err )
710753 }
711754
755+ p .consecutiveUnplayableSkips = 0
712756 return hasNextTrack , nil
713757}
714758
0 commit comments