#!/usr/bin/env perl #=============================================================================== # # FILE: edit_series # # USAGE: ./edit_series [-help] [-debug=N] [-[no]update] [-config=FILE] # [-series_id=id] [-[no]regex] [-[no]description] [-[no]private] # [-[no]valid] series_name # # DESCRIPTION: A simple editor for the HPR miniseries table # # OPTIONS: --- # REQUIREMENTS: --- # BUGS: --- # NOTES: Based on edit_host # AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com # VERSION: 0.0.3 # CREATED: 2020-06-21 17:58:19 # REVISION: 2021-06-23 22:11:13 # #=============================================================================== use 5.010; use strict; use warnings; use utf8; use Getopt::Long; use Pod::Usage; use Config::General; use File::Temp; use File::Slurper qw{ read_text }; use SQL::Abstract; use DBI; use Data::Dumper; # # Version number (manually incremented) # our $VERSION = '0.0.3'; # # Script and directory names # ( my $PROG = $0 ) =~ s|.*/||mx; ( my $DIR = $0 ) =~ s|/?[^/]*$||mx; $DIR = '.' unless $DIR; #------------------------------------------------------------------------------- # Declarations #------------------------------------------------------------------------------- # # Constants and other declarations # my $basedir = "$ENV{HOME}/HPR/Database"; my $configfile = "$basedir/.hpr_db.cfg"; # # Declarations # my ( $dbh, $sth1, $h1, $rc ); my ( $series_name, %changes ); # # Enable Unicode mode # binmode STDOUT, ":encoding(UTF-8)"; binmode STDERR, ":encoding(UTF-8)"; # # Options and arguments # my %options; Options( \%options ); # # Default help # pod2usage( -msg => "Version $VERSION\n", -exitval => 1 ) if ( $options{'help'} ); # # Collect options # my $DEBUG = ( $options{'debug'} ? $options{'debug'} : 0 ); my $cfgfile = ( defined( $options{config} ) ? $options{config} : $configfile ); my $updatedb = ( defined( $options{'updatedb'} ) ? $options{'updatedb'} : 0 ); my $regexp = ( defined( $options{'regexp'} ) ? $options{'regexp'} : 0 ); my $description = $options{'description'}; my $private = $options{'private'}; my $valid = $options{'valid'}; # # There must be at least one field to change. We check for the definition here # because the values returned may be zero or one or the variable may be # undefined. # die "Select one of -[no]description, -[no]private and -[no]valid\n" unless ( defined($description) || defined($private) || defined($valid) ); # # Was a series id provided (through an option)? # my $series_id = $options{'series_id'}; # # Deal with the two routes: one via the unique series id, and the other vai the # less unique series name # unless ($series_id) { # # Get the arg # $series_name = shift; pod2usage( -msg => "Specify the series name\n", -exitval => 1 ) unless $series_name; } # # Sanity check # die "Unable to find $cfgfile\n" unless ( -e $cfgfile ); # # Load configuration data # my $conf = new Config::General( -ConfigFile => $cfgfile, -InterPolateVars => 1, -ExtendedAccess => 1 ); my %config = $conf->getall(); # # 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}; # 2022-04-12 The MariaDB driver was there one minute and then it wasn't! # #$dbh = DBI->connect( "DBI:MariaDB:database=$dbname;host=$dbhost;port=$dbport", # $dbuser, $dbpwd, { AutoCommit => 1 } ) # or die $DBI::errstr; $dbh = DBI->connect( "dbi:mysql:database=$dbname;host=$dbhost;port=$dbport", $dbuser, $dbpwd, { AutoCommit => 1 } ) or die $DBI::errstr; # # Enable client-side UTF8 # $dbh->{mysql_enable_utf8} = 1; # # Prepare to read the database either for the series_id or the exact or # approximate name # if ($series_id) { # # Simple series_id query # $sth1 = $dbh->prepare(q{SELECT * FROM miniseries WHERE id = ?}); $sth1->execute($series_id); if ( $dbh->err ) { warn $dbh->errstr; } } else { # # Series name query # if ($regexp) { # # Regexp match requested. Count how many matches there are # $sth1 = $dbh->prepare( q{SELECT count(*) AS count FROM miniseries WHERE name REGEXP ?}); $sth1->execute($series_name); if ( $dbh->err ) { warn $dbh->errstr; } if ( $h1 = $sth1->fetchrow_hashref ) { my $matches = $h1->{count}; if ($matches > 1) { die "Too many matches to regex '$series_name' ($matches)\n"; } elsif ($matches == 0) { die "No matches to regex '$series_name'\n"; } } else { die "Unable to find series matching regex '$series_name'\n"; } $sth1 = $dbh->prepare(q{SELECT * FROM miniseries WHERE name REGEXP ?}); } else { $sth1 = $dbh->prepare(q{SELECT * FROM miniseries WHERE name = ?}); } # # Execute the query # $sth1->execute($series_name); if ( $dbh->err ) { warn $dbh->errstr; } } # # Did we find it? # if ( $h1 = $sth1->fetchrow_hashref ) { # # Found. Save the series_id to simplify the update if we don't already have # it # $series_id //= $h1->{id}; # # Report on the series details # printf "Series details\n" . "Id: %s\n" . "Name: %s\n" . "Description: %s\n" . "Private: %s\n" . "Image: '%s'\n" . "Valid: %s\n", $h1->{id}, $h1->{name}, ( length( $h1->{description} ) > 80 ? substr( $h1->{description}, 0, 80 ) . '...' : $h1->{description} ), $h1->{private}, $h1->{image}, $h1->{valid}; # # So what needs changing? # #<<< [perltidy messes up the following] if ($description) { $changes{description} = check_field( 'description', scalar( run_editor( $h1->{description}, ['+set paste'] ) ), 1500, qr{(\n)} ); } if (defined($private)) { $changes{private} = $private if ($h1->{private} ne $private); } if (defined($valid)) { $changes{valid} = $valid if ($h1->{valid} ne $valid); } #>>> print Dumper( \%changes ), "\n" if ( $DEBUG > 2 ); # # Are we updating the database then? # if ($updatedb) { # # Was anything edited? # if (%changes) { # # Did the edits do anything? If not delete the element from the # hash since there's no point writing it to the database # for my $key ( keys(%changes) ) { if ( $changes{$key} eq $h1->{$key} ) { print "No change made to $key, ignored\n"; delete( $changes{$key} ); } } print Dumper( \%changes ), "\n" if ( $DEBUG > 2 ); } # # If there's anything left apply the changes # if (%changes) { # # Go into transaction mode here so we can fail safely # $rc = $dbh->begin_work or die $dbh->errstr; my $sql = SQL::Abstract->new; my %where = ( id => $series_id ); my ( $stmt, @bind ) = $sql->update( 'miniseries', \%changes, \%where ); #print "$stmt\n"; #print join( ",", map {"'$_'"} @bind ), "\n"; my $sth = $dbh->prepare($stmt); my $rv = $sth->execute(@bind); # # Respond to any error by rolling back # if ( $dbh->err ) { warn $dbh->errstr; eval { $dbh->rollback }; $rv = 0; } else { $dbh->commit; } $rv = 0 if ( $rv eq '0E0' ); # # Update the log file # if ($rv) { print "Updated database\n"; print "Changed fields: ", join( ", ", sort( keys(%changes) ) ), "\n"; } else { print "Series not updated due to error\n"; } } else { print "There was nothing to do\n"; } } else { print "Database not updated\n"; } } else { if ($series_id) { print "Unable to find series number $series_id\n"; } else { print "Unable to find series name $series_name\n"; } } exit; #=== FUNCTION ================================================================ # NAME: run_editor # PURPOSE: Run an interactive vim editor on a string # PARAMETERS: $string String to edit # $options An arrayref containing options for vim # (optional). Example '+set paste'. Each option # (such as '-c startinsert') needs to be # a separate array element. # RETURNS: Edited string # DESCRIPTION: Makes a temporary file with File::Temp ensuring that the file # is in utf8 mode. Writes the edit string to the file and invokes # the 'vim' editor on it. The resulting file is then read back # into a string and returned to the caller, again taking care to # retain utf8 mode. # THROWS: No exceptions # COMMENTS: File::Slurp and UTF-8 don't go well together. Moved to # File::Slurper instead # SEE ALSO: N/A #=============================================================================== sub run_editor { my ( $string, $options ) = @_; # # Build an arguments array for 'system' # my @args; push( @args, @$options ) if $options; # # Make a temporary file # my $tfh = File::Temp->new; binmode $tfh, ":encoding(UTF-8)"; my $tfn = $tfh->filename; print $tfh $string if $string; $tfh->close; # # Add the filename to the arguments # push( @args, $tfn ); die "Edit failed\n" unless ( system( ( 'vim', @args ) ) == 0 ); return read_text($tfn); } #=== FUNCTION ================================================================ # NAME: check_field # PURPOSE: Checks the a field is not too long and doesn't contain certain # characters # PARAMETERS: $name name of field # $field string to be checked # $maxlen maximum string length # $regex regex containing illegal characters to be removed # RETURNS: The input string truncated and with any illegal characters # removed. # DESCRIPTION: Runs a substitution on the string then truncates the result if # it is too long. # THROWS: No exceptions # COMMENTS: None # SEE ALSO: N/A #=============================================================================== sub check_field { my ( $name, $field, $maxlen, $regex ) = @_; return unless $field; $field =~ s/$regex//g; if ( length($field) > $maxlen ) { warn "Field '$name' too long (" . length($field) . "); truncated to " . $maxlen . "\n"; $field = substr( $field, 0, $maxlen ); } return $field; } #=== 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", "debug=i", "updatedb!", "config=s", "series_id=i", "regexp", "description!", "private!", "valid!", ); if ( !GetOptions( $optref, @options ) ) { pod2usage( -msg => "Version $VERSION\n", -exitval => 1 ); } return; } __END__ #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% # Application Documentation #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% #{{{ =head1 NAME edit_series - edit one or more fields in the database for a given series =head1 VERSION This documentation refers to edit_series version 0.0.3 =head1 USAGE edit_series [-h] [-debug=N] [-[no]updatedb] [-series_id=ID] [-regexp] [-[no]description] [-[no]private] [-[no]valid] [-config=FILE] series_name edit_series -updatedb -description GIMP edit_series -updatedb -description -regexp Awk edit_series -updatedb -noprivate -valid -series_id=102 =head1 REQUIRED ARGUMENTS =over 4 =item B Unless the B<-series_id=ID> option is given (see the OPTIONS section) it is necessary to provide a series name. Unless the B<-regexp> option is provided (see the OPTIONS section) the series name must match exactly, otherwise it is regarded as a MySQL regular expression. =back =head1 OPTIONS =over 4 =item B<-[no]updatedb> This option is required to make the script apply any changes that are made to the database. By default no updates are applied (B<-noupdatedb>). =item B<-regexp> This option causes the B argument to be interpreted as a regular expression for the MySQL database. By default the argument is treated as if it is an exact match. =item B<-series_id=ID> The series can be specified by the series ID through this route. If this used then the B argument is not required (and is ignored if given). =item B<-[no]description> This option, if given (as B<-description>), indicates that the 'description' field is to be edited. The Vim editor is invoked to make changes. The default is B<-nodescription> meaning that this field is not to be edited. =item B<-[no]private> This option, if given (as B<-private>), indicates that the 'private' field is to be set to 'true'. If given as B<-noprivate> this field is set to 'false'. If omitted altogether then the field is not changed. =item B<-[no]valid> This option, if given (as B<-valid>), indicates that the 'valid' field is to be set to 'true'. If given as B<-novalid> this field is set to 'false'. If omitted altogether then the field is not changed. =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> =back =head1 DESCRIPTION The script B provides an editor interface to certain fields in the B table of the HPR database. The fields are: =over 4 =item B This field is stored in a 'TEXT' field in the database. It is possible for the field to contain HTML, and some series do. In general it is better to keep this field short since it is displayed in its entirety before the notes for each show in the series. =back =head1 DIAGNOSTICS =over 4 =item B