From http://www.jwz.org/xscreensaver/xscreensaver-5.19.tar.gz
[xscreensaver] / hacks / webcollage
index 9d76d1ab64d41c7bb37e63054c6e6c43edf81a08..e9a3aa37a0acd5393ecd2a22e43a2aae7ceffeb4 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -w
 #
-# webcollage, Copyright (c) 1999-2005 by Jamie Zawinski <jwz@jwz.org>
+# webcollage, Copyright (c) 1999-2012 by Jamie Zawinski <jwz@jwz.org>
 # This program decorates the screen with random images from the web.
 # One satisfied customer described it as "a nonstop pop culture brainbath."
 #
@@ -60,21 +60,35 @@ use bytes;  # Larry can take Unicode and shove it up his ass sideways.
 
 
 my $progname = $0; $progname =~ s@.*/@@g;
-my $version = q{ $Revision: 1.135 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/;
-my $copyright = "WebCollage $version, Copyright (c) 1999-2005" .
+my $version = q{ $Revision: 1.159 $ }; $version =~ s/^[^0-9]+([0-9.]+).*$/$1/;
+my $copyright = "WebCollage $version, Copyright (c) 1999-2011" .
     " Jamie Zawinski <jwz\@jwz.org>\n" .
     "            http://www.jwz.org/webcollage/\n";
 
 
 
-my @search_methods = (  56, "altavista",    \&pick_from_alta_vista_random_link,
-                        11, "livejournal",  \&pick_from_livejournal_images,
-                         5, "yahoorand",    \&pick_from_yahoo_random_link,
-                        10, "googlephotos", \&pick_from_google_image_photos,
-                         5, "googleimgs",   \&pick_from_google_images,
-                         3, "googlenums",   \&pick_from_google_image_numbers,
-                         2, "flickr_recent", \&pick_from_flickr_recent,
-                         8, "flickr_random", \&pick_from_flickr_random,
+my @search_methods = ( 26, "googlephotos",  \&pick_from_google_image_photos,
+                       15, "googleimgs",    \&pick_from_google_images,
+                       15, "googlenums",    \&pick_from_google_image_numbers,
+                       17, "flickr_recent", \&pick_from_flickr_recent,
+                       14, "flickr_random", \&pick_from_flickr_random,
+# twitpic went stale. don't have time to fix it right now.
+#                       10, "twitpic",       \&pick_from_twitpic_images,
+                        9, "livejournal",   \&pick_from_livejournal_images,
+                        4, "yahoorand",     \&pick_from_yahoo_random_link,
+
+                     # This one doesn't work very well: too many non-img links.
+                        0, "twitter",       \&pick_from_twitter_images,
+
+                     # This is a cute way to search for a certain webcams.
+                     # Not included in default methods, since these images
+                     # aren't terribly interesting by themselves.
+                     # See also "SurveillanceSaver".
+                     #
+                        0, "securitycam",   \&pick_from_security_camera,
+
+                     # Nonfunctional as of June 2011.
+                     #  0, "altavista",     \&pick_from_alta_vista_random_link,
 
                      # In Apr 2002, Google asked me to stop searching them.
                      # I asked them to add a "random link" url.  They said
@@ -87,31 +101,31 @@ my @search_methods = (  56, "altavista",    \&pick_from_alta_vista_random_link,
                      # it's no longer possible to do "or" searches on news
                      # images, so we rarely get any hits there any more.
                      # 
-                     #  0, "yahoonews",   \&pick_from_yahoo_news_text,
+                     #  0, "yahoonews",     \&pick_from_yahoo_news_text,
 
                      # Dec 2004: the ircimages guy's server can't take the
                      # heat, so he started banning the webcollage user agent.
                      # I tried to convince him to add a lighter-weight page to
                      # support webcollage better, but he doesn't care.
                      #
-                     #  0, "ircimages", \&pick_from_ircimages,
+                     #  0, "ircimages",     \&pick_from_ircimages,
 
                      # Dec 2002: Alta Vista has a new "random link" URL now.
                      # They added it specifically to better support webcollage!
                      # That was super cool of them.  This is how we used to do
                      # it, before:
                      #
-                     #  0, "avimages", \&pick_from_alta_vista_images,
-                     #  0, "avtext",   \&pick_from_alta_vista_text,
+                     #  0, "avimages",      \&pick_from_alta_vista_images,
+                     #  0, "avtext",        \&pick_from_alta_vista_text,
 
                      # This broke in 2004.  Eh, Lycos sucks anyway.
                      #
-                     #   0, "lycos",      \&pick_from_lycos_text,
+                     #  0, "lycos",         \&pick_from_lycos_text,
 
                      # This broke in 2003, I think.  I suspect Hotbot is
                      # actually the same search engine data as Lycos.
                      #
-                     #  0, "hotbot",     \&pick_from_hotbot_text,
+                     #  0, "hotbot",        \&pick_from_hotbot_text,
                       );
 
 # programs we can use to write to the root window (tried in ascending order.)
@@ -181,6 +195,11 @@ my %poisoners = (
                                    # (I don't see how they did it, though!)
   "alwayshotels.com"        => 1,  # Poisoned Lycos pretty heavily.
   "nextag.com"              => 1,  # Poisoned Alta Vista real good.
+  "ghettodriveby.com"       => 1,  # Poisoned Google Images.
+  "crosswordsolver.org"     => 1,  # Poisoned Google Images.
+  "xona.com"                => 1,  # Poisoned Google Images.
+  "freepatentsonline.com"   => 1,  # Poisoned Google Images.
+  "herbdatanz.com"          => 1,  # Poisoned Google Images.
 );
 
 
@@ -191,14 +210,28 @@ my %poisoners = (
 # site" diagnostic message.
 #
 my %warningless_sites = (
-  "home.earthlink.net"      => 1,  # Lots of home pages here.
-  "www.geocities.com"       => 1,
+  "home.earthlink.net"      => 1,
   "www.angelfire.com"       => 1,
   "members.aol.com"         => 1,
   "img.photobucket.com"     => 1,
   "pics.livejournal.com"    => 1,
   "tinypic.com"             => 1,
   "flickr.com"              => 1,
+  "pbase.com"               => 1,
+  "blogger.com"             => 1,
+  "multiply.com"            => 1,
+  "wikimedia.org"           => 1,
+  "twitpic.com"             => 1,
+  "amazonaws.com"           => 1,
+  "blogspot.com"            => 1,
+  "photoshelter.com"        => 1,
+  "myspacecdn.com"          => 1,
+  "feedburner.com"          => 1,
+  "wikia.com"               => 1,
+  "ljplus.ru"               => 1,
+  "yandex.ru"               => 1,
+  "imgur.com"               => 1,
+  "yfrog.com"               => 1,
 
   "yimg.com"                => 1,  # This is where dailynews.yahoo.com stores
   "eimg.com"                => 1,  # its images, so pick_from_yahoo_news_text()
@@ -242,7 +275,19 @@ my %entity_table = (
    "ocirc"  => 'ô', "otilde" => 'õ', "ouml"   => 'ö', "divide" => '÷',
    "oslash" => 'ø', "ugrave" => 'ù', "uacute" => 'ú', "ucirc"  => 'û',
    "uuml"   => 'ü', "yacute" => 'ý', "thorn"  => 'þ', "yuml"   => 'ÿ',
-   "ndash"  => '-', "mdash"  => "--"
+
+   # HTML 4 entities that do not have 1:1 Latin1 mappings.
+   "bull"  => "*",   "hellip"=> "...",  "prime" => "'",  "Prime" => "\"",
+   "frasl" => "/",   "trade" => "[tm]", "larr"  => "<-", "rarr"  => "->",
+   "harr"  => "<->", "lArr"  => "<=",   "rArr"  => "=>", "hArr"  => "<=>",
+   "empty" => "Ø",   "minus" => "-",    "lowast"=> "*",  "sim"   => "~",
+   "cong"  => "=~",  "asymp" => "~",    "ne"    => "!=", "equiv" => "==",
+   "le"    => "<=",  "ge"    => ">=",   "lang"  => "<",  "rang"  => ">",
+   "loz"   => "<>",  "OElig" => "OE",   "oelig" => "oe", "Yuml"  => "Y",
+   "circ"  => "^",   "tilde" => "~",    "ensp"  => " ",  "emsp"  => " ",
+   "thinsp"=> " ",   "ndash" => "-",    "mdash" => "--", "lsquo" => "`",
+   "rsquo" => "'",   "sbquo" => "'",    "ldquo" => "\"", "rdquo" => "\"",
+   "bdquo" => "\"",  "lsaquo"=> "<",    "rsaquo"=> ">",
 );
 
 
@@ -396,16 +441,17 @@ sub get_document_1($$$) {
 
       if ($url =~ m@^http://www\.altavista\.com/@ ||
           $url =~ m@^http://random\.yahoo\.com/@ ||
-          $url =~ m@^http://images\.google\.com/@) {
+          $url =~ m@^http://images\.google\.com/@ ||
+          $url =~ m@^http://www\.google\.com/@) {
         # block this, you turkeys.
-        $user_agent = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.5)" .
-          " Gecko/20041111 Firefox/1.0";
+        $user_agent = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.7)" .
+          " Gecko/20070914 Firefox/2.0.0.7";
 
         # 28-Jun-2007: Google Images now emits the entire page in JS if
         # you claim to be Gecko.  They also still block "webcollage".
         # They serve non-JS for unrecognised agents, so let's try this...
         $user_agent = "NoJavascriptPlease/1.0"
-          if ($url =~ m@^http://images\.google\.com/@);
+          if ($url =~ m@^http://[a-z]+\.google\.com/@);
       }
 
       my $hdrs = "GET " . ($http_proxy ? $url : "/$path") . " HTTP/1.0\r\n" .
@@ -684,30 +730,39 @@ sub pick_image_from_body($$) {
   my %unique_urls;
 
   foreach (split(/ *</)) {
-    if ( m/^meta /i ) {
+    if ( m/^meta.*["']keywords["']/i ) {
 
       # Likewise, reject any web pages that have a KEYWORDS meta tag
       # that is too long.
       #
-      if (m/name ?= ?\"?keywords\"?/i &&
-          m/content ?= ?\"([^\"]+)\"/) {
-        my $L = length($1);
-        if ($L > 1000) {
-          LOG (($verbose_filter || $verbose_load),
-               "excessive keywords ($L bytes) in $url: rejecting.");
-          $rejected_urls{$url} = $L;
-          $body = undef;
-          $_ = undef;
-          return ();
-        } else {
-          LOG ($verbose_filter, "  keywords ($L bytes) in $url (ok)");
-        }
+      my $L = length($_);
+      if ($L > 1000) {
+        LOG (($verbose_filter || $verbose_load),
+             "excessive keywords ($L bytes) in $url: rejecting.");
+        $rejected_urls{$url} = $L;
+        $body = undef;
+        $_ = undef;
+        return ();
+      } else {
+        LOG ($verbose_filter, "  keywords ($L bytes) in $url (ok)");
       }
 
-    } elsif ( m/^(img|a) .*(src|href) ?= ?\"? ?(.*?)[ >\"]/io ) {
+    } elsif (m/^ (IMG|A) \b .* (SRC|HREF) \s* = \s* ["']? (.*?) [ "'<>] /six ||
+             m/^ (LINK|META) \b .* (REL|PROPERTY) \s* = \s*
+                 ["']? (image_src|og:image) ["']? /six) {
 
-      my $was_inline = (! ( "$1" eq "a" || "$1" eq "A" ));
+      my $was_inline = (lc($1) eq 'img');
+      my $was_meta   = (lc($1) eq 'link' || lc($1) eq 'meta');
       my $link = $3;
+
+      # For <link rel="image_src" href="...">
+      # and <meta property="og:image" content="...">
+      #
+      if ($was_meta) {
+        next unless (m/ (HREF|CONTENT) \s* = \s* ["']? (.*?) [ "'<>] /six);
+        $link = $2;
+      }
+
       my ( $width )  = m/width ?=[ \"]*(\d+)/oi;
       my ( $height ) = m/height ?=[ \"]*(\d+)/oi;
       $_ = $link;
@@ -779,20 +834,24 @@ sub pick_image_from_body($$) {
       LOG ($verbose_filter,
            "  image $url" .
            ($width && $height ? " (${width}x${height})" : "") .
-           ($was_inline ? " (inline)" : ""));
+           ($was_meta ? " (meta)" : $was_inline ? " (inline)" : ""));
 
-      $urls[++$#urls] = $url;
-      $unique_urls{$url}++;
 
-      # JPEGs are preferable to GIFs and PNGs.
-      $_ = $url;
-      if ( ! m@[.](gif|png)$@io ) {
-        $urls[++$#urls] = $url;
+      my $weight = 1;
+
+      if ($was_meta) {
+        $weight = 20;   # meta tag images are far preferable to inline images.
+      } else {
+        if ($url !~ m@[.](gif|png)$@io ) {
+          $weight += 2;         # JPEGs are preferable to GIFs and PNGs.
+        }
+        if (! $was_inline) {
+          $weight += 4;         # pointers to images are preferable to inlined images.
+        }
       }
 
-      # pointers to images are preferable to inlined images.
-      if ( ! $was_inline ) {
-        $urls[++$#urls] = $url;
+      $unique_urls{$url}++;
+      for (my $i = 0; $i < $weight; $i++) {
         $urls[++$#urls] = $url;
       }
     }
@@ -819,6 +878,7 @@ sub pick_image_from_body($$) {
   return $url;
 }
 
+
 # Given a URL and the RSS feed from that URL, pick a random image from
 # the feed.  This is a lot simpler than extracting images out of a page:
 # we already know we have reasonable images, so we just pick one.
@@ -854,7 +914,10 @@ sub pick_image_from_rss($$) {
 sub pick_dictionary() {
   my @dicts = ("/usr/dict/words",
                "/usr/share/dict/words",
-               "/usr/share/lib/dict/words");
+               "/usr/share/lib/dict/words",
+               "/usr/share/dict/cracklib-small",
+               "/usr/share/dict/cracklib-words"
+               );
   foreach my $f (@dicts) {
     if (-f $f) {
       $wordlist = $f;
@@ -868,12 +931,9 @@ sub pick_dictionary() {
 #
 sub random_word() {
 
-  local *IN;
-  if (! open (IN, "<$wordlist")) {
-    return undef;
-  }
+  return undef unless open (my $in, '<', $wordlist);
 
-  my $size = (stat(IN))[7];
+  my $size = (stat($in))[7];
   my $word = undef;
   my $count = 0;
 
@@ -882,9 +942,9 @@ sub random_word() {
       if (++$count > 100);
 
     my $pos = int (rand ($size));
-    if (seek (IN, $pos, 0)) {
-      $word = <IN>;   # toss partial line
-      $word = <IN>;   # keep next line
+    if (seek ($in, $pos, 0)) {
+      $word = <$in>;   # toss partial line
+      $word = <$in>;   # keep next line
     }
 
     next unless ($word);
@@ -908,7 +968,7 @@ sub random_word() {
     last if ($word);
   }
 
-  close (IN);
+  close ($in);
 
   if ( $word =~ s/\s/\+/gs ) {  # convert intra-word spaces to "+".
     $word = "\%22$word\%22";    # And put quotes (%22) around it.
@@ -919,13 +979,12 @@ sub random_word() {
 
 
 sub random_words($) {
-  my ($or_p) = @_;
-  my $sep = ($or_p ? "%20OR%20" : "%20");
-  return (random_word . $sep .
-          random_word . $sep .
-          random_word . $sep .
-          random_word . $sep .
-          random_word);
+  my ($sep) = @_;
+  return (random_word() . $sep .
+          random_word() . $sep .
+          random_word() . $sep .
+          random_word() . $sep .
+          random_word());
 }
 
 
@@ -1025,11 +1084,10 @@ sub pick_from_search_engine($$$) {
   1 while ($search_count =~ s/^(\d+)(\d{3})/$1,$2/);
 
 #  if ($search_count eq "?" || $search_count eq "0") {
-#    local *OUT;
 #    my $file = "/tmp/wc.html";
-#    open(OUT, ">$file") || error ("writing $file: $!");
-#    print OUT $body;
-#    close OUT;
+#    open (my $out, '>', $file) || error ("writing $file: $!");
+#    print $out $body;
+#    close $out;
 #    print STDERR  blurb() . "###### wrote $file\n";
 #  }
 
@@ -1189,6 +1247,7 @@ sub pick_from_yahoo_random_link($) {
 ############################################################################
 #
 # Pick images from random pages returned by the Alta Vista Random Link
+# Note: this seems to have gotten a *lot* less random lately (2007).
 #
 ############################################################################
 
@@ -1287,6 +1346,55 @@ sub pick_from_alta_vista_images($) {
 }
 
 
+\f
+############################################################################
+#
+# Pick images from Aptix security cameras
+# Cribbed liberally from google image search code.
+# By Jason Sullivan <jasonsul@us.ibm.com>
+#
+############################################################################
+
+my $aptix_images_url = ("http://www.google.com/search" .
+                        "?q=inurl:%22jpg/image.jpg%3Fr%3D%22");
+
+# securitycam
+sub pick_from_security_camera($) {
+  my ($timeout) = @_;
+
+  my $page = (int(rand(9)) + 1);
+  my $num = 20;                                        # 20 images per page
+  my $search_url = $aptix_images_url;
+
+  if ($page > 1) {
+    $search_url .= "&start=" . $page*$num;     # page number
+    $search_url .= "&num="   . $num;           #images per page
+  }
+
+  my ($search_hit_count, @subpages) =
+    pick_from_search_engine ($timeout, $search_url, '');
+
+  my @candidates = ();
+  my %referers;
+  foreach my $u (@subpages) {
+    next if ($u =~ m@[/.]google\.com\b@i);        # skip google builtins (most links)
+    next unless ($u =~ m@jpg/image.jpg\?r=@i);    #  All pics contain this
+
+    LOG ($verbose_filter, "  candidate: $u");
+    push @candidates, $u;
+    $referers{$u} = $u;
+    }
+
+  @candidates = depoison (@candidates);
+  return () if ($#candidates < 0);
+  my $i = int(rand($#candidates+1));
+  my $img = $candidates[$i];
+  my $ref = $referers{$img};
+
+  LOG ($verbose_load, "picked image " . ($i+1) . ": $img (on $ref)");
+  return ($ref, $img);
+}
+
 \f
 ############################################################################
 #
@@ -1308,7 +1416,7 @@ sub pick_from_google_images($;$$) {
   my ($timeout, $words, $max_page) = @_;
 
   if (!defined($words)) {
-    $words = random_word;   # only one word for Google
+    $words = random_word();   # only one word for Google
   }
 
   my $page = (int(rand(9)) + 1);
@@ -1329,11 +1437,15 @@ sub pick_from_google_images($;$$) {
     next unless ($u =~ m@imgres\?imgurl@i);    #  All pics start with this
     next if ($u =~ m@[/.]google\.com\b@i);     # skip google builtins
 
-    if ($u =~ m@^/imgres\?imgurl=(.*?)\&imgrefurl=(.*?)\&@) {
+    $u = html_unquote($u);
+    if ($u =~ m@^/imgres\?imgurl=(.*?)&imgrefurl=(.*?)\&@) {
       my $ref = $2;
       my $img = $1;
       $img = "http://$img" unless ($img =~ m/^http:/i);
 
+      $ref = url_decode($ref);
+      $img = url_decode($img);
+
       LOG ($verbose_filter, "  candidate: $ref");
       push @candidates, $img;
       $referers{$img} = $ref;
@@ -1462,7 +1574,7 @@ my $alta_vista_url = "http://www.altavista.com/web/results" .
 sub pick_from_alta_vista_text($) {
   my ($timeout) = @_;
 
-  my $words = random_words(0);
+  my $words = random_words('%20');
   my $page = (int(rand(9)) + 1);
   my $search_url = $alta_vista_url . $words;
 
@@ -1782,6 +1894,195 @@ sub pick_from_ircimages($) {
   return ($search_url, $img);
 }
 
+\f
+############################################################################
+#
+# Pick images from Twitpic's list of recently-posted images.
+#
+############################################################################
+
+my $twitpic_img_url = "http://twitpic.com/public_timeline/feed.rss";
+
+# With most of our image sources, we get a random page and then select
+# from the images on it.  However, in the case of Twitpic, the page
+# of images tends to update slowly; so we'll remember the last N entries
+# on it and randomly select from those, to get a wider variety each time.
+
+my $twitpic_cache_size = 1000;
+my @twitpic_cache = (); # fifo, for ordering by age
+my %twitpic_cache = (); # hash, for detecting dups
+
+# twitpic
+sub pick_from_twitpic_images($) {
+  my ($timeout) = @_;
+
+  $last_search = $twitpic_img_url;   # for warnings
+
+  my ( $base, $body ) = get_document ($twitpic_img_url, undef, $timeout);
+
+  # Update the cache.
+
+  if ($body) {
+    $body =~ s/\n/ /gs;
+    $body =~ s/(<item)\b/\n$1/gsi;
+
+    my @items = split (/\n/, $body);
+    shift @items;
+    foreach (@items) {
+      next unless (m@<link>([^<>]*)</link>@si);
+      my $page = html_unquote ($1);
+
+      $page =~ s@/$@@s;
+      $page .= '/full';
+
+      next if ($twitpic_cache{$page}); # already have it
+
+      LOG ($verbose_filter, "  candidate: $page");
+      push @twitpic_cache, $page;
+      $twitpic_cache{$page} = $page;
+    }
+  }
+
+  # Pull from the cache.
+
+  return () if ($#twitpic_cache == -1);
+
+  my $n = $#twitpic_cache+1;
+  my $i = int(rand($n));
+  my $page = $twitpic_cache[$i];
+
+  # delete this one from @twitpic_cache and from %twitpic_cache.
+  #
+  @twitpic_cache = ( @twitpic_cache[0 .. $i-1],
+                     @twitpic_cache[$i+1 .. $#twitpic_cache] );
+  delete $twitpic_cache{$page};
+
+  # Keep the size of the cache under the limit by nuking older entries
+  #
+  while ($#twitpic_cache >= $twitpic_cache_size) {
+    my $page = shift @twitpic_cache;
+    delete $twitpic_cache{$page};
+  }
+
+  ( $base, $body ) = get_document ($page, undef, $timeout);
+  my $img = undef;
+  $body = '' unless defined($body);
+
+  foreach (split (/<img\s+/, $body)) {
+    my ($src) = m/\bsrc=[\"\'](.*?)[\"\']/si;
+    next unless $src;
+    next if m@/js/@s;
+    next if m@/images/@s;
+
+    $img = $src;
+
+    $img = "http:$img" if ($img =~ m@^//@s);  # Oh come on
+
+    # Sometimes these images are hosted on twitpic, sometimes on Amazon.
+    if ($img =~ m@^/@) {
+      $base =~ s@^(https?://[^/]+)/.*@$1@s;
+      $img = $base . $img;
+    }
+    last;
+  }
+
+  if (!$img) {
+    LOG ($verbose_load, "no matching images on $page\n");
+    return ();
+  }
+
+  LOG ($verbose_load, "picked image " .($i+1) . "/$n: $img");
+
+  return ($page, $img);
+}
+
+\f
+############################################################################
+#
+# Pick images from Twitter's list of recently-posted updates.
+#
+############################################################################
+
+# With most of our image sources, we get a random page and then select
+# from the images on it.  However, in the case of Twitter, the page
+# of images only updates once a minute; so we'll remember the last N entries
+# on it and randomly select from those, to get a wider variety each time.
+
+my $twitter_img_url = "http://api.twitter.com/1/statuses/" .
+                     "public_timeline.json" .
+                     "?include_entities=true" .
+                     "&include_rts=true" .
+                     "&count=200";
+
+my $twitter_cache_size = 1000;
+
+my @twitter_cache = (); # fifo, for ordering by age
+my %twitter_cache = (); # hash, for detecting dups
+
+
+# twitter
+sub pick_from_twitter_images($) {
+  my ($timeout) = @_;
+
+  $last_search = $twitter_img_url;   # for warnings
+
+  my ( $base, $body ) = get_document ($twitter_img_url, undef, $timeout);
+  # Update the cache.
+
+  if ($body) {
+    $body =~ s/[\r\n]+/ /gs;
+
+    # Parsing JSON is a pain in the ass.  So we halfass it as usual.
+    $body =~ s/^\[|\]$//s;
+    $body =~ s/(\[.*?\])/{ $_ = $1; s@\},@\} @gs; $_; }/gsexi;
+    my @items = split (/},{/, $body);
+    foreach (@items) {
+      my ($name) = m@"screen_name":"([^\"]+)"@si;
+      my ($img)  = m@"media_url":"([^\"]+)"@si;
+      my ($page) = m@"display_url":"([^\"]+)"@si;
+      next unless ($name && $img && $page);
+      foreach ($img, $page) {
+        s/\\//gs;
+        $_ = "http://$_" unless (m/^http/si);
+      }
+
+      next if ($twitter_cache{$page}); # already have it
+
+      LOG ($verbose_filter, "  candidate: $page - $img");
+      push @twitter_cache, $page;
+      $twitter_cache{$page} = $img;
+    }
+  }
+
+  # Pull from the cache.
+
+  return () if ($#twitter_cache == -1);
+
+  my $n = $#twitter_cache+1;
+  my $i = int(rand($n));
+  my $page = $twitter_cache[$i];
+  my $url  = $twitter_cache{$page};
+
+  # delete this one from @twitter_cache and from %twitter_cache.
+  #
+  @twitter_cache = ( @twitter_cache[0 .. $i-1],
+                     @twitter_cache[$i+1 .. $#twitter_cache] );
+  delete $twitter_cache{$page};
+
+  # Keep the size of the cache under the limit by nuking older entries
+  #
+  while ($#twitter_cache >= $twitter_cache_size) {
+    my $page = shift @twitter_cache;
+    delete $twitter_cache{$page};
+  }
+
+  LOG ($verbose_load, "picked page $url");
+
+  $suppress_audit = 1;
+
+  return ($page, $url);
+}
+
 \f
 ############################################################################
 #
@@ -1826,7 +2127,7 @@ sub pick_from_flickr_recent($) {
     $page = html_unquote ($page);
     $thumb = html_unquote ($thumb);
 
-    next unless ($thumb =~ m@^http://farm\d*\.static\.flickr\.com/@);
+    next unless ($thumb =~ m@^http://farm\d*\.static\.?flickr\.com/@);
 
     my $base = "http://www.flickr.com/";
     $page  =~ s@^/@$base@;
@@ -1877,8 +2178,8 @@ sub pick_from_flickr_recent($) {
 #
 ############################################################################
 
-my $flickr_rss_base = ("http://www.flickr.com/services/feeds/photos_public.gne" .
-                       "?format=rss_200_enc&tags=");
+my $flickr_rss_base = ("http://www.flickr.com/services/feeds/photos_public.gne".
+                       "?format=rss_200_enc&tagmode=any&tags=");
 
 # Picks a random RSS feed; picks a random image from that feed;
 # returns 2 URLs: the page containing the image, and the image.
@@ -1888,10 +2189,15 @@ my $flickr_rss_base = ("http://www.flickr.com/services/feeds/photos_public.gne"
 sub pick_from_flickr_random($) {
   my $timeout = shift;
 
-  my $rss = $flickr_rss_base . random_word();
+  my $words = random_words(',');
+  my $rss = $flickr_rss_base . $words;
   $last_search = $rss;
 
+  $_ = $words;
+  s/,/ /g;
+
   print STDERR "\n\n" if ($verbose_load);
+  LOG ($verbose_load, "words: $_");
   LOG ($verbose_load, "URL: $last_search");
 
   $suppress_audit = 1;
@@ -1940,16 +2246,15 @@ sub pick_from_driftnet($) {
   $last_search = $id;
 
   while ($now = time, $now < $start + $timeout) {
-    local *DIR;
-    opendir (DIR, $dir) || error ("$dir: $!");
-    while (my $file = readdir(DIR)) {
+    opendir (my $dir, $dir) || error ("$dir: $!");
+    while (my $file = readdir($dir)) {
       next if ($file =~ m/^\./);
       $file = "$dir/$file";
-      closedir DIR;
+      closedir ($dir);
       LOG ($verbose_load, "picked file $file ($id)");
       return ($id, $file);
     }
-    closedir DIR;
+    closedir ($dir);
   }
   LOG (($verbose_net || $verbose_load), "timed out for $id");
   return ();
@@ -1962,15 +2267,14 @@ sub get_driftnet_file($) {
   error ("\$driftnet_dir unset?") unless ($driftnet_dir);
 
   my $id = $driftnet_magic;
-  my $re = qr/$driftnet_dir/;
   error ("$id: $file not in $driftnet_dir?")
-    unless ($file =~ m@^$re@o);
+    unless ($file =~ m@^\Q$driftnet_dir@o);
 
-  local *IN;
-  open (IN, $file) || error ("$id: $file: $!");
+  open (my $in, '<', $file) || error ("$id: $file: $!");
   my $body = '';
-  while (<IN>) { $body .= $_; }
-  close IN || error ("$id: $file: $!");
+  local $/ = undef;  # read entire file
+  $body = <$in>;
+  close ($in) || error ("$id: $file: $!");
   unlink ($file) || error ("$id: $file: rm: $!");
   return ($id, $body);
 }
@@ -2019,8 +2323,8 @@ sub spawn_driftnet($) {
 }
 
 # local-directory
-sub pick_from_local_dir {
-  my ( $timeout ) = @_;
+sub pick_from_local_dir($) {
+  my ($timeout) = @_;
 
   my $id = $local_magic;
   $last_search = $id;
@@ -2033,27 +2337,27 @@ sub pick_from_local_dir {
 
   my $v = ($verbose_exec ? "-v" : "");
   my $pick = `xscreensaver-getimage-file $v "$dir"`;
+  $pick =~ s/\s+$//s;
+  $pick = "$dir/$pick" unless ($pick =~ m@^/@s);       # relative path
 
   LOG ($verbose_load, "picked file $pick ($id)");
   return ($id, $pick);
 }
 
 
-sub get_local_file {
+sub get_local_file($) {
   my ($file) = @_;
 
   error ("\$local_dir unset?") unless ($local_dir);
 
   my $id = $local_magic;
-  my $re = qr/$local_dir/;
   error ("$id: $file not in $local_dir?")
-    unless ($file =~ m@^$re@o);
+    unless ($file =~ m@^\Q$local_dir@o);
 
-  local *IN;
-  open (IN, $file) || error ("$id: $file: $!");
-  my $body = '';
-  while (<IN>) { $body .= $_; }
-  close IN || error ("$id: $file: $!");
+  open (my $in, '<', $file) || error ("$id: $file: $!");
+  local $/ = undef;  # read entire file
+  my $body = <$in>;
+  close ($in) || error ("$id: $file: $!");
   return ($id, $body);
 }
 
@@ -2283,8 +2587,9 @@ sub report_performance() {
     my $suc = $stats_successes{$name} || 0;
     my $pct = int($suc * 100 / $try);
     my $secs = $stats_elapsed{$name};
-    my $secs_link = int($secs / $try);
-    print STDERR sprintf ("$blurb   %-12s %4s (%d/%d);\t %2d secs/link\n",
+    my $secs_link = $secs / $try;
+    print STDERR sprintf ("$blurb %-14s %4s (%d/%d);" .
+                          "       \t %.1f secs/link\n",
                           "$name:", "$pct%", $suc, $try, $secs_link);
   }
 }
@@ -2522,13 +2827,13 @@ sub url_only_output() {
 #
 ##############################################################################
 
-my $image_ppm   = sprintf ("%s/webcollage-%08x",
+my $image_ppm   = sprintf ("%s/webcollage-%08x.ppm",
                            ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp"),
                            rand(0xFFFFFFFF));
-my $image_tmp1  = sprintf ("%s/webcollage-1-%08x",
+my $image_tmp1  = sprintf ("%s/webcollage-1-%08x.ppm",
                            ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp"),
                            rand(0xFFFFFFFF));
-my $image_tmp2  = sprintf ("%s/webcollage-2-%08x",
+my $image_tmp2  = sprintf ("%s/webcollage-2-%08x.ppm",
                            ($ENV{TMPDIR} ? $ENV{TMPDIR} : "/tmp"),
                            rand(0xFFFFFFFF));
 
@@ -2641,12 +2946,12 @@ sub image_to_pnm($$$) {
       $body = undef;
     };
 
-    if (($pid = open(PIPE, "| $cmd2 > $output"))) {
+    if (($pid = open (my $pipe, "| $cmd2 > $output"))) {
       $timed_out = 0;
       alarm $cvt_timeout;
-      print PIPE $body;
+      print $pipe $body;
       $body = undef;
-      close PIPE;
+      close $pipe;
 
       LOG ($verbose_exec, "awaiting $pid");
       waitpid ($pid, 0);
@@ -2701,10 +3006,9 @@ sub ppmmake($$$$) {
   my $pixel = pack('CCC', $r, $g, $b);
   my $bits = "P6\n$w $h\n255\n" . ($pixel x ($w * $h));
 
-  local *OUT;
-  open (OUT, ">$outfile") || error ("$outfile: $!");
-  print OUT $bits;
-  close OUT;
+  open (my $out, '>', $outfile) || error ("$outfile: $!");
+  print $out $bits;
+  close $out;
 }
 
 
@@ -2843,12 +3147,12 @@ sub x_or_pbm_output($) {
     my ($iw, $ih);
 
     my $body = "";
-    local *IMG;
-    open(IMG, "<$bgimage") || error "couldn't open $bgimage: $!";
-    my $cmd;
-    while (<IMG>) { $body .= $_; }
-    close (IMG);
+    open (my $imgf, '<', $bgimage) || error "couldn't open $bgimage: $!";
+    local $/ = undef;  # read entire file
+    $body = <$imgf>;
+    close ($imgf);
 
+    my $cmd;
     if ((@_ = gif_size ($body))) {
       ($iw, $ih) = @_;
       $cmd = "giftopnm |";
@@ -2876,10 +3180,10 @@ sub x_or_pbm_output($) {
          "pasting $bgimage (${iw}x$ih) into base image at $x,$y");
 
     $cmd .= "pnmpaste - $x $y $image_ppm > $image_tmp1";
-    open (IMG, "| $cmd") || error "running $cmd: $!";
-    print IMG $body;
+    open ($imgf, "| $cmd") || error "running $cmd: $!";
+    print $imgf $body;
     $body = undef;
-    close (IMG);
+    close ($imgf);
     LOG ($verbose_exec, "subproc exited normally.");
     rename ($image_tmp1, $image_ppm) ||
       error "renaming $image_tmp1 to $image_ppm: $!";
@@ -2937,10 +3241,9 @@ sub paste_image($$$$) {
       return 0;
     }
 
-    local *OUT;
-    open (OUT, ">$image_tmp1") || error ("writing $image_tmp1: $!");
-    print OUT $body || error ("writing $image_tmp1: $!");
-    close OUT || error ("writing $image_tmp1: $!");
+    open (my $out, '>', $image_tmp1) || error ("writing $image_tmp1: $!");
+    (print $out $body) || error ("writing $image_tmp1: $!");
+    close ($out) || error ("writing $image_tmp1: $!");
 
   } else {
     ($iw, $ih) = image_to_pnm ($img, $body, $image_tmp1);
@@ -2972,12 +3275,11 @@ sub paste_image($$$$) {
     rename ($image_tmp2, $image_tmp1);
 
     # re-get the width/height in case the filter resized it.
-    local *IMG;
-    open(IMG, "<$image_tmp1") || return 0;
-    $_ = <IMG>;
-    $_ = <IMG>;
+    open (my $imgf, '<', $image_tmp1) || return 0;
+    $_ = <$imgf>;
+    $_ = <$imgf>;
     ($iw, $ih) = m/^(\d+) (\d+)$/;
-    close (IMG);
+    close ($imgf);
     return 0 unless ($iw && $ih);
   }
 
@@ -3192,7 +3494,7 @@ sub paste_image($$$$) {
       # the next network retrieval, which is probably a better thing
       # to do anyway.
       #
-      $cmd .= " &";
+      $cmd .= " &" unless ($cocoa_p);
 
       $rc = nontrapping_system ($cmd);
 
@@ -3254,10 +3556,10 @@ sub update_imagemap($$$$$$$$) {
   #
   my $template_html = '';
   {
-    local *IN;
-    if (open (IN, "<$imagemap_html")) {
-      while (<IN>) { $template_html .= $_; }
-      close IN;
+    if (open (my $in, '<', $imagemap_html)) {
+      local $/ = undef;  # read entire file
+      $template_html = <$in>;
+      close $in;
       LOG ($verbose_pbm, "read template $imagemap_html");
     }
 
@@ -3307,10 +3609,9 @@ sub update_imagemap($$$$$$$$) {
       $body =~ s@(<SPAN\s+CLASS=\"webcollage_size\">).*?(</SPAN>)@$1$size$2@si;
     }
 
-    local *OUT;
-    open (OUT, ">$imagemap_html_tmp") || error ("$imagemap_html_tmp: $!");
-    print OUT $body                   || error ("$imagemap_html_tmp: $!");
-    close OUT                         || error ("$imagemap_html_tmp: $!");
+    open (my $out, '>', $imagemap_html_tmp) || error ("$imagemap_html_tmp: $!");
+    (print $out $body)                      || error ("$imagemap_html_tmp: $!");
+    close ($out)                            || error ("$imagemap_html_tmp: $!");
     LOG ($verbose_pbm, "wrote $imagemap_html_tmp");
   }
 
@@ -3325,6 +3626,40 @@ sub update_imagemap($$$$$$$$) {
 }
 
 
+# Figure out what the proxy server should be, either from environment
+# variables or by parsing the output of the (MacOS) program "scutil",
+# which tells us what the system-wide proxy settings are.
+#
+sub set_proxy() {
+
+  if (! $http_proxy) {
+    # historical suckage: the environment variable name is lower case.
+    $http_proxy = $ENV{http_proxy} || $ENV{HTTP_PROXY};
+  }
+
+  if (defined ($http_proxy)) {
+    if ($http_proxy && $http_proxy =~ m@^http://([^/]*)/?$@ ) {
+      # historical suckage: allow "http://host:port" as well as "host:port".
+      $http_proxy = $1;
+    }
+
+  } else {
+    my $proxy_data = `scutil --proxy 2>/dev/null`;
+    my ($server) = ($proxy_data =~ m/\bHTTPProxy\s*:\s*([^\s]+)/s);
+    my ($port)   = ($proxy_data =~ m/\bHTTPPort\s*:\s*([^\s]+)/s);
+    # Note: this ignores the "ExceptionsList".
+    if ($server) {
+      $http_proxy = $server;
+      $http_proxy .= ":$port" if $port;
+    }
+  }
+
+  if ($http_proxy) {
+    LOG ($verbose_net, "proxy server: $http_proxy");
+  }
+}
+
+
 sub init_signals() {
 
   $SIG{HUP}  = \&signal_cleanup;
@@ -3355,9 +3690,6 @@ sub main() {
   my $root_p = 0;
   my $window_id = undef;
 
-  # historical suckage: the environment variable name is lower case.
-  $http_proxy = $ENV{http_proxy} || $ENV{HTTP_PROXY};
-
   while ($_ = $ARGV[0]) {
     shift @ARGV;
     if ($_ eq "-display" ||
@@ -3426,6 +3758,9 @@ sub main() {
       } else {
         error ("local directory path must be set")
       }
+    } elsif ($_ eq "-fps") {
+      # -fps only works on MacOS, via "webcollage-cocoa.m".
+      # Ignore it if passed to this script in an X11 context.
     } elsif ($_ eq "-debug" || $_ eq "--debug") {
       my $which = shift @ARGV;
       my @rest = @search_methods;
@@ -3449,7 +3784,8 @@ sub main() {
               "[-root] [-display dpy] [-verbose] [-debug which]\n" .
         "\t\t  [-timeout secs] [-delay secs] [-size WxH]\n" .
         "\t\t  [-no-output] [-urls-only] [-imagemap filename]\n" .
-        "\t\t  [-filter cmd] [-filter2 cmd] [-background color]\n" .
+        "\t\t  [-background color] [-opacity f]\n" .
+        "\t\t  [-filter cmd] [-filter2 cmd]\n" .
         "\t\t  [-dictionary dictionary-file] [-http-proxy host[:port]]\n" .
         "\t\t  [-driftnet [driftnet-program-and-args]]\n" .
         "\t\t  [-directory local-image-directory]\n" .
@@ -3458,14 +3794,6 @@ sub main() {
     }
   }
 
-  if ($http_proxy && $http_proxy eq "") {
-    $http_proxy = undef;
-  }
-  if ($http_proxy && $http_proxy =~ m@^http://([^/]*)/?$@ ) {
-    # historical suckage: allow "http://host:port" as well as "host:port".
-    $http_proxy = $1;
-  }
-
   if (!$root_p && !$no_output_p && !$cocoa_p) {
     print STDERR $copyright;
     error "the -root argument is mandatory (for now.)";
@@ -3543,6 +3871,7 @@ sub main() {
   }
 
   init_signals();
+  set_proxy();
 
   spawn_driftnet ($driftnet_cmd) if ($driftnet_cmd);