forked from HPR/hpr-tools
		
	
		
			
				
	
	
		
			2107 lines
		
	
	
		
			75 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			2107 lines
		
	
	
		
			75 KiB
		
	
	
	
		
			Perl
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env perl
 | 
						|
#===============================================================================
 | 
						|
#
 | 
						|
#         FILE: make_shownotes
 | 
						|
#
 | 
						|
#        USAGE: ./make_shownotes [-help] [-documentation] [-debug=N]
 | 
						|
#               [-from=DATE] [-[no]comments] [-[no]markcomments] [-[no]ctext]
 | 
						|
#               [-lastrecording=DATETIME] [-[no]silent] [-out=FILE]
 | 
						|
#               [-episode=[N|auto]] [-[no]overwrite] [-mailnotes[=FILE]]
 | 
						|
#               [-anyotherbusiness=FILE] [-template=FILE] [-config=FILE]
 | 
						|
#               [-interlock=PASSWORD]
 | 
						|
#
 | 
						|
#  DESCRIPTION: Builds shownotes for a Community News show from the HPR
 | 
						|
#               database using a template.  Writes the result to STDOUT or to
 | 
						|
#               a file. Also writes to the database if requested.
 | 
						|
#
 | 
						|
#      OPTIONS: ---
 | 
						|
# REQUIREMENTS: ---
 | 
						|
#         BUGS: ---
 | 
						|
#        NOTES: To view the entire documentation use:
 | 
						|
#
 | 
						|
#               ./make_shownotes -documentation
 | 
						|
#
 | 
						|
#               To create a PDF version of the documentation:
 | 
						|
#
 | 
						|
#               pod2pdf make_shownotes --out=make_shownotes.pdf
 | 
						|
#
 | 
						|
#               Where pod2pdf comes from App::pod2pdf
 | 
						|
#
 | 
						|
#       AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
 | 
						|
#      VERSION: 0.2.2
 | 
						|
#      CREATED: 2014-04-24 16:08:30
 | 
						|
#     REVISION: 2023-06-30 23:52:06
 | 
						|
#
 | 
						|
#===============================================================================
 | 
						|
 | 
						|
use 5.010;
 | 
						|
use strict;
 | 
						|
use warnings;
 | 
						|
use utf8;
 | 
						|
 | 
						|
use Carp;
 | 
						|
use Getopt::Long;
 | 
						|
use Pod::Usage;
 | 
						|
 | 
						|
use Config::General;
 | 
						|
 | 
						|
use Date::Parse;
 | 
						|
use Date::Calc qw{:all};
 | 
						|
use DateTime;
 | 
						|
use DateTime::Duration;
 | 
						|
 | 
						|
use Template;
 | 
						|
use Template::Filters;
 | 
						|
Template::Filters->use_html_entities;    # Use HTML::Entities in the template
 | 
						|
 | 
						|
use HTML::Entities;
 | 
						|
 | 
						|
use DBI;
 | 
						|
 | 
						|
use Data::Dumper;
 | 
						|
 | 
						|
#
 | 
						|
# Version number (manually incremented)
 | 
						|
#
 | 
						|
our $VERSION = '0.2.2';
 | 
						|
 | 
						|
#
 | 
						|
# Various constants
 | 
						|
#
 | 
						|
( my $PROG = $0 ) =~ s|.*/||mx;
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Declarations
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
#
 | 
						|
# Constants and other declarations
 | 
						|
#
 | 
						|
my $basedir    = "$ENV{HOME}/HPR/Community_News";
 | 
						|
my $configfile = "$basedir/.hpr_db.cfg";
 | 
						|
my $bpfile     = "$basedir/shownote_template.tpl";
 | 
						|
 | 
						|
my $title_template = 'HPR Community News for %s %s';
 | 
						|
 | 
						|
#
 | 
						|
# Needed to allow an older episode to have its notes regenerated. This is an
 | 
						|
# 'apg'-generated password which is just hard to remember and requires some
 | 
						|
# thought to use. The idea is to prevent older shownote rewriting by accident.
 | 
						|
#
 | 
						|
my $interlock_password = 'lumRacboikac';
 | 
						|
my $interlock_enabled = 0;
 | 
						|
 | 
						|
my ( $dbh, $sth1, $h1 );
 | 
						|
my ( @startdate, $hosts, $shows );
 | 
						|
my ( @dc_lr, $dt_lr, @dc_lm, $dt_lm );
 | 
						|
my ( $t_days, $missed_comments, $missed_count );
 | 
						|
my ( $comments, $comment_count, $past_count, $ignore_count );
 | 
						|
my ( %past, %current );
 | 
						|
 | 
						|
#
 | 
						|
# The normal recording time (UTC). Any change should be copied in the POD
 | 
						|
# documentation below.
 | 
						|
# TODO: Should this be in a configuration file?
 | 
						|
#
 | 
						|
my @deftime = (15, 00, 00);
 | 
						|
 | 
						|
#
 | 
						|
# Earliest comment release time
 | 
						|
#
 | 
						|
my @releasetime = (19, 00, 00);
 | 
						|
 | 
						|
#
 | 
						|
# Enable Unicode mode
 | 
						|
#
 | 
						|
binmode STDOUT, ":encoding(UTF-8)";
 | 
						|
binmode STDERR, ":encoding(UTF-8)";
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Options and arguments
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
#
 | 
						|
# Option defaults
 | 
						|
#
 | 
						|
my $DEFDEBUG = 0;
 | 
						|
 | 
						|
#
 | 
						|
# Process options
 | 
						|
#
 | 
						|
my %options;
 | 
						|
Options( \%options );
 | 
						|
 | 
						|
#
 | 
						|
# Default help is just the USAGE section
 | 
						|
#
 | 
						|
pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 0, -exitval => 1 )
 | 
						|
    if ( $options{'help'} );
 | 
						|
 | 
						|
#
 | 
						|
# Full documentation if requested with -documentation
 | 
						|
#
 | 
						|
pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 2, -exitval => 1 )
 | 
						|
    if ( $options{'documentation'} );
 | 
						|
 | 
						|
#
 | 
						|
# Collect options
 | 
						|
#
 | 
						|
my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEFDEBUG );
 | 
						|
my $cfgfile
 | 
						|
    = ( defined( $options{config} ) ? $options{config} : $configfile );
 | 
						|
my $silent = ( defined( $options{silent} ) ? $options{silent} : 0 );
 | 
						|
my $show_comments
 | 
						|
    = ( defined( $options{comments} ) ? $options{comments} : 0 );
 | 
						|
my $mark_comments
 | 
						|
    = ( defined( $options{markcomments} ) ? $options{markcomments} : 0 );
 | 
						|
my $ctext         = ( defined( $options{ctext} ) ? $options{ctext} : 0 );
 | 
						|
my $lastrecording = $options{lastrecording};
 | 
						|
my $outfile       = $options{out};
 | 
						|
my $episode       = $options{episode};
 | 
						|
my $overwrite = ( defined( $options{overwrite} ) ? $options{overwrite} : 0 );
 | 
						|
my $template
 | 
						|
    = ( defined( $options{template} ) ? $options{template} : $bpfile );
 | 
						|
my $mailnotes = $options{mailnotes};
 | 
						|
my $aobfile   = $options{anyotherbusiness};
 | 
						|
my $interlock = $options{interlock};
 | 
						|
 | 
						|
#
 | 
						|
# Sanity checks
 | 
						|
#
 | 
						|
die "Unable to find configuration file $cfgfile\n" unless ( -e $cfgfile );
 | 
						|
