File Coverage

blib/lib/WebService/GarminConnect.pm
Criterion Covered Total %
statement 28 90 31.1
branch 2 32 6.2
condition 2 7 28.5
subroutine 9 11 81.8
pod 2 2 100.0
total 43 142 30.2


line stmt bran cond sub pod time code
1             package WebService::GarminConnect;
2              
3 3     3   173150 use 5.006;
  3         29  
4 3     3   16 use warnings FATAL => 'all';
  3         5  
  3         105  
5 3     3   13 use strict;
  3         5  
  3         72  
6 3     3   16 use Carp;
  3         4  
  3         153  
7 3     3   1809 use LWP::UserAgent;
  3         116140  
  3         99  
8 3     3   25 use URI;
  3         7  
  3         55  
9 3     3   1834 use JSON;
  3         25831  
  3         15  
10 3     3   1368 use Data::Dumper;
  3         10688  
  3         2098  
11              
12             our $VERSION = '1.0.9'; # VERSION
13              
14             =head1 NAME
15              
16             WebService::GarminConnect - Access data from Garmin Connect
17              
18             =head1 VERSION
19              
20             version 1.0.9
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 1721 my $self = shift;
68 4         10 my %options = @_;
69              
70             # Check for mandatory options
71 4         8 foreach my $required_option ( qw( username password ) ) {
72             croak "option \"$required_option\" is required"
73 6 100       296 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     16 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 $headers = [
199             'NK' => 'NT',
200             ];
201 0           my $request = HTTP::Request->new('GET', $searchurl, $headers);
202 0           my $response = $ua->request($request);
203 0 0         croak "Can't make search request: " . $response->status_line
204             unless $response->is_success;
205              
206             # Parse the JSON search results
207 0           $data = $json->decode($response->content);
208              
209             # Add this set of activities to the list.
210 0           foreach my $activity ( @{$data} ) {
  0            
211 0 0         if( defined $opts{limit} ) {
212             # add this activity only if we're under the limit
213 0 0         if( @activities < $opts{limit} ) {
214 0           push @activities, { activity => $activity };
215             } else {
216 0           $data = []; # stop retrieving more activities
217 0           last;
218             }
219             } else {
220 0           push @activities, { activity => $activity };
221             }
222             }
223              
224             # Increment the start offset for the next request.
225 0           $start += $pagesize;
226              
227 0           } while( @{$data} > 0 );
  0            
228              
229 0           return @activities;
230             }
231              
232             =head1 AUTHOR
233              
234             Joel Loudermilk, C<< >>
235              
236             =head1 BUGS
237              
238             Please report any bugs or feature requests to L.
239              
240              
241              
242              
243             =head1 SUPPORT
244              
245             You can find documentation for this module with the perldoc command.
246              
247             perldoc WebService::GarminConnect
248              
249              
250             You can also look for information at:
251              
252             =over 4
253              
254             =item * AnnoCPAN: Annotated CPAN documentation
255              
256             L
257              
258             =item * CPAN Ratings
259              
260             L
261              
262             =item * Search CPAN
263              
264             L
265              
266             =item * GitHub Repository
267              
268             L
269              
270             =back
271              
272             =head1 COPYRIGHT & LICENSE
273              
274             Copyright 2015 Joel Loudermilk.
275              
276             This program is free software: you can redistribute it and/or modify
277             it under the terms of the GNU General Public License as published by
278             the Free Software Foundation, either version 3 of the License, or
279             (at your option) any later version.
280              
281             This program is distributed in the hope that it will be useful,
282             but WITHOUT ANY WARRANTY; without even the implied warranty of
283             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
284             GNU General Public License for more details.
285              
286             You should have received a copy of the GNU General Public License
287             along with this program. If not, see L.
288              
289             =cut
290              
291             1; # End of WebService::GarminConnect