From ceb151a02c3ecedc52b908af70e877b7060e46fd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:56:54 +0000 Subject: [PATCH 01/25] Initial plan From 0eccc8e490eb85b57aee85c795587d814ac40d4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:59:58 +0000 Subject: [PATCH 02/25] Support play-behind-live metadata selection using pdt timestamp files Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/d3f58cf7-6417-4b92-9e3e-c6ee08bbd157 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 96 +++++++++++++++++++++++++++++++-- 1 file changed, 92 insertions(+), 4 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 71bc0da..dd229fa 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -10,6 +10,7 @@ use Slim::Networking::SimpleAsyncHTTP; use JSON::XS; use Date::Parse; use Time::HiRes; +use File::Spec; my $log = logger('plugin.siriusxm'); my $prefs = preferences('plugin.siriusxm'); @@ -234,10 +235,49 @@ sub _processResponse { return; } - my $latest_track = $results->[0]; - my $track_info = $latest_track->{track}; - my $spotify_info = $latest_track->{spotify}; - my $timestamp = $latest_track->{timestamp}; + my $selected_track = $results->[0]; + my $selected_reason = 'default latest xmplaylist record'; + + if ($channel_info && $channel_info->{id}) { + my $tmp_dir = $ENV{TMPDIR} || $ENV{TEMP} || '/tmp'; + my $pdt_file = File::Spec->catfile($tmp_dir, 'siriusxm', 'pdt_' . $channel_info->{id} . '.txt'); + my $play_ts = _readPlayTimestampFromFile($pdt_file); + + if (defined $play_ts) { + my $matched_track; + my $matched_ts; + + for my $result (@$results) { + next unless $result && ref($result) eq 'HASH'; + + my $result_ts = _parseTimestampToEpoch($result->{timestamp}); + next unless defined $result_ts; + next if $result_ts > $play_ts; + + if (!defined $matched_ts || $result_ts > $matched_ts) { + $matched_track = $result; + $matched_ts = $result_ts; + } + } + + if ($matched_track) { + $selected_track = $matched_track; + $selected_reason = 'matched record at/before play timestamp'; + $log->debug("Selected xmplaylist record timestamp " . ($selected_track->{timestamp} || 'unknown') . " for play timestamp $play_ts"); + } else { + $log->debug("No xmplaylist record timestamp <= play timestamp $play_ts, falling back to latest record"); + } + } else { + $log->debug("No usable play timestamp from $pdt_file, falling back to latest xmplaylist record"); + } + } else { + $log->debug("Missing channel id in channel info, falling back to latest xmplaylist record"); + } + + my $track_info = $selected_track->{track}; + my $spotify_info = $selected_track->{spotify}; + my $timestamp = $selected_track->{timestamp}; + $log->debug("Metadata source selection: $selected_reason"); return unless $track_info; @@ -325,4 +365,52 @@ sub _processResponse { } } +sub _readPlayTimestampFromFile { + my ($pdt_file) = @_; + + unless (-e $pdt_file) { + $log->debug("PDT file not found: $pdt_file"); + return; + } + + open(my $fh, '<', $pdt_file) or do { + $log->warn("Unable to read PDT file $pdt_file: $!"); + return; + }; + + my $raw_ts = <$fh>; + close($fh); + + unless (defined $raw_ts) { + $log->debug("PDT file is empty: $pdt_file"); + return; + } + + $raw_ts =~ s/^\s+|\s+$//g; + unless (length $raw_ts) { + $log->debug("PDT file contains no timestamp text: $pdt_file"); + return; + } + + my $play_ts = _parseTimestampToEpoch($raw_ts); + unless (defined $play_ts) { + $log->debug("Failed to parse play timestamp '$raw_ts' from $pdt_file"); + return; + } + + $log->debug("Read play timestamp '$raw_ts' ($play_ts) from $pdt_file"); + return $play_ts; +} + +sub _parseTimestampToEpoch { + my ($timestamp) = @_; + return unless defined $timestamp; + + if ($timestamp =~ /^\s*(\d+(?:\.\d+)?)\s*$/) { + return int($1); + } + + return str2time($timestamp); +} + 1; From 4971b85a068acfa22c1bd6ac568e5b0fe2bcbe0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:02:30 +0000 Subject: [PATCH 03/25] Harden PDT timestamp parsing and selection safeguards Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/d3f58cf7-6417-4b92-9e3e-c6ee08bbd157 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 67 +++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index dd229fa..c9ce5cd 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -11,6 +11,7 @@ use JSON::XS; use Date::Parse; use Time::HiRes; use File::Spec; +use Errno qw(ENOENT); my $log = logger('plugin.siriusxm'); my $prefs = preferences('plugin.siriusxm'); @@ -239,36 +240,44 @@ sub _processResponse { my $selected_reason = 'default latest xmplaylist record'; if ($channel_info && $channel_info->{id}) { - my $tmp_dir = $ENV{TMPDIR} || $ENV{TEMP} || '/tmp'; - my $pdt_file = File::Spec->catfile($tmp_dir, 'siriusxm', 'pdt_' . $channel_info->{id} . '.txt'); - my $play_ts = _readPlayTimestampFromFile($pdt_file); + my $channel_id = $channel_info->{id}; + unless ($channel_id =~ /^[A-Za-z0-9_-]+$/ && $channel_id !~ /\.\./) { + $log->warn("Invalid channel id '$channel_id' for PDT lookup, falling back to latest xmplaylist record"); + $channel_id = undef; + } + + if ($channel_id) { + my $tmp_dir = $ENV{TMPDIR} || $ENV{TEMP} || File::Spec->tmpdir() || '/tmp'; + my $pdt_file = File::Spec->catfile($tmp_dir, 'siriusxm', 'pdt_' . $channel_id . '.txt'); + my $play_ts = _readPlayTimestampFromFile($pdt_file); - if (defined $play_ts) { - my $matched_track; - my $matched_ts; + if (defined $play_ts) { + my $matched_track; + my $matched_ts; - for my $result (@$results) { - next unless $result && ref($result) eq 'HASH'; + for my $result (@$results) { + next unless $result && ref($result) eq 'HASH'; - my $result_ts = _parseTimestampToEpoch($result->{timestamp}); - next unless defined $result_ts; - next if $result_ts > $play_ts; + my $result_ts = _parseTimestampToEpoch($result->{timestamp}); + next unless defined $result_ts; + next if $result_ts > $play_ts; - if (!defined $matched_ts || $result_ts > $matched_ts) { - $matched_track = $result; - $matched_ts = $result_ts; + if (!defined $matched_ts || $result_ts > $matched_ts) { + $matched_track = $result; + $matched_ts = $result_ts; + } } - } - if ($matched_track) { - $selected_track = $matched_track; - $selected_reason = 'matched record at/before play timestamp'; - $log->debug("Selected xmplaylist record timestamp " . ($selected_track->{timestamp} || 'unknown') . " for play timestamp $play_ts"); + if ($matched_track) { + $selected_track = $matched_track; + $selected_reason = 'matched record at/before play timestamp'; + $log->debug("Selected xmplaylist record timestamp " . ($selected_track->{timestamp} || 'unknown') . " for play timestamp $play_ts"); + } else { + $log->debug("No xmplaylist record timestamp <= play timestamp $play_ts, falling back to latest record"); + } } else { - $log->debug("No xmplaylist record timestamp <= play timestamp $play_ts, falling back to latest record"); + $log->debug("No usable play timestamp from $pdt_file, falling back to latest xmplaylist record"); } - } else { - $log->debug("No usable play timestamp from $pdt_file, falling back to latest xmplaylist record"); } } else { $log->debug("Missing channel id in channel info, falling back to latest xmplaylist record"); @@ -368,13 +377,12 @@ sub _processResponse { sub _readPlayTimestampFromFile { my ($pdt_file) = @_; - unless (-e $pdt_file) { - $log->debug("PDT file not found: $pdt_file"); - return; - } - open(my $fh, '<', $pdt_file) or do { - $log->warn("Unable to read PDT file $pdt_file: $!"); + if ($!{ENOENT}) { + $log->debug("PDT file not found: $pdt_file"); + } else { + $log->warn("Unable to read PDT file $pdt_file: $!"); + } return; }; @@ -406,8 +414,9 @@ sub _parseTimestampToEpoch { my ($timestamp) = @_; return unless defined $timestamp; + # Handle epoch seconds (optionally fractional) before trying Date::Parse formats. if ($timestamp =~ /^\s*(\d+(?:\.\d+)?)\s*$/) { - return int($1); + return $1 + 0; } return str2time($timestamp); From aca5a7067c79ce07d9ba0cc1115eaddadba46773 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:32:30 +0000 Subject: [PATCH 04/25] Apply PR feedback for channel-metadata fallback and stale-check removal Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/55872c72-26ab-4c7c-9bf7-fb5ba0ac8f0d Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 52 ++++++++++----------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index c9ce5cd..6596b84 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -17,7 +17,6 @@ my $log = logger('plugin.siriusxm'); my $prefs = preferences('plugin.siriusxm'); my $cache = Slim::Utils::Cache->new(); -use constant METADATA_STALE_TIME => 230; use constant STATION_CACHE_TIMEOUT => 21600; # 6 hours # xmplaylists.com API JSON Schema: @@ -238,20 +237,24 @@ sub _processResponse { my $selected_track = $results->[0]; my $selected_reason = 'default latest xmplaylist record'; + my $pdt_timestamp_available = 0; if ($channel_info && $channel_info->{id}) { my $channel_id = $channel_info->{id}; unless ($channel_id =~ /^[A-Za-z0-9_-]+$/ && $channel_id !~ /\.\./) { - $log->warn("Invalid channel id '$channel_id' for PDT lookup, falling back to latest xmplaylist record"); + $log->warn("Invalid channel id '$channel_id' for PDT lookup, falling back to channel metadata"); + $selected_reason = 'invalid siriusxm channel id'; $channel_id = undef; } if ($channel_id) { my $tmp_dir = $ENV{TMPDIR} || $ENV{TEMP} || File::Spec->tmpdir() || '/tmp'; my $pdt_file = File::Spec->catfile($tmp_dir, 'siriusxm', 'pdt_' . $channel_id . '.txt'); + $log->debug("Checking PDT file for SiriusXM channel id $channel_id: $pdt_file"); my $play_ts = _readPlayTimestampFromFile($pdt_file); if (defined $play_ts) { + $pdt_timestamp_available = 1; my $matched_track; my $matched_ts; @@ -276,50 +279,27 @@ sub _processResponse { $log->debug("No xmplaylist record timestamp <= play timestamp $play_ts, falling back to latest record"); } } else { - $log->debug("No usable play timestamp from $pdt_file, falling back to latest xmplaylist record"); + $selected_reason = 'no usable play timestamp from pdt file'; + $log->debug("No usable play timestamp from $pdt_file, falling back to channel metadata"); } } } else { - $log->debug("Missing channel id in channel info, falling back to latest xmplaylist record"); + $selected_reason = 'missing siriusxm channel id'; + $log->debug("Missing SiriusXM channel id in channel info, falling back to channel metadata"); } my $track_info = $selected_track->{track}; my $spotify_info = $selected_track->{spotify}; - my $timestamp = $selected_track->{timestamp}; $log->debug("Metadata source selection: $selected_reason"); - return unless $track_info; - - # Determine whether to use xmplaylists metadata or fallback to channel info - # based on timestamp (if metadata is 0-230 seconds old, use it; otherwise use channel info) + # Determine whether to use xmplaylist metadata or channel metadata. + # xmplaylist metadata is only used when we have a usable playback timestamp from the pdt file. my $use_xmplaylists_metadata = 0; my $metadata_is_fresh = 0; - - # Only consider xmplaylists metadata if metadata is enabled - if ($prefs->get('enable_metadata') && $timestamp) { - eval { - # Use Date::Parse to handle UTC timestamp format: 2025-08-09T15:57:41.586Z - my $track_time = str2time($timestamp); - die "Failed to parse timestamp" unless defined $track_time; - - my $current_time = time(); - my $age_seconds = $current_time - $track_time; - - $log->debug("Track timestamp: $timestamp, age: ${age_seconds}s"); - - # Use xmplaylists metadata if timestamp is (200 seconds) or newer - if ($age_seconds <= METADATA_STALE_TIME) { - $use_xmplaylists_metadata = 1; - $metadata_is_fresh = 1; - } - }; - - if ($@) { - $log->warn("Failed to parse timestamp '$timestamp': $@"); - # Default to using xmplaylists metadata if we can't parse timestamp - $use_xmplaylists_metadata = 1; - $metadata_is_fresh = 1; - } + + if ($prefs->get('enable_metadata') && $pdt_timestamp_available && $track_info) { + $use_xmplaylists_metadata = 1; + $metadata_is_fresh = 1; } # Build new metadata @@ -352,7 +332,7 @@ sub _processResponse { $new_meta->{bitrate} = ''; } else { - # Fall back to basic channel info when metadata is too old or disabled + # Fall back to basic channel info when xmplaylist metadata is unavailable or disabled if ($channel_info) { # Fall back to basic channel info $new_meta->{artist} = $channel_info->{name}; From 72abed582bf3ffb15a9cdb3005b1791a62d7230e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:46:37 +0000 Subject: [PATCH 05/25] Fix metadata refresh lag by comparing metadata content signatures Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/e2012d0d-80fc-429d-a656-9c1d26cc5678 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index 3f49761..8b104ce 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -172,6 +172,7 @@ sub onPlayerEvent { url => $url, channel_info => $channel_info, last_next => undef, + last_metadata_signature => undef, timer => undef, }; _fetchMetadataFromAPI($client); @@ -206,6 +207,7 @@ sub _startMetadataTimer { url => $url, channel_info => $channel_info, last_next => undef, + last_metadata_signature => undef, timer => undef, }; @@ -312,9 +314,16 @@ sub _updateClientMetadata { my $new_meta = $result->{metadata}; my $next = $result->{next}; my $metadata_is_fresh = $result->{is_fresh}; - - # Check if metadata has changed using "next" field - if (defined $state->{last_next} && defined $next && $state->{last_next} eq $next) { + my $metadata_signature = _metadataSignature($new_meta); + + # Check if metadata content has changed. + # Using metadata signature avoids lag when xmplaylist's "next" token + # stays constant while the selected record for play-behind-live changes. + if ( + defined $state->{last_metadata_signature} + && defined $metadata_signature + && $state->{last_metadata_signature} eq $metadata_signature + ) { # Only skip update if metadata is fresh - if stale, we need to update display if ($metadata_is_fresh) { $log->debug("No new metadata available and current metadata is fresh - skipping update"); @@ -326,6 +335,7 @@ sub _updateClientMetadata { # Update the last_next value $state->{last_next} = $next; + $state->{last_metadata_signature} = $metadata_signature; # Update the current song's metadata if we have new information if ($new_meta && keys %$new_meta) { @@ -356,6 +366,13 @@ sub _updateClientMetadata { } } +sub _metadataSignature { + my ($meta) = @_; + return unless $meta && ref($meta) eq 'HASH'; + + return JSON::XS->new->canonical(1)->encode($meta); +} + # Handle sxm: protocol URLs by converting them to HTTP proxy URLs sub getFormatForURL { my ($class, $url) = @_; From 06ec31b8b1ec56701e61ac2b34b088078ad5cbff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 18:48:55 +0000 Subject: [PATCH 06/25] Fix delayed metadata updates by using metadata signatures Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/e2012d0d-80fc-429d-a656-9c1d26cc5678 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index 8b104ce..dd1d30c 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -21,6 +21,7 @@ use Plugins::SiriusXM::APImetadata; my $log = logger('plugin.siriusxm'); my $prefs = preferences('plugin.siriusxm'); +my $json_encoder = JSON::XS->new->canonical(1); # Metadata update interval (25 seconds) use constant METADATA_UPDATE_INTERVAL => 25; @@ -171,7 +172,6 @@ sub onPlayerEvent { $playerStates{$clientId} = { url => $url, channel_info => $channel_info, - last_next => undef, last_metadata_signature => undef, timer => undef, }; @@ -206,7 +206,6 @@ sub _startMetadataTimer { $playerStates{$clientId} = { url => $url, channel_info => $channel_info, - last_next => undef, last_metadata_signature => undef, timer => undef, }; @@ -312,9 +311,8 @@ sub _updateClientMetadata { return unless $state; my $new_meta = $result->{metadata}; - my $next = $result->{next}; my $metadata_is_fresh = $result->{is_fresh}; - my $metadata_signature = _metadataSignature($new_meta); + my $metadata_signature = _metadataSignature($new_meta, "client $clientId"); # Check if metadata content has changed. # Using metadata signature avoids lag when xmplaylist's "next" token @@ -333,8 +331,6 @@ sub _updateClientMetadata { } } - # Update the last_next value - $state->{last_next} = $next; $state->{last_metadata_signature} = $metadata_signature; # Update the current song's metadata if we have new information @@ -367,10 +363,20 @@ sub _updateClientMetadata { } sub _metadataSignature { - my ($meta) = @_; + my ($meta, $context) = @_; return unless $meta && ref($meta) eq 'HASH'; + $context ||= 'unknown context'; - return JSON::XS->new->canonical(1)->encode($meta); + my $signature; + eval { + $signature = $json_encoder->encode($meta); + }; + if ($@) { + $log->warn("Failed to encode metadata signature for $context: $@"); + return; + } + + return $signature; } # Handle sxm: protocol URLs by converting them to HTTP proxy URLs From a50bd5e5812cebc5552f94e1a0d5c596d0c01737 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:27:49 +0000 Subject: [PATCH 07/25] Schedule metadata checks using next xmplaylist track offset Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/31bc80a5-4475-4eba-bafb-99f4aed978d6 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 19 +++++++++- Plugins/SiriusXM/ProtocolHandler.pm | 58 ++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 6596b84..8a4cfc5 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -238,6 +238,7 @@ sub _processResponse { my $selected_track = $results->[0]; my $selected_reason = 'default latest xmplaylist record'; my $pdt_timestamp_available = 0; + my $next_update_delay; if ($channel_info && $channel_info->{id}) { my $channel_id = $channel_info->{id}; @@ -257,13 +258,20 @@ sub _processResponse { $pdt_timestamp_available = 1; my $matched_track; my $matched_ts; + my $next_track_ts; for my $result (@$results) { next unless $result && ref($result) eq 'HASH'; my $result_ts = _parseTimestampToEpoch($result->{timestamp}); next unless defined $result_ts; - next if $result_ts > $play_ts; + + if ($result_ts > $play_ts) { + if (!defined $next_track_ts || $result_ts < $next_track_ts) { + $next_track_ts = $result_ts; + } + next; + } if (!defined $matched_ts || $result_ts > $matched_ts) { $matched_track = $result; @@ -271,6 +279,14 @@ sub _processResponse { } } + if (defined $next_track_ts) { + $next_update_delay = $next_track_ts - $play_ts; + if ($next_update_delay < 1) { + $next_update_delay = 1; + } + $log->debug("Next xmplaylist track timestamp is in ${next_update_delay}s relative to play timestamp"); + } + if ($matched_track) { $selected_track = $matched_track; $selected_reason = 'matched record at/before play timestamp'; @@ -350,6 +366,7 @@ sub _processResponse { metadata => $new_meta, next => $data->{next}, is_fresh => $metadata_is_fresh, + next_update_delay => $next_update_delay, }); } } diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index dd1d30c..bb8f43f 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -212,13 +212,6 @@ sub _startMetadataTimer { # Start immediate metadata fetch _fetchMetadataFromAPI($client); - - # Schedule periodic updates - $playerStates{$clientId}->{timer} = Slim::Utils::Timers::setTimer( - $client, - time() + METADATA_UPDATE_INTERVAL, - \&_onMetadataTimer - ); } # Stop metadata update timer for a client @@ -268,14 +261,6 @@ sub _onMetadataTimer { return; } - # Schedule next update if still playing - if (exists $playerStates{$clientId}) { - $playerStates{$clientId}->{timer} = Slim::Utils::Timers::setTimer( - $client, - time() + METADATA_UPDATE_INTERVAL, - \&_onMetadataTimer - ); - } } # Fetch metadata from xmplaylist.com API using APImetadata module @@ -293,12 +278,49 @@ sub _fetchMetadataFromAPI { Plugins::SiriusXM::APImetadata->fetchMetadata($client, $channel_info, sub { my $result = shift; - return unless $result; - - _updateClientMetadata($client, $result); + my $next_delay = METADATA_UPDATE_INTERVAL; + + if ($result) { + _updateClientMetadata($client, $result); + + if ( + defined $result->{next_update_delay} + && $result->{next_update_delay} =~ /^\d+(?:\.\d+)?$/ + && $result->{next_update_delay} > 0 + ) { + $next_delay = $result->{next_update_delay}; + } + } + + _scheduleNextMetadataUpdate($client, $next_delay); }); } +sub _scheduleNextMetadataUpdate { + my ($client, $delay) = @_; + return unless $client; + + # Let the metadata refresh one more time, to return player screens to channel artwork. + unless ($prefs->get('enable_metadata')) { + $log->debug("Metadata updates disabled by user preference, stopping timer"); + _stopMetadataTimer($client); + return; + } + + my $clientId = $client->id(); + return unless exists $playerStates{$clientId}; + + $delay = METADATA_UPDATE_INTERVAL unless defined $delay && $delay > 0; + $delay = 1 if $delay < 1; + $log->debug("Scheduling next metadata update for client $clientId in ${delay}s"); + + $playerStates{$clientId}->{timer} = Slim::Utils::Timers::setTimer( + $client, + time() + $delay, + \&_onMetadataTimer + ); +} + # Update client with new metadata sub _updateClientMetadata { my ($client, $result) = @_; From fc1f00047abbb911eebfef8d3bd789eb7aaf876c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:30:35 +0000 Subject: [PATCH 08/25] Use xmplaylist future-track offset to schedule metadata refresh Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/31bc80a5-4475-4eba-bafb-99f4aed978d6 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 12 +++++++----- Plugins/SiriusXM/ProtocolHandler.pm | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 8a4cfc5..50bb144 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -18,6 +18,7 @@ my $prefs = preferences('plugin.siriusxm'); my $cache = Slim::Utils::Cache->new(); use constant STATION_CACHE_TIMEOUT => 21600; # 6 hours +use constant MIN_NEXT_UPDATE_DELAY_SECONDS => 1; # xmplaylists.com API JSON Schema: # { @@ -267,9 +268,10 @@ sub _processResponse { next unless defined $result_ts; if ($result_ts > $play_ts) { - if (!defined $next_track_ts || $result_ts < $next_track_ts) { - $next_track_ts = $result_ts; - } + # Track the nearest upcoming record so ProtocolHandler can + # schedule the next metadata refresh near the transition time. + $next_track_ts = $result_ts + if !defined $next_track_ts || $result_ts < $next_track_ts; next; } @@ -281,8 +283,8 @@ sub _processResponse { if (defined $next_track_ts) { $next_update_delay = $next_track_ts - $play_ts; - if ($next_update_delay < 1) { - $next_update_delay = 1; + if ($next_update_delay < MIN_NEXT_UPDATE_DELAY_SECONDS) { + $next_update_delay = MIN_NEXT_UPDATE_DELAY_SECONDS; } $log->debug("Next xmplaylist track timestamp is in ${next_update_delay}s relative to play timestamp"); } diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index bb8f43f..facbb3d 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -284,14 +284,17 @@ sub _fetchMetadataFromAPI { _updateClientMetadata($client, $result); if ( - defined $result->{next_update_delay} - && $result->{next_update_delay} =~ /^\d+(?:\.\d+)?$/ - && $result->{next_update_delay} > 0 + _isValidDelay($result->{next_update_delay}) ) { $next_delay = $result->{next_update_delay}; } } + unless ($prefs->get('enable_metadata')) { + _stopMetadataTimer($client); + return; + } + _scheduleNextMetadataUpdate($client, $next_delay); }); } @@ -300,18 +303,10 @@ sub _scheduleNextMetadataUpdate { my ($client, $delay) = @_; return unless $client; - # Let the metadata refresh one more time, to return player screens to channel artwork. - unless ($prefs->get('enable_metadata')) { - $log->debug("Metadata updates disabled by user preference, stopping timer"); - _stopMetadataTimer($client); - return; - } - my $clientId = $client->id(); return unless exists $playerStates{$clientId}; - $delay = METADATA_UPDATE_INTERVAL unless defined $delay && $delay > 0; - $delay = 1 if $delay < 1; + $delay = METADATA_UPDATE_INTERVAL unless _isValidDelay($delay); $log->debug("Scheduling next metadata update for client $clientId in ${delay}s"); $playerStates{$clientId}->{timer} = Slim::Utils::Timers::setTimer( @@ -401,6 +396,11 @@ sub _metadataSignature { return $signature; } +sub _isValidDelay { + my ($delay) = @_; + return defined $delay && $delay =~ /^\d+(?:\.\d+)?$/ && $delay > 0; +} + # Handle sxm: protocol URLs by converting them to HTTP proxy URLs sub getFormatForURL { my ($class, $url) = @_; From 60911792b2af89fbd3b409f4ad597ddab5fc0c3d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:43:41 +0000 Subject: [PATCH 09/25] Adjust metadata delay for PDT file age and dedupe timers Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/69c2cf84-9b9b-4705-9501-d540bbe4a4b5 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 20 ++++++++++++++++---- Plugins/SiriusXM/ProtocolHandler.pm | 4 ++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 50bb144..2ba919a 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -253,7 +253,7 @@ sub _processResponse { my $tmp_dir = $ENV{TMPDIR} || $ENV{TEMP} || File::Spec->tmpdir() || '/tmp'; my $pdt_file = File::Spec->catfile($tmp_dir, 'siriusxm', 'pdt_' . $channel_id . '.txt'); $log->debug("Checking PDT file for SiriusXM channel id $channel_id: $pdt_file"); - my $play_ts = _readPlayTimestampFromFile($pdt_file); + my ($play_ts, $pdt_file_mtime) = _readPlayTimestampFromFile($pdt_file); if (defined $play_ts) { $pdt_timestamp_available = 1; @@ -282,11 +282,17 @@ sub _processResponse { } if (defined $next_track_ts) { - $next_update_delay = $next_track_ts - $play_ts; + my $pdt_age = 0; + if (defined $pdt_file_mtime) { + $pdt_age = Time::HiRes::time() - $pdt_file_mtime; + $pdt_age = 0 if $pdt_age < 0; + } + + $next_update_delay = $next_track_ts - $play_ts - $pdt_age; if ($next_update_delay < MIN_NEXT_UPDATE_DELAY_SECONDS) { $next_update_delay = MIN_NEXT_UPDATE_DELAY_SECONDS; } - $log->debug("Next xmplaylist track timestamp is in ${next_update_delay}s relative to play timestamp"); + $log->debug("Next xmplaylist track timestamp is in ${next_update_delay}s relative to playback (pdt age=${pdt_age}s)"); } if ($matched_track) { @@ -405,8 +411,14 @@ sub _readPlayTimestampFromFile { return; } + my $pdt_file_mtime; + my @stats = stat($pdt_file); + if (@stats) { + $pdt_file_mtime = $stats[9]; + } + $log->debug("Read play timestamp '$raw_ts' ($play_ts) from $pdt_file"); - return $play_ts; + return ($play_ts, $pdt_file_mtime); } sub _parseTimestampToEpoch { diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index facbb3d..f0abe43 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -309,6 +309,10 @@ sub _scheduleNextMetadataUpdate { $delay = METADATA_UPDATE_INTERVAL unless _isValidDelay($delay); $log->debug("Scheduling next metadata update for client $clientId in ${delay}s"); + if ($playerStates{$clientId}->{timer}) { + Slim::Utils::Timers::killTimers($client, \&_onMetadataTimer); + } + $playerStates{$clientId}->{timer} = Slim::Utils::Timers::setTimer( $client, time() + $delay, From 2b95d0f81342ec856fc1e7af22c6d4defc9e833a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:44:10 +0000 Subject: [PATCH 10/25] Clarify PDT age fallback in delay calculation Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/69c2cf84-9b9b-4705-9501-d540bbe4a4b5 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 2ba919a..640119b 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -282,6 +282,8 @@ sub _processResponse { } if (defined $next_track_ts) { + # If mtime is unavailable, treat age as 0 and use the raw + # timestamp delta (best-effort fallback). my $pdt_age = 0; if (defined $pdt_file_mtime) { $pdt_age = Time::HiRes::time() - $pdt_file_mtime; From 82fb5a3144b7c594832a37412498275b7d662b35 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:44:44 +0000 Subject: [PATCH 11/25] Log PDT clock skew and document stat mtime index Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/69c2cf84-9b9b-4705-9501-d540bbe4a4b5 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 640119b..018b95a 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -287,7 +287,10 @@ sub _processResponse { my $pdt_age = 0; if (defined $pdt_file_mtime) { $pdt_age = Time::HiRes::time() - $pdt_file_mtime; - $pdt_age = 0 if $pdt_age < 0; + if ($pdt_age < 0) { + $log->warn("PDT file mtime is in the future (clock skew?), clamping age to 0 for $pdt_file"); + $pdt_age = 0; + } } $next_update_delay = $next_track_ts - $play_ts - $pdt_age; @@ -416,6 +419,7 @@ sub _readPlayTimestampFromFile { my $pdt_file_mtime; my @stats = stat($pdt_file); if (@stats) { + # stat()[9] is mtime (seconds since epoch) $pdt_file_mtime = $stats[9]; } From e16d047cf8f5aa8d2099a6e7e805a73c0e17d848 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:45:21 +0000 Subject: [PATCH 12/25] Log raw and adjusted metadata delay values Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/69c2cf84-9b9b-4705-9501-d540bbe4a4b5 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 018b95a..80fb337 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -282,6 +282,8 @@ sub _processResponse { } if (defined $next_track_ts) { + my $raw_next_update_delay = $next_track_ts - $play_ts; + # If mtime is unavailable, treat age as 0 and use the raw # timestamp delta (best-effort fallback). my $pdt_age = 0; @@ -293,11 +295,11 @@ sub _processResponse { } } - $next_update_delay = $next_track_ts - $play_ts - $pdt_age; + $next_update_delay = $raw_next_update_delay - $pdt_age; if ($next_update_delay < MIN_NEXT_UPDATE_DELAY_SECONDS) { $next_update_delay = MIN_NEXT_UPDATE_DELAY_SECONDS; } - $log->debug("Next xmplaylist track timestamp is in ${next_update_delay}s relative to playback (pdt age=${pdt_age}s)"); + $log->debug("Next xmplaylist track timestamp raw delay=${raw_next_update_delay}s adjusted=${next_update_delay}s (pdt age=${pdt_age}s)"); } if ($matched_track) { From fb07a2db3a2c330e7addceeaaaa2828f7bacad03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:52:39 +0000 Subject: [PATCH 13/25] Use timestamp-only delay rounding and drop mtime adjustment Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/cd18f41e-70d8-40c3-a99f-884002791bdf Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 29 ++++++----------------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 80fb337..02bf6a5 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -9,7 +9,6 @@ use Slim::Utils::Cache; use Slim::Networking::SimpleAsyncHTTP; use JSON::XS; use Date::Parse; -use Time::HiRes; use File::Spec; use Errno qw(ENOENT); @@ -253,7 +252,7 @@ sub _processResponse { my $tmp_dir = $ENV{TMPDIR} || $ENV{TEMP} || File::Spec->tmpdir() || '/tmp'; my $pdt_file = File::Spec->catfile($tmp_dir, 'siriusxm', 'pdt_' . $channel_id . '.txt'); $log->debug("Checking PDT file for SiriusXM channel id $channel_id: $pdt_file"); - my ($play_ts, $pdt_file_mtime) = _readPlayTimestampFromFile($pdt_file); + my $play_ts = _readPlayTimestampFromFile($pdt_file); if (defined $play_ts) { $pdt_timestamp_available = 1; @@ -283,23 +282,14 @@ sub _processResponse { if (defined $next_track_ts) { my $raw_next_update_delay = $next_track_ts - $play_ts; - - # If mtime is unavailable, treat age as 0 and use the raw - # timestamp delta (best-effort fallback). - my $pdt_age = 0; - if (defined $pdt_file_mtime) { - $pdt_age = Time::HiRes::time() - $pdt_file_mtime; - if ($pdt_age < 0) { - $log->warn("PDT file mtime is in the future (clock skew?), clamping age to 0 for $pdt_file"); - $pdt_age = 0; - } + $next_update_delay = int($raw_next_update_delay); + if ($raw_next_update_delay > $next_update_delay) { + $next_update_delay++; } - - $next_update_delay = $raw_next_update_delay - $pdt_age; if ($next_update_delay < MIN_NEXT_UPDATE_DELAY_SECONDS) { $next_update_delay = MIN_NEXT_UPDATE_DELAY_SECONDS; } - $log->debug("Next xmplaylist track timestamp raw delay=${raw_next_update_delay}s adjusted=${next_update_delay}s (pdt age=${pdt_age}s)"); + $log->debug("Next xmplaylist track timestamp raw delay=${raw_next_update_delay}s rounded=${next_update_delay}s"); } if ($matched_track) { @@ -418,15 +408,8 @@ sub _readPlayTimestampFromFile { return; } - my $pdt_file_mtime; - my @stats = stat($pdt_file); - if (@stats) { - # stat()[9] is mtime (seconds since epoch) - $pdt_file_mtime = $stats[9]; - } - $log->debug("Read play timestamp '$raw_ts' ($play_ts) from $pdt_file"); - return ($play_ts, $pdt_file_mtime); + return $play_ts; } sub _parseTimestampToEpoch { From 5d4f0b75204e9d294635c40a8fd92f67d53238ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:21:33 +0000 Subject: [PATCH 14/25] Avoid reinitializing metadata when stream is stopped Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/c5fce86f-cb8f-4e01-bcc3-c593fe679ab7 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index f0abe43..d7ad736 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -166,6 +166,11 @@ sub onPlayerEvent { # Initialize Player Metatadata my $state = $playerStates{$clientId}; if (!$state) { + unless ($client->isPlaying()) { + $log->debug("Client $clientId is not playing, skipping metadata state initialization"); + return; + } + $log->debug("No current player state, configuring"); my $channel_info = __PACKAGE__->getChannelInfoFromUrl($url); # Initialize player state From 7cc5f2eec54be14b106dc8ec91997c11566c547f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:22:02 +0000 Subject: [PATCH 15/25] Tighten stop-loop fix and clean nearby comment text Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/c5fce86f-cb8f-4e01-bcc3-c593fe679ab7 Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index d7ad736..d4319d9 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -163,7 +163,7 @@ sub onPlayerEvent { } } - # Initialize Player Metatadata + # Initialize player metadata my $state = $playerStates{$clientId}; if (!$state) { unless ($client->isPlaying()) { From 207f9c2df53869bcf2d4286f6995baa3fd064fb6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:41:30 +0000 Subject: [PATCH 16/25] Guard async metadata callbacks against stale player state Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/7263d41a-a4dd-4708-9f54-c2c46c08aacd Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index d4319d9..9c695d4 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -32,6 +32,9 @@ my %playerStates = (); # Global hash to track metadata by channel ID my %channelMetadata = (); +# Monotonic token to invalidate stale async metadata callbacks +my $metadataRequestToken = 0; + sub new { my $class = shift; my $args = shift; @@ -178,6 +181,7 @@ sub onPlayerEvent { url => $url, channel_info => $channel_info, last_metadata_signature => undef, + metadata_request_token => 0, timer => undef, }; _fetchMetadataFromAPI($client); @@ -212,6 +216,7 @@ sub _startMetadataTimer { url => $url, channel_info => $channel_info, last_metadata_signature => undef, + metadata_request_token => 0, timer => undef, }; @@ -280,10 +285,41 @@ sub _fetchMetadataFromAPI { return unless $state && $state->{channel_info}; my $channel_info = $state->{channel_info}; + my $request_token = ++$metadataRequestToken; + my $request_channel_id = $channel_info->{id}; + $state->{metadata_request_token} = $request_token; Plugins::SiriusXM::APImetadata->fetchMetadata($client, $channel_info, sub { my $result = shift; my $next_delay = METADATA_UPDATE_INTERVAL; + my $current_state = $playerStates{$clientId}; + + unless ($current_state) { + $log->debug("Ignoring async metadata response for client $clientId: no active player state"); + return; + } + + if (($current_state->{metadata_request_token} || 0) != $request_token) { + $log->debug("Ignoring stale async metadata response for client $clientId token $request_token"); + return; + } + + unless ($client->isPlaying()) { + $log->debug("Ignoring async metadata response for client $clientId: client no longer playing"); + _stopMetadataTimer($client); + return; + } + + my $current_channel_id = $current_state->{channel_info} + ? $current_state->{channel_info}->{id} + : undef; + if ( + defined $request_channel_id && defined $current_channel_id + && $request_channel_id ne $current_channel_id + ) { + $log->debug("Ignoring stale async metadata response for client $clientId channel $request_channel_id"); + return; + } if ($result) { _updateClientMetadata($client, $result); From 800e4e201b2739405d37ed8ed3e0bc9281029cc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:42:09 +0000 Subject: [PATCH 17/25] Refine async token checks from review feedback Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/7263d41a-a4dd-4708-9f54-c2c46c08aacd Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index 9c695d4..cfe0b06 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -285,7 +285,8 @@ sub _fetchMetadataFromAPI { return unless $state && $state->{channel_info}; my $channel_info = $state->{channel_info}; - my $request_token = ++$metadataRequestToken; + my $request_token = $metadataRequestToken + 1; + $metadataRequestToken = $request_token; my $request_channel_id = $channel_info->{id}; $state->{metadata_request_token} = $request_token; @@ -299,7 +300,7 @@ sub _fetchMetadataFromAPI { return; } - if (($current_state->{metadata_request_token} || 0) != $request_token) { + if (($current_state->{metadata_request_token} // 0) != $request_token) { $log->debug("Ignoring stale async metadata response for client $clientId token $request_token"); return; } From 29f39e7fa6c78b61aa763a7388c3a411dac73473 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:42:45 +0000 Subject: [PATCH 18/25] Use unique request tokens for async metadata callbacks Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/7263d41a-a4dd-4708-9f54-c2c46c08aacd Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index cfe0b06..724e62d 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -32,9 +32,6 @@ my %playerStates = (); # Global hash to track metadata by channel ID my %channelMetadata = (); -# Monotonic token to invalidate stale async metadata callbacks -my $metadataRequestToken = 0; - sub new { my $class = shift; my $args = shift; @@ -285,8 +282,7 @@ sub _fetchMetadataFromAPI { return unless $state && $state->{channel_info}; my $channel_info = $state->{channel_info}; - my $request_token = $metadataRequestToken + 1; - $metadataRequestToken = $request_token; + my $request_token = join(':', $clientId, Time::HiRes::time(), int(rand(1_000_000))); my $request_channel_id = $channel_info->{id}; $state->{metadata_request_token} = $request_token; @@ -300,7 +296,7 @@ sub _fetchMetadataFromAPI { return; } - if (($current_state->{metadata_request_token} // 0) != $request_token) { + if (($current_state->{metadata_request_token} // '') ne $request_token) { $log->debug("Ignoring stale async metadata response for client $clientId token $request_token"); return; } From 54c35b39a91de9f7d3d6d59f58fea8820cbc18c8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:43:17 +0000 Subject: [PATCH 19/25] Refine async request token generation and checks Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/7263d41a-a4dd-4708-9f54-c2c46c08aacd Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index 724e62d..d464dc0 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -12,6 +12,7 @@ use Slim::Utils::Cache; use Slim::Utils::Timers; use Slim::Networking::SimpleAsyncHTTP; use Slim::Player::Playlist; +use Scalar::Util qw(refaddr); use JSON::XS; use Data::Dumper; use Date::Parse; @@ -282,7 +283,7 @@ sub _fetchMetadataFromAPI { return unless $state && $state->{channel_info}; my $channel_info = $state->{channel_info}; - my $request_token = join(':', $clientId, Time::HiRes::time(), int(rand(1_000_000))); + my $request_token = join(':', $clientId, refaddr($state), Time::HiRes::time()); my $request_channel_id = $channel_info->{id}; $state->{metadata_request_token} = $request_token; @@ -296,7 +297,8 @@ sub _fetchMetadataFromAPI { return; } - if (($current_state->{metadata_request_token} // '') ne $request_token) { + my $current_request_token = $current_state->{metadata_request_token}; + if (!defined $current_request_token || $current_request_token ne $request_token) { $log->debug("Ignoring stale async metadata response for client $clientId token $request_token"); return; } From e58c605ce34b449a9b6f38901a3c3813a6d063ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:43:48 +0000 Subject: [PATCH 20/25] Import hi-res time and make token init explicit undef Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/7263d41a-a4dd-4708-9f54-c2c46c08aacd Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index d464dc0..0876624 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -13,6 +13,7 @@ use Slim::Utils::Timers; use Slim::Networking::SimpleAsyncHTTP; use Slim::Player::Playlist; use Scalar::Util qw(refaddr); +use Time::HiRes qw(time); use JSON::XS; use Data::Dumper; use Date::Parse; @@ -179,7 +180,7 @@ sub onPlayerEvent { url => $url, channel_info => $channel_info, last_metadata_signature => undef, - metadata_request_token => 0, + metadata_request_token => undef, timer => undef, }; _fetchMetadataFromAPI($client); @@ -214,7 +215,7 @@ sub _startMetadataTimer { url => $url, channel_info => $channel_info, last_metadata_signature => undef, - metadata_request_token => 0, + metadata_request_token => undef, timer => undef, }; @@ -283,7 +284,7 @@ sub _fetchMetadataFromAPI { return unless $state && $state->{channel_info}; my $channel_info = $state->{channel_info}; - my $request_token = join(':', $clientId, refaddr($state), Time::HiRes::time()); + my $request_token = join(':', $clientId, refaddr($state), time()); my $request_channel_id = $channel_info->{id}; $state->{metadata_request_token} = $request_token; From e01d866655504c5fdf8b4123a827fd881a30753d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:44:20 +0000 Subject: [PATCH 21/25] Add per-state request sequence for async token uniqueness Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/7263d41a-a4dd-4708-9f54-c2c46c08aacd Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index 0876624..bc29850 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -181,6 +181,7 @@ sub onPlayerEvent { channel_info => $channel_info, last_metadata_signature => undef, metadata_request_token => undef, + metadata_request_seq => 0, timer => undef, }; _fetchMetadataFromAPI($client); @@ -216,6 +217,7 @@ sub _startMetadataTimer { channel_info => $channel_info, last_metadata_signature => undef, metadata_request_token => undef, + metadata_request_seq => 0, timer => undef, }; @@ -284,7 +286,8 @@ sub _fetchMetadataFromAPI { return unless $state && $state->{channel_info}; my $channel_info = $state->{channel_info}; - my $request_token = join(':', $clientId, refaddr($state), time()); + my $request_seq = ++$state->{metadata_request_seq}; + my $request_token = join(':', $clientId, refaddr($state), $request_seq, time()); my $request_channel_id = $channel_info->{id}; $state->{metadata_request_token} = $request_token; @@ -299,7 +302,12 @@ sub _fetchMetadataFromAPI { } my $current_request_token = $current_state->{metadata_request_token}; - if (!defined $current_request_token || $current_request_token ne $request_token) { + if (!defined $current_request_token) { + $log->debug("Ignoring stale async metadata response for client $clientId: missing request token"); + return; + } + + if ($current_request_token ne $request_token) { $log->debug("Ignoring stale async metadata response for client $clientId token $request_token"); return; } From ae4bf1042a925f52457a7a0245c8f3d24d64c589 Mon Sep 17 00:00:00 2001 From: paul-1 <6473457+paul-1@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:42:44 -0400 Subject: [PATCH 22/25] Move Current playing pointer 20 behind last segment pulled. --- Plugins/SiriusXM/APImetadata.pm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 02bf6a5..4a5a665 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -17,7 +17,7 @@ my $prefs = preferences('plugin.siriusxm'); my $cache = Slim::Utils::Cache->new(); use constant STATION_CACHE_TIMEOUT => 21600; # 6 hours -use constant MIN_NEXT_UPDATE_DELAY_SECONDS => 1; +use constant MIN_NEXT_UPDATE_DELAY_SECONDS => 10; # xmplaylists.com API JSON Schema: # { @@ -408,6 +408,8 @@ sub _readPlayTimestampFromFile { return; } + $play_ts -= 20; # This is when the segment is pulled by FFMpeg. Playing segment is 2 segments behind. + $log->debug("Read play timestamp '$raw_ts' ($play_ts) from $pdt_file"); return $play_ts; } From 36c4fdd97a377b8793284164a2abb026bfa8400d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:50:37 +0000 Subject: [PATCH 23/25] Cache next-track metadata for delayed local apply Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/d75b7846-4258-4cad-bc14-390a93faf08f Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/APImetadata.pm | 37 +++++++++++++++++++++++++++-- Plugins/SiriusXM/ProtocolHandler.pm | 24 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Plugins/SiriusXM/APImetadata.pm b/Plugins/SiriusXM/APImetadata.pm index 4a5a665..b754e38 100644 --- a/Plugins/SiriusXM/APImetadata.pm +++ b/Plugins/SiriusXM/APImetadata.pm @@ -239,6 +239,7 @@ sub _processResponse { my $selected_reason = 'default latest xmplaylist record'; my $pdt_timestamp_available = 0; my $next_update_delay; + my $next_track; if ($channel_info && $channel_info->{id}) { my $channel_id = $channel_info->{id}; @@ -269,8 +270,10 @@ sub _processResponse { if ($result_ts > $play_ts) { # Track the nearest upcoming record so ProtocolHandler can # schedule the next metadata refresh near the transition time. - $next_track_ts = $result_ts - if !defined $next_track_ts || $result_ts < $next_track_ts; + if (!defined $next_track_ts || $result_ts < $next_track_ts) { + $next_track_ts = $result_ts; + $next_track = $result; + } next; } @@ -365,6 +368,35 @@ sub _processResponse { } } + my $next_meta; + if ($next_track && ref($next_track) eq 'HASH') { + my $next_track_info = $next_track->{track}; + my $next_spotify_info = $next_track->{spotify}; + + if ($next_track_info) { + $next_meta = {}; + + if ($next_track_info->{title}) { + $next_meta->{title} = $next_track_info->{title}; + } + + if ($next_track_info->{artists} && ref($next_track_info->{artists}) eq 'ARRAY') { + my @artists = @{$next_track_info->{artists}}; + if (@artists) { + $next_meta->{artist} = join(', ', @artists); + } + } + + if ($next_spotify_info && $next_spotify_info->{albumImageLarge}) { + $next_meta->{cover} = $next_spotify_info->{albumImageLarge}; + $next_meta->{icon} = $next_spotify_info->{albumImageLarge}; + } + + $next_meta->{album} = $channel_info->{name} || 'SiriusXM'; + $next_meta->{bitrate} = ''; + } + } + # Return metadata and freshness info through callback if ($callback) { $callback->({ @@ -372,6 +404,7 @@ sub _processResponse { next => $data->{next}, is_fresh => $metadata_is_fresh, next_update_delay => $next_update_delay, + next_metadata => $next_meta, }); } } diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index bc29850..0c2011a 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -182,6 +182,7 @@ sub onPlayerEvent { last_metadata_signature => undef, metadata_request_token => undef, metadata_request_seq => 0, + pending_metadata_result => undef, timer => undef, }; _fetchMetadataFromAPI($client); @@ -218,6 +219,7 @@ sub _startMetadataTimer { last_metadata_signature => undef, metadata_request_token => undef, metadata_request_seq => 0, + pending_metadata_result => undef, timer => undef, }; @@ -253,6 +255,7 @@ sub _onMetadataTimer { return unless $client; my $clientId = $client->id(); + my $state = $playerStates{$clientId}; # Verify client is still playing my $isPlaying = $client->isPlaying(); @@ -261,6 +264,21 @@ sub _onMetadataTimer { _stopMetadataTimer($client); return; } + + if ($state && $state->{pending_metadata_result}) { + my $pending_result = delete $state->{pending_metadata_result}; + $log->debug("Applying cached next-track metadata for client $clientId"); + _updateClientMetadata($client, $pending_result); + + unless ($prefs->get('enable_metadata')) { + $log->debug("Metadata updates disabled by user preference, stopping timer"); + _stopMetadataTimer($client); + return; + } + + _scheduleNextMetadataUpdate($client, METADATA_UPDATE_INTERVAL); + return; + } # Fetch metadata update _fetchMetadataFromAPI($client); @@ -331,11 +349,17 @@ sub _fetchMetadataFromAPI { if ($result) { _updateClientMetadata($client, $result); + delete $current_state->{pending_metadata_result}; if ( _isValidDelay($result->{next_update_delay}) + && $result->{next_metadata} ) { $next_delay = $result->{next_update_delay}; + $current_state->{pending_metadata_result} = { + metadata => $result->{next_metadata}, + is_fresh => 1, + }; } } From 49273db5b864ba1b4f80b57101335c0496df068c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:51:19 +0000 Subject: [PATCH 24/25] Respect metadata preference before applying cached metadata Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/d75b7846-4258-4cad-bc14-390a93faf08f Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index 0c2011a..ab1ff9e 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -265,17 +265,17 @@ sub _onMetadataTimer { return; } + unless ($prefs->get('enable_metadata')) { + $log->debug("Metadata updates disabled by user preference, stopping timer"); + _stopMetadataTimer($client); + return; + } + if ($state && $state->{pending_metadata_result}) { my $pending_result = delete $state->{pending_metadata_result}; $log->debug("Applying cached next-track metadata for client $clientId"); _updateClientMetadata($client, $pending_result); - unless ($prefs->get('enable_metadata')) { - $log->debug("Metadata updates disabled by user preference, stopping timer"); - _stopMetadataTimer($client); - return; - } - _scheduleNextMetadataUpdate($client, METADATA_UPDATE_INTERVAL); return; } @@ -284,12 +284,6 @@ sub _onMetadataTimer { _fetchMetadataFromAPI($client); # Let the meta data refresh one more time, to return player screens to channel artwork. - unless ($prefs->get('enable_metadata')) { - $log->debug("Metadata updates disabled by user preference, stopping timer"); - _stopMetadataTimer($client); - return; - } - } # Fetch metadata from xmplaylist.com API using APImetadata module From b51fd3bdebfb9ae733293a3322016b84b498cb08 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:51:51 +0000 Subject: [PATCH 25/25] Guard metadata timer callback when player state is missing Agent-Logs-Url: https://github.com/paul-1/plugin-SiriusXM/sessions/d75b7846-4258-4cad-bc14-390a93faf08f Co-authored-by: paul-1 <6473457+paul-1@users.noreply.github.com> --- Plugins/SiriusXM/ProtocolHandler.pm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Plugins/SiriusXM/ProtocolHandler.pm b/Plugins/SiriusXM/ProtocolHandler.pm index ab1ff9e..913aa6d 100644 --- a/Plugins/SiriusXM/ProtocolHandler.pm +++ b/Plugins/SiriusXM/ProtocolHandler.pm @@ -256,6 +256,12 @@ sub _onMetadataTimer { my $clientId = $client->id(); my $state = $playerStates{$clientId}; + + unless ($state) { + $log->debug("No metadata state for client $clientId, stopping timer"); + _stopMetadataTimer($client); + return; + } # Verify client is still playing my $isPlaying = $client->isPlaying();