File Coverage

blib/lib/WWW/Spotify.pm
Criterion Covered Total %
statement 32 276 11.5
branch 0 110 0.0
condition 0 33 0.0
subroutine 11 71 15.4
pod 50 57 87.7
total 93 547 17.0


line stmt bran cond sub pod time code
1             package WWW::Spotify;
2              
3 4     4   896053 use 5.012;
  4         36  
4 4     4   25 use strict;
  4         22  
  4         122  
5 4     4   21 use warnings;
  4         9  
  4         355  
6              
7 4     4   2795 use Moo 2.002004;
  4         81391  
  4         31  
8              
9             # roles will be composed later in the file (after attribute declarations)
10              
11             our $VERSION = '0.014';
12              
13 4     4   9830 use Data::Dumper qw( Dumper );
  4         40829  
  4         5494  
14 4     4   2629 use IO::CaptureOutput qw( capture );
  4         132325  
  4         293  
15 4     4   2172 use JSON::Path ();
  4         395375  
  4         151  
16              
17             # JSON::MaybeXS exports encode_json only when asked; we now need both
18 4     4   40 use JSON::MaybeXS qw( decode_json encode_json );
  4         8  
  4         246  
19 4     4   1675 use MIME::Base64 qw( encode_base64 );
  4         2698  
  4         337  
20 4     4   3037 use Types::Standard qw( Bool InstanceOf Int Str CodeRef );
  4         466865  
  4         62  
21 4     4   15695 use HTTP::Status qw( HTTP_OK HTTP_NO_CONTENT );
  4         16039  
  4         25268  
