Skip to content
Open
2 changes: 1 addition & 1 deletion debian/compat
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5
10
18 changes: 18 additions & 0 deletions gmusicbrowser_songs.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,24 @@ our %timespan_menu=
editsubmenu=>0,
category=>'extra',
},
# https://github.com/carl-di-ortus/gmusicbrowser/issues/3
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
# musicbrainz_trackid =>
# { name => _"MBID",
# id3v2 => 'TXXX;MusicBrainz Release Track Id;%v', vorbis => 'musicbrainz_releasetrackid', ape => 'MUSICBRAINZ_RELEASETRACKID', ilst => "----:com.apple.iTunes:MusicBrainz Release Track Id", 'id3v2.3' => 'TXXX;MusicBrainz Release Track Id;%v',
# flags => 'fgaescpi',
# type => 'string',
# category=>'extra',
# postread=> sub { my $v=shift; warn "V $v"; },
# },
# acoustid =>
# { name => _"AcoustID",
# id3v2 => 'TXXX;Acoustid Id;%v', vorbis => 'ACOUSTID_ID', ape => 'ACOUSTID_ID', ilst => "----:com.apple.iTunes:Acoustid Id",
# flags => 'fgaescpi',
# type => 'string',
# category=>'extra',
# postread=> sub { my $v=shift },
# },
style =>
{ name => _"Styles", width => 180, flags => 'fgaescpil',
type => 'flags',
Expand Down
257 changes: 257 additions & 0 deletions plugins/listenbrainz.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
# Copyright (C) 2024 Carl di Ortus <reklamukibiras@gmail.com>
#
# This file is part of Gmusicbrowser.
# Gmusicbrowser is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation

=for gmbplugin LISTENBRAINZ
name listenbrainz
title listenbrainz.org plugin
desc Submit played songs to listenbrainz
=cut


package GMB::Plugin::LISTENBRAINZ;
use strict;
use warnings;
use JSON;
use List::Util qw(max);
use constant
{ CLIENTID => 'gmb', VERSION => '0.1',
OPT => 'PLUGIN_LISTENBRAINZ_', #used to identify the plugin's options
SAVEFILE => 'listenbrainz.queue', #file used to save unsent data
};
require $::HTTP_module;

our $ignore_current_song;

my $self=bless {},__PACKAGE__;
my @ToSubmit; my @NowPlaying; my $NowPlayingID;
my $interval=10; my ($timeout,$waiting);
my ($Stop);
my $Log= Gtk3::ListStore->new('Glib::String');

sub Start
{ ::Watch($self,PlayingSong=> \&SongChanged);
::Watch($self,Played => \&Played);
$self->{on}=1;
Sleep();
SongChanged() if $::TogPlay;
$Stop=undef;
}
sub Stop
{
@NowPlaying=undef;
$waiting->abort if $waiting;
Glib::Source->remove($timeout) if $timeout;
$timeout=$waiting=undef;
::UnWatch($self,$_) for qw/PlayingSong Played/;
$self->{on}=undef;
$interval=10;
}

sub prefbox
{ my $vbox= Gtk3::VBox->new(::FALSE, 2);
my $sg1= Gtk3::SizeGroup->new('horizontal');
my $sg2= Gtk3::SizeGroup->new('horizontal');
my $entry2=::NewPrefEntry(OPT.'TOKEN',_"Token :", cb => \&Stop, sizeg1 => $sg1,sizeg2=>$sg2, hide => 1);
my $label2= Gtk3::Button->new(_"(see https://listenbrainz.org)");
$label2->set_relief('none');
$label2->signal_connect(clicked => sub
{ my $url='https://listenbrainz.org';
my $user=$::Options{OPT.'USER'};
$url.="/user/$user/" if defined $user && $user ne '';
::openurl($url);
});
my $ignore= Gtk3::CheckButton->new(_"Don't submit current song");
$ignore->signal_connect(toggled=>sub { return if $_[0]->{busy}; $ignore_current_song= $_[0]->get_active ? $::SongID : undef; ::HasChanged('Listenbrainz_ignore_current'); });
::Watch($ignore,Listenbrainz_ignore_current => sub { $_[0]->{busy}=1; $_[0]->set_active(defined $ignore_current_song); delete $_[0]->{busy}; } );
my $queue= Gtk3::Label->new;
my $sendnow= Gtk3::Button->new(_"Send now");
$sendnow->signal_connect(clicked=> \&SendNow);
my $qbox= ::Hpack($queue,$sendnow);
$vbox->pack_start($_,::FALSE,::FALSE,0) for $label2,$entry2,$ignore,$qbox;
$vbox->add( ::LogView($Log) );
$qbox->{label}=$queue;
$qbox->{button}=$sendnow;
$qbox->show_all;
update_queue_label($qbox);
$qbox->set_no_show_all(1);
::Watch($qbox,Listenbrainz_state_change=>\&update_queue_label);
return $vbox;
}
sub update_queue_label
{ my $qbox=shift;
my $label= $qbox->{label};
if (@ToSubmit && (!$waiting && (!$timeout || $interval>10)))
{ $label->set_text(::__n("song waiting to be sent"));
$label->get_parent->show;
$qbox->{button}->set_sensitive(!$waiting);
}
else { $label->get_parent->hide }
}

sub SongChanged
{
@NowPlaying=undef;
if (defined $ignore_current_song)
{ return if defined $::SongID && $::SongID == $ignore_current_song;
$ignore_current_song=undef; ::HasChanged('Listenbrainz_ignore_current');
}
my ($title,$artist,$album)= Songs::Get($::SongID,qw/title artist album/);
return if $title eq '' || $artist eq '';
@NowPlaying= ( $artist, $title, $album );
$NowPlayingID=$::SongID;
$interval=10;
SendNow();
}

sub Played
{ my (undef,$ID,undef,$start_time,$seconds,$coverage)=@_;
return if $ignore_current_song;
return unless $seconds>10;
my $length= Songs::Get($ID,'length');
if ($length>=30 && ($seconds >= 240 || $coverage >= .5) )
{ my ($title,$artist,$album)= Songs::Get($ID,qw/title artist album/);
return if $title eq '' || $artist eq '';
@ToSubmit= ( $artist, $title, $album );
$interval=10;
Sleep();
::QHasChanged('Listenbrainz_state_change');
}
}

sub Submit
{
my $i=0;
my $url= 'https://api.listenbrainz.org/1/submit-listens';
my $listen_type;
my $listened_at;
my @payload;
if (@ToSubmit)
{ @payload= @ToSubmit;
$listen_type= "single";
$listened_at= time();
}
elsif (@NowPlaying)
{ if (!defined $::PlayingID || $::PlayingID!=$NowPlayingID) { @NowPlaying=undef; return }
@payload= @NowPlaying;
$listen_type= "playing_now";
$listened_at= undef;
}
else { return; }
my $post= {
listen_type => $listen_type,
payload => [
{
#listened_at => $listened_at,
track_metadata => {
artist_name => $payload[0],
track_name => $payload[1]
#release_name => $payload[2]
}
}
]
};
$post->{payload}[0]->{listened_at} = $listened_at if $listened_at;
$post->{payload}[0]->{track_metadata}->{release_name} = $payload[2] if $payload[2];
my $response_cb=sub
{
my ($response,@lines)=@_;
my $error;
if (!defined $response) {$error=_"connection failed";}
elsif ($response eq '{"status":"ok"}')
{ unlink $::HomeDir.SAVEFILE;
if (@ToSubmit) {
Log( _("Submit OK ") .
::__x( _"{song} by {artist}", song=> $payload[1], artist => $payload[0]) );
undef @ToSubmit;
undef $waiting;
$interval=10;
return
};
if (@NowPlaying) {
Log( _("NowPlaying OK ") .
::__x( _"{song} by {artist}", song=> $payload[1], artist => $payload[0]) );
$interval=60;
undef $waiting;
return
};
}
elsif ($response eq 'BADSESSION')
{ $error=_"Bad session";
}
elsif ($response=~m/^FAILED (.*)$/)
{ $error=$1;
}
else {$error=_"unknown error";}

if (defined $error)
{ Log(_("Submit failed : ").$error);
Log(_("Response : ").$response) if $response;
$interval*=2;
$interval=max($interval, 300);
}
};

my $authtoken=$::Options{OPT.'TOKEN'};
Save($post);
Send($response_cb,$url,$::HomeDir.SAVEFILE,$authtoken);
}

sub SendNow
{ $interval=10;
$Stop=undef;
Awake();
}

sub Sleep
{ return unless $self->{on};
::QHasChanged('Listenbrainz_state_change');
return if $Stop || $waiting || $timeout;
$timeout=Glib::Timeout->add(1000*$interval,\&Awake) if @ToSubmit || @NowPlaying;
}

sub Awake
{ Glib::Source->remove($timeout) if $timeout;
$timeout=undef;
return 0 if !$self->{on} || $waiting;
Submit();
Sleep();
return 0;
}

sub Send
{ my ($response_cb,$url,$post,$authtoken)=@_;
my $cb=sub
{ my @response=(defined $_[0])? split "\012",$_[0] : ();
$waiting=undef;
&$response_cb(@response);
Sleep();
};

$waiting=Simple_http::get_with_cb(cb => $cb,url => $url,post => $post,authtoken => $authtoken);
::QHasChanged('Listenbrainz_state_change');
}

sub Log
{ my $text=$_[0];
$Log->set( $Log->prepend,0, localtime().' '.$text );
warn "$text\n" if $::debug;
if (my $iter=$Log->iter_nth_child(undef,50)) { $Log->remove($iter); }
}

sub Save
{ my $savebody=$_[0];
unless ($savebody)
{ unlink $::HomeDir.SAVEFILE; return }
my $fh;
unless (open $fh,'>:utf8',$::HomeDir.SAVEFILE)
{ warn "Error creating '$::HomeDir".SAVEFILE."' : $!\nUnsent listenbrainz.org data will be lost.\n"; return; }
my $json=(to_json $savebody);
print $fh $json;
close $fh;
}

1;
9 changes: 7 additions & 2 deletions simple_http.pm
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ sub get_with_cb
my %params=@_;
$self->{params}=\%params;
delete $params{cache} unless $UseCache;
my ($callback,$url,$post)=@params{qw/cb url post/};
my ($callback,$url,$post,$authtoken)=@params{qw/cb url post authtoken/};
if (my $cached= $params{cache} && GMB::Cache::get($url))
{ warn "cached result\n" if $::debug;
Glib::Timeout->add(10,sub { $callback->( ${$cached->{data}}, type=>$cached->{type}, filename=>$cached->{filename}, ); 0});
Expand Down Expand Up @@ -109,11 +109,16 @@ sub connecting_cb
print $socket "Host: $host:$port".EOL;
print $socket "User-Agent: $useragent".EOL;
print $socket "Referer: $params->{referer}".EOL if $params->{referer};
print $socket "Authorization: Token $params->{authtoken}".EOL if $params->{authtoken};
print $socket "Accept: $accept".EOL;
print $socket "Accept-Encoding: gzip".EOL if $gzip_ok;
#print $socket "Connection: Keep-Alive".EOL;
if (defined $post)
{ print $socket 'Content-Type: application/x-www-form-urlencoded; charset=utf-8'.EOL;
{ if ($params->{authtoken}) {
print $socket 'Content-Type: application/json'.EOL;
} else {
print $socket 'Content-Type: application/x-www-form-urlencoded; charset=utf-8'.EOL;
}
print $socket "Content-Length: ".length($post).EOL.EOL;
print $socket $post.EOL;
}
Expand Down
21 changes: 12 additions & 9 deletions simple_http_wget.pm
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ sub get_with_cb
{ my $self=bless {};
my %params=@_;
$self->{params}=\%params;
my ($callback,$url,$post)=@params{qw/cb url post/};
my ($callback,$url,$post,$authtoken)=@params{qw/cb url post authtoken/};
delete $params{cache} unless $UseCache;
if (my $cached= $params{cache} && GMB::Cache::get($url))
{ warn "cached result\n" if $::debug;
Expand All @@ -36,13 +36,16 @@ sub get_with_cb
: $orig_proxy;
$ENV{http_proxy}=$proxy;

my $useragent= $params{user_agent} || 'Mozilla/5.0';
my $accept= $params{'accept'} || '';
my $gzip= $gzip_ok ? '--header=Accept-Encoding: gzip' : '';
my @cmd_and_args= (qw/wget --timeout=40 -S -O -/, $gzip, "--header=Accept: $accept", "--user-agent=$useragent");
push @cmd_and_args, "--referer=$params{referer}" if $params{referer};
push @cmd_and_args, '--post-data='.$post if $post; #FIXME not sure if I should escape something
push @cmd_and_args, '--',$url;
my $cmd_and_args= 'wget --timeout=40 -S -O -';
$cmd_and_args.= " -U ".($params{user_agent} || "'Mozilla/5.0'");
$cmd_and_args.= " --header='Accept-Encoding: gzip'" if $gzip_ok;
$cmd_and_args.= " --header='Authorization: Token ".$authtoken."'" if $authtoken;
$cmd_and_args.= " --header='Content-Type: application/json'" if $authtoken;
$cmd_and_args.= " --referer=$params{referer}" if $params{referer};
$cmd_and_args.= " --post-file='".$post."'" if $post;
$cmd_and_args.= " -- '$url'";
#warn "$cmd_and_args\n";

pipe my($content_fh),my$wfh;
pipe my($error_fh),my$ewfh;
my $pid=fork;
Expand All @@ -52,7 +55,7 @@ sub get_with_cb
open my($olderr), ">&", \*STDERR;
open \*STDOUT,'>&='.fileno $wfh;
open \*STDERR,'>&='.fileno $ewfh;
exec @cmd_and_args or print $olderr "launch failed (@cmd_and_args) : $!\n";
exec $cmd_and_args or print $olderr "launch failed ($cmd_and_args) : $!\n";
POSIX::_exit(1);
}
close $wfh; close $ewfh;
Expand Down