-
Notifications
You must be signed in to change notification settings - Fork 1
Add track duration metadata system with timer-only timing for SiriusXM streams #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
b87d4c8
4326ced
97e1c13
f2763bd
a54575a
699446c
09d1d7c
3d14f91
8f7cde8
c98ee0e
e7f93f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'); | ||
|
|
@@ -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; | ||
|
||
| $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}; | ||
|
|
@@ -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; | ||
Uh oh!
There was an error while loading. Please reload this page.