if ( defined($episode) ) {
 | 
						|
    if ( $episode =~ /^\d+$/ ) {
 | 
						|
        die "Episode number must be greater than zero\n"
 | 
						|
            unless ( $episode > 0 );
 | 
						|
    }
 | 
						|
    else {
 | 
						|
        die "Episode must be a number or 'auto'\n"
 | 
						|
            unless ( $episode eq 'auto' );
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
die "Error: Unable to find template $template\n" unless -r $template;
 | 
						|
 | 
						|
#
 | 
						|
# We accept '-mailnotes' meaning we want to use a default set of mail notes,
 | 
						|
# or '-mailnotes=FILE' which means the notes are in a file for inclusion. If
 | 
						|
# the option is omitted then we don't include mail notes (and the template is
 | 
						|
# expected to do the right thing).
 | 
						|
#
 | 
						|
if (defined($mailnotes)) {
 | 
						|
    if ($mailnotes =~ /^$/) {
 | 
						|
        #
 | 
						|
        # The default mail inclusion is provided in a named BLOCK directive in
 | 
						|
        # the template. The name is hard-wired here
 | 
						|
        #
 | 
						|
        # FIXME: there's a dependency between the script and the template here
 | 
						|
        # which is inflexible.
 | 
						|
        #
 | 
						|
        $mailnotes = 'default_mail';
 | 
						|
    }
 | 
						|
    else {
 | 
						|
        die "Error: Unable to find includefile '$mailnotes'\n" unless -r $mailnotes;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#
 | 
						|
# The -anyotherbusiness=FILE or -aob=FILE options provide an HTML file to be
 | 
						|
# added to the end of the notes.
 | 
						|
#
 | 
						|
if (defined($aobfile)) {
 | 
						|
    die "Error: Unable to find includefile '$aobfile'\n" unless -r $aobfile;
 | 
						|
}
 | 
						|
 | 
						|
#
 | 
						|
# Use the date provided or the default
 | 
						|
#
 | 
						|
if ( defined( $options{from} ) ) {
 | 
						|
    #
 | 
						|
    # Parse and perform rudimentary validation on the -from option
 | 
						|
    #
 | 
						|
#    my @parsed = strptime( $options{from} );
 | 
						|
#    die "Invalid -from=DATE option '$options{from}'\n"
 | 
						|
#        unless ( defined( $parsed[3] )
 | 
						|
#        && defined( $parsed[4] )
 | 
						|
#        && defined( $parsed[5] ) );
 | 
						|
#
 | 
						|
#    $parsed[5] += 1900;
 | 
						|
#    $parsed[4] += 1;
 | 
						|
#    @startdate = @parsed[ 5, 4, 3 ];
 | 
						|
    @startdate = parse_to_dc($options{from}, undef);
 | 
						|
}
 | 
						|
else {
 | 
						|
    #
 | 
						|
    # Default to the current date
 | 
						|
    #
 | 
						|
    @startdate = Today();
 | 
						|
}
 | 
						|
 | 
						|
#
 | 
						|
# If -interlock=PASSWORD was provided is the password valid?
 | 
						|
#
 | 
						|
if ( defined($interlock) ) {
 | 
						|
    $interlock_enabled = $interlock eq $interlock_password;
 | 
						|
    emit( $silent, "Interlock ",
 | 
						|
        ( $interlock_enabled ? "accepted" : "rejected" ), "\n" );
 | 
						|
}
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Configuration file - load data
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
emit( $silent, "Configuration file: ", $cfgfile, "\n" );
 | 
						|
my $conf = Config::General->new(
 | 
						|
    -ConfigFile      => $cfgfile,
 | 
						|
    -InterPolateVars => 1,
 | 
						|
    -ExtendedAccess  => 1,
 | 
						|
);
 | 
						|
my %config = $conf->getall();
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Date setup
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
#
 | 
						|
# Transfer Date::Calc values into a hash for initialising a DateTime object.
 | 
						|
# Force the day to 1
 | 
						|
#
 | 
						|
my ( @sd, $dt );
 | 
						|
@sd = ( @startdate, 0, 0, 0 );
 | 
						|
$sd[2] = 1;
 | 
						|
$dt = dc_to_dt(\@sd);
 | 
						|
 | 
						|
emit( $silent, "Start of month: ", $dt->ymd, "\n" );
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Connect to the database
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
my $dbhost = $config{database}->{host} // '127.0.0.1';
 | 
						|
my $dbport = $config{database}->{port} // 3306;
 | 
						|
my $dbname = $config{database}->{name};
 | 
						|
my $dbuser = $config{database}->{user};
 | 
						|
my $dbpwd  = $config{database}->{password};
 | 
						|
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
 | 
						|
    $dbuser, $dbpwd, { AutoCommit => 1 } )
 | 
						|
    or croak $DBI::errstr;
 | 
						|
 | 
						|
#
 | 
						|
# Enable client-side UTF8
 | 
						|
#
 | 
						|
$dbh->{mysql_enable_utf8} = 1;
 | 
						|
 | 
						|
#
 | 
						|
# Set the local timezone to UTC for this connection
 | 
						|
#
 | 
						|
$dbh->do("set time_zone = '+00:00'") or carp $dbh->errstr;
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Open the output file (or STDOUT) - we may need the date to do it
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
my $outfh;
 | 
						|
if ($outfile) {
 | 
						|
    $outfile
 | 
						|
        = sprintf( $outfile, sprintf( "%d-%02d", $dt->year, $dt->month ) )
 | 
						|
        if ( $outfile =~ /%s/ );
 | 
						|
    emit( $silent, "Output: ", $outfile, "\n" );
 | 
						|
 | 
						|
    open( $outfh, ">:encoding(UTF-8)", $outfile )
 | 
						|
        or croak "Unable to open $outfile for writing: $!";
 | 
						|
}
 | 
						|
else {
 | 
						|
    open( $outfh, ">&", \*STDOUT )
 | 
						|
        or croak "Unable to initialise for writing: $!";
 | 
						|
}
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Check the episode specification if given
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
if ( defined($episode) ) {
 | 
						|
    my $title = sprintf( $title_template, $dt->month_name, $dt->year );
 | 
						|
 | 
						|
    emit( $silent, "\n" );
 | 
						|
 | 
						|
    #
 | 
						|
    # Is it a number?
 | 
						|
    #
 | 
						|
    if ( $episode =~ /^\d+$/ ) {
 | 
						|
        emit( $silent, "Writing to numbered episode option selected\n" );
 | 
						|
 | 
						|
        #
 | 
						|
        # Does the number exist in the database?
 | 
						|
        #
 | 
						|
        $sth1 = $dbh->prepare(q{SELECT * FROM eps WHERE id = ?});
 | 
						|
        $sth1->execute($episode);
 | 
						|
        if ( $dbh->err ) {
 | 
						|
            carp $dbh->errstr;
 | 
						|
        }
 | 
						|
        if ( $h1 = $sth1->fetchrow_hashref() ) {
 | 
						|
            #
 | 
						|
            # Episode exists, do more checks
 | 
						|
            #
 | 
						|
            emit( $silent, "Found episode $episode\n" );
 | 
						|
            emit( $silent, "Title: ", $h1->{title}, "\n" );
 | 
						|
            die "Error: wrong show selected\n"
 | 
						|
                unless ( $h1->{title} eq $title );
 | 
						|
 | 
						|
            unless (validate_date($h1->{date}) || $interlock_enabled) {
 | 
						|
                die "Error: show $episode has a date in the past\n";
 | 
						|
            }
 | 
						|
 | 
						|
            unless ($overwrite) {
 | 
						|
                die "Error: show $episode already has notes\n"
 | 
						|
                    unless length( $h1->{notes} ) == 0;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            die "Error: episode $episode does not exist in the database\n";
 | 
						|
        }
 | 
						|
    }
 | 
						|
    else {
 | 
						|
        #
 | 
						|
        # The required episode is 'auto' (we already checked). Now we actually
 | 
						|
        # find the episode number corresponding to the month we're processing.
 | 
						|
        # We do this by searching for the relevant title in the database.
 | 
						|
        #
 | 
						|
        emit( $silent, "Searching for show title: '$title'\n" );
 | 
						|
        $sth1 = $dbh->prepare(q{SELECT * FROM eps WHERE title = ?});
 | 
						|
        $sth1->execute($title);
 | 
						|
        if ( $dbh->err ) {
 | 
						|
            carp $dbh->errstr;
 | 
						|
        }
 | 
						|
        if ( $h1 = $sth1->fetchrow_hashref() ) {
 | 
						|
            #
 | 
						|
            # Found the episode by title
 | 
						|
            #
 | 
						|
            $episode = $h1->{id};
 | 
						|
            emit( $silent, "Found episode $episode\n" );
 | 
						|
            emit( $silent, "Title: ", $h1->{title}, "\n" );
 | 
						|
 | 
						|
            unless (validate_date($h1->{date}) || $interlock_enabled) {
 | 
						|
                die "Error: show $episode has a date in the past\n";
 | 
						|
            }
 | 
						|
 | 
						|
            unless ($overwrite) {
 | 
						|
                die "Error: show $episode already has notes\n"
 | 
						|
                    unless length( $h1->{notes} ) == 0;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            die 'Error: Unable to find an episode '
 | 
						|
                . "for the selected month's notes\n";
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# If asked (-comments -markcomments) compute the last recording date
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
if ($show_comments && $mark_comments) {
 | 
						|
    #
 | 
						|
    # We're marking comments so need to find the date of the recording of the
 | 
						|
    # last show.
 | 
						|
    #
 | 
						|
    # It's possible to specify date and time of the last recording via option
 | 
						|
    # '-lastrecording=DATETIME' (in case we recorded early or something), but
 | 
						|
    # it needs to be parsed.
 | 
						|
    #
 | 
						|
    if ( defined( $options{lastrecording} ) ) {
 | 
						|
        #
 | 
						|
        # Parse and perform rudimentary validation on the -lastrecording option
 | 
						|
        #
 | 
						|
        @dc_lr = parse_to_dc( $options{lastrecording}, \@deftime );
 | 
						|
 | 
						|
    }
 | 
						|
    else {
 | 
						|
        #
 | 
						|
        # Otherwise we assume the recording was on a Saturday and compute when
 | 
						|
        # that was. We get back the date and time of the recording as
 | 
						|
        # a Date::Calc object.
 | 
						|
        #
 | 
						|
        @dc_lr = find_last_recording( \@startdate, \@deftime );
 | 
						|
    }
 | 
						|
 | 
						|
    #
 | 
						|
    # Convert the D::C datetime to a DateTime object
 | 
						|
    #
 | 
						|
    $dt_lr = dc_to_dt(\@dc_lr);
 | 
						|
 | 
						|
    # Report it for checking (since this algorithm is complex)
 | 
						|
    emit(
 | 
						|
        $silent,
 | 
						|
        sprintf("* %-100s *\n",
 | 
						|
            ( defined( $options{lastrecording} ) ? 'Given' : 'Found' ) .
 | 
						|
            ' last recording date on ' .
 | 
						|
            $dt_lr->datetime . ' time zone ' . $dt_lr->strftime('%Z') .
 | 
						|
            ' (' . $dt_lr->epoch . ')'
 | 
						|
        )
 | 
						|
    );
 | 
						|
 | 
						|
    #
 | 
						|
    # Also, we need to know the last month for comment marking
 | 
						|
    #
 | 
						|
    @dc_lm = find_last_month(\@startdate);
 | 
						|
    $dt_lm = dc_to_dt(\@dc_lm);
 | 
						|
 | 
						|
    # Report it for checking (since this algorithm is complex)
 | 
						|
    emit(
 | 
						|
        $silent,
 | 
						|
        sprintf("* %-100s *\n",
 | 
						|
            'Last month computed to be ' .
 | 
						|
            $dt_lm->datetime . ' time zone ' . $dt_lm->strftime('%Z') .
 | 
						|
            ' (' . $dt_lm->epoch . ')'
 | 
						|
        )
 | 
						|
    );
 | 
						|
 | 
						|
    #
 | 
						|
    # Work out if the the recording date was before the end of the last
 | 
						|
    # reviewed month.
 | 
						|
    #
 | 
						|
    $t_days = trailing_days(\@dc_lr, \@dc_lm);
 | 
						|
    emit(
 | 
						|
        $silent,
 | 
						|
        sprintf("* %-100s *\n",
 | 
						|
            'Recording was in the reviewed month and not on the ' .
 | 
						|
            'last day, so comments may have been missed'
 | 
						|
        )
 | 
						|
    ) if $t_days;
 | 
						|
 | 
						|
    _debug( $DEBUG > 2, '@dc_lr = (' . join(',',@dc_lr) .')' );
 | 
						|
    _debug( $DEBUG > 2, '$dt_lr->ymd = ' . $dt_lr->ymd );
 | 
						|
    _debug( $DEBUG > 2, '$dt_lr->hms = ' . $dt_lr->hms );
 | 
						|
    _debug( $DEBUG > 2, '@dc_lm = (' . join(',',@dc_lm) .')' );
 | 
						|
}
 | 
						|
else {
 | 
						|
    #
 | 
						|
    # We now need a default for $dt_lr in all cases because the query has been
 | 
						|
    # changed.
 | 
						|
    #
 | 
						|
    @dc_lr = find_last_recording( \@startdate, \@deftime );
 | 
						|
    $dt_lr = dc_to_dt(\@dc_lr);
 | 
						|
}
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Data collection
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
#
 | 
						|
# Prepare to get any new hosts for the required month. We let MySQL compute
 | 
						|
# the end of the month. Order by date of first show.
 | 
						|
#
 | 
						|
$sth1 = $dbh->prepare(
 | 
						|
    q{SELECT ho.host, ho.hostid, md.mindate
 | 
						|
        FROM hosts ho
 | 
						|
        JOIN (SELECT hostid, MIN(date) mindate FROM eps GROUP BY hostid) AS md
 | 
						|
            ON ho.hostid = md.hostid
 | 
						|
        WHERE md.mindate >= ? AND md.mindate < last_day(?) + interval 1 day
 | 
						|
        ORDER BY mindate}
 | 
						|
);
 | 
						|
$sth1->execute( $dt->ymd, $dt->ymd );
 | 
						|
if ( $dbh->err ) {
 | 
						|
    carp $dbh->errstr;
 | 
						|
}
 | 
						|
 | 
						|
#
 | 
						|
# Grab the data as an arrayref of hashrefs
 | 
						|
#
 | 
						|
$hosts = $sth1->fetchall_arrayref( {} );
 | 
						|
 | 
						|
#
 | 
						|
# Prepare to get the episodes for the required month. We let MySQL compute the
 | 
						|
# end of the month. We include every column here just in case they'll be
 | 
						|
# useful in the template, though this requires some aliasing.
 | 
						|
# 2015-04-05 The date field has been reformatted so that the 'date' plugin in
 | 
						|
# the form is happy with it.
 | 
						|
#
 | 
						|
$sth1 = $dbh->prepare(
 | 
						|
    q{SELECT    eps.id AS eps_id,
 | 
						|
--              eps.type,
 | 
						|
--              date_format(eps.date,'%a %Y-%m-%d') AS date,
 | 
						|
                date_format(eps.date,'00:00:00 %d/%m/%Y') AS date,
 | 
						|
--              eps.date,
 | 
						|
                eps.title,
 | 
						|
                sec_to_time(eps.duration) as length,
 | 
						|
                eps.summary,
 | 
						|
                eps.notes,
 | 
						|
--              eps.host AS eps_host,
 | 
						|
                eps.hostid AS eps_hostid,
 | 
						|
                eps.series,
 | 
						|
                eps.explicit,
 | 
						|
                eps.license AS eps_license,
 | 
						|
                eps.tags,
 | 
						|
                eps.version,
 | 
						|
                eps.valid AS eps_valid,
 | 
						|
                ho.hostid AS ho_hostid,
 | 
						|
                ho.host AS ho_host,
 | 
						|
                ho.email,
 | 
						|
                ho.profile, -- was website,
 | 
						|
                ho.license AS ho_license,
 | 
						|
--              ho.repeat,
 | 
						|
                ho.valid AS ho_valid
 | 
						|
            FROM eps
 | 
						|
            JOIN hosts ho ON eps.hostid = ho.hostid
 | 
						|
        WHERE eps.date >= ?
 | 
						|
        AND eps.date < last_day(?) + interval 1 day
 | 
						|
        ORDER BY id}
 | 
						|
);
 | 
						|
$sth1->execute( $dt->ymd, $dt->ymd );
 | 
						|
if ( $dbh->err ) {
 | 
						|
    carp $dbh->errstr;
 | 
						|
}
 | 
						|
 | 
						|
#
 | 
						|
# Grab the data as an arrayref of hashrefs
 | 
						|
#
 | 
						|
$shows = $sth1->fetchall_arrayref( {} );
 | 
						|
 | 
						|
#
 | 
						|
# Collect the comments if requested
 | 
						|
#
 | 
						|
if ($show_comments) {
 | 
						|
 | 
						|
    #
 | 
						|
    # Grab the comments for the selected period. These have weird \' sequences
 | 
						|
    # in the author, title and text, which doesn't seem right, so we strip
 | 
						|
    # them.  Note that the end date in date ranges seems only to work as
 | 
						|
    # 'last_day(?) + interval 1 day' presumably because without it the date is
 | 
						|
    # interpreted as midnight the previous day (e.g. 2014-06-30 is early on
 | 
						|
    # this day, not early on 2014-07-01 whereas adding 1 day gets this right).
 | 
						|
    #
 | 
						|
    # The logic here was rewritten 2015-03-04 and consists of:
 | 
						|
    # - the sub-select collects the id numbers of all comments that have
 | 
						|
    #   occurred in the selected period
 | 
						|
    # - because all comments relating to a given show have the same id number
 | 
						|
    #   this identifies all comment groups (comments relating to a given show)
 | 
						|
    #   with members in the period
 | 
						|
    # - the main query then selects all comments in these groups and joins all
 | 
						|
    #   the required tables to them to get the other data we need; it computes
 | 
						|
    #   the boolean 'in_range' to indicate whether a comment within a group
 | 
						|
    #   should be displayed; it returns *all* comments so we can number them
 | 
						|
    # - there might have been a way to generate a comment sequence number
 | 
						|
    #   within the SQL but it was decided not to and to do this in the script
 | 
						|
    #
 | 
						|
    # Update 2015-03-26: the sort order of the final result used
 | 
						|
    # 'comment_identifier_id' which didn't seem to work reliably. Changed this
 | 
						|
    # to 'episode' which seems to work fine. The use of the former was only
 | 
						|
    # arrived at by guesswork. The guess was obviously wrong.
 | 
						|
    #
 | 
						|
    # Update 2015-06-04: the test for whether a comment is approved was
 | 
						|
    # failing because it was in the wrong place. Also, the sub-select seemed
 | 
						|
    # wrong, and running through EXPLAIN EXTENDED showed some flaws.
 | 
						|
    # Redesigned this and the whole query to be more in keeping with the
 | 
						|
    # algorithm sketched out above. This seems to have solved the problems.
 | 
						|
    #
 | 
						|
    # Update 2015-08-17: Still not right; somehow the 'in_range' check got
 | 
						|
    # mangled and the process of counting comments where a recent one was made
 | 
						|
    # to an old show was messed up. Dropped the two queries with a UNION and
 | 
						|
    # went back to the check for comment_identifier_id in a sub-query. This
 | 
						|
    # time the sub-query is cleverer and returns identifiers where the
 | 
						|
    # `comment_timestamp` is in the target month *and* the episode date is in
 | 
						|
    # the target month or before the start of the target month. This works for
 | 
						|
    # July 2015 (where there's a comment made about an August show) and August
 | 
						|
    # 2015 (where there's a comment made about a July show).
 | 
						|
    #
 | 
						|
    # Update 2023-05-02: Issues with comments received on the day of
 | 
						|
    # recording. This matter has been addresed earlier in the year but not all
 | 
						|
    # that well. The way this is all handled is not very clever so tried
 | 
						|
    # adjusting the main comment query to return such comments since we had
 | 
						|
    # one on 2023-04-29 which wasn't in the report.
 | 
						|
    #
 | 
						|
    #{{{
 | 
						|
    #
 | 
						|
    # Graveyard of old failed queries...
 | 
						|
    # ==================================
 | 
						|
#    $sth1 = $dbh->prepare(
 | 
						|
#        q{
 | 
						|
#        SELECT
 | 
						|
#            cc.comment_identifier_id,
 | 
						|
#            eps.id AS episode,
 | 
						|
#--          ci.identifier_url,
 | 
						|
#            substr(ci.identifier_url,1,locate('/eps.php?id=',ci.identifier_url)+15)
 | 
						|
#                AS identifier_url,
 | 
						|
#            eps.title,
 | 
						|
#            eps.date,
 | 
						|
#            ho.host,
 | 
						|
#            ho.hostid,
 | 
						|
#            from_unixtime(cc.comment_timestamp,'%Y-%m-%dT%TZ') AS timestamp,
 | 
						|
#            replace(cc.comment_author_name,'\\\\','') AS comment_author_name,
 | 
						|
#            replace(cc.comment_title,'\\\\','') AS comment_title,
 | 
						|
#            replace(cc.comment_text,'\\\\','') AS comment_text,
 | 
						|
#            cc.comment_timestamp,
 | 
						|
#            (CASE WHEN (cc.comment_timestamp >= unix_timestamp(?)
 | 
						|
#                    AND cc.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day)) THEN
 | 
						|
#                1 ELSE 0 END) AS in_range
 | 
						|
#        FROM c5t_comment cc
 | 
						|
#        JOIN c5t_identifier ci ON ci.identifier_id = cc.comment_identifier_id
 | 
						|
#        JOIN eps ON eps.id =
 | 
						|
#            substr(ci.identifier_url,locate('/eps.php?id=',ci.identifier_url)+12)
 | 
						|
#        JOIN hosts ho ON eps.hostid = ho.hostid
 | 
						|
#        WHERE cc.comment_status = 0
 | 
						|
#        AND cc.comment_identifier_id IN (
 | 
						|
#            SELECT DISTINCT cc2.comment_identifier_id FROM c5t_comment cc2
 | 
						|
#                WHERE (
 | 
						|
#                    cc2.comment_timestamp >= unix_timestamp(?)
 | 
						|
#                        AND cc2.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day)
 | 
						|
#                    )
 | 
						|
#            )
 | 
						|
#        AND (eps.date < (last_day(?) + interval 1 day))
 | 
						|
#        ORDER BY eps.id ASC, comment_timestamp ASC
 | 
						|
#        }
 | 
						|
#    );
 | 
						|
#
 | 
						|
# This one is nuts...
 | 
						|
#    $sth1 = $dbh->prepare( q{
 | 
						|
#        (
 | 
						|
#            SELECT
 | 
						|
#                cc.comment_identifier_id,
 | 
						|
#                eps.id AS episode,
 | 
						|
#                substr(ci.identifier_url,1,locate('/eps.php?id=',ci.identifier_url)+15)
 | 
						|
#                    AS identifier_url,
 | 
						|
#                eps.title,
 | 
						|
#                eps.date,
 | 
						|
#                ho.host,
 | 
						|
#                ho.hostid,
 | 
						|
#                from_unixtime(cc.comment_timestamp,'%Y-%m-%dT%TZ') AS timestamp,
 | 
						|
#                replace(cc.comment_author_name,'\\\\','') AS comment_author_name,
 | 
						|
#                replace(cc.comment_title,'\\\\','') AS comment_title,
 | 
						|
#                replace(cc.comment_text,'\\\\','') AS comment_text,
 | 
						|
#                cc.comment_timestamp,
 | 
						|
#                (CASE WHEN (cc.comment_timestamp >= unix_timestamp(?)
 | 
						|
#                        AND cc.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day)) THEN
 | 
						|
#                    1 ELSE 0 END) AS in_range
 | 
						|
#            FROM c5t_comment cc
 | 
						|
#            JOIN c5t_identifier ci ON ci.identifier_id = cc.comment_identifier_id
 | 
						|
#            JOIN eps ON eps.id =
 | 
						|
#                substr(ci.identifier_url,locate('/eps.php?id=',ci.identifier_url)+12,4)
 | 
						|
#            JOIN hosts ho ON eps.hostid = ho.hostid
 | 
						|
#            WHERE eps.date >= ? AND eps.date < (last_day(?) + interval 1 day)
 | 
						|
#        )
 | 
						|
#        UNION
 | 
						|
#        (
 | 
						|
#            SELECT
 | 
						|
#                cc.comment_identifier_id,
 | 
						|
#                eps.id AS episode,
 | 
						|
#                substr(ci.identifier_url,1,locate('/eps.php?id=',ci.identifier_url)+15)
 | 
						|
#                    AS identifier_url,
 | 
						|
#                eps.title,
 | 
						|
#                eps.date,
 | 
						|
#                ho.host,
 | 
						|
#                ho.hostid,
 | 
						|
#                from_unixtime(cc.comment_timestamp,'%Y-%m-%dT%TZ') AS timestamp,
 | 
						|
#                replace(cc.comment_author_name,'\\\\','') AS comment_author_name,
 | 
						|
#                replace(cc.comment_title,'\\\\','') AS comment_title,
 | 
						|
#                replace(cc.comment_text,'\\\\','') AS comment_text,
 | 
						|
#                cc.comment_timestamp,
 | 
						|
#                (CASE WHEN (cc.comment_timestamp >= unix_timestamp(?)
 | 
						|
#                        AND cc.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day)) THEN
 | 
						|
#                    1 ELSE 0 END) AS in_range
 | 
						|
#            FROM c5t_comment cc
 | 
						|
#            JOIN c5t_identifier ci ON ci.identifier_id = cc.comment_identifier_id
 | 
						|
#            JOIN eps ON eps.id =
 | 
						|
#                substr(ci.identifier_url,locate('/eps.php?id=',ci.identifier_url)+12,4)
 | 
						|
#            JOIN hosts ho ON eps.hostid = ho.hostid
 | 
						|
#            WHERE (cc.comment_timestamp >= unix_timestamp(?)
 | 
						|
#                AND cc.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day))
 | 
						|
#            AND eps.date < (last_day(?) + interval 1 day)
 | 
						|
#        )
 | 
						|
#        ORDER BY episode ASC, comment_timestamp ASC
 | 
						|
#    });
 | 
						|
#
 | 
						|
# This one worked fine - after much messing around admittedly:
 | 
						|
#    $sth1 = $dbh->prepare( q{
 | 
						|
#        SELECT
 | 
						|
#            cc.comment_identifier_id,
 | 
						|
#            eps.id AS episode,
 | 
						|
#            substr(ci.identifier_url,1,locate('/eps.php?id=',ci.identifier_url)+15)
 | 
						|
#                AS identifier_url,
 | 
						|
#            eps.title,
 | 
						|
#            eps.date,
 | 
						|
#            ho.host,
 | 
						|
#            ho.hostid,
 | 
						|
#            from_unixtime(cc.comment_timestamp,'%Y-%m-%dT%TZ') AS timestamp,
 | 
						|
#            replace(cc.comment_author_name,'\\\\','') AS comment_author_name,
 | 
						|
#            replace(cc.comment_title,'\\\\','') AS comment_title,
 | 
						|
#            replace(cc.comment_text,'\\\\','') AS comment_text,
 | 
						|
#            cc.comment_timestamp,
 | 
						|
#            (CASE WHEN
 | 
						|
#                (
 | 
						|
#                    (cc.comment_timestamp >= unix_timestamp(?)
 | 
						|
#                        AND cc.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day))
 | 
						|
#                    AND eps.date < (last_day(?) + interval 1 day)
 | 
						|
#                    OR (cc.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day)
 | 
						|
#                        AND (eps.date >= ? AND eps.date < (last_day(?) + interval 1 day)))
 | 
						|
#                )
 | 
						|
#                THEN 1 ELSE 0 END) AS in_range
 | 
						|
#        FROM c5t_comment cc
 | 
						|
#        JOIN c5t_identifier ci ON ci.identifier_id = cc.comment_identifier_id
 | 
						|
#        JOIN eps ON eps.id =
 | 
						|
#            substr(ci.identifier_url,locate('/eps.php?id=',ci.identifier_url)+12,4)
 | 
						|
#        JOIN hosts ho ON eps.hostid = ho.hostid
 | 
						|
#        WHERE cc.comment_status = 0
 | 
						|
#        AND cc.comment_identifier_id IN
 | 
						|
#        (
 | 
						|
#            SELECT DISTINCT
 | 
						|
#                cc.comment_identifier_id
 | 
						|
#            FROM c5t_comment cc
 | 
						|
#            JOIN c5t_identifier ci ON ci.identifier_id = cc.comment_identifier_id
 | 
						|
#            JOIN eps ON eps.id =
 | 
						|
#                substr(ci.identifier_url,locate('/eps.php?id=',ci.identifier_url)+12,4)
 | 
						|
#            WHERE
 | 
						|
#                (eps.date >= ? AND eps.date < (last_day(?) + interval 1 day))
 | 
						|
#                OR (
 | 
						|
#                    ( (cc.comment_timestamp >= unix_timestamp(?)
 | 
						|
#                        AND cc.comment_timestamp < unix_timestamp(last_day(?) + interval 1 day)) )
 | 
						|
#                    AND (eps.date >= ? AND eps.date < (last_day(?) + interval 1 day)
 | 
						|
#                        OR (eps.date < ?))
 | 
						|
#                )
 | 
						|
#        )
 | 
						|
#        ORDER BY episode ASC, comment_timestamp ASC
 | 
						|
#    });
 | 
						|
    #}}}
 | 
						|
 | 
						|
    #-------------------------------------------------------------------------------
 | 
						|
    # Main comment query
 | 
						|
    #-------------------------------------------------------------------------------
 | 
						|
    $sth1 = $dbh->prepare( q{
 | 
						|
        SELECT
 | 
						|
            eps.id AS episode,
 | 
						|
            concat('https://hackerpublicradio.org/eps/hpr',
 | 
						|
                lpad(eps_id,4,0),'/index.html') AS identifier_url,
 | 
						|
            eps.title,
 | 
						|
            eps.summary,
 | 
						|
            eps.date,
 | 
						|
            ho.host,
 | 
						|
            ho.hostid,
 | 
						|
            date_format(co.comment_timestamp,'%Y-%m-%dT%TZ') AS timestamp,
 | 
						|
            co.comment_author_name,
 | 
						|
            co.comment_title,
 | 
						|
            co.comment_text,
 | 
						|
            unix_timestamp(co.comment_timestamp) AS comment_timestamp_ut,
 | 
						|
            unix_timestamp(
 | 
						|
                cast(concat(date(co.comment_timestamp),' ',?) AS DATETIME)
 | 
						|
            ) AS comment_released_ut,
 | 
						|
            (CASE WHEN
 | 
						|
                (
 | 
						|
                    (co.comment_timestamp >= ?
 | 
						|
                        AND co.comment_timestamp < (last_day(?) + interval 1 day))
 | 
						|
                    OR (co.comment_timestamp >= ?)
 | 
						|
                    AND eps.date < (last_day(?) + interval 1 day)
 | 
						|
                    OR (co.comment_timestamp < (last_day(?) + interval 1 day)
 | 
						|
                        AND (eps.date >= ? AND eps.date < (last_day(?) + interval 1 day)))
 | 
						|
                )
 | 
						|
                THEN 1 ELSE 0 END) AS in_range
 | 
						|
        FROM comments co
 | 
						|
        JOIN eps ON eps.id = co.eps_id
 | 
						|
        JOIN hosts ho ON eps.hostid = ho.hostid
 | 
						|
        WHERE eps.id IN
 | 
						|
        (
 | 
						|
            SELECT DISTINCT
 | 
						|
                eps.id
 | 
						|
            FROM eps
 | 
						|
            JOIN comments co ON (eps.id = co.eps_id)
 | 
						|
            WHERE
 | 
						|
                (eps.date >= ? AND eps.date < (last_day(?) + interval 1 day))
 | 
						|
                OR (co.comment_timestamp >= ?)
 | 
						|
                OR (
 | 
						|
                    ( (co.comment_timestamp >= ?
 | 
						|
                            AND co.comment_timestamp < (last_day(?) + interval 1 day)) )
 | 
						|
                    AND (eps.date >= ? AND eps.date < (last_day(?) + interval 1 day)
 | 
						|
                        OR (eps.date < ?))
 | 
						|
                )
 | 
						|
        )
 | 
						|
        ORDER BY episode ASC, comment_timestamp ASC
 | 
						|
    });
 | 
						|
 | 
						|
    $sth1->execute(
 | 
						|
        $dt_lr->hms,
 | 
						|
        ( $dt->ymd ) x 2,
 | 
						|
        $dt_lr->ymd,
 | 
						|
        ( $dt->ymd ) x 6,
 | 
						|
        $dt_lr->ymd,
 | 
						|
        ( $dt->ymd ) x 5
 | 
						|
    );
 | 
						|
    if ( $dbh->err ) {
 | 
						|
        carp $dbh->errstr;
 | 
						|
    }
 | 
						|
 | 
						|
    #
 | 
						|
    # Grab the data as an arrayref of hashrefs
 | 
						|
    #
 | 
						|
    $comments = $sth1->fetchall_arrayref( {} );
 | 
						|
 | 
						|
    #-------------------------------------------------------------------------------
 | 
						|
    # Post-process the results of the query
 | 
						|
    #-------------------------------------------------------------------------------
 | 
						|
    # The comment structure needs some further work because it contains all
 | 
						|
    # comments for a given show but no indexes we can use (because the query
 | 
						|
    # didn't generate any - this is a lot easier in PostgreSQL (and possibly
 | 
						|
    # SQLite)). We also need to know how many comments we have within the
 | 
						|
    # target period, which is usually a smaller number than the number of
 | 
						|
    # comments we got back. The query has marked the ones we want to count
 | 
						|
    # (and display) using 'in_range'.
 | 
						|
    #
 | 
						|
    my ( $ep, $lastep, $index ) = ( 0, 0, 0 );
 | 
						|
    $comment_count = $past_count = $ignore_count = 0;
 | 
						|
    for my $row (@$comments) {
 | 
						|
        #
 | 
						|
        # Give each comment an index whether in_range or not. The indexes
 | 
						|
        # start from 1 when the episode number changes otherwise they
 | 
						|
        # increment.
 | 
						|
        #
 | 
						|
        $ep = $row->{episode};
 | 
						|
        if ( $ep == $lastep ) {
 | 
						|
            $index++;
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            $index = 1;
 | 
						|
        }
 | 
						|
        #print "$ep $index ", $row->{in_range}, "\n";
 | 
						|
        _debug(
 | 
						|
            $DEBUG > 2,
 | 
						|
            sprintf(
 | 
						|
                "Index generation: episode=%s, index=%s, in_range=%s",
 | 
						|
                $ep, $index, $row->{in_range}
 | 
						|
            )
 | 
						|
        );
 | 
						|
        $lastep = $ep;
 | 
						|
 | 
						|
        #
 | 
						|
        # Save the index for the template
 | 
						|
        #
 | 
						|
        $row->{index} = $index;
 | 
						|
 | 
						|
        #
 | 
						|
        # Count the valid ones so the template doesn't have to to give a total
 | 
						|
        # for this month
 | 
						|
        #
 | 
						|
        $comment_count++ if $row->{in_range};
 | 
						|
 | 
						|
        #
 | 
						|
        # Make the comment text cleaner by removing carriage returns (not sure
 | 
						|
        # why they are there in the first place)
 | 
						|
        #
 | 
						|
        $row->{comment_text} =~ s/\r+//g;
 | 
						|
 | 
						|
        #
 | 
						|
        # Dates from the database look like '2016-08-01', we compare them to
 | 
						|
        # the month we're processing to see if the comments are attached to
 | 
						|
        # old shows.
 | 
						|
        # FIXME: test for 'in_range' before setting 'past'
 | 
						|
        #
 | 
						|
        if ( validate_date($row->{date},$dt->ymd) ) {
 | 
						|
            $row->{past} = 0;
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            $row->{past} = 1;
 | 
						|
            $past_count++ if $row->{in_range};
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    #
 | 
						|
    # Now prune all of the comments which are not in_range to give the
 | 
						|
    # template an easier job
 | 
						|
    #
 | 
						|
    @$comments = grep { $_->{in_range} } @$comments;
 | 
						|
 | 
						|
    # Explanation of the resulting structure {{{
 | 
						|
    #
 | 
						|
    # Restructure the comments into two hashes keyed by episode number where
 | 
						|
    # each comment to that episode is stored in sequence in an array. The two
 | 
						|
    # hashes %past and %current hold comments for shows in the past and for
 | 
						|
    # the current month. These can be dealt with separately in the template,
 | 
						|
    # making the logic therein somewhat simpler and clearer.
 | 
						|
    #
 | 
						|
    # The hash (of arrays of hashes) can be visualised thus:
 | 
						|
    # %past = {
 | 
						|
    #          '2457' => [
 | 
						|
    #                      {
 | 
						|
    #                        'past' => 1,
 | 
						|
    #                        'hostid' => 111,
 | 
						|
    #                        'identifier_url' => 'https://hackerpublicradio.org/eps.php?id=2457',
 | 
						|
    #                        'comment_timestamp_ut' => 1556192523,
 | 
						|
    #                        'date' => '2018-01-02',
 | 
						|
    #                        'comment_text' => [snip],
 | 
						|
    #                        'comment_title' => 'aren\'t you forgetting a hub?',
 | 
						|
    #                        'timestamp' => '2019-04-25T11:42:03Z',
 | 
						|
    #                        'in_range' => 1,
 | 
						|
    #                        'index' => 1,
 | 
						|
    #                        'host' => 'knightwise',
 | 
						|
    #                        'title' => 'Getting ready for my new Macbook Pro',
 | 
						|
    #                        'comment_author_name' => 'Bart',
 | 
						|
    #                        'episode' => 2457,
 | 
						|
    #                        'ignore' => 0
 | 
						|
    #                      }
 | 
						|
    #                    ]
 | 
						|
    #          }
 | 
						|
    #}}}
 | 
						|
 | 
						|
    #
 | 
						|
    # Also, as we go, mark and count the comments in the %past hash which were
 | 
						|
    # likely to have been read in the last show, so by this means simplify
 | 
						|
    # what the template has to do.
 | 
						|
    #
 | 
						|
    for my $row (@$comments) {
 | 
						|
        my $ep = $row->{episode};
 | 
						|
        if ($row->{past}) {
 | 
						|
            if ( $show_comments && $mark_comments ) {
 | 
						|
               #if ( $row->{comment_timestamp_ut} <= $dt_lr->epoch
 | 
						|
                if ( $row->{comment_released_ut} <= $dt_lr->epoch
 | 
						|
                    && substr( $row->{date}, 0, 7 ) eq
 | 
						|
                    substr( $dt_lm->ymd, 0, 7 ) )
 | 
						|
                {
 | 
						|
                    $row->{ignore} = 1;
 | 
						|
                    $ignore_count++;
 | 
						|
                }
 | 
						|
                else {
 | 
						|
                    $row->{ignore} = 0;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            else {
 | 
						|
                $row->{ignore} = 0;
 | 
						|
            }
 | 
						|
 | 
						|
            if (exists($past{$ep})) {
 | 
						|
                push(@{$past{$ep}},$row);
 | 
						|
            }
 | 
						|
            else {
 | 
						|
                $past{$ep} = [$row];
 | 
						|
            }
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            if (exists($current{$ep})) {
 | 
						|
                push(@{$current{$ep}},$row);
 | 
						|
            }
 | 
						|
            else {
 | 
						|
                $current{$ep} = [$row];
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    _debug ($DEBUG > 2,
 | 
						|
        '%past: ' . Dumper(\%past),
 | 
						|
        '%current: ' . Dumper(\%current)
 | 
						|
    );
 | 
						|
 | 
						|
    #-------------------------------------------------------------------------------
 | 
						|
    # Make another data structure of missed coments *if* $t_days is true
 | 
						|
    #-------------------------------------------------------------------------------
 | 
						|
    # If $t_days is true then there might be comments from the previous month
 | 
						|
    # that weren't covered in the recording. So we add them to the notes just
 | 
						|
    # for the recording. Trouble is, if they exist they aren't in the comments
 | 
						|
    # we have gathered, so we'll have to go and search for them with this
 | 
						|
    # special query.
 | 
						|
    #
 | 
						|
    if ($t_days) {
 | 
						|
        $sth1 = $dbh->prepare( q{
 | 
						|
            SELECT
 | 
						|
                eps.id AS episode,
 | 
						|
                concat('https://hackerpublicradio.org/eps/hpr',
 | 
						|
                    lpad(eps_id,4,0),'/index.html') AS identifier_url,
 | 
						|
                eps.title,
 | 
						|
                eps.summary,
 | 
						|
                eps.date,
 | 
						|
                ho.host,
 | 
						|
                ho.hostid,
 | 
						|
                date_format(co.comment_timestamp,'%Y-%m-%dT%TZ') AS timestamp,
 | 
						|
                co.comment_author_name,
 | 
						|
                co.comment_title,
 | 
						|
                co.comment_text,
 | 
						|
                unix_timestamp(co.comment_timestamp) AS comment_timestamp_ut
 | 
						|
            FROM comments co
 | 
						|
            JOIN eps ON eps.id = co.eps_id
 | 
						|
            JOIN hosts ho ON eps.hostid = ho.hostid
 | 
						|
            WHERE
 | 
						|
                co.comment_timestamp >= ?
 | 
						|
                AND co.comment_timestamp < (last_day(?)+ interval 1 day)
 | 
						|
            ORDER BY episode ASC, comment_timestamp ASC
 | 
						|
        });
 | 
						|
 | 
						|
        #
 | 
						|
        # Need the date and time of the last recording and the start of the
 | 
						|
        # last month we reviewed to perform the query.
 | 
						|
        #
 | 
						|
        # $sth1->execute( $dt_lr->datetime . 'Z', $dt_lm->ymd );
 | 
						|
        $sth1->execute( $dt_lr->ymd, $dt_lm->ymd );
 | 
						|
        if ( $dbh->err ) {
 | 
						|
            carp $dbh->errstr;
 | 
						|
        }
 | 
						|
 | 
						|
        #
 | 
						|
        # Grab the data as an arrayref of hashrefs
 | 
						|
        #
 | 
						|
        $missed_comments = $sth1->fetchall_arrayref( {} );
 | 
						|
        $missed_count = (
 | 
						|
            defined($missed_comments)
 | 
						|
            ? scalar(@$missed_comments)
 | 
						|
            : 0
 | 
						|
        );
 | 
						|
 | 
						|
        #
 | 
						|
        # Make the comment text cleaner by removing carriage returns (not sure
 | 
						|
        # why they are there in the first place)
 | 
						|
        #
 | 
						|
        for my $ch (@$missed_comments) {
 | 
						|
            $ch->{comment_text} =~ s/\r+//g;
 | 
						|
        }
 | 
						|
 | 
						|
        _debug ($DEBUG > 2,
 | 
						|
            '@missed_comments: ' . Dumper($missed_comments)
 | 
						|
        );
 | 
						|
 | 
						|
        #
 | 
						|
        # After a change in design around 2023-05-02 there may be duplicates
 | 
						|
        # in the %past hash and the @$missed_comments array. We need to hide
 | 
						|
        # the former for now. They will not be hidden when $t_days is false
 | 
						|
        # because we're not bothered about missed comments!
 | 
						|
        #
 | 
						|
        if ( $past_count > 0 ) {
 | 
						|
            my @missed_episodes = map { $_->{episode} } @$missed_comments;
 | 
						|
 | 
						|
            _debug( $DEBUG > 2,
 | 
						|
                '@missed_episodes: ' . Dumper( \@missed_episodes ) );
 | 
						|
 | 
						|
            delete( @past{@missed_episodes} );
 | 
						|
 | 
						|
            my $old_pc = $past_count;
 | 
						|
            $past_count = scalar( keys(%past) );
 | 
						|
            $comment_count -= ($old_pc - $past_count);
 | 
						|
 | 
						|
            _debug(
 | 
						|
                $DEBUG > 2,
 | 
						|
                '%past (edited): ' . Dumper( \%past ),
 | 
						|
                "\$past_count: $past_count",
 | 
						|
                "\$comment_count: $comment_count"
 | 
						|
            );
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
# Fill and print the template
 | 
						|
#-------------------------------------------------------------------------------
 | 
						|
my $tt = Template->new(
 | 
						|
    {   ABSOLUTE     => 1,
 | 
						|
        ENCODING     => 'utf8',
 | 
						|
        INCLUDE_PATH => $basedir,
 | 
						|
        FILTERS      => {
 | 
						|
            # For HTML->ASCII in comments_only.tpl, decode HTML entities
 | 
						|
            'decode_entities' => \&my_decode_entities,
 | 
						|
        },
 | 
						|
    }
 | 
						|
);
 | 
						|
 | 
						|
my $vars = {
 | 
						|
    review_month    => $dt->month_name,
 | 
						|
    review_year     => $dt->year,
 | 
						|
    hosts           => $hosts,
 | 
						|
    shows           => $shows,
 | 
						|
    comment_count   => $comment_count,
 | 
						|
    past_count      => $past_count,
 | 
						|
    ignore_count    => $ignore_count,
 | 
						|
    missed_count    => $missed_count,
 | 
						|
    missed_comments => $missed_comments,
 | 
						|
    comments        => $comments,                                # legacy
 | 
						|
    past            => \%past,
 | 
						|
    current         => \%current,
 | 
						|
    skip_comments   => ( $show_comments ? 0 : 1 ),
 | 
						|
    mark_comments   => $mark_comments,
 | 
						|
    ctext           => $ctext,
 | 
						|
    last_recording  => ( $mark_comments ? $dt_lr->epoch : 0 ),
 | 
						|
    last_month      => (
 | 
						|
        $mark_comments ? sprintf( "%d-%02d", $dt_lm->year, $dt_lm->month ) : 0
 | 
						|
    ),
 | 
						|
    includefile     => $mailnotes,
 | 
						|
    aob             => defined($aobfile),
 | 
						|
    aobfile         => $aobfile,
 | 
						|
};
 | 
						|
 | 
						|
#print Dumper($vars),"\n";
 | 
						|
 | 
						|
my $document;
 | 
						|
$tt->process( $template, $vars, \$document, { binmode => ':utf8' } )
 | 
						|
    || die $tt->error(), "\n";
 | 
						|
print $outfh $document;
 | 
						|
 | 
						|
#
 | 
						|
# The episode number tests passed earlier on, so add the notes to the database
 | 
						|
# if so requested
 | 
						|
#
 | 
						|
if ($episode) {
 | 
						|
    emit( $silent,
 | 
						|
        "Writing shownotes to the database for episode $episode\n" );
 | 
						|
    $sth1 = $dbh->prepare(q{UPDATE eps SET notes = ? WHERE id = ?});
 | 
						|
    $sth1->execute( $document, $episode );
 | 
						|
    if ( $dbh->err ) {
 | 
						|
        carp $dbh->errstr;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
$dbh->disconnect;
 | 
						|
 | 
						|
exit;
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: parse_to_dc
 | 
						|
#      PURPOSE: Parse a textual date (and optional time) to a Date::Calc
 | 
						|
#               datetime
 | 
						|
#   PARAMETERS: $datetime       Datetime as a string
 | 
						|
#               $deftime        Arrayref default time (as a Date::Calc array
 | 
						|
#                               or undef)
 | 
						|
#      RETURNS: A Date::Calc date (and possibly time) as a list
 | 
						|
#  DESCRIPTION: The $datetime argument is parsed ewith Date::Parse. The year
 | 
						|
#               and month need to be adjusted. If a default time has been
 | 
						|
#               supplied then the parsed time is checked and the default time
 | 
						|
#               used if nothing was found, otherwise the parsed time is used
 | 
						|
#               and a full 6-component time returned.
 | 
						|
#               If the default time us undefined this means we don't care
 | 
						|
#               about the time and so we just return the parsed date as
 | 
						|
#               a 3-component list.
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub parse_to_dc {
 | 
						|
    my ( $datetime, $deftime ) = @_;
 | 
						|
 | 
						|
    # What strptime returns:
 | 
						|
    #  0   1   2   3    4      5     6
 | 
						|
    # ($ss,$mm,$hh,$day,$month,$year,$zone)
 | 
						|
    #
 | 
						|
    my @parsed = strptime($datetime);
 | 
						|
    die "Invalid DATE or DATETIME '$datetime'\n"
 | 
						|
        unless ( defined( $parsed[3] )
 | 
						|
        && defined( $parsed[4] )
 | 
						|
        && defined( $parsed[5] ) );
 | 
						|
 | 
						|
    $parsed[5] += 1900;
 | 
						|
    $parsed[4] += 1;
 | 
						|
 | 
						|
    if ( defined($deftime) ) {
 | 
						|
        #
 | 
						|
        # If no time was supplied add a default one
 | 
						|
        #
 | 
						|
        unless ( defined( $parsed[2] )
 | 
						|
            && defined( $parsed[1] )
 | 
						|
            && defined( $parsed[0] ) )
 | 
						|
        {
 | 
						|
            @parsed[ 2, 1, 0 ] = @$deftime;
 | 
						|
        }
 | 
						|
 | 
						|
        #
 | 
						|
        # Return a list
 | 
						|
        #
 | 
						|
        return ( @parsed[ 5, 4, 3, 2, 1, 0 ] );
 | 
						|
    }
 | 
						|
    else {
 | 
						|
        return ( @parsed[ 5, 4, 3 ] );
 | 
						|
    }
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: dc_to_dt
 | 
						|
#      PURPOSE: Converts a Date::Calc datetime into a DateTime equivalent
 | 
						|
#   PARAMETERS: $refdt          Reference to an array holding a Date::Calc
 | 
						|
#                               date and time
 | 
						|
#      RETURNS: Returns a DateTime object converted from the input
 | 
						|
#  DESCRIPTION: 
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub dc_to_dt {
 | 
						|
    my ($refdt) = @_;
 | 
						|
 | 
						|
    #
 | 
						|
    # Check we got a 6-element array
 | 
						|
    #
 | 
						|
    if (scalar(@$refdt) != 6) {
 | 
						|
        print  "Invalid Date::Calc date and time (@$refdt)\n";
 | 
						|
        die "Aborted\n";
 | 
						|
    }
 | 
						|
 | 
						|
    #
 | 
						|
    # Convert to DateTime to get access to formatting stuff
 | 
						|
    #
 | 
						|
    my ( %dtargs, $dt );
 | 
						|
    @dtargs{ 'year', 'month', 'day', 'hour', 'minute', 'second', 'time_zone' }
 | 
						|
        = ( @$refdt, 'UTC' );
 | 
						|
    $dt = DateTime->new(%dtargs);
 | 
						|
 | 
						|
    #
 | 
						|
    # Return the date as a DateTime object
 | 
						|
    #
 | 
						|
    return $dt;
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: find_last_recording
 | 
						|
#      PURPOSE: Finds the recording date of the Community News episode
 | 
						|
#               relating to the target month
 | 
						|
#   PARAMETERS: $refdate        Reference to an array holding a Date::Calc
 | 
						|
#                               date - the first of the selected month
 | 
						|
#               $reftime        Reference to an array holding a Date::Calc
 | 
						|
#                               time (UTC)
 | 
						|
#      RETURNS: A Date::Calc object containing the date and time of the last
 | 
						|
#               Community News recording in the UTC time zone
 | 
						|
#  DESCRIPTION: We want to find the date of the last Community News recording
 | 
						|
#               to determine whether a given comment preceded it. The scenario
 | 
						|
#               is that we're using these notes while making such a recording
 | 
						|
#               and want to know if comments occured before the last one. If
 | 
						|
#               they did we should have read them during that show and don't
 | 
						|
#               need to do so now. We want to pass the date generated here to
 | 
						|
#               the template so it can compare it with comment dates. To
 | 
						|
#               complete the story, the template will mark such comments, but
 | 
						|
#               we'll turn of the marking before the notes are released - they
 | 
						|
#               are just for use by the people recording the episode.
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub find_last_recording {
 | 
						|
    my ($refdate, $reftime) = @_;
 | 
						|
 | 
						|
    my $monday = 1;                    # Day of week number 1-7, Monday-Sunday
 | 
						|
 | 
						|
    #
 | 
						|
    # Using the given date (the requested month), ensure it's the first day of
 | 
						|
    # the month
 | 
						|
    #
 | 
						|
    my @lastmonth = @$refdate;
 | 
						|
    $lastmonth[2] = 1;
 | 
						|
 | 
						|
    #
 | 
						|
    # Work out the recording date in the target month
 | 
						|
    #
 | 
						|
    @lastmonth = Nth_Weekday_of_Month_Year( @$refdate[ 0, 1 ], $monday, 1 );
 | 
						|
    @lastmonth = Add_Delta_Days( @lastmonth, -2 );
 | 
						|
 | 
						|
    #
 | 
						|
    # Return the date as a DateTime object
 | 
						|
    #
 | 
						|
    return (@lastmonth,@$reftime);
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: find_last_month
 | 
						|
#      PURPOSE: Finds the previous month for working out marks
 | 
						|
#   PARAMETERS: $refdate        Reference to an array holding a Date::Calc
 | 
						|
#                               date - the first of the selected month
 | 
						|
#      RETURNS: A DateTime object containing the first day of the last month.
 | 
						|
#  DESCRIPTION: We need the details of the last month because if we're marking
 | 
						|
#               comments we have already read out in the previous Community
 | 
						|
#               News show, but don't want to mark comments to shows before
 | 
						|
#               that month, even if the comments fell within the target month.
 | 
						|
#               This is a complex edge condition that wasn't appreciated in the
 | 
						|
#               first implementation.
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub find_last_month {
 | 
						|
    my ($refdate) = @_;
 | 
						|
 | 
						|
    my $monday = 1;                    # Day of week number 1-7, Monday-Sunday
 | 
						|
    my @starttime = ( 00, 00, 00 );    # UTC
 | 
						|
 | 
						|
    #
 | 
						|
    # Using the given date (the requested month for the notes), ensure it's
 | 
						|
    # the first day of the month
 | 
						|
    #
 | 
						|
    my @lastmonth = @$refdate;
 | 
						|
    $lastmonth[2] = 1;
 | 
						|
 | 
						|
    #
 | 
						|
    # Subtract one day to enter the previous month and force the first day of
 | 
						|
    # the resulting month
 | 
						|
    #
 | 
						|
    @lastmonth = Add_Delta_Days( @lastmonth, -1 );
 | 
						|
    $lastmonth[2] = 1;
 | 
						|
 | 
						|
    #
 | 
						|
    # Return the date as a DateTime object
 | 
						|
    #
 | 
						|
    return (@lastmonth,@starttime);
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: trailing_days
 | 
						|
#      PURPOSE: Determines if the last month had 'trailing' days - those after
 | 
						|
#               the recording date - during which unread comments could have
 | 
						|
#               been posted.
 | 
						|
#   PARAMETERS: $dc_lr          reference to an array containing a Date::Calc
 | 
						|
#                               date of the last recording
 | 
						|
#               $dc_lm          reference to an array containing a Date::Calc
 | 
						|
#                               date of the first day of last month
 | 
						|
#      RETURNS: A true/false result - 1 if there were trailing days,
 | 
						|
#               0 otherwise
 | 
						|
#  DESCRIPTION: If the recording of a Community News show was during the month
 | 
						|
#               being reviewed (e.g. March 2019; recording on 2019-03-30,
 | 
						|
#               release on 2019-04-01) then a comment could have been made
 | 
						|
#               after (or during!) the recording and probably would not have
 | 
						|
#               been read during the show. We want to spot such a case and
 | 
						|
#               highlight it in the *next* recording!
 | 
						|
#               Yes, I know this is obsessive, but it needs a solution!!!
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub trailing_days {
 | 
						|
    my ( $dc_lr, $dc_lm ) = @_;
 | 
						|
 | 
						|
    my $offset;
 | 
						|
 | 
						|
    #
 | 
						|
    # Are the last month and the recording month the same?
 | 
						|
    #
 | 
						|
    if ( $dc_lm->[1] eq $dc_lr->[1] ) {
 | 
						|
        #
 | 
						|
        # Compute the offset as Delta_Days((First day of last month + days in
 | 
						|
        # month), recording date). A positive offset (not sure if we'd get
 | 
						|
        # a negative one) means there's some of the month still to go.
 | 
						|
        #
 | 
						|
        $offset
 | 
						|
            = Delta_Days( @$dc_lr[0..2],
 | 
						|
            Add_Delta_Days( @$dc_lm[0..2], Days_in_Month( @$dc_lm[ 0, 1 ] ) ) );
 | 
						|
        return 1 if $offset gt 0;
 | 
						|
    }
 | 
						|
 | 
						|
    return 0;
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: validate_date
 | 
						|
#      PURPOSE: Checks the date found in the database ($date) is before the
 | 
						|
#               reference date ($refdate)
 | 
						|
#   PARAMETERS: $date           Textual date from the database
 | 
						|
#               $refdate        Optional textual date to compare with. If
 | 
						|
#                               omitted then we use 'Today'
 | 
						|
#      RETURNS: True (1) if the $date is later than the $refdate, false (0)
 | 
						|
#               otherwise
 | 
						|
#  DESCRIPTION: We need to check that the script is not being used to change
 | 
						|
#               the notes for a Community News show in the past. This is
 | 
						|
#               because sometimes the generated notes are edited after they
 | 
						|
#               have been created to add other elements, and we do not want to
 | 
						|
#               accidentally destroy such changes. We just compute the
 | 
						|
#               difference between today and the date of the target episode.
 | 
						|
#               If the difference in days is greater than 0 then it's OK.
 | 
						|
#               We also want to be able to use this routine to check whether
 | 
						|
#               comments relate to old shows or this month's.
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub validate_date {
 | 
						|
    my ($date, $refdate) = @_;
 | 
						|
 | 
						|
    my @refdate;
 | 
						|
 | 
						|
    unless (defined($date)) {
 | 
						|
        warn "check_date: invalid argument\n";
 | 
						|
        return 0;
 | 
						|
    }
 | 
						|
 | 
						|
    my @date = ($date =~ /^(\d{4})-(\d{2})-(\d{2})$/);
 | 
						|
    if (defined($refdate)) {
 | 
						|
        @refdate = ($refdate =~ /^(\d{4})-(\d{2})-(\d{2})$/);
 | 
						|
    }
 | 
						|
    else {
 | 
						|
        @refdate = Today();
 | 
						|
    }
 | 
						|
 | 
						|
    return (Delta_Days(@refdate,@date) >= 0);
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: my_decode_entities
 | 
						|
#      PURPOSE: Call 'HTML::Entities::decode_entities' as a filter in a template
 | 
						|
#   PARAMETERS: $text           The text string to process
 | 
						|
#      RETURNS: The text string with all HTML entities decoded to Unicode
 | 
						|
#  DESCRIPTION: This is a local filter to be called in a template. The name
 | 
						|
#               it's called by is defined in the 'FILTERS' definition in the
 | 
						|
#               call to Template::new. Maybe this function is redundant and
 | 
						|
#               the real 'decode_entities' could have been called directly,
 | 
						|
#               but this is slightly easier to understand (I think).
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub my_decode_entities {
 | 
						|
    my $text = shift;
 | 
						|
 | 
						|
    decode_entities($text);
 | 
						|
    return $text;
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: emit
 | 
						|
#      PURPOSE: Print text on STDERR unless silent mode has been selected
 | 
						|
#   PARAMETERS: - Boolean indicating whether to be silent or not
 | 
						|
#               - list of arguments to 'print'
 | 
						|
#      RETURNS: Nothing
 | 
						|
#  DESCRIPTION: This is a wrapper around 'print' to determine whether to send
 | 
						|
#               a message to STDERR depending on a boolean. We need this to be
 | 
						|
#               able to make the script silent when the -silent option is
 | 
						|
#               selected
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub emit {
 | 
						|
    unless (shift) {
 | 
						|
        print STDERR @_;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: _debug
 | 
						|
#      PURPOSE: Prints debug reports
 | 
						|
#   PARAMETERS: $active         Boolean: 1 for print, 0 for no print
 | 
						|
#               @messages       List of messages to print
 | 
						|
#      RETURNS: Nothing
 | 
						|
#  DESCRIPTION: Outputs messages to STDERR if $active is true. For each
 | 
						|
#               message it removes any trailing newline and then adds one in
 | 
						|
#               the 'print' so the caller doesn't have to bother. Prepends
 | 
						|
#               each message with 'D> ' to show it's a debug message.
 | 
						|
#       THROWS: No exceptions
 | 
						|
#     COMMENTS: None
 | 
						|
#     SEE ALSO: N/A
 | 
						|
#===============================================================================
 | 
						|
sub _debug {
 | 
						|
    my ( $active, @messages ) = @_;
 | 
						|
 | 
						|
    return unless $active;
 | 
						|
    foreach my $msg (@messages) {
 | 
						|
        chomp($msg);
 | 
						|
        print STDERR "D> $msg\n";
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
#===  FUNCTION  ================================================================
 | 
						|
#         NAME: Options
 | 
						|
#      PURPOSE: Processes command-line options
 | 
						|
#   PARAMETERS: $optref     Hash reference to hold the options
 | 
						|
#      RETURNS: Undef
 | 
						|
#  DESCRIPTION:
 | 
						|
#       THROWS: no exceptions
 | 
						|
#     COMMENTS: none
 | 
						|
#     SEE ALSO: n/a
 | 
						|
#===============================================================================
 | 
						|
sub Options {
 | 
						|
    my ($optref) = @_;
 | 
						|
 | 
						|
    my @options = (
 | 
						|
        "help",                   "documentation|man",
 | 
						|
        "debug=i",                "from=s",
 | 
						|
        "out=s",                  "template=s",
 | 
						|
        "comments!",              "markcomments|mc!",
 | 
						|
        "ctext!",                 "lastrecording|lr=s",
 | 
						|
        "silent!",                "episode=s",
 | 
						|
        "overwrite!",             "mailnotes:s",
 | 
						|
        "anyotherbusiness|aob=s", "config=s",
 | 
						|
        "interlock=s"
 | 
						|
    );
 | 
						|
 | 
						|
    if ( !GetOptions( $optref, @options ) ) {
 | 
						|
        pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
 | 
						|
    }
 | 
						|
 | 
						|
    return;
 | 
						|
}
 | 
						|
 | 
						|
__END__
 | 
						|
 | 
						|
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 | 
						|
#  Application Documentation
 | 
						|
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 | 
						|
#{{{
 | 
						|
 | 
						|
=head1 NAME
 | 
						|
 | 
						|
make_shownotes - Make HTML show notes for the Hacker Public Radio Community News show
 | 
						|
 | 
						|
=head1 VERSION
 | 
						|
 | 
						|
This documentation refers to B<make_shownotes> version 0.2.2
 | 
						|
 | 
						|
 | 
						|
=head1 USAGE
 | 
						|
 | 
						|
    make_shownotes [-help] [-documentation] [-from=DATE] [-[no]comments]
 | 
						|
        [-[no]markcomments] [-[no]ctext] [-lastrecording=DATETIME]
 | 
						|
        [-[no]silent] [-out=FILE] [-episode=[N|auto]] [-[no]overwrite]
 | 
						|
        [-mailnotes[=FILE]] [-anyotherbusiness=FILE] [-template=FILE]
 | 
						|
        [-config=FILE] [-interlock=PASSWORD]
 | 
						|
 | 
						|
=head1 OPTIONS
 | 
						|
 | 
						|
=over 8
 | 
						|
 | 
						|
=item B<-help>
 | 
						|
 | 
						|
Displays a brief help message describing the usage of the program, and then exits.
 | 
						|
 | 
						|
=item B<-documentation>
 | 
						|
 | 
						|
Displays the entirety of the documentation (using a pager), and then exits. To
 | 
						|
generate a PDF version use:
 | 
						|
 | 
						|
    pod2pdf make_shownotes --out=make_shownotes.pdf
 | 
						|
 | 
						|
=item B<-from=DATE>
 | 
						|
 | 
						|
This option is used to indicate the month for which the shownotes are to be
 | 
						|
generated. The script is able to parse a variety of date formats, but it is
 | 
						|
recommended that ISO8601 YYYY-MM-DD format be used (for example 2014-06-30).
 | 
						|
 | 
						|
The day part of the date must be present but is ignored and only the month and
 | 
						|
year parts are used.
 | 
						|
 | 
						|
If this option is omitted the current month is used.
 | 
						|
 | 
						|
=item B<-[no]comments>
 | 
						|
 | 
						|
This option controls whether the comments pertaining to the selected month are
 | 
						|
included in the output. If the option is omitted then no comments are included
 | 
						|
(B<-nocomments>).
 | 
						|
 | 
						|
=item B<-[no]markcomments> or B<-[no]mc>
 | 
						|
 | 
						|
This option controls whether certain comments are marked in the HTML. The
 | 
						|
default is B<-nomarkcomments>. The option can be abbreviated to B<-mc> and
 | 
						|
B<-nomc>.
 | 
						|
 | 
						|
The scenario is that we want to use the notes the script generates while
 | 
						|
making a Community News recording and we also want them to be the show notes
 | 
						|
in the database once the show has been released.
 | 
						|
 | 
						|
Certain comments relating to shows earlier than this month were already
 | 
						|
discussed last month, because they were made before that show was recorded. We
 | 
						|
don't want to read them again during this show, so a means of marking them is
 | 
						|
needed.
 | 
						|
 | 
						|
The script determines the date of the last recording (or it can be specified
 | 
						|
with the B<-lastrecording=DATETIME> option, or its abbreviation
 | 
						|
B<-lr=DATETIME>) and passes it to the template. The template can then compare
 | 
						|
this date with the dates of the relevant comments and take action to highlight
 | 
						|
those we don't want to re-read. It is up to the template to do what is
 | 
						|
necessary to highlight them.
 | 
						|
 | 
						|
The idea is that we will turn off the marking before the notes are released
 | 
						|
- they are just for use by the people recording the episode.
 | 
						|
 | 
						|
Another action is taken during the processing of comments when this option is
 | 
						|
on. On some months of the year the recording is made during the month itself
 | 
						|
because the first Monday of the next month is in the first few days of that
 | 
						|
month. For example, in March 2019 the date of recording is the 30th, and the
 | 
						|
show is released on April 1st. Between the recording and the release of the
 | 
						|
show there is time during which more comments could be submitted.
 | 
						|
 | 
						|
Such comments should be in the notes for March (and these can be regenerated
 | 
						|
to make sure this is so) but they will not have been read on the March
 | 
						|
recording. The B<make_shownotes> script detects this problem and, if
 | 
						|
B<-markcomments> is set (and comments enabled) will show a list of any
 | 
						|
eligible comments in a red highlighted box. This is so that the volunteers
 | 
						|
recording the show can ensure they read comments that have slipped through
 | 
						|
this loophole. The display shows the entire comment including the contents,
 | 
						|
but disappears when the notes are refreshed with B<-nomarkcomments> (the
 | 
						|
default).
 | 
						|
 | 
						|
In this mode the preamble warning about comments to be ignored used to be
 | 
						|
included, but now it is skipped if there are no such comments. This means one
 | 
						|
switch can serve two purposes.
 | 
						|
 | 
						|
=item B<-lastrecording=DATETIME> or B<-lr=DATETIME>
 | 
						|
 | 
						|
As mentioned for B<-markcomments>, the date of the last recording can be
 | 
						|
computed in the assumption that it's on the Saturday before the first Monday
 | 
						|
of the month at 15:00. However, on rare occasions it may be necessary to
 | 
						|
record on an earlier date and time, which cannot be computed. This value can
 | 
						|
be defined with this option.
 | 
						|
 | 
						|
The format can be an ISO 8601 date followed by a 24-hour time, such as
 | 
						|
'2020-01-25 15:00'. If the time is omitted it defaults to 15:00.
 | 
						|
 | 
						|
=item B<-[no]ctext>
 | 
						|
 | 
						|
This option controls whether the comment text itself is listed with comments.
 | 
						|
This is controlled by the template, but the current default template only
 | 
						|
shows the text in the B<Past shows> section of the output. The default
 | 
						|
state is B<-noctext> in which the comment texts are not written.
 | 
						|
 | 
						|
=item B<-[no]silent>
 | 
						|
 | 
						|
This option controls whether the script reports details of its progress
 | 
						|
to STDERR. If the option is omitted the report is generated (B<-nosilent>).
 | 
						|
 | 
						|
The script reports: the month it is working on, the name of the output file
 | 
						|
(if appropriate) and details of the process of writing notes to the database
 | 
						|
(if the B<-episode=[N|auto]> option is selected).
 | 
						|
 | 
						|
=item B<-mailnotes[=FILE]>
 | 
						|
 | 
						|
If desired, the show notes may include a section about recent discussions on
 | 
						|
the HPR mailing list. Obviously, this text will change every month, so this
 | 
						|
option provides a way in which an external file can be included in the show
 | 
						|
notes.
 | 
						|
 | 
						|
The filename may be omitted which is a way in which a B<BLOCK> directive can
 | 
						|
be placed in the template and used rather than the file. The B<BLOCK> must be
 | 
						|
named B<default_mail> because this is the name the script uses in this
 | 
						|
circumstance. See B<shownote_template8.tpl> for an example of its use.
 | 
						|
 | 
						|
The template must contain instructions to include the file or block. The file
 | 
						|
name is stored in a variable 'B<includefile>' in the template. Directives of
 | 
						|
the following form may be added to achive this:
 | 
						|
 | 
						|
    [%- IF includefile.defined %]
 | 
						|
    Constant header, preamble, etc
 | 
						|
    [%- INCLUDE $includefile %]
 | 
						|
    Other constant text or tags
 | 
						|
    [%- END %]
 | 
						|
 | 
						|
The first directive causes the whole block to be ignored if there is no
 | 
						|
B<-mailnotes> option. The use of the B<INCLUDE> directive means that the
 | 
						|
included file may contain Template directives itself if desired.
 | 
						|
 | 
						|
See existing templates for examples of how this is done.
 | 
						|
 | 
						|
=item B<-anyotherbusiness=FILE> or B<-aob=FILE>
 | 
						|
 | 
						|
If desired the shownotes may contain an 'Any other business' section. This is
 | 
						|
implemented in a template thus:
 | 
						|
 | 
						|
    [% IF aob == 1 -%]
 | 
						|
    <h2>Any other business</h2>
 | 
						|
    [% INCLUDE $aobfile -%]
 | 
						|
    [%- END %]
 | 
						|
 | 
						|
The template variable B<aob> is set to 1 if a (valid) file has been provided,
 | 
						|
and the name of the file is in B<aobfile>.
 | 
						|
 | 
						|
The included file is assumed to be HTML.
 | 
						|
 | 
						|
=item B<-out=FILE>
 | 
						|
 | 
						|
This option defines an output file to receive the show notes. If the option is
 | 
						|
omitted the notes are written to STDOUT, allowing them to be redirected if
 | 
						|
required.
 | 
						|
 | 
						|
The output file name may contain the characters 'B<%s>'. This denotes the point
 | 
						|
at which the year and month in the format B<YYYY-MM> are inserted. For example
 | 
						|
if the script is being run for July 2014 the option:
 | 
						|
 | 
						|
    -out=shownotes_%s.html
 | 
						|
 | 
						|
will cause the generation of the file:
 | 
						|
 | 
						|
    shownotes_2014-07.html
 | 
						|
 | 
						|
=item B<-episode=[N|auto]>
 | 
						|
 | 
						|
This option provides a means of specifying an episode number in the database to
 | 
						|
receive the show notes.
 | 
						|
 | 
						|
It either takes a number, or it takes the string 'B<auto>' which makes the
 | 
						|
script find the correct show number.
 | 
						|
 | 
						|
First the episode number has to have been reserved in the database. This is
 | 
						|
done by running the script 'B<reserve_cnews>'. This makes a reservation with
 | 
						|
the title "HPR Community News for <monthname> <year>". Normally Community News
 | 
						|
slots are reserved several months in advance.
 | 
						|
 | 
						|
Close to the date of the Community News show recording this script can be run
 | 
						|
to write show notes to the database. For example:
 | 
						|
 | 
						|
    ./make_shownotes -from=1-Dec-2014 -out=/dev/null \
 | 
						|
        -comm -tem=shownote_template5.tpl -ep=auto
 | 
						|
 | 
						|
This will search for the episode with the title "HPR Community News for
 | 
						|
December 2014" and will add notes if the field is empty. Note that it is
 | 
						|
necessary to direct the output to /dev/null since the script needs to write
 | 
						|
a copy of the notes to STDOUT or to a file. In this case we request comments
 | 
						|
to be added to the notes, and we use the template file
 | 
						|
B<shownote_template5.tpl> which generates an HTML snippet suitable for the
 | 
						|
database.
 | 
						|
 | 
						|
The writing of the notes to the database will fail if the field is not empty.
 | 
						|
See the B<-overwrite> option for how to force the notes to be written.
 | 
						|
 | 
						|
If the B<-episode=[N|auto]> option is omitted no attempt is made to write to
 | 
						|
the database.
 | 
						|
 | 
						|
=item B<-[no]overwrite>
 | 
						|
 | 
						|
This option is only relevant in conjunction with the B<-episode=[N|auto]>
 | 
						|
option. If B<-overwrite> is chosen the new show notes will overwrite any notes
 | 
						|
already in the database. If B<-nooverwrite> is selected, or the option is
 | 
						|
omitted, no over writing will take place - it will only be possible to write
 | 
						|
notes to the database if the field is empty.
 | 
						|
 | 
						|
=item B<-template=FILE>
 | 
						|
 | 
						|
This option defines the template used to generate the notes. The template is
 | 
						|
written using the B<Template> toolkit language.
 | 
						|
 | 
						|
If the option is omitted then the script uses the file
 | 
						|
B<shownote_template.tpl> in the same directory as the script. If this file
 | 
						|
does not exist then the script will exit with an error message.
 | 
						|
 | 
						|
For convenience B<shownote_template.tpl> is a soft link which points to the
 | 
						|
file which is the current default. This allows the development of versions
 | 
						|
without changing the usual way this script is run.
 | 
						|
 | 
						|
=item B<-config=FILE>
 | 
						|
 | 
						|
This option allows an alternative configuration file to be used. This file
 | 
						|
defines the location of the database, its port, its name and the username and
 | 
						|
password to be used to access it. This feature was added to allow the script
 | 
						|
to access alternative databases or the live database over an SSH tunnel.
 | 
						|
 | 
						|
See the CONFIGURATION AND ENVIRONMENT section below for the file format.
 | 
						|
 | 
						|
If the option is omitted the default file is used: B<.hpr_db.cfg>
 | 
						|
 | 
						|
=item B<-interlock=PASSWORD>
 | 
						|
 | 
						|
This option was added to handle the case where the notes for a Community News
 | 
						|
episode have been posted after the show was recorded, but, since the recording
 | 
						|
date was not the last day of the month further comments could be added after
 | 
						|
upload. Logically these comments belong in the previous month's shownotes, so
 | 
						|
we'd need to add them retrospecively.
 | 
						|
 | 
						|
Up until the addition of this option the script would not allow the
 | 
						|
regeneration of the notes. This option requires a password to enable the
 | 
						|
feature, but the password is in a constant inside the script. This means that
 | 
						|
it's difficult to run in this mode by accident, but not particulary difficult
 | 
						|
if it's really needed.
 | 
						|
 | 
						|
Take care not to run in this mode if the notes have been edited after they
 | 
						|
were generated!
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=head1 DESCRIPTION
 | 
						|
 | 
						|
=head2 Overview
 | 
						|
 | 
						|
This script generates notes for the next Hacker Public Radio I<Community News>
 | 
						|
show. It does this by collecting various details of activity from the HPR
 | 
						|
database and passing them to a template. The default template is called
 | 
						|
B<shownote_template.tpl> and this generates HTML, but any suitable textual
 | 
						|
format could be generated if required, by using a different template.
 | 
						|
 | 
						|
=head2 Data Gathering
 | 
						|
 | 
						|
Four types of information are collected by the script:
 | 
						|
 | 
						|
=over 4
 | 
						|
 | 
						|
=item -
 | 
						|
 | 
						|
Details of new hosts who have released new shows in the selected month
 | 
						|
 | 
						|
=item -
 | 
						|
 | 
						|
Details of shows which have been released in the selected month
 | 
						|
 | 
						|
=item -
 | 
						|
 | 
						|
Details of topics on the mailing list in the past month can be included. This
 | 
						|
is only done if the B<-mailnotes=FILE> option is used. This option must
 | 
						|
reference a file of HTML, which may contain Template directives if required.
 | 
						|
 | 
						|
=item -
 | 
						|
 | 
						|
Comments which have been submitted to the HPR website in the selected month.
 | 
						|
These need to be related to shows in the current period or in the past.
 | 
						|
Comments made about shows which have not yet been released (but are visible on
 | 
						|
the website) are not included even though they are made in the current month.
 | 
						|
 | 
						|
Comments are only gathered if the B<-comments> option is selected.
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=head2 Report Generation
 | 
						|
 | 
						|
The four components listed above are formatted in the following way by the
 | 
						|
default template.
 | 
						|
 | 
						|
=over 4
 | 
						|
 | 
						|
=item B<New Hosts>
 | 
						|
 | 
						|
These are formatted as a list of links to the hostid with the host's name.
 | 
						|
 | 
						|
=item B<Shows>
 | 
						|
 | 
						|
These are formatted into an HTML table containing the show number, title and
 | 
						|
host name. The show title is a link to the show page on the HPR website. The
 | 
						|
host name is a link to the host page on the website.
 | 
						|
 | 
						|
=item B<Mailing list discussions>
 | 
						|
 | 
						|
If there have been significant topics on the mailing list in the month in
 | 
						|
question then these can be summarised in this section. This is done by
 | 
						|
preparing an external HTML file and referring to it with the
 | 
						|
B<-mailnotes=FILE> option. If this is done then the file is included into the
 | 
						|
template.
 | 
						|
 | 
						|
See the explanation of the B<-mailnotes> option for more details.
 | 
						|
 | 
						|
=item B<Comments>
 | 
						|
 | 
						|
These are formatted with <article> tags separated by horizontal lines.
 | 
						|
A <header> shows the author name and title and a <footer> displays a link to
 | 
						|
the show and the show's host and the show title is also included. The body of
 | 
						|
the article contains the comment text with line breaks.
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=head2 Variable, Field and Hash names
 | 
						|
 | 
						|
If you wish to write your own template refer to the following lists for the
 | 
						|
names of items. Also refer to the default template B<shownote_template.tpl>
 | 
						|
for the techniques used there. (Note that B<shownote_template.tpl> is a link
 | 
						|
to the current default template, such as B<shownote_template8.tpl>).
 | 
						|
 | 
						|
The hash and field names available to the template are as follows
 | 
						|
 | 
						|
=over 4
 | 
						|
 | 
						|
=item B<Global variables>
 | 
						|
 | 
						|
 Variable Name          Details
 | 
						|
 -------------          -------
 | 
						|
 review_month           The month name of the report date
 | 
						|
 review_year            The year of the report date
 | 
						|
 comment_count          The number of comments in total
 | 
						|
 past_count             The number of comments on old shows
 | 
						|
 skip_comments          Set when -comments is omitted
 | 
						|
 mark_comments          Set when -markcomments is used
 | 
						|
 ctext                  Set when the comment bodies in the 'Past shows'
 | 
						|
                        section are to be shown
 | 
						|
 last_recording         The date the last recording was made
 | 
						|
                         (computed if -markcomments is selected) in
 | 
						|
                         Unixtime format
 | 
						|
 last_month             The month prior to the month for which the notes are
 | 
						|
                        being generated (computed if -markcomments is
 | 
						|
                        selected) in 'YYYY-MM' format
 | 
						|
 | 
						|
=item B<New Hosts>
 | 
						|
 | 
						|
The name of the hash in the template is B<hosts>. The hash might be empty if
 | 
						|
there are no new hosts in the month. See the default template for how to
 | 
						|
handle this.
 | 
						|
 | 
						|
 Field Name     Details
 | 
						|
 ----------     -------
 | 
						|
 host           Name of host
 | 
						|
 hostid         Host id number
 | 
						|
 | 
						|
=item B<Show Details>
 | 
						|
 | 
						|
The name of the hash in the template is B<shows>. Note that there are more
 | 
						|
fields available than are used in the default template. Note also that certain
 | 
						|
field names are aliases to avoid clashes (e.g. eps_hostid and ho_hostid).
 | 
						|
 | 
						|
 Field Name     Details
 | 
						|
 ----------     -------
 | 
						|
 eps_id         Episode number
 | 
						|
 date           Episode date
 | 
						|
 title          Episode title
 | 
						|
 length         Episode duration
 | 
						|
 summary        Episode summary
 | 
						|
 notes          Episode show notes
 | 
						|
 eps_hostid     The numerical host id from the 'eps' table
 | 
						|
 series         The series number from the 'eps' table
 | 
						|
 explicit       The explicit marker for the show
 | 
						|
 eps_license    The license for the show
 | 
						|
 tags           The show's tags as a comma-delimited string
 | 
						|
 version        ?Obsolete?
 | 
						|
 eps_valid      The valid value from the 'eps' table
 | 
						|
 ho_hostid      The host id number form the 'hosts' table
 | 
						|
 ho_host        The host name
 | 
						|
 email          The hosts's email address (true address - caution)
 | 
						|
 profile        The host's profile
 | 
						|
 ho_license     The default license for the host
 | 
						|
 ho_valid       The valid value from the 'hosts' table
 | 
						|
 | 
						|
=item B<Mailing List Notes>
 | 
						|
 | 
						|
The variable B<includefile> contains the path to the file (which may only be
 | 
						|
located in the same directory as the script).
 | 
						|
 | 
						|
=item B<Comment Details>
 | 
						|
 | 
						|
Two hashes are created for comments. The hash named B<past> contains comments
 | 
						|
to shows before the current month, and B<current> contains comments to this
 | 
						|
month's shows. Note that these hashes are only populated if the B<-comments>
 | 
						|
option is provided. Both hashes have the same structure.
 | 
						|
 | 
						|
 Field Name             Details
 | 
						|
 ----------             -------
 | 
						|
 episode                Episode number
 | 
						|
 identifier_url         Full show URL
 | 
						|
 title                  Episode title
 | 
						|
 date                   Episode date
 | 
						|
 host                   Host name
 | 
						|
 hostid                 Host id number
 | 
						|
 timestamp              Comment timestamp in ISO8601 format
 | 
						|
 comment_author_name    Name of the commenter
 | 
						|
 comment_title          Title of comment
 | 
						|
 comment_text           Text of the comment
 | 
						|
 comment_timestamp_ut   Comment timestamp in Unixtime format
 | 
						|
 in_range               Boolean (0/1) denoting whether the comment was made
 | 
						|
                        in the target month
 | 
						|
 index                  The numerical index of the comment for a given show
 | 
						|
 | 
						|
The purpose of the B<in_range> value is to denote whether a comment was made
 | 
						|
in the target month. This is used in the script to split the comments into the
 | 
						|
B<past> and B<current> hashes. It is therefore of little use in the template,
 | 
						|
but is retained in case it might be useful. The B<index> value can be used in
 | 
						|
the template to refer to the comment, make linking URLs etc. It is generated
 | 
						|
by the script (unfortunately it couldn't be done in the SQL).
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=head2 Filters
 | 
						|
 | 
						|
A filter called B<decode_entities> is available to the template. The reason
 | 
						|
for creating this was when the HTML of a comment is being listed as text
 | 
						|
(Unicode actually). Since comment text is stored in the database as HTML with
 | 
						|
entities when appropriate this is needed to prevent the plain text showing
 | 
						|
I<&> and the like verbatim. It is currently used in B<comments_only.tpl>.
 | 
						|
 | 
						|
=head1 DIAGNOSTICS
 | 
						|
 | 
						|
=over 4
 | 
						|
 | 
						|
=item B<Unable to find configuration file ...>
 | 
						|
 | 
						|
The nominated configuration file in B<-config=FILE> (or the default file)
 | 
						|
cannot be found.
 | 
						|
 | 
						|
=item B<Episode number must be greater than zero>
 | 
						|
 | 
						|
The B<-episode=N> option must use a positive number.
 | 
						|
 | 
						|
=item B<Episode must be a number or 'auto'>
 | 
						|
 | 
						|
The B<-episode=> option must be followed by a number or the word 'auto'
 | 
						|
 | 
						|
=item B<Error: Unable to find includefile ...>
 | 
						|
 | 
						|
The include file referred to in the error message is missing.
 | 
						|
 | 
						|
=item B<Error: Unable to find template ...>
 | 
						|
 | 
						|
The template file referred to in the error message is missing.
 | 
						|
 | 
						|
=item B<Invalid -from=DATE option '...'>
 | 
						|
 | 
						|
The date provided through the B<-from=DATE> option is invalid. Use an ISO8601
 | 
						|
date in the format YYYY-MM-DD.
 | 
						|
 | 
						|
=item B<Unable to open ... for writing: ...>
 | 
						|
 | 
						|
The file specified in the B<-out=FILE> option cannot be written to. This may
 | 
						|
be because you do not have permission to write to the file or directory.
 | 
						|
Further information about why this failed should be included in the message.
 | 
						|
 | 
						|
=item B<Unable to initialise for writing: ...>
 | 
						|
 | 
						|
The script was unable to open STDOUT for writing the report. Further
 | 
						|
information about why this failed should be included in the message.
 | 
						|
 | 
						|
=item B<Error: wrong show selected>
 | 
						|
 | 
						|
The B<-episode=N> option has been selected and the script is checking the
 | 
						|
numbered show but has not found a Community News title.
 | 
						|
 | 
						|
=item B<Error: show ... has a date in the past>
 | 
						|
 | 
						|
The B<-episode=> option has been selected and a Community News show entry has
 | 
						|
been found in the database. However, this entry is for today's show or is in
 | 
						|
the past, which is not permitted. It is possible to override this restriction
 | 
						|
by using the B<-interlock=PASSWORD> option. See the relevant documentation for
 | 
						|
details.
 | 
						|
 | 
						|
=item B<Error: show ... already has notes>
 | 
						|
 | 
						|
The B<-episode=> option has been selected and a Community News show entry has
 | 
						|
been found in the database. However, this entry already has notes associated
 | 
						|
with it and the B<-overwrite> option has not been specified.
 | 
						|
 | 
						|
=item B<Error: episode ... does not exist in the database>
 | 
						|
 | 
						|
The B<-episode=N> option has been selected but the script cannot find this
 | 
						|
episode number in the database.
 | 
						|
 | 
						|
=item B<Error: Unable to find an episode for this month's notes>
 | 
						|
 | 
						|
The B<-episode=auto> option has been selected but the script cannot find the
 | 
						|
episode for the month being processed.
 | 
						|
 | 
						|
Possible reasons for this are that the show has not been reserved in the
 | 
						|
database or that the title is not as expected. Use B<reserve_cnews> to reserve
 | 
						|
the slot. The title should be "HPR Community News for <monthname> <year>".
 | 
						|
 | 
						|
=back
 | 
						|
 | 
						|
=head1 CONFIGURATION AND ENVIRONMENT
 | 
						|
 | 
						|
The script obtains the credentials it requires to open the HPR database from
 | 
						|
a configuration file. The name of the file it expects is B<.hpr_db.cfg> in the
 | 
						|
directory holding the script. To change this will require changing the script.
 | 
						|
 | 
						|
The configuration file format is as follows:
 | 
						|
 | 
						|
 <database>
 | 
						|
     host = 127.0.0.1
 | 
						|
     port = PORT
 | 
						|
     name = DATABASE
 | 
						|
     user = USERNAME
 | 
						|
     password = PASSWORD
 | 
						|
 </database>
 | 
						|
 | 
						|
=head1 DEPENDENCIES
 | 
						|
 | 
						|
 Carp
 | 
						|
 Config::General
 | 
						|
 Date::Calc
 | 
						|
 Date::Parse
 | 
						|
 DateTime
 | 
						|
 DateTime::Duration
 | 
						|
 DBI
 | 
						|
 Getopt::Long
 | 
						|
 Pod::Usage
 | 
						|
 Template
 | 
						|
 Template::Filters
 | 
						|
 | 
						|
=head1 BUGS AND LIMITATIONS
 | 
						|
 | 
						|
There are no known bugs in this module.
 | 
						|
Please report problems to Dave Morriss (Dave.Morriss@gmail.com)
 | 
						|
Patches are welcome.
 | 
						|
 | 
						|
=head1 AUTHOR
 | 
						|
 | 
						|
Dave Morriss  (Dave.Morriss@gmail.com)
 | 
						|
 | 
						|
 | 
						|
=head1 LICENCE AND COPYRIGHT
 | 
						|
 | 
						|
Copyright (c) 2014-2019 Dave Morriss (Dave.Morriss@gmail.com). All rights reserved.
 | 
						|
 | 
						|
This module is free software; you can redistribute it and/or
 | 
						|
modify it under the same terms as Perl itself. See perldoc perlartistic.
 | 
						|
 | 
						|
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.
 | 
						|
 | 
						|
=cut
 | 
						|
 | 
						|
#}}}
 | 
						|
 | 
						|
# [zo to open fold, zc to close or za to toggle]
 | 
						|
 | 
						|
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
 |