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
 |