File Coverage

blib/lib/WWW/Eksi.pm
Criterion Covered Total %
statement 88 124 70.9
branch 8 32 25.0
condition 9 43 20.9
subroutine 16 26 61.5
pod 4 4 100.0
total 125 229 54.5


line stmt bran cond sub pod time code
1             package WWW::Eksi;
2             $WWW::Eksi::VERSION = '0.31';
3             =head1 NAME
4              
5             WWW::Eksi - Interface for Eksisozluk.com
6              
7             =head1 DESCRIPTION
8              
9             An interface for Eksisozluk, a Turkish social network.
10             Provides easy access to entries and lists of entries.
11              
12             =head1 SYNOPSIS
13              
14             use WWW::Eksi;
15             my $e = WWW::Eksi->new;
16              
17             # Last week's most popular entries
18             my @ghebe_fast = $e->ghebe; # might get rate limited
19             my @ghebe_slow = $e->ghebe(5); # add a politeness delay
20              
21             # Yesterday's most popular entries
22             my @debe_fast = $e->debe; # might get rate limited
23             my @debe_slow = $e->debe(5); # add a politeness delay
24              
25             # Single entry
26             my $entry = $e->download_entry(1);
27              
28             =cut
29              
30 2     2   960363 use warnings;
  2         9  
  2         78  
31 2     2   13 use strict;
  2         3  
  2         43  
32 2     2   11 use Carp;
  2         4  
  2         125  
33 2     2   13 use List::Util qw/any/;
  2         4  
  2         109  
34              
35 2     2   539 use URI;
  2         4531  
  2         59  
36 2     2   453 use Furl;
  2         25034  
  2         60  
37 2     2   551 use Mojo::DOM;
  2         177416  
  2         78  
38 2     2   531 use WWW::Lengthen;
  2         40726  
  2         61  
39 2     2   1694 use IO::Socket::SSL;
  2         134379  
  2         21  
40              
41 2     2   1353 use DateTime;
  2         472957  
  2         90  
42 2     2   856 use DateTime::Format::Strptime;
  2         55582  
  2         19  
