Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
69 changes: 69 additions & 0 deletions Plugins/SiriusXM/APImetadata.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use JSON::XS;
use Date::Parse;
use Time::HiRes;

use Plugins::SiriusXM::TrackDurationDB;
use Plugins::SiriusXM::MusicBrainzAPI;

my $log = logger('plugin.siriusxm');
my $prefs = preferences('plugin.siriusxm');

Expand Down Expand Up @@ -116,6 +119,7 @@ sub _processResponse {
my $track_info = $latest_track->{track};
my $spotify_info = $latest_track->{spotify};
my $timestamp = $latest_track->{timestamp};
my $xmplaylist_id = $latest_track->{id};

return unless $track_info;

Expand Down Expand Up @@ -151,6 +155,21 @@ sub _processResponse {
}
}

# Handle track duration processing if we're using xmplaylists metadata
if ($use_xmplaylists_metadata && $track_info->{title} && $track_info->{artists}) {
my $title = $track_info->{title};
my $artist = ref($track_info->{artists}) eq 'ARRAY' ?
join(', ', @{$track_info->{artists}}) :
$track_info->{artists};

# Start async duration lookup and continue with metadata building
$class->_lookupTrackDuration($xmplaylist_id, $title, $artist, sub {
my ($duration) = @_;
# Duration will be stored in database, but we don't wait for it
# to complete the current metadata response
});
}

# Build new metadata
my $new_meta = {};

