667 lines
18 KiB
Perl
Executable File
667 lines
18 KiB
Perl
Executable File
#!/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<series_name>
|
|
|
|
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<series_name> 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<series_name> 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<edit_series> provides an editor interface to certain fields in
|
|
the B<miniseries> table of the HPR database. The fields are:
|
|
|
|
=over 4
|
|
|
|
=item B<description>
|
|
|
|
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<Select one of -description, -private and -valid>
|
|
|
|
At least one of these options is required. This a fatal error.
|
|
|
|
=item B<Specify the series name>
|
|
|
|
If no series name has been provided, and the B<-series_id=ID> option has not been
|
|
used the script is unable to determine the series to edit. This is a fatal
|
|
error.
|
|
|
|
=item B<Unable to find ...>
|
|
|
|
The configuration file containing details of the database cannot be found.
|
|
This is a fatal error.
|
|
|
|
=item B<DBI connect ... failed: Access denied for user ... >
|
|
|
|
The database connection has been denied. Check the configuration details (see
|
|
below). This a fatal error.
|
|
|
|
=item B<Too many matches to regex ...>
|
|
|
|
In B<-regex> mode a series name has been provided that matches too many series in
|
|
the database. Try again with a less ambiguous name. This a fatal error.
|
|
|
|
=item B<Unable to find series matching regex ...>
|
|
|
|
In B<-regex> mode a series name has been provided that matches no qseries in the
|
|
database. Try again. This a fatal error.
|
|
|
|
=item B<Edit failed>
|
|
|
|
If the Vim edit session fails in some way the script reports it this way. This
|
|
a fatal error.
|
|
|
|
=item B<Field '...' is too long (...); truncated to ...>
|
|
|
|
The string provided for the field is greater than the limit and has been
|
|
truncated. This is a warning.
|
|
|
|
=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
|
|
|
|
Config::General
|
|
DBI
|
|
Data::Dumper
|
|
File::Slurper
|
|
File::Temp
|
|
Getopt::Long
|
|
Pod::Usage
|
|
SQL::Abstract
|
|
|
|
=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) 2020 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]
|
|
|
|
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
|
|