43              
44             =head1 METHODS
45              
46             =head2 new
47              
48             Returns a new WWW::Eksi object.
49              
50             =cut
51              
52             sub new {
53 1     1 1 147 my $class = shift;
54 1         10 my $today = DateTime->now->ymd;
55              
56 1         613 my $eksi = {
57             base => 'https://eksisozluk.com',
58             entry => 'https://eksisozluk.com/entry/',
59             debe => 'https://eksisozluk.com/istatistik/dunun-en-begenilen-entryleri',
60             ghebe => 'https://eksisozluk.com/istatistik/gecen-haftanin-en-begenilen-entryleri',
61             strp_dt => DateTime::Format::Strptime->new( pattern => '%d.%m.%Y%H:%M'),
62             strp_d => DateTime::Format::Strptime->new( pattern => '%d.%m.%Y'),
63             };
64              
65 1         3000 return bless $eksi, $class;
66             }
67              
68             =head2 download_entry($id)
69              
70             Takes entry id as argument, returns its data (if available) as follows.
71              
72             {
73             entry_url => Str
74             topic_url => Str
75             topic_title => Str
76             topic_channels => [Str]
77              
78             author_name => Str
79             author_url => Str
80             author_id => Int
81              
82             body_raw => Str
83             body_text => Str (html tags removed)
84             body_processed => Str (html tags processed)
85             fav_count => Int
86             create_time => DateTime
87             update_time => DateTime
88             }
89              
90             =cut
91              
92             sub download_entry {
93 1     1 1 676 my ($self,$id) = @_;
94 1 50 33     21 my $data = $self->_download($self->{entry}.$id) if ($id && $id=~/^\d{1,}$/);
95 1 50       8 return unless $data;
96 1         4 return $self->_parse_entry($data,$id);
97             }
98              
99             sub _parse_entry {
100 1     1   4 my ($self,$data, $id) = @_;
101 1 50       4 return unless $data;
102              
103 1         3 my $e = {};
104 1         9 my $dom = Mojo::DOM->new($data);
105              
106 1 50       1704 unless ($id){
107 0         0 $id = $dom->at('a[class~=entry-date]')->{href};
108 0         0 $id =~ s/[^\d]//g;
109 0 0 0     0 return unless ($id && $id=~/^\d{1,}$/);
110             }
111              
112             # entry_url
113 1         7 $e->{entry_url} = $self->{entry}.$id;
114              
115             # body_raw, body_text, body_processed
116 1         5 $e->{body_raw} = $dom->at('div[class=content]')->content;
117 1         911 $e->{body_text} = $dom->at('div[class=content]')->text;
118 1         439 $e->{body_processed} = $self->_process_entry($e->{body_raw});
119              
120              
121             # time_as_seen, create_time, update_time
122 1         5 my $time_as_seen = $dom->at('a[class~=entry-date]')->text;
123 1         519 $e->{time_as_seen} = $time_as_seen;
124              
125 1         9 $time_as_seen =~/
126             ^
127             \s*
128             (?<date_posted>\d\d\.\d\d\.\d{4})
129             \s*
130             (?<time_posted>\d\d:\d\d)? #old entries lack time
131             ( # update block
132             \s*
133             ~
134             \s*
135             (?<date_updated>\d\d\.\d\d\.\d{4})?
136             # date won't be shown if updated on the same day
137             \s*
138             (?<time_updated>\d\d:\d\d)?
139             )? # will not exist if not updated
140             \s*
141             $
142             /x;
143              
144 2   50 2   2133 my $date_posted = $+{date_posted} // '';
  2         877  
  2         2640  
  1         16  
145 1   50     12 my $time_posted = $+{time_posted} // '';
146 1   50     10 my $date_updated = $+{date_updated} // '';
147 1   50     7 my $time_updated = $+{time_updated} // '';
148              
149 1 50       4 Carp::croak "Entry date could not be found" unless $date_posted;
150              
151             $e->{create_time} = $time_posted
152             ? $self->{strp_dt}->parse_datetime($date_posted.$time_posted)
153 1 50       10 : $self->{strp_d}->parse_datetime($date_posted);
154             $e->{update_time} = $time_updated
155             ? $self->{strp_dt}->parse_datetime(
156 1 50 0     982 ($date_updated || $date_posted).$time_updated)
157             : '';
158              
159              
160             # author_name, author_url, author_id, fav_count
161 1         7 my $li_data_id_entry = $dom->at("li[data-id=$id]");
162 1         398 my $a_entry_author = $dom->at('a[class=entry-author]');
163 1   33     526 $e->{author_name} = $li_data_id_entry->{"data-author"}
164             // $a_entry_author->text;
165 1         28 $e->{author_url} = $self->{base}.$a_entry_author->{href};
166 1   50     17 $e->{author_id} = $li_data_id_entry->{"data-author-id"} // 0;
167 1   50     20 $e->{fav_count} = $li_data_id_entry->{"data-favorite-count"} // 0;
168              
169              
170             # topic_channels
171 1   50     20 my $channels_text = $dom->at('section[id=hidden-channels]')->text // 0;
172 1         490 $channels_text =~s/^\s*//;
173 1         7 $channels_text =~s/\s*$//;
174 1         5 my @channels = split ',',$channels_text;
175 1         3 $e->{topic_channels} = \@channels;
176              
177              
178             # topic_title, topic_url
179 1         4 my $h1_id_title = $dom->at('h1[id=title]');
180 1         296 $e->{topic_title} = $h1_id_title->{'data-title'};
181 1         18 $e->{topic_url} = $self->{base}.$h1_id_title->at('a')->{href};
182              
183 1         190 return $e;
184             }
185              
186             =head2 ghebe($politeness_delay)
187              
188             Returns an array of entries for top posts of last week.
189             Ordered from more popular to less popular.
190              
191             =cut
192              
193             sub ghebe {
194 0     0 1 0 my ($self, $sleep_seconds) = @_;
195 0         0 return $self->_get_list($sleep_seconds,$self->{ghebe});
196             }
197              
198             =head2 debe($politeness_delay)
199              
200             Returns an array of entries for top posts of yesterday.
201             Ordered from more popular to less popular.
202              
203             =cut
204              
205             sub debe {
206 0     0 1 0 my ($self, $sleep_seconds) = @_;
207 0         0 return $self->_get_list($sleep_seconds,$self->{debe});
208             }
209              
210             sub _get_list {
211 0     0   0 my ($self, $sleep_seconds, $url) = @_;
212 0   0     0 $sleep_seconds //= 0;
213 0         0 my $data = $self->_download($url);
214 0 0       0 return unless $data;
215              
216 0         0 my $dom = Mojo::DOM->new($data);
217 0         0 my $links = $dom->at('ol[class~=stats]')->find('a');
218 0     0   0 my $ids = $links->map(sub{$_->{href}=~m/%23(\d+)$/})->to_array;
  0         0  
219 0         0 my @entries = ();
220              
221 0         0 foreach my $id (@$ids){
222 0         0 my $entry = $self->download_entry($id);
223 0         0 push @entries, $entry;
224 0         0 sleep $sleep_seconds
225             }
226              
227 0         0 return @entries;
228             }
229              
230             sub _download {
231 0     0   0 my ($self,$url) = @_;
232              
233 0 0       0 my $u = URI->new($url) if $url;
234 0 0 0 0   0 return 0 unless ($url && $u && (any {$u->scheme eq $_} qw/http https/));
  0   0     0  
235              
236 0         0 my $response = Furl->new->get($u);
237              
238 0 0 0     0 return ($response && $response->is_success)
239             ? $response->content
240             : 0;
241             }
242              
243             sub _lengthen {
244 0     0   0 my ($self, $url) = @_;
245              
246 0 0       0 my $u = URI->new($url) if $url;
247 0 0 0 0   0 return 0 unless ($url && $u && (any {$u->scheme eq $_} qw/http https/));
  0   0     0  
248              
249 0         0 my $lengthener = WWW::Lenghten->new;
250              
251 0 0   0   0 return (any {$u->host eq $_} qw/is.gd goo.gl/)
  0         0  
252             ? $lengthener->try($u)
253             : $u;
254             }
255              
256             sub _process_entry {
257 1     1   3 my ($self,$e) = @_;
258 1 50       4 return unless $e;
259              
260             # Expand goo.gl and is.gd links
261 1         4 $e=~s/href="(https?:\/\/(goo\.gl|is\.gd)[^"]*)"/"href=\""._lengthen($1)."\""/ieg;
  0         0  
