#!/usr/bin/env perl
#
# The world's most insecure web-based PVR manager and streaming proxy for get_iplayer
# ** WARNING ** Never run this in an untrusted environment or facing the internet
#
# Copyright (C) 2008-2010 Phil Lewis, 2010- get_iplayer contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# Authors: Phil Lewis, get_iplayer contributors
# Web: https://github.com/get-iplayer/get_iplayer/wiki
# License: GPLv3 (see LICENSE.txt)
#
my $VERSION = 3.35;
my $VERSION_TEXT;
$VERSION_TEXT = sprintf("v%.2f", $VERSION) unless $VERSION_TEXT;
use CGI qw(-utf8 :all);
use CGI::Cookie;
use Cwd 'abs_path';
use Encode qw(:DEFAULT :fallback_all);
use Getopt::Long;
use File::Basename;
use File::Copy;
use HTML::Entities;
use IO::File;
use IO::Handle;
use IPC::Open3;
use LWP::ConnCache;
#use LWP::Debug qw(+);
use Unicode::Normalize;
use LWP::UserAgent;
use PerlIO::encoding;
use strict;
use constant FB_EMPTY => sub { '' };
use constant IS_WIN32 => $^O eq 'MSWin32' ? 1 : 0;
use constant DEFAULT_THUMBNAIL => "https://ichef.bbci.co.uk/images/ic/480xn/p01tqv8z.png";
$PerlIO::encoding::fallback = XMLCREF;
# suppress Perl 5.22/CGI 4 warning
$CGI::LIST_CONTEXT_WARN = 0;
$| = 1;
my $opt_cmdline;
$opt_cmdline->{debug} = 0;
# Allow bundling of single char options
Getopt::Long::Configure ("bundling");
# cmdline opts take precedence
GetOptions(
"help|h" => \$opt_cmdline->{help},
"listen|address|l=s" => \$opt_cmdline->{listen},
"port|p=n" => \$opt_cmdline->{port},
"getiplayer|get_iplayer|g=s" => \$opt_cmdline->{getiplayer},
"ffmpeg=s" => \$opt_cmdline->{ffmpeg},
"debug" => \$opt_cmdline->{debug},
"baseurl|base-url|b=s" => \$opt_cmdline->{baseurl},
"encodinglocale|encoding-locale=s" => \$opt_cmdline->{encodinglocale},
"encodinglocalefs|encoding-locale-fs=s" => \$opt_cmdline->{encodinglocalefs},
"encodingconsoleout|encoding-console-out=s" => \$opt_cmdline->{encodingconsoleout},
"encodingconsolein|encoding-console-in=s" => \$opt_cmdline->{encodingconsolein},
"encodingwebrequest|encoding-webrequest=s" => \$opt_cmdline->{encodingwebrequest},
) || die usage();
# Display usage if old method of invocation is used or --help
usage() if $opt_cmdline->{help} || @ARGV;
# Usage
sub usage {
my $text = "get_iplayer Web PVR Manager $VERSION_TEXT\n";
$text .= <<'EOF';
Copyright (C) 2008-2010 Phil Lewis, 2010- get_iplayer contributors
This program comes with ABSOLUTELY NO WARRANTY; This is free software,
and you are welcome to redistribute it under certain conditions;
See the GPLv3 for details.
Options:
--listen,-l Use the built-in web server and listen on this interface address (default: 0.0.0.0)
--port,-p Use the built-in web server and listen on this TCP port
--getiplayer,-g Path to the get_iplayer script
--ffmpeg Path to the ffmpeg binary (for streaming)
--debug Debug mode
--baseurl,-b Base URL for link generation. Set to full proxy URL if running behind reverse proxy.
--help,-h This help text
--encodinglocale Encoding for command line (default: Linux/Unix/OSX = UTF-8, Windows = cp1252)
--encodinglocalefs Encoding for file names (default: Linux/Unix/OSX = UTF-8, Windows = cp1252)
--encodingconsoleout Encoding for STDOUT/STDERR (default: Linux/Unix/OSX = UTF-8, Windows = cp850)
--encodingconsolein Encoding for STDIN (default: Linux/Unix/OSX = UTF-8, Windows = cp850)
--encodingwebrequest Encoding for requests to get_iplayer (default: Linux/Unix/OSX = UTF-8, Windows = UTF-8)
EOF
print $text;
exit 1;
}
# fallback encodings
$opt_cmdline->{encodinglocale} = $opt_cmdline->{encodinglocalefs} = default_encodinglocale();
$opt_cmdline->{encodingconsoleout} = $opt_cmdline->{encodingconsolein} = default_encodingconsoleout();
$opt_cmdline->{encodingwebrequest} = default_encodingwebrequest();
# attempt to automatically determine encodings
eval {
require Encode::Locale;
};
if (!$@) {
# set encodings unless already set by PERL_UNICODE or perl -C
$opt_cmdline->{encodinglocale} = $Encode::Locale::ENCODING_LOCALE unless (${^UNICODE} & 32);
$opt_cmdline->{encodinglocalefs} = $Encode::Locale::ENCODING_LOCALE_FS unless (${^UNICODE} & 32);
$opt_cmdline->{encodingconsoleout} = $Encode::Locale::ENCODING_CONSOLE_OUT unless (${^UNICODE} & 6);
$opt_cmdline->{encodingconsolein} = $Encode::Locale::ENCODING_CONSOLE_IN unless (${^UNICODE} & 1);
}
binmode(STDOUT, ":encoding($opt_cmdline->{encodingconsoleout})");
binmode(STDERR, ":encoding($opt_cmdline->{encodingconsoleout})");
binmode(STDIN, ":encoding($opt_cmdline->{encodingconsolein})");
my $fh;
# Send log messages to this fh
my $se = *STDERR;
binmode $se, ":encoding($opt_cmdline->{encodingconsoleout})";
for my $key ( keys %{$opt_cmdline} ) {
# decode @ARGV unless already decoded by PERL_UNICODE or perl -C
unless ( ${^UNICODE} & 32 ) {
$opt_cmdline->{$key} = decode_cl($opt_cmdline->{$key});
}
# compose UTF-8 args if necessary
if ( $opt_cmdline->{encodinglocale} =~ /UTF-?8/i ) {
$opt_cmdline->{$key} = NFKC($opt_cmdline->{$key});
}
}
# Some defaults
my $default_modes = 'default';
$opt_cmdline->{listen} = '0.0.0.0' if ! $opt_cmdline->{listen};
$opt_cmdline->{baseurl} .= "/" if $opt_cmdline->{baseurl} && $opt_cmdline->{baseurl} !~ m{/$};
$opt_cmdline->{ffmpeg} = encode_fs($opt_cmdline->{ffmpeg}) || 'ffmpeg';
$opt_cmdline->{getiplayer} = encode_fs($opt_cmdline->{getiplayer}) if $opt_cmdline->{getiplayer};
# Search for get_iplayer
if ( ! $opt_cmdline->{getiplayer} ) {
for ( './get_iplayer', './get_iplayer.cmd', './get_iplayer.pl', '/usr/bin/get_iplayer', '/usr/local/bin/get_iplayer' ) {
$opt_cmdline->{getiplayer} = $_ if -x $_;
}
}
if ( ( ! $opt_cmdline->{getiplayer} ) || ! -f $opt_cmdline->{getiplayer} ) {
print "ERROR: Cannot find get_iplayer, please specify its location using the --getiplayer option.\n";
exit 2;
}
my @gip_cmd_base = (
decode_fs($opt_cmdline->{getiplayer}),
'--encoding-webrequest='.$opt_cmdline->{encodingwebrequest},
'--encoding-console-out=UTF-8',
'--nocopyright',
'--expiry=999999999',
);
# Path to get_iplayer (+ set HOME env var cos apache seems to not set it)
my $home = $ENV{HOME};
my %prog;
my @pids;
my @displaycols;
# Field names to be grabbed from get_iplayer
my @headings = qw(
index
thumbnail
pid
available
expires
type
name
episode
versions
duration
desc
channel
categories
timeadded
guidance
web
seriesnum
episodenum
filename
mode
);
# Default Displayed headings
my @headings_default = qw( thumbnail type name episode desc channel timeadded );
# Lookup table for nice field name headings
my %fieldname = (
index => 'Index',
pid => 'PID',
available => 'Available',
expires => 'Expires',
type => 'Type',
name => 'Name',
episode => 'Episode',
versions => 'Versions',
duration => 'Duration',
desc => 'Description',
channel => 'Channel',
categories => 'Categories',
thumbnail => 'Image',
timeadded => 'Time Added',
guidance => 'Guidance',
web => 'Web Page',
pvrsearch => 'PVR Search',
comment => 'Comment',
filename => 'Filename',
mode => 'Mode',
seriesnum => 'Series Number',
episodenum => 'Episode Number',
'name,episode' => 'Name+Episode',
'name,episode,desc' => 'Name+Episode+Desc',
);
my %cols_order = ();
my %cols_names = ();
my %prog_types = (
tv => 'BBC TV',
radio => 'BBC Radio'
);
my %prog_types_order = (
1 => 'tv',
2 => 'radio'
);
my $icons_base_url = './icons/';
my $cgi;
my $nextpage;
# Page routing based on NEXTPAGE CGI parameter
my %nextpages = (
'search_progs' => \&search_progs, # Main Programme Listings
'search_history' => \&search_history, # Recorded Programme Listings
'pvr_queue' => \&pvr_queue, # Queue Recording of Selected Progs
'recordings_delete' => \&recordings_delete, # Delete Files for Selected Recordings
'pvr_list' => \&show_pvr_list, # Show all current PVR searches
'pvr_del' => \&pvr_del, # Delete selected PVR searches
'pvr_add' => \&pvr_add,
'pvr_edit' => \&pvr_edit,
'pvr_save' => \&pvr_save,
'pvr_run' => \&pvr_run,
'record_now' => \&record_now,
'show_info' => \&show_info,
'refresh' => \&refresh,
);
##### Options #####
my $opt;
# Options Layout on page tabs
my $layout;
$layout->{BASICTAB}->{title} = 'Search Options',
$layout->{BASICTAB}->{heading} = 'Search Options:',
$layout->{BASICTAB}->{order} = [ qw/ SEARCH SEARCHFIELDS PROGTYPES HISTORY URL / ];
$layout->{SEARCHTAB}->{title} = 'Advanced Search';
$layout->{SEARCHTAB}->{heading} = 'Advanced Search Options:';
$layout->{SEARCHTAB}->{order} = [ qw/ EXCLUDE CATEGORY EXCLUDECATEGORY CHANNEL EXCLUDECHANNEL SINCE BEFORE FUTURE / ],
$layout->{DISPLAYTAB}->{title} = 'Display';
$layout->{DISPLAYTAB}->{heading} = 'Display Options:';
$layout->{DISPLAYTAB}->{order} = [ qw/ SORT REVERSE PAGESIZE HIDE HIDEDELETED / ];
$layout->{COLUMNSTAB}->{title} = 'Columns';
$layout->{COLUMNSTAB}->{heading} = 'Column Options:';
$layout->{COLUMNSTAB}->{order} = [ qw/ COLS / ];
$layout->{RECORDINGTAB}->{title} = 'Recording';
$layout->{RECORDINGTAB}->{heading} = 'Recording Options:';
$layout->{RECORDINGTAB}->{order} = [ qw/ OUTPUT VERSIONLIST MODES PROXY SUBTITLES METADATA THUMB PVRHOLDOFF FORCE AUTOWEBREFRESH AUTOPVRRUN REFRESHFUTURE FPS25 / ];
$layout->{STREAMINGTAB}->{title} = 'Streaming';
$layout->{STREAMINGTAB}->{heading} = 'Streaming Options:';
$layout->{STREAMINGTAB}->{order} = [ qw/ BITRATE VSIZE VFR STREAMTYPE / ];
$layout->{HIDDENTAB}->{title} = '';
$layout->{HIDDENTAB}->{heading} = '';
$layout->{HIDDENTAB}->{order} = [ qw/ SAVE SEARCHTAB COLUMNSTAB DISPLAYTAB RECORDINGTAB STREAMINGTAB PAGENO INFO NEXTPAGE ACTION / ];
# Order of displayed tab buttoms (BASICTAB and HIDDEN are always displayed regardless of order)
$layout->{taborder} = [ qw/ BASICTAB SEARCHTAB DISPLAYTAB COLUMNSTAB RECORDINGTAB STREAMINGTAB HIDDENTAB / ];
# Any params that should never get into the get_iplayer pvr-add search
my @nosearch_params = qw/ /;
### Perl CGI Web Server ###
use Socket;
use IO::Socket;
use POSIX ":sys_wait_h";
my $IGNOREEXIT = 0;
# If the port number is specified then run embedded web server
if ( $opt_cmdline->{port} > 0 ) {
# Autoreap zombies
$SIG{CHLD} = 'IGNORE';
# Need this because with $SIG{CHLD} = 'IGNORE', backticks and systems calls always return -1
$IGNOREEXIT = 1;
for (;;) {
# Setup and create socket
my $server = new IO::Socket::INET(
Proto => 'tcp',
LocalAddr => $opt_cmdline->{listen},
LocalPort => $opt_cmdline->{port},
Listen => SOMAXCONN,
Reuse => 1,
);
$server or die "Unable to create server socket: $!";
print $se "INFO: Listening on $opt_cmdline->{listen}:$opt_cmdline->{port}\n";
print $se "WARNING: Insecure Remote access is allowed, use --listen=127.0.0.1 to limit to this host only\n" if $opt_cmdline->{listen} ne '127.0.0.1';
print $se "INFO: Using base URL $opt_cmdline->{baseurl}\n" if $opt_cmdline->{baseurl};
# Await requests and handle them as they arrive
while (my $client = $server->accept()) {
my $procid = fork();
die "Cannot fork" unless defined $procid;
# Parent
if ( $procid ) {
close $client;
# must call waitpid() on Windows
if ( IS_WIN32 ) {
while ( abs(waitpid(-1, WNOHANG)) > 1 ) {}
}
next;
}
# Child
binmode $se, ":encoding($opt_cmdline->{encodingconsoleout})";
$client->autoflush(1);
my %request = ();
my $query_string;
my %data;
{
# Read Request
local $/ = Socket::CRLF;
while (<$client>) {
# Main http request
chomp;
if (/\s*(\w+)\s*([^\s]+)\s*HTTP\/(\d.\d)/) {
$request{METHOD} = uc $1;
$request{URL} = $2;
$request{HTTP_VERSION} = $3;
# Standard headers
} elsif (/:/) {
my ( $type, $val ) = split /:/, $_, 2;
$type =~ s/^\s+//;
for ($type, $val) {
s/^\s+//;
s/\s+$//;
}
$request{lc $type} = $val;
print "REQUEST HEADER: $type: $val\n" if $opt_cmdline->{debug};
# POST data
} elsif (/^$/) {
read( $client, $request{CONTENT}, $request{'content-length'} ) if defined $request{'content-length'};
last;
}
}
}
# Determine method and parse parameters
if ($request{METHOD} eq 'GET') {
if ($request{URL} =~ /(.*)\?(.*)/) {
$request{URL} = $1;
$request{CONTENT} = $2;
$query_string = $request{CONTENT};
}
$data{"_method"} = "GET";
} elsif ($request{METHOD} eq 'POST') {
$query_string = parse_post_form_string( $request{CONTENT} );
$data{"_method"} = "POST";
} else {
$data{"_method"} = "ERROR";
}
# Log Request
print $se "$data{_method}: $request{URL}\n";
# Is this the CGI or some other file request?
if ( $request{URL} =~ /^\/?(recordings_delete|playlist.+|genplaylist.+|)\/?$/ ) {
# remove any vars that might affect the CGI
#%ENV = ();
@ARGV = ();
# Setup CGI http vars
print $se "QUERY_STRING = $query_string\n" if defined $query_string;
$ENV{'QUERY_STRING'} = $query_string;
$ENV{'REQUEST_URI'} = $request{URL};
$ENV{'COOKIE'} = $request{cookie};
$ENV{'SERVER_PORT'} = $opt_cmdline->{port};
my $request_host = "http://$request{host}/";
if ( $opt_cmdline->{baseurl} ) {
$ENV{'REQUEST_URI'} = $opt_cmdline->{baseurl};
$request_host = $opt_cmdline->{baseurl};
}
# respond OK to browser
print $client "HTTP/1.1 200 OK", Socket::CRLF;
# Invoke CGI
run_cgi( $client, $query_string, $request{URL}, $request_host );
# Else 404
} else {
print $se "ERROR: 404 Not Found\n";
print $client "HTTP/1.1 404 Not Found", Socket::CRLF;
print $client Socket::CRLF;
print $client "
404 Not Found";
$data{"_status"} = "404";
}
# Close Connection
close $client;
# Exit child
exit 0;
}
}
# If we're running as a proper CGI from a web server...
} else {
# If we were called by a webserver and not the builtin webserver then seed some vars
my $prefix = $ENV{REQUEST_URI};
my $request_uri;
# remove trailing query
$prefix =~ s/\?.*$//gi;
my $query_string = $ENV{QUERY_STRING};
my $request_host = "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}${prefix}";
# determine whether http or https
my $request_protocol = 'http';
if ( defined $ENV{'HTTPS'} ) {
$request_protocol = $ENV{'HTTPS'}=='on'?'https':'http';
}
my $request_host = "${request_protocol}://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}${prefix}";
$home = $ENV{HOME};
# Read POSTed data from STDIN if this is a form POST
if ( $ENV{REQUEST_METHOD} eq 'POST' ) {
my $content;
while ( ) {
$content .= $_;
}
$query_string = parse_post_form_string( $content );
}
run_cgi( *STDOUT, $query_string, undef, $request_host );
}
exit 0;
sub default_encodinglocale {
return 'UTF-8' if (${^UNICODE} & 32);
return (IS_WIN32 ? 'cp1252' : 'UTF-8');
}
sub default_encodingconsoleout {
return 'UTF-8' if (${^UNICODE} & 6);
return (IS_WIN32 ? 'cp850' : 'UTF-8');
}
sub default_encodingwebrequest {
return 'UTF-8';
}
sub encode_fs {
return encode($opt_cmdline->{encodinglocalefs}, shift, FB_EMPTY);
}
sub decode_fs {
return decode($opt_cmdline->{encodinglocalefs}, shift, FB_EMPTY);
}
sub encode_cl {
return encode($opt_cmdline->{encodinglocale}, shift, FB_EMPTY);
}
sub decode_cl {
return decode($opt_cmdline->{encodinglocale}, shift, FB_EMPTY);
}
sub encode_wr {
return encode($opt_cmdline->{encodingwebrequest}, shift, FB_EMPTY);
}
sub decode_wr {
return decode($opt_cmdline->{encodingwebrequest}, shift, FB_EMPTY);
}
sub cleanup {
my $signal = shift;
print $se "INFO: Cleaning up PID $$ (signal = $signal)\n";
exit 0;
}
# wrap HTML::Entities::encode_entities to limit encoding
sub encode_entities {
my $value = shift;
return HTML::Entities::encode_entities( $value, '&<>"\'' );
}
sub parse_post_form_string {
my $form = $_[0];
my @data;
while ( $form =~ /Content-Disposition:(.+?)--/sg ) {
$_ = $1;
# form-data; name = "KEY"
m{name.+?"(.+?)"[\n\r\s]*(.+)}sg;
my ($key, $val) = ( $1, $2 );
next if ! $1;
$val =~ s/[\r\n]//g;
$val =~ s/\+/ /g;
# Decode entities first
decode_entities($val);
# url encode each entry
# $val =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
$val = CGI::escape($val);
push @data, "$key=$val";
}
return join '&', @data;
}
sub run_cgi {
# Get filehandle for output
$fh = shift;
binmode $fh, ':utf8';
my $query_string = shift;
my $request_uri = shift;
my $request_host = shift;
# Clean globals
%prog = ();
@pids = ();
@displaycols = ();
# new cgi instance
$cgi->delete_all() if defined $cgi;
$cgi = new CGI( $query_string );
# Get next page
$nextpage = $cgi->param( 'NEXTPAGE' ) || 'search_progs';
# Process All options
process_params();
# Set HOME env var for forked processes
$ENV{HOME} = $home;
my $action = $cgi->param( 'ACTION' ) || $request_uri;
# Strip the leading '/' to get the action
$action =~ s|^\/||g;
# Stream from file (optionally transcoding if required)
if ( $action eq 'direct' || $action eq 'playdirect' ) {
binmode $fh, ':raw';
# get filename first
my $progtype = $cgi->param( 'PROGTYPES' );
my $pid = $cgi->param( 'PID' );
my $mode = $cgi->param( 'MODES' );
my $filename = get_direct_filename( $pid, $mode, $progtype );
my $ext = lc( $cgi->param('STREAMTYPE') || $cgi->param( 'OUTTYPE' ) );
# get file source ext
my $src_ext = $filename;
$src_ext =~ s/^.*\.//g;
# Stream mime types
my %mimetypes = (
aac => 'audio/aac',
adts => 'audio/aac',
flac => 'audio/x-flac',
m4a => 'audio/mp4',
mp3 => 'audio/mpeg',
oga => 'audio/vorbis',
wav => 'audio/x-wav',
asf => 'video/x-ms-asf',
avi => 'video/avi',
flv => 'video/x-flv',
matroska => 'video/x-matroska',
mkv => 'video/x-matroska',
mov => 'video/quicktime',
mp4 => 'video/mp4',
mpegts => 'video/MP2T',
rm => 'audio/x-pn-realaudio',
ts => 'video/MP2T',
);
# default recipes
my $notranscode = 0;
# Disable transcoding if none is specified as OUTTYPE/STREAMTYPE
# Or if streaming MP4 via play direct
if ( $ext =~ /none/i ) {
print $se "INFO: Transcoding disabled (OUTTYPE=$ext)\n";
$ext = $src_ext;
$notranscode = 1;
# Else known types re-mux into flv unless play direct
} elsif ( $action ne 'playdirect' && ! $ext && $src_ext =~ m{^(m4a|mp4|mp3|aac|avi|mkv|mov|ts)$} ) {
$ext = 'flv';
# Else default to no transcoding
} elsif ( ! $ext ) {
$ext = $src_ext;
}
print $se "INFO: Streaming OUTTYPE:$ext MIMETYPE=$mimetypes{$ext} FILE:$filename to client\n";
# If type is defined
if ( $mimetypes{$ext} ) {
# Output headers
# to stream
# This will enable seekable -Accept_Ranges=>'bytes',
my $headers = $cgi->header( -type => $mimetypes{$ext}, -Connection => 'close' );
# Send the headers to the browser
print $se "\r\nHEADERS:\n$headers\n"; #if $opt_cmdline->{debug};
print $fh $headers;
stream_file( $filename, $mimetypes{$ext}, $src_ext, $ext, $notranscode, $cgi->param( 'BITRATE' ), $cgi->param( 'VSIZE' ), $cgi->param( 'VFR' ) );
} else {
print $se "ERROR: Aborting client thread - output mime type is undetermined\n";
}
# Get a playlist for a specified 'PROGTYPES'
} elsif ( $action eq 'playlistdirect' || $action eq 'playlistfiles' ) {
# Output headers
my $headers = $cgi->header( -type => 'audio/x-mpegurl' );
# To save file
#my $headers = $cgi->header( -type => 'audio/x-mpegurl', -attachment => 'get_iplayer.m3u' );
# Send the headers to the browser
print $se "\r\nHEADERS:\n$headers\n"; #if $opt_cmdline->{debug};
print $fh $headers;
# determine output type
my $outtype = $cgi->param('OUTTYPE');
$outtype = $cgi->param('STREAMTYPE') || $cgi->param('OUTTYPE') if $action eq 'playlistdirect';
# ( host, outtype, modes, progtype, bitrate, search, searchfields, action )
print $fh create_playlist_m3u_single( $request_host, $outtype, $opt->{MODES}->{current}, $opt->{PROGTYPES}->{current} , $cgi->param('BITRATE') || '', $opt->{SEARCH}->{current}, $opt->{SEARCHFIELDS}->{current} || 'name', $opt->{VERSIONLIST}->{current}, $action );
# Get a playlist for a selected progs in form
} elsif ( $action eq 'genplaylistdirect' || $action eq 'genplaylistfile' ) {
# Output headers
my $headers = $cgi->header( -type => 'audio/x-mpegurl' );
# To save file
#my $headers = $cgi->header( -type => 'audio/x-mpegurl', -attachment => 'get_iplayer.m3u' );
# Send the headers to the browser
print $se "\r\nHEADERS:\n$headers\n"; #if $opt_cmdline->{debug};
print $fh $headers;
# determine output type
my $outtype = $cgi->param('OUTTYPE');
$outtype = $cgi->param('STREAMTYPE') || $cgi->param('OUTTYPE') if $action eq 'genplaylistdirect';
# ( host, outtype, modes, bitrate, action )
print $fh create_playlist_m3u_multi( $request_host, $outtype, $cgi->param('BITRATE') || '', $action );
# HTML page
} else {
# Output header and html start
begin_html( $request_host );
# Page Routing
form_header( $request_host );
#print $fh $cgi->Dump();
if ( $opt_cmdline->{debug} ) {
print $fh $cgi->Dump();
#for my $key (sort keys %ENV) {
# print $fh $key, " = ", $ENV{$key}, "\n";
#}
}
if ($nextpages{$nextpage}) {
# call the correct subroutine
$nextpages{$nextpage}->();
}
form_footer();
html_end();
}
$cgi->delete_all();
return 0;
}
sub pvr_run {
print $fh "The PVR will auto-run every $opt->{AUTOPVRRUN}->{current} hour(s) if you leave this page open
" if $opt->{AUTOPVRRUN}->{current};
if ( IS_WIN32 ) {
print $fh "Windows users: You may encounter errors if you perform other tasks in the Web PVR Manager while this page is reloading
" if $opt->{AUTOPVRRUN}->{current};
print $fh "Windows users: The Web PVR Manager may crash if you leave this window open for a long period of time
" if $opt->{AUTOPVRRUN}->{current};
}
print $se "INFO: Starting PVR Run\n";
my @cmd = (
@gip_cmd_base,
'--hash',
'--pvr',
);
#print $se "DEBUG: running: $cmd\n";
print $fh '';
# Redirect both STDOUT and STDERR to client browser socket
run_cmd_autorefresh( $fh, $fh, 1, @cmd );
print $fh '
';
print $fh p("PVR Run complete");
# Load the refresh tab if required
my $autopvrrun = $cgi->cookie( 'AUTOPVRRUN' ) || $cgi->param( 'AUTOPVRRUN' );
# Render options actions
print $fh div( { -class=>'action' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
a(
{
-class=>'action',
-title => 'Run PVR Now',
-onClick => "RefreshTab( '?NEXTPAGE=pvr_run&AUTOPVRRUN=$autopvrrun', ".(1000*3600*$autopvrrun).", 1 );",
},
'PVR Run Now'
),
a(
{
-class=>'action',
-title => 'Close',
-onClick => "window.close()",
},
'Close'
),
]),
),
);
}
sub record_now {
my @record;
# The 'Record' action button uses SEARCH to pass it's pvr_queue data
if ( $cgi->param( 'SEARCH' ) ) {
push @record, $cgi->param( 'SEARCH' );
} else {
@record = ( $cgi->param( 'PROGSELECT' ) );
}
my @params = get_search_params();
my $out;
# If a URL was specified by the User (assume auto mode list is OK):
if ( $opt->{URL}->{current} =~ m{^https?://} ) {
push @record, "$opt->{PROGTYPES}->{current}|$opt->{URL}->{current}|$opt->{URL}->{current}|-";
}
print $fh "Please leave this page open until the recording completes
";
# Render options actions
print $fh div( { -class=>'action' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
a(
{
-class=>'action',
-title => 'Close',
-onClick => "window.close()",
},
'Close'
),
]),
),
);
print $fh "Recording The Following Programmes
\n";
for (@record) {
chomp();
my ( $type, $pid, $name, $episode ) = (split /\|/)[0,1,2,3];
next if ! ($type && $pid );
print $fh "- $name - $episode ($pid)
\n";
}
print $fh "
\n";
print $se "INFO: Starting Recording Now\n";
# Queue all selected 'TYPE|PID|NAME|EPISODE|MODE|CHANNEL' entries in the PVR
for (@record) {
chomp();
my ( $type, $pid, $name, $episode ) = (split /\|/)[0,1,2,3];
next if ! ($type && $pid );
my $comment = "$name - $episode";
my @cmd = (
@gip_cmd_base,
'--hash',
'--webrequest',
get_iplayer_webrequest_args(
"pid=$pid",
"type=$type",
build_cmd_options( grep !/^(HISTORY|SINCE|BEFORE|HIDEDELETED|FUTURE|SEARCH|SEARCHFIELDS|PROGTYPES|EXCLUDEC.+)$/, @params )
),
);
print $fh p("Command: ".( join ' ', @cmd ) ) if $opt_cmdline->{debug};
print $fh '';
# Redirect both STDOUT and STDERR to client browser socket
run_cmd_autorefresh( $fh, $fh, 1, @cmd );
print $fh '
';
}
print $fh p("Recording complete");
return 0;
}
# Stream a file to browser/client
sub stream_file {
my ( $filename, $mimetype, $src_ext, $ext, $notranscode, $abitrate, $vsize, $vfr ) = ( @_ );
print $se "INFO: Start Direct Streaming $filename to browser using mimetype '$mimetype', output ext '$ext', audio bitrate '$abitrate', video size '$vsize', video frame rate '$vfr'\n";
# If transcoding required (i.e. output ext != source ext) - OR, if one of the transcoing options is set
if ( ( ! $notranscode ) && ( lc( $ext ) ne lc( $src_ext ) || $abitrate || $vsize || $vfr ) ) {
$fh->autoflush(0);
my @cmd = build_ffmpeg_args( $filename, $mimetype, $ext, $abitrate, $vsize, $vfr, $src_ext );
run_cmd( $fh, $se, 100000, @cmd );
print $se "INFO: Finished Streaming and transcoding $filename to browser\n";
} else {
print $se "INFO: Streaming file directly: $filename\n";
if ( ! open( STREAMIN, "< $filename" ) ) {
print $se "INFO: Cannot Read file '$filename'\n";
exit 4;
}
# Read each char from command output and push to socket fh
my $char;
my $bytes;
# Assume that we don't want to buffer STDERR output of the command
my $size = 100000;
while ( $bytes = read( STREAMIN, $char, $size ) ) {
if ( $bytes <= 0 ) {
close STREAMIN;
print $se "DEBUG: Stream thread has completed\n";
exit 0;
} else {
print $fh $char;
print $se '#';
}
last if $bytes < $size;
}
close STREAMIN;
print $se "INFO: Finished Streaming $filename to browser\n";
}
return 0;
}
sub build_ffmpeg_args {
my ( $filename, $mimetype, $ext, $abitrate, $vsize, $vfr, $src_ext ) = ( @_ );
my @cmd;
my @cmd_vopts;
my @cmd_aopts;
if ( $abitrate =~ m{^\d+$} ) {
push @cmd_aopts, ( '-ab', "${abitrate}k" );
}
if ( lc( $ext ) eq 'flv' ) {
push @cmd_aopts, ( '-ar', '44100' );
}
# If conversion is necessary
# Video
if ( $mimetype =~ m{^video} && $filename !~ m{\.(aac|m4a|mp3)$} ) {
# Apply video size
push @cmd_vopts, ( '-s', "${vsize}" ) if $vsize =~ m{^\d+x\d+$};
# Apply video framerate - caveat - bitrate defaults to 200k if only vfr is set
push @cmd_vopts, ( '-r', $vfr ) if $vfr =~ m{^\d+$};
# Add in the codec if we are transcoding and not remuxing the stream
if ( @cmd_vopts ) {
push @cmd_vopts, ( '-vcodec', 'libx264' );
} else {
push @cmd_vopts, ( '-vcodec', 'copy' );
}
# Audio
} else {
push @cmd_vopts, ( '-vn' );
}
@cmd = (
decode_fs($opt_cmdline->{ffmpeg}),
'-i', decode_fs($filename),
@cmd_vopts,
@cmd_aopts,
'-ac', 2,
'-f', $ext,
'-',
);
print $se "DEBUG: Command args: ".(join ' ', @cmd)."\n";
return @cmd;
}
sub create_playlist_m3u_single {
my ( $request_host, $outtype, $modes, $type, $bitrate, $search, $searchfields, $versionlist, $request ) = ( @_ );
my @playlist;
$outtype =~ s/^.*\.//g;
my $searchterm = $search;
# make search term regex friendly
if ( $searchterm ne '.*' && $searchterm !~ m{^http} ) {
$searchterm =~ s|([\/\.\?\+\-\*\^\(\)\[\]\{\}])|\\$1|g;
}
print $se "INFO: Getting playlist for type '$type' using modes '$modes' and bitrate '$bitrate'\n";
my @cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( 'history=1', 'skipdeleted=1', "type=$type", 'listformat=ENTRY||||||', "fields=$searchfields", "search=$searchterm", "versionlist=$versionlist" ),
);
my @out = get_cmd_output( @cmd );
push @playlist, "#EXTM3U\n";
# Extract and rewrite into m3u format
# /home/lewispj/mp3/Rock/radiohead/Ok Computer/radiohead - (07) fitter happier.mp3||(07) Fitter Happier|, , (256kbps/44.1kHz)||
for ( grep !/^(Added:|Matches|$)/ , @out ) {
chomp();
my $url;
my ( $pid, $name, $episode, $desc, $filename, $mode, $channel ) = (split /\|/)[1,2,3,4,5,6,7];
#print $se "DEBUG: $pid, $name, $episode, $desc, $filename, $mode\n";
# sanitze modes && filename
$mode = '' if $mode eq '';
$filename = '' if $filename eq '';
# playlist with direct streaming for files through webserver
if ( $request eq 'playlistdirect' ) {
next if ! ( $pid && $type && $mode );
$url = build_url_direct( $request_host, $type, $pid, $mode, $outtype, $opt->{STREAMTYPE}->{current}, $opt->{HISTORY}->{current}, $opt->{BITRATE}->{current}, $opt->{VSIZE}->{current}, $opt->{VFR}->{current}, $opt->{VERSIONLIST}->{current} );
# playlist with local files
} elsif ( $request eq 'playlistfiles' ) {
next if ! $filename;
$url = search_absolute_path( $filename );
}
push @playlist, "#EXTINF:-1,$type - $channel - $name - $episode - $desc";
push @playlist, "$url\n";
}
print $se join ("\n", @playlist);
return join ("\n", @playlist);
}
sub create_playlist_m3u_multi {
my ( $request_host, $outtype, $bitrate, $request ) = ( @_ );
my @playlist;
push @playlist, "#EXTM3U\n";
my @record = ( $cgi->param( 'PROGSELECT' ) );
# Create m3u from all selected 'TYPE|PID|NAME|EPISODE|MODE|CHANNEL' entries in the PVR
for (@record) {
my $url;
chomp();
my ( $type, $pid, $name, $episode, $mode, $channel ) = (split /\|/)[0,1,2,3,4,5];
next if ! ($type && $pid );
# playlist with direct streaming fo files through webserver
if ( $request eq 'genplaylistdirect' ) {
$url = build_url_direct( $request_host, $type, $pid, $mode, $outtype, $opt->{STREAMTYPE}->{current}, $opt->{HISTORY}->{current}, $opt->{BITRATE}->{current}, $opt->{VSIZE}->{current}, $opt->{VFR}->{current}, $opt->{VERSIONLIST}->{current} );
# playlist with local files
} elsif ( $request eq 'genplaylistfile' ) {
# Lookup filename (add it if defined - even if relative)
# check for -f $filename if you want to exclude files that cannot be found
my $filename = get_direct_filename( $pid, $mode, $type );
$url = $filename if -f $filename;
}
# Skip empty urls
next if ! $url;
push @playlist, "#EXTINF:-1,$type - $channel - $name - $episode";
push @playlist, "$url\n";
}
print $se join ("\n", @playlist);
return join ("\n", @playlist);
}
### Playlist URL builders
sub build_url_direct {
my ( $request_host, $progtypes, $pid, $modes, $outtype, $streamtype, $history, $bitrate, $vsize, $vfr, $versionlist, $action ) = ( @_ );
# Sanity check
#print $se "DEBUG: building direct playback request using: PROGTYPES=${progtypes} PID=${pid} MODES=${modes} OUTTYPE=${outtype}\n";
# CGI::escape
$_ = CGI::escape($_) for ( $progtypes, $pid, $modes, $outtype, $streamtype, $history, $bitrate, $vsize );
#print $se "DEBUG: building direct playback request using: PROGTYPES=${progtypes} PID=${pid} MODES=${modes} OUTTYPE=${outtype} BITRATE=${bitrate} VSIZE=${vsize} VFR=${vfr}\n";
# Build URL
$action ||= 'direct';
return "${request_host}?ACTION=$action&PROGTYPES=${progtypes}&PID=${pid}&MODES=${modes}&HISTORY=${history}&OUTTYPE=${outtype}&STREAMTYPE=${streamtype}&BITRATE=${bitrate}&VSIZE=${vsize}&VFR=${vfr}&VERSIONLIST=${versionlist}";
}
# Play from Internet/'Play': ?ACTION=playlist &SEARCHFIELDS=pid &SEARCH=$pid &MODES=${modes} &PROGTYPES=${type} &OUTTYPE=${outtype}'
## 'PlayFile' - works with vlc
# Play from local file/'PlayFile' ?ACTION=playlistfiles &SEARCHFIELDS=pid &SEARCH=$pid &MODES=${modes} &PROGTYPES=${type}
## 'PlayWeb' - not on vlc
# Play from file on web server/'PlayWeb' ?ACTION=playlistdirect &SEARCHFIELDS=pid &SEARCH=$pid &MODES=${modes}
sub build_url_playlist {
my ( $request_host, $action, $searchfields, $search, $modes, $progtypes, $outtype, $streamtype, $bitrate, $vsize, $vfr, $versionlist ) = ( @_ );
# Sanity check
#print $se "DEBUG: building $action request using: SEARCHFIELDS=${searchfields} SEARCH=${search} MODES=${modes} PROGTYPES=${progtypes} OUTTYPE=${outtype}\n";
# CGI::escape
$_ = CGI::escape($_) for ( $action, $searchfields, $search, $modes, $progtypes, $outtype, $streamtype, $bitrate, $vsize, $vfr );
#print $se "DEBUG: building $action request using: SEARCHFIELDS=${searchfields} SEARCH=${search} MODES=${modes} PROGTYPES=${progtypes} OUTTYPE=${outtype}\n";
# Build URL
return "${request_host}?ACTION=${action}&SEARCHFIELDS=${searchfields}&SEARCH=${search}&MODES=${modes}&PROGTYPES=${progtypes}&OUTTYPE=${outtype}&STREAMTYPE=${streamtype}&BITRATE=${bitrate}&VSIZE=${vsize}&VFR=${vfr}&VERSIONLIST=${versionlist}";
}
# Generic
# Gets the contents of a URL and retries if it fails, returns '' if no page could be retrieved
# Usage = request_url_retry(, , , , []);
sub request_url_retry {
my %OPTS = @LWP::Protocol::http::EXTRA_SOCK_OPTS;
$OPTS{SendTE} = 0;
@LWP::Protocol::http::EXTRA_SOCK_OPTS = %OPTS;
my ($ua, $url, $retries, $succeedmsg, $failmsg) = @_;
my $res;
# Malformed URL check
if ( $url !~ m{^\s*https?\:\/\/}i ) {
print $se "ERROR: Malformed URL: '$url'\n";
return '';
}
my $i;
print $se "INFO: Getting page $url\n" if $opt->{verbose};
for ($i = 0; $i < $retries; $i++) {
$res = $ua->request( HTTP::Request->new( GET => $url ) );
if ( ! $res->is_success ) {
print $se $failmsg;
} else {
print $se $succeedmsg;
last;
}
}
# Return empty string if we failed
return '' if $i == $retries;
return $res->content;
}
# Invokes command in @args as a system call (hopefully) without using a shell
# Can also redirect all stdout and stderr to either: STDOUT, STDERR or unchanged
# Usage: run_cmd( <''|STDOUTFH>, <''|STDERRFH>, @args )
# Returns: exit code
# Note: doesn't appear to work with 'in memory' filehandles
sub run_cmd_unix {
# Define what to do with STDOUT and STDERR of the child process
my $fh_child_out = shift || "STDOUT";
my $fh_child_err = shift || "STDERR";
my @cmd = ( @_ );
my $rtn;
print $se "INFO: Command: ".(join ' ', @cmd)."\n"; # if $opt->{verbose};
@cmd = map { encode_cl($_) } @cmd;
#print $se "INFO: open3( 0, \">&".fileno($fh_child_out).", \">&".fileno($fh_child_err).", )\n";
# Don't use NULL for the 1st arg of open3 otherwise we end up with a messed up STDIN once it returns
my $procid = open3( 0, ">&".fileno($fh_child_out), ">&".fileno($fh_child_err), @cmd );
# Wait for child to complete
waitpid( $procid, 0 );
$rtn = $?;
# Interpret return code
return interpret_return_code( $rtn );
}
# Invokes command in @args as a system call (hopefully) without using a shell
# Can also redirect all stdout and stderr to either: STDOUT, STDERR or unchanged
# Usage: run_cmd( $stdout_fh, $stderr_fh, , @args )
# Returns: exit code
sub run_cmd {
# win32 kludge cos win is so broken
return run_cmd_win32( @_ ) if IS_WIN32;
# Define what to do with STDOUT and STDERR of the child process
use IO::Select;
use Symbol qw(gensym);
my $fh_cmd_out = shift;
my $fh_cmd_err = shift;
my $size = shift;
my $from = new IO::Handle;
my $err = new IO::Handle;
my @cmd = ( @_ );
my $ffmpeg = decode_fs($opt_cmdline->{ffmpeg});
my $direct = grep(/$ffmpeg/, @cmd);
my $is_hls = grep(/modes%3Dhl(s|x)/, @cmd);
my $stdout_raw = $direct;
my $rtn;
$fh_cmd_out->autoflush(1);
$fh_cmd_err->autoflush(1);
print $se "INFO: Command: ".(join ' ', @cmd)."\n"; # if $opt->{verbose};
my $procid;
# Setup signal handlers so that when the browser is closed the SIGPIPE results in sending a SIGTERM to the forked command.
local $SIG{PIPE} = sub {
my $signal = shift;
print $se "\nINFO: $$ Cleaning up (signal = $signal), killing cmd PID=$procid:\n";
for my $sig ( qw/INT PIPE TERM KILL/ ) {
# Kill process with SIGs
print $se "INFO: $$ killing cmd PID=$procid with SIG${sig}\n";
kill $sig, $procid;
sleep 1;
if ( ! kill 0, $procid ) {
print $se "INFO: $$ killed cmd PID=$procid\n";
last;
}
sleep 4;
}
exit 0;
};
@cmd = map { encode_cl($_) } @cmd;
# Don't use NULL for the 1st arg of open3 otherwise we end up with a messed up STDIN once it returns
$procid = open3( gensym, $from, $err, @cmd ) || print $se "ERROR: Could not execute command: $!\n";
my $childpidout = fork();
# Fork a child process to read from the indirect (STDOUT) fh of the spawned command and write it to the selected fh (browser client)
if ( $childpidout <= 0 ) {
# Not sure if these are necessary:
$fh_cmd_out->autoflush(1);
$from->autoflush(1);
if ( $stdout_raw) {
binmode $from, ':raw';
} else {
binmode $from, ':utf8';
}
# Read each char from command output and push to socket fh
my $char;
my $bytes;
while ( $bytes = read( $from, $char, $size ) ) {
if ( $bytes <= 0 ) {
print $se "DEBUG: STDOUT fd closed - exiting thread\n";
exit 0;
} else {
print $fh_cmd_out $char;
}
last if $bytes < $size;
}
#print $se "CMD STDOUT FH EMPTY\n";
exit 0;
# Parent continues here
} elsif ( defined $childpidout ) {
print $se "DEBUG: Forked STDOUT reader with PID $childpidout\n";
# Failed to fork
} else {
print $se "ERROR: Failed to fork STDOUT reader process: $!\n";
exit 1;
}
my $childpiderr = fork();
# Fork a child process to read from the indirect (STDERR) fh of the spawned command and write it to the selected fh (browser client)
if ( $childpiderr <= 0 ) {
# Not sure if these are necessary:
$fh_cmd_err->autoflush(1);
$err->autoflush(1);
binmode $err, ':utf8';
# Read each char from command output and push to socket fh
my $char;
my $bytes;
# Assume that we don't want to buffer STDERR output of the command
$size = 1;
if ( $is_hls ) {
my ($count, $buf);
while ( $bytes = read( $err, $char, $size ) ) {
if ( $bytes <= 0 ) {
print $se "DEBUG: STDERR fd closed - exiting thread\n";
exit 0;
} else {
if ( $char eq "#" ) {
print $fh_cmd_err $char;
} elsif ( $char =~ /[\r\n]/ ) {
if ( $buf =~ /size=/ ) {
$count++;
print $fh_cmd_err "#";
print $fh_cmd_err "\n" if ! ($count % 100);
} else {
print $fh_cmd_err $buf;
print $fh_cmd_err "\n";
}
$buf = '';
} else {
$buf .= $char;
}
}
if ( $bytes < $size ) {
print $fh_cmd_err "$buf\n" if $buf;
last;
}
}
} else {
while ( $bytes = read( $err, $char, $size ) ) {
if ( $bytes <= 0 ) {
print $se "DEBUG: STDERR fd closed - exiting thread\n";
exit 0;
} else {
print $fh_cmd_err $char;
}
last if $bytes < $size;
}
}
#print $se "CMD STDERR FH EMPTY\n";
exit 0;
# Parent continues here
} elsif ( defined $childpiderr ) {
print $se "DEBUG: Forked STDERR reader with PID $childpiderr\n";
# Failed to fork
} else {
print $se "ERROR: Failed to fork STDERR reader process: $!\n";
exit 1;
}
# Reap reader processes
waitpid( $childpidout, 0 );
waitpid( $childpiderr, 0 );
# Reap command child
waitpid( $procid, 0 );
$rtn = $?;
# Restore sigpipe handler for reader and writer processes
$SIG{PIPE} = 'DEFAULT';
# Interpret return code
return interpret_return_code( $rtn );
}
# Works except for where both from and err go to fh - does not die when browser closes.
# Also the browser does not get closed after cmd completes...
# Uses shell when stderr needs to be redirected to stdout
sub run_cmd_win32 {
# Define what to do with STDOUT and STDERR of the child process
my $fh_child_out = shift;
my $fh_child_err = shift;
my $size = shift;
my @cmd = ( @_ );
# eek! - works around win32 inability to redirect STDERR nicely
# If the stderr is supposed to go to the same fh and stdout then add '2>&1'
push @cmd, '2>&1' if fileno($fh_child_out) == fileno($fh_child_err);
my $rtn;
# Disable buffering
$fh_child_out->autoflush(1);
print $se "INFO: Win32 Command: ".(join ' ', @cmd)."\n"; # if $opt->{verbose};
# Redirect $fh_child_out to STDOUT
open(STDOUT, ">&", $fh_child_out ) || die "can't dup client to stdout";
@cmd = map { encode_cl($_) } @cmd;
$rtn = system( @cmd );
# Interpret return code
return interpret_return_code( $rtn );
}
# PVR Run and Refresh Cache pages will not auto-refresh if client socket
# is dup()-ed to STDOUT (as in run_cmd_win32). Run command in shell and
# copy get_iplayer output to client socket instead.
sub run_cmd_autorefresh {
return run_cmd( @_ ) unless IS_WIN32;
# Define what to do with STDOUT and STDERR of the child process
my $fh_child_out = shift;
my $fh_child_err = shift;
my $size = shift;
my @cmd = ( @_ );
# workaround to add quotes around the args because we are using a shell here
for ( @cmd ) {
s/^(.+)$/"$1"/g if ! m{^[\-\"]};
}
# eek! - works around win32 inability to redirect STDERR nicely
# If the stderr is supposed to go to the same fh and stdout then add '2>&1'
push @cmd, '2>&1' if fileno($fh_child_out) == fileno($fh_child_err);
# Disable buffering
$fh_child_out->autoflush(1);
print $se "INFO: Win32 Command: ".(join ' ', @cmd)."\n"; # if $opt->{verbose};
my $buf;
my $bytes;
@cmd = map { encode_cl($_) } @cmd;
open( CMD, ( join ' ', @cmd ).'|' ) || die "can't open pipe: $!\n";
binmode CMD, ':utf8';
while ( $bytes = read( CMD, $buf, $size ) ) {
if ( $bytes <= 0 ) {
print $se "DEBUG: pipe fd closed - exiting thread\n";
exit 0;
} else {
print $fh_child_out $buf;
}
last if $bytes < $size;
}
close(CMD);
# Interpret return code
return interpret_return_code( $? );
}
# Same as backticks but without needing a shell
# sets $?
# returns array of output
sub get_cmd_output {
# win32 kludge cos win is so broken
return get_cmd_output_win32( @_ ) if IS_WIN32;
use Symbol qw(gensym);
my @cmd = ( @_ );
#my $to = new IO::Handle;
my $from = new IO::Handle;
my $error = new IO::Handle;
my $rtn;
my @out_from;
my @out_error;
#$to->autoflush(1);
$from->autoflush(1);
$error->autoflush(1);
print $se "INFO: Command: ".(join ' ', @cmd)."\n"; # if $opt->{verbose};
my $procid;
# Setup signal handlers so that when the browser is closed the SIGPIPE results in sending a SIGTERM to the forked command.
local $SIG{PIPE} = sub {
my $signal = shift;
print $se "\nINFO: $$ Cleaning up (signal = $signal), killing cmd PID=$procid:\n";
for my $sig ( qw/INT PIPE TERM KILL/ ) {
# Kill process with SIGs
print $se "INFO: $$ killing cmd PID=$procid with SIG${sig}\n";
kill $sig, $procid;
sleep 1;
if ( ! kill 0, $procid ) {
print $se "INFO: $$ killed cmd PID=$procid\n";
last;
}
sleep 4;
}
exit 0;
};
@cmd = map { encode_cl($_) } @cmd;
#print $se "INFO: open3( 0, \">&".fileno($fh_child_out).", \">&".fileno($fh_child_err).", )\n";
# Don't use NULL for the 1st arg of open3 otherwise we end up with a messed up STDIN once it returns
$procid = open3( gensym, $from, $error, @cmd );
# Wait for child to complete
my $childpid = fork();
binmode $se, ":encoding($opt_cmdline->{encodingconsoleout})";
# Child
if ( $childpid == 0 ) {
binmode $error, ':utf8';
while ( <$error> ) {
print $se "CMD STDERR: $_";
}
#print $se "CMD STDERR EMPTY\n";
exit 0;
# Parent
} elsif ( defined $childpid ) {
binmode $from, ':utf8';
while ( <$from> ) {
push @out_from, $_;
}
} else {
print $se "ERROR: Could not fork STDERR reader process\n";
exit 1;
}
waitpid( $childpid, 0 );
waitpid( $procid, 0 );
$rtn = $?;
# Restore sigpipe handler for reader and writer processes
$SIG{PIPE} = 'DEFAULT';
# Interpret return code
interpret_return_code( $rtn );
return @out_from;
}
# Still uses shell
sub get_cmd_output_win32 {
my ( @cmd ) = ( @_ );
# workaround to add quotes around the args because we are using a shell here
for ( @cmd ) {
s/^(.+)$/"$1"/g if ! m{^[\-\"]};
}
print $se "DEBUG: Command: ".( join ' ', @cmd )."\n";
@cmd = map { encode_cl($_) } @cmd;
open( CMD, ( join ' ', @cmd ).'|' ) || print $se "ERROR: open failed: $!\n";
binmode CMD, ':utf8';
my @out;
my @out = ;
close CMD;
# Interpret return code
interpret_return_code( $? );
return @out;
}
sub interpret_return_code {
my $rtn = shift;
# Interpret return code and force return code 2 upon error
my $return = $rtn >> 8;
if ( $rtn == -1 && $IGNOREEXIT ) {
$return = 0;
} elsif ( $rtn == -1 ) {
print $se "ERROR: Command failed to execute: $!\n";
$return = 2 if ! $return;
} elsif ( $rtn & 128 ) {
print $se "WARNING: Command executed but coredumped\n";
$return = 2 if ! $return;
} elsif ( $rtn & 127 ) {
print $se sprintf "WARNING: Command executed but died with signal %d\n", $rtn & 127;
$return = 2 if ! $return;
}
print $se sprintf "INFO: Command exit code %d\n", $return if $return;
return $return;
}
sub get_pvr_list {
my $pvrsearch;
my $out = join "\n", get_cmd_output(
@gip_cmd_base,
'--pvrlist',
);
# Remove text before first pvrsearch entry
$out =~ s/^.+?(pvrsearch\s.+)$/$1/s;
# Parse all 'pvrsearch' elements
for ( split /pvrsearch\s+\=\s+/, $out ) {
next if /^get_iplayer/;
my $name;
$_ = "pvrsearch = $_";
# Get each element
while ( /([\w\-]+?)\s+=\s+(.+?)\n/sg ) {
if ( $1 eq 'pvrsearch' ) {
$name = $2;
}
$pvrsearch->{$name}->{$1} = $2;
# Remove disabled entries
if ( $pvrsearch->{$name}->{disable} == 1 ) {
delete $pvrsearch->{$name};
last;
}
}
}
return $pvrsearch;
}
sub show_pvr_list {
my %fields;
my $pvrsearch = get_pvr_list();
my $sort_field = $cgi->param( 'PVRSORT' ) || 'name';
my $reverse = $cgi->param( 'PVRREVERSE' ) || '0';
# Sort data
my @pvrsearches = get_sorted( $pvrsearch, $sort_field, $reverse );
# Parse all 'pvrsearch' elements to get all fields used
for my $name ( @pvrsearches ) {
# Get each element
for ( keys %{ $pvrsearch->{$name} } ) {
$fields{$_} = 1;
}
}
# Render options actions
my $buttons = div( { -class=>'action' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
a(
{
-class=>'action',
-title => 'Go Back',
-onClick => "history.back()",
},
'Back'
),
a(
{
-class => 'action',
-title => 'Delete selected programmes from PVR search list',
-onClick => "if(! check_if_selected(document.form1, 'PVRSELECT')) { alert('No programmes were selected'); return false; } BackupFormVars(form1); form1.NEXTPAGE.value='pvr_del'; form1.submit(); RestoreFormVars(form1);",
},
'Delete'
),
]),
),
);
my @html;
my @displaycols = ( 'pvrsearch', ( grep !/pvrsearch/, ( sort keys %fields ) ) );
# Build header row
push @html, "";
push @html, th( { -class => 'search' }, checkbox( -class=>'search', -title=>'Select/Unselect All PVR Searches', -onClick=>"check_toggle(document.form1, 'PVRSELECT')", -name=>'SELECTOR', -value=>'1', -label=>'' ) );
# Display data in nested table
for my $heading (@displaycols) {
# Sort by column click and change display class (colour) according to sort status
my ($title, $class, $onclick);
if ( $sort_field eq $heading && not $reverse ) {
($title, $class, $onclick) = ("Sort by Reverse $fieldname{$heading}", 'sorted pointer', "BackupFormVars(form1); form1.NEXTPAGE.value='pvr_list'; form1.PVRSORT.value='$heading'; form1.PVRREVERSE.value=1; form1.submit(); RestoreFormVars(form1);");
} else {
($title, $class, $onclick) = ("Sort by $fieldname{$heading}", 'unsorted pointer', "BackupFormVars(form1); form1.NEXTPAGE.value='pvr_list'; form1.PVRSORT.value='$heading'; form1.submit(); RestoreFormVars(form1); ");
}
$class = 'sorted_reverse pointer' if $sort_field eq $heading && $reverse;
push @html, th( { -class => 'search' },
label( {
-title => $title,
-class => $class,
-onClick => $onclick,
},
$fieldname{$heading} || $heading,
)
);
}
push @html, "
";
# Build each row
for my $name ( @pvrsearches ) {
my @row;
push @row, td( {-class=>'search'},
checkbox(
-class => 'search',
-name => 'PVRSELECT',
-label => '',
-value => "$name",
-checked => 0,
-override => 1,
)
);
for ( @displaycols ) {
push @row, td( {-class=>'search'},
label( {
-title => "Click to Edit",
-class => 'search',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='pvr_edit'; form1.PVRSEARCH.value='$name'; form1.submit(); RestoreFormVars(form1);",
},
$pvrsearch->{$name}->{$_},
)
);
}
push @html, Tr( {-class=>'search'}, @row );
}
# Search form
print $fh start_form(
-name => "form1",
-method => "POST",
);
print $fh p("Click to Edit any PVR Search");
# Render options actions
print $fh $buttons;
# Render table
print $fh table( {-class=>'search'} , @html );
print $fh $buttons;
# Make sure we go to the correct nextpage for processing
print $fh hidden(
-name => "NEXTPAGE",
-value => "pvr_list",
-override => 1,
);
# Reverse sort value
print $fh hidden(
-name => "PVRREVERSE",
-value => 0,
-override => 1,
);
print $fh hidden(
-name => "PVRSORT",
-value => $sort_field,
-override => 1,
);
print $fh hidden(
-name => "PVRSEARCH",
-value => '',
-override => 1,
);
print $fh end_form();
return 0;
}
# Edits a single record indicated by PVRSELECT
sub pvr_edit {
my %fields;
my $pvrsearch = get_pvr_list();
my @html;
my $pvrname = $cgi->param( 'PVRSEARCH' );
# Determine max field length
my $maxwidth = 30;
for ( values %{ $pvrsearch->{$pvrname} } ) {
$maxwidth = length($_) if length($_) > $maxwidth && $maxwidth < 200;
}
# Get each element
for my $key ( keys %{ $pvrsearch->{$pvrname} } ) {
my $val = $pvrsearch->{$pvrname}->{$key};
# Put INPUT field here
my $element;
#if ( $key eq 'pvrsearch' ) {
# $element = $val;
#} else {
$element = hidden(
-name => "EDITKEYS",
-value => $key,
-override => 1,
).
textfield(
-class => 'edit',
-name => "EDITVALUES",
-value => $val,
-size => $maxwidth + 20,
);
#}
push @html, Tr( { -class => 'info' }, th( { -class => 'info' }, $key ).td( { -class => 'info' }, $element ) );
}
# Editing form
print $fh start_form(
-name => "form1",
-method => "POST",
);
print $fh table( { -class => 'info' }, @html );
# Render options actions
print $fh div( { -class=>'action' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
a(
{
-class=>'action',
-title => 'Go Back',
-onClick => "history.back()",
},
'Back'
),
a(
{
-class => 'action',
-title => 'Save changes',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='pvr_save'; form1.submit(); RestoreFormVars(form1);",
},
'Save Changes'
),
]),
),
);
# Make sure we go to the correct nextpage for processing
print $fh hidden(
-name => "NEXTPAGE",
-value => "pvr_add",
-override => 1,
);
print $fh hidden(
-name => "PVRSEARCH",
-value => $pvrname,
-override => 1,
);
print $fh end_form();
return 0;
}
#
# Will return a list of pids sorted by the requested Heading
#
sub get_sorted {
my @sorted;
my @unsorted;
my $data = shift;
my $sort_field = shift;
my $reverse = shift;
# Lookup table for nice field name headings
my %sorttype = (
index => 'numeric',
duration => 'numeric',
timeadded => 'numeric',
seriesnum => 'numeric',
episodenum => 'numeric',
expires => 'numeric',
);
# Insert search '~~~' for each prog in hash
for my $key (keys %{ $data } ) {
# generate sort column
push @unsorted, $data->{$key}->{$sort_field}.'~~~'.$key;
}
# If this a purely numerical field
if ( defined $sorttype{$sort_field} && $sorttype{$sort_field} eq 'numeric' ) {
if ($reverse) {
@sorted = reverse sort {$a <=> $b} @unsorted;
} else {
@sorted = sort {$a <=> $b} @unsorted;
}
# otherwise sort alphabetically
} else {
if ($reverse) {
@sorted = reverse sort { lc $a cmp lc $b } @unsorted;
} else {
@sorted = sort { lc $a cmp lc $b } @unsorted;
}
}
# Strip off search key at beginning of each line
s/^.*~~~// for @sorted;
return @sorted;
}
sub pvr_del {
my @record = ( $cgi->param( 'PVRSELECT' ) );
my $out;
# Queue all selected '|' entries in the PVR
for my $name (@record) {
chomp();
my @cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( "pvrdel=$name" ),
);
print $fh p("Command: ".( join ' ', @cmd ) ) if $opt_cmdline->{debug};
my $cmdout = join "", get_cmd_output( @cmd );
return p("ERROR: ".$out) if $? && not $IGNOREEXIT;
print $fh p("Deleted: $name");
$out .= $cmdout;
}
print $fh "$out
";
# Show list below
show_pvr_list();
return $out;
}
sub show_info {
my $progdata = ( $cgi->param( 'INFO' ) );
my $out;
my @html;
my %prog;
my ( $type, $pid ) = split /\|/, $progdata;
# Queue all selected '|' entries in the PVR
chomp();
my @cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( "type=$type", "future=$opt->{FUTURE}->{current}", "history=$opt->{HISTORY}->{current}", "skipdeleted=$opt->{HIDEDELETED}->{current}", 'info=1', 'fields=pid', "search=$pid" ),
);
print $fh p("Command: ".( join ' ', @cmd ) ) if $opt_cmdline->{debug};
my @cmdout = get_cmd_output( @cmd );
return p("ERROR: ".@cmdout) if $? && not $IGNOREEXIT;
for ( grep !/^(Added|INFO):/, @cmdout ) {
my ( $key, $val ) = ( $1, $2 ) if m{^(\w+?):\s*(.+?)\s*$};
next if $key =~ /(^$|^\d+$)/ || $val =~ /Matching Program/i;
$out .= "$key: $val\n";
$prog{$pid}->{$key} = $val;
# Make into a link if this value is a URL
$val = a( { -class=>'info', -title=>'Open URL', -href=>$val, -target=>'_new' }, $val ) if $val =~ m{^https?://.+};
push @html, Tr( { -class => 'info' }, th( { -class => 'info' }, $key ).td( { -class => 'info' }, $val ) );
}
# Show thumb if one exists
$prog{$pid}->{thumbnail} ||= DEFAULT_THUMBNAIL;
print $fh img( { -height=>216, -class=>'action', -src=>$prog{$pid}->{thumbnail} } ) if $prog{$pid}->{thumbnail};
# Set optional output dir for pvr queue if set
my $outdir;
$outdir = '&OUTPUT='.CGI::escape("$opt->{OUTPUT}->{current}") if $opt->{OUTPUT}->{current};
# Render options actions
print $fh div( { -class=>'action' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
a(
{
-class=>'action',
-title => 'Close',
-onClick => "window.close()",
},
'Close'
),
]),
),
);
print $fh table( { -class => 'info' }, @html );
return $out;
}
# Get filename from history based on PID, MODE and TYPE
# If the PID is a filename then filename is still searched using PID and TYPE
sub get_direct_filename {
my ( $pid, $mode, $type ) = ( @_ );
my $history = 1;
print $se "DEBUG: Looking up filename for MODE=$mode TYPE=$type PID=$pid\n";
if ( ! ( $pid && $mode && $type ) ) {
print $se "ERROR: Cannot lookup filename unless PID, MODE and TYPE are set\n";
return '';
}
# Get the 'filename' entry from --history --info for this pid
my @cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( "history=$history", 'fields=pid', "search=$pid", "type=$type", 'listformat=filename: ||' ),
);
print $se "Command: ".( join ' ', @cmd )."\n"; # if $opt_cmdline->{debug};
my @cmdout = get_cmd_output( @cmd );
return p("ERROR: ".@cmdout) if $? && not $IGNOREEXIT;
# Extract the filename
my $match = ( grep /^filename:/, @cmdout )[0];
my $filename;
$filename = $1 if $match =~ m{^filename: .+?\|\s*(.+?)\|$mode\s*$};
return search_absolute_path( encode_fs($filename) );
}
# Hack to work around relative paths in recordings history
sub search_absolute_path {
my $filename = shift;
my $abs_path;
# win32 doesn't seem to like abs_path
# abs_path croaks on cygwin if file not found
# rewrite win32 paths
if ( IS_WIN32 || $^O eq "cygwin" ) {
# add a hardcoded prefix for now if relative path (assume relative to local get_iplayer script)
if ( $filename !~ m{^[A-Za-z]:} && $filename =~ m{^(\.|\.\.|[A-Za-z_])} ) {
$filename = dirname( abs_path( $opt_cmdline->{getiplayer} ) ).'/'.$filename;
}
if ( IS_WIN32 ) {
# twiddle the / to \
$filename =~ s!(\\/|/|\/)!\\!g;
}
return $filename;
}
#print $se "FILENAME='$filename'";
# Try using CWD
if ( -f abs_path($filename) ) {
$abs_path = abs_path($filename);
# else try dir of get_iplayer
} elsif ( -f dirname( abs_path( $opt_cmdline->{getiplayer} ) ).'/'.$filename ) {
$abs_path = dirname( abs_path( $opt_cmdline->{getiplayer} ) ).'/'.$filename;
# else try dir current output dir option
} elsif ( $opt->{OUTPUT}->{current} && -f abs_path( $opt->{OUTPUT}->{current} ).'/'.$filename ) {
$abs_path = abs_path( encode_fs($opt->{OUTPUT}->{current}) ).'/'.$filename;
# Else just return the relative path
} else {
$abs_path = $filename;
}
#print $se " -> ABSPATH='$abs_path'\n";
return $abs_path;
}
sub pvr_queue {
# Gets the multiple selections of progs to queue from PROGSELECT
my @record;
# The 'Record' action button uses SEARCH to pass it's pvr_queue data
if ( $cgi->param( 'SEARCH' ) ) {
push @record, $cgi->param( 'SEARCH' );
} else {
@record = ( $cgi->param( 'PROGSELECT' ) );
}
my @params = get_search_params();
my $out;
# If a URL was specified by the User (assume auto mode list is OK):
if ( $opt->{URL}->{current} =~ m{^https?://} ) {
push @record, "$opt->{PROGTYPES}->{current}|$opt->{URL}->{current}|$opt->{URL}->{current}|-";
}
print $fh "Queuing The Following Programmes in the PVR
\n";
for (@record) {
chomp();
my ( $type, $pid, $name, $episode ) = (split /\|/)[0,1,2,3];
next if ! ($type && $pid );
print $fh "- $name - $episode ($pid)
\n";
}
print $fh "
\n";
# Queue all selected 'TYPE|PID|NAME|EPISODE|MODE|CHANNEL' entries in the PVR
for (@record) {
chomp();
my ( $type, $pid, $name, $episode ) = (split /\|/)[0,1,2,3];
next if ! ($type && $pid );
my $comment = "$name - $episode";
$comment =~ s/\'\"//g;
$comment =~ s/[^\s\w\d\-:\(\)]/_/g;
$comment =~ s/^_*//g;
$comment =~ s/_*$//g;
my @cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args(
'pvrqueue=1',
"pid=$pid",
"comment=$comment (queued: ".localtime().')',
"type=$type",
build_cmd_options( grep !/^(HISTORY|SINCE|BEFORE|HIDEDELETED|FUTURE|SEARCH|SEARCHFIELDS|PROGTYPES|EXCLUDEC.+)$/, @params )
),
);
print $fh p("Command: ".( join ' ', @cmd ) ) if $opt_cmdline->{debug};
my $cmdout = join "", get_cmd_output( @cmd );
return p("ERROR: ".$out) if $? && not $IGNOREEXIT;
print $fh p("Queued: $type: '$name - $episode' ($pid)");
$out .= $cmdout;
}
print $fh "$out
";
# Show list below
show_pvr_list();
return $out;
}
sub recordings_delete {
# Gets the multiple selections of progs to queue from PROGSELECT
my @record;
# The 'Record' action button uses SEARCH to pass it's pvr_queue data
if ( $cgi->param( 'SEARCH' ) ) {
push @record, $cgi->param( 'SEARCH' );
} else {
@record = ( $cgi->param( 'PROGSELECT' ) );
}
my @params = get_search_params();
# Render options actions
my $buttons = div( { -class=>'action' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
a(
{
-class=>'action',
-title => 'Go Back',
-onClick => "history.back()",
},
'Back'
),
]),
),
);
# Render options actions
print $fh $buttons;
print $fh "Deleting the Following Programmes:
\n";
for (@record) {
chomp();
my ( $type, $pid, $name, $episode ) = (split /\|/)[0,1,2,3];
next if ! ($type && $pid );
print $fh "- $name - $episode ($pid)
\n";
}
print $fh "
\n";
# Queue all selected 'TYPE|PID|NAME|EPISODE|MODE|CHANNEL' entries in the PVR
for (@record) {
chomp();
my ( $type, $pid, $name, $episode, $mode ) = (split /\|/)[0,1,2,3,4];
next if ! ($mode && $pid );
my $filename = get_direct_filename( $pid, $mode, $type );
my $dir = dirname( $filename );
my $fileregex = basename( $filename );
# get the filename less the ext
$fileregex =~ s/\.\w+$//g;
# escape regex metachars
$fileregex =~ s/([\\\^\$\.\|\?\*\+\(\)\[\]])/\\$1/g;
$fileregex .= '\.\w+$';
# Find matching files .*
my $deleted;
if ( opendir DIR, $dir ) {
for my $file ( grep { /$fileregex/ } readdir(DIR) ) {
# Use absolute path
$file = "${dir}/${file}";
if ( -f $file ) {
if ( ! unlink( $file ) ) {
print $fh p("ERROR: Failed to delete $file");
} else {
$deleted = 1;
print $fh p("Successfully deleted: $type: '$name - $episode', MODE: $mode, PID: $pid");
}
} else {
print $fh p("ERROR: File does not exist for: $type: '$name - $episode', MODE: $mode, PID: $pid, FILENAME: $filename");
}
}
if ( ! $deleted ) {
print $fh p("No files deleted: $type: '$name - $episode', MODE: $mode, PID: $pid");
}
closedir(DIR);
} else {
print $fh p("ERROR: Cannot open dir '$dir' for file deletion\n");
}
}
# Render options actions
print $fh $buttons;
return '';
}
sub build_cmd_options {
my @options;
for ( @_ ) {
# skip non-options
next if $opt->{$_}->{optkey} eq '' || not defined $opt->{$_}->{optkey} || not $opt->{$_}->{optkey};
my $value = $opt->{$_}->{current};
push @options, "$opt->{$_}->{optkey}=$value" if $value ne '';
}
return @options;
}
sub get_search_params {
my @params;
for ( keys %{ $opt } ) {
# skip non-options
next if $opt->{$_}->{optkey} eq '' || not defined $opt->{$_}->{optkey} || not $opt->{$_}->{optkey};
next if grep /^$_$/, @nosearch_params;
push @params, $_;
}
return @params;
}
# Return get_iplayer command options when supplied an array of = options
sub get_iplayer_webrequest_args {
my @cmdopts;
print $se 'DEBUG: get_iplayer options: "'.join('" "', @_)."\"\n";
for (@_) {
push @cmdopts, CGI::escape(encode_wr($_));
}
my $cmdline = join('?', @cmdopts);
return $cmdline;
}
sub pvr_add {
my $out;
my @params = get_search_params();
# Only allow alphanumerics,_,-,. here for security reasons
my $searchname = "$opt->{SEARCH}->{current}_$opt->{SEARCHFIELDS}->{current}_$opt->{PROGTYPES}->{current}";
$searchname =~ s/[^\w]+/_/g;
# Remove a few options from leaking into a PVR search
my @cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( "pvradd=$searchname", build_cmd_options( grep !/^(HISTORY|HIDEDELETED|SINCE|BEFORE|HIDE|FORCE|FUTURE)$/, @params ) ),
);
print $se "DEBUG: Command: ".( join ' ', @cmd )."\n";
print $fh p("Command: ".( join ' ', @cmd ) ) if $opt_cmdline->{debug};
$out = join "", get_cmd_output( @cmd );
return p("ERROR: ".$out) if $? && not $IGNOREEXIT;
print $fh p("Added PVR Search ($searchname):\n\tTypes: $opt->{PROGTYPES}->{current}\n\tSearch: $opt->{SEARCH}->{current}\n\tSearch Fields: $opt->{SEARCHFIELDS}->{current}\n");
print $fh "$out
";
# Show list below
show_pvr_list();
return $out;
}
# Delete then add again - just in case user has edited name of pvr search
sub pvr_save {
my $out;
my @keys = $cgi->param( 'EDITKEYS' );
my @values = $cgi->param( 'EDITVALUES' );
my @params;
my @search_args;
my $newsearchname;
# Convert the two keys and values arrays into a KEY=VALUE params array
for ( @keys ) {
my $val = shift @values;
if ( $_ eq 'pvrsearch' ) {
$newsearchname = $val;
# append search terms to cmdline
} elsif ( /^search\d+$/ && $val !~ /^\-/ ) {
push @search_args, $val;
} else {
push @params, $_.'='.$val;
}
}
#print STDERR "ELEMENTS for save: ".(join ',', @params)."\n\n";
# Sanity check
if ( $newsearchname eq '' ) {
print $fh p("No PVR Search Name Specified - not updated");
return;
}
# Delete the original pvr entry
my $searchname = $cgi->param( 'PVRSEARCH' );
my @cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( "pvrdel=$searchname" ),
);
print $fh p("Command: ".( join ' ', @cmd ) ) if $opt_cmdline->{debug};
my $cmdout = join "", get_cmd_output( @cmd );
return p("ERROR: ".$out) if $? && not $IGNOREEXIT;
print $fh p("Deleted: $searchname");
$out .= $cmdout;
# Add the new pvr entry
@cmd = (
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( "pvradd=$newsearchname", @params ),
'--',
@search_args,
);
print $se "DEBUG: Command: ".( join ' ', @cmd )."\n";
print $fh p("Command: ".( join ' ', @cmd ) ) if $opt_cmdline->{debug};
$out = join "", get_cmd_output( @cmd );
return p("ERROR: ".$out) if $? && not $IGNOREEXIT;
print $fh p("Added Updated PVR Search '$newsearchname'\n");
print $fh "$out
";
# Show list below
show_pvr_list();
return $out;
}
# Build templated HTML for an option specified by passed hashref
sub build_option_html {
my $arg = shift;
my $title = $arg->{title};
my $tooltip = $arg->{tooltip};
my $webvar = $arg->{webvar};
my $option = $arg->{option};
my $type = $arg->{type};
my $label = $arg->{label};
my $current = $arg->{current};
my $value = $arg->{value};
my $status = $arg->{status};
my @html;
# On/Off
if ( $type eq 'hidden' ) {
push @html, hidden(
-name => $webvar,
-id => "option_$webvar",
#-value => $arg->{default},
-value => $current,
-override => 1,
);
# On/Off
} elsif ( $type eq 'boolean' ) {
push @html, th( { -class => 'options', -title => $tooltip, -id => "label_option_$webvar" }, $title ).
td( { -class => 'options', -title => $tooltip },
checkbox(
-class => 'options',
-name => $webvar,
-id => "option_$webvar",
-label => '',
#-value => 1,
-checked => $current,
-override => 1,
"aria-labelledby" => "label_option_$webvar",
)
);
# On/Off
} elsif ( $type eq 'radioboolean' ) {
push @html, th( { -class => 'options', -title => $tooltip }, $title ).
td( { -class => 'options', -title => $tooltip },
radio_group(
-class => 'options',
-name => $webvar,
-values => [ 0 , 1 ],
-labels => { 0=>'Off' , 1=>'On' },
-default => $current,
-override => 1,
)
);
# Multi-On/Off
} elsif ( $type eq 'multiboolean' ) {
my $element;
# values in hash of $value->{} => value
# labels in hash of $label->{$value}
# selected status in $status->{$value}
my @keylist = sort { $a <=> $b } keys %{ $value };
my $count = 0;
while ( @keylist ) {
my $val = $value->{shift @keylist};
$element .=
td( { -class => 'options' },
table ( { -class => 'options_embedded', -title => $tooltip, -role=>'presentation' }, Tr( { -class => 'options_embedded' }, td( { -class => 'options_embedded' }, [
checkbox(
-class => 'options',
-name => $webvar,
-id => "option_${webvar}_$val",
-label => '',
-value => $val,
-checked => $status->{$val},
-override => 1,
"aria-labelledby" => "label_option_${webvar}_$val",
),
label( { -for => "option_${webvar}_$val"}, span({ -id=> "label_option_${webvar}_$val" }, $label->{$val} ) )
] ) ) )
);
# Spread over more rows if there are many elements
if ( not ( ($count+1) % 3 ) ) {
$element .= '';
}
$count++;
}
my $inner_table = table ( { -class => 'options_embedded' }, Tr( { -class => 'options_embedded' },
$element
) );
push @html, th( { -class => 'options', -title => $tooltip }, $title ).td( { -class => 'options' }, $inner_table );
# Popup type
} elsif ( $type eq 'popup' ) {
my @value = $arg->{value};
push @html, th( { -class => 'options', -title => $tooltip, -id => "label_option_$webvar" }, $title ).
td( { -class => 'options', -title => $tooltip },
popup_menu(
-class => 'options',
-name => $webvar,
-id => "option_$webvar",
-values => @value,
-labels => $label,
-default => $current,
-onChange => $arg->{onChange},
"aria-labelledby" => "label_option_$webvar",
)
);
# text field
} elsif ( $type eq 'text' ) {
push @html, th( { -class => 'options', -title => $tooltip, -id => "label_option_$webvar" }, $title ).
td( { -class => 'options', -title => $tooltip },
textfield(
-class => 'options',
-name => $webvar,
-value => $current,
-size => $value,
-onKeyDown => 'return submitonEnter(event);',
"aria-labelledby" => "label_option_$webvar",
)
);
}
return @html;
}
sub refresh {
my $typelist = join(",", $cgi->param( 'PROGTYPES' )) || 'tv';
my $refreshfuture = $cgi->param( 'REFRESHFUTURE' ) || 0;
print $fh "The cache will auto-refresh every $opt->{AUTOWEBREFRESH}->{current} hour(s) if you leave this page open
" if $opt->{AUTOWEBREFRESH}->{current};
if ( IS_WIN32 ) {
print $fh "Windows users: You may encounter errors if you perform other tasks in the Web PVR Manager while this page is reloading
" if $opt->{AUTOWEBREFRESH}->{current};
print $fh "Windows users: The Web PVR Manager may crash if you leave this window open for a long period of time
" if $opt->{AUTOWEBREFRESH}->{current};
}
print $se "INFO: Refreshing\n";
my @cmd = (
@gip_cmd_base,
'--refresh',
'--webrequest',
get_iplayer_webrequest_args( "type=$typelist", "refreshfuture=$refreshfuture" ),
);
print $fh '';
run_cmd_autorefresh( $fh, $se, 1, @cmd );
print $fh '
';
print $fh p("Flushed Programme Caches for Types: $typelist");
# Load the refresh tab if required
my $autorefresh = $cgi->cookie( 'AUTOWEBREFRESH' ) || $cgi->param( 'AUTOWEBREFRESH' );
# Render options actions
print $fh div( { -class=>'action' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
a(
{
-class=>'action',
-title => 'Refresh Cache Now',
-onClick => "RefreshTab( '?NEXTPAGE=refresh&PROGTYPES=$typelist&AUTOWEBREFRESH=$autorefresh', ".(1000*3600*$autorefresh).", 1 );",
},
'Force Refresh'
),
a(
{
-class=>'action',
-title => 'Go Back',
-onClick => "window.close()",
},
'Close'
),
]),
),
);
}
# Just a wrapper to search_progs which defines history search settings for 'Recordings' tab
sub search_history {
$opt->{HISTORY}->{current} = 1;
$opt->{SORT}->{current} = 'timeadded';
$opt->{REVERSE}->{current} = 1;
$opt->{SINCE}->{current} = '';
$opt->{BEFORE}->{current} = '';
$opt->{EXCLUDE}->{current} = '';
$opt->{CATEGORY}->{current} = '';
$opt->{EXCLUDECATEGORY}->{current} = '';
$opt->{CHANNEL}->{current} = '';
$opt->{EXCLUDECHANNEL}->{current} = '';
search_progs();
}
sub search_progs {
# Set default status for progtypes
my %type;
$type{$_} = 1 for split /,/, $opt->{PROGTYPES}->{current};
$opt->{PROGTYPES}->{status} = \%type;
# Determine which cols to display and Set default status for cols
get_display_cols();
#for my $key (sort keys %ENV) {
# print $fh $key, " = ", $ENV{$key}, "\n
";
#}
# Get prog data
my @params = get_search_params();
my ( $matchcount, $response ) = ( get_progs( @params ) );
if ( $response ) {
print $fh p("ERROR: get_iplayer returned non-zero:").br().p( join '
', $response );
return 1;
}
$matchcount ||= 0;
my ($first, $last, @pagetrail) = pagetrail( $opt->{PAGENO}->{current}, $opt->{PAGESIZE}->{current}, $matchcount, 7 );
# Default displaycols
my @html;
push @html, "
";
push @html, th( { -class => 'search' }, checkbox( -class=>'search', -title=>'Select/Unselect All Programmes', -onClick=>"check_toggle(document.form1, 'PROGSELECT')", -name=>'SELECTOR', -value=>'1', -label=>'' ) );
# Pad empty column for R/S
push @html, th( { -class => 'search' }, 'Actions' );
# Display data in nested table
for my $heading (@displaycols) {
# Sort by column click and change display class (colour) according to sort status
my ($title, $class, $onclick);
if ( $opt->{SORT}->{current} eq $heading && not $opt->{REVERSE}->{current} ) {
($title, $class, $onclick) = ("Sort by Reverse $heading", 'sorted pointer', "form1.NEXTPAGE.value='search_progs'; form1.SORT.value='$heading'; form1.REVERSE[1].checked=true; form1.submit();");
} else {
($title, $class, $onclick) = ("Sort by $heading", 'unsorted pointer', "form1.NEXTPAGE.value='search_progs'; form1.SORT.value='$heading'; form1.REVERSE[0].checked=true; form1.submit();");
}
$class = 'sorted_reverse pointer' if $opt->{SORT}->{current} eq $heading && $opt->{REVERSE}->{current};
push @html,
th( { -class => 'search' },
table( { -class => 'searchhead', -role=>'presentation' },
Tr( { -class => 'search' }, [
th( { -class => 'search' },
label( {
-title => $title,
-class => $class,
-onClick => $onclick,
},
$fieldname{$heading},
)
)
]
)
)
);
}
push @html, "
";
# Set optional output dir for pvr queue if set
my $outdir;
$outdir = '&OUTPUT='.CGI::escape("$opt->{OUTPUT}->{current}") if $opt->{OUTPUT}->{current};
# Build each prog row
my $time = time();
for ( my $i = 0; $i <= $#pids; $i++ ) {
my $search_class = 'search';
my $pid = $pids[$i];
my @row;
# Grey-out history lines which files have been deleted or where the history doesn't have a filename mentioned
if ( $opt->{HISTORY}->{current} && ! $opt->{HIDEDELETED}->{current} ) {
if ( ( ! $prog{$pid}->{filename} ) || ! -f $prog{$pid}->{filename} ) {
$search_class = 'search darker';
}
}
# Format of PROGSELECT: TYPE|PID|NAME|EPISODE|MODE|CHANNEL
if ( $opt->{HISTORY}->{current} && ! -f $prog{$pid}->{filename} ) {
push @row, td( {-class=>$search_class} );
} else {
push @row, td( {-class=>$search_class},
checkbox(
-class => $search_class,
-name => 'PROGSELECT',
-label => '',
-value => "$prog{$pid}->{type}|$pid|$prog{$pid}->{name}|$prog{$pid}->{episode}|$prog{$pid}->{mode}|$prog{$pid}->{channel}",
-checked => 0,
-override => 1,
)
);
}
# Record links
my $links;
# History mode
if ( $opt->{HISTORY}->{current} ) {
if ( -f $prog{$pid}->{filename} ) {
# Play (Play Remote)
$links .= a( { -id=>'nowrap', -target=>'_blank', -class=>$search_class, -title=>"Stream from file on web server", -href=>build_url_playlist( '', 'playlistdirect', 'pid', $pid, $prog{$pid}->{mode}, $prog{$pid}->{type}, $opt->{STREAMTYPE}->{current}, $opt->{STREAMTYPE}->{current}, $opt->{BITRATE}->{current}, $opt->{VSIZE}->{current}, $opt->{VFR}->{current}, $opt->{VERSIONLIST}->{current} ) }, 'Play' ).'
';
# PlayFile
$links .= a( { -id=>'nowrap', -target=>'_blank', -class=>$search_class, -title=>"Play from local file", -href=>build_url_playlist( '', 'playlistfiles', 'pid', $pid, $prog{$pid}->{mode}, $prog{$pid}->{type}, undef ) }, 'Play File' ).'
';
# PlayDirect - depends on browser support
if ( $prog{$pid}->{filename} =~ m{\.(m4a|mp4|mp3)$} ) {
$links .= a( { -id=>'nowrap', -target=>'_blank', -class=>$search_class, -title=>"Stream file into browser", -href=>build_url_direct( '', $prog{$pid}->{type}, $pid, $prog{$pid}->{mode}, $opt->{STREAMTYPE}->{current}, $opt->{STREAMTYPE}->{current}, $opt->{HISTORY}->{current}, $opt->{BITRATE}->{current}, $opt->{VSIZE}->{current}, $opt->{VFR}->{current}, $opt->{VERSIONLIST}->{current}, 'playdirect' ) }, 'Play Direct' ).'
';
}
}
# Search mode
} else {
# Record
$links .= label( { -id=>'nowrap', -class=>$search_class, -title=>"Record '$prog{$pid}->{name} - $prog{$pid}->{episode}' Now", -onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='record_now'; form1.SEARCH.value='".encode_entities("$prog{$pid}->{type}|$pid|$prog{$pid}->{name}|$prog{$pid}->{episode}|$prog{$pid}->{mode}")."'; form1.target='_newtab_$pid'; form1.submit(); RestoreFormVars(form1); form1.target='';" }, 'Record' ).'
';
# Queue
$links .= label( { -id=>'nowrap', -class=>$search_class, -title=>"Queue '$prog{$pid}->{name} - $prog{$pid}->{episode}' for PVR Recording", -onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='pvr_queue'; form1.SEARCH.value='".encode_entities("$prog{$pid}->{type}|$pid|$prog{$pid}->{name}|$prog{$pid}->{episode}|$prog{$pid}->{mode}")."'; form1.submit(); RestoreFormVars(form1);" }, 'Queue' ).'
';
# Add Series
# escape regex metacharacters in programme name
(my $escaped_name = $prog{$pid}->{name}) =~ s/([\\\^\$\.\|\?\*\+\(\)\[\]])/\\\\$1/g;
$links .= label( {
-id=>'nowrap',
-class=>'search pointer_noul',
-title=>"Add Series '$prog{$pid}->{name}' to PVR",
-onClick=>"BackupFormVars(form1); form1.NEXTPAGE.value='pvr_add'; form1.SEARCH.value='".encode_entities("^$escaped_name\$")."'; form1.SEARCHFIELDS.value='name'; form1.PROGTYPES.value='$prog{$pid}->{type}'; form1.HISTORY.value='0'; form1.SINCE.value=''; form1.BEFORE.value=''; form1.submit(); RestoreFormVars(form1);" }, 'Add Series' );
}
# Add links to row
push @row, td( {-class=>$search_class}, $links );
# This builds each row in turn
for ( @displaycols ) {
# display thumb if defined (will have to use proxy to get file:// thumbs)
if ( /^thumbnail$/ ) {
if ( $prog{$pid}->{$_} !~ m{^https?://} ) {
$prog{$pid}->{$_} = DEFAULT_THUMBNAIL;
}
push @row, td( {-class=>$search_class}, a( { -title=>"Open original web URL", -class=>$search_class, -href=>$prog{$pid}->{web}, -target => "_blank" }, img( { -class=>$search_class, -height=>40, -src=>$prog{$pid}->{$_} } ) ) );
} elsif ( /^web$/ ) {
push @row, td( {-class=>$search_class}, a( { -title=>"Open original web URL", -class=>$search_class, -href=>$prog{$pid}->{$_}, -target => "_blank" }, 'Open URL' ) );
# Calculate the seconds difference between epoch_now and epoch_datestring and convert back into array_time
} elsif ( /^timeadded$/ ) {
my @t = gmtime( $time - $prog{$pid}->{$_} );
my $years = ($t[5]-70)."y " if ($t[5]-70) > 0;
push @row, td( {-class=>$search_class}, label( { -class=>$search_class, -title=>"Click for full info", -onClick=>"BackupFormVars(form1); form1.NEXTPAGE.value='show_info'; form1.INFO.value='".encode_entities("$prog{$pid}->{type}|$pid")."'; form1.target='_blank'; form1.submit(); RestoreFormVars(form1); form1.target='';" }, "${years}$t[7]d $t[2]h ago" ) );
} elsif ( /^expires$/ ) {
my $expires;
if ( $prog{$pid}->{$_} && $prog{$pid}->{$_} > $time ) {
my @t = gmtime( $prog{$pid}->{$_} - $time );
my $years = ($t[5]-70)."y " if ($t[5]-70) > 0;
$expires = "in ${years}$t[7]d $t[2]h";
}
push @row, td( {-class=>$search_class}, label( { -class=>$search_class, -title=>"Click for full info", -onClick=>"BackupFormVars(form1); form1.NEXTPAGE.value='show_info'; form1.INFO.value='".encode_entities("$prog{$pid}->{type}|$pid")."'; form1.target='_blank'; form1.submit(); RestoreFormVars(form1); form1.target='';" }, $expires ) );
# truncate the description if it is too long
} elsif ( /^desc$/ ) {
my $text = $prog{$pid}->{$_};
$text = substr($text, 0, 256).'...[more]' if length( $text ) > 256;
push @row, td( {-class=>$search_class}, label( { -class=>$search_class, -title=>"Click for full info", -onClick=>"BackupFormVars(form1); form1.NEXTPAGE.value='show_info'; form1.INFO.value='".encode_entities("$prog{$pid}->{type}|$pid")."'; form1.target='_blank'; form1.submit(); RestoreFormVars(form1); form1.target='';" }, $text ) );
# Name / Series link
} elsif ( /^name$/ ) {
push @row, td( {-class=>$search_class}, label( { -class=>$search_class, -id=>'underline', -title=>"Click to list '$prog{$pid}->{$_}'",
-onClick=>"
BackupFormVars(form1);
form1.NEXTPAGE.value='search_progs';
form1.SEARCHFIELDS.value='name';
form1.SEARCH.value='".encode_entities('^'.$prog{$pid}->{$_}.'$')."';
form1.PAGENO.value=1;
form1.submit();
RestoreFormVars(form1);
"}, $prog{$pid}->{$_} )
);
# Channel link
} elsif ( /^channel$/ ) {
push @row, td( {-class=>$search_class}, label( { -class=>$search_class, -id=>'underline', -title=>"Click to list '$prog{$pid}->{$_}'",
-onClick=>"
BackupFormVars(form1);
form1.NEXTPAGE.value='search_progs';
form1.CHANNEL.value='".encode_entities('^'.$prog{$pid}->{$_}.'$')."';
form1.EXCLUDECHANNEL.value='';
form1.SEARCH.value='.*';
form1.PAGENO.value=1;
form1.submit();
RestoreFormVars(form1);
"}, $prog{$pid}->{$_} )
);
# Category links
} elsif ( /^categories$/ ) {
my @cats = split /,/, $prog{$pid}->{$_};
for ( @cats ) {
my $category = $_;
$_ = label( { -class=>$search_class, -id=>'underline', -title=>"Click to list '$category'",
-onClick=>"
BackupFormVars(form1);
form1.NEXTPAGE.value='search_progs';
form1.EXCLUDE.value='';
form1.CATEGORY.value='".encode_entities($category)."';
form1.EXCLUDECATEGORY.value='';
form1.SEARCH.value='.*';
form1.PAGENO.value=1;
form1.submit();
RestoreFormVars(form1);
"},
$category );
}
push @row, td( {-class=>$search_class}, @cats );
} elsif ( /^filename$/ ) {
push @row, td( {-class=>$search_class}, label( { -class=>$search_class, -title=>"Click for full info", -onClick=>"BackupFormVars(form1); form1.NEXTPAGE.value='show_info'; form1.INFO.value='".encode_entities("$prog{$pid}->{type}|$pid")."'; form1.target='_blank'; form1.submit(); RestoreFormVars(form1); form1.target='';" }, decode_fs($prog{$pid}->{$_}) ) );
# Every other column type
} else {
push @row, td( {-class=>$search_class}, label( { -class=>$search_class, -title=>"Click for full info", -onClick=>"BackupFormVars(form1); form1.NEXTPAGE.value='show_info'; form1.INFO.value='".encode_entities("$prog{$pid}->{type}|$pid")."'; form1.target='_blank'; form1.submit(); RestoreFormVars(form1); form1.target='';" }, $prog{$pid}->{$_} ) );
}
}
push @html, Tr( {-class=>$search_class}, @row );
}
# Search form
print $fh start_form(
-name => "form1",
-method => "POST",
);
# Create options tabs and buttons
# Build tab 'buttons' (actually list labels)
# Add options buttons into the list
my @optrows_nav;
my @tablist = grep !/(BASICTAB|HIDDENTAB)/, @{ $layout->{taborder} };
for my $tabname ( @tablist ) {
my $label = $layout->{$tabname}->{title};
# Set the colour to grey and change tab appearance if it is selected
my $class = 'options_tab';
if ( defined $opt->{$tabname}->{current} && $opt->{$tabname}->{current} eq 'yes' ) {
$class = 'options_tab_sel';
}
push @optrows_nav, li( { -class=>$class, -id=>"li_${tabname}" },
label( {
-class => 'options_outer pointer_noul',
-id => 'button_'.$tabname,
-title => "Show $label tab",
-onClick => "show_options_tab( '$tabname', [ '".(join "', '", @tablist )."' ] );",
},
$label ),
)
}
# add a save button on to end of list
my $options_buttons = ul( { -class=>'options_tab' },
li( { -class=>'options_button' }, [
# Apply button (same as 'Search')
label( {
-class => 'options_outer pointer_noul',
-title => 'Apply Current Options',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='search_progs'; form1.PAGENO.value=1; form1.submit(); RestoreFormVars(form1);",
-role => "button",
},
'Apply Settings',
),
# Save as Default button
label( {
-class => 'options_outer pointer_noul',
-title => 'Remember Current Options as Default',
-onClick => "BackupFormVars(form1); form1.SAVE.value=1; form1.submit(); RestoreFormVars(form1);",
-role => "button",
},
'Save As Default',
),
] )
);
# Build each tab with it's contained options tables
my @opt_td;
my @opt_td_basic;
for my $tabname ( @{ $layout->{taborder} } ) {
my $tab = $layout->{$tabname};
my @order = @{ $tab->{order} };
my $heading = $tab->{heading};
# Set displayed tab status (i.e. style) based on posted/cookie vars (always display basic tab)
$tab->{style} = "display: none; visibility: collapse;";
$tab->{style} = "display: table-cell; visibility: visible;" if $tabname eq 'BASICTAB' || ( defined $opt->{$tabname}->{current} && $opt->{$tabname}->{current} eq 'yes' );
# Each option within the tab
my @optrows;
#push @optrows, td( { -class=>'options' }, label( { -class => 'options_heading' }, $heading ) ) if $heading;
for my $optname ( @order ) {
push @optrows, build_option_html( $opt->{$optname} );
}
# Set the basic search tab to be rowspan=3
if ( $tabname eq 'BASICTAB' ) {
push @opt_td_basic, td( { -class=>'options_outer', -id=>"tab_${tabname}", -rowspan=>3, -style=>"$tab->{style}", -role=>'search' },
table( { -class=>'options' }, Tr( { -class=>'options' }, [ @optrows ] ) )
);
} else {
push @opt_td, td( { -class=>'options_outer', -id=>"tab_${tabname}", -style=>"$tab->{style}" },
table( { -class=>'options' }, Tr( { -class=>'options' }, [ @optrows ] ) )
);
}
}
# Render outer options table frame (keeping some tabs hidden)
print $fh table( { -class=>'options_outer' },
Tr( { -class=>'options_outer' }, (join '', @opt_td_basic). td( { -class=>'options_outer' }, ul( { -class=>'options_tab', -role=>'navigation', 'aria-label'=>'Settings' }, @optrows_nav ) ) ).
Tr( { -class=>'options_outer' }, (join '', @opt_td) ).
Tr( { -class=>'options_outer' }, td( { -class=>'options_outer' }, $options_buttons ) )
);
# Grey-out 'Add Current Search to PVR' button if too many programme matches
my $add_search_class_suffix;
$add_search_class_suffix = ' darker' if $matchcount > 30;
my %action_button;
$action_button{'Search'} = a(
{
-class => 'action',
-title => 'Perform search based on search options',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='search_progs'; form1.PAGENO.value=1; form1.submit(); RestoreFormVars(form1);",
},
'Search'
);
$action_button{'Queue'} = a(
{
-class => 'action',
-title => 'Queue selected programmes (or Quick URL) for one-off recording',
-onClick => "if(! ( check_if_selected(document.form1, 'PROGSELECT') || form1.URL.value ) ) { alert('No Quick URL or programmes were selected'); return false; } BackupFormVars(form1); form1.SEARCH.value=''; form1.NEXTPAGE.value='pvr_queue'; form1.submit(); RestoreFormVars(form1); form1.URL.value=''; disable_selected_checkboxes(document.form1, 'PROGSELECT');",
},
'Queue'
);
$action_button{'Record'} = a(
{
-class => 'action',
-title => 'Immediately Record selected programmes (or Quick URL) in a new tab',
-onClick => "if(! ( check_if_selected(document.form1, 'PROGSELECT') || form1.URL.value ) ) { alert('No Quick URL or programmes were selected'); return false; } BackupFormVars(form1); form1.SEARCH.value=''; form1.NEXTPAGE.value='record_now'; var random=Math.floor(Math.random()*99999); form1.target='_newtab_'+random; form1.submit(); RestoreFormVars(form1); form1.target=''; form1.URL.value=''; disable_selected_checkboxes(document.form1, 'PROGSELECT');",
},
'Record'
);
$action_button{'Delete'} = a(
{
-class => 'action',
-title => 'Permanently delete selected recorded files',
-onClick => "if(! check_if_selected(document.form1, 'PROGSELECT')) { alert('No programmes were selected'); return false; } BackupFormVars(form1); form1.SEARCH.value=''; form1.NEXTPAGE.value='recordings_delete'; form1.submit(); RestoreFormVars(form1);",
},
'Delete'
);
$action_button{'Play'} = a(
{
-class => 'action',
-title => 'Get a Playlist based on selected programmes for remote file streaming in your media player',
-onClick => "if(! check_if_selected(document.form1, 'PROGSELECT')) { alert('No programmes were selected'); return false; } BackupFormVars(form1); form1.SEARCH.value=''; form1.ACTION.value='genplaylistdirect'; form1.submit(); RestoreFormVars(form1);",
},
'Play'
);
$action_button{'Play Files'} = a(
{
-class => 'action',
-title => 'Get a Playlist based on selected programmes for local file streaming in your media player',
-onClick => "if(! check_if_selected(document.form1, 'PROGSELECT')) { alert('No programmes were selected'); return false; } BackupFormVars(form1); form1.SEARCH.value=''; form1.ACTION.value='genplaylistfile'; form1.submit(); RestoreFormVars(form1);",
},
'Play Files'
);
# check for an non-whitespace advanced search entries
# excluding Programme Version and Search Future Schedule
my $num_adv_srch = grep /\S/, (
$opt->{EXCLUDE}->{current},
$opt->{EXCLUDECATEGORY}->{current},
$opt->{CATEGORY}->{current},
$opt->{CHANNEL}->{current},
$opt->{EXCLUDECHANNEL}->{current},
$opt->{SINCE}->{current},
$opt->{BEFORE}->{current}
);
(my $escaped_search = $opt->{SEARCH}->{current}) =~ s/'/\\'/g;
$action_button{'Add Search to PVR'} = a(
{
-class => 'action'.$add_search_class_suffix,
-title => 'Create a persistent PVR search using the current search terms (i.e. all below programmes)',
-onClick => "if ('".$escaped_search."' == '.*' && $num_adv_srch == 0) { alert('Search = .* will download all available programmes. Please enter a more specific search term or additional advanced search criteria (excluding $opt->{VERSIONLIST}->{title} and $opt->{FUTURE}->{title}).'); return false; } if ('".$escaped_search."' == '' ) { alert('Please enter a search term. Use Search = .* to record all programmes matching advanced search criteria.'); return false; } if ( $matchcount > 30 ) { alert('Please limit your search to result in no more than 30 current programmes'); return false; } BackupFormVars(form1); form1.NEXTPAGE.value='pvr_add'; form1.submit(); RestoreFormVars(form1);",
},
'Add Search to PVR'
);
#my $autorefresh = $cgi->cookie( 'AUTOWEBREFRESH' ) || $cgi->param( 'AUTOWEBREFRESH' );
$action_button{'Refresh Cache'} = a(
{
-class => 'action',
-title => 'Refresh the list of programmes - can take a while',
-onClick => "BackupFormVars(form1); form1.target='_newtab_refresh'; form1.NEXTPAGE.value='refresh'; form1.submit(); RestoreFormVars(form1); form1.target=''; form1.NEXTPAGE.value=''; ",
#-onClick => "window.frames['dataframe'].window.location.replace('?NEXTPAGE=refresh&AUTOWEBREFRESH=$autorefresh')",
},
'Refresh Cache'
);
# Render action bar
my @actionbar;
if ( $opt->{HISTORY}->{current} ) {
push @actionbar, div( { -class=>'action', -role=>'navigation', 'aria-label'=>'Actions' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
$action_button{'Search'},
$action_button{'Delete'},
$action_button{'Play'},
$action_button{'Play Files'},
]),
),
);
} else {
push @actionbar, div( { -class=>'action', -role=>'navigation', 'aria-label'=>'Actions' },
ul( { -class=>'action' },
li( { -class=>'action' }, [
$action_button{'Search'},
$action_button{'Record'},
$action_button{'Queue'},
$action_button{'Add Search to PVR'},
$action_button{'Refresh Cache'},
]),
),
);
}
print $fh @actionbar;
print $fh @pagetrail;
print $fh table( {-class=>'search', -role=>'main' }, @html );
print $fh @pagetrail;
print $fh @actionbar;
print $fh div( {id=>'status'} );
print $fh end_form();
return 0;
}
# Build page trail
sub pagetrail {
my ( $page, $pagesize, $count, $trailsize ) = ( @_ );
# How many pages
my $pages = int( $count / $pagesize );
$pages++ if $count % $pagesize;
# If we request a page that is too high
$page = $pages if $page > $pages;
# Calc first and last programme numbers
my $first = $pagesize * ($page - 1);
my $last = $first + $pagesize;
$last = $count if $last > $count;
#print $se "PAGETRAIL: page=$page, first=$first, last=$first, pages=$pages, trailsize=$trailsize\n";
# Page trail
my @pagetrail;
push @pagetrail, td( { -class=>'pagetrail pointer' }, label( {
-title => "Previous Page",
-class => 'pagetrail pointer',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='search_progs'; form1.PAGENO.value=$page-1; form1.submit(); RestoreFormVars(form1);",},
"<<",
)) if $page > 1;
push @pagetrail, td( { -class=>'pagetrail pointer' }, label( {
-title => "Page 1",
-class => 'pagetrail pointer',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='search_progs'; form1.PAGENO.value=1; form1.submit(); RestoreFormVars(form1);",},
"1",
)) if $page > 1;
push @pagetrail, td( { -class=>'pagetrail' }, '...' ) if $page > $trailsize+2;
for (my $pn=$page-$trailsize; $pn <= $page+$trailsize; $pn++) {
push @pagetrail, td( { -class=>'pagetrail pointer' }, label( {
-title => "Page $pn",
-class => 'pagetrail pointer',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='search_progs'; form1.PAGENO.value='$pn'; form1.submit(); RestoreFormVars(form1);",},
"$pn",
)) if $pn > 1 && $pn != $page && $pn < $pages;
push @pagetrail, td( { -class=>'pagetrail' }, label( {
-title => "Current Page",
-class => 'pagetrail-current', },
"$page",
)) if $pn == $page;
}
push @pagetrail, td( { -class=>'pagetrail' }, '...' ) if $page < $pages-$trailsize-1;
push @pagetrail, td( { -class=>'pagetrail pointer' }, label( {
-title => "Page ".$pages,
-class => 'pagetrail pointer',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='search_progs'; form1.PAGENO.value=$pages; form1.submit(); RestoreFormVars(form1);",},
"$pages",
)) if $page < $pages;
push @pagetrail, td( { -class=>'pagetrail pointer' }, label( {
-title => "Next Page",
-class => 'pagetrail pointer',
-onClick => "BackupFormVars(form1); form1.NEXTPAGE.value='search_progs'; form1.PAGENO.value=$page+1; form1.submit(); RestoreFormVars(form1);",},
">>",
)) if $page < $pages;
push @pagetrail, td( { -class=>'pagetrail' }, label( {
-title => "Matches",
-class => 'pagetrail',},
"($count programmes)",
));
my @html = table( { -id=>'centered', -class=>'pagetrail' }, Tr( { -class=>'pagetrail' }, @pagetrail ));
return ($first, $last, @html);
}
sub get_progs {
my @params = @_;
my $options = '';
my $fields;
$fields .= "|<$_>" for @headings;
my ( @webrequest_args ) = ( build_cmd_options( grep !/^(PVRHOLDOFF)$/, @params ), "listformat=ENTRY${fields}" );
# Page params
if ( $opt->{PAGENO}->{current} && $opt->{PAGESIZE}->{current} ) {
push @webrequest_args, ( "page=$opt->{PAGENO}->{current}", "pagesize=$opt->{PAGESIZE}->{current}" );
}
# Sort param
push @webrequest_args, "sortreverse=$opt->{PAGENO}->{current}" if $opt->{REVERSE}->{current};
# sort reverse param
push @webrequest_args, "sortmatches=$opt->{SORT}->{current}" if $opt->{SORT}->{current} && $opt->{SORT}->{current} ne 'name';
# Run command
my @list = get_cmd_output(
@gip_cmd_base,
'--webrequest',
get_iplayer_webrequest_args( @webrequest_args ),
);
return ( '0', join("\n", @list) ) if $? && not $IGNOREEXIT;
# Get total matches count
my $matchcount = pop @list;
$matchcount = $1 if $matchcount =~ m{^INFO:\s*(\d+?)\s+};
for ( grep /^ENTRY/, @list ) {
chomp();
# Strip white space
s/\|\s*$//;
my $record;
my @element = split /\|/, $_;
shift @element;
# Put data for this contact into temporary record hash for this user
for (my $i=0; $i <= $#headings; $i++) {
$record->{$headings[$i]} = $element[$i];
}
my $search_class = 'search';
# get the real path if file is defined
if ( $record->{filename} && $record->{filename} ne "" ) {
$record->{filename} = search_absolute_path( encode_fs($record->{filename}) );
}
# store record in the prog global hash (prog => pid)
$prog{ $record->{'pid'} } = $record;
push @pids, $record->{'pid'};
}
return ( $matchcount, '' );
}
#
# Get the columns to display
#
sub get_display_cols {
@displaycols = ();
# Set default status for columns options tab checkboxes
my %cols_status;
# Add some default headings for history mode
push @headings_default, 'mode' if $opt->{HISTORY}->{current};
# Determine which columns to display (all if $cols not defined)
my $cols = join(",", $opt->{COLS}->{current} ) || join ',', @headings_default;
my @columns = split /,/, $cols;
# Re-sort selected display columns into original header order
for my $heading (@headings) {
if ( grep /^$heading$/, @columns ) {
# Remove display of mode and filename if not history mode
if ( ( ! $opt->{HISTORY}->{current} ) && $heading =~ /^(mode|filename)$/ ) {
# skip
} else {
push @displaycols, $heading;
}
$cols_status{$heading} = 1;
}
}
# Make sure we select all if no cols are specified
@displaycols = @headings_default if $#displaycols < 0;
# Set defaults for checkboxes
$opt->{COLS}->{status} = \%cols_status;
# Rebuild the hash for the checkboxes
%cols_order = ();
%cols_names = ();
for ( my $i = 0; $i <= $#headings; $i++ ) {
$cols_names{$headings[$i]} = $fieldname{$headings[$i]};
$cols_order{$i} = $headings[$i];
}
return 0;
}
#############################################
#
# Form Header
#
#############################################
sub form_header {
my $request_host = shift;
my $nextpage = shift || $cgi->param( 'NEXTPAGE' );
print $fh $cgi->start_form(
-name => "formheader",
-method => "POST",
);
# set $class for tab selection in nav bar
my $class = {};
$class->{search} = 'nav_tab';
$class->{recordings} = 'nav_tab';
$class->{pvrlist} = 'nav_tab';
$class->{pvrrun} = 'nav_tab';
$class->{search} = 'nav_tab_sel' if ( $nextpage eq 'search_progs' || ! $nextpage ) && ! $opt->{HISTORY}->{current};
$class->{recordings} = 'nav_tab_sel' if $nextpage eq 'search_history' || $opt->{HISTORY}->{current};
$class->{pvrrun} = 'nav_tab_sel' if $nextpage eq 'pvr_run';
$class->{pvrlist} = 'nav_tab_sel' if $nextpage =~ m{^(pvr_list|pvr_queue|pvr_del)$};
print $fh div( { -class=>'nav', -role=>'navigation' },
ul( { -class=>'nav' },
li( { -id=>'logo', -class=>'nav_tab' },
span( { -class=>'logotext' }, 'get_iplayer' )
).
li( { -class=>$class->{search} }, a( { -class=>'nav', -title=>'Main search page', -onClick => "BackupFormVars(formheader); formheader.NEXTPAGE.value='search_progs'; formheader.submit(); RestoreFormVars(formheader);" }, 'Search' ) ).
li( { -class=>$class->{recordings} }, a( { -class=>'nav', -title=>'History search page', -onClick => "BackupFormVars(formheader); formheader.NEXTPAGE.value='search_history'; formheader.submit(); RestoreFormVars(formheader);" }, 'Recordings' ) ).
li( { -class=>$class->{pvrlist} }, a( { -class=>'nav', -title=>'List all saved PVR searches', -onClick => "BackupFormVars(formheader); formheader.NEXTPAGE.value='pvr_list'; formheader.submit(); RestoreFormVars(formheader);" }, 'PVR List' ) ).
li( { -class=>$class->{pvrrun} }, a( { -class=>'nav', -title=>'Run the PVR now - wait for the PVR to complete', -onClick => "BackupFormVars(formheader); formheader.NEXTPAGE.value='pvr_run'; formheader.target='_newtab_pvrrun'; formheader.submit(); RestoreFormVars(formheader); formheader.target='';" }, 'Run PVR' ) ).
li( { -class=>'nav_tab' }, a( { -class=>'nav', -title=>'Show help and instructions', -href => "https://github.com/get-iplayer/get_iplayer/wiki/webpvr", -target => "_newtab_help" }, 'Help' ) )
),
);
print $fh hidden( -name => 'AUTOPVRRUN', -value => $opt->{AUTOPVRRUN}->{current}, -override => 1 );
print $fh hidden( -name => 'NEXTPAGE', -value => 'search_progs', -override => 1 );
print $fh $cgi->end_form();
}
# Form Footer
sub form_footer {
#print $fh "";
#print $fh "";
#
print $fh p( b({-class=>"footer"},
"get_iplayer Web PVR Manager $VERSION_TEXT, ©2009-2010 Phil Lewis - Licensed under GPLv3"
));
}
# End HTML
sub html_end {
print $fh "\n