22              
23             has 'oauth_authorize_url' => (
24             is => 'rw',
25             isa => Str,
26             default => 'https://accounts.spotify.com/authorize'
27             );
28              
29             has 'oauth_token_url' => (
30             is => 'rw',
31             isa => Str,
32             default => 'https://accounts.spotify.com/api/token'
33             );
34              
35             has 'oauth_redirect_uri' => (
36             is => 'rw',
37             isa => Str,
38             default => 'http://www.spotify.com'
39             );
40              
41             has 'oauth_client_id' => (
42             is => 'rw',
43             isa => Str,
44             default => $ENV{SPOTIFY_CLIENT_ID} || q{}
45             );
46              
47             has 'oauth_client_secret' => (
48             is => 'rw',
49             isa => Str,
50             default => $ENV{SPOTIFY_CLIENT_SECRET} || q{}
51             );
52              
53             # keep for backwards compat: alias misspelled attribute name
54             # DEPRECATED: use current_oauth_code instead (fixed spelling)
55             # The original attribute was misspelled as "current_oath_code".
56             # It is retained here as a lazy delegate to the correctly spelled
57             # attribute so that existing user code continues to work without
58             # modification.
59              
60             has 'current_oauth_code' => (
61             is => 'rw',
62             isa => Str,
63             default => q{}
64             );
65              
66             # backward‑compat alias (read/write)
67              
68             # The misspelled accessor is retained as a thin wrapper so external
69             # code that might call it continues to work. It simply forwards to
70             # current_oauth_code.
71              
72             sub current_oath_code {
73 0     0 0   my $self = shift;
74 0           return $self->current_oauth_code(@_);
75             }
76              
77             has 'current_access_token' => (
78             is => 'rw',
79             isa => Str,
80             default => q{}
81             );
82              
83             has 'result_format' => (
84             is => 'rw',
85             isa => Str,
86             default => 'json'
87             );
88              
89             has 'grab_response_header' => (
90             is => 'rw',
91             isa => Int,
92             default => 0
93             );
94              
95             has 'results' => (
96             is => 'rw',
97             isa => Int,
98             default => '15'
99             );
100              
101             has 'debug' => (
102             is => 'rw',
103             isa => Bool,
104             default => 0
105             );
106              
107             has 'uri_scheme' => (
108             is => 'rw',
109             isa => Str,
110             default => 'https'
111             );
112              
113             has 'current_client_credentials' => (
114             is => 'rw',
115             isa => Str,
116             default => q{}
117             );
118              
119             has 'force_client_auth' => (
120             is => 'rw',
121             isa => Bool,
122             default => 1
123             );
124              
125             has 'uri_hostname' => (
126             is => 'rw',
127             isa => Str,
128             default => 'api.spotify.com'
129             );
130              
131             has 'uri_domain_path' => (
132             is => 'rw',
133             isa => Str,
134             default => 'api'
135             );
136              
137             has 'call_type' => (
138             is => 'rw',
139             isa => Str
140             );
141              
142             has 'auto_json_decode' => (
143             is => 'rw',
144             isa => Int,
145             default => 0
146             );
147              
148             has 'last_result' => (
149             is => 'rw',
150             isa => Str,
151             default => q{}
152             );
153              
154             has 'last_response' => (
155             is => 'rw',
156             isa => InstanceOf ['WWW::Spotify::Response'],
157             predicate => 'has_last_response',
158             );
159              
160             has 'last_error' => (
161             is => 'rw',
162             isa => Str,
163             default => q{}
164             );
165              
166             has 'response_headers' => (
167             is => 'rw',
168             isa => Str,
169             default => q{}
170             );
171              
172             has 'problem' => (
173             is => 'rw',
174             isa => Str,
175             default => q{}
176             );
177              
178             has 'ua' => (
179             is => 'ro',
180             isa => InstanceOf ['LWP::UserAgent'],
181             handles => { _mech => 'clone' },
182             lazy => 1,
183             default => sub {
184             require WWW::Mechanize;
185             WWW::Mechanize->new( autocheck => 0 );
186             },
187             );
188              
189             has 'response_status' => (
190             is => 'rw',
191             isa => Int
192             );
193              
194             has 'response_content_type' => (
195             is => 'rw',
196             isa => Str
197             );
198              
199             has 'custom_request_handler' => (
200             is => 'rw',
201             isa => CodeRef,
202             predicate => '_has_custom_request_handler',
203             );
204              
205             has 'custom_request_handler_result' => (
206             is => 'ro',
207             writer => '_set_custom_request_handler_result'
208             );
209              
210             has 'die_on_response_error' => (
211             is => 'rw',
212             isa => Bool,
213             default => 0
214             );
215              
216             # ------------------------------------------------------------------
217             # Compose roles *after* all attribute declarations so that the
218             # requirements declared by those roles are satisfied. The roles are
219             # currently responsible for authentication logic and generic HTTP
220             # helpers.
221             # ------------------------------------------------------------------
222              
223             with qw(
224             WWW::Spotify::Client
225             WWW::Spotify::Endpoint
226             );
227              
228             my %api_call_options = (
229             '/v1/albums/{id}' => {
230             info => 'Get an album',
231             type => 'GET',
232             method => 'album'
233             },
234              
235             '/v1/audiobooks/{id}' => {
236             info => 'Get an audiobook',
237             type => 'GET',
238             method => 'get_audiobook',
239             params => ['market']
240             },
241              
242             '/v1/audiobooks' => {
243             info => 'Get several audiobooks',
244             type => 'GET',
245             method => 'get_several_audiobooks',
246             params => [ 'ids', 'market' ]
247             },
248              
249             '/v1/audiobooks/{id}/chapters' => {
250             info => 'Get Audiobook Chapters',
251             type => 'GET',
252             method => 'get_audiobook_chapters',
253             params => [ 'id', 'market', 'limit', 'offset' ]
254             },
255              
256             '/v1/me/audiobooks|GET' => {
257             info => 'Get User\'s Saved Audiobooks',
258             type => 'GET',
259             method => 'get_users_saved_audiobooks',
260             params => [ 'limit', 'offset' ]
261             },
262              
263             '/v1/me/audiobooks|PUT' => {
264             info => 'Save Audiobooks for Current User',
265             type => 'PUT',
266             method => 'save_audiobooks_for_current_user',
267             params => ['ids']
268             },
269              
270             '/v1/me/audiobooks|DELETE' => {
271             info => 'Remove User\'s Saved Audiobooks',
272             type => 'DELETE',
273             method => 'remove_users_saved_audiobooks',
274             params => ['ids']
275             },
276              
277             '/v1/me/audiobooks/contains' => {
278             info => 'Check User\'s Saved Audiobooks',
279             type => 'GET',
280             method => 'check_users_saved_audiobooks',
281             params => ['ids']
282             },
283              
284             '/v1/me/shows|GET' => {
285             info => 'Get User\'s Saved Shows',
286             type => 'GET',
287             method => 'get_users_saved_shows',
288             params => [ 'limit', 'offset' ]
289             },
290              
291             '/v1/me/shows|PUT' => {
292             info => 'Save Shows for Current User',
293             type => 'PUT',
294             method => 'save_shows_for_current_user',
295             params => ['ids']
296             },
297              
298             '/v1/me/shows/contains' => {
299             info => 'Check User\'s Saved Shows',
300             type => 'GET',
301             method => 'check_users_saved_shows',
302             params => ['ids']
303             },
304              
305             '/v1/browse/categories' => {
306             info => 'Get Several Browse Categories',
307             type => 'GET',
308             method => 'get_categories',
309             params => [ 'country', 'locale', 'limit', 'offset' ]
310             },
311              
312             '/v1/browse/categories/{category_id}' => {
313             info => 'Get Single Browse Category',
314             type => 'GET',
315             method => 'get_category',
316             params => [ 'category_id', 'locale' ]
317             },
318              
319             '/v1/chapters/{id}' => {
320             info => 'Get a Chapter',
321             type => 'GET',
322             method => 'get_chapter',
323             params => [ 'id', 'market' ]
324             },
325              
326             '/v1/chapters' => {
327             info => 'Get Several Chapters',
328             type => 'GET',
329             method => 'get_several_chapters',
330             params => [ 'ids', 'market' ]
331             },
332              
333             '/v1/recommendations/available-genre-seeds' => {
334             info => 'Get Available Genre Seeds',
335             type => 'GET',
336             method => 'get_available_genre_seeds'
337             },
338              
339             '/v1/markets' => {
340             info => 'Get Available Markets',
341             type => 'GET',
342             method => 'get_available_markets'
343             },
344              
345             '/v1/shows/{id}' => {
346             info => 'Get a Show',
347             type => 'GET',
348             method => 'get_show',
349             params => ['market']
350             },
351              
352             '/v1/shows' => {
353             info => 'Get Several Shows',
354             type => 'GET',
355             method => 'get_several_shows',
356             params => [ 'ids', 'market' ]
357             },
358              
359             '/v1/shows/{id}/episodes' => {
360             info => 'Get Show Episodes',
361             type => 'GET',
362             method => 'get_show_episodes',
363             params => [ 'id', 'market', 'limit', 'offset' ]
364             },
365              
366             '/v1/albums?ids={ids}' => {
367             info => 'Get several albums',
368             type => 'GET',
369             method => 'albums',
370             params => [ 'limit', 'offset' ]
371             },
372              
373             '/v1/playlists/{playlist_id}' => {
374             info => 'Get a playlist',
375             type => 'GET',
376             method => 'get_playlist'
377             },
378              
379             '/v1/playlists/{playlist_id}/tracks|GET' => {
380             info => 'Get playlist items',
381             type => 'GET',
382             method => 'get_playlist_items',
383             params => [ 'limit', 'offset', 'market', 'fields' ]
384             },
385              
386             '/v1/users/{user_id}/playlists|POST' => {
387             info => 'Create a playlist',
388             type => 'POST',
389             method => 'create_playlist'
390             },
391              
392             '/v1/me/playlists' => {
393             info => 'Get current user\'s playlists',
394             type => 'GET',
395             method => 'get_current_user_playlists',
396             params => [ 'limit', 'offset' ]
397             },
398              
399             '/v1/playlists/{playlist_id}/tracks|POST' => {
400             info => 'Add items to a playlist',
401             type => 'POST',
402             method => 'add_items_to_playlist'
403             },
404              
405             '/v1/me/tracks' => {
406             info => 'Remove User\'s Saved Tracks',
407             type => 'DELETE',
408             method => 'remove_user_saved_tracks'
409             },
410              
411             '/v1/me/tracks/contains' => {
412             info => 'Check User\'s Saved Tracks',
413             type => 'GET',
414             method => 'check_users_saved_tracks'
415             },
416              
417             '/v1/audio-features' => {
418             info => 'Get Several Tracks\' Audio Features',
419             type => 'GET',
420             method => 'get_several_tracks_audio_features'
421             },
422             '/v1/audio-features/{id}' => {
423             info => 'Get Track\'s Audio Features',
424             type => 'GET',
425             method => 'get_track_audio_features'
426             },
427             '/v1/audio-analysis/{id}' => {
428             info => 'Get Track\'s Audio Analysis',
429             type => 'GET',
430             method => 'get_track_audio_analysis'
431             },
432              
433             '/v1/recommendations' => {
434             info => 'Get Recommendations',
435             type => 'GET',
436             method => 'get_recommendations',
437             params => [
438             'seed_artists', 'seed_genres', 'seed_tracks', 'limit', 'market'
439             ]
440             },
441              
442             '/v1/me/following|GET' => {
443             info => 'Get Followed Artists',
444             type => 'GET',
445             method => 'get_followed_artists',
446             params => [ 'type', 'after', 'limit' ]
447             },
448              
449             '/v1/me/following|PUT' => {
450             info => 'Follow Artists or Users',
451             type => 'PUT',
452             method => 'follow_artists_or_users',
453             params => [ 'type', 'ids' ]
454             },
455              
456             '/v1/me/following|DELETE' => {
457             info => 'Unfollow Artists or Users',
458             type => 'DELETE',
459             method => 'unfollow_artists_or_users',
460             params => [ 'type', 'ids' ]
461             },
462              
463             '/v1/me/following/contains' => {
464             info => 'Check if Current User Follows Artists or Users',
465             type => 'GET',
466             method => 'check_if_user_follows_artists_or_users',
467             params => [ 'type', 'ids' ]
468             },
469              
470             '/v1/playlists/{playlist_id}/followers/contains' => {
471             info => 'Check if Current User Follows Playlist',
472             type => 'GET',
473             method => 'check_if_user_follows_playlist',
474             params => [ 'playlist_id', 'ids' ]
475             },
476              
477             '/v1/albums/{id}/tracks' => {
478             info => q{Get an album's tracks},
479             type => 'GET',
480             method => 'albums_tracks'
481             },
482              
483             '/v1/artists/{id}' => {
484             info => 'Get an artist',
485             type => 'GET',
486             method => 'artist'
487             },
488              
489             '/v1/artists?ids={ids}' => {
490             info => 'Get several artists',
491             type => 'GET',
492             method => 'artists'
493             },
494              
495             '/v1/artists/{id}/albums' => {
496             info => q{Get an artist's albums},
497             type => 'GET',
498             method => 'artist_albums',
499             params => [ 'limit', 'offset', 'country', 'album_type' ]
500             },
501              
502             '/v1/artists/{id}/top-tracks?country={country}' => {
503             info => q{Get an artist's top tracks},
504             type => 'GET',
505             method => 'artist_top_tracks',
506             params => ['country']
507             },
508              
509             '/v1/artists/{id}/related-artists' => {
510             info => q{Get an artist's top tracks},
511             type => 'GET',
512             method => 'artist_related_artists',
513              
514             # params => [ 'country' ]
515             },
516              
517             # adding q and type to url unlike example since they are both required
518             '/v1/search?q={q}&type={type}' => {
519             info => 'Search for an item',
520             type => 'GET',
521             method => 'search',
522             params => [ 'limit', 'offset', 'q', 'type' ]
523             },
524              
525             '/v1/tracks/{id}' => {
526             info => 'Get a track',
527             type => 'GET',
528             method => 'track'
529             },
530              
531             '/v1/tracks?ids={ids}' => {
532             info => 'Get several tracks',
533             type => 'GET',
534             method => 'tracks'
535             },
536              
537             '/v1/users/{user_id}' => {
538             info => q{Get a user's profile},
539             type => 'GET',
540             method => 'user'
541             },
542              
543             '/v1/me' => {
544             info => q{Get current user's profile},
545             type => 'GET',
546             method => 'me'
547             },
548              
549             '/v1/users/{user_id}/playlists|GET' => {
550             info => q{Get a list of a user's playlists},
551             type => 'GET',
552             method => 'user_playlist'
553             },
554              
555             '/v1/users/{user_id}/playlists/{playlist_id}' => {
556             info => 'Get a playlist',
557             type => 'GET',
558             method => q{}
559             },
560              
561             '/v1/browse/featured-playlists' => {
562             info => 'Get a list of featured playlists',
563             type => 'GET',
564             method => 'browse_featured_playlists'
565             },
566              
567             '/v1/browse/new-releases' => {
568             info => 'Get a list of new releases',
569             type => 'GET',
570             method => 'browse_new_releases'
571             },
572              
573             '/v1/users/{user_id}/playlists/{playlist_id}/tracks' => {
574             info => q{Get a playlist's tracks},
575             type => 'POST',
576             method => q{}
577             },
578              
579             '/v1/users/{user_id}/playlists' => {
580             info => 'Create a playlist',
581             type => 'POST',
582             method => q{}
583             },
584              
585             '/v1/users/{user_id}/playlists/{playlist_id}/tracks' => {
586             info => 'Add tracks to a playlist',
587             type => 'POST',
588             method => q{}
589             }
590             );
591              
592             our %method_to_uri = ();
593              
594             # Build %method_to_uri mapping while tolerating duplicate URI paths that
595             # are distinguished by HTTP verb suffixes appended to the hash key (eg
596             # "/v1/me/audiobooks|GET"). The verb portion – everything from the last
597             # pipe ("|") character to the end of the string – is stripped off before
598             # the mapping is stored so that the final URL remains unchanged.
599              
600             foreach my $key ( keys %api_call_options ) {
601             my $entry = $api_call_options{$key};
602             next if $entry->{method} eq q{}; # skip placeholders
603              
604             # Remove an optional "|VERB" suffix that we add to disambiguate
605             # duplicate paths (eg "/v1/me/audiobooks|PUT"). This preserves the
606             # original request URI while still allowing each HTTP verb to have a
607             # distinct hash key.
608             my ($path_without_suffix) = split /\|/, $key, 2;
609              
610             $method_to_uri{ $entry->{method} } = $path_without_suffix;
611             }
612              
613             # Provide a small accessor so other roles can access the mapping without
614             # needing direct package‑level knowledge.
615             sub _method_to_uri {
616 0     0     return \%method_to_uri;
617             }
618              
619             sub _set_response_headers {
620 0     0     my $self = shift;
621 0           my $mech = shift;
622              
623 0           my $hd;
624 0     0     capture { $mech->dump_headers(); } \$hd;
  0            
625              
626 0           $self->response_headers($hd);
627 0           return;
628             }
629              
630             sub format_results {
631 0     0 0   my $self = shift;
632 0           my $content = shift;
633              
634             # want to store the result in case
635             # we want to interact with it via a helper method
636 0           $self->last_result($content);
637              
638             # FIX ME / TEST ME
639             # verify both of these work and return the *same* perl hash
640              
641             # when / how should we check the status? Do we need to?
642             # if so then we need to create another method that will
643             # manage a Sucess vs. Fail request
644              
645 0           require WWW::Spotify::Response;
646              
647 0           my $resp = WWW::Spotify::Response->new(
648             raw => $content,
649             content_type => $_[0],
650             status => $_[1],
651             );
652              
653 0           $self->last_response($resp);
654              
655 0 0 0       if ( $self->auto_json_decode && $self->result_format eq 'json' ) {
656 0           return $resp->json;
657             }
658              
659 0           return $content;
660             }
661              
662             sub get {
663              
664             # This seemed like a simple enough method
665             # but everything I tried resulted in unacceptable
666             # trade offs and explict defining of the structures
667             # The new method, which I hope I remember when I
668             # revisit it, was to use JSON::Path
669             # It is an awesome module, but a little heavy
670             # on dependencies. However I would not have been
671             # able to do this in so few lines without it
672              
673             # Making a generalization here
674             # if you use a * you are looking for an array
675             # if you don't have an * you want the first 1 (or should I say you get the first 1)
676              
677 0     0 1   my ( $self, @return ) = @_;
678              
679             # my @return = @_;
680              
681 0           my @out;
682              
683 0           my $result = decode_json $self->last_result();
684              
685 0           my $search_ref = $result;
686              
687 0 0         warn Dumper($result) if $self->debug();
688              
689 0           foreach my $key (@return) {
690 0           my $type = 'value';
691 0 0         if ( $key =~ /\*\]/ ) {
692 0           $type = 'values';
693             }
694              
695 0           my $jpath = JSON::Path->new("\$.$key");
696              
697 0           my @t_arr = $jpath->$type($result);
698              
699 0 0         if ( $type eq 'value' ) {
700 0           push @out, $t_arr[0];
701             }
702             else {
703 0           push @out, \@t_arr;
704             }
705             }
706 0 0         if (wantarray) {
707 0           return @out;
708             }
709             else {
710 0           return $out[0];
711             }
712              
713             }
714              
715             sub build_url_base {
716              
717             # first the uri type
718 0     0 0   my $self = shift;
719 0   0       my $call_type = shift || $self->call_type();
720              
721 0           my $url = $self->uri_scheme();
722              
723             # the ://
724 0           $url .= '://';
725              
726             # the domain
727 0           $url .= $self->uri_hostname();
728              
729             # the path
730 0 0         if ( $self->uri_domain_path() ) {
731 0           $url .= '/' . $self->uri_domain_path();
732             }
733              
734 0           return $url;
735             }
736              
737             #-- spotify specific methods
738              
739             sub album {
740 0     0 1   my $self = shift;
741 0           my $id = shift;
742              
743 0           return $self->send_get_request(
744             {
745             method => 'album',
746             params => { 'id' => $id },
747             client_auth_required => 1
748             }
749             );
750             }
751              
752             sub albums {
753 0     0 1   my $self = shift;
754 0           my $ids = shift;
755              
756 0 0         if ( ref($ids) eq 'ARRAY' ) {
757 0           $ids = join_ids($ids);
758             }
759              
760 0           return $self->send_get_request(
761             {
762             method => 'albums',
763             params => { 'ids' => $ids },
764             client_auth_required => 1
765             }
766             );
767              
768             }
769              
770             sub join_ids {
771 0     0 0   my $array = shift;
772 0           return join( ',', @$array );
773             }
774              
775             sub albums_tracks {
776 0     0 1   my $self = shift;
777 0           my $album_id = shift;
778 0           my $extras = shift;
779              
780 0           return $self->send_get_request(
781             {
782             method => 'albums_tracks',
783             params => { 'id' => $album_id },
784             extras => $extras,
785             client_auth_required => 1
786             }
787             );
788              
789             }
790              
791             sub artist {
792 0     0 1   my $self = shift;
793 0           my $id = shift;
794              
795 0           return $self->send_get_request(
796             {
797             method => 'artist',
798             params => { 'id' => $id },
799             client_auth_required => 1
800             }
801             );
802              
803             }
804              
805             sub artists {
806 0     0 1   my $self = shift;
807 0           my $artists = shift;
808              
809 0 0         if ( ref($artists) eq 'ARRAY' ) {
810 0           $artists = join_ids($artists);
811             }
812              
813 0           return $self->send_get_request(
814             {
815             method => 'artists',
816             params => { 'ids' => $artists },
817             client_auth_required => 1
818             }
819             );
820              
821             }
822              
823             sub artist_albums {
824 0     0 1   my $self = shift;
825 0           my $artist_id = shift;
826 0           my $extras = shift;
827              
828 0           return $self->send_get_request(
829             {
830             method => 'artist_albums',
831             params => { 'id' => $artist_id },
832             extras => $extras,
833             client_auth_required => 1
834             }
835             );
836              
837             }
838              
839             sub artist_top_tracks {
840 0     0 1   my $self = shift;
841 0           my $artist_id = shift;
842 0           my $country = shift;
843              
844 0           return $self->send_get_request(
845             {
846             method => 'artist_top_tracks',
847             params => {
848             'id' => $artist_id,
849             'country' => $country,
850             },
851             client_auth_required => 1
852             }
853             );
854              
855             }
856              
857             sub artist_related_artists {
858 0     0 1   my $self = shift;
859 0           my $artist_id = shift;
860              
861 0           return $self->send_get_request(
862             {
863             method => 'artist_related_artists',
864             params => {
865             'id' => $artist_id,
866             },
867             client_auth_required => 1
868             }
869             );
870              
871             }
872              
873             sub me {
874 0     0 0   my $self = shift;
875              
876 0           return $self->send_get_request(
877             {
878             method => 'me',
879             client_auth_required => 1
880             }
881             );
882             }
883              
884             sub next_result_set {
885 0     0 0   my $self = shift;
886 0           my $result = shift;
887              
888             # Parse JSON result if it's a string
889 0 0 0       if ( $result && !ref($result) ) {
890 0           $result = decode_json($result);
891             }
892              
893 0 0 0       return unless $result && ref($result) eq 'HASH';
894 0 0 0       return unless exists $result->{next} && $result->{next};
895              
896 0           return $self->query_full_url( $result->{next}, 1 );
897             }
898              
899             sub previous_result_set {
900 0     0 0   my $self = shift;
901 0           my $result = shift;
902              
903             # Parse JSON result if it's a string
904 0 0 0       if ( $result && !ref($result) ) {
905 0           $result = decode_json($result);
906             }
907              
908 0 0 0       return unless $result && ref($result) eq 'HASH';
909 0 0 0       return unless exists $result->{previous} && $result->{previous};
910              
911 0           return $self->query_full_url( $result->{previous}, 1 );
912             }
913              
914             sub search {
915 0     0 1   my $self = shift;
916 0           my $q = shift;
917 0           my $type = shift;
918 0           my $extras = shift;
919              
920             # looks like search now requires auth
921             # we will force authentication but need to
922             # reset this to the previous value since not
923             # all requests require auth
924 0           my $old_force_client_auth = $self->force_client_auth();
925 0           $self->force_client_auth(1);
926              
927 0           my $params = {
928             method => 'search',
929             q => $q,
930             type => $type,
931             extras => $extras
932              
933             };
934              
935 0           my $response = $self->send_get_request($params);
936              
937             # reset auth to what it was before to avoid overly chatty
938             # requests
939 0           $self->force_client_auth($old_force_client_auth);
940 0           return $response;
941             }
942              
943             sub track {
944 0     0 1   my $self = shift;
945 0           my $id = shift;
946 0           return $self->send_get_request(
947             {
948             method => 'track',
949             params => { 'id' => $id }
950             }
951             );
952             }
953              
954             sub browse_featured_playlists {
955 0     0 1   my $self = shift;
956 0           my $extras = shift;
957              
958             # locale
959             # country
960             # limit
961             # offset
962              
963 0           return $self->send_get_request(
964             {
965             method => 'browse_featured_playlists',
966             extras => $extras,
967             client_auth_required => 1
968             }
969             );
970             }
971              
972             sub browse_new_releases {
973 0     0 1   my $self = shift;
974 0           my $extras = shift;
975              
976             # locale
977             # country
978             # limit
979             # offset
980              
981 0           return $self->send_get_request(
982             {
983             method => 'browse_new_releases',
984             extras => $extras,
985             client_auth_required => 1
986             }
987             );
988             }
989              
990             sub tracks {
991 0     0 1   my $self = shift;
992 0           my $tracks = shift;
993              
994 0 0         if ( ref($tracks) eq 'ARRAY' ) {
995 0           $tracks = join_ids($tracks);
996             }
997              
998 0           return $self->send_get_request(
999             {
1000             method => 'tracks',
1001             params => { 'ids' => $tracks }
1002             }
1003             );
1004              
1005             }
1006              
1007             sub user {
1008 0     0 1   my $self = shift;
1009 0           my $user_id = shift;
1010 0           return $self->send_get_request(
1011             {
1012             method => 'user',
1013             params => { 'user_id' => $user_id },
1014             client_auth_required => 1
1015             }
1016             );
1017              
1018             }
1019              
1020             sub get_playlist {
1021 0     0 1   my ( $self, $playlist_id ) = @_;
1022 0           return $self->send_get_request(
1023             {
1024             method => 'get_playlist',
1025             params => { 'playlist_id' => $playlist_id },
1026             client_auth_required => 1
1027             }
1028             );
1029             }
1030              
1031             sub get_playlist_items {
1032 0     0 1   my ( $self, $playlist_id, $extras ) = @_;
1033 0           return $self->send_get_request(
1034             {
1035             method => 'get_playlist_items',
1036             params => { 'playlist_id' => $playlist_id },
1037             extras => $extras,
1038             client_auth_required => 1
1039             }
1040             );
1041             }
1042              
1043             sub create_playlist {
1044 0     0 1   my ( $self, $user_id, $name, $public, $description ) = @_;
1045 0           return $self->send_post_request(
1046             {
1047             method => 'create_playlist',
1048             params => {
1049             'user_id' => $user_id,
1050             'name' => $name,
1051             'public' => $public,
1052             'description' => $description,
1053             },
1054             client_auth_required => 1
1055             }
1056             );
1057             }
1058              
1059             sub get_current_user_playlists {
1060 0     0 1   my ( $self, $extras ) = @_;
1061 0           return $self->send_get_request(
1062             {
1063             method => 'get_current_user_playlists',
1064             extras => $extras,
1065             client_auth_required => 1
1066             }
1067             );
1068             }
1069              
1070             sub add_items_to_playlist {
1071 0     0 1   my ( $self, $playlist_id, $uris, $position ) = @_;
1072 0           return $self->send_post_request(
1073             {
1074             method => 'add_items_to_playlist',
1075             params => {
1076             'playlist_id' => $playlist_id,
1077             'uris' => $uris,
1078             'position' => $position,
1079             },
1080             client_auth_required => 1
1081             }
1082             );
1083             }
1084              
1085             sub remove_user_saved_tracks {
1086 0     0 1   my ( $self, $ids ) = @_;
1087              
1088 0 0         if ( ref($ids) eq 'ARRAY' ) {
1089 0           $ids = join_ids($ids);
1090             }
1091              
1092 0           return $self->send_delete_request(
1093             {
1094             method => 'remove_user_saved_tracks',
1095             params => { 'ids' => $ids },
1096             client_auth_required => 1
1097             }
1098             );
1099             }
1100              
1101             sub check_users_saved_tracks {
1102 0     0 1   my ( $self, $ids ) = @_;
1103              
1104 0 0         if ( ref($ids) eq 'ARRAY' ) {
1105 0           $ids = join_ids($ids);
1106             }
1107              
1108 0           return $self->send_get_request(
1109             {
1110             method => 'check_users_saved_tracks',
1111             params => { 'ids' => $ids },
1112             client_auth_required => 1
1113             }
1114             );
1115             }
1116              
1117             sub get_several_tracks_audio_features {
1118 0     0 1   my ( $self, $ids ) = @_;
1119              
1120 0 0         if ( ref($ids) eq 'ARRAY' ) {
1121 0           $ids = join_ids($ids);
1122             }
1123              
1124 0           return $self->send_get_request(
1125             {
1126             method => 'get_several_tracks_audio_features',
1127             params => { 'ids' => $ids },
1128             client_auth_required => 1
1129             }
1130             );
1131             }
1132              
1133             sub get_track_audio_features {
1134 0     0 1   my ( $self, $id ) = @_;
1135              
1136 0           return $self->send_get_request(
1137             {
1138             method => 'get_track_audio_features',
1139             params => { 'id' => $id },
1140             client_auth_required => 1
1141             }
1142             );
1143             }
1144              
1145             sub get_track_audio_analysis {
1146 0     0 1   my ( $self, $id ) = @_;
1147              
1148 0           return $self->send_get_request(
1149             {
1150             method => 'get_track_audio_analysis',
1151             params => { 'id' => $id },
1152             client_auth_required => 1
1153             }
1154             );
1155             }
1156              
1157             sub get_recommendations {
1158 0     0 1   my ( $self, %params ) = @_;
1159              
1160 0           return $self->send_get_request(
1161             {
1162             method => 'get_recommendations',
1163             params => \%params,
1164             client_auth_required => 1
1165             }
1166             );
1167             }
1168              
1169             sub get_followed_artists {
1170 0     0 1   my ( $self, %params ) = @_;
1171              
1172             # Ensure 'type' is set to 'artist' as it's the only supported value
1173 0           $params{type} = 'artist';
1174              
1175 0           return $self->send_get_request(
1176             {
1177             method => 'get_followed_artists',
1178             params => \%params,
1179             client_auth_required => 1
1180             }
1181             );
1182             }
1183              
1184             sub follow_artists_or_users {
1185 0     0 1   my ( $self, $type, $ids ) = @_;
1186              
1187 0 0 0       die "Type must be 'artist' or 'user'"
1188             unless $type eq 'artist' or $type eq 'user';
1189              
1190 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1191              
1192 0           return $self->send_put_request(
1193             {
1194             method => 'follow_artists_or_users',
1195             params => {
1196             type => $type,
1197             ids => $id_list
1198             },
1199             client_auth_required => 1
1200             }
1201             );
1202             }
1203              
1204             sub unfollow_artists_or_users {
1205 0     0 1   my ( $self, $type, $ids ) = @_;
1206              
1207 0 0 0       die "Type must be 'artist' or 'user'"
1208             unless $type eq 'artist' or $type eq 'user';
1209              
1210 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1211              
1212 0           return $self->send_delete_request(
1213             {
1214             method => 'unfollow_artists_or_users',
1215             params => {
1216             type => $type,
1217             ids => $id_list
1218             },
1219             client_auth_required => 1
1220             }
1221             );
1222             }
1223              
1224             sub check_if_user_follows_artists_or_users {
1225 0     0 1   my ( $self, $type, $ids ) = @_;
1226              
1227 0 0 0       die "Type must be 'artist' or 'user'"
1228             unless $type eq 'artist' or $type eq 'user';
1229              
1230 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1231              
1232 0           return $self->send_get_request(
1233             {
1234             method => 'check_if_user_follows_artists_or_users',
1235             params => {
1236             type => $type,
1237             ids => $id_list
1238             },
1239             client_auth_required => 1
1240             }
1241             );
1242             }
1243              
1244             sub check_if_user_follows_playlist {
1245 0     0 1   my ( $self, $playlist_id, $ids ) = @_;
1246              
1247 0 0         die "playlist_id is required" unless $playlist_id;
1248 0 0         die "ids is required" unless $ids;
1249              
1250 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1251              
1252 0           return $self->send_get_request(
1253             {
1254             method => 'check_if_user_follows_playlist',
1255             params => {
1256             playlist_id => $playlist_id,
1257             ids => $id_list
1258             },
1259             client_auth_required => 1
1260             }
1261             );
1262             }
1263              
1264             sub get_audiobook {
1265 0     0 1   my ( $self, $id, $market ) = @_;
1266              
1267 0 0         die "Audiobook ID is required" unless $id;
1268              
1269 0           my $params = { id => $id };
1270 0 0         $params->{market} = $market if $market;
1271              
1272 0           return $self->send_get_request(
1273             {
1274             method => 'get_audiobook',
1275             params => $params,
1276             client_auth_required => 1
1277             }
1278             );
1279             }
1280              
1281             sub get_several_audiobooks {
1282 0     0 1   my ( $self, $ids, $market ) = @_;
1283              
1284 0 0         die "Audiobook IDs are required" unless $ids;
1285              
1286 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1287              
1288 0           my $params = { ids => $id_list };
1289 0 0         $params->{market} = $market if $market;
1290              
1291 0           return $self->send_get_request(
1292             {
1293             method => 'get_several_audiobooks',
1294             params => $params,
1295             client_auth_required => 1
1296             }
1297             );
1298             }
1299              
1300             sub get_audiobook_chapters {
1301 0     0 1   my ( $self, $id, %params ) = @_;
1302              
1303 0 0         die "Audiobook ID is required" unless $id;
1304              
1305 0           $params{id} = $id;
1306              
1307 0           return $self->send_get_request(
1308             {
1309             method => 'get_audiobook_chapters',
1310             params => \%params,
1311             client_auth_required => 1
1312             }
1313             );
1314             }
1315              
1316             sub get_users_saved_audiobooks {
1317 0     0 1   my ( $self, $limit, $offset ) = @_;
1318              
1319 0           my $params = {};
1320 0 0         $params->{limit} = $limit if $limit;
1321 0 0         $params->{offset} = $offset if defined $offset;
1322              
1323 0           return $self->send_get_request(
1324             {
1325             method => 'get_users_saved_audiobooks',
1326             params => $params,
1327             client_auth_required => 1
1328             }
1329             );
1330             }
1331              
1332             sub save_audiobooks_for_current_user {
1333 0     0 1   my ( $self, $ids ) = @_;
1334              
1335 0 0         die "Audiobook IDs are required" unless $ids;
1336              
1337 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1338              
1339 0           return $self->send_put_request(
1340             {
1341             method => 'save_audiobooks_for_current_user',
1342             params => { ids => $id_list },
1343             client_auth_required => 1
1344             }
1345             );
1346             }
1347              
1348             sub remove_users_saved_audiobooks {
1349 0     0 1   my ( $self, $ids ) = @_;
1350              
1351 0 0         die "Audiobook IDs are required" unless $ids;
1352              
1353 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1354              
1355 0           return $self->send_delete_request(
1356             {
1357             method => 'remove_users_saved_audiobooks',
1358             params => { ids => $id_list },
1359             client_auth_required => 1
1360             }
1361             );
1362             }
1363              
1364             sub check_users_saved_audiobooks {
1365 0     0 1   my ( $self, $ids ) = @_;
1366              
1367 0 0         die "Audiobook IDs are required" unless $ids;
1368              
1369 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1370              
1371 0           return $self->send_get_request(
1372             {
1373             method => 'check_users_saved_audiobooks',
1374             params => { ids => $id_list },
1375             client_auth_required => 1
1376             }
1377             );
1378             }
1379              
1380             sub get_users_saved_shows {
1381 0     0 1   my ( $self, %params ) = @_;
1382              
1383 0           return $self->send_get_request(
1384             {
1385             method => 'get_users_saved_shows',
1386             params => \%params,
1387             client_auth_required => 1
1388             }
1389             );
1390             }
1391              
1392             sub save_shows_for_current_user {
1393 0     0 1   my ( $self, $ids ) = @_;
1394              
1395 0 0         die "Show IDs are required" unless $ids;
1396              
1397 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1398              
1399 0           return $self->send_put_request(
1400             {
1401             method => 'save_shows_for_current_user',
1402             params => { ids => $id_list },
1403             client_auth_required => 1
1404             }
1405             );
1406             }
1407              
1408             sub check_users_saved_shows {
1409 0     0 1   my ( $self, $ids ) = @_;
1410              
1411 0 0         die "Show IDs are required" unless $ids;
1412              
1413 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1414              
1415 0           return $self->send_get_request(
1416             {
1417             method => 'check_users_saved_shows',
1418             params => { ids => $id_list },
1419             client_auth_required => 1
1420             }
1421             );
1422             }
1423              
1424             sub get_categories {
1425 0     0 1   my ( $self, %params ) = @_;
1426              
1427 0           return $self->send_get_request(
1428             {
1429             method => 'get_categories',
1430             params => \%params,
1431             client_auth_required => 1
1432             }
1433             );
1434             }
1435              
1436             sub get_category {
1437 0     0 1   my ( $self, $category_id, %params ) = @_;
1438              
1439 0 0         die "Category ID is required" unless $category_id;
1440              
1441 0           $params{category_id} = $category_id;
1442              
1443 0           return $self->send_get_request(
1444             {
1445             method => 'get_category',
1446             params => \%params,
1447             client_auth_required => 1
1448             }
1449             );
1450             }
1451              
1452             sub get_chapter {
1453 0     0 1   my ( $self, $id, %params ) = @_;
1454              
1455 0 0         die "Chapter ID is required" unless $id;
1456              
1457 0           $params{id} = $id;
1458              
1459 0           return $self->send_get_request(
1460             {
1461             method => 'get_chapter',
1462             params => \%params,
1463             client_auth_required => 1
1464             }
1465             );
1466             }
1467              
1468             sub get_several_chapters {
1469 0     0 1   my ( $self, $ids, %params ) = @_;
1470              
1471 0 0         die "Chapter IDs are required" unless $ids;
1472              
1473 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1474              
1475 0           $params{ids} = $id_list;
1476              
1477 0           return $self->send_get_request(
1478             {
1479             method => 'get_several_chapters',
1480             params => \%params,
1481             client_auth_required => 1
1482             }
1483             );
1484             }
1485              
1486             sub get_available_genre_seeds {
1487 0     0 1   my ($self) = @_;
1488              
1489 0           return $self->send_get_request(
1490             {
1491             method => 'get_available_genre_seeds',
1492             client_auth_required => 1
1493             }
1494             );
1495             }
1496              
1497             sub get_available_markets {
1498 0     0 1   my ($self) = @_;
1499              
1500 0           return $self->send_get_request(
1501             {
1502             method => 'get_available_markets',
1503             client_auth_required => 1
1504             }
1505             );
1506             }
1507              
1508             sub get_show {
1509 0     0 1   my ( $self, $id, $market ) = @_;
1510              
1511 0 0         die "Show ID is required" unless $id;
1512              
1513 0           my $params = { id => $id };
1514 0 0         $params->{market} = $market if $market;
1515              
1516 0           return $self->send_get_request(
1517             {
1518             method => 'get_show',
1519             params => $params,
1520             client_auth_required => 1
1521             }
1522             );
1523             }
1524              
1525             sub get_several_shows {
1526 0     0 1   my ( $self, $ids, $market ) = @_;
1527              
1528 0 0         die "Show IDs are required" unless $ids;
1529              
1530 0 0         my $id_list = ref($ids) eq 'ARRAY' ? join( ',', @$ids ) : $ids;
1531              
1532 0           my $params = { ids => $id_list };
1533 0 0         $params->{market} = $market if $market;
1534              
1535 0           return $self->send_get_request(
1536             {
1537             method => 'get_several_shows',
1538             params => $params,
1539             client_auth_required => 1
1540             }
1541             );
1542             }
1543              
1544             sub get_show_episodes {
1545 0     0 1   my ( $self, $id, %params ) = @_;
1546              
1547 0 0         die "Show ID is required" unless $id;
1548              
1549 0           $params{id} = $id;
1550              
1551 0           return $self->send_get_request(
1552             {
1553             method => 'get_show_episodes',
1554             params => \%params,
1555             client_auth_required => 1
1556             }
1557             );
1558             }
1559              
1560             1;
1561              
1562             =pod
1563              
1564             =encoding UTF-8
1565              
1566             =head1 NAME
1567              
1568             WWW::Spotify - Spotify Web API Wrapper
1569              
1570             =head1 VERSION
1571              
1572             version 0.014
1573              
1574             =head1 SYNOPSIS
1575              
1576             use WWW::Spotify ();
1577              
1578             my $spotify = WWW::Spotify->new();
1579              
1580             my $result;
1581              
1582             $result = $spotify->album('0sNOF9WDwhWunNAHPD3Baj');
1583              
1584             # $result is a json structure, you can operate on it directly
1585             # or you can use the "get" method see below
1586              
1587             $result = $spotify->albums( '41MnTivkwTO3UUJ8DrqEJJ,6JWc4iAiJ9FjyK0B59ABb4,6UXCm6bOO4gFlDQZV5yL37' );
1588              
1589             $result = $spotify->albums_tracks( '6akEvsycLGftJxYudPjmqK',
1590             {
1591             limit => 1,
1592             offset => 1
1593              
1594             }
1595             );
1596              
1597             $result = $spotify->artist( '0LcJLqbBmaGUft1e9Mm8HV' );
1598              
1599             my $artists_multiple = '0oSGxfWSnnOXhD2fKuz2Gy,3dBVyJ7JuOMt4GE9607Qin';
1600              
1601             $result = $spotify->artists( $artists_multiple );
1602              
1603             $result = $spotify->artist_albums( '1vCWHaC5f2uS3yhpwWbIA6' ,
1604             { album_type => 'single',
1605             # country => 'US',
1606             limit => 2,
1607             offset => 0
1608             } );
1609              
1610             $result = $spotify->track( '0eGsygTp906u18L0Oimnem' );
1611              
1612             $result = $spotify->tracks( '0eGsygTp906u18L0Oimnem,1lDWb6b6ieDQ2xT7ewTC3G' );
1613              
1614             $result = $spotify->artist_top_tracks( '43ZHCT0cAZBISjO8DG9PnE', # artist id
1615             'SE' # country
1616             );
1617              
1618             $result = $spotify->search(
1619             'tania bowra' ,
1620             'artist' ,
1621             { limit => 15 , offset => 0 }
1622             );
1623              
1624             $result = $spotify->user( 'glennpmcdonald' );
1625              
1626             # public play interaction example
1627             # NEED TO SET YOUR o_auth client_id and secret for these to work
1628              
1629             $spotify->browse_featured_playlists( country => 'US' );
1630              
1631             my $link = $spotify->get('playlists.items[*].href');
1632              
1633             # $link is an arrayfef of the all the playlist urls
1634              
1635             foreach my $playlist (@{$link}) {
1636             # make sure the links look valid
1637             next if $playlist !~ /playlists/;
1638             $spotify->query_full_url($playlist,1);
1639             my $pl_name = $spotify->get('name');
1640             my $tracks = $spotify->get('tracks.items[*].track.id');
1641             foreach my $track (@{$tracks}) {
1642             print "$track\n";
1643             }
1644             }
1645              
1646             =head1 DESCRIPTION
1647              
1648             Wrapper for the Spotify Web API.
1649              
1650             Since version 0.014 the implementation has been modularised:
1651              
1652             WWW::Spotify – public wrapper (this module)
1653             WWW::Spotify::Client – role with authentication / OAuth helpers
1654             WWW::Spotify::Endpoint – role with low‑level HTTP verbs
1655             WWW::Spotify::Response – object wrapper around an HTTP response
1656              
1657             Splitting the code into roles and small classes keeps the public API
1658             completely intact while making the internals much easier to test and
1659             extend. If you were subclassing C directly nothing
1660             changes – the roles are composed automatically.
1661              
1662             The attribute C was misspelled; it is now
1663             C. A shim accessor is retained for backwards
1664             compatibility.
1665              
1666             https://developer.spotify.com/web-api/
1667              
1668             Have access to a JSON viewer to help develop and debug. The Chrome JSON viewer is
1669             very good and provides the exact path of the item within the JSON in the lower left
1670             of the screen as you mouse over an element.
1671              
1672             =head1 NAME
1673              
1674             WWW::Spotify - Spotify Web API Wrapper
1675              
1676             =head1 VERSION
1677              
1678             version 0.013
1679              
1680             =head1 CONSTRUCTOR ARGS
1681              
1682             =head2 ua
1683              
1684             You may provide your own user agent object to the constructor. This should be
1685             a L or a subclass of it, like L. If you are
1686             using L, you may want to set autocheck off. To get extra
1687             debugging information, you can do something like this:
1688              
1689             use LWP::ConsoleLogger::Easy qw( debug_ua );
1690             use WWW::Mechanize ();
1691             use WWW::Spotify ();
1692              
1693             my $mech = WWW::Mechanize->new( autocheck => 0 );
1694             debug_ua( $mech );
1695             my $spotify = WWW::Spotify->new( ua => $mech )
1696              
1697             =head1 METHODS
1698              
1699             =head2 auto_json_decode
1700              
1701             When true results will be returned as JSON instead of a perl data structure
1702              
1703             $spotify->auto_json_decode(1);
1704              
1705             =head2 get
1706              
1707             Returns a specific item or array of items from the JSON result of the
1708             last action.
1709              
1710             $result = $spotify->search(
1711             'tania bowra' ,
1712             'artist' ,
1713             { limit => 15 , offset => 0 }
1714             );
1715              
1716             my $image_url = $spotify->get( 'artists.items[0].images[0].url' );
1717              
1718             JSON::Path is the underlying library that actually parses the JSON.
1719              
1720             =head2 query_full_url( $url , [needs o_auth] )
1721              
1722             Results from some calls (playlist for example) return full urls that can be in their entirety. This method allows you
1723             make a call to that url and use all of the o_auth and other features provided.
1724              
1725             $spotify->query_full_url( "https://api.spotify.com/v1/users/spotify/playlists/06U6mm6KPtPIg9D4YGNEnu" , 1 );
1726              
1727             =head2 album
1728              
1729             equivalent to /v1/albums/{id}
1730              
1731             $spotify->album('0sNOF9WDwhWunNAHPD3Baj');
1732              
1733             used album vs albums since it is a singular request
1734              
1735             =head2 albums
1736              
1737             equivalent to /v1/albums?ids={ids}
1738              
1739             $spotify->albums( '41MnTivkwTO3UUJ8DrqEJJ,6JWc4iAiJ9FjyK0B59ABb4,6UXCm6bOO4gFlDQZV5yL37' );
1740              
1741             or
1742              
1743             $spotify->albums( [ '41MnTivkwTO3UUJ8DrqEJJ',
1744             '6JWc4iAiJ9FjyK0B59ABb4',
1745             '6UXCm6bOO4gFlDQZV5yL37' ] );
1746              
1747             =head2 albums_tracks
1748              
1749             equivalent to /v1/albums/{id}/tracks
1750              
1751             $spotify->albums_tracks('6akEvsycLGftJxYudPjmqK',
1752             {
1753             limit => 1,
1754             offset => 1
1755              
1756             }
1757             );
1758              
1759             =head2 artist
1760              
1761             equivalent to /v1/artists/{id}
1762              
1763             $spotify->artist( '0LcJLqbBmaGUft1e9Mm8HV' );
1764              
1765             used artist vs artists since it is a singular request and avoids collision with "artists" method
1766              
1767             =head2 artists
1768              
1769             equivalent to /v1/artists?ids={ids}
1770              
1771             my $artists_multiple = '0oSGxfWSnnOXhD2fKuz2Gy,3dBVyJ7JuOMt4GE9607Qin';
1772              
1773             $spotify->artists( $artists_multiple );
1774              
1775             =head2 artist_albums
1776              
1777             equivalent to /v1/artists/{id}/albums
1778              
1779             $spotify->artist_albums( '1vCWHaC5f2uS3yhpwWbIA6' ,
1780             { album_type => 'single',
1781             # country => 'US',
1782             limit => 2,
1783             offset => 0
1784             } );
1785              
1786             =head2 artist_top_tracks
1787              
1788             equivalent to /v1/artists/{id}/top-tracks
1789              
1790             $spotify->artist_top_tracks( '43ZHCT0cAZBISjO8DG9PnE', # artist id
1791             'SE' # country
1792             );
1793              
1794             =head2 artist_related_artists
1795              
1796             equivalent to /v1/artists/{id}/related-artists
1797              
1798             $spotify->artist_related_artists( '43ZHCT0cAZBISjO8DG9PnE' );
1799              
1800             =head2 search
1801              
1802             equivalent to /v1/search?type=album (etc)
1803              
1804             $spotify->search(
1805             'tania bowra' ,
1806             'artist' ,
1807             { limit => 15 , offset => 0 }
1808             );
1809              
1810             =head2 track
1811              
1812             equivalent to /v1/tracks/{id}
1813              
1814             $spotify->track( '0eGsygTp906u18L0Oimnem' );
1815              
1816             =head2 tracks
1817              
1818             equivalent to /v1/tracks?ids={ids}
1819              
1820             $spotify->tracks( '0eGsygTp906u18L0Oimnem,1lDWb6b6ieDQ2xT7ewTC3G' );
1821              
1822             =head2 browse_featured_playlists
1823              
1824             equivalent to /v1/browse/featured-playlists
1825              
1826             $spotify->browse_featured_playlists();
1827              
1828             requires OAuth
1829              
1830             =head2 browse_new_releases
1831              
1832             equivalent to /v1/browse/new-releases
1833              
1834             requires OAuth
1835              
1836             $spotify->browse_new_releases
1837              
1838             =head2 force_client_auth
1839              
1840             Boolean
1841              
1842             will pass authentication (OAuth) on all requests when set
1843              
1844             $spotify->force_client_auth(1);
1845              
1846             =head2 user
1847              
1848             equivalent to /v1/users/{user_id}
1849              
1850             $spotify->user('glennpmcdonald');
1851              
1852             =head2 get_playlist
1853              
1854             equivalent to GET /v1/playlists/{playlist_id}
1855              
1856             $spotify->get_playlist('37i9dQZF1DXcBWIGoYBM5M');
1857              
1858             This method retrieves a playlist owned by a Spotify user. The playlist must be public or owned by the authenticated user.
1859              
1860             =head2 get_playlist_items
1861              
1862             equivalent to /v1/playlists/{playlist_id}/tracks
1863              
1864             $spotify->get_playlist_items('37i9dQZF1DXcBWIGoYBM5M', { limit => 10, offset => 0 });
1865              
1866             =head2 create_playlist
1867              
1868             equivalent to /v1/users/{user_id}/playlists
1869              
1870             $spotify->create_playlist('user_id', 'My New Playlist', 1, 'A description of my playlist');
1871              
1872             =head2 get_current_user_playlists
1873              
1874             equivalent to /v1/me/playlists
1875              
1876             $spotify->get_current_user_playlists({ limit => 20, offset => 0 });
1877              
1878             =head2 add_items_to_playlist
1879              
1880             equivalent to /v1/playlists/{playlist_id}/tracks
1881              
1882             $spotify->add_items_to_playlist('playlist_id', ['spotify:track:4iV5W9uYEdYUVa79Axb7Rh', 'spotify:track:1301WleyT98MSxVHPZCA6M'], 0);
1883              
1884             =head2 remove_user_saved_tracks
1885              
1886             equivalent to /v1/me/tracks
1887              
1888             $spotify->remove_user_saved_tracks(['4iV5W9uYEdYUVa79Axb7Rh', '1301WleyT98MSxVHPZCA6M']);
1889              
1890             =head2 check_users_saved_tracks
1891              
1892             equivalent to /v1/me/tracks/contains
1893              
1894             $spotify->check_users_saved_tracks(['4iV5W9uYEdYUVa79Axb7Rh', '1301WleyT98MSxVHPZCA6M']);
1895              
1896             =head2 check_users_saved_shows
1897              
1898             equivalent to GET /v1/me/shows/contains
1899              
1900             $spotify->check_users_saved_shows(['5CfCWKI5pZ28U0uOzXkDHe', '5as3aKmN2k11yfDDDSrvaZ']);
1901              
1902             or
1903              
1904             $spotify->check_users_saved_shows('5CfCWKI5pZ28U0uOzXkDHe,5as3aKmN2k11yfDDDSrvaZ');
1905              
1906             This method checks if one or more shows are already saved in the current Spotify user's library.
1907              
1908             =head2 get_several_tracks_audio_features
1909              
1910             equivalent to /v1/audio-features
1911              
1912             $spotify->get_several_tracks_audio_features(['4iV5W9uYEdYUVa79Axb7Rh', '1301WleyT98MSxVHPZCA6M']);
1913              
1914             =head2 get_track_audio_features
1915              
1916             equivalent to /v1/audio-features/{id}
1917              
1918             $spotify->get_track_audio_features('4iV5W9uYEdYUVa79Axb7Rh');
1919              
1920             =head2 get_track_audio_analysis
1921              
1922             equivalent to /v1/audio-analysis/{id}
1923              
1924             $spotify->get_track_audio_analysis('4iV5W9uYEdYUVa79Axb7Rh');
1925              
1926             =head2 get_recommendations
1927              
1928             equivalent to /v1/recommendations
1929              
1930             $spotify->get_recommendations(
1931             seed_artists => '4NHQUGzhtTLFvgF5SZesLK',
1932             seed_genres => 'classical,country',
1933             seed_tracks => '0c6xIDDpzE81m2q797ordA',
1934             limit => 10,
1935             market => 'ES'
1936             );
1937              
1938             =head2 get_followed_artists
1939              
1940             equivalent to /v1/me/following
1941              
1942             $spotify->get_followed_artists(
1943             limit => 20,
1944             after => '0I2XqVXqHScXjHhk6AYYRe'
1945             );
1946              
1947             Note: This method always sets the 'type' parameter to 'artist' as it's the only supported value.
1948              
1949             =head2 follow_artists_or_users
1950              
1951             equivalent to PUT /v1/me/following
1952              
1953             $spotify->follow_artists_or_users('artist', ['2CIMQHirSU0MQqyYHq0eOx', '57dN52uHvrHOxijzpIgu3E']);
1954              
1955             or
1956              
1957             $spotify->follow_artists_or_users('user', '2CIMQHirSU0MQqyYHq0eOx,57dN52uHvrHOxijzpIgu3E');
1958              
1959             =head2 unfollow_artists_or_users
1960              
1961             equivalent to DELETE /v1/me/following
1962              
1963             $spotify->unfollow_artists_or_users('artist', ['2CIMQHirSU0MQqyYHq0eOx', '57dN52uHvrHOxijzpIgu3E']);
1964              
1965             or
1966              
1967             $spotify->unfollow_artists_or_users('user', '2CIMQHirSU0MQqyYHq0eOx,57dN52uHvrHOxijzpIgu3E');
1968              
1969             =head2 check_if_user_follows_artists_or_users
1970              
1971             equivalent to GET /v1/me/following/contains
1972              
1973             $spotify->check_if_user_follows_artists_or_users('artist', ['2CIMQHirSU0MQqyYHq0eOx', '57dN52uHvrHOxijzpIgu3E']);
1974              
1975             or
1976              
1977             $spotify->check_if_user_follows_artists_or_users('user', '2CIMQHirSU0MQqyYHq0eOx,57dN52uHvrHOxijzpIgu3E');
1978              
1979             =head2 check_if_user_follows_playlist
1980              
1981             equivalent to GET /v1/playlists/{playlist_id}/followers/contains
1982              
1983             $spotify->check_if_user_follows_playlist('3cEYpjA9oz9GiPac4AsH4n', 'jmperezperez');
1984              
1985             or
1986              
1987             $spotify->check_if_user_follows_playlist('3cEYpjA9oz9GiPac4AsH4n', ['jmperezperez']);
1988              
1989             =head2 get_audiobook
1990              
1991             equivalent to GET /v1/audiobooks/{id}
1992              
1993             $spotify->get_audiobook('7iHfbu1YPACw6oZPAFJtqe');
1994              
1995             or with market parameter:
1996              
1997             $spotify->get_audiobook('7iHfbu1YPACw6oZPAFJtqe', 'US');
1998              
1999             =head2 get_users_saved_audiobooks
2000              
2001             equivalent to GET /v1/me/audiobooks
2002              
2003             $spotify->get_users_saved_audiobooks(20, 0);
2004              
2005             =head2 remove_users_saved_audiobooks
2006              
2007             equivalent to DELETE /v1/me/audiobooks
2008              
2009             $spotify->remove_users_saved_audiobooks(['18yVqkdbdRvS24c0Ilj2ci', '1HGw3J3NxZO1TP1BTtVhpZ']);
2010              
2011             or
2012              
2013             $spotify->remove_users_saved_audiobooks('18yVqkdbdRvS24c0Ilj2ci,1HGw3J3NxZO1TP1BTtVhpZ');
2014              
2015             This method removes one or more audiobooks from the current user's library.
2016              
2017             =head2 get_available_genre_seeds
2018              
2019             equivalent to GET /v1/recommendations/available-genre-seeds
2020              
2021             $spotify->get_available_genre_seeds();
2022              
2023             This method retrieves a list of available genres seed parameter values for recommendations.
2024              
2025             =head2 get_available_markets
2026              
2027             equivalent to GET /v1/markets
2028              
2029             $spotify->get_available_markets();
2030              
2031             This method retrieves the list of markets where Spotify is available.
2032              
2033             =head2 get_show
2034              
2035             equivalent to GET /v1/shows/{id}
2036              
2037             $spotify->get_show('38bS44xjbVVZ3No3ByF1dJ', 'US');
2038              
2039             This method retrieves Spotify catalog information for a single show identified by its unique Spotify ID.
2040              
2041             =head2 get_several_shows
2042              
2043             equivalent to GET /v1/shows
2044              
2045             $spotify->get_several_shows(['5CfCWKI5pZ28U0uOzXkDHe', '5as3aKmN2k11yfDDDSrvaZ'], 'US');
2046              
2047             or
2048              
2049             $spotify->get_several_shows('5CfCWKI5pZ28U0uOzXkDHe,5as3aKmN2k11yfDDDSrvaZ', 'US');
2050              
2051             This method retrieves Spotify catalog information for several shows based on their Spotify IDs.
2052              
2053             =head2 get_show_episodes
2054              
2055             equivalent to GET /v1/shows/{id}/episodes
2056              
2057             $spotify->get_show_episodes('38bS44xjbVVZ3No3ByF1dJ', market => 'US', limit => 10, offset => 5);
2058              
2059             This method retrieves Spotify catalog information about a show's episodes. Optional parameters can be used to limit the number of episodes returned.
2060              
2061             =head2 get_audiobook_chapters
2062              
2063             equivalent to GET /v1/audiobooks/{id}/chapters
2064              
2065             $spotify->get_audiobook_chapters('3ZXb8FKZGU0EHALYX6uCzU', market => 'US', limit => 50, offset => 0);
2066              
2067             This method retrieves the chapters of an audiobook.
2068              
2069             =head2 get_several_audiobooks
2070              
2071             equivalent to GET /v1/audiobooks
2072              
2073             $spotify->get_several_audiobooks(['18yVqkdbdRvS24c0Ilj2ci', '1HGw3J3NxZO1TP1BTtVhpZ'], 'US');
2074              
2075             or
2076              
2077             $spotify->get_several_audiobooks('18yVqkdbdRvS24c0Ilj2ci,1HGw3J3NxZO1TP1BTtVhpZ', 'US');
2078              
2079             This method retrieves multiple audiobooks based on their Spotify IDs.
2080              
2081             =head2 send_delete_request
2082              
2083             Internal method used to send DELETE requests to the Spotify API.
2084              
2085             =head2 send_put_request
2086              
2087             Internal method used to send PUT requests to the Spotify API.
2088              
2089             =head2 check_users_saved_audiobooks
2090              
2091             equivalent to GET /v1/me/audiobooks/contains
2092              
2093             $spotify->check_users_saved_audiobooks(['18yVqkdbdRvS24c0Ilj2ci', '1HGw3J3NxZO1TP1BTtVhpZ']);
2094              
2095             or
2096              
2097             $spotify->check_users_saved_audiobooks('18yVqkdbdRvS24c0Ilj2ci,1HGw3J3NxZO1TP1BTtVhpZ');
2098              
2099             =head2 get_users_saved_shows
2100              
2101             equivalent to GET /v1/me/shows
2102              
2103             $spotify->get_users_saved_shows(limit => 20, offset => 0);
2104              
2105             This method retrieves a list of shows saved in the current Spotify user's library. Optional parameters can be used to limit the number of shows returned.
2106              
2107             =head2 save_shows_for_current_user
2108              
2109             equivalent to PUT /v1/me/shows
2110              
2111             $spotify->save_shows_for_current_user(['5CfCWKI5pZ28U0uOzXkDHe', '5as3aKmN2k11yfDDDSrvaZ']);
2112              
2113             or
2114              
2115             $spotify->save_shows_for_current_user('5CfCWKI5pZ28U0uOzXkDHe,5as3aKmN2k11yfDDDSrvaZ');
2116              
2117             This method saves one or more shows to the current user's library.
2118              
2119             =head2 get_categories
2120              
2121             equivalent to GET /v1/browse/categories
2122              
2123             $spotify->get_categories(
2124             country => 'US',
2125             locale => 'en_US',
2126             limit => 20,
2127             offset => 0
2128             );
2129              
2130             =head2 get_category
2131              
2132             equivalent to GET /v1/browse/categories/{category_id}
2133              
2134             $spotify->get_category('dinner', locale => 'en_US');
2135              
2136             =head2 get_chapter
2137              
2138             equivalent to GET /v1/chapters/{id}
2139              
2140             $spotify->get_chapter('0D5wENdkdwbqlrHoaJ9g29', market => 'US');
2141              
2142             =head2 get_several_chapters
2143              
2144             equivalent to GET /v1/chapters
2145              
2146             $spotify->get_several_chapters(['0IsXVP0JmcB2adSE338GkK', '3ZXb8FKZGU0EHALYX6uCzU', '0D5wENdkdwbqlrHoaJ9g29'], market => 'US');
2147              
2148             or
2149              
2150             $spotify->get_several_chapters('0IsXVP0JmcB2adSE338GkK,3ZXb8FKZGU0EHALYX6uCzU,0D5wENdkdwbqlrHoaJ9g29', market => 'US');
2151              
2152             =head2 save_audiobooks_for_current_user
2153              
2154             equivalent to PUT /v1/me/audiobooks
2155              
2156             $spotify->save_audiobooks_for_current_user(['18yVqkdbdRvS24c0Ilj2ci', '1HGw3J3NxZO1TP1BTtVhpZ']);
2157              
2158             or
2159              
2160             $spotify->save_audiobooks_for_current_user('18yVqkdbdRvS24c0Ilj2ci,1HGw3J3NxZO1TP1BTtVhpZ');
2161              
2162             This method saves one or more audiobooks to the current user's library.
2163              
2164             =head2 oauth_client_id
2165              
2166             needed for requests that require OAuth, see Spotify API documentation for more information
2167              
2168             $spotify->oauth_client_id('2xfjijkcjidjkfdi');
2169              
2170             Can also be set via environment variable, SPOTIFY_CLIENT_ID
2171              
2172             =head2 oauth_client_secret
2173              
2174             needed for requests that require OAuth, see Spotify API documentation for more information
2175              
2176             $spotify->oauth_client_secret('2xfjijkcjidjkfdi');
2177              
2178             Can also be set via environment variable, SPOTIFY_CLIENT_SECRET
2179              
2180             =head2 response_status
2181              
2182             returns the response code for the last request made
2183              
2184             my $status = $spotify->response_status();
2185              
2186             =head2 response_content_type
2187              
2188             returns the response type for the last request made, helpful to verify JSON
2189              
2190             my $content_type = $spotify->response_content_type();
2191              
2192             =head2 custom_request_handler
2193              
2194             pass a callback subroutine to this method that will be run at the end of the
2195             request prior to die_on_response_error, if enabled
2196              
2197             # $m is the WWW::Mechanize object
2198             $spotify->custom_request_handler(
2199             sub { my $m = shift;
2200             if ($m->status() == 401) {
2201             return 1;
2202             }
2203             }
2204             );
2205              
2206             =head2 custom_request_handler_result
2207              
2208             returns the result of the most recent execution of the custom_request_handler callback
2209             this allows you to determine the success/failure criteria of your callback
2210              
2211             my $callback_result = $spotify->custom_request_handler_result();
2212              
2213             =head2 die_on_response_error
2214              
2215             Boolean - default 0
2216              
2217             added to provide minimal automated checking of responses
2218              
2219             $spotify->die_on_response_error(1);
2220              
2221             eval {
2222             # run assuming you do NOT have proper authentication setup
2223             $result = $spotify->album('0sNOF9WDwhWunNAHPD3Baj');
2224             };
2225              
2226             if ($@) {
2227             warn $spotify->last_error();
2228             }
2229              
2230             =head2 last_error
2231              
2232             returns last_error (if applicable) from the most recent request.
2233             reset to empty string on each request
2234              
2235             print $spotify->last_error() , "\n";
2236              
2237             =head1 THANKS
2238              
2239             Paul Lamere at The Echo Nest / Spotify
2240              
2241             All the great Perl community members that keep Perl fun
2242              
2243             Olaf Alders for all his help and support in maintaining this module
2244              
2245             =head1 AUTHOR
2246              
2247             Aaron Johnson
2248              
2249             =head1 COPYRIGHT AND LICENSE
2250              
2251             This software is copyright (c) 2024 by Aaron Johnson.
2252              
2253             This is free software; you can redistribute it and/or modify it under
2254             the same terms as the Perl 5 programming language system itself.
2255              
2256             =cut
2257              
2258             __END__