262              
263             # Make hidden references (akıllı bkz) visible
264 1         3 $e=~s/(<sup class="ab"><a data-query=")([^"]*)("[^<>]*>)\*/$1$2$3* ($2)/g;
265              
266             # Make local links global
267 1         3 $e=~s/href="\//target="_blank" href="https:\/\/eksisozluk.com\//g;
268              
269             # Force no decoration to disable underline in Gmail
270 1         2 $e=~s/href="/style="text-decoration:none;" href="/g;
271              
272             # Add JPG to imgur images with no extension
273 1         5 $e=~s/(href="https?:\/\/[^.]*\.?imgur.com\/\w{7})"/$1\.jpg"/g;
274              
275             # Make JPG/PNG images visible
276 1         3 $e=~s/(href="([^"]*\.(jpe?g|png)(:large)?)"[^<]*<\/a>)/$1<br><br><img src="$2"><br><br>/g;
277              
278             # Add NW arrow to external links
279 1         3 $e=~s/(https?:\/\/(?!eksisozluk.com)([^\/<]*\.[^\/<]*)[^<]*<\/a>)/$1 \($2 &#8599;\)/g;
280              
281 1         3 return $e;
282              
283             }
284              
285             sub _entry_not_found {
286              
287             return {
288 0     0     topic_title => '?',
289             topic_url => '?',
290             topic_channels => [],
291             author_name => '?',
292             author_id => 0,
293             body_raw => "<i>bu entry silinmi&#351;.</i>",
294             body_text => "bu entry silinmi&#351;.",
295             body_processed => "<i>bu entry silinmi&#351;.</i>",
296             fav_count => '?',
297             create_time => 0,
298             update_time => 0,
299             };
300             }
301              
302             1;
303              
304             __END__
305              
306             =head1 AUTHOR
307              
308             Kivanc Yazan C<< <kyzn at cpan.org> >>
309              
310             =head1 CONTRIBUTORS
311              
312             Mohammad S Anwar, C<< <mohammad.anwar at yahoo.com> >>
313              
314             =head1 COPYRIGHT AND LICENSE
315              
316             This software is copyright (c) 2017 by Kivanc Yazan.
317              
318             This is free software; you can redistribute it and/or modify it under
319             the same terms as the Perl 5 programming language system itself.
320              
321             Content you reach by using this module might be subject to copyright
322             terms of Eksisozluk. See eksisozluk.com for details.
323              
324             =cut