From 61a16fa11d392392694c9989342b580be76439b8 Mon Sep 17 00:00:00 2001 From: Alex 'AdUser' Z Date: Sun, 14 Dec 2014 20:59:45 +1000 Subject: [PATCH] * initial --- .gitignore | 1 + cache/.keepme | 0 lib/Livecam.pm | 24 +++++ lib/Livecam/Main.pm | 148 ++++++++++++++++++++++++++++++ public/.keepme | 0 script/livecam | 14 +++ t/basic.t | 7 ++ templates/layouts/default.html.ep | 5 + 8 files changed, 199 insertions(+) create mode 100644 .gitignore create mode 100644 cache/.keepme create mode 100644 lib/Livecam.pm create mode 100644 lib/Livecam/Main.pm create mode 100644 public/.keepme create mode 100755 script/livecam create mode 100644 t/basic.t create mode 100644 templates/layouts/default.html.ep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e934adf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cache/ diff --git a/cache/.keepme b/cache/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/lib/Livecam.pm b/lib/Livecam.pm new file mode 100644 index 0000000..f61f798 --- /dev/null +++ b/lib/Livecam.pm @@ -0,0 +1,24 @@ +package Livecam; +use Mojo::Base 'Mojolicious'; + +sub startup { + my ($self) = @_; + + my $r = $self->routes; + + $r->get('/')->to(cb => sub { + shift->redirect_to('/playlist') + }); + + $self->app->attr(ua => sub { + require Mojo::UserAgent; + my $ua = Mojo::UserAgent->new; + $ua->name(qq{Mozilla/5.0 (X11; Linux i686; rv:24.0) Gecko/20140925 Firefox/24.0 Iceweasel/24.8.1}); + return $ua; + }); + + $r->route('/playlist') -> via('GET') -> to('main#playlist'); + $r->route('/livecam') -> via('GET') -> to('main#livecam'); +} + +1; diff --git a/lib/Livecam/Main.pm b/lib/Livecam/Main.pm new file mode 100644 index 0000000..bd8e6b0 --- /dev/null +++ b/lib/Livecam/Main.pm @@ -0,0 +1,148 @@ +package Livecam::Main; + +use Graphics::Magick; +use Mojo::Base 'Mojolicious::Controller'; +use Mojo::Asset::File; +use Mojo::JSON; + +sub _pls_cache_path { + my ($self, $name) = @_; + return $self->app->home->rel_file('cache/playlist.json'); +} + +sub _pls_cache_save { + my ($self, $data) = @_; + + my $asset = Mojo::Asset::File->new(path => $self->_pls_cache_path); + unlink $asset->path; + $asset->add_chunk(Mojo::JSON->new->encode($data)); + $asset->cleanup(0); + return 1; +} + +sub _pls_cache_load { + my ($self) = @_; + + my $asset = Mojo::Asset::File->new(path => $self->_pls_cache_path); + return Mojo::JSON->new->decode($asset->slurp); +}; + +sub _cam_cache_path { + my ($self, $name) = @_; + $name =~ y|/|+|; + return $self->app->home->rel_file("cache/cam-$name.jpg"); +} + +sub _cam_cache_save { + my ($self, $name, $asset) = @_; + + $asset->move_to($self->_cam_cache_path($name)); + return 1; +} + +sub _cam_cache_load { + my ($self, $name) = @_; + my $path = $self->_cam_cache_path($name); + return unless (-f $path); + my $mtime = (stat($path))[9]; + return if ($mtime + 20 < time()); # expire in 20 seconds + return Mojo::Asset::File->new(path => $path); +} + +sub playlist { + my ($self) = @_; + + # fetch cam list + my $domain = "http://live.podryad.tv"; + my @pages = ("", "page/2", "page/3", "page/4"); + my @cams; + foreach my $page (@pages) { + $self->app->ua->get("$domain/$page")->res->dom('div.cam-preview')->each(sub { + my $title = $_->at('div.title a')->text; + my ($id) = $_->at('div.image img')->{src} =~ m|/cams/snap-(.+)\.jpg|oi; + return unless ($title and $id); + my $url = sprintf "%s/camws/cam-image.php?cam=%s&r=%.15f", $domain, $id, rand(1); + push @cams, {id => $id, title => $title, url => $url}; + }); + } + + $self->_pls_cache_save(\@cams); + my $playlist = "#EXTM3U aspect-ratio=4:3\n"; + + # gen playlists + foreach my $cam (@cams) { + $playlist .= sprintf "#EXTINF:-1 %s,%s\n", $cam->{id}, $cam->{title}; + my $url = $self->req->url->clone; + $url->path('/livecam'); + $url->query->param(id => $cam->{id}); + $playlist .= sprintf "%s\n", $url->to_abs; + } + + $self->render_text($playlist); +} + +sub livecam { + my ($self) = @_; + my $id = $self->req->param('id'); + my $cache = $self->_pls_cache_load; + + unless ($id and $cache) { + $self->res->code(404); + $self->render_text('Cam id not set or cache is empty'); + return; + } + + my $cam; + foreach my $entry (@$cache) { + next unless $entry->{id} eq $id; + $cam = $entry; + } + + unless ($cam) { + $self->res->code(404); + $self->render_text('Cam not found'); + return; + } + + $self->res->headers->content_type('multipart/x-mixed-replace;boundary=--Frame'); + + srand(time()); + my $url = Mojo::URL->new; + $url->scheme('http'); + $url->host('live.podryad.tv'); + $self->app->ua->get($url->to_abs); # setup cookies + $url->path('/camws/cam-image.php'); + + my $send_image = sub { + my $seed = sprintf "%.15f", rand(1); + $url->query(cam => $cam->{id}, r => $seed); + my $asset = $self->_cam_cache_load($cam->{id}); + unless ($asset) { + $self->app->log->debug("fetch new frame from $cam->{id}"); + $asset = $self->app->ua->get($url->to_abs)->res->content->asset; + $self->_cam_cache_save($cam->{id}, $asset) if $asset; + } + $self->write_chunk("--Frame\r\n"); + $self->write_chunk("Content-Type: image/jpg\r\n"); + $self->write_chunk("Content-Length: " . $asset->size . "\r\n"); + $self->write_chunk("\r\n"); + $self->write_chunk($asset->slurp()); + $self->write_chunk("\r\n" x 2); + my $msg = sprintf "Send frame from cam %s with size %d", $cam->{id}, $asset->size; + $self->app->log->debug($msg); + }; + + $send_image->(); # initial send + + Mojo::IOLoop->stream($self->tx->connection)->timeout(30); + Mojo::IOLoop->singleton->reactor->on(error => sub { + my ($reactor, $err) = @_; + $self->app->log->error($err); + }); + + my $timer = Mojo::IOLoop->recurring(3 => \&$send_image); + + $self->on(finish => sub { Mojo::IOLoop->remove($timer) }); +} + +1; diff --git a/public/.keepme b/public/.keepme new file mode 100644 index 0000000..e69de29 diff --git a/script/livecam b/script/livecam new file mode 100755 index 0000000..b2d09bf --- /dev/null +++ b/script/livecam @@ -0,0 +1,14 @@ +#!/usr/bin/env perl +use Mojo::Base -strict; + +use File::Basename 'dirname'; +use File::Spec::Functions qw(catdir splitdir); + +# Source directory has precedence +my @base = (splitdir(dirname(__FILE__)), '..'); +my $lib = join('/', @base, 'lib'); +-e catdir(@base, 't') ? unshift(@INC, $lib) : push(@INC, $lib); + +# Start commands for application +require Mojolicious::Commands; +Mojolicious::Commands->start_app('Livecam'); diff --git a/t/basic.t b/t/basic.t new file mode 100644 index 0000000..9d6502e --- /dev/null +++ b/t/basic.t @@ -0,0 +1,7 @@ +use Mojo::Base -strict; + +use Test::More tests => 3; +use Test::Mojo; + +my $t = Test::Mojo->new('Livecam'); +$t->get_ok('/')->status_is(200)->content_like(qr/Mojolicious/i); diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep new file mode 100644 index 0000000..599c556 --- /dev/null +++ b/templates/layouts/default.html.ep @@ -0,0 +1,5 @@ + + + <%= title %> + <%= content %> +