Category: | Web Stuff |
Author/Contact Info | Flavio Poletti <flavio AT polettix.it> |
Description: | A simple upload script for Zooomr, an online photo album service that will hopefully reach Flickr standards. The script contains full documentation in POD, but you will be able to do the following:Happy uploading! You can also save your recurrent options inside a configuration file .zooomr inside your home directory. Update: added more features and checks for good uploads vs fake ones (Zooomr sometimes simply seems to throw an upload in the dust bin). Update: added support for browser cookie file, managing external authentication via browser (needed for OpenID accounts). Added some missing documentation. Update: added support for summary file at the end of upload, to report managed files and their URIs (see this post in Zooomr groups). Update: changed upload URI to conform to current non-flash upload URI. |
#!/usr/bin/env perl use strict; use warnings; use Carp; use Pod::Usage qw( pod2usage ); use Getopt::Long qw( :config gnu_getopt ); use version; my $VERSION = qv('0.3.0'); use English qw( -no_match_vars ); use WWW::Mechanize; use IO::Prompt qw( prompt ); use File::Slurp qw( slurp ); use Config::Tiny; use Path::Class qw( file ); use List::MoreUtils qw( uniq ); # Integrated logging facility use Log::Log4perl qw( :easy :no_extra_logdie_message ); Log::Log4perl->easy_init($INFO); my %defaults = ( config => file($ENV{HOME}, '.zooomr')->stringify(), login_page => 'http://www.zooomr.com/login/', upload_page => 'http://www.zooomr.com/photos/upload/?noflash=okiwil +l', logout_page => 'http://www.zooomr.com/logout/', search_page => 'http://www.zooomr.com/search/photos/', login => 1, # by default, try to login pause => 5, backoff => 5, max_retry => 4, debug => $INFO, ); # Script implementation here my %config = get_configuration(%defaults); if ($config{check}) { print {*STDERR} "configuration OK\n"; exit 0; } get_logger()->level( { TRACE => $TRACE, DEBUG => $DEBUG, INFO => $INFO, WARN => $WARN, ERROR => $ERROR, FATAL => $FATAL, }->{uc $config{debug}} || $INFO ); INFO 'configuration OK'; # On with the show my $ua = WWW::Mechanize->new(autocheck => 1); eval { my $action = __PACKAGE__->can($config{action}) or die "You've found a bug! Someway, $config{action} is not supported\n" +; $ua->env_proxy(); $ua->proxy('http', $config{proxy}) if exists $config{proxy}; if ($config{login}) { if ($config{cookie}) { INFO "getting login info from cookie file '$config{cookie}'"; require HTTP::Cookies::Netscape; my $jar = HTTP::Cookies::Netscape->new(autosave => 0); $jar->load($config{cookie}) or die "could not load cookie file $config{cookie}\n"; $ua->cookie_jar($jar); } else { INFO "logging into account $config{username}"; login(); } } $action->(); if ($config{login} && !$config{cookie}) { INFO "logging out"; logout(); } }; ERROR $EVAL_ERROR if $EVAL_ERROR; INFO 'all operations completed'; sub get_configuration { my %config = @_; my $action = shift @ARGV; $action = '--usage' unless defined $action; # First of all, try to honor meta-options pod2usage(message => "$0 $VERSION", -verbose => 99, -sections => '' +) if $action eq '--version'; pod2usage(-verbose => 99, -sections => 'USAGE') if $action eq '--us +age'; pod2usage(-verbose => 99, -sections => 'USAGE|EXAMPLES|OPTIONS') if $action eq '--help'; pod2usage(-verbose => 2) if $action eq '--man'; # getting here means that it's a real call for something my @common_options = qw( username|user|u=s password|pass|p=s proxy|P=s cookie|cookie-file| +k=s config|C=s check|dry-run|c! dump|d! summary|summary-file|s=s tag|t=s@ debug|D=s ); my %options_for = ( add => [ qw( files|files-from|f=s@ resize=i public! private! family! friends! backoff=i pause=i max_retry|max-retry|m=i ) ], search => [qw( login! maxpage|max-page|m=i who|w=s )], ); pod2usage( message => "error: operation '$action' not supported\n", -verbose => 99, -sections => 'USAGE' ) unless exists $options_for{$action}; # This will record the original parameters from different sources my %sources = (_script => {%config}); my %cmdline; GetOptions(\%cmdline, @common_options, @{$options_for{$action}}); %config = (%config, %cmdline); # First merge, cmdline overrides $sources{_cmdline} = \%cmdline; # Read defaults from configuration file, if exists if (-e $config{config}) { my $cfile = Config::Tiny->read($config{config}); %config = (%config, %{$cfile->{_}}, %cmdline); $sources{_cfile} = $cfile; } ## end if (-e $config{config}) # Record original options for validators to make changes and action %config = (%config, %sources, action => $action, argv => [@ARGV]); @ARGV = (); # empty, already recorded in config # Call relevant method for parameter checking, if exists if (my $sub = __PACKAGE__->can('validate_' . $action)) { $sub->(\%config); } if ($config{dump}) { require Data::Dumper; local $Data::Dumper::Indent; local $Data::Dumper::Indent = 1; print {*STDOUT} Data::Dumper->Dump([\%config], ['configuration'] +); } ## end if ($config{dump}) # Now check/adjust common parameters push @{$config{tag}}, $config{tags} if defined $config{tags}; $config{tags} = join ' ', @{$config{tag} || []}; # Check username existence and prompt for password if necessary # If cookie file has been set, don't ask for a username, will check # later if it's all ok if ($config{login}) { if ($config{cookie}) { pod2usage( message => "error: please provide an existent cookie file\ +n", -verbose => 99, -sections => 'USAGE', ) unless -r $config{cookie} && -f $config{cookie}; } else { pod2usage( message => "error: please provide a username or a cookie fil +e\n", -verbose => 99, -sections => 'USAGE' ) unless exists $config{username}; $config{password} = prompt 'password: ', -echo => '*' unless exists $config{password}; } } ## end if ($config{login} && ... return %config; } ## end sub get_configuration sub login { $ua->get($config{login_page}); $ua->form_with_fields(qw( username password )); $ua->set_fields( username => $config{username}, password => $config{password}, ); $ua->submit(); return; } ## end sub login sub logout { $ua->get($config{logout_page}); return; } sub validate_add { my $config = shift; my %config = %$config; # Establish real privacy, command line overrides configuration @config{qw( private family friends )} = () if $config{_cmdline}{public}; $config{private} ||= $config{family} || $config{friends}; $config{public} = $config{private} ? 0 : 1; delete $config{private}; # Ensure there's some files to work on my @cmdline_files = @{$config{argv}}; my @filed_files = map { chomp(my @lines = slurp($_)); @lines; } @{$config{files} || []}; my @filenames = uniq grep { if (-e $_) { 1 } else { print {*STDERR} "file '$_' does not exist, ignoring\n"; 0; } } @cmdline_files, @filed_files; pod2usage( message => "error: no file to upload\n", -verbose => 99, -sections => 'USAGE' ) unless @filenames; $config{filenames} = \@filenames; %$config = %config; return; } ## end sub validate_add sub add { INFO "starting file upload"; my $retry = 0; my $backoff = $config{backoff}; my @results; my @failed; FILE: for my $filename (@{$config{filenames}}) { (my $barename = file($filename)->basename()) =~ s{\.\w+\z}{}mxs; eval { $ua->get($config{upload_page}) or die "couldn't get '$config{upload_page}'\n"; $ua->form_with_fields(qw( Filedata labels )); $ua->set_fields( labels => $config{tags}, is_public => $config{public}, Filedata => $filename, ); $ua->tick('is_friend', 1, $config{friends}); $ua->tick('is_family', 1, $config{family}); if ($config{resize} && (my $resized = add_resize($filename))) + { my $input = $ua->current_form()->find_input('Filedata'); $input->filename($filename); # just to be on the safe s +ide $input->content($resized); } INFO "uploading '$filename'"; $ua->submit(); }; my $error = 0; # assume no error just to +begin if ($EVAL_ERROR) { ERROR $EVAL_ERROR; $error = 1; } else { # Now check that the photo is actually there... if (my $link = $ua->find_link(text => $barename)) { INFO 'upload successful'; push @results, [ $filename, $link->url_abs() ]; } else { ERROR 'no error received, but the photo is not there'; $error = 1; } } ## end else [ if ($EVAL_ERROR) if ($error) { # Retry scheme if (++$retry <= $config{max_retry}) { if ($backoff) { INFO "sleeping $backoff second" . ($backoff == 1 ? '' : 's'); sleep $backoff if $backoff; $backoff *= 2; # exponential backoff } ## end if ($backoff) redo FILE; } ## end if (++$retry <= $config... ERROR "giving up on '$filename'"; push @results, [ $filename, '**FAILED**' ]; push @failed, $filename; } ## end if ($error) # Reset these values for next photo upload $retry = 0; $backoff = $config{backoff}; # Sleep a bit if configured. Avoid sleeping after last photo sleep $config{pause} if $config{pause} and $filename ne $config{filenames}[-1]; } ## end for my $filename (@{$config... # Recap on failed files ERROR 'failed files: ', join ' | ', @failed if @failed; # Summary, if requested if (defined $config{summary}) { if (open my $fh, '>', $config{summary}) { print {$fh} "File\tURI\n"; print {$fh} join("\t", @$_), "\n" for @results; close $fh; } else { ERROR "could not open $config{summary}: $OS_ERROR"; } } return; } ## end sub add sub add_resize { my ($filename) = @_; require Image::Magick; require File::Temp; my $magick = Image::Magick->new(); my ($width, $height, $size, $format) = $magick->Ping($filename); return if $width <= $config{resize} && $height <= $config{resize}; my ($neww, $newh); if ($width > $height) { $neww = $config{resize}; $newh = int($height * $neww / $width); } else { $newh = $config{resize}; $neww = int($width * $newh / $height); } my $ecode; $ecode = $magick->Read($filename) and die $ecode; $ecode = $magick->Resize(width => $neww, height => $newh) and die $ecode; my $scaled; my $fh = File::Temp::tempfile(); binmode $fh; $magick->Write(file => $fh, filename => $filename); seek $fh, 0, 0; # rewind $scaled = slurp $fh; close $fh; return $scaled; } ## end sub add_resize sub validate_search { my $conf = shift; my %entry_for = ( all => 1, social => 3, contacts => 3, everyone => 1, ); $conf->{who} = $entry_for{$conf->{who} || ''}; $conf->{who} ||= 2; # default to 'me' $conf->{who} = 1 unless $conf->{login}; return; } ## end sub validate_search sub search { INFO "starting search"; $ua->get($config{search_page}); $ua->form_with_fields(qw( w q )); $ua->select('w', {n => $config{who}}); $ua->field('q', $config{tags}); $ua->click(); my ($matches, $word) = $ua->content() =~ /we found ([\d,]+) (photos?)/msi; if (!defined $matches) { INFO "no photo matching the criteria"; return; } my $npages = 1; if (my @pages = $ua->find_all_links(url_regex => qr/\&page=(\d+)\z/ +mxs)) { ($npages) = $pages[-2]->url() =~ /(\d+)\z/msx; } my $npages_w = $npages == 1 ? 'page' : 'pages'; INFO "found $matches $word in $npages $npages_w"; my $base_url = $ua->uri(); $npages = $config{maxpage} if defined $config{maxpage} && $npages > $config{maxpage}; for my $page_id (1 .. $npages) { $ua->get($base_url . "&page=$page_id"); my @photos = $ua->find_all_links(url_regex => qr{/photos/.*/\d+/ +},); print join("\n* ", "Page $page_id:", map { $_->url() } @photos), "\n"; } ## end for my $page_id (1 .. $npages) return; } ## end sub search __END__ =head1 NAME zooomr - a simple command-line interface for Zooomr =head1 VERSION shell$ zooomr --version =head1 USAGE zooomr [--usage] [--help] [--man] [--version] zooomr <command> [<command-specific options>] [--check|-c] [--config|-C <filename>] [--cookie|--cookie-file|-k <filename>] [--debug|-D] [--dump|-d] [--login] [--password|-p password] [--proxy | -P <proxy>] [--summary|--summary-file|-s <filenam +e>] [--tag|-t tag1 [--tag|-t tag2 [...]]] [--username|-u <userna +me>] command: add [--backoff <time>] [--family] [--files|--files-from|-f <filename>] [--friends] [--max-retry|-m <retries>] [--pause <time>] [--private] [--public] [--resize <max-size>] =head1 EXAMPLES shell$ zooomr add -u pippo@example.com -p pluto foto*.jpg # Let the program ask you for the password, so that you don't have # to show it on the screen shell$ zooomr add -u pippo@example.com photo*.jpg # Add a tag to the new upload shell$ zooomr add -u pippo@example.com --tag awesome photo*.jpg # Add two tags shell$ zooomr add -u pippo@example.com -t awesome -t good photo*.jp +g =head1 DESCRIPTION This script lets you interact with your Zooomr account (see L<http://www.zooomr.com/>). You have to provide your account details, tell it what you want to do and voil�, you're done. At the mome +nt, the following commands are recognised: =over =item * B<add> one or more photo. =back =head2 Configuration File You can put any configuration described in the L<OPTIONS> into a configuration file. By default, the C<~/.zooomer> file is looked for and loaded if exists. The file format is quite easy: just put the configuration name, an equal sign and the value you want to set. For boolean settings like C<family> or C<public> use C<0> for false and C<1> for true. Example: username = pippo@example.com password = pluto private = 1 You can put comments and all the stuff, see L<Config::Tiny> for detail +s. Configurations on the command line override those found in the configuration file. A notable exception to this rule is represented by the tags, that are always added; this means that the tags in the configuration file will be B<always> applied. =head2 Logging in Zooomr For operations that require authentication with Zooomr, like adding ne +w photos in your photostream, you can use two different methods: =over =item B<explicit> in this case, you have to provide your B<username>/B<password> details +, either setting them in the configuration file, or passing them on the command line, or passing the B<username> and waiting until prompted fo +r the B<password>. This method will not work for OpenID registered users +. =item B<cookie> in this case, you rely on a previous authentication performed in the browser (currently only Netscape and its descendants Mozilla/Firefox are supported), and use cookies set by the browser. The mechanism is simple: open the browser and log into Zooomr. Then find the cookies file, and pass the path to this file with the C<--cookie> option (see below). For example, in my Linux system I have a C<.mozilla> subdirectory in my home directory; to locate my cookies file, I would do the following +: shell$ find "$HOME/.mozilla" -name 'cookies.txt' B<NOTE: OpenID users MUST use this method>. Users with normal B<username>/B<password> pair can choose to use this method or the B<explicit> one explained above. =back =head1 OPTIONS Available options depend on the required action. In particular, you have: =over =item B<* meta-options> options that deal with the B<zooomr> script itself; =item B<* common options> options that are common to all actions, e.g. the account details =item B<* per-action options> options that are specific to a given required action. =back =head2 Meta-Options These options don't really fire the script, but deal with the script itself. =over =item --help print a somewhat more verbose help, showing usage, this description of the options and some examples from the synopsis. =item --man print out the full documentation for the script. =item --usage print a concise usage line and exit. =item --version print the version of the script. =back =head2 Common Options These options are common to all commands. =over =item --check | -c check options, print them and exit. =item --config | -C <filename> set name of configuration file to use, defaults to C<~/.zooomer>. =item --cookie | --cookie-file | -k <filename> set the file name for the cookie file to use. You can use this to login in Zooomr with your browser (currently only Netscape/Mozilla/Firefox are supported) and then use the browser's cookies to access Zooomr with this script. In this case, it's not necessary to provide a B<username>/B<password> pair, but you have to ensure that you're logged in Zooomr with the browser. OpenID users MUST use this method. =item --debug | -D set the debug level. Use one of the following: =over =item - DEBUG =item - INFO =item - WARN =item - ERROR =item - FATAL =back =item --dump | -d dump the entire configuration. Note that this will show your password in clear in the screen, so use judiciously. =item --login this is a flag parameter to indicate that a login should be performed. It is set by default, so you don't have to bother with it; in case you want to I<disable> it, you can pass C<--no-login>. =item --password | --pass | -p <password> provide a password on the command line. If you don't set a password, you will be asked one and you'll be able to type it without the risk to display it explicitly (asterisks are written instead of the actual keys pressed). =item --proxy | -P <proxy> set the HTTP proxy in use. Note that you can also set the environment variable C<http_proxy>. =item --summary | --summary-file | -s <filename> save a summary of the upload into the given filename. The file will contain a line for each file, containing the photo file name, a C<TAB> and the URI where the photo is available in Zooomr. Note that this file is written only at the end of the upload session. If you break the upload script you won't get any file. This may change in the future. =item --tag | -t <tag> add a tag to the tag list (aka I<labels>). You can set multiple tags by using this parameter multiple times. =item --username | --user | -u <username> provide your account details. As of November 2007, this is an email address. This parameter is mandatory. =back =head2 Action: I<add> Action B<add> supports the options in the following list. Moreover, yo +u have to provide a list of filenames of photos that you want to upload. =over =item --backoff <time> pause between two consecutive attempts to upload the same file. This v +alue is doubled every new attempt, and reset for each new file. =item --family set photos as private and accessible by family. =item --files | --files-from | -f <filename> get filenames of photos to upload from specified argument, e.g.: shell$ ls | grep 'jpg$' > lista-jpg.text shell$ zooomr add -u pippo@example.com -f lista-jpg-text =item --friends set photos as private and accessible by friends. =item --max-retry | -m <retries> set the maximum number of re-tries for an upload. Setting it to 0 mean +s that only one single attempt (i.e. the first) is tried. =item --pause <time> pause between the upload of two consecutive files, in second. Defaults to 5 seconds. =item --private set photos as private. =item --public set photos as public. =item --resize <max-size> set a maximum dimension to which the uploaded photo must comply. If ei +ther width or height are greater than the provided value, the photo will be resized in order for both to comply. The resize is done preserving the aspect ratio. You will need L<Image::Magick> to do this. =back A C<public> permission on the command line overrides any other privacy configuration, either on the configuration file or on the command line itself. On the other hand, stricter privacy configuration +s in the file override a public configuration in the file itself. While it may seem counter-intuitive, you should probably avoid trying to give such contradictive commands, and just make peace with your brain. The retry scheme tries to cope with some issues in the B<Zooomr> websi +te. Up to 1 photo out of 6 isn't actually received by the system, whatever the result of the upload; for this reason, we try to see if the newly uploaded photo is in the response page, which I<should> mean that the upload was successful. The retry mechanism is ruled by options C<backoff> and C<max-retry>. Last, but not least, a pause can be inserted between two consecutive files, in order to limit server CPU usage. =head1 DIAGNOSTICS To date, every error terminates the script. The error message should b +e explicit enough that you don't need more explainations (most of them are provided by L<WWW::Mechanize>, so don't blame me). =head1 CONFIGURATION AND ENVIRONMENT zooomr relies on no environment variables. By default, a configuration file is searched in C<~/.zoomer>. See L<Configuration file> in L<DESCRIPTION> for details, and see L<OPTIONS> for allowed parameters. =head1 DEPENDENCIES All stuff you can find on CPAN: =over =item L<version> =item L<WWW::Mechanize> (the real star) =item L<IO::Prompt> =item L<Log::Log4perl> =item L<Config::Tiny> =item L<Path::Class> =item L<List::MoreUtils> =item L<Image::Magick> but only if you want to use the C<resize> options in C<add>. =back =head1 BUGS AND LIMITATIONS No bugs have been reported. Please report any bugs or feature requests through http://rt.cpan.org/ The only supported action so far is B<add>, which is pretty little. To be true, a B<search> action is currently implemented but still not documented, mainly due to some residual decisions about the output of the action. The summary file (option C<--summary>) is written only after the uploa +d process completion for all files, so will be missing if anything interrupts the upload itself. =head1 AUTHOR Flavio Poletti C<flavio@polettix.it> =head1 LICENCE AND COPYRIGHT Copyright (c) 2006, Flavio Poletti C<flavio@polettix.it>. All rights r +eserved. This script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L<perlartistic> and L<perlgpl>. Questo script � software libero: potete ridistribuirlo e/o modificarlo negli stessi termini di Perl stesso. Vedete anche L<perlartistic> e L<perlgpl>. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WH +EN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. TH +E ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. =head1 NEGAZIONE DELLA GARANZIA Poiché questo software viene dato con una licenza gratuita, non c'è alcuna garanzia associata ad esso, ai fini e per quanto permesso dalle leggi applicabili. A meno di quanto possa essere specificato altrove, il proprietario e detentore del copyright fornisce questo software "così com'è" senza garanzia di alcun tipo, sia essa espressa o implicita, includendo fra l'altro (senza però limitarsi a questo) eventuali garanzie implicite di commerciabilità e adeguatezza per uno scopo particolare. L'intero rischio riguardo alla qualità ed alle prestazioni di questo software rimane a voi. Se il software dovesse dimostrarsi difettoso, vi assumete tutte le responsabilità ed i costi per tutti i necessari servizi, riparazioni o correzioni. In nessun caso, a meno che ciò non sia richiesto dalle leggi vigenti o sia regolato da un accordo scritto, alcuno dei detentori del diritto di copyright, o qualunque altra parte che possa modificare, o redistri +buire questo software così come consentito dalla licenza di cui sopra, potrò essere considerato responsabile nei vostri confronti per danni, ivi inclusi danni generali, speciali, incidentali o conseguenziali, deriva +nti dall'utilizzo o dall'incapacità di utilizzo di questo software. Ciò include, a puro titolo di esempio e senza limitarsi ad essi, la perdit +a di dati, l'alterazione involontaria o indesiderata di dati, le perdite sostenute da voi o da terze parti o un fallimento del software ad operare con un qualsivoglia altro software. Tale negazione di garanzia rimane in essere anche se i dententori del copyright, o qualsiasi altr +a parte, sono stati avvisati della possibilità di tali danneggiamenti. Se decidete di utilizzare questo software, lo fate a vostro rischio e pericolo. Se pensate che i termini di questa negazione di garanzia non si confacciano alle vostre esigenze, o al vostro modo di considerare un software, o ancora al modo in cui avete sempre trattato software di terze parti, non usatelo. Se lo usate, accettate espressam +ente questa negazione di garanzia e la piena responsabilità per qualsiasi tipo di danno, di qualsiasi natura, possa derivarne. =cut |
|
---|
Replies are listed 'Best First'. | |
---|---|
Re: Zooomr uploader
by Anonymous Monk on Nov 28, 2007 at 19:45 UTC | |
by polettix (Vicar) on Nov 28, 2007 at 22:59 UTC | |
by Anonymous Monk on Nov 29, 2007 at 12:26 UTC | |
Re: Zooomr uploader
by Shaef (Initiate) on Jan 26, 2008 at 08:37 UTC | |
by polettix (Vicar) on Jan 26, 2008 at 13:01 UTC | |
Re: Zooomr uploader
by Anonymous Monk on Oct 22, 2008 at 10:27 UTC | |
by polettix (Vicar) on Oct 26, 2008 at 01:16 UTC | |
by Anonymous Monk on Oct 26, 2008 at 22:12 UTC | |
by Anonymous Monk on Dec 13, 2008 at 23:21 UTC | |
by polettix (Vicar) on Dec 23, 2008 at 18:15 UTC | |
by Anonymous Monk on Dec 16, 2008 at 16:24 UTC | |
Re: Zooomr uploader
by Anonymous Monk on Nov 26, 2007 at 14:21 UTC | |
by polettix (Vicar) on Nov 26, 2007 at 22:40 UTC | |
by Anonymous Monk on Nov 27, 2007 at 16:53 UTC | |
Re: Zooomr uploader
by Anonymous Monk on Nov 30, 2007 at 15:41 UTC | |
by polettix (Vicar) on Dec 01, 2007 at 02:04 UTC |