package Net::MythWeb;
use Moose;
use MooseX::StrictConstructor;
use DateTime;
use DateTime::Format::Strptime;
use HTML::TreeBuilder::XPath;
use Net::MythWeb::Channel;
use Net::MythWeb::Programme;
use URI::URL;
use WWW::Mechanize;
our $VERSION = '0.33';
has 'hostname' => (
is => 'ro',
isa => 'Str',
default => 'localhost',
);
has 'port' => (
is => 'ro',
isa => 'Int',
default => 80,
);
has 'mechanize' => (
is => 'ro',
isa => 'LWP::UserAgent',
default => sub {
my $ua = WWW::Mechanize->new;
$ua->default_header( 'Accept-Language' => 'en' );
return $ua;
},
);
__PACKAGE__->meta->make_immutable;
sub channels {
my $self = shift;
my @channels;
my $response = $self->_request('/mythweb/settings/tv/channels');
my $tree = HTML::TreeBuilder::XPath->new;
$tree->parse_content( $response->decoded_content );
foreach
my $tr ( $tree->findnodes('//tr[@class="settings"]')->get_nodelist )
{
my @tr_parts = $tr->content_list;
my $number_input = ( $tr_parts[3]->content_list )[0];
my $id = $number_input->attr('id');
$id =~ s/^channum_//;
my $number = $number_input->attr('value');
my $name_input = ( $tr_parts[4]->content_list )[0];
my $name = $name_input->attr('value');
push @channels,
Net::MythWeb::Channel->new(
id => $id,
number => $number,
name => $name,
);
}
return @channels;
}
sub recordings {
my $self = shift;
my @recordings;
my $response = $self->_request('/mythweb/tv/recorded');
my $tree = HTML::TreeBuilder::XPath->new;
$tree->parse_content( $response->decoded_content );
foreach
my $row ( $tree->findnodes('//tr[@class="recorded"]')->get_nodelist )
{
next unless $row->attr('id') =~ /inforow_/;
next if $row->as_HTML =~ /Still Recording/;
my %seen;
foreach my $link ( $tree->findnodes( '//a', $row )->get_nodelist ) {
my $href = $link->attr('href');
next unless $href;
next unless $href =~ m{/detail/};
next if $seen{$href}++;
push @recordings, $self->_programme($href);
}
}
return @recordings;
}
sub programme {
my ( $self, $channel, $start ) = @_;
my $channel_id = $channel->id;
my $start_epoch = $start->epoch;
return $self->_programme("/mythweb/tv/detail/$channel_id/$start_epoch");
}
sub _programme {
my ( $self, $path ) = @_;
my $response = $self->_request($path);
my ( $channel_id, $programme_id ) = $path =~ m{(\d+)/(\d+)};
my $tree = HTML::TreeBuilder::XPath->new;
$tree->parse_content( $response->decoded_content );
my @channel_parts
= $tree->findnodes('//td[@class="x-channel"]/a')->pop->content_list;
my $channel_number = $channel_parts[3]->content->[0];
my $channel_name = $channel_parts[5];
$channel_name =~ s/^ +//;
$channel_name =~ s/ +$//;
my $channel = Net::MythWeb::Channel->new(
id => $channel_id,
number => $channel_number,
name => $channel_name
);
my @title_parts
= $tree->findnodes('//td[id("x-title")]/a')->pop->content_list;
my $title = $title_parts[0];
my $subtitle = $title_parts[2] || '';
my $year = DateTime->from_epoch( epoch => $programme_id )->year;
my $strptime = DateTime::Format::Strptime->new(
pattern => '%Y %a, %b %d, %I:%M %p',
locale => 'en_GB',
on_error => 'croak',
);
# Sun, Jun 14, 10:00 PM to 11:00 PM (75 mins)
my @time_parts
= $tree->findnodes('//div[id("x-time")]')->pop->content_list;
my $time_text = $time_parts[0];
my ( $start_text, $stop_text ) = split ' to ', $time_text;
$start_text = "$year $start_text";
my $start = $strptime->parse_datetime($start_text);
$stop_text =~ s/ \(.+$//;
my $strptime2 = DateTime::Format::Strptime->new(
pattern => '%I:%M %p',
locale => 'en_GB',
on_error => 'croak',
);
my $time = $strptime2->parse_datetime($stop_text);
my $stop = DateTime->new(
year => $start->year,
month => $start->month,
day => $start->day,
hour => $time->hour,
minute => $time->minute,
);
# programme runs over midnight
if ( $stop < $start ) {
$stop->add( days => 1 );
}
my @description_parts
= $tree->findnodes('//td[id("x-description")]')->pop->content_list;
my $description = $description_parts[0];
$description =~ s/^ +//;
$description =~ s/ +$//;
return Net::MythWeb::Programme->new(
id => $programme_id,
channel => $channel,
start => $start,
stop => $stop,
title => $title,
subtitle => $subtitle,
description => $description,
mythweb => $self,
);
}
sub _download_programme {
my ( $self, $programme, $filename ) = @_;
my $uri
= $self->_uri( '/mythweb/pl/stream/'
. $programme->channel->id . '/'
. $programme->id );
my $mirror_response
= $self->mechanize->get( $uri, ':content_file' => $filename );
confess( $mirror_response->status_line )
unless $mirror_response->is_success;
}
sub _delete_programme {
my ( $self, $programme ) = @_;
$self->_request( '/mythweb/tv/recorded?delete=yes&chanid='
. $programme->channel->id
. '&starttime='
. $programme->id );
}
sub _record_programme {
my ( $self, $programme, $start_extra, $stop_extra ) = @_;
$start_extra ||= 0;
$stop_extra ||= 0;
my $channel_id = $programme->channel->id;
my $programme_id = $programme->id;
$self->_request("/mythweb/tv/detail/$channel_id/$programme_id");
$self->mechanize->submit_form(
form_name => 'program_detail',
fields => {
record => 1,
startoffset => $start_extra,
endoffset => $stop_extra,
},
button => 'save',
);
}
sub _request {
my ( $self, $path ) = @_;
my $uri = $self->_uri($path);
my $response = $self->mechanize->get($uri);
confess( "Error fetching $uri: " . $response->status_line )
unless $response->is_success;
return $response;
}
sub _uri {
my ( $self, $path ) = @_;
return 'http://' . $self->hostname . ':' . $self->port . $path;
}
__END__
=head1 NAME
Net::MythWeb - Interface to MythTV via MythWeb
=head1 SYNOPSIS
use Net::MythWeb;
my $mythweb = Net::MythWeb->new( hostname => 'owl.local', port => 80 );
foreach my $channel ( $mythweb->channels ) {
print $channel->name . "\n";
}
foreach my $recording ( $mythweb->recordings ) {
print $recording->channel->id, ', ', $recording->channel->number, ', ',
$recording->channel->name, "\n";
print $recording->start, ' -> ', $recording->stop, ': ', $recording->title,
', ',
$recording->subtitle, ', ',
$recording->description;
$recording->download("recording.mpg");
$recording->delete;
}
my $programme = $mythweb->programme( $channel, $start_as_datetime );
$programme->record;
=head1 DESCRIPTION
This module provides a simple interface to MythTV by making HTTP
requests to its MythWeb web server front end. MythTV is a free
open source digital video recorder. Find out more at
L.
This module allows you to query the recordings, download
them to a local file and schedule new recordings.
=head1 METHODS
=head2 new
The constructor takes a hostname and port:
my $mythweb = Net::MythWeb->new( hostname => 'owl.local', port => 80 );
=head2 channels
List the channels and return them as L objects:
foreach my $channel ( $mythweb->channels ) {
print $channel->name . "\n";
}
=head2 recordings
List the recordings and return them as L objects:
foreach my $recording ( $mythweb->recordings ) {
print $recording->channel->id, ', ', $recording->channel->number, ', ',
$recording->channel->name, "\n";
print $recording->start, ' -> ', $recording->stop, ': ', $recording->title,
', ',
$recording->subtitle, ', ',
$recording->description;
$recording->download("recording.mpg");
$recording->delete;
}
=head2 programme
Returns a L for the programme which starts
at a given time on the channel:
my $programme = $mythweb->programme( $channel, $start_as_datetime );
$programme->record;
=head1 SEE ALSO
L, L.
=head1 AUTHOR
Leon Brocard .
=head1 COPYRIGHT
Copyright (C) 2009, Leon Brocard
=head1 LICENSE
This module is free software; you can redistribute it or modify it
under the same terms as Perl itself.