File Coverage

blib/lib/WebService/GarminConnect.pm
Criterion Covered Total %
statement 28 89 31.4
branch 2 32 6.2
condition 2 7 28.5
subroutine 9 11 81.8
pod 2 2 100.0
total 43 141 30.5


line stmt bran cond sub pod time code
1             package WebService::GarminConnect;
2              
3 3     3   206812 use 5.006;
  3         46  
4 3     3   19 use warnings FATAL => 'all';
  3         6  
  3         110  
5 3     3   17 use strict;
  3         5  
  3         71  
6 3     3   16 use Carp;
  3         13  
  3         186  
7 3     3   2041 use LWP::UserAgent;
  3         150421  
  3         110  
8 3     3   27 use URI;
  3         8  
  3         67  
9 3     3   2067 use JSON;
  3         30443  
  3         19  
10 3     3   1730 use Data::Dumper;
  3         13010  
  3         2456  
11              
12             our $VERSION = '1.0.8'; # VERSION
13              
14             =head1 NAME
15              
16             WebService::GarminConnect - Access data from Garmin Connect
17              
18             =head1 VERSION
19              
20             version 1.0.8
21              
22             =head1 SYNOPSIS
23              
24             With WebService::GarminConnect, you can search the activities stored on the
25             Garmin Connect site.
26              
27             use WebService::GarminConnect;
28              
29             my $gc = WebService::GarminConnect->new( username => 'myuser',
30             password => 'password' );
31             my @activities = $gc->activities( limit => 20 );
32             foreach my $a ( @activities ) {
33             my $name = $a->{name};
34             ...
35             }
36              
37             =head1 FUNCTIONS
38              
39             =head2 new( %options )
40              
41             Creates a new WebService::GarminConnect object. One or more options may be
42             specified:
43              
44             =over
45              
46             =item username
47              
48             (Required) The Garmin Connect username to use for searches.
49              
50             =item password
51              
52             (Required) The user's Garmin Connect password.
53              
54             =item loginurl
55              
56             (Optional) Override the default login URL for Garmin Connect.
57              
58             =item searchurl
59              
60             (Optional) Override the default search URL for Garmin Connect.
61              
62             =back
63              
64             =cut
65              
66             sub new {
67 4     4 1 2040 my $self = shift;
68 4         13 my %options = @_;
69              
70             # Check for mandatory options
71 4         9 foreach my $required_option ( qw( username password ) ) {
72             croak "option \"$required_option\" is required"
73 6 100       353 unless defined $options{$required_option};
74             }
75              
76             return bless {
77             username => $options{username},
78             password => $options{password},
79             loginurl => $options{loginurl} || 'https://sso.garmin.com/sso/signin',
80 1   50     19 searchurl => $options{searchurl} || 'https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities',
      50        
81             }, $self;
82             }
83              
84             sub _login {
85 0     0     my $self = shift;
86              
87             # Bail out if we're already logged in.
88 0 0         return if defined $self->{is_logged_in};
89              
90 0           my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko');
91 0           $ua->cookie_jar( {} );
92 0           push @{ $ua->requests_redirectable }, 'POST';
  0            
93              
94             # Retrieve the login page
95 0           my %params = (
96             service => "https://connect.garmin.com/post-auth/login",
97             gauthHost => "https://sso.garmin.com/sso",
98             clientId => "GarminConnect",
99             consumeServiceTicket => "false",
100             );
101 0           my $uri = URI->new($self->{loginurl});
102 0           $uri->query_form(%params);
103 0           my $response = $ua->get($uri);
104 0 0         croak "Can't retrieve login page: " . $response->status_line
105             unless $response->is_success;
106              
107             # Get sso ticket
108 0           $uri = URI->new("https://sso.garmin.com/sso/login");
109 0           $uri->query_form(%params);
110             $response = $ua->post($uri, origin => 'https://sso.garmin.com',
111             content => {
112             username => $self->{username},
113             password => $self->{password},
114 0           _eventId => "submit",
115             embed => "true",
116             });
117 0 0         croak "Can't retrieve sso page: " . $response->status_line
118             unless $response->is_success;
119 0 0         if ($response->content =~ />sendEvent\('FAIL'\)/) {
120 0           croak "invalid login";
121             }
122 0 0         if ($response->content =~ />sendEvent\('ACCOUNT_LOCKED'\)/) {
123 0           croak "account locked";
124             }
125 0 0         if ($response->content =~ /renewPassword/) {
126 0           croak "renew password";
127             }
128 0 0         if ($response->content !~ /\?ticket=([^"]+)"/) {
129 0           croak "no service ticket in response";
130             }
131 0           my $ticket=$1;
132              
133             #$uri = URI->new('https://connect.garmin.com/post-auth/login?ticket=$1');
134 0           $uri = URI->new("https://connect.garmin.com/modern/?ticket=$ticket");
135 0           $response = $ua->get($uri);
136 0 0         croak "Can't retrieve post-auth page: " . $response->status_line
137             unless $response->is_success;
138              
139             # Record our logged-in status so future calls will skip login.
140 0           $self->{useragent} = $ua;
141 0           $self->{is_logged_in} = 1;
142             }
143              
144             =head2 activities( %search_criteria )
145              
146             Returns a list of activities matching the requested criteria. If no criteria
147             are specified, returns all the user's activities. Possible criteria:
148              
149             =over
150              
151             =item limit
152              
153             (Optional) The maximum number of activities to return. If not specified,
154             all the user's activities will be returned.
155              
156             =item pagesize
157              
158             (Optional) The number of activities to return in each call to Garmin
159             Connect. (One call to this subroutine may call Garmin Connect several
160             times to retrieve all the requested activities.) Defaults to 50.
161              
162             =back
163              
164             =cut
165              
166             sub activities {
167 0     0 1   my $self = shift;
168 0           my %opts = @_;
169 0           my $json = JSON->new();
170              
171             # Ensure we are logged in
172 0           $self->_login();
173 0           my $ua = $self->{useragent};
174              
175             # We can only fetch a fixed number of activities at a time.
176 0           my @activities;
177 0           my $start = 0;
178 0           my $pagesize = 50;
179 0 0         if( defined $opts{pagesize} ) {
180 0 0 0       if( $opts{pagesize} > 0 && $opts{pagesize} < 50 ) {
181 0           $pagesize = $opts{pagesize};
182             }
183             }
184              
185             # Special case when the limit is smaller than one page.
186 0 0         if( defined $opts{limit} ) {
187 0 0         if( $opts{limit} < $pagesize ) {
188 0           $pagesize = $opts{limit};
189             }
190             }
191              
192 0           my $data = [];
193             do {
194             # Make a search request
195             my $searchurl = $self->{searchurl} .
196 0           "?start=$start&limit=$pagesize";
197              
198 0           my $request = HTTP::Request->new(GET => $searchurl);
199 0           my $response = $ua->request($request);
200 0 0         croak "Can't make search request: " . $response->status_line
201             unless $response->is_success;
202              
203             # Parse the JSON search results
204 0           $data = $json->decode($response->content);
205              
206             # Add this set of activities to the list.
207 0           foreach my $activity ( @{$data} ) {
  0            
208 0 0         if( defined $opts{limit} ) {
209             # add this activity only if we're under the limit
210 0 0         if( @activities < $opts{limit} ) {
211 0           push @activities, { activity => $activity };
212             } else {
213 0           $data = []; # stop retrieving more activities
214 0           last;
215             }
216             } else {
217 0           push @activities, { activity => $activity };
218             }
219             }
220              
221             # Increment the start offset for the next request.
222 0           $start += $pagesize;
223              
224 0           } while( @{$data} > 0 );
  0            
225              
226 0           return @activities;
227             }
228              
229             =head1 AUTHOR
230              
231             Joel Loudermilk, C<< >>
232              
233             =head1 BUGS
234              
235             Please report any bugs or feature requests to L.
236              
237              
238              
239              
240             =head1 SUPPORT
241              
242             You can find documentation for this module with the perldoc command.
243              
244             perldoc WebService::GarminConnect
245              
246              
247             You can also look for information at:
248              
249             =over 4
250              
251             =item * AnnoCPAN: Annotated CPAN documentation
252              
253             L
254              
255             =item * CPAN Ratings
256              
257             L
258              
259             =item * Search CPAN
260              
261             L
262              
263             =item * GitHub Repository
264              
265             L
266              
267             =back
268              
269             =head1 COPYRIGHT & LICENSE
270              
271             Copyright 2015 Joel Loudermilk.
272              
273             This program is free software: you can redistribute it and/or modify
274             it under the terms of the GNU General Public License as published by
275             the Free Software Foundation, either version 3 of the License, or
276             (at your option) any later version.
277              
278             This program is distributed in the hope that it will be useful,
279             but WITHOUT ANY WARRANTY; without even the implied warranty of
280             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
281             GNU General Public License for more details.
282              
283             You should have received a copy of the GNU General Public License
284             along with this program. If not, see L.
285              
286             =cut
287              
288             1; # End of WebService::GarminConnect