I have blogged about trying to combine my love of music and programming before.

Today, I have better results.

Not perfect, but better.

I have a query that gives me exactly what I want.

SELECT  t.artist    artist 
    ,   t.song      song 
    ,   t.album     album 
    ,   COUNT(p.play) plays

FROM last_fm_plays p 

LEFT JOIN last_fm_tracks t 
ON p.id = t.id

GROUP BY p.id
ORDER BY COUNT(p.play)
DESC 
LIMIT 10

You will have to trust that my tables are sound and this works, because it gives me this:

artist	song	album	plays
Happy Mondays	Wrote For Luck	Double Double Good: The Best of The Happy Mondays	200
Happy Mondays	Step On	Double Double Good: The Best of The Happy Mondays	200
Pharrell Williams	Happy	Despicable Me 2 (Original Motion Picture Soundtrack)	126
Pantera	Walk	Vulgar Display of Power	101
Radiohead	Optimistic	Kid A	94
Mark Ronson	Uptown Funk	Uptown Special	91
Kings Of Groove feat. Michelle Weeks	You Have a Purpose - Original Mix	Auténtico Baleárica (Blank & Jones present)	89
Daft Punk	Harder Better Faster Stronger	Harder Better Faster Stronger	89
Neu!	Hallogallo	Neu!	89
Jonathan Coulton	I Feel Fantastic	JoCo Looks Back	85

(I could go longer, but why?)

So, I can assert that I listen to Groove Metal, Acid House and Krautrock a lot. I listen to a lot of Happy Mondays because I want to assert that I’m not Garfield, hating Mondays and being a grump. The top tracks on this list were part of my Monday Morning Motivation Mix.

(Except “Hallogallo”. For that, there was just a day where it got me into Flow State. It works.)

What I can’t do is put it all into a playlist and let y’all know what kind of rocker I am.

Big problem I see is that many Perl modules are written with an OAuth 1.0 state of mind, while the cool kids are all on OAuth 2.0. I tried to get it going with WebService::Spotify, but I saw no mechanism to handle tokens.

So, I went back to other OAuth-related code I wrote and modified it. I can do that.

package Spotify ;
use strict ;
use warnings ;
use feature qw{ postderef say signatures state } ;
no warnings qw{ experimental::postderef experimental::signatures } ;

use Carp ;
use JSON::XS ;
use LWP::UserAgent ;
use LWP::Protocol::https ;
use Mojo::UserAgent ;

our $VERSION = '0.0.1' ;

sub new ( $class, $token ) {
    croak 'No Token' unless defined $token ;
    my $self = {} ;
    bless $self, $class ;
    $self->{ token } = $token ;
    $self->{ json }  = JSON::XS->new->pretty->canonical ;
    $self->{ ua }    = Mojo::UserAgent->new ;
    $self->{ host }  = 'https://api.spotify.com' ;
    return $self ;
    }

sub add_to_playlist ( $self, $user_id, $playlist, $uris, $position = 0 ) {
    my $url = join '/', $self->{ host }, 'v1', 'users', $user_id, 'playlists', $playlist, 'tracks' ;
    my @querystring ;
    push @querystring, qq{uris=$uris} ;
    push @querystring, qq{position=$position} if $position > 0 ;
    $url = $url . '?' . ( join '&', @querystring ) ;
    my $res = $self->{ ua }->post(
        $url,
        {
            'Authorization' => "Bearer $self->{token}",
            'Accept-Type'   => 'application/json',
            'Content-Type'  => 'application/json',
            }
        )->result ;
    say $res->{ code } ; # You will notice this in the output
    if ( $res->is_success ) {
        my $obj = $self->{ json }->decode( $res->body ) ;
        return $obj ;
        }
    elsif ( $res->is_error ) { croak $res->message }
    else                     { croak $res->message }
    return 1 ;
    }

sub search_track ( $self, $q, $limit = 0, $offset = 0, $market = 'US' ) {
    my @response ;
    $q =~ s/\W/%20/g ;
    my $type = "track" ;
    my $url = join '/', $self->{ host }, 'v1', 'search' ;
    my @querystring ;
    push @querystring, qq{q=$q} ;
    push @querystring, qq{type=$type} ;
    push @querystring, qq{market=$market} ;
    push @querystring, qq{limit=$limit} if $limit > 0 ;
    push @querystring, qq{offset=$offset} if $offset > 0 ;
    $url = $url . '?' . ( join '&', @querystring ) ;
    my $res = $self->{ ua }->get(
        $url => {
            'Authorization' => "Bearer $self->{token}",
            'Accept-Type'   => 'application/json',
            'Content-Type'  => 'application/json',
            }
            )->result ;

    if ( $res->is_success ) {
        my $obj = $self->{ json }->decode( $res->body ) ;
        @response = $obj->{ tracks }{ items }->@* ;
        return wantarray ? @response : \@response ;
        }
    elsif ( $res->is_error ) { croak $res->message }
    else                     { croak $res->message }
    return wantarray ? @response : \@response ;
    }

1 ;

This is minimal. There is a fair amount here that I want to rethink and clarify and write tests for and all that. I’ll hat-tip Mojolicious, because with Mojo::UserAgent, I didn’t have to make GET and POST functions separately, I can just include a Mojo object in my Spotify object. And no, I am not 100% sure I’m doing this right.

On to the program code:

#!/usr/bin/env perl

use strict ;
use warnings ;
use utf8 ;
use feature qw{ postderef say signatures state } ;
no warnings qw{ experimental::postderef experimental::signatures } ;

use JSON ;
use Mojo ;

use lib '/home/jacoby/lib' ;
use oDB ;
use Spotify ;

my $json    = JSON->new->pretty->canonical ;
my $user    = 'jacobydave' ;
my $token   = 'YOU DO NOT GET MY TOKENS, BUT THE ONES I HAVE TIME OUT TOO QUICKLY' ;
my $spotify = Spotify->new( $token ) ;

my $db       = oDB->new( 'oz' ) ;
my $q        = <<'SQL' ;
        SELECT  t.artist    artist 
            ,   t.song      song 
            ,   t.album     album 
            ,   COUNT(p.play) plays

        FROM last_fm_plays p 

        LEFT JOIN last_fm_tracks t 
        ON p.id = t.id

        GROUP BY p.id
        ORDER BY COUNT(p.play)
        DESC 
        LIMIT 100
SQL
my $r = $db->arrayref( $q, { controls => {} } ) ;

# Normally, I'd avoid hard-coding this, but it's part of the playlist
# link below
my $playlist = '68jq4AvcZg2Rrkwbm5mvNF' ;
for my $s ( $r->@* ) {

    # this formatting mess is to try to get things like 
    # "You Have A Purpose - Original Mix" by Kings of Groove featuring Michelle Weeks 
    # found by my code. Fails so far - track is
    #   https://open.spotify.com/track/7aLFq7oGcPZCnVziW22vRn 

    my $song = $s->{ song } ;
    ( $song ) = split m{-}, $song ;
    my $artist = $s->{ artist } ;
    ( $artist ) = split m{feat}, $artist ;
    my $q = join ' ', $song, $artist ;

    my @tracks = $spotify->search_track( $q, 50 ) ;
    my $c      = 1 ; # counter; how many search resuld did we go through?
    my $f      = 1 ; # flag; if flag is down, that's the one we play
    my $d      = 0 ; # done; if not done, I will have to add it by hand
    for my $t ( @tracks ) {
        next unless $f == 1 ;
        # lower-case because "Vulgar Display Of Power" is not "Vulgar Display of Power"
        $f = 0 if lc $t->{ album }{ name } eq lc $s->{ album } ;
        next unless $f == 0 ;
        my $track_uri = $t->{ uri } ;
        say join "\t", '+', $c, $f, $user, $playlist, $track_uri ;
        my $r = $spotify->add_to_playlist( $user, $playlist, $track_uri ) ;
        $d = 1 ;
        }
    say qq{ BY HAND: $q } if $d == 0 ;
    sleep 3 ;
    }

I may have to explain the oz thing eventually.

So we get this:

...
	artist	Kings Of Groove feat. Michelle Weeks
	song	You Have a Purpose - Original Mix
	album	Auténtico Baleárica (Blank & Jones present)
 BY HAND: You Have a Purpose  Kings Of Groove  
	artist	Daft Punk
	song	Harder Better Faster Stronger
	album	Harder Better Faster Stronger
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:2cJz1loJp5EZM6shmQpLZN
201
	artist	Neu!
	song	Hallogallo
	album	Neu!
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:1GkZZHT9uJjdzrrksrpczR
201
	artist	Jonathan Coulton
	song	I Feel Fantastic
	album	JoCo Looks Back
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:0iq3MFEbuKTWJgdhwdOwXI
201
...
	artist	Steel Pulse
	song	Chant A Psalm
	album	True Democracy (US Release)
 BY HAND: Chant A Psalm Steel Pulse 
	artist	Arctic Monkeys
	song	I Bet You Look Good on the Dancefloor
	album	Whatever People Say I Am That's What I'm Not
 BY HAND: I Bet You Look Good on the Dancefloor Arctic Monkeys 
	artist	U2
	song	Beautiful Day
	album	All That You Can't Leave Behind
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:1VuBmEauSZywQVtqbxNqka
201
	artist	The Shamen
	song	Move Any Mountain (Beat Edit)
	album	Collection
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:6l26mQLF7A4lu9djfIez7O
201
	artist	Ned's Atomic Dustbin
	song	Grey Cell Green
	album	God Fodder
 BY HAND: Grey Cell Green Ned's Atomic Dustbin 
	artist	Dinosaur Jr.
	song	Thumb
	album	Green Mind [Digital Version] [with Bonus Tracks]
 BY HAND: Thumb Dinosaur Jr. 
	artist	Pegboy
	song	Strong Reaction
	album	Strong Reaction
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:1HZXGK0yyl1ygZn0wsdVoL
201
	artist	Happy Mondays
	song	24 Hour Party People
	album	Double Double Good: The Best of The Happy Mondays
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:6g4MtYaLFIyTQmOIiMY2oV
201
	artist	U2
	song	Beautiful Day
	album	All That You Can't Leave Behind (Non EU Version)
 BY HAND: Beautiful Day U2 
	artist	The Beach Boys
	song	Good Vibrations
	album	Smiley Smile (2001 - Remaster)
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:5t9KYe0Fhd5cW6UYT4qP8f
201
	artist	Rubblebucket
	song	If U C My Enemies
	album	If U C My Enemies
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:0NyzJxVxHhu2mDgdNv1ZP2
201
...
	artist	Arctic Monkeys
	song	Perhaps Vampires Is a Bit Strong But…
	album	Whatever People Say I Am That's What I'm Not
 BY HAND: Perhaps Vampires Is a Bit Strong But… Arctic Monkeys 
	artist	Orbital
	song	Halcyon [Tom Middleton Re-Model]
	album	Orbital 20
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:64GYkWWxjK38M2l0pSdfx1
201
	artist	Dinosaur Jr.
	song	The Wagon
	album	Green Mind [Digital Version] [with Bonus Tracks]
 BY HAND: The Wagon Dinosaur Jr. 
	artist	Dinosaur Jr.
	song	Green Mind
	album	Green Mind [Digital Version] [with Bonus Tracks]
 BY HAND: Green Mind Dinosaur Jr. 
	artist	Rob Base & DJ EZ Rock
	song	It Takes Two
	album	Profilin': The Hits
+	1	0	jacobydave	68jq4AvcZg2Rrkwbm5mvNF	spotify:track:3Yxmpx64AdWAzG3qAD4Dty
201
	artist	Dinosaur Jr.
	song	Flying Cloud
	album	Green Mind [Digital Version] [with Bonus Tracks]
 BY HAND: Flying Cloud Dinosaur Jr. 
	artist	Dinosaur Jr.
	song	I Live For That Look
	album	Green Mind [Digital Version] [with Bonus Tracks]
 BY HAND: I Live For That Look Dinosaur Jr. 
	artist	Dinosaur Jr.
	song	How'd You Pin That One On Me
	album	Green Mind [Digital Version] [with Bonus Tracks]
 BY HAND: How'd You Pin That One On Me Dinosaur Jr. 
...

Clearly, what I have doesn’t like Dinosaur Jr., probably because of [Digital Version] [with Bonus Tracks]. I treat Green Mind as just one very long song, and always have. Similar problem with U2’s “Beautiful Day” and Santa Esmeralda’s “Don’t Let Me Be Misunderstood”. I cannot explain the Ned’s Atomic Dustbin tracks, or the Atomic Monkeys. I think that I’m doing the wrong thing with HTML character escaping.

And I am grabbing the token from the Spotify beta developer’s console, and they time out. I need to figure out how to get long-term tokens.

Anyway, I have a 3/4 success rate, a little bit of closure, and a groovy playlist for your listening pleasure. If you are also a Spotify user, make your Discover Weekly playlist public and send me a link: I’m always wanting do diversify my listening.

Top 100 For 2017

If you have any questions or comments, I would be glad to hear it. Ask me on Twitter or make an issue on my blog repo.