Expand Down Expand Up @@ -180,6 +199,22 @@ sub _processResponse {
$new_meta->{album} = $channel_info->{name} || 'SiriusXM';
$new_meta->{bitrate} = '';

# Add duration if available from database
if ($xmplaylist_id) {
my $duration = Plugins::SiriusXM::TrackDurationDB->getDuration($xmplaylist_id);
if (defined $duration && $duration > 0) {
$new_meta->{duration} = $duration;
$new_meta->{secs} = $duration;
$log->debug("Added cached duration to metadata: ${duration}s");
}
}

# Add track start time info for timing calculations
if ($timestamp) {
$new_meta->{track_timestamp} = $timestamp;
$new_meta->{xmplaylist_id} = $xmplaylist_id if $xmplaylist_id;
}

} else {
# Fall back to basic channel info when metadata is too old or disabled
if ($channel_info) {
Expand All @@ -203,4 +238,38 @@ sub _processResponse {
}
}

# Lookup track duration with database cache and MusicBrainz fallback
sub _lookupTrackDuration {
my ($class, $xmplaylist_id, $title, $artist, $callback) = @_;

return unless $xmplaylist_id && $title && $artist;

# First check database cache
my $cached_duration = Plugins::SiriusXM::TrackDurationDB->getDuration($xmplaylist_id);
if (defined $cached_duration) {
$callback->($cached_duration) if $callback;
return;
}

# Fallback to MusicBrainz API search
$log->debug("Looking up duration for: $title by $artist");

Plugins::SiriusXM::MusicBrainzAPI->searchTrackDuration($title, $artist, sub {
my ($duration, $score) = @_;

if (defined $duration && $score) {
# Store in database for future use
Plugins::SiriusXM::TrackDurationDB->storeDuration(
$xmplaylist_id, $title, $artist, $duration, $score
);

$log->info("Found and cached duration for '$title' by '$artist': ${duration}s (score: $score%)");
$callback->($duration) if $callback;
} else {
$log->debug("No suitable duration found for: $title by $artist");
$callback->() if $callback;
}
});
}

1;
136 changes: 136 additions & 0 deletions Plugins/SiriusXM/MusicBrainzAPI.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package Plugins::SiriusXM::MusicBrainzAPI;

use strict;
use warnings;

use Slim::Utils::Log;
use Slim::Networking::SimpleAsyncHTTP;
use JSON::XS;
use URI::Escape;

my $log = logger('plugin.siriusxm.musicbrainz');

# MusicBrainz API endpoint
use constant MUSICBRAINZ_URL => 'https://musicbrainz.org/ws/2/recording';

# Minimum score threshold (75%)
use constant MIN_SCORE_THRESHOLD => 75;

# Search for track duration using MusicBrainz API
sub searchTrackDuration {
my ($class, $title, $artist, $callback) = @_;

return unless $title && $artist && $callback;

# Clean up search terms
$title = _cleanSearchTerm($title);
$artist = _cleanSearchTerm($artist);

# Build search query
my $query = uri_escape("recording:\"$title\" AND artist:\"$artist\"");
my $url = MUSICBRAINZ_URL . "?query=$query&limit=10&fmt=json";

$log->debug("Searching MusicBrainz for: $title by $artist");

my $http = Slim::Networking::SimpleAsyncHTTP->new(
sub {
my $response = shift;
$class->_processResponse($title, $artist, $response, $callback);
},
sub {
my ($http, $error) = @_;
$log->warn("MusicBrainz API error: $error");
$callback->();
},
{
timeout => 20,
# User-Agent required by MusicBrainz
'User-Agent' => 'LMS-SiriusXM-Plugin/1.0 (https://github.com/paul-1/plugin-SiriusXM)',
}
);

$http->get($url);
}

# Process MusicBrainz API response
sub _processResponse {
my ($class, $title, $artist, $response, $callback) = @_;

my $content = $response->content;
my $data;

eval {
$data = decode_json($content);
};

if ($@) {
$log->warn("Failed to parse MusicBrainz response: $@");
$callback->();
return;
}

my $recordings = $data->{recordings};
unless ($recordings && @$recordings) {
$log->debug("No recordings found for: $title by $artist");
$callback->();
return;
}

# Find the best match based on score
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we get quite a few tracks that have a score of 100, wo we need to also rank them based on the oldest "first-release-date" field.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enhanced ranking logic to consider release dates when scores are equal. When multiple tracks have score 100, now selects the one with the oldest "first-release-date" field. Added _getEarliestReleaseDate() helper function to extract and normalize release dates from MusicBrainz recordings. (f2763bd)

my $best_match;
my $best_score = 0;

foreach my $recording (@$recordings) {
my $score = $recording->{score} || 0;

# Skip if below threshold
next if $score < MIN_SCORE_THRESHOLD;

# Check if this recording has a duration
my $length = $recording->{length};
next unless defined $length;

# Convert milliseconds to seconds
my $duration_seconds = int($length / 1000);
next if $duration_seconds <= 0;

# Update best match if score is higher
if ($score > $best_score) {
$best_score = $score;
$best_match = {
duration => $duration_seconds,
score => $score,
title => $recording->{title},
length_ms => $length,
};
}
}

if ($best_match) {
$log->info("Found MusicBrainz match for '$title' by '$artist': " .
$best_match->{duration} . "s (score: $best_score%)");

$callback->($best_match->{duration}, $best_score);
} else {
$log->debug("No suitable MusicBrainz matches found for: $title by $artist (threshold: " . MIN_SCORE_THRESHOLD . "%)");
$callback->();
}
}

# Clean search terms for better matching
sub _cleanSearchTerm {
my $term = shift || '';

# Remove common noise
$term =~ s/\s*\(.*?\)\s*//g; # Remove parentheses content
$term =~ s/\s*\[.*?\]\s*//g; # Remove brackets content
$term =~ s/\s*feat\.?\s+.*$//i; # Remove featuring
$term =~ s/\s*ft\.?\s+.*$//i; # Remove ft.
$term =~ s/\s*with\s+.*$//i; # Remove with
$term =~ s/^\s+|\s+$//g; # Trim whitespace
$term =~ s/\s+/ /g; # Normalize whitespace

return $term;
}

1;
7 changes: 7 additions & 0 deletions Plugins/SiriusXM/Plugin.pm
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use Proc::Background;
use Plugins::SiriusXM::API;
use Plugins::SiriusXM::Settings;
use Plugins::SiriusXM::ProtocolHandler;
use Plugins::SiriusXM::TrackDurationDB;

my $prefs = preferences('plugin.siriusxm');
my $log = Slim::Utils::Log->addLogCategory({
Expand Down Expand Up @@ -60,6 +61,9 @@ sub initPlugin {
# Initialize the API module
Plugins::SiriusXM::API->init();

# Initialize track duration database
Plugins::SiriusXM::TrackDurationDB->init();

# Add to music services menu
Slim::Menu::TrackInfo->registerInfoProvider( siriusxm => (
parent => 'moreinfo',
Expand Down Expand Up @@ -94,6 +98,9 @@ sub shutdownPlugin {
# Clean up API connections
Plugins::SiriusXM::API->cleanup();

# Shutdown track duration database
Plugins::SiriusXM::TrackDurationDB->shutdown();

}

sub validateHLSSupport {
Expand Down
52 changes: 49 additions & 3 deletions Plugins/SiriusXM/ProtocolHandler.pm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use Date::Parse;

use Plugins::SiriusXM::API;
use Plugins::SiriusXM::APImetadata;
use Plugins::SiriusXM::TrackDurationDB;

my $log = logger('plugin.siriusxm');
my $prefs = preferences('plugin.siriusxm');
Expand Down Expand Up @@ -478,9 +479,29 @@ sub getMetadataFor {
$meta->{album} = $xmplaylist_meta->{album} if $xmplaylist_meta->{album};
$meta->{bitrate} = '';

# Really noisy log message when using a LMS web.
# $log->debug("Using xmplaylist metadata: " . ($meta->{title} || 'Unknown') .
# " by " . ($meta->{artist} || 'Unknown Artist'));
# Add duration if available
if (defined $xmplaylist_meta->{duration}) {
$meta->{duration} = $xmplaylist_meta->{duration};
$meta->{secs} = $xmplaylist_meta->{duration};

# Set track duration for LMS
Slim::Music::Info::setDuration($song->track, $meta->{duration}) if $song;
}

# Handle track timing if we have timestamp information
if ($xmplaylist_meta->{track_timestamp}) {
my $elapsed = $class->_calculateTrackElapsed($xmplaylist_meta->{track_timestamp});
if (defined $elapsed && $elapsed >= 0 && defined $meta->{duration}) {
# Store elapsed time in metadata for potential use by clients
# Don't force current position as this might interfere with LMS streaming
$meta->{track_elapsed} = $elapsed;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not valid metadata, to set the track start and elapsed time, we need to use getBlockData and setBlockData routines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced invalid metadata approach with proper LMS block data system. Now uses song->getBlockData() and song->setBlockData() to store track timing information instead of setting track_elapsed in metadata. (f2763bd)

$log->debug("Track elapsed time: ${elapsed}s of " . $meta->{duration} . "s");
}
}

# Really noisy log message when using a LMS web.
# $log->debug("Using xmplaylist metadata: " . ($meta->{title} || 'Unknown') .
# " by " . ($meta->{artist} || 'Unknown Artist'));
} elsif ($channel_info) {
# Fall back to basic channel info when metadata is enabled
$meta->{artist} = $channel_info->{name};
Expand Down Expand Up @@ -564,4 +585,29 @@ sub requestString {
return $class->SUPER::requestString($client, $url, $maxRedirects);
}

# Calculate elapsed time for a track based on its start timestamp
sub _calculateTrackElapsed {
my ($class, $timestamp) = @_;

return unless $timestamp;

eval {
# Parse the UTC timestamp format: 2025-08-09T15:57:41.586Z
my $track_start_time = str2time($timestamp);
return unless defined $track_start_time;

my $current_time = time();
my $elapsed = $current_time - $track_start_time;

# Only return positive elapsed times (track has started)
return $elapsed >= 0 ? $elapsed : 0;
};

if ($@) {
$log->warn("Failed to calculate track elapsed time from timestamp '$timestamp': $@");
}

return;
}

1;
Loading