| 1 | #!/usr/bin/perl |
|---|
| 2 | |
|---|
| 3 | package FlickrFetcher; |
|---|
| 4 | |
|---|
| 5 | use Moose; |
|---|
| 6 | use Moose::Util::TypeConstraints; |
|---|
| 7 | use Params::Coerce (); |
|---|
| 8 | |
|---|
| 9 | use Digest::MD5 qw(md5_hex); |
|---|
| 10 | use Encode; |
|---|
| 11 | use LWP::UserAgent; |
|---|
| 12 | use Path::Class; |
|---|
| 13 | use POSIX qw(ceil); |
|---|
| 14 | use WebService::Simple; |
|---|
| 15 | use WebService::Simple::Parser::XML::Simple; |
|---|
| 16 | use XML::Simple; |
|---|
| 17 | use Perl6::Say; |
|---|
| 18 | |
|---|
| 19 | our $VERSION = '0.01'; |
|---|
| 20 | |
|---|
| 21 | with 'MooseX::Getopt'; |
|---|
| 22 | |
|---|
| 23 | subtype 'Dir' => as 'Object' => where { $_->isa('Path::Class::Dir') }; |
|---|
| 24 | coerce 'Dir' => from 'Str' => via { Path::Class::Dir->new($_) }; |
|---|
| 25 | |
|---|
| 26 | MooseX::Getopt::OptionTypeMap->add_option_type_to_map( 'Dir' => '=s' ); |
|---|
| 27 | |
|---|
| 28 | has 'keyword' => ( is => 'rw', isa => 'Str', required => 1 ); |
|---|
| 29 | has 'dir' => ( is => 'rw', isa => 'Dir', required => 1, coerce => 1 ); |
|---|
| 30 | has 'api_key' => ( is => 'rw', isa => 'Str' ); |
|---|
| 31 | has 'license' => ( is => 'rw', isa => 'Int' ); |
|---|
| 32 | has '_perpage' => ( is => 'ro', isa => 'Int', default => 500 ); |
|---|
| 33 | has '_flickr' => ( is => 'rw', isa => 'WebService::Simple' ); |
|---|
| 34 | has '_ua' => ( |
|---|
| 35 | is => 'ro', |
|---|
| 36 | isa => 'LWP::UserAgent', |
|---|
| 37 | default => sub { LWP::UserAgent->new( keep_alive => 1 ) } |
|---|
| 38 | ); |
|---|
| 39 | |
|---|
| 40 | sub BUILD { |
|---|
| 41 | my ( $self, $args ) = @_; |
|---|
| 42 | |
|---|
| 43 | unless ( $self->api_key ) { |
|---|
| 44 | if ( my $api_key = $ENV{FLICKR_API_KEY} ) { |
|---|
| 45 | $self->api_key($api_key); |
|---|
| 46 | } |
|---|
| 47 | else { |
|---|
| 48 | die "api_key is required\n"; |
|---|
| 49 | } |
|---|
| 50 | } |
|---|
| 51 | |
|---|
| 52 | my $xs = XML::Simple->new( KeepRoot => 1, keyattr => [] ); |
|---|
| 53 | my $parser = WebService::Simple::Parser::XML::Simple->new( xs => $xs ); |
|---|
| 54 | my $flickr = WebService::Simple->new( |
|---|
| 55 | base_url => "http://api.flickr.com/services/rest/", |
|---|
| 56 | param => { api_key => $self->api_key }, |
|---|
| 57 | response_parser => $parser, |
|---|
| 58 | ); |
|---|
| 59 | $self->_flickr($flickr); |
|---|
| 60 | } |
|---|
| 61 | |
|---|
| 62 | __PACKAGE__->meta->make_immutable; |
|---|
| 63 | no Moose; |
|---|
| 64 | |
|---|
| 65 | sub run { |
|---|
| 66 | my $self = shift; |
|---|
| 67 | mkdir $self->dir->relative if !-d $self->dir->is_absolute; |
|---|
| 68 | say "search keyword : " . $self->keyword; |
|---|
| 69 | my $photo_total = $self->photo_total( $self->keyword ); |
|---|
| 70 | say "total count : " . $photo_total; |
|---|
| 71 | my $pages = ceil( $photo_total / $self->_perpage ); |
|---|
| 72 | for my $current_page ( 1 .. $pages ) { |
|---|
| 73 | say "search page : $current_page"; |
|---|
| 74 | $self->search( $self->keyword, $current_page, $self->_perpage ); |
|---|
| 75 | } |
|---|
| 76 | } |
|---|
| 77 | |
|---|
| 78 | sub search { |
|---|
| 79 | my ( $self, $keyword, $page , $perpage) = @_; |
|---|
| 80 | my $response = $self->_flickr->get( |
|---|
| 81 | { |
|---|
| 82 | method => "flickr.photos.search", |
|---|
| 83 | text => $keyword, |
|---|
| 84 | per_page => $perpage, |
|---|
| 85 | sort => 'date-posted-desc', |
|---|
| 86 | extras => 'date_upload', |
|---|
| 87 | page => $page, |
|---|
| 88 | license => $self->license || "", |
|---|
| 89 | } |
|---|
| 90 | ); |
|---|
| 91 | my $xml = $response->parse_response; |
|---|
| 92 | $self->fetch($xml->{rsp}->{photos}->{photo}); |
|---|
| 93 | } |
|---|
| 94 | |
|---|
| 95 | sub fetch { |
|---|
| 96 | my ( $self, $photo_ref ) = @_; |
|---|
| 97 | for my $photo ( @$photo_ref ){ |
|---|
| 98 | my $url = $self->photo_url( $photo->{id} ); |
|---|
| 99 | my $file = $self->dir->file( md5_hex($url) . ".jpg" ); |
|---|
| 100 | my $res = $self->_ua->mirror( $url, $file ); |
|---|
| 101 | say "try to fetch : " . $res->status_line . " : $url"; |
|---|
| 102 | } |
|---|
| 103 | } |
|---|
| 104 | |
|---|
| 105 | sub photo_url { |
|---|
| 106 | my ( $self, $photo_id ) = @_; |
|---|
| 107 | my $response = $self->_flickr->get( |
|---|
| 108 | { |
|---|
| 109 | method => "flickr.photos.getSizes", |
|---|
| 110 | photo_id => $photo_id |
|---|
| 111 | } |
|---|
| 112 | ); |
|---|
| 113 | my $xml = $response->parse_response; |
|---|
| 114 | my $largest_ref = pop @{ $xml->{rsp}->{sizes}->{size} }; |
|---|
| 115 | return $largest_ref->{source}; |
|---|
| 116 | } |
|---|
| 117 | |
|---|
| 118 | sub photo_total { |
|---|
| 119 | my ( $self, $keyword ) = @_; |
|---|
| 120 | my $response = $self->_flickr->get( |
|---|
| 121 | { |
|---|
| 122 | method => "flickr.photos.search", |
|---|
| 123 | text => $keyword, |
|---|
| 124 | per_page => 1, |
|---|
| 125 | license => $self->license || "", |
|---|
| 126 | } |
|---|
| 127 | ); |
|---|
| 128 | my $xml = $response->parse_response; |
|---|
| 129 | return $xml->{rsp}->{photos}->{total}; |
|---|
| 130 | } |
|---|
| 131 | |
|---|
| 132 | package main; |
|---|
| 133 | |
|---|
| 134 | my $fetcher = FlickrFetcher->new_with_options(); |
|---|
| 135 | $fetcher->run(); |
|---|
| 136 | |
|---|
| 137 | __END__ |
|---|
| 138 | |
|---|
| 139 | =head1 NAME |
|---|
| 140 | |
|---|
| 141 | flickr_fetcher.pl - Fetch Flickr photos by keyword |
|---|
| 142 | |
|---|
| 143 | =head1 SYNOPSIS |
|---|
| 144 | |
|---|
| 145 | ./flickr_fetcher.pl --keyword hoge --dir hoge --api_key yourflickrapikey |
|---|
| 146 | |
|---|
| 147 | =head1 AUTHOR |
|---|
| 148 | |
|---|
| 149 | Yusuke Wada E<lt>yusuke (at) kamawada.comE<gt> |
|---|
| 150 | |
|---|
| 151 | =head1 LICENSE |
|---|
| 152 | |
|---|
| 153 | This library is free software; you can redistribute it and/or modify |
|---|
| 154 | it under the same terms as Perl itself. |
|---|
| 155 | |
|---|
| 156 | =cut |
|---|
| 157 | |
|---|