cli.pl (2671B) [raw]
1 #!/usr/bin/env perl 2 use strict; 3 use warnings; 4 5 # utf8 shenanis -- files written and stdout both utf8 6 use utf8; 7 use open qw(:std :encoding(UTF-8)); 8 9 use LWP::UserAgent; 10 use JSON::PP; 11 use Data::Dumper; 12 13 if (!$ENV{SPOTIFY_TOKEN}) { 14 die "SPOTIFY_TOKEN not set in environment\n"; 15 } 16 17 if (!@ARGV) { 18 die "usage: $0 username\n"; 19 } 20 21 my $user = $ARGV[0]; 22 my $API = "https://api.spotify.com/v1"; 23 my $ua = LWP::UserAgent->new(); 24 $ua->default_header("Authorization" => "Bearer $ENV{SPOTIFY_TOKEN}"); 25 26 sub get_playlists { 27 my $limit = shift; 28 $limit //= 10000; 29 my @playlists; 30 my $url = "$API/users/$user/playlists"; 31 while (1) { 32 my $res = $ua->get($url); 33 if (!$res->is_success) { 34 print(Dumper($res)); 35 my $err = $res->decoded_content; 36 die "Failed to fetch playlists: $err"; 37 } 38 my $j = decode_json($res->decoded_content); 39 foreach my $item (@{$j->{items}}) { 40 push(@playlists, $item); 41 } 42 if (!$j->{next} || scalar @playlists > $limit) { 43 last; 44 } 45 print("Paging Playlists: $url\n"); 46 $url = $j->{next}; 47 } 48 return @playlists; 49 } 50 51 sub describe { 52 my $p = shift; 53 my $desc = $p->{description} ? "\"$p->{description}\"\n\n" : ""; 54 my $s = <<EOM; 55 # $p->{name} 56 57 ${desc}By $p->{owner}->{display_name} 58 59 EOM 60 my $url = "$API/playlists/$p->{id}?fields=tracks.next,tracks.items(added_at,track.name,track.artists(name))"; 61 if ($p->{id} eq "LIBRARY") { 62 $url = "$API/me/tracks"; 63 } 64 my $i = 1; 65 while (1) { 66 my $res = $ua->get($url); 67 if (!$res->is_success) { 68 print(Dumper($res)); 69 my $err = $res->decoded_content; 70 die "Failed to fetch playlist: $err"; 71 } 72 # NOTE: after paging, the shape changes.. 73 my $j = decode_json($res->decoded_content); 74 my $tracks = $j->{tracks} || $j; 75 my $n = scalar @{$tracks->{items}}; 76 print("$n Tracks\n"); 77 foreach my $t (@{$tracks->{items}}) { 78 (my $d = $t->{added_at}) =~ s/T.*Z$//; 79 my $fart = @{$t->{track}->{artists}}[0]->{name}; 80 $s .= sprintf( 81 "[%s] %-4d \"%s\" by %s\n", 82 $d, $i, $t->{track}->{name}, $fart, 83 ); 84 $i += 1; 85 } 86 if (!$tracks->{next}) { 87 last; 88 } 89 # HACK: the fields filter breaks in the next URL, 90 # so we have to strip the tracks. prefix 91 ($url = $tracks->{next}) =~ s/tracks\.//g; 92 } 93 return $s; 94 } 95 96 sub writep { 97 my $p = shift; 98 my $desc = describe($p); 99 (my $fname = $p->{name}) =~ s/\s/_/g; 100 $fname =~ s/\W//g; 101 open(my $fh, ">", "out/$fname.txt") or die $!; 102 print $fh $desc; 103 close $fh; 104 } 105 106 my %library = ( 107 id => "LIBRARY", 108 name => "My Saved Songs", 109 owner => { 110 display_name => $user 111 } 112 ); 113 114 mkdir "out" or die $! unless -e "out"; 115 116 my @playlists = get_playlists(); 117 foreach my $p (\%library, @playlists) { 118 print("Processing $p->{name}\n"); 119 writep($p); 120 } 121 122 123 exit 0;