forked from HPR/hpr-tools
Moved project directories and files to an empty local repo
This commit is contained in:
230
Database/.find_series.yml
Normal file
230
Database/.find_series.yml
Normal file
@@ -0,0 +1,230 @@
|
||||
ignore:
|
||||
- 'aka'
|
||||
- 'all'
|
||||
- 'amp'
|
||||
- 'an'
|
||||
- 'and'
|
||||
- 'app'
|
||||
- 'are'
|
||||
- 'art'
|
||||
- 'as'
|
||||
- 'at'
|
||||
- 'ayn'
|
||||
- 'bad'
|
||||
- 'bag'
|
||||
- 'bbs'
|
||||
- 'be'
|
||||
- 'ben'
|
||||
- 'big'
|
||||
- 'bit'
|
||||
- 'box'
|
||||
- 'bug'
|
||||
- 'by'
|
||||
- 'car'
|
||||
- 'cd'
|
||||
- 'cje'
|
||||
- 'cu'
|
||||
- 'cut'
|
||||
- 'dad'
|
||||
- 'dan'
|
||||
- 'day'
|
||||
- 'do'
|
||||
- 'doe'
|
||||
- 'dso'
|
||||
- 'ec'
|
||||
- 'ed'
|
||||
- 'eee'
|
||||
- 'egg'
|
||||
- 'eol'
|
||||
- 'ep'
|
||||
- 'era'
|
||||
- 'eve'
|
||||
- 'fab'
|
||||
- 'fav'
|
||||
- 'feb'
|
||||
- 'fix'
|
||||
- 'for'
|
||||
- 'fun'
|
||||
- 'gd'
|
||||
- 'gen'
|
||||
- 'get'
|
||||
- 'gmc'
|
||||
- 'go'
|
||||
- 'got'
|
||||
- 'gsm'
|
||||
- 'guy'
|
||||
- 'har'
|
||||
- 'has'
|
||||
- 'his'
|
||||
- 'how'
|
||||
- 'hpr'
|
||||
- 'ian'
|
||||
- 'ilf'
|
||||
- 'im'
|
||||
- 'in'
|
||||
- 'ink'
|
||||
- 'ip'
|
||||
- 'ipv'
|
||||
- 'is'
|
||||
- 'it'
|
||||
- 'its'
|
||||
- 'jan'
|
||||
- 'jon'
|
||||
- 'jwp'
|
||||
- 'ken'
|
||||
- 'la'
|
||||
- 'lee'
|
||||
- 'lot'
|
||||
- 'low'
|
||||
- 'lug'
|
||||
- 'man'
|
||||
- 'map'
|
||||
- 'may'
|
||||
- 'me'
|
||||
- 'mf'
|
||||
- 'mod'
|
||||
- 'mp'
|
||||
- 'mrs'
|
||||
- 'my'
|
||||
- 'new'
|
||||
- 'nix'
|
||||
- 'no'
|
||||
- 'non'
|
||||
- 'not'
|
||||
- 'now'
|
||||
- 'of'
|
||||
- 'off'
|
||||
- 'oh'
|
||||
- 'old'
|
||||
- 'on'
|
||||
- 'one'
|
||||
- 'or'
|
||||
- 'os'
|
||||
- 'oss'
|
||||
- 'ota'
|
||||
- 'our'
|
||||
- 'out'
|
||||
- 'own'
|
||||
- 'pam'
|
||||
- 'pat'
|
||||
- 'pay'
|
||||
- 'pc'
|
||||
- 'pe'
|
||||
- 'pis'
|
||||
- 'pre'
|
||||
- 'prn'
|
||||
- 'pt'
|
||||
- 'qsk'
|
||||
- 'rds'
|
||||
- 'rf'
|
||||
- 'rfa'
|
||||
- 'rm'
|
||||
- 'rob'
|
||||
- 'run'
|
||||
- 'rxy'
|
||||
- 'sap'
|
||||
- 'sdf'
|
||||
- 'set'
|
||||
- 'sex'
|
||||
- 'sfl'
|
||||
- 'sfs'
|
||||
- 'she'
|
||||
- 'sky'
|
||||
- 'so'
|
||||
- 'son'
|
||||
- 'tab'
|
||||
- 'tag'
|
||||
- 'ted'
|
||||
- 'th'
|
||||
- 'the'
|
||||
- 'tip'
|
||||
- 'to'
|
||||
- 'tom'
|
||||
- 'too'
|
||||
- 'tv'
|
||||
- 'two'
|
||||
- 'up'
|
||||
- 'us'
|
||||
- 'use'
|
||||
- 'van'
|
||||
- 'vol'
|
||||
- 'vs'
|
||||
- 'war'
|
||||
- 'way'
|
||||
- 'we'
|
||||
- 'wep'
|
||||
- 'who'
|
||||
- 'why'
|
||||
- 'win'
|
||||
- 'wow'
|
||||
- 'wtf'
|
||||
- 'xd'
|
||||
- 'xdc'
|
||||
- 'xgo'
|
||||
- 'xp'
|
||||
- 'you'
|
||||
- 'with'
|
||||
- '--'
|
||||
- '--tz'
|
||||
- 'updating'
|
||||
- 'hosts'
|
||||
- 'future'
|
||||
- 'favorite'
|
||||
- 'making'
|
||||
- 'screen'
|
||||
- 'should'
|
||||
- 'next'
|
||||
- 'word'
|
||||
- 'city'
|
||||
- 'others'
|
||||
- 'some'
|
||||
- 'needs'
|
||||
- 'other'
|
||||
- 'call'
|
||||
- 'alternative'
|
||||
- 'road'
|
||||
- 'month'
|
||||
- 'july'
|
||||
- 'care'
|
||||
- 'quick'
|
||||
- 'pt.'
|
||||
- 'over'
|
||||
- 'under'
|
||||
- 'made'
|
||||
- 'without'
|
||||
- '//'
|
||||
- 'more'
|
||||
- 'when'
|
||||
- 'p-'
|
||||
- 'behind'
|
||||
- 'using'
|
||||
- 'after'
|
||||
- 'about'
|
||||
- 'part'
|
||||
- 'show'
|
||||
- 'public'
|
||||
- 'from'
|
||||
- 'your'
|
||||
- 'this'
|
||||
- 'david'
|
||||
- 'tony'
|
||||
- 'chris'
|
||||
- 'jonathan'
|
||||
- 'kulp'
|
||||
- 'john'
|
||||
- 'klaatu'
|
||||
- 'into'
|
||||
- 'whitman'
|
||||
- 'pokey'
|
||||
- 'xokes'
|
||||
- 'drachenblut'
|
||||
- 'scott'
|
||||
- 'nybill'
|
||||
- 'conder'
|
||||
- 'jake'
|
||||
- 'leclanche'
|
||||
- 'stuart'
|
||||
- 'they'
|
||||
- 'josh'
|
||||
- 'jezra'
|
||||
- 'smith'
|
324
Database/clean_csv_tags
Executable file
324
Database/clean_csv_tags
Executable file
@@ -0,0 +1,324 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: clean_csv_tags
|
||||
#
|
||||
# USAGE: ./clean_csv_tags
|
||||
#
|
||||
# DESCRIPTION: Make sure tags in the eps.tags field of the HPR database
|
||||
# conform to CSV format.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.2
|
||||
# CREATED: 2017-01-30 15:32:04
|
||||
# REVISION: 2019-10-06 21:50:52
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Carp;
|
||||
use Getopt::Long;
|
||||
use Config::General;
|
||||
use Text::CSV_XS;
|
||||
use List::MoreUtils qw{uniq};
|
||||
use SQL::Abstract;
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.2';
|
||||
|
||||
#
|
||||
# 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";
|
||||
|
||||
my ( $dbh, $sth1, $h1, $rv );
|
||||
my ( %eps_tags, %diffs );
|
||||
my $status;
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
Usage() if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $cfgfile
|
||||
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
||||
|
||||
my $aq = ( defined( $options{aq} ) ? $options{aq} : 0 );
|
||||
my $verbose = ( defined( $options{verbose} ) ? $options{verbose} : 0 );
|
||||
my $dry_run = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 1 );
|
||||
|
||||
#
|
||||
# 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};
|
||||
$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;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Collect and process the id numbers and tags from the 'eps' table
|
||||
#-------------------------------------------------------------------------------
|
||||
%eps_tags = %{ collect_eps_tags( $dbh ) };
|
||||
|
||||
#
|
||||
# Dump all id numbers and tags if the verbose level is high enough
|
||||
#
|
||||
if ( $verbose >= 2 ) {
|
||||
my $csv = Text::CSV_XS->new( { always_quote => $aq } );
|
||||
|
||||
print "\nTags collected from the 'eps' table\n\n";
|
||||
foreach my $id ( sort { $a <=> $b } keys(%eps_tags) ) {
|
||||
$status = $csv->combine( @{ $eps_tags{$id} } );
|
||||
printf "%04d: %s\n", $id, $csv->string();
|
||||
}
|
||||
}
|
||||
|
||||
if ($dry_run) {
|
||||
print "\nNo changes made, dry-run mode\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Turn all the saved and cleaned tags into CSV strings again and save them
|
||||
# back to the database. TODO: find differences and only write those back
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Force quoting everywhere
|
||||
#
|
||||
my $csv = Text::CSV_XS->new( { always_quote => $aq } );
|
||||
|
||||
#
|
||||
# Loop through the hash in order of show number
|
||||
#
|
||||
for my $id ( sort keys %eps_tags ) {
|
||||
#
|
||||
# Put the array fields back together
|
||||
#
|
||||
$status = $csv->combine( @{ $eps_tags{$id} } );
|
||||
|
||||
#
|
||||
# Write them to the database
|
||||
#
|
||||
$dbh->do( q{UPDATE eps SET tags = ? WHERE id = ?},
|
||||
undef, $csv->string(), $id );
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: collect_eps_tags
|
||||
# PURPOSE: Collects the tags from the eps.tags field
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# RETURNS: A reference to the hash created by collecting all the tags
|
||||
# DESCRIPTION: Read the 'id' and tags' fields from the database. Parse the
|
||||
# tags as CSV data, flagging any errors. Trim each one and store
|
||||
# them in a hash keyed on the id number. The list of tags is
|
||||
# stored as an array in sorted order after ensuring there are
|
||||
# no duplicates.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub collect_eps_tags {
|
||||
my ( $dbh ) = @_;
|
||||
|
||||
my ( $status, @fields, %hash );
|
||||
my ( $sth, $h );
|
||||
|
||||
#
|
||||
# For parsing the field as CSV
|
||||
# NOTE: Unexplained error in [E. E. "Doc" Smith] (show 2462). Works with
|
||||
# double replaced by single quote, but doesn't work if quotes escaped (by
|
||||
# doubling) whether all tags are quoted or not. With 'auto_diag' enabled
|
||||
# get the error:
|
||||
# CSV_XS ERROR: 2034 - EIF - Loose unescaped quote @ rec 1632 pos 40 field 3
|
||||
#
|
||||
# NOTE: Adding 'allow_loose_quotes' avoids the issue
|
||||
#
|
||||
my $csv = Text::CSV_XS->new(
|
||||
{ binary => 1, auto_diag => 1, allow_loose_quotes => 1 } );
|
||||
|
||||
#
|
||||
# Query the eps table for all the id and tags
|
||||
#
|
||||
$sth = $dbh->prepare(
|
||||
q{SELECT id,tags FROM eps
|
||||
WHERE length(tags) > 0
|
||||
ORDER BY id}
|
||||
) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
$sth->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Loop through what we got
|
||||
#
|
||||
while ( $h = $sth->fetchrow_hashref ) {
|
||||
#
|
||||
# Parse the tag list
|
||||
#
|
||||
$status = $csv->parse( $h->{tags} );
|
||||
unless ($status) {
|
||||
#
|
||||
# Report and skip any errors
|
||||
#
|
||||
print "Parse error on episode ", $h->{id}, "\n";
|
||||
print $csv->error_input(), "\n";
|
||||
next;
|
||||
}
|
||||
@fields = $csv->fields();
|
||||
|
||||
next unless (@fields);
|
||||
|
||||
#
|
||||
# Trim all tags (don't alter $_ when doing it)
|
||||
#
|
||||
@fields = map {
|
||||
my $t = $_;
|
||||
$t =~ s/(^\s+|\s+$)//g;
|
||||
$t;
|
||||
} @fields;
|
||||
|
||||
#
|
||||
# De-duplicate
|
||||
#
|
||||
@fields = uniq(@fields);
|
||||
|
||||
#print "$h->{id}: ",join(",",@fields),"\n";
|
||||
|
||||
#
|
||||
# Save the id and its tags, sorted for comparison, with empty elements
|
||||
# removed too
|
||||
#
|
||||
$hash{ $h->{id} } = [ sort grep {!/^$/} @fields ];
|
||||
|
||||
}
|
||||
|
||||
#print Dumper(\%hash),"\n";
|
||||
|
||||
return \%hash;
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: Usage
|
||||
# PURPOSE: Display a usage message and exit
|
||||
# PARAMETERS: None
|
||||
# RETURNS: To command line level with exit value 1
|
||||
# DESCRIPTION: Builds the usage message using global values
|
||||
# THROWS: no exceptions
|
||||
# COMMENTS: none
|
||||
# SEE ALSO: n/a
|
||||
#===============================================================================
|
||||
sub Usage {
|
||||
print STDERR <<EOD;
|
||||
Usage: $PROG [options] project
|
||||
|
||||
$PROG v$VERSION
|
||||
|
||||
-help Display this information
|
||||
-config=FILE Define an alternative configuration file (e.g. that
|
||||
for the live database)
|
||||
-[no]aq Turn on/off the 'always_quote' setting in Text::CSV_XS,
|
||||
Default is off (0)
|
||||
-[no]dry-run Display what would have been done but make no changes.
|
||||
Default is -dry-run.
|
||||
-verbose A repeatable option which turns up the verbosity from
|
||||
0 (silent) to 2 (lots of stuff). Default is 0.
|
||||
|
||||
EOD
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#=== 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", "verbose+", "dry-run!", "config=s", "aq!", );
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
Usage();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
657
Database/convert_latin1
Executable file
657
Database/convert_latin1
Executable file
@@ -0,0 +1,657 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: convert_latin1
|
||||
#
|
||||
# USAGE: ./convert_latin1 [-help] [-doc] [-config=FILE] [-debug=N]
|
||||
#
|
||||
# DESCRIPTION: Find and convert 'latin1' characters to 'utf8' in the HPR
|
||||
# database
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.1.2
|
||||
# CREATED: 2023-05-04 10:07:04
|
||||
# REVISION: 2023-05-08 12:15:49
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use v5.16;
|
||||
use strict;
|
||||
use warnings;
|
||||
#use utf8;
|
||||
|
||||
# Using experimental features, some of which require warnings to be turned off
|
||||
use feature qw{ postderef say signatures state try };
|
||||
no warnings qw{
|
||||
experimental::postderef
|
||||
experimental::signatures
|
||||
experimental::try
|
||||
};
|
||||
|
||||
use Getopt::Long;
|
||||
use Pod::Usage;
|
||||
|
||||
use Config::General;
|
||||
|
||||
#use Encode qw( encode decode is_utf8 );
|
||||
#use Try::Tiny;
|
||||
#use TryCatch;
|
||||
|
||||
use SQL::Abstract;
|
||||
use DBI;
|
||||
|
||||
use Log::Handler;
|
||||
use Log::Handler::Output::File;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.1.2';
|
||||
|
||||
#
|
||||
# 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";
|
||||
my $logfile = "$basedir/${PROG}.log";
|
||||
|
||||
my ( $dbh, $sth1, $sth2, $h1 );
|
||||
my ( $sql, $utf8, $viewed );
|
||||
|
||||
#
|
||||
# Map of latin1 characters with their Unicode equivalents {{{
|
||||
#
|
||||
# Commented out 2023-05-10 since no longer wanted
|
||||
#
|
||||
#my %map_latin1 = (
|
||||
# q{€â‚¬} => "\N{U+20AC}",
|
||||
# q{ÀÀ} => "\N{U+00C0}",
|
||||
# q{ÁÃ} => "\N{U+00C1}",
|
||||
# q{‚‚} => "\N{U+201A}",
|
||||
# q{ÂÂ} => "\N{U+00C2}",
|
||||
# q{ƒÆ’} => "\N{U+0192}",
|
||||
# q{ÃÃ} => "\N{U+00C3}",
|
||||
# q{„„} => "\N{U+201E}",
|
||||
# q{ÄÄ} => "\N{U+00C4}",
|
||||
# q{……} => "\N{U+2026}",
|
||||
# q{ÅÃ…} => "\N{U+00C5}",
|
||||
# q{†â€} => "\N{U+2020}",
|
||||
# q{ÆÃ†} => "\N{U+00C6}",
|
||||
# q{‡â€¡} => "\N{U+2021}",
|
||||
# q{ÇÇ} => "\N{U+00C7}",
|
||||
# q{ˆË†} => "\N{U+02C6}",
|
||||
# q{ÈÈ} => "\N{U+00C8}",
|
||||
# q{‰â€°} => "\N{U+2030}",
|
||||
# q{ÉÉ} => "\N{U+00C9}",
|
||||
# q{ŠÅ} => "\N{U+0160}",
|
||||
# q{ÊÊ} => "\N{U+00CA}",
|
||||
# q{‹â€¹} => "\N{U+2039}",
|
||||
# q{ËË} => "\N{U+00CB}",
|
||||
# q{ŒÅ’} => "\N{U+0152}",
|
||||
# q{ÌÃŒ} => "\N{U+00CC}",
|
||||
# q{ÍÃ} => "\N{U+00CD}",
|
||||
# q{ŽÅ½} => "\N{U+017D}",
|
||||
# q{ÎÃŽ} => "\N{U+00CE}",
|
||||
# q{ÏÃ} => "\N{U+00CF}",
|
||||
# q{ÐÃ} => "\N{U+00D0}",
|
||||
# q{‘‘} => "\N{U+2018}",
|
||||
# q{ÑÑ} => "\N{U+00D1}",
|
||||
# q{Չ۪} => "\N{U+2019}",
|
||||
# q{ÒÃ’} => "\N{U+00D2}",
|
||||
# q{““} => "\N{U+201C}",
|
||||
# q{ÓÓ} => "\N{U+00D3}",
|
||||
# q{”â€} => "\N{U+201D}",
|
||||
# q{ÔÔ} => "\N{U+00D4}",
|
||||
# q{••} => "\N{U+2022}",
|
||||
# q{ÕÕ} => "\N{U+00D5}",
|
||||
# q{––} => "\N{U+2013}",
|
||||
# q{ÖÖ} => "\N{U+00D6}",
|
||||
# q{——} => "\N{U+2014}",
|
||||
# q{××} => "\N{U+00D7}",
|
||||
# q{˜Ëœ} => "\N{U+02DC}",
|
||||
# q{ØÃ˜} => "\N{U+00D8}",
|
||||
# q{™â„¢} => "\N{U+2122}",
|
||||
# q{ÙÙ} => "\N{U+00D9}",
|
||||
# q{šÅ¡} => "\N{U+0161}",
|
||||
# q{ÚÚ} => "\N{U+00DA}",
|
||||
# q{݉ۼ} => "\N{U+203A}",
|
||||
# q{ÛÛ} => "\N{U+00DB}",
|
||||
# q{œÅ“} => "\N{U+0153}",
|
||||
# q{ÜÜ} => "\N{U+00DC}",
|
||||
# q{ÝÃ} => "\N{U+00DD}",
|
||||
# q{žÅ¾} => "\N{U+017E}",
|
||||
# q{ÞÞ} => "\N{U+00DE}",
|
||||
# q{ŸÅ¸} => "\N{U+0178}",
|
||||
# q{ßß} => "\N{U+00DF}",
|
||||
# q{Â} => "\N{U+00A0}",
|
||||
# q{àÃ} => "\N{U+00E0}",
|
||||
# q{¡Â¡} => "\N{U+00A1}",
|
||||
# q{áá} => "\N{U+00E1}",
|
||||
# q{¢Â¢} => "\N{U+00A2}",
|
||||
# q{ââ} => "\N{U+00E2}",
|
||||
# q{£Â£} => "\N{U+00A3}",
|
||||
# q{ãã} => "\N{U+00E3}",
|
||||
# q{¤Â¤} => "\N{U+00A4}",
|
||||
# q{ää} => "\N{U+00E4}",
|
||||
# q{¥Â¥} => "\N{U+00A5}",
|
||||
# q{åÃ¥} => "\N{U+00E5}",
|
||||
# q{¦Â¦} => "\N{U+00A6}",
|
||||
# q{æÃ¦} => "\N{U+00E6}",
|
||||
# q{§Â§} => "\N{U+00A7}",
|
||||
# q{çç} => "\N{U+00E7}",
|
||||
# q{¨Â¨} => "\N{U+00A8}",
|
||||
# q{èè} => "\N{U+00E8}",
|
||||
# q{©Â©} => "\N{U+00A9}",
|
||||
# q{éé} => "\N{U+00E9}",
|
||||
# q{ªÂª} => "\N{U+00AA}",
|
||||
# q{êê} => "\N{U+00EA}",
|
||||
# q{«Â«} => "\N{U+00AB}",
|
||||
# q{ëë} => "\N{U+00EB}",
|
||||
# q{¬Â¬} => "\N{U+00AC}",
|
||||
# q{ìì} => "\N{U+00EC}",
|
||||
# q{Â} => "\N{U+00AD}",
|
||||
# q{íÃ} => "\N{U+00ED}",
|
||||
# q{®Â®} => "\N{U+00AE}",
|
||||
# q{îî} => "\N{U+00EE}",
|
||||
# q{¯Â¯} => "\N{U+00AF}",
|
||||
# q{ïï} => "\N{U+00EF}",
|
||||
# q{°Â°} => "\N{U+00B0}",
|
||||
# q{ðð} => "\N{U+00F0}",
|
||||
# q{±Â±} => "\N{U+00B1}",
|
||||
# q{ññ} => "\N{U+00F1}",
|
||||
# q{²Â²} => "\N{U+00B2}",
|
||||
# q{òò} => "\N{U+00F2}",
|
||||
# q{³Â³} => "\N{U+00B3}",
|
||||
# q{óó} => "\N{U+00F3}",
|
||||
# q{´Â´} => "\N{U+00B4}",
|
||||
# q{ôô} => "\N{U+00F4}",
|
||||
# q{µÂµ} => "\N{U+00B5}",
|
||||
# q{õõ} => "\N{U+00F5}",
|
||||
# q{¶Â¶} => "\N{U+00B6}",
|
||||
# q{öö} => "\N{U+00F6}",
|
||||
# q{·Â·} => "\N{U+00B7}",
|
||||
# q{÷÷} => "\N{U+00F7}",
|
||||
# q{¸Â¸} => "\N{U+00B8}",
|
||||
# q{øÃ¸} => "\N{U+00F8}",
|
||||
# q{¹Â¹} => "\N{U+00B9}",
|
||||
# q{ùù} => "\N{U+00F9}",
|
||||
# q{ºÂº} => "\N{U+00BA}",
|
||||
# q{úú} => "\N{U+00FA}",
|
||||
# q{»Â»} => "\N{U+00BB}",
|
||||
# q{ûû} => "\N{U+00FB}",
|
||||
# q{¼Â¼} => "\N{U+00BC}",
|
||||
# q{üü} => "\N{U+00FC}",
|
||||
# q{½Â½} => "\N{U+00BD}",
|
||||
# q{ýý} => "\N{U+00FD}",
|
||||
# q{¾Â¾} => "\N{U+00BE}",
|
||||
# q{þþ} => "\N{U+00FE}",
|
||||
# q{¿Â¿} => "\N{U+00BF}",
|
||||
# q{ÿÿ} => "\N{U+00FF}",
|
||||
#);
|
||||
|
||||
#
|
||||
# Build a regex from all of the hash keys
|
||||
#
|
||||
#my $regex = join('|',sort(keys(%map_latin1)));
|
||||
#$regex=qr{$regex};
|
||||
|
||||
#}}}
|
||||
|
||||
#
|
||||
# Enable Unicode output mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments {{{
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
#
|
||||
# Default help
|
||||
#
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 )
|
||||
if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Full documentation if requested with -doc
|
||||
#
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 2, -exitval => 1 )
|
||||
if ( $options{'doc'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $cfgfile
|
||||
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
||||
my $dry_run = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 0 );
|
||||
my $verbose = ( defined( $options{verbose} ) ? $options{verbose} : 0 );
|
||||
my $field = $options{field};
|
||||
my $skip = $options{skip} // 0;
|
||||
my $limit = $options{limit} // 0;
|
||||
|
||||
# }}}
|
||||
|
||||
#
|
||||
# Sanity checks
|
||||
#
|
||||
die "Unable to find $cfgfile\n" unless ( -e $cfgfile );
|
||||
if ($field) {
|
||||
$field = lc($field);
|
||||
die "Invalid value for -field=FIELD\n"
|
||||
unless ( $field =~ /title|summary|tags|notes/ );
|
||||
}
|
||||
else {
|
||||
$field = 'title';
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 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};
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Set up logging keeping the default log layout except for the date
|
||||
#-------------------------------------------------------------------------------
|
||||
my $log = Log::Handler->new();
|
||||
|
||||
$log->add(
|
||||
file => {
|
||||
timeformat => "%Y-%m-%d %H:%M:%S",
|
||||
filename => $logfile,
|
||||
maxlevel => 7,
|
||||
minlevel => 0,
|
||||
utf8 => 1,
|
||||
}
|
||||
);
|
||||
|
||||
#
|
||||
# Log the settings being used
|
||||
#
|
||||
$log->info("---- Running version $VERSION");
|
||||
$log->info("Configuration file $cfgfile");
|
||||
$log->info("Processing field '$field'");
|
||||
$log->info("Skipping $skip non-ASCII rows") if $skip;
|
||||
$log->info("Update limit is $limit") if $limit;
|
||||
$log->info("Dry-run mode") if ($dry_run);
|
||||
|
||||
#
|
||||
# Adjust limit
|
||||
#
|
||||
$limit += $skip if $skip;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Perform a scan of episodes for the chosen field which contains non-ASCII
|
||||
#-------------------------------------------------------------------------------
|
||||
$sql = sprintf(
|
||||
q{SELECT id,%s FROM eps WHERE %s <> CONVERT(%s USING ASCII) ORDER BY id},
|
||||
$field, $field, $field
|
||||
);
|
||||
|
||||
$sth1 = $dbh->prepare($sql) or die $DBI::errstr;
|
||||
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Prepare SQL::Abstract and the SQL template for the updates
|
||||
#
|
||||
my $sqla = SQL::Abstract->new;
|
||||
|
||||
my $stmt1 = sprintf(
|
||||
q{UPDATE eps SET %s = CONVERT(BINARY CONVERT(%s USING latin1) USING utf8)},
|
||||
$field, $field
|
||||
);
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Loop through what we get from the main query, attempting to convert each field
|
||||
#-------------------------------------------------------------------------------
|
||||
$viewed = 0;
|
||||
while ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
$viewed++;
|
||||
next if $viewed <= $skip;
|
||||
|
||||
#
|
||||
# Prepare the 'WHERE' part of the SQL
|
||||
#
|
||||
my %where = ( id => $h1->{id} );
|
||||
my ( $stmt2, @bind ) = $sqla->where( \%where );
|
||||
my $stmt = "${stmt1}${stmt2}";
|
||||
|
||||
#
|
||||
# In dry-run mode just report what would have been done, otherwise try and
|
||||
# make the change.
|
||||
#
|
||||
if ($dry_run) {
|
||||
if ($verbose) {
|
||||
printf "[%04d] %s\n", $h1->{id},
|
||||
(
|
||||
$field eq 'notes'
|
||||
? ''
|
||||
: $h1->{$field}
|
||||
);
|
||||
}
|
||||
|
||||
say "SQL: ${stmt}";
|
||||
say "Arguments: ",join( ',', @bind );
|
||||
}
|
||||
else {
|
||||
$sth2 = $dbh->prepare($stmt) or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# The SQL could generate an error which we'll try and intercept
|
||||
#
|
||||
try {
|
||||
$sth2->execute(@bind)
|
||||
or die $DBI::errstr;
|
||||
$log->info("Updated $field field for row $h1->{id}");
|
||||
}
|
||||
catch ($e) {
|
||||
$log->info("Failed to update $field field for row $h1->{id}");
|
||||
$log->info("Error: $e");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
continue {
|
||||
if ($limit) {
|
||||
if ($viewed >= $limit) {
|
||||
$log->info("Update limit reached");
|
||||
last;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
#=== 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", "doc", "dry-run!", "verbose!",
|
||||
"config=s", "field=s", "skip=i", "limit=i",
|
||||
);
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
=head1 NAME
|
||||
|
||||
convert_latin1 - a script to convert fields in the HPR database to UTF-8
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to convert_latin1 version 0.1.2
|
||||
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
./convert_latin1 [-help] [-doc] [-config=FILE] [-[no]dry-run]
|
||||
[-[no]verbose] [-field=FIELDNAME] [-skip=N] [-limit=N]
|
||||
|
||||
./convert_latin1 -config=.hpr_livedb.cfg -verb -field=title
|
||||
./convert_latin1 -config=.hpr_livedb.cfg -verb -dry-run -field=notes
|
||||
-limit=10
|
||||
|
||||
|
||||
=head1 OPTIONS
|
||||
|
||||
=over 8
|
||||
|
||||
=item B<-help>
|
||||
|
||||
Prints a brief help message describing the usage of the program, and then exits.
|
||||
|
||||
=item B<-doc>
|
||||
|
||||
Displays the entirety of the documentation (using a pager), and then exits. To
|
||||
generate a PDF version use:
|
||||
|
||||
pod2pdf convert_latin1 --out=convert_latin1.pdf
|
||||
|
||||
=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<-[no]dry-run>
|
||||
|
||||
Controls whether the program runs in a mode where it performs database
|
||||
updates. When enabled the details of the updates to be performed are shown,
|
||||
otherwise the updates are applied. The default B<-nodry-run> allows the
|
||||
program to perform the changes.
|
||||
|
||||
=item B<-[no]verbose>
|
||||
|
||||
Normally very little is reported by the script, although details of errors
|
||||
are reported. When B<-verbose> is selected more information
|
||||
about the number of rows needing work, the updates performed (or which would
|
||||
have been performed) and how many changes were made is reported.
|
||||
|
||||
=item B<-field=FIELDNAME>
|
||||
|
||||
This option defines the database field name to be converted. The permitted
|
||||
names are B<title>, B<summary>, B<tags> and B<notes> and the table is asumed
|
||||
to be B<eps>. If the option is not provided the default field B<title> will be
|
||||
used.
|
||||
|
||||
=item B<-skip=N>
|
||||
|
||||
This option defines the number of database rows to skip when processing the
|
||||
selected field. If omitted then no rows are skipped. The option is useful to
|
||||
allow the work to be split into manageable batches, in conjunction with the
|
||||
B<-limit=N> option below.
|
||||
|
||||
=item B<-limit=N>
|
||||
|
||||
This option defines the number of database rows to work on when processing the
|
||||
selected field. If omitted then all rows are processed (after any skip defined
|
||||
with te B<-skip=N> option). The option is useful to allow the work to split
|
||||
into manageable batches, in conjunction with the B<-skip=N> option above.
|
||||
|
||||
=back
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
=head2 OVERVIEW
|
||||
|
||||
The script is designed to repair the HPR MySQL (MariaDB) database which holds
|
||||
show metadata. The database was created with 'latin1' encoding, and was later
|
||||
changed to use UTF-8. However, no action was taken to ensure the PHP software
|
||||
managing the database also used UTF-8. This meant that the 'latin1' encoded data
|
||||
was still being generated as Unicode UTF-8 data was being added, and was being
|
||||
rendered in the expected way, while there was little or no UTF-8 data being
|
||||
stored.
|
||||
|
||||
The PHP deficiencies were rectified in April 2023 but this meant that all
|
||||
non-ASCII characters stored in the database before that were rendered
|
||||
incorrectly. The solution was to convert all 'latin1' non-ASCII data into
|
||||
UTF-8, and that is what this script does.
|
||||
|
||||
Detecting non ASCII in database fields was performed with the following SQL:
|
||||
|
||||
SELECT id,field FROM eps WHERE field <> CONVERT(field USING ASCII) ORDER BY id
|
||||
|
||||
This is used to generate a list of all rows which might need conversion to
|
||||
UTF-8. However, the test is only whether there is non-ASCII data in the row.
|
||||
|
||||
Ideally, the conversion could have been performed entirely within the database
|
||||
with SQL such as the following (for each field):
|
||||
|
||||
UPDATE eps SET field = CONVERT(binary CONVERT(field USING latin1) USING utf8)
|
||||
WHERE field <> CONVERT(field USING ASCII);
|
||||
|
||||
However, the conversion to UTF-8 fails when the field already contains such
|
||||
characters, stopping the query.
|
||||
|
||||
MySQL and MariaDB are capable of trapping errors (like using B<try/catch> in
|
||||
various programming languages), but only in stored procedures. It was felt to
|
||||
be undesirable to create stored procedures on the HPR database since this was
|
||||
only possible through B<phpMyAdmin> which is due to be phased out.
|
||||
|
||||
This script was written to enable the catching of errors instead.
|
||||
|
||||
=head2 SCRIPT DESIGN
|
||||
|
||||
The main loop returns all rows with non-ASCII characters in the field being
|
||||
processed. For each row an 'UPDATE' query is performed using the 'id' field
|
||||
(episode number) to select it:
|
||||
|
||||
UPDATE eps SET field = CONVERT(BINARY CONVERT(field USING latin1) USING utf8)
|
||||
WHERE id = value
|
||||
|
||||
This is performed inside a B<try/catch> statement so that if the query fails
|
||||
it does not stop the script. Successes and failures are logged.
|
||||
|
||||
This algorithm is fairly slow, particularly for the 'notes' field which has
|
||||
the most (nearly 600) non-ASCII rows. However, it seems to work as desired.
|
||||
|
||||
The B<-skip=N> and B<-limit=N> options allow control over the conversion
|
||||
process such that the work can be done in batches.
|
||||
|
||||
Note that the log file used by the script is called B<convert_latin1.log>. It
|
||||
is appended to on every run. The file name can only be changed by editing the
|
||||
script.
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
A list of every error and warning message that the application can generate
|
||||
(even the ones that will "never happen"), with a full explanation of each
|
||||
problem, one or more likely causes, and any suggested remedies. If the
|
||||
application generates exit status codes (e.g. under Unix) then list the exit
|
||||
status associated with each error.
|
||||
|
||||
|
||||
=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. This can be changed by use of the
|
||||
B<-configuration=FILE> option as described above.
|
||||
|
||||
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
|
||||
Getopt::Long
|
||||
Log::Handler
|
||||
Log::Handler::Output::File
|
||||
Pod::Usage
|
||||
SQL::Abstract
|
||||
|
||||
The script uses the experimental B<try> feature and disables the warning that
|
||||
this feature generates. Note that this feature is only available in Perl
|
||||
versions at 5.34.0 or above (the script was developed under v5.36.0).
|
||||
|
||||
=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) 2023 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
|
||||
|
1438
Database/copy_mysql_pg
Executable file
1438
Database/copy_mysql_pg
Executable file
File diff suppressed because it is too large
Load Diff
469
Database/create_series
Executable file
469
Database/create_series
Executable file
@@ -0,0 +1,469 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: create_series
|
||||
#
|
||||
# USAGE: ./create_series -name=NAME -description=DESC [-[no]private]
|
||||
# [-image=IMAGE] [-[no]valid] [-[no]updatedb] [-config=FILE] [-help]
|
||||
#
|
||||
# DESCRIPTION: Create a new series in the HPR database
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.4
|
||||
# CREATED: 2015-01-15 16:09:09
|
||||
# REVISION: 2022-04-12 21:37:02
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Getopt::Long;
|
||||
use Pod::Usage;
|
||||
|
||||
use Config::General;
|
||||
|
||||
use Try::Tiny;
|
||||
use IO::Prompter;
|
||||
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.4';
|
||||
|
||||
#
|
||||
# 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";
|
||||
|
||||
my ( $dbh, $sth1, $h1, $rv, $rc );
|
||||
my ( $answer, $id );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
#
|
||||
# Default help
|
||||
#
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 )
|
||||
if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $cfgfile
|
||||
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
||||
|
||||
my $updatedb = ( defined( $options{'updatedb'} ) ? $options{'updatedb'} : 0 );
|
||||
|
||||
my $name = $options{'name'};
|
||||
my $description = $options{'description'};
|
||||
my $private = ( defined( $options{'private'} ) ? $options{'private'} : 0 );
|
||||
my $image = ( defined( $options{'image'} ) ? $options{'image'} : '' );
|
||||
my $valid = ( defined( $options{'valid'} ) ? $options{'valid'} : 1 );
|
||||
|
||||
die "Options -name and -description are mandatory\n"
|
||||
unless ( $name && $description );
|
||||
|
||||
#
|
||||
# Sanity check
|
||||
#
|
||||
die "Unable to find $cfgfile\n" unless ( -e $cfgfile );
|
||||
|
||||
#
|
||||
# Check we have the right values
|
||||
#
|
||||
printf "Planning to add the following series:\n" .
|
||||
"Name: %s\n" .
|
||||
"Description: %s\n" .
|
||||
"Private: %s\n" .
|
||||
"Image: '%s'\n" .
|
||||
"Valid: %s\n",
|
||||
$name,
|
||||
$description,
|
||||
( $private ? 'Yes' : 'No' ),
|
||||
$image,
|
||||
( $valid ? 'Yes' : 'No');
|
||||
|
||||
print "Note that -updatedb has not been set, so no changes will be made.\n"
|
||||
unless ($updatedb);
|
||||
|
||||
#
|
||||
# Ask for confirmation, failing gracefully if there's a problem
|
||||
#
|
||||
try {
|
||||
$answer = prompt(
|
||||
-in => *STDIN,
|
||||
-out => *STDERR,
|
||||
-prompt => 'Is this correct? ',
|
||||
-style => 'red',
|
||||
-yn
|
||||
);
|
||||
}
|
||||
catch {
|
||||
warn "Problem collecting answer $_";
|
||||
$answer = 0;
|
||||
};
|
||||
|
||||
unless ($answer) {
|
||||
print "Exiting...\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Configuration file - load data
|
||||
#-------------------------------------------------------------------------------
|
||||
my $conf = Config::General->new(
|
||||
-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 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;
|
||||
|
||||
#
|
||||
# Does a series with this name already exist?
|
||||
#
|
||||
$sth1 = $dbh->prepare(q{
|
||||
SELECT id AS count FROM miniseries WHERE name = ?
|
||||
});
|
||||
$sth1->execute($name);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
if ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
print "A series with the name '$name' already exists\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
#
|
||||
# Should be OK to create the series if we get here, so long as we've been
|
||||
# asked to do so.
|
||||
#
|
||||
if ($updatedb) {
|
||||
#
|
||||
# Go into transaction mode here so we can fail safely
|
||||
#
|
||||
$rc = $dbh->begin_work or die $dbh->errstr;
|
||||
|
||||
#
|
||||
# Perform the INSERT
|
||||
#
|
||||
$rv = $dbh->do(q{
|
||||
INSERT INTO miniseries (name,description,private,image,valid)
|
||||
VALUES(?,?,?,?,?)
|
||||
},
|
||||
undef,
|
||||
$name,
|
||||
$description,
|
||||
$private,
|
||||
$image,
|
||||
$valid
|
||||
);
|
||||
|
||||
#
|
||||
# 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' );
|
||||
|
||||
#
|
||||
# Report any success
|
||||
#
|
||||
if ($rv) {
|
||||
#
|
||||
# Find out what id we just generated and report it if found
|
||||
#
|
||||
$id = $dbh->last_insert_id();
|
||||
if ($id) {
|
||||
print "Series added with id $id\n";
|
||||
} else {
|
||||
print "Series added\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "Series not added due to error\n";
|
||||
}
|
||||
|
||||
} else {
|
||||
print "Option -noupdatedb chosen, database not updated\n";
|
||||
}
|
||||
|
||||
#
|
||||
# We've finished with the database
|
||||
#
|
||||
$dbh->disconnect;
|
||||
|
||||
exit;
|
||||
|
||||
#=== 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", "config=s", "updatedb!", "name=s",
|
||||
"description=s", "private!", "image:s", "valid!"
|
||||
);
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
=head1 NAME
|
||||
|
||||
create_series - create a new series in the 'miniseries' table in the HPR DB
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to create_series version 0.0.4
|
||||
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
create_series -name=NAME -description=DESC [-[no]private]
|
||||
[-image=IMAGE] [-[no]valid] [-[no]updatedb] [-config=FILE] [-help]
|
||||
|
||||
desc="An overview of this open-source graphics program, "
|
||||
desc+="with a focus on photographic issues."
|
||||
create_series -update -name='GIMP' -description="$desc"
|
||||
|
||||
=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<-name=NAME>
|
||||
|
||||
This mandatory option defines the title for the new series. The limit on the
|
||||
length of the name is 100 characters and the script will reject anything
|
||||
longer than this.
|
||||
|
||||
=item B<-description=DESC>
|
||||
|
||||
This mandatory option defines the description for the new series. There is no limit on the
|
||||
length of this field, but it may be difficult to enter very large amounts of
|
||||
text here. One solution might be to prepare the text in a file and use
|
||||
a command substitution to enter it.
|
||||
|
||||
create_series -update -name='GIMP' -description="$(cat GIMP.txt)"
|
||||
|
||||
=item B<-[no]private>
|
||||
|
||||
Series can be private or public. Selecting B<-private> creates a new private
|
||||
series, whereas B<-noprivate> creates a public series. The default is to
|
||||
create a public one.
|
||||
|
||||
=item B<-image=IMAGE>
|
||||
|
||||
The image field in the database is not currently used. Three series have
|
||||
a short text string in this field, but no data in the field seems to be used
|
||||
anywhere. It would be possible to add data to this field in the database when
|
||||
creating a series, and this option is available to do so, but by default
|
||||
an empty string is inserted. Note that the database design does not allow this
|
||||
field to be NULL for unknown reasons.
|
||||
|
||||
=item B<-[no]valid>
|
||||
|
||||
Series can be valid or invalid. Selecting B<-valid> creates a new valid
|
||||
series, whereas B<-novalid> creates an invalid series. The default is to
|
||||
create a valid one.
|
||||
|
||||
Series marked invalid are not displayed, but there are none in this state at
|
||||
the moment.
|
||||
|
||||
=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 collects the necessary attributes for a new series in the HPR
|
||||
database, displays them for validation and if requested, adds them to the
|
||||
database.
|
||||
|
||||
Every series must have a name and a description. The settings for I<private>,
|
||||
I<image> and I<valid> have defaults as described above. The values and
|
||||
defaults are shown as follows and the user is prompted to decide whether to
|
||||
proceed with series creation or not:
|
||||
|
||||
Planning to add the following series:
|
||||
Name: GIMP
|
||||
Description: An overview of this open-source graphics program, with a focus on photographic issues.
|
||||
Private: No
|
||||
Image: ''
|
||||
Valid: Yes
|
||||
Is this correct?
|
||||
|
||||
Answering 'Y' to this prompt will result in creation (assuming this is
|
||||
possible).
|
||||
|
||||
Upon creation the script reports the B<id> value assigned to the series. This
|
||||
is useful to know when adding episodes to the series.
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<Options -name and -description are mandatory>
|
||||
|
||||
Both of these options must be present when creating a new series. 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.
|
||||
|
||||
=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. This configuration file can be overridden using
|
||||
the B<-config=FILE> option as described above.
|
||||
|
||||
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
|
||||
Getopt::Long
|
||||
IO::Prompter
|
||||
Pod::Usage
|
||||
Try::Tiny
|
||||
|
||||
=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) 2015-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
|
||||
|
18
Database/double_host.sql
Normal file
18
Database/double_host.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------
|
||||
* Written a while ago. I think it finds hosts called "'host1' and 'host2'"
|
||||
* and pulls the individual hosts out of the pair so that they can be
|
||||
* installed into the table as separate hosts
|
||||
---------------------------------------------------------------------------- */
|
||||
select ho.hostid, ho.host, n1.hostid, n1.host, n1.host1, n2.hostid, n2.host, n2.host2
|
||||
from hosts ho
|
||||
left join (select hostid, host, left(host,instr(host,' and ')-1) as host1
|
||||
from hosts
|
||||
where host like '% and %') as n1
|
||||
on n1.host1 = ho.host
|
||||
left join (select hostid, host, substring(host,instr(host,' and ')+5) as host2
|
||||
from hosts
|
||||
where host like '% and %') as n2
|
||||
on n2.host2 = ho.host
|
||||
where n1.host1 is not null
|
||||
or n2.host2 is not null
|
||||
;
|
832
Database/edit_episode
Executable file
832
Database/edit_episode
Executable file
@@ -0,0 +1,832 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: edit_episode
|
||||
#
|
||||
# USAGE: ./edit_episode [-h] [-debug=N] [-config=FILE] [-[no]update]
|
||||
# [-[no]title] [-[no]summary] [-[no]tags] [-[no]notes]
|
||||
# [-[no]ctitle] [-[no]ctext] [-cnumber=N] shownumber
|
||||
#
|
||||
# DESCRIPTION: A simple command-line editor for the HPR database
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: Had to revert to MySQL due to a problem with DBD::MariaDB
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.1.3
|
||||
# CREATED: 2015-06-17 23:17:50
|
||||
# REVISION: 2022-02-16 20:07:45
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
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.1.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 (%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 $title = ( defined( $options{'title'} ) ? $options{'title'} : 0 );
|
||||
my $summary = ( defined( $options{'summary'} ) ? $options{'summary'} : 0 );
|
||||
my $tags = ( defined( $options{'tags'} ) ? $options{'tags'} : 0 );
|
||||
my $notes = ( defined( $options{'notes'} ) ? $options{'notes'} : 0 );
|
||||
my $ctitle = ( defined( $options{'ctitle'} ) ? $options{'ctitle'} : 0 );
|
||||
my $ctext = ( defined( $options{'ctext'} ) ? $options{'ctext'} : 0 );
|
||||
my $cnumber = $options{'cnumber'};
|
||||
|
||||
die "Select one of -title, -summary, -tags, -notes, -ctitle and -ctext\n"
|
||||
unless ( $title || $summary || $tags || $notes || $ctitle || $ctext );
|
||||
|
||||
die "Needs a comment number (-cnumber=N)\n"
|
||||
if ( ( $ctitle || $ctext ) && ( !$cnumber ) );
|
||||
|
||||
#
|
||||
# Get the arg
|
||||
#
|
||||
my $show = shift;
|
||||
pod2usage( -msg => "Specify the show number\n", -exitval => 1 ) unless $show;
|
||||
|
||||
#
|
||||
# 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};
|
||||
|
||||
#$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 for the selected episode and count the number
|
||||
# of comments it has in case we need to know later.
|
||||
#
|
||||
$sth1 = $dbh->prepare(q{
|
||||
SELECT
|
||||
e.*, count(c.id) as comment_count
|
||||
FROM eps e
|
||||
LEFT JOIN comments c ON e.id = c.eps_id
|
||||
GROUP BY e.id
|
||||
HAVING e.id = ?
|
||||
});
|
||||
$sth1->execute($show);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Did we find the episode?
|
||||
#
|
||||
if ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
#
|
||||
# Found, so do the episode details need changing?
|
||||
#
|
||||
if ( $title || $summary || $tags || $notes ) {
|
||||
change_episode( $dbh, $h1, $show, $updatedb, $title, $summary, $tags,
|
||||
$notes );
|
||||
}
|
||||
|
||||
#
|
||||
# Are we to change comment details?
|
||||
#
|
||||
if ( $ctitle || $ctext ) {
|
||||
if ( $h1->{comment_count} > 0 ) {
|
||||
change_comment( $dbh, $h1, $show, $cnumber, $updatedb, $ctitle,
|
||||
$ctext );
|
||||
}
|
||||
else {
|
||||
print "This show has no comments\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "Unable to find show number $show\n";
|
||||
}
|
||||
|
||||
#$dbh->disconnect;
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: change_episode
|
||||
# PURPOSE: Make changes to a row in the 'eps' table for a show
|
||||
# PARAMETERS: $dbh open handle of the MySQL database
|
||||
# $h handle of the query that returned the episode
|
||||
# record and comment count
|
||||
# $show show number being updated
|
||||
# $updatedb Boolean; true when changes are to be made
|
||||
# $title Boolean; true when the episode title is to be
|
||||
# changed
|
||||
# $summary Boolean; true when the episode summary is to be
|
||||
# changed
|
||||
# $tags Boolean; true when the episode tags are to be
|
||||
# changed
|
||||
# $notes Boolean; true when the episode notes are to be
|
||||
# changed
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: The episode has been found in the database. The requested
|
||||
# changes are applied. If after comparing old with new changes
|
||||
# are found they are applied, otherwise nothing is done.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub change_episode {
|
||||
my ( $dbh, $h, $show, $updatedb, $title, $summary, $tags, $notes ) = @_;
|
||||
|
||||
my (%changes);
|
||||
|
||||
#<<< [perltidy messes up the following]
|
||||
if ($title) {
|
||||
$changes{title} = check_field( 'title',
|
||||
scalar( run_editor( $h->{title} ) ), 100, qr{(\n)} );
|
||||
}
|
||||
if ($summary) {
|
||||
$changes{summary} = check_field( 'summary',
|
||||
scalar( run_editor( $h->{summary} ) ), 100, qr{(\n)} );
|
||||
}
|
||||
if ($tags) {
|
||||
$changes{tags} = check_field( 'tags',
|
||||
scalar( run_editor( $h->{tags} ) ), 200, qr{(\n)} );
|
||||
}
|
||||
if ($notes) {
|
||||
$changes{notes} = run_editor( $h->{notes}, ['+set filetype=html'] );
|
||||
}
|
||||
#>>>
|
||||
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 $h->{$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 => $show );
|
||||
my ( $stmt, @bind ) = $sql->update( 'eps', \%changes, \%where );
|
||||
|
||||
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' );
|
||||
|
||||
#
|
||||
# Report the update
|
||||
#
|
||||
if ($rv) {
|
||||
my $ccount = scalar( keys(%changes) );
|
||||
printf "Updated database (%d %s to the eps row)\n",
|
||||
$ccount, _plural( 'change', $ccount );
|
||||
}
|
||||
else {
|
||||
print "Episode not updated due to error\n";
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
#
|
||||
# No changes were found
|
||||
#
|
||||
print "There was nothing to do\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "Option -noupdatedb chosen database not updated\n";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: change_comment
|
||||
# PURPOSE: Make changes to a comment relating to a show
|
||||
# PARAMETERS: $dbh open handle of the MySQL database
|
||||
# $h handle of the query that returned the episode
|
||||
# record and comment count
|
||||
# $show show number being updated
|
||||
# $cnumber comment number within show
|
||||
# $updatedb Boolean; true when changes are to be made
|
||||
# $ctitle Boolean; true when the comment title is to be
|
||||
# changed
|
||||
# $ctext Boolean; true when the comment text is to be
|
||||
# changed
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: The episode has been found in the database and the number of
|
||||
# comments determined. We know there are more than zero comments
|
||||
# otherwise this routine woulkd not have been called. We check
|
||||
# that the requested comment number is in range here (if could
|
||||
# have been done before invocation). We query the target comment
|
||||
# and modify one or both of the requested fields. If, after
|
||||
# comparing old with new, changes are found, they are applied,
|
||||
# otherwise nothing is done.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub change_comment {
|
||||
my ( $dbh, $h, $show, $cnumber, $updatedb, $ctitle, $ctext ) = @_;
|
||||
|
||||
my ( $sth1, $h1 );
|
||||
my (%changes);
|
||||
|
||||
if ( $cnumber <= $h->{comment_count} ) {
|
||||
#
|
||||
# Get the requested comment
|
||||
#
|
||||
$sth1 = $dbh->prepare(
|
||||
q{
|
||||
SELECT *
|
||||
FROM comments
|
||||
WHERE eps_id = ?
|
||||
ORDER BY comment_timestamp
|
||||
LIMIT 1
|
||||
OFFSET ?
|
||||
}
|
||||
);
|
||||
$sth1->execute( $show, $cnumber - 1 );
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# If found perform changes, otherwise it wasn't found (not sure how
|
||||
# that's possible but you never know)
|
||||
#
|
||||
if ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
if ($ctitle) {
|
||||
$changes{comment_title} = run_editor( $h1->{comment_title} );
|
||||
}
|
||||
if ($ctext) {
|
||||
$changes{comment_text} = run_editor( $h1->{comment_text} );
|
||||
}
|
||||
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 => $h1->{id} );
|
||||
my ( $stmt, @bind )
|
||||
= $sql->update( 'comments', \%changes, \%where );
|
||||
|
||||
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' );
|
||||
|
||||
#
|
||||
# Report the update
|
||||
#
|
||||
if ($rv) {
|
||||
my $ccount = scalar(keys(%changes));
|
||||
printf "Updated database (%d %s to the comments row)\n",
|
||||
$ccount, _plural( 'change', $ccount );
|
||||
}
|
||||
else {
|
||||
print "Comment not updated due to error\n";
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
print "There was nothing to do\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "Option -noupdatedb chosen database not updated\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "Requested comment is out of range\n";
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
#=== 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: _plural
|
||||
# PURPOSE: Add an 's' to a word depending on a number
|
||||
# PARAMETERS: $word word to pluralise
|
||||
# $count number being used in message
|
||||
# RETURNS: The word in a plural form or not
|
||||
# DESCRIPTION: Just hides the expression that adds an 's' or not behind
|
||||
# a function call
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub _plural {
|
||||
my ( $word, $count ) = @_;
|
||||
|
||||
return $word . ( abs($count) != 1 ? 's' : '' );
|
||||
}
|
||||
|
||||
#=== 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", "config=s", "updatedb!", "title!",
|
||||
"summary!", "tags!", "notes!", "ctitle!", "ctext!", "cnumber=i",
|
||||
);
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "Version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
=head1 NAME
|
||||
|
||||
edit_episode - edit one or more fields in the database for a given HPR show
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to edit_episode version 0.1.3
|
||||
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
edit_episode [-help] [-debug=N] [-config=FILE] [-[no]updatedb] [-[no]title]
|
||||
[-[no]summary] [-[no]tags] [-[no]notes] [-[no]ctitle] [-[no]ctext]
|
||||
[-cnumber=N] shownumber
|
||||
|
||||
edit_episode -updatedb -title 1234
|
||||
edit_episode -updatedb -title -summary 2000
|
||||
edit_episode -updatedb -tags 2050
|
||||
edit_episode -updatedb -notes 2045
|
||||
edit_episode -updatedb -ctext -cnumber=1 2813
|
||||
|
||||
|
||||
=head1 REQUIRED ARGUMENTS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<shownumber>
|
||||
|
||||
The script must be provided with a single show number to operate on.
|
||||
|
||||
=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<-[no]title>
|
||||
|
||||
This option, if given (as B<-title>) indicates that the 'title' field of the
|
||||
row for the selected episode is to be edited. The Vim editor is invoked to
|
||||
make changes. The default is B<-notitle> meaning that this field is not to be
|
||||
edited.
|
||||
|
||||
=item B<-[no]summary>
|
||||
|
||||
This option, if given (as B<-summary>) indicates that the 'summary' field of the
|
||||
row for the selected episode is to be edited. The Vim editor is invoked to
|
||||
make changes. The default is B<-nosummary> meaning that this field is not to be
|
||||
edited.
|
||||
|
||||
=item B<-[no]tags>
|
||||
|
||||
This option, if given (as B<-tags>) indicates that the 'tags' field of the
|
||||
row for the selected episode is to be edited. The Vim editor is invoked to
|
||||
make changes. The default is B<-notags> meaning that this field is not to be
|
||||
edited.
|
||||
|
||||
=item B<-[no]notes>
|
||||
|
||||
This option, if given (as B<-notes>) indicates that the 'notes' field of the
|
||||
row for the selected episode is to be edited. The Vim editor is invoked to
|
||||
make changes. The default is B<-nonotes> meaning that this field is not to be
|
||||
edited.
|
||||
|
||||
=item B<-[no]ctitle>
|
||||
|
||||
This option, if given (as B<-ctitle>) indicates that the 'title' field of the
|
||||
row for the selected comment is to be edited. The Vim editor is invoked to
|
||||
make changes. The default is B<-noctitle> meaning that this field is not to be
|
||||
edited.
|
||||
|
||||
=item B<-[no]ctext>
|
||||
|
||||
This option, if given (as B<-ctext>) indicates that the 'comment_text' field
|
||||
of the row for the selected comment is to be edited. The Vim editor is invoked
|
||||
to make changes. The default is B<-noctext> meaning that this field is not to
|
||||
be edited.
|
||||
|
||||
=item B<-cnumber=N>
|
||||
|
||||
If comment fields are being edited then a comment index is required. The
|
||||
comments are numbered starting from 1 and are sorted in the submission
|
||||
timestamp order. This number must be in the range 1..N where I<N> is the
|
||||
number of comments on this particular show.
|
||||
|
||||
=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
|
||||
|
||||
At least one of the options B<-title>, B<-summary>, B<-tags> and B<-notes>
|
||||
must be provided otherwise the script will abort with an error.
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
The script B<edit_episode> provides an editor interface to certain fields in
|
||||
the HPR database. The fields are:
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<title>
|
||||
|
||||
A single line of up to 100 characters of text. The line is rendered as an
|
||||
"<h1>" tag on the web page and is incorporated into the RSS feed, so it
|
||||
must only contain characters legal in these contexts.
|
||||
|
||||
=item B<summary>
|
||||
|
||||
A single line of up to 100 characters of text. The line is rendered as an
|
||||
"<h3>" tag on the web page and is incorporated into the RSS feed, so it
|
||||
must only contain characters legal in these contexts.
|
||||
|
||||
=item B<tags>
|
||||
|
||||
A single line of up to 200 characters of text. The field holds tags
|
||||
relevant to the content of the episode in CSV format.
|
||||
|
||||
=item B<notes>
|
||||
|
||||
A block of HTML which is to be included inside "<article>" tags making up
|
||||
the show notes on the web page for the episode.
|
||||
|
||||
=item B<comment_title>
|
||||
|
||||
A single line of text. The title is stored in a 'text' field in the
|
||||
database and could be of any length, but will not be rendered correctly
|
||||
if it exceeds 100 characters.
|
||||
|
||||
=item B<comment_text>
|
||||
|
||||
A block of text (NOT HTML) which is the body of the comment. There are no
|
||||
limit contraints here although the code that initiaslly accepts a comment
|
||||
does impose a limit. Thus it would be unwise to make this field too large.
|
||||
|
||||
=back
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<Select one of -title, -summary, -tags, -notes, -ctitle and -ctext>
|
||||
|
||||
At least one of these options is required. This a fatal error.
|
||||
|
||||
=item B<Needs a comment number (-cnumber=N)>
|
||||
|
||||
If one of B<-ctitle> and B<-ctext> is provided then a comment number is needed.
|
||||
|
||||
=item B<Specify the show number>
|
||||
|
||||
The show number has been omitted. This 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<Edit failed>
|
||||
|
||||
If the Vim edit session fails in some way the script reports it this way.
|
||||
|
||||
=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. This configuration file can be overridden using
|
||||
the B<-config=FILE> option as described above.
|
||||
|
||||
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::Slurp
|
||||
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) 2015-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]
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
648
Database/edit_host
Executable file
648
Database/edit_host
Executable file
@@ -0,0 +1,648 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: edit_host
|
||||
#
|
||||
# USAGE: ./edit_host [-help] [-debug=N] [-[no]update] [-config=FILE]
|
||||
# [-[no]espeak_name] [-hostid=id] [-[no]regex] host_name
|
||||
#
|
||||
# DESCRIPTION: A simple editor for the HPR hosts table
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: DBI::MariaDB was here for a while then reverted to DBI::mysql.
|
||||
# Had to revert this script onn 2023-01-22.
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.7
|
||||
# CREATED: 2018-04-07 22:05:06
|
||||
# REVISION: 2023-01-22 14:06:48
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
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.7';
|
||||
|
||||
#
|
||||
# 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 ( $host_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 $email = ( defined( $options{'email'} ) ? $options{'email'} : 0 );
|
||||
my $profile = ( defined( $options{'profile'} ) ? $options{'profile'} : 0 );
|
||||
my $espeak_name
|
||||
= ( defined( $options{'espeak_name'} ) ? $options{'espeak_name'} : 0 );
|
||||
|
||||
#
|
||||
# There must be at least one field to change
|
||||
#
|
||||
die "Select one of -email, -profile and -espeak_name\n"
|
||||
unless ( $email || $profile || $espeak_name );
|
||||
|
||||
#
|
||||
# Was a host id provided (through an option)?
|
||||
#
|
||||
my $hostid = $options{'hostid'};
|
||||
|
||||
#
|
||||
# Deal with the two routes: one via the unique host id, and the other vai the
|
||||
# less unique host name
|
||||
#
|
||||
unless ($hostid) {
|
||||
#
|
||||
# Get the arg
|
||||
#
|
||||
$host_name = shift;
|
||||
pod2usage( -msg => "Specify the host name\n", -exitval => 1 )
|
||||
unless $host_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};
|
||||
|
||||
#$dbh = DBI->connect( "DBI:MariaDB:database=$dbname;host=$dbhost;port=$dbport",
|
||||
# $dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
# or die $DBI::errstr;
|
||||
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Prepare to read the database either for the hostid or the exact or
|
||||
# approximate name
|
||||
#
|
||||
if ($hostid) {
|
||||
#
|
||||
# Simple hostid query
|
||||
#
|
||||
$sth1 = $dbh->prepare(q{SELECT * FROM hosts WHERE hostid = ?});
|
||||
$sth1->execute($hostid);
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
#
|
||||
# Host name query
|
||||
#
|
||||
if ($regexp) {
|
||||
#
|
||||
# Regexp match requested. Count how many matches there are
|
||||
#
|
||||
$sth1 = $dbh->prepare(
|
||||
q{SELECT count(*) AS count FROM hosts WHERE host REGEXP ?});
|
||||
$sth1->execute($host_name);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
if ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
die "Too many matches to regex $host_name\n"
|
||||
unless $h1->{count} == 1;
|
||||
}
|
||||
else {
|
||||
die "Unable to find host matching regex $host_name\n";
|
||||
}
|
||||
|
||||
$sth1 = $dbh->prepare(q{SELECT * FROM hosts WHERE host REGEXP ?});
|
||||
}
|
||||
else {
|
||||
$sth1 = $dbh->prepare(q{SELECT * FROM hosts WHERE host = ?});
|
||||
}
|
||||
|
||||
#
|
||||
# Execute the query
|
||||
#
|
||||
$sth1->execute($host_name);
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Did we find it?
|
||||
#
|
||||
if ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
#
|
||||
# Found. Save the hostid to simplify the update if we don't already have
|
||||
# it
|
||||
#
|
||||
$hostid //= $h1->{hostid};
|
||||
|
||||
#
|
||||
# So what needs changing?
|
||||
#
|
||||
#<<< [perltidy messes up the following]
|
||||
if ($email) {
|
||||
$changes{email} = check_field( 'email',
|
||||
scalar( run_editor( $h1->{email}, ['+set paste'] ) ), 256, qr{(\n)} );
|
||||
}
|
||||
if ($profile) {
|
||||
$changes{profile} = run_editor( $h1->{profile}, ['+set paste'] );
|
||||
}
|
||||
if ($espeak_name) {
|
||||
$changes{espeak_name} = check_field( 'espeak_name',
|
||||
scalar( run_editor( $h1->{espeak_name} ) ), 256, qr{(\n)} );
|
||||
}
|
||||
#>>>
|
||||
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 = ( hostid => $hostid );
|
||||
my ( $stmt, @bind ) = $sql->update( 'hosts', \%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";
|
||||
}
|
||||
else {
|
||||
print "Database not updated due to error\n";
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
print "There was nothing to do\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "Database not updated\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($hostid) {
|
||||
print "Unable to find host number $hostid\n";
|
||||
}
|
||||
else {
|
||||
print "Unable to find host name $host_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",
|
||||
"hostid=i", "regexp", "email!", "profile!",
|
||||
"espeak_name!",
|
||||
);
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "Version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
=head1 NAME
|
||||
|
||||
edit_host - edit one or more fields in the database for a given host
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to edit_host version 0.0.7
|
||||
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
edit_host [-h] [-debug=N] [-[no]updatedb] [-hostid=ID] [-regexp]
|
||||
[-[no]email] [-[no]profile] [-[no]espeak_name] [-config=FILE] hostname
|
||||
|
||||
edit_host -updatedb -espeak_name operat0r
|
||||
edit_host -updatedb -espeak_name -regexp oper
|
||||
edit_host -updatedb -email -espeak -host=225
|
||||
|
||||
|
||||
=head1 REQUIRED ARGUMENTS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<hostname>
|
||||
|
||||
Unless the B<-hostid=ID> option is given (see the OPTIONS section) it is
|
||||
necessary to provide a host name.
|
||||
|
||||
Unless the B<-regexp> option is provided (see the OPTIONS section) the
|
||||
hostname 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<hostname> 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<-hostid=ID>
|
||||
|
||||
The host can be specified by the host ID through this route. If this used then
|
||||
the B<hostname> argument is not required (and is ignored if given).
|
||||
|
||||
=item B<-[no]email>
|
||||
|
||||
This option, if given (as B<-email>), indicates that the 'email' field is to
|
||||
be edited. The Vim editor is invoked to make changes. The default is
|
||||
B<-noemail> meaning that this field is not to be edited.
|
||||
|
||||
=item B<-[no]profile>
|
||||
|
||||
This option, if given (as B<-profile>), indicates that the 'profile' field is
|
||||
to be edited. The Vim editor is invoked to make changes. The default is
|
||||
B<-noprofile> meaning that this field is not to be edited.
|
||||
|
||||
=item B<-[no]espeak_name>
|
||||
|
||||
This option, if given (as B<-espeak_name>), indicates that the 'espeak_name'
|
||||
field is to be edited. The Vim editor is invoked to make changes. The default
|
||||
is B<-noespeak_name> meaning that this field is not to be edited.
|
||||
|
||||
=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_host> provides an editor interface to certain fields in
|
||||
the B<hosts> table of the HPR database. The fields are:
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<email>
|
||||
|
||||
A single line of up to 256 characters of text. The line is stored in a 'TEXT'
|
||||
field but it makes no sense to make it too long even though an email address
|
||||
can be arbitrarily long. The current maximum address length in the database is
|
||||
44 characters.
|
||||
|
||||
=item B<profile>
|
||||
|
||||
A multi-line line of text of arbitrary length and content. The contents are
|
||||
the host's profile in optional HTML format to be displayed on the page which
|
||||
lists all of their contributions to HPR.
|
||||
|
||||
=item B<espeak_name>
|
||||
|
||||
A single line of up to 256 characters of text. The line is stored in a 'TEXT'
|
||||
field but it makes no sense to make it too long. Its purpose is to provide the
|
||||
'espeak' program with a form of the host name (or alias) which can be spoken
|
||||
as the host requires. For example the host 'thelovebug' finds his name spoken
|
||||
as "thel ove bug" and this can be corrected by storing 'TheLoveBug' in this
|
||||
field.
|
||||
|
||||
=back
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<Select one of -email, -profile and -espeak_name>
|
||||
|
||||
At least one of these options is required. This a fatal error.
|
||||
|
||||
=item B<Specify the host name>
|
||||
|
||||
If no host name has been provided, and the B<-hostid=ID> option has not been
|
||||
used the script is unable to determine the host 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 hostname has been provided that matches too many hosts in
|
||||
the database. Try again with a less ambiguous name. This a fatal error.
|
||||
|
||||
=item B<Unable to find host matching regex ...>
|
||||
|
||||
In B<-regex> mode a hostname has been provided that matches no hosts 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) 2018 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
|
||||
|
666
Database/edit_series
Executable file
666
Database/edit_series
Executable file
@@ -0,0 +1,666 @@
|
||||
#!/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
|
||||
|
293
Database/edit_tsu_blank
Executable file
293
Database/edit_tsu_blank
Executable file
@@ -0,0 +1,293 @@
|
||||
#!/bin/bash -
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: edit_tsu_blank
|
||||
#
|
||||
# USAGE: ./edit_tsu_blank
|
||||
#
|
||||
# DESCRIPTION: Edit a template for generating a tag and summary update email.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: Now obsolete but retained for reference
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.11
|
||||
# CREATED: 2016-06-16 10:58:32
|
||||
# REVISION: 2021-09-16 12:51:18
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
set -o nounset # Treat unset variables as an error
|
||||
|
||||
SCRIPT=${0##*/}
|
||||
|
||||
VERSION="0.0.11"
|
||||
|
||||
#
|
||||
# Load library functions
|
||||
#
|
||||
LIB="$HOME/bin/function_lib.sh"
|
||||
[ -e "$LIB" ] || { echo "$SCRIPT: Unable to source functions"; exit 1; }
|
||||
# shellcheck source=/home/cendjm/bin/function_lib.sh
|
||||
source "$LIB"
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: find_work
|
||||
# DESCRIPTION: Using 'grep' to count the number of un-edited lines in certain
|
||||
# files make a list of their names and the number of edits for
|
||||
# display in a 'select'.
|
||||
# PARAMETERS: 1 - the name of the directory holding the files
|
||||
# 2 - the prefix of each file to identify them exactly
|
||||
# 3 - the name of an array to hold the list
|
||||
# RETURNS: Nothing (uses a nameref argument)
|
||||
#===============================================================================
|
||||
find_work () {
|
||||
local bd="${1:?Usage: find_work basedir prefix array}"
|
||||
local pf="${2:?Usage: find_work basedir prefix array}"
|
||||
local -n result="${3:?Usage: find_work basedir prefix array}"
|
||||
local -a work
|
||||
local elem count
|
||||
|
||||
# Load all filenames into an array
|
||||
mapfile -t work < <(grep -E -c "^(summary|tags): *$" "$bd/$pf"*[^~])
|
||||
|
||||
# Add names containing work to the result array
|
||||
for elem in "${work[@]}"; do
|
||||
count="${elem##*:}"
|
||||
if [[ $count -gt 0 ]]; then
|
||||
printf -v count '%2d' "$count"
|
||||
# Add colour and a reset for the yellow to be added later
|
||||
result+=( "${elem%%:*}${reset} (${red}$count${reset} edits)" )
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
#
|
||||
# Check arguments
|
||||
#
|
||||
if [[ $# -ne 0 ]]; then
|
||||
echo "($SCRIPT Version $VERSION)"
|
||||
echo "Usage: $SCRIPT"
|
||||
exit
|
||||
fi
|
||||
|
||||
#
|
||||
# Directories and files
|
||||
#
|
||||
BASEDIR="$HOME/HPR/Database"
|
||||
TSU="$BASEDIR/tsu"
|
||||
VALIDATOR="$BASEDIR/validate_edits.awk"
|
||||
STATUSFILE="$BASEDIR/tag_summary_actions.csv"
|
||||
|
||||
PREFIX="tag_summary_updates_"
|
||||
|
||||
#
|
||||
# Sanity checks
|
||||
#
|
||||
[ -d "$BASEDIR" ] || { echo "Unable to find directory $BASEDIR"; exit 1; }
|
||||
[ -d "$TSU" ] || { echo "Unable to find directory $TSU"; exit 1; }
|
||||
|
||||
[ -e "$VALIDATOR" ] || { echo "File $VALIDATOR not found"; exit 1; }
|
||||
[ -e "$STATUSFILE" ] || { echo "File $STATUSFILE not found"; exit 1; }
|
||||
|
||||
#
|
||||
# Colour codes
|
||||
#
|
||||
define_colours
|
||||
|
||||
#
|
||||
# Using a function find which files have un-edited parts and save their names
|
||||
# and the number of edits in an array
|
||||
#
|
||||
declare -a choices
|
||||
find_work "$TSU" "${PREFIX}" choices
|
||||
|
||||
#
|
||||
# There could be no files with edits
|
||||
#
|
||||
if [[ ${#choices[@]} -eq 0 ]]; then
|
||||
echo "${red}There are no files in need of editing!${reset}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Prompt for a choice from the array of files, removing the path from each
|
||||
# choice for readability. (Sadly 'select' changed its behaviour in Bash
|
||||
# 5 necessitating this). Add a yellow colour code before the resulting
|
||||
# filename; there's already a reset after the name.
|
||||
#
|
||||
PS3="Enter a number: "
|
||||
echo "Files in need of editing:"
|
||||
# select choice in "${choices[@]##*/}"
|
||||
select choice in "${choices[@]/${TSU}\//${yellow}}"
|
||||
do
|
||||
break
|
||||
done
|
||||
|
||||
retval=$?
|
||||
if [[ $retval -ne 0 ]]; then
|
||||
echo "${red}Selection aborted${reset}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Since we removed the full path in the 'select' list 'choice' contains that
|
||||
# name rather than the file path, so we need to make it such a path. We also
|
||||
# need to remove the string "(X edits)" from the end, and all the colour codes
|
||||
# we added earlier, then edit the file.
|
||||
#
|
||||
# We include Vim settings for the text width and filetype, and perform
|
||||
# a search for the next field that needs work (using 'silent!' to stop nasty
|
||||
# error messages if there's nothing that matches).
|
||||
#
|
||||
# We use an Awk script to determine if the file contains any shows which have
|
||||
# already been updated by another person. We use the file $STATUSFILE which
|
||||
# gets updated every time a report is generated.
|
||||
#
|
||||
|
||||
# Strip colour codes
|
||||
choice="${choice//$yellow/}"
|
||||
choice="${choice//$red/}"
|
||||
choice="${choice//$reset/}"
|
||||
|
||||
#
|
||||
# Run the Awk validator, and if OK edit the file, otherwise try to explain
|
||||
# what's wrong.
|
||||
#
|
||||
choice="$TSU/${choice%% *}"
|
||||
if awk --assign "csv=$STATUSFILE" -f "$VALIDATOR" "$choice"; then
|
||||
csum1=$(md5sum < "$choice")
|
||||
echo "Editing $choice"
|
||||
vim +"set tw=100 ft=text" -c 'silent! /^\(summary\|tags\):\s*$' "$choice"
|
||||
csum2=$(md5sum < "$choice")
|
||||
|
||||
if [[ $csum1 == "$csum2" ]]; then
|
||||
echo "${yellow}No change was made${reset}"
|
||||
exit
|
||||
fi
|
||||
else
|
||||
echo "${red}Errors found checking the file${reset}"
|
||||
echo "Show(s) in this file have already been updated in the database."
|
||||
echo "Somebody else has probably sent in an update for show(s) in the range."
|
||||
echo "The file ${yellow}${choice}${reset}"
|
||||
echo "has been edited automatically to comment out the updated show(s) and"
|
||||
echo "is now ready for editing in the usual way (rerun this script to do it)."
|
||||
echo "----"
|
||||
echo "(This error may also be caused by an internal fault when running"
|
||||
echo "awk. Check the file to be certain.)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Perform a check on what is now in the file looking for lines that are too
|
||||
# long or with a bad show number
|
||||
#
|
||||
echo "${yellow}Checking show numbers and lengths of summaries and tags${reset}"
|
||||
re="^([A-Za-z]+): *(.*) *$"
|
||||
count=0; errors=0
|
||||
while read -r line; do
|
||||
((count++))
|
||||
if [[ $line =~ $re ]]; then
|
||||
key="${BASH_REMATCH[1]}"
|
||||
value="${BASH_REMATCH[2]}"
|
||||
case $key in
|
||||
show)
|
||||
if [[ ! $value =~ [0-9]{1,4} ]]; then
|
||||
((errors++))
|
||||
printf '**Error**\n%02d: %s\n' "$count" "$line"
|
||||
echo "${blue}The show value must be a number (${#value})${reset}"
|
||||
fi
|
||||
;;
|
||||
|
||||
summary)
|
||||
if [[ ${#value} -gt 100 ]]; then
|
||||
((errors++))
|
||||
printf '**Error**\n%02d: %s\n' "$count" "$line"
|
||||
echo "${blue}Value too long (${#value}, should be 100 max)${reset}"
|
||||
fi
|
||||
;;
|
||||
|
||||
tags)
|
||||
if [[ ${#value} -gt 200 ]]; then
|
||||
((errors++))
|
||||
printf '**Error**\n%02d: %s\n' "$count" "$line"
|
||||
echo "${blue}Value too long (${#value}, should be 200 max)${reset}"
|
||||
fi
|
||||
;;
|
||||
|
||||
esac
|
||||
fi
|
||||
done < "$choice"
|
||||
|
||||
#
|
||||
# Report a summary of the check
|
||||
#
|
||||
if [[ $errors -eq 0 ]]; then
|
||||
echo "${green}No errors found${reset}"
|
||||
else
|
||||
echo "${red}Found $errors errors${reset}"
|
||||
fi
|
||||
|
||||
#
|
||||
# Make temporary files and set traps to delete them
|
||||
#
|
||||
TMP1=$(mktemp) || {
|
||||
echo "$SCRIPT: ${red}creation of temporary file failed!${reset}"
|
||||
exit 1
|
||||
}
|
||||
trap 'cleanup_temp $TMP1' SIGHUP SIGINT SIGPIPE SIGTERM EXIT
|
||||
|
||||
#
|
||||
# Make a temporary Awk script
|
||||
#
|
||||
cat > "$TMP1" <<'ENDAWK'
|
||||
BEGIN {
|
||||
shows = total = finished = todo = 0
|
||||
}
|
||||
|
||||
/^show:/ { shows++ }
|
||||
/^tags:\s*$/ { total++; todo++ }
|
||||
/^tags:\s*\S+/ { total++; finished++ }
|
||||
/^summary:\s*$/ { total++; todo++ }
|
||||
/^summary:\s*\S+/ { total++; finished++ }
|
||||
|
||||
END {
|
||||
printf "shows=%d\n",shows
|
||||
printf "total=%d\n",total
|
||||
printf "finished=%d\n",finished
|
||||
printf "todo=%d\n",todo
|
||||
printf "left=%2.1f%%\n",(todo/total)*100
|
||||
}
|
||||
ENDAWK
|
||||
|
||||
#
|
||||
# Run the Awk script and make variables
|
||||
#
|
||||
declare shows total todo finished left
|
||||
eval "$(awk -f "$TMP1" "$choice")"
|
||||
|
||||
# todo="$(grep -E -c "^(summary|tags):\s*$" "$choice")"
|
||||
# total="$(grep -E -c "^(summary|tags):" "$choice")"
|
||||
# completed="$(grep -E -c "^(summary|tags): *\w+" "$choice")"
|
||||
|
||||
#
|
||||
# Is there still work to do on this file?
|
||||
#
|
||||
echo "${yellow}File statistics:${reset}"
|
||||
printf '%s%-19s %s%s\n' "${purple}" "Total shows:" "$shows" "${reset}"
|
||||
printf '%s%-19s %s%s\n' "${purple}" "Additions required:" "$total" "${reset}"
|
||||
printf '%s%-19s %s%s\n' "${purple}" "Already done:" "$finished" "${reset}"
|
||||
printf '%s%-19s %s%s\n' "${purple}" "Percent left:" "$left" "${reset}"
|
||||
case $todo in
|
||||
0) echo "${green}All required work on this file has been done${reset}";;
|
||||
1) echo "${red}There is still $todo tag/summary to add${reset}";;
|
||||
*) echo "${red}There are still $todo tags/summaries to add${reset}"
|
||||
esac
|
||||
|
||||
exit
|
||||
|
||||
# vim: syntax=sh:ts=8:sw=4:ai:et:tw=78:fo=tcrqn21
|
||||
|
425
Database/find_double_hosts
Executable file
425
Database/find_double_hosts
Executable file
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: find_double_hosts
|
||||
#
|
||||
# USAGE: ./find_double_hosts
|
||||
#
|
||||
# DESCRIPTION: Find HPR shows with two hosts (host is "A and B"), find the
|
||||
# hosts if possible and flag updates to the database to
|
||||
# represent the dual nature.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.2
|
||||
# CREATED: 2017-10-13 19:17:51
|
||||
# REVISION: 2017-10-13 19:19:43
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Config::General;
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.2';
|
||||
|
||||
#
|
||||
# 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 $configfile1 = "$basedir/.hpr_db.cfg";
|
||||
my $configfile2 = "$basedir/.hpr_pg.cfg";
|
||||
|
||||
my $email_template = 'host_%s@hackerpublicradio.org';
|
||||
my $default_licence = 'CC-BY-SA';
|
||||
|
||||
my ( $dbh1, $dbh2, $sth1, $h1, $rv1, $sth2, $h2, $rv2, $sth3, $h3, $rv3, $sth4, $h4, $rv4 );
|
||||
|
||||
my ( %doubles, @h, %hosts, $unknown, $default_email );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load database configuration data
|
||||
#
|
||||
my $conf1 = Config::General->new(
|
||||
-ConfigFile => $configfile1,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1
|
||||
);
|
||||
my %config1 = $conf1->getall();
|
||||
|
||||
my $conf2 = Config::General->new(
|
||||
-ConfigFile => $configfile2,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1
|
||||
);
|
||||
my %config2 = $conf2->getall();
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Connect to the MariaDB database
|
||||
#-------------------------------------------------------------------------------
|
||||
my $dbtype1 = $config1{database}->{type} // 'mysql';
|
||||
my $dbhost1 = $config1{database}->{host} // '127.0.0.1';
|
||||
my $dbport1 = $config1{database}->{port} // 3306;
|
||||
my $dbname1 = $config1{database}->{name};
|
||||
my $dbuser1 = $config1{database}->{user};
|
||||
my $dbpwd1 = $config1{database}->{password};
|
||||
$dbh1
|
||||
= DBI->connect( "dbi:$dbtype1:host=$dbhost1;port=$dbport1;database=$dbname1",
|
||||
$dbuser1, $dbpwd1, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh1->{mysql_enable_utf8} = 1;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Connect to the PostgreSQL database
|
||||
#-------------------------------------------------------------------------------
|
||||
my $dbtype2 = $config2{database}->{type} // 'Pg';
|
||||
my $dbhost2 = $config2{database}->{host} // '127.0.0.1';
|
||||
my $dbport2 = $config2{database}->{port} // 5432;
|
||||
my $dbname2 = $config2{database}->{name};
|
||||
my $dbuser2 = $config2{database}->{user};
|
||||
my $dbpwd2 = $config2{database}->{password};
|
||||
$dbh2 = DBI->connect( "dbi:$dbtype2:host=$dbhost2;database=$dbname2;port=$dbport2",
|
||||
$dbuser2, $dbpwd2, { PrintError => 0, AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh2->{pg_enable_utf8} = 1;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Query preparation
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# MariaDB query to find double hosts
|
||||
#
|
||||
my $sql1 = q{
|
||||
SELECT hostid, host FROM hosts
|
||||
WHERE host regexp '[[:<:]]and[[:>:]]'
|
||||
ORDER BY hostid
|
||||
};
|
||||
|
||||
$sth1 = $dbh1->prepare($sql1) or die $DBI::errstr;
|
||||
if ( $dbh1->err ) {
|
||||
warn $dbh1->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# MariaDB query to find the host by name
|
||||
#
|
||||
$sth2 = $dbh1->prepare(q{SELECT hostid FROM hosts WHERE host REGEXP ?})
|
||||
or die $DBI::errstr;
|
||||
if ( $dbh1->err ) {
|
||||
warn $dbh1->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# PostgreSQL query to register an unknown host
|
||||
#
|
||||
$sth3
|
||||
= $dbh2->prepare(q{INSERT INTO hosts (host,email,license) VALUES (?,?,?)})
|
||||
or die $DBI::errstr;
|
||||
if ( $dbh2->err ) {
|
||||
warn $dbh2->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# PostgreSQL query to find shows with particular host ids
|
||||
#
|
||||
$sth4 = $dbh2->prepare(
|
||||
q{
|
||||
SELECT e.id AS eps_id
|
||||
FROM episodes e
|
||||
JOIN episodes_hosts_xref eh ON (e.id = eh.episodes_id)
|
||||
JOIN hosts h ON (h.id = eh.hosts_id)
|
||||
WHERE h.id = ?
|
||||
}
|
||||
) or die $DBI::errstr;
|
||||
if ( $dbh2->err ) {
|
||||
warn $dbh2->errstr;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Find all the "double hosts"
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Query MariaDB for the target hosts
|
||||
#
|
||||
$sth1->execute;
|
||||
if ( $dbh1->err ) {
|
||||
die $dbh1->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Loop through the list of double hostnames and parse them out. Save the
|
||||
# originals in the %doubles hash and the parsed names in the %hosts hash.
|
||||
#
|
||||
while ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
#
|
||||
# Each hash value is a hash containing the original id, and, in a sub-hash
|
||||
# the replacement ids
|
||||
#
|
||||
$doubles{$h1->{host}} = {
|
||||
double => $h1->{hostid},
|
||||
singles => {},
|
||||
};
|
||||
|
||||
#
|
||||
# Parse the double host string
|
||||
#
|
||||
@h = ( $h1->{host} =~ /^(.+)\s+and\s+(.+)$/ );
|
||||
|
||||
printf "%-4d %s", $h1->{hostid}, $h1->{host};
|
||||
print " [", join( ",", @h ), "]\n";
|
||||
|
||||
#
|
||||
# Initialise the entries for %doubles and %hosts
|
||||
#
|
||||
for my $host (@h) {
|
||||
$doubles{$h1->{host}}->{singles}->{$host} = undef;
|
||||
unless ( exists( $hosts{$host} ) ) {
|
||||
$hosts{$host} = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print '-' x 80,"\n";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Find the single hosts in the 'hosts' table
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Scan the list of individual hosts and find them in the 'hosts' table
|
||||
#
|
||||
$unknown = 0;
|
||||
foreach my $host ( sort(keys(%hosts)) ) {
|
||||
$rv2 = $sth2->execute("^$host\$");
|
||||
if ( $dbh1->err ) {
|
||||
die $dbh1->errstr;
|
||||
}
|
||||
$rv2 = 0 if ( $rv2 eq '0E0' );
|
||||
|
||||
if ($rv2) {
|
||||
$h2 = $sth2->fetchrow_hashref;
|
||||
print "Found id for $host: ", $h2->{hostid}, "\n";
|
||||
$hosts{$host} = $h2->{hostid};
|
||||
save_hostid(\%doubles,$host,$h2->{hostid});
|
||||
}
|
||||
else {
|
||||
print "Can't find $host\n";
|
||||
$unknown++;
|
||||
}
|
||||
}
|
||||
|
||||
#print Dumper(\%hosts),"\n";
|
||||
|
||||
print '-' x 80,"\n";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Allocate all unknown hosts a host id in the PostgreSQL database, and give an
|
||||
# unique email address.
|
||||
#-------------------------------------------------------------------------------
|
||||
if ( $unknown > 0 ) {
|
||||
print "Registering $unknown hosts\n";
|
||||
|
||||
foreach my $host ( sort( keys(%hosts) ) ) {
|
||||
if ( $hosts{$host} == 0 ) {
|
||||
$rv3 = $sth3->execute( $host, undef, $default_licence );
|
||||
if ( $dbh2->err ) {
|
||||
die $dbh2->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Write a row to the 'hosts' table and save the id number
|
||||
# generated
|
||||
#
|
||||
my $newid = $dbh2->last_insert_id( undef, undef, undef, undef,
|
||||
{ sequence => 'host_seq' } );
|
||||
$hosts{$host} = $newid;
|
||||
save_hostid(\%doubles,$host,$newid);
|
||||
print "Host $host added with id $newid\n";
|
||||
|
||||
#
|
||||
# Give the new host entry a default email address
|
||||
#
|
||||
$default_email = sprintf($email_template,$newid);
|
||||
$rv3 = $dbh2->do( 'UPDATE hosts SET email = ? WHERE id = ?',
|
||||
undef, $default_email, $newid );
|
||||
if ( $dbh2->err ) {
|
||||
warn $dbh2->errstr;
|
||||
}
|
||||
$rv3 = 0 if ( $rv3 eq '0E0' );
|
||||
|
||||
warn "Failed to set email address $default_email for $host\n"
|
||||
unless ( defined($rv3) );
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print '-' x 80,"\n";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Now %doubles contains all the original names and host ids and %hosts
|
||||
# contains the parsed out names and their ids. We can look for shows
|
||||
# attributed to the first set and re-attribute them to the second set.
|
||||
#-------------------------------------------------------------------------------
|
||||
print "Changing host associations for shows with two hosts\n";
|
||||
foreach my $double ( sort( keys(%doubles) ) ) {
|
||||
print "Processing $double\n";
|
||||
my ( $doubleid, @newids ) = (
|
||||
$doubles{$double}->{double},
|
||||
values( %{ $doubles{$double}->{singles} } )
|
||||
);
|
||||
print " Original id: $doubleid\n";
|
||||
print " Replacements: ", join( ", ", @newids ), "\n";
|
||||
|
||||
#
|
||||
# Find shows marked as belonging to this double-host
|
||||
#
|
||||
$sth4->execute($doubleid);
|
||||
if ( $dbh2->err ) {
|
||||
die $dbh2->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Process all the shows
|
||||
#
|
||||
while ( $h4 = $sth4->fetchrow_hashref ) {
|
||||
my $eps_id = $h4->{eps_id};
|
||||
print " Show $eps_id is ascribed to host $doubleid\n";
|
||||
|
||||
$dbh2->begin_work();
|
||||
|
||||
#
|
||||
# Delete the xref link for the double host
|
||||
#
|
||||
$rv4
|
||||
= $dbh2->do(
|
||||
'DELETE FROM episodes_hosts_xref WHERE episodes_id = ?',
|
||||
undef, $eps_id );
|
||||
if ( $dbh2->err ) {
|
||||
warn $dbh2->errstr;
|
||||
}
|
||||
$rv4 = 0 if ( $rv4 eq '0E0' );
|
||||
|
||||
if ( defined($rv4) ) {
|
||||
print " Deleted entry from episodes_hosts_xref for $eps_id\n";
|
||||
}
|
||||
else {
|
||||
warn "Problem deleting from episodes_hosts_xref for $eps_id\n";
|
||||
}
|
||||
|
||||
#
|
||||
# Add links for the single hosts
|
||||
#
|
||||
foreach my $hid (@newids) {
|
||||
$rv4 = $dbh2->do( 'INSERT INTO episodes_hosts_xref VALUES (?,?)',
|
||||
undef, $eps_id, $hid );
|
||||
if ( $dbh2->err ) {
|
||||
warn $dbh2->errstr;
|
||||
}
|
||||
$rv4 = 0 if ( $rv4 eq '0E0' );
|
||||
|
||||
if ( defined($rv4) ) {
|
||||
print " Added entry to episodes_hosts_xref values ",
|
||||
"$eps_id,$hid\n";
|
||||
}
|
||||
else {
|
||||
warn "Problem adding to episodes_hosts_xref values "
|
||||
. "$eps_id,$hid\n";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#
|
||||
# Commit the delete/inserts above
|
||||
#
|
||||
$dbh2->commit();
|
||||
|
||||
}
|
||||
print '~' x 80, "\n";
|
||||
|
||||
#
|
||||
# Delete the double host (NOTE: This will fail due to referential
|
||||
# integrity if the DELETE above failed, so there is scope for debris to be
|
||||
# left around)
|
||||
#
|
||||
$rv4 = $dbh2->do( 'DELETE FROM hosts WHERE id = ?', undef, $doubleid );
|
||||
if ( $dbh2->err ) {
|
||||
warn $dbh2->errstr;
|
||||
}
|
||||
$rv4 = 0 if ( $rv4 eq '0E0' );
|
||||
|
||||
if ( defined($rv4) ) {
|
||||
print " Deleted entry from hosts for id $doubleid ($double)\n";
|
||||
}
|
||||
else {
|
||||
warn "Problem deleting from hosts for id $doubleid ($double)\n";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
print '-' x 80,"\n";
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: save_hostid
|
||||
# PURPOSE: Saves the host id after searching for the key in the %doubles
|
||||
# hash
|
||||
# PARAMETERS: $doubles hashref to %doubles
|
||||
# $host host key
|
||||
# $hostid host id number
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: Searches the %doubles hash for particular keys in the
|
||||
# 'singles' sub-hash. If found saves the corresponding host id
|
||||
# there.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub save_hostid {
|
||||
my ( $doubles, $host, $hostid ) = @_;
|
||||
|
||||
foreach my $key ( keys(%$doubles) ) {
|
||||
if ( exists( $doubles->{$key}->{singles}->{$host} ) ) {
|
||||
$doubles->{$key}->{singles}->{$host} = $hostid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
397
Database/find_series
Executable file
397
Database/find_series
Executable file
@@ -0,0 +1,397 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: find_series
|
||||
#
|
||||
# USAGE: ./find_series
|
||||
#
|
||||
# DESCRIPTION: Gathers information from the HPR database to assist with the
|
||||
# process of placing episodes into series groups
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.5
|
||||
# CREATED: 2014-04-25 17:11:21
|
||||
# REVISION: 2016-06-15 15:51:55
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use v5.16;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Config::General;
|
||||
use List::MoreUtils qw(uniq);
|
||||
use YAML::XS qw{LoadFile};
|
||||
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.5';
|
||||
|
||||
#
|
||||
# Various constants
|
||||
#
|
||||
( 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";
|
||||
my $ignorefile = "$basedir/.$PROG.yml";
|
||||
|
||||
my $file_template = "${PROG}_%d.out";
|
||||
|
||||
my ( $dbh, $sth1, $h1 );
|
||||
my ( @ignore, $title, @words, $pair );
|
||||
my ( %eps, %tags, @taglist, %single_words, %double_words );
|
||||
my ( $phase, $outfile, $outfh );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load YAML ignore list
|
||||
#
|
||||
my $yaml = LoadFile($ignorefile);
|
||||
@ignore = uniq( @{ $yaml->{ignore} } );
|
||||
|
||||
#
|
||||
# Load database configuration data
|
||||
#
|
||||
my $conf = Config::General->new(
|
||||
-ConfigFile => $configfile,
|
||||
-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};
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#
|
||||
# Prepare to collect episode titles with the series it's related to
|
||||
#
|
||||
$sth1 = $dbh->prepare(
|
||||
q{SELECT
|
||||
eps.id AS eps_id,
|
||||
eps.date,
|
||||
eps.title,
|
||||
eps.duration,
|
||||
eps.summary,
|
||||
eps.notes,
|
||||
eps.hostid,
|
||||
eps.series,
|
||||
eps.explicit,
|
||||
eps.license,
|
||||
eps.tags,
|
||||
eps.version,
|
||||
eps.downloads,
|
||||
eps.valid AS eps_valid,
|
||||
ms.id AS ms_id,
|
||||
ms.name,
|
||||
ms.description,
|
||||
ms.private,
|
||||
ms.image,
|
||||
ms.valid AS ms_valid
|
||||
FROM eps
|
||||
JOIN miniseries ms ON eps.series = ms.id
|
||||
WHERE eps.valid = 1}
|
||||
);
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Generate an output file for phase 1
|
||||
#
|
||||
$phase = 1;
|
||||
newOutput( \$outfh, $file_template, $phase++ );
|
||||
print $outfh "Show (Series) Title\n";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Walk through the episode/series list from the database and build structures
|
||||
# from them.
|
||||
#-------------------------------------------------------------------------------
|
||||
# The structures built are:
|
||||
#
|
||||
# %eps - a hash indexed by the episode number with an arrayref as the
|
||||
# value. The arrayref starts with the episode title and is followed by the
|
||||
# important words from the title in lower case. The term "important" means
|
||||
# that the word must consist of the allowed characters and not be in the
|
||||
# 'ignore' list.
|
||||
#
|
||||
# %tags - a hash indexed by the episode number with a CSV string of tags
|
||||
# associated with the episode as the value.
|
||||
#
|
||||
# %single_words - a hash indexed by one of the words collected from the title
|
||||
# (see %eps above). The value of each element is complex. It consists of an
|
||||
# arrayref, the first element of which is a count of the succeeding elements.
|
||||
# The next N elements are arrayrefs each of which contains two elements: an
|
||||
# episode number and an episode title. Here is an example in Data::Dumper
|
||||
# format:
|
||||
#
|
||||
# 'pre-ibm' => [
|
||||
# 2,
|
||||
# [
|
||||
# 687,
|
||||
# 'pre-IBM PC computer history 1'
|
||||
# ],
|
||||
# [
|
||||
# 691,
|
||||
# 'pre-IBM PC computer history 2'
|
||||
# ]
|
||||
# ],
|
||||
#
|
||||
#-------------------------------------------------------------------------------
|
||||
while ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
#
|
||||
# Report what we found
|
||||
#
|
||||
printf $outfh "%4d: (%-2d) %s\n", $h1->{eps_id}, $h1->{ms_id},
|
||||
$h1->{title};
|
||||
|
||||
#
|
||||
# Skip this episode if it already has a series
|
||||
#
|
||||
next if $h1->{series} > 0;
|
||||
|
||||
#
|
||||
# Save this episode for later
|
||||
#
|
||||
$eps{ $h1->{eps_id} } = [ $h1->{title} ];
|
||||
|
||||
#
|
||||
# Save tag details
|
||||
#
|
||||
$tags{ $h1->{eps_id} } = $h1->{tags};
|
||||
|
||||
#
|
||||
# Strip any trailing full stop, and chop the title into words
|
||||
#
|
||||
( $title = $h1->{title} ) =~ s/\.$//;
|
||||
@words = split( /\s+/, $title );
|
||||
|
||||
#
|
||||
# Remove all unwanted characters and force to lowercase (use 'fc' for case
|
||||
# folding since it's aware of character sets)
|
||||
#
|
||||
foreach my $word (@words) {
|
||||
$word =~ s{[^a-zA-Z./_-]}{}g;
|
||||
$word = fc($word);
|
||||
}
|
||||
|
||||
#
|
||||
# Clean up the word list after removing unwanted characters
|
||||
#
|
||||
@words = grep { $_ !~ /^-*$/ } @words;
|
||||
|
||||
#
|
||||
# Extract pairs of words before they're made unique and make a hash
|
||||
# pointing to the episodes they originated from
|
||||
#
|
||||
for (my $i = 0; $i < $#words; $i++) {
|
||||
$pair = "$words[$i] $words[$i+1]";
|
||||
|
||||
if ( exists( $double_words{$pair} ) ) {
|
||||
$double_words{$pair}->[0] += 1;
|
||||
push( @{ $double_words{$pair} },
|
||||
[ $h1->{eps_id}, $h1->{title} ] );
|
||||
}
|
||||
else {
|
||||
$double_words{$pair} = [ 1, [ $h1->{eps_id}, $h1->{title} ] ];
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Make the word list unique
|
||||
#
|
||||
@words = uniq(@words);
|
||||
|
||||
#
|
||||
# Walk the tidied single word list
|
||||
#
|
||||
foreach my $word (@words) {
|
||||
#
|
||||
# Ignore very short words and words in the ignore list
|
||||
#
|
||||
next if length($word) < 2;
|
||||
next if grep( /^$word$/, @ignore );
|
||||
|
||||
#
|
||||
# Save this word in the episodes hash
|
||||
#
|
||||
push( @{ $eps{ $h1->{eps_id} } }, $word );
|
||||
|
||||
#
|
||||
# If the word is not known initialise the entry containing an arrayref
|
||||
# with a counter and another arrayref with the saved episode number
|
||||
# and title. If it's known, increment the counter and stash the
|
||||
# episode details as another arrayref.
|
||||
#
|
||||
if ( exists( $single_words{$word} ) ) {
|
||||
$single_words{$word}->[0] += 1;
|
||||
push( @{ $single_words{$word} },
|
||||
[ $h1->{eps_id}, $h1->{title} ] );
|
||||
}
|
||||
else {
|
||||
$single_words{$word} = [ 1, [ $h1->{eps_id}, $h1->{title} ] ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# We've finished with the database
|
||||
#
|
||||
$dbh->disconnect;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Done the first pass, prepare for the next
|
||||
#-------------------------------------------------------------------------------
|
||||
newOutput( \$outfh, $file_template, $phase++ );
|
||||
|
||||
#
|
||||
# Process the saved data in increasing order of the frequency. Print the word
|
||||
# and its frequency and follow that by the stashed episode details in the
|
||||
# order we saw them
|
||||
#
|
||||
foreach my $key (
|
||||
sort { $single_words{$a}->[0] <=> $single_words{$b}->[0] }
|
||||
sort( keys(%single_words) )
|
||||
)
|
||||
{
|
||||
if ( $single_words{$key}->[0] > 3 ) {
|
||||
printf $outfh "%15s: %s\n", $key, $single_words{$key}->[0];
|
||||
for ( my $i = 1; $i <= $single_words{$key}->[0]; $i++ ) {
|
||||
printf $outfh "%17s%4d: %s", ' ', @{ $single_words{$key}->[$i] };
|
||||
@taglist
|
||||
= split( /\s*,\s*/, $tags{ $single_words{$key}->[$i]->[0] } );
|
||||
if (@taglist) {
|
||||
print $outfh " [", join( ",", @taglist ), "]\n";
|
||||
}
|
||||
else {
|
||||
print $outfh "\n";
|
||||
}
|
||||
}
|
||||
print $outfh "\n";
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Done the second pass, prepare for the next
|
||||
#-------------------------------------------------------------------------------
|
||||
newOutput( \$outfh, $file_template, $phase++ );
|
||||
|
||||
#
|
||||
# Look through the collected data from the point of view of the episode, list
|
||||
# all the (relevant) words in the title in order and report their frequencies
|
||||
#
|
||||
for my $key ( sort { $a <=> $b } keys(%eps) ) {
|
||||
printf $outfh "%4d: %s\n", $key, $eps{$key}->[0];
|
||||
for ( my $i = 1; $i < scalar( @{ $eps{$key} } ); $i++ ) {
|
||||
my $word = $eps{$key}->[$i];
|
||||
printf $outfh " %15s %d\n", $word, $single_words{$word}->[0];
|
||||
}
|
||||
print $outfh "\n";
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Done the third pass, prepare for the next
|
||||
#-------------------------------------------------------------------------------
|
||||
newOutput( \$outfh, $file_template, $phase++ );
|
||||
|
||||
#
|
||||
# So the pairs of words we collected earlier might show something interesting.
|
||||
# Let's see.
|
||||
#
|
||||
foreach my $key (
|
||||
sort { $double_words{$a}->[0] <=> $double_words{$b}->[0] }
|
||||
sort( keys(%double_words) )
|
||||
)
|
||||
{
|
||||
if ( $double_words{$key}->[0] > 3 ) {
|
||||
printf $outfh "%15s: %s\n", $key, $double_words{$key}->[0];
|
||||
for ( my $i = 1; $i <= $double_words{$key}->[0]; $i++ ) {
|
||||
printf $outfh "%17s%4d: %s", ' ', @{ $double_words{$key}->[$i] };
|
||||
@taglist
|
||||
= split( /\s*,\s*/, $tags{ $double_words{$key}->[$i]->[0] } );
|
||||
if (@taglist) {
|
||||
print $outfh " [", join( ",", @taglist ), "]\n";
|
||||
}
|
||||
else {
|
||||
print $outfh "\n";
|
||||
}
|
||||
}
|
||||
print $outfh "\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
close($outfh);
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: newOutput
|
||||
# PURPOSE: Generate a new output file
|
||||
# PARAMETERS: $fh a scalar ref pointing to a variable to hold a
|
||||
# file handle
|
||||
# $template a string suitable for sprintf for defining the
|
||||
# name of the output file
|
||||
# $phase an integer to be incorporated into the output
|
||||
# file name
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: Closes any existing file and opens a new one with the same
|
||||
# file handle. The name of the file is derived from the template
|
||||
# and the phase number.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub newOutput {
|
||||
my ( $fh, $template, $phase ) = @_;
|
||||
|
||||
if ($$fh) {
|
||||
close($$fh) if ( tell($$fh) > -1 );
|
||||
}
|
||||
|
||||
my $outfile = sprintf( $template, $phase );
|
||||
open( $$fh, '>:encoding(UTF-8)', $outfile )
|
||||
or die "Unable to open $outfile\n";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
708
Database/fix_urls
Executable file
708
Database/fix_urls
Executable file
@@ -0,0 +1,708 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: fix_urls
|
||||
#
|
||||
# USAGE: ./fix_urls [-help] [-doc] [-debug=N] [-dry-run] [-config=FILE]
|
||||
# [-limit=N] tablename
|
||||
#
|
||||
# DESCRIPTION: Scans the HPR database to find URLs which do not have the
|
||||
# 'https:' scheme and correct them.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: Had to revert to MySQL because of a problem with DBD::MariaDB
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.5
|
||||
# CREATED: 2021-12-29 13:57:28
|
||||
# REVISION: 2022-02-28 10:51:27
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use v5.16;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
use feature qw{ postderef say signatures state };
|
||||
no warnings qw{ experimental::postderef experimental::signatures };
|
||||
|
||||
use Getopt::Long;
|
||||
use Pod::Usage;
|
||||
|
||||
use Config::General;
|
||||
use DBI;
|
||||
|
||||
use SQL::Abstract;
|
||||
|
||||
use Log::Handler;
|
||||
use Log::Handler::Output::File;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.5';
|
||||
|
||||
#
|
||||
# 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";
|
||||
my $logfile = "$basedir/${PROG}.log";
|
||||
|
||||
my $dbh;
|
||||
|
||||
#<<< do not let perltidy change formatting
|
||||
#
|
||||
# The database tables we'll search and how to do what we need.
|
||||
#
|
||||
# main_key - the name of a table (with a hashref as the value)
|
||||
# index the database field we're using to find a row
|
||||
# fields fields we may need to edit (an arrayref)
|
||||
# logfields fields we will log (an arrayref)
|
||||
# mainQ the query that finds all rows needing work
|
||||
# rowQ a query for finding a row using the index value
|
||||
#
|
||||
my %tables = (
|
||||
'comments' => {
|
||||
index => 'id',
|
||||
fields => [qw{comment_title comment_text}],
|
||||
logfields => [qw{eps_id comment_title}],
|
||||
mainQ => q{
|
||||
select id
|
||||
from comments
|
||||
where comment_text regexp 'http://[^[:space:]]'
|
||||
or comment_title regexp 'http://[^[:space:]]'
|
||||
order by id},
|
||||
rowQ => q{select * from comments where id = ?},
|
||||
},
|
||||
'eps' => {
|
||||
index => 'id',
|
||||
fields => [qw{title summary notes}],
|
||||
logfields => [qw{title date}],
|
||||
mainQ => q{
|
||||
select id
|
||||
from eps
|
||||
where title regexp 'http://[^[:space:]]'
|
||||
or summary regexp 'http://[^[:space:]]'
|
||||
or notes regexp 'http://[^[:space:]]'
|
||||
order by id
|
||||
},
|
||||
rowQ => q{select * from eps where id = ?},
|
||||
},
|
||||
'hosts' => {
|
||||
index => 'hostid',
|
||||
fields => [qw{profile}],
|
||||
logfields => [qw{host email}],
|
||||
mainQ => q{
|
||||
select hostid
|
||||
from hosts
|
||||
where profile regexp 'http://[^[:space:]]'
|
||||
order by hostid},
|
||||
rowQ => q{select * from hosts where hostid = ?},
|
||||
},
|
||||
'miniseries' => {
|
||||
index => 'id',
|
||||
fields => [qw{description}],
|
||||
logfields => [qw{name}],
|
||||
mainQ => q{
|
||||
select id
|
||||
from miniseries
|
||||
where description regexp 'http://[^[:space:]]'
|
||||
order by id},
|
||||
rowQ => q{select * from miniseries where id = ?},
|
||||
},
|
||||
);
|
||||
#>>>
|
||||
my @table_names = keys(%tables);
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
my $DEF_DEBUG = 0;
|
||||
my $DEF_LIMIT = 0;
|
||||
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
#
|
||||
# Default help
|
||||
#
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 0, -exitval => 1 )
|
||||
if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Full documentation if requested with -doc
|
||||
#
|
||||
pod2usage(
|
||||
-msg => "$PROG version $VERSION\n",
|
||||
-verbose => 2,
|
||||
-exitval => 1,
|
||||
-noperldoc => 0,
|
||||
) if ( $options{'doc'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG );
|
||||
|
||||
my $cfgfile
|
||||
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
||||
|
||||
my $dry_run = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 0 );
|
||||
my $verbose = ( defined( $options{'verbose'} ) ? $options{'verbose'} : 0 );
|
||||
|
||||
my $limit = ( defined( $options{limit} ) ? $options{limit} : $DEF_LIMIT );
|
||||
$limit = abs($limit);
|
||||
|
||||
#
|
||||
# Sanity check
|
||||
#
|
||||
die "Unable to find config file '$cfgfile'\n" unless ( -e $cfgfile );
|
||||
|
||||
#
|
||||
# Table choice
|
||||
#
|
||||
my $table = shift;
|
||||
die "Database table not specified\n" unless $table;
|
||||
die "Not a valid table name: $table\n"
|
||||
unless ( grep { $_ =~ /^$table$/ } @table_names );
|
||||
|
||||
#
|
||||
# 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};
|
||||
#$dbh = DBI->connect( "DBI:MariaDB:host=$dbhost;port=$dbport;database=$dbname",
|
||||
# $dbuser, $dbpwd, { AutoCommit => 1, RaiseError => 1 } )
|
||||
# or die $DBI::errstr;
|
||||
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Set up logging keeping the default log layout except for the date
|
||||
#-------------------------------------------------------------------------------
|
||||
my $log = Log::Handler->new();
|
||||
|
||||
$log->add(
|
||||
file => {
|
||||
timeformat => "%Y-%m-%d %H:%M:%S",
|
||||
filename => $logfile,
|
||||
maxlevel => 7,
|
||||
minlevel => 0,
|
||||
utf8 => 1,
|
||||
}
|
||||
);
|
||||
|
||||
$log->info("Configuration file $cfgfile");
|
||||
$log->info("Processing table $table");
|
||||
$log->info("Dry-run mode") if ($dry_run);
|
||||
|
||||
process_table( $dbh, \%tables, $table, $dry_run, $limit );
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: process_table
|
||||
# PURPOSE: Processes a table to change any instances of 'http://' to
|
||||
# 'https://'
|
||||
# PARAMETERS: $dbh open database handle
|
||||
# $rtables reference to the %tables hash
|
||||
# $table name of the table being processed
|
||||
# $dry_run Boolean showing whether this is dry run or not
|
||||
# $limit number of updates to apply, 0 = no limit
|
||||
# RETURNS:
|
||||
# DESCRIPTION:
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub process_table {
|
||||
my ( $dbh, $rtables, $table, $dry_run, $limit ) = @_;
|
||||
|
||||
my ( $sth1, $sth2, $h );
|
||||
my ( $work_query, @work, $row_query, @fields, @logfields, $new, $index );
|
||||
my ( $sql, $stmt, @bind, %fieldvals, %where );
|
||||
my ( $workcount, $count, $updates, $logfmt );
|
||||
|
||||
#
|
||||
# Prepare to build SQL
|
||||
#
|
||||
$sql = SQL::Abstract->new;
|
||||
|
||||
$count = 0;
|
||||
$updates = 0;
|
||||
|
||||
#
|
||||
# Find any rows in need of work as an array/list of the index values. The
|
||||
# selectall_arrayref returns a reference to an array containing arrayrefs,
|
||||
# so the 'map' flattens that structure.
|
||||
#
|
||||
$work_query = $rtables->{$table}->{mainQ};
|
||||
@work = map { $_->[0] } @{ $dbh->selectall_arrayref($work_query) };
|
||||
|
||||
$workcount = scalar(@work);
|
||||
printf "Number of rows requiring work: %d\n", $workcount if $verbose;
|
||||
|
||||
_debug( $DEBUG >= 1, "Number of rows requiring work: $workcount" )
|
||||
unless $verbose;
|
||||
_debug( $DEBUG >= 2,
|
||||
"Rows requiring work: " . join( ",", @work ) . "\n" );
|
||||
|
||||
#
|
||||
# If there's nothing to do say so and leave
|
||||
#
|
||||
unless (@work) {
|
||||
print "Nothing to do to table '$table'!\n";
|
||||
return;
|
||||
}
|
||||
|
||||
#
|
||||
# Pull configuration values from the hash
|
||||
#
|
||||
$row_query = $rtables->{$table}->{rowQ};
|
||||
@fields = @{ $rtables->{$table}->{fields} };
|
||||
@logfields = @{ $rtables->{$table}->{logfields} };
|
||||
$index = $rtables->{$table}->{index};
|
||||
|
||||
_debug( $DEBUG >= 3, "\$row_query = $row_query" );
|
||||
_debug( $DEBUG >= 3, "\@fields = " . join( ",", @fields ) );
|
||||
_debug( $DEBUG >= 3, "\$index = $index" );
|
||||
|
||||
#
|
||||
# Prepare for logging by making a format string for sprintf
|
||||
#
|
||||
$logfmt = 'Updated row with ';
|
||||
$logfmt .= join( ", ", map {"$_ = '%s'"} $index, @logfields );
|
||||
|
||||
_debug( $DEBUG >= 3, "\$logfmt = $logfmt" );
|
||||
|
||||
#
|
||||
# Set up query for the next eligible row
|
||||
#
|
||||
$sth1 = $dbh->prepare($row_query) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Loop through rows needing work
|
||||
#
|
||||
foreach my $pkey (@work) {
|
||||
#
|
||||
# The row is indexed by the per-table key
|
||||
#
|
||||
$sth1->execute($pkey) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Grab the row (there must be only one with this index)
|
||||
#
|
||||
if ( $h = $sth1->fetchrow_hashref ) {
|
||||
#
|
||||
# Set up the 'where' options for SQL::Abstract
|
||||
#
|
||||
%where = ( $index => { '=', $pkey } );
|
||||
_debug( $DEBUG >= 3, Dumper( \%where ) );
|
||||
|
||||
#
|
||||
# Work on the fields we know might contain HTML
|
||||
#
|
||||
for my $field (@fields) {
|
||||
#
|
||||
# Perform the change
|
||||
#
|
||||
( $new = $h->{$field} ) =~ s{\bhttp://(\S)}{https://$1}ig;
|
||||
|
||||
#
|
||||
# Set up SQL::Abstract parameters
|
||||
#
|
||||
$fieldvals{$field} = $new;
|
||||
}
|
||||
|
||||
#
|
||||
# Use SQL::Abstract to make the statement and the bind parameters
|
||||
#
|
||||
( $stmt, @bind ) = $sql->update( $table, \%fieldvals, \%where );
|
||||
|
||||
#
|
||||
# Do the change or report it depending on dry-run mode
|
||||
#
|
||||
unless ($dry_run) {
|
||||
$sth2 = $dbh->prepare($stmt) or die $DBI::errstr;
|
||||
$sth2->execute(@bind) or die $DBI::errstr;
|
||||
$log->info( sprintf( $logfmt, $pkey, @{$h}{@logfields} ) );
|
||||
printf $logfmt. "\n", $pkey, @{$h}{@logfields} if $verbose;
|
||||
$updates++;
|
||||
}
|
||||
else {
|
||||
print "No change made in dry-run mode\n";
|
||||
if ($verbose) {
|
||||
print "SQL: $stmt\n";
|
||||
print "Bind> ", join( "\nBind> ", @bind ), "\n";
|
||||
print '-' x 80, "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Apply the limit if appropriate
|
||||
#
|
||||
$count++;
|
||||
unless ( $limit == 0 ) {
|
||||
last if ( $count >= $limit );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
unless ($dry_run) {
|
||||
$log->info("Number of updates = $updates");
|
||||
if ($verbose) {
|
||||
print "Number of updates = $updates\n";
|
||||
printf "Remaining rows needing attention: %d\n",
|
||||
$workcount - $updates;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: concat
|
||||
# PURPOSE: Reimplementation of join but with any undefined or empty
|
||||
# arguments removed
|
||||
# PARAMETERS: $sep The string to be used to separate elements in
|
||||
# the result
|
||||
# [variable args] Any number of arguments to be joined together
|
||||
# with the separator
|
||||
# RETURNS: The concatenated arguments
|
||||
# DESCRIPTION: Giving 'join' an array that may contain undefined elements will
|
||||
# result in empty results in the output string and error
|
||||
# messages as the undefined elements are processed. Giving it
|
||||
# empty string elements will result in dangling separators in
|
||||
# the output. This routine removes the undefined and empty
|
||||
# elements before joining the rest.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO:
|
||||
#===============================================================================
|
||||
sub concat {
|
||||
my $sep = shift;
|
||||
|
||||
my @args = grep { defined($_) && length($_) > 0 } @_;
|
||||
|
||||
return join( $sep, @args );
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _debug
|
||||
# PURPOSE: Prints debug reports
|
||||
# PARAMETERS: $active Boolean: 1 for print, 0 for no print
|
||||
# $message Message to print
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: Outputs a message if $active is true. It removes any trailing
|
||||
# newline and then adds one in the 'print' to the caller doesn't
|
||||
# have to bother. Prepends the message with 'D> ' to show it's
|
||||
# a debug message.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub _debug {
|
||||
my ( $active, $message ) = @_;
|
||||
|
||||
chomp($message);
|
||||
print "D> $message\n" if $active;
|
||||
}
|
||||
|
||||
#=== 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", "doc", "debug=i", "verbose!",
|
||||
"dry-run!", "config=s", "limit=i",
|
||||
);
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
|
||||
=head1 NAME
|
||||
|
||||
fix_urls - Fixes 'http://' urls in the HPR database
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to fix_urls version 0.0.5
|
||||
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
./fix_urls [-help] [-doc] [-debug=N] [-[no]dry-run] [-[no]verbose]
|
||||
[-config=FILE] [-limit=N] tablename
|
||||
|
||||
fix_urls -help
|
||||
fix_urls -doc
|
||||
|
||||
fix_urls -limit=10 -dry-run comments
|
||||
fix_urls -limit=10 comments
|
||||
fix_urls -limit=10 -verbose comments
|
||||
|
||||
fix_urls -config=.hpr_livedb.cfg -debug=1 -dry-run -limit=1 comments
|
||||
|
||||
=head1 REQUIRED ARGUMENTS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<tablename>
|
||||
|
||||
The mandatory argument required by the script is the name of the table to
|
||||
process. The choices are:
|
||||
|
||||
comments
|
||||
eps
|
||||
hosts
|
||||
miniseries
|
||||
|
||||
=back
|
||||
|
||||
=head1 OPTIONS
|
||||
|
||||
=over 8
|
||||
|
||||
=item B<-help>
|
||||
|
||||
Prints a brief help message describing the usage of the program, and then exits.
|
||||
|
||||
=item B<-doc>
|
||||
|
||||
Displays the entirety of the documentation (using a pager), and then exits. To
|
||||
generate a PDF version use:
|
||||
|
||||
pod2pdf fix_urls --out=fix_urls.pdf
|
||||
|
||||
=item B<-debug=N>
|
||||
|
||||
Selects a level of debugging. Debug information consists of a line or series
|
||||
of lines prefixed with the characters 'D>':
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<0>
|
||||
|
||||
No debug output is generated: this is the default
|
||||
|
||||
=item B<1>
|
||||
|
||||
Displays the number of updates required in a table.
|
||||
|
||||
=item B<2>
|
||||
|
||||
As for level 1, and also displays the primary key values of all rows requiring
|
||||
work in the table.
|
||||
|
||||
=item B<3>
|
||||
|
||||
As for level 2, and also displays some internal values for verification.
|
||||
|
||||
=back
|
||||
|
||||
=item B<-[no]dry-run>
|
||||
|
||||
Controls whether the program runs in a mode where it performs database
|
||||
updates. When enabled the details of the updates to be performed are shown,
|
||||
otherwise the updates are applied. The default B<-nodry-run> allows the
|
||||
program to perform the changes.
|
||||
|
||||
=item B<-[no]verbose>
|
||||
|
||||
Normally very little is reported by the script, although details of which rows
|
||||
have been changed are logged. When B<-verbose> is selected more information
|
||||
about the number of rows needing work, the updates performed (or which would
|
||||
have been performed) and how many changes were made is reported.
|
||||
|
||||
=item B<-limit=N>
|
||||
|
||||
This option allows the number of rows in the chosen table to be limited during
|
||||
a B<-dry-run> pass or an update pass. If omitted, or if a value of zero is
|
||||
given, then all eligible rows are processed.
|
||||
|
||||
=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 B<fix_urls> script performs edits on fields in tables in the HPR database.
|
||||
As written, the purpose is to change all occurrences of 'http://' to
|
||||
'https://', though it could be used for other tasks. It is not designed to be
|
||||
easily changeable from one to another, but the code can be changed to do this
|
||||
if needed.
|
||||
|
||||
A single table is processed in each run, and the number of rows may be limited
|
||||
if required by using the B<-limit=N> option.
|
||||
|
||||
The eligible tables are defined in a hash structure B<%tables> which defines
|
||||
the fields to be processed and the queries needed to search for all rows
|
||||
requiring work and to get a particular row to work on. It also defines which
|
||||
fields are to be reported in the log file.
|
||||
|
||||
A log file is appended to when the script is run, which has the name
|
||||
B<fix_urls.log> in the same directory as the script.
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<Unable to find config file '...'>
|
||||
|
||||
Type: fatal
|
||||
|
||||
The configuration file in the B<-config=FILE> option cannot be found.
|
||||
|
||||
=item B<Database table not specified>
|
||||
|
||||
Type: fatal
|
||||
|
||||
The mandatory table name argument was not provided.
|
||||
|
||||
=item B<Not a valid table name: ...>
|
||||
|
||||
Type: fatal
|
||||
|
||||
The mandatory table name argument specified an unknown table name.
|
||||
|
||||
=item B<[DBI error messages]>
|
||||
|
||||
Type: fatal
|
||||
|
||||
Generated when a database interface error has been detected, such as failure
|
||||
to connect to the database or failure to prepare or execute a query.
|
||||
|
||||
=back
|
||||
|
||||
=head1 CONFIGURATION AND ENVIRONMENT
|
||||
|
||||
The script obtains the credentials it requires to open a local copy of 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. This configuration file
|
||||
can be overridden using the B<-config=FILE> option as described above.
|
||||
|
||||
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
|
||||
Getopt::Long
|
||||
Log::Handler
|
||||
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) 2021-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.
|
||||
|
||||
=cut
|
||||
|
||||
#}}}
|
||||
|
||||
# [zo to open fold, zc to close]
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
75
Database/generate_tag_reports
Executable file
75
Database/generate_tag_reports
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash -
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: generate_tag_reports
|
||||
#
|
||||
# USAGE: ./generate_tag_reports
|
||||
#
|
||||
# DESCRIPTION: Runs 'report_missing_tags' and 'make tags' to generate the
|
||||
# pages for the HPR website: report_missing_tags.php and
|
||||
# tags.php
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.5
|
||||
# CREATED: 2021-01-02 13:02:47
|
||||
# REVISION: 2022-09-09 08:05:47
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
set -o nounset # Treat unset variables as an error
|
||||
|
||||
SCRIPT=${0##*/}
|
||||
VERSION="0.0.5"
|
||||
|
||||
BASEDIR="$HOME/HPR/Database"
|
||||
cd "$BASEDIR" || { echo "$SCRIPT: Failed to cd to $BASEDIR"; exit 1; }
|
||||
|
||||
REPORT="$BASEDIR/make_tag_index"
|
||||
LIVECFG="$BASEDIR/.hpr_livedb.cfg" # soft link
|
||||
# PHPREP="$BASEDIR/report_missing_tags.php"
|
||||
# CSVREP1="$BASEDIR/tag_summary_actions.csv"
|
||||
# CSVREP2="$BASEDIR/tags_shows.csv"
|
||||
# JSONREP="$BASEDIR/tag_data.json"
|
||||
# HTMLTAGS="$BASEDIR/tags.html"
|
||||
PHPTAGS="$BASEDIR/tags.php"
|
||||
|
||||
#
|
||||
# Sanity checks
|
||||
#
|
||||
[ -e "$REPORT" ] || { echo "$SCRIPT: missing script $REPORT"; exit 1; }
|
||||
[ -e "$LIVECFG" ] || { echo "$SCRIPT: missing file $LIVECFG"; exit 1; }
|
||||
|
||||
if ! tunnel_is_open; then
|
||||
echo "Open the tunnel to run this script"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Run the main report with the default template (make_tag_index.tpl)
|
||||
#
|
||||
if $REPORT -config="$LIVECFG" -out="$PHPTAGS"; then
|
||||
echo "$SCRIPT v$VERSION"
|
||||
[ -e "$PHPTAGS" ] && echo "Generated $PHPTAGS"
|
||||
else
|
||||
echo "$SCRIPT: failed to run $REPORT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Generate the standalone tag list from the output of the report script
|
||||
#
|
||||
# if [[ -e $HTMLTAGS ]]; then
|
||||
# make tags
|
||||
# [ -e "$PHPTAGS" ] && echo "Generated $PHPTAGS"
|
||||
# else
|
||||
# echo "$SCRIPT: missing file $HTMLTAGS"
|
||||
# fi
|
||||
|
||||
exit
|
||||
|
||||
# vim: syntax=sh:ts=8:sw=4:ai:et:tw=78:fo=tcrqn21
|
||||
|
179
Database/host_image
Executable file
179
Database/host_image
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: host_image
|
||||
#
|
||||
# USAGE: ./host_image
|
||||
#
|
||||
# DESCRIPTION: Collects Gravatar images for HPR hosts. This version simply
|
||||
# cycles through the list of hosts from the local copy of the
|
||||
# database and attempts to collect the Gravatar for every one
|
||||
# that has an email address and isn't marked as having a local
|
||||
# image (provided via the show upload form) in the database.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.2
|
||||
# CREATED: 2016-08-31 16:52:52
|
||||
# REVISION: 2021-10-15 21:02:52
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Config::General;
|
||||
use Digest::MD5 qw{md5_hex};
|
||||
#use Digest::MD5::File qw{file_md5_hex};
|
||||
use LWP::Simple;
|
||||
use DBI;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.2';
|
||||
|
||||
#
|
||||
# Script name
|
||||
#
|
||||
( 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";
|
||||
my $imgpath = "$basedir/www/images/hosts/%s.png";
|
||||
my $urlformat = 'https://secure.gravatar.com/avatar/%s.png?d=404&s=90';
|
||||
|
||||
my ( $dbh, $sth1, $h1, $rv );
|
||||
my ( $host, $hostid, $email, $grav_url, $img, $res );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load configuration data
|
||||
#
|
||||
my $conf = new Config::General(
|
||||
-ConfigFile => $configfile,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1
|
||||
);
|
||||
my %config = $conf->getall();
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Connect to the database
|
||||
# 2021-10-15: moved to MariaDB
|
||||
#-------------------------------------------------------------------------------
|
||||
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:MariaDB:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1, RaiseError => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8 (MySQL only)
|
||||
#
|
||||
# $dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Prepare SQL for finding hosts
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(
|
||||
q{SELECT host, hostid, email FROM hosts
|
||||
WHERE valid = '1' AND local_image = '0'
|
||||
ORDER BY hostid ASC}
|
||||
);
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Loop through the hosts gathering gravatars
|
||||
#-------------------------------------------------------------------------------
|
||||
while ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
$host = $h1->{host};
|
||||
$hostid = $h1->{hostid};
|
||||
$email = $h1->{email};
|
||||
|
||||
#
|
||||
# We need an email address
|
||||
#
|
||||
next unless ($email);
|
||||
|
||||
$res = fetch( $hostid, $host, $email, $urlformat, $imgpath );
|
||||
}
|
||||
|
||||
$sth1->finish;
|
||||
|
||||
$dbh->disconnect;
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: fetch
|
||||
# PURPOSE: Perform the fetching and saving of a gravatar image
|
||||
# PARAMETERS: $hostid - host number from database
|
||||
# $host - host name from database
|
||||
# $email - email address from database
|
||||
# $urlformat - template for building the gravatar URL
|
||||
# $imgpath - template for building the file path
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: Uses LWP to collect the gravatar image using the URL
|
||||
# constructed from a template and the email address, writes it
|
||||
# to the constructed file path.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub fetch {
|
||||
my ( $hostid, $host, $email, $urlformat, $imgpath ) = @_;
|
||||
|
||||
#
|
||||
# Build the URL and the image file path
|
||||
#
|
||||
my $grav_url = sprintf( $urlformat, md5_hex( lc($email) ) );
|
||||
my $img = sprintf( $imgpath, $hostid );
|
||||
|
||||
printf "%3d: %s (%s) %s %s - ", $hostid, $host, $email, $grav_url, $img;
|
||||
|
||||
#
|
||||
# Collect the gravatar if there is one
|
||||
#
|
||||
my $res = getstore( $grav_url, $img );
|
||||
|
||||
#
|
||||
# Remove any garbage
|
||||
#
|
||||
if ( $res != 200 ) {
|
||||
print "Failed ($res)\n";
|
||||
unlink($img);
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
print "OK\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
107
Database/hosts_eps.sql
Normal file
107
Database/hosts_eps.sql
Normal file
@@ -0,0 +1,107 @@
|
||||
--
|
||||
-- Set up a many-to-many relationship between tables hosts and eps
|
||||
-- -----------------------------------------------------------------------------
|
||||
--
|
||||
|
||||
--
|
||||
-- Make a table called 'new_hosts' containing a copy of the hosts table
|
||||
-- with the same structure but no data. Shrink the hostid column to int(5).
|
||||
--
|
||||
DROP TABLE IF EXISTS new_hosts;
|
||||
CREATE TABLE IF NOT EXISTS new_hosts (
|
||||
hostid int(5) NOT NULL AUTO_INCREMENT,
|
||||
PRIMARY KEY (hostid)
|
||||
) ENGINE=InnoDB
|
||||
SELECT * FROM hosts WHERE 0 = 1;
|
||||
|
||||
-- without the "double host" entries (host='Host1 and Host2')
|
||||
-- SELECT * FROM hosts where host NOT LIKE '% and %';
|
||||
|
||||
SHOW warnings;
|
||||
|
||||
--
|
||||
-- Make a table called 'new_eps' containing a copy of the eps table without
|
||||
-- the host-related columns but no data.
|
||||
--
|
||||
DROP TABLE IF EXISTS new_eps;
|
||||
CREATE TABLE IF NOT EXISTS new_eps (
|
||||
id int(5) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB
|
||||
SELECT
|
||||
id,
|
||||
date,
|
||||
title,
|
||||
summary,
|
||||
notes,
|
||||
series,
|
||||
explicit,
|
||||
license,
|
||||
tags,
|
||||
version,
|
||||
valid
|
||||
FROM eps WHERE 0 = 1;
|
||||
|
||||
SHOW warnings;
|
||||
|
||||
--
|
||||
-- Table structure for the mapping table 'hosts_eps'
|
||||
--
|
||||
DROP TABLE IF EXISTS hosts_eps;
|
||||
CREATE TABLE IF NOT EXISTS hosts_eps (
|
||||
host_id int(5) NOT NULL
|
||||
REFERENCES new_hosts(hostid),
|
||||
eps_id int(5) NOT NULL
|
||||
REFERENCES new_eps(id),
|
||||
PRIMARY KEY hosts_eps_pk (host_id,eps_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
SHOW warnings;
|
||||
|
||||
--
|
||||
-- Populate the hosts_eps table
|
||||
--
|
||||
/*
|
||||
INSERT INTO hosts_eps (host_id, eps_id)
|
||||
SELECT ho.hostid, eps.id
|
||||
FROM hosts ho
|
||||
JOIN eps ON ho.hostid = eps.hostid
|
||||
ORDER BY ho.hostid, eps.id;
|
||||
|
||||
SHOW warnings;
|
||||
*/
|
||||
|
||||
--
|
||||
-- Make a view to simplify access to new_hosts and new_eps
|
||||
--
|
||||
DROP VIEW IF EXISTS hosts_with_eps;
|
||||
CREATE VIEW hosts_with_eps AS
|
||||
SELECT
|
||||
nh.hostid,
|
||||
nh.host,
|
||||
nh.email,
|
||||
nh.profile,
|
||||
nh.license AS host_license,
|
||||
nh.local_image,
|
||||
nh.valid AS host_valid,
|
||||
ne.id,
|
||||
ne.date,
|
||||
ne.title,
|
||||
ne.summary,
|
||||
ne.notes,
|
||||
ne.series,
|
||||
ne.explicit,
|
||||
ne.license AS eps_license,
|
||||
ne.tags,
|
||||
ne.version,
|
||||
ne.valid AS eps_valid
|
||||
FROM new_hosts nh
|
||||
JOIN hosts_eps he ON (nh.hostid = he.host_id)
|
||||
JOIN new_eps ne ON (he.eps_id = ne.id)
|
||||
ORDER BY nh.hostid, ne.id;
|
||||
|
||||
SHOW warnings;
|
||||
|
||||
/*
|
||||
vim: syntax=sql ai tw=75:
|
||||
*/
|
336
Database/hpr_schema.pgsql
Normal file
336
Database/hpr_schema.pgsql
Normal file
@@ -0,0 +1,336 @@
|
||||
/* =============================================================================
|
||||
* PostgreSQL Schema - designs for a new HPR database
|
||||
*
|
||||
* File: hpr_schema.pgsql
|
||||
* Created: 2017-03-15
|
||||
* Updated: 2017-10-16
|
||||
* =============================================================================
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Drop everything to start with. The order is important because of the
|
||||
* relations between tables. Also, some items are dependent and go with the
|
||||
* tables.
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
DROP TABLE IF EXISTS comments CASCADE;
|
||||
DROP TABLE IF EXISTS episodes CASCADE;
|
||||
DROP TABLE IF EXISTS episodes_hosts_xref CASCADE;
|
||||
DROP TABLE IF EXISTS episodes_series_xref CASCADE;
|
||||
DROP TABLE IF EXISTS episodes_tags_xref CASCADE;
|
||||
DROP TABLE IF EXISTS hosts CASCADE;
|
||||
DROP TABLE IF EXISTS licenses CASCADE;
|
||||
DROP TABLE IF EXISTS series CASCADE;
|
||||
DROP TABLE IF EXISTS tags CASCADE;
|
||||
|
||||
-- DROP INDEX IF EXISTS episode_release_date_key;
|
||||
|
||||
DROP SEQUENCE IF EXISTS comment_seq;
|
||||
DROP SEQUENCE IF EXISTS episode_seq;
|
||||
DROP SEQUENCE IF EXISTS host_seq;
|
||||
DROP SEQUENCE IF EXISTS license_seq;
|
||||
DROP SEQUENCE IF EXISTS series_seq;
|
||||
DROP SEQUENCE IF EXISTS tag_seq;
|
||||
|
||||
-- DROP VIEW IF EXISTS eht_view;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'licenses' - licenses relating to episodes (needed because 'hosts'
|
||||
* references it)
|
||||
* ------------------------------------------------------------------------------
|
||||
*/
|
||||
CREATE SEQUENCE license_seq;
|
||||
|
||||
ALTER TABLE license_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE licenses (
|
||||
id integer default nextval('license_seq') PRIMARY KEY,
|
||||
short_name varchar(11) NOT NULL UNIQUE,
|
||||
long_name varchar(40) NOT NULL,
|
||||
url varchar(80) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE licenses
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/*
|
||||
* Load the table since it's quite short. Don't set the id to ensure the
|
||||
* sequence is updated properly.
|
||||
*/
|
||||
INSERT INTO licenses (short_name, long_name, url) VALUES
|
||||
('CC-0', 'Public Domain Dedication', 'http://creativecommons.org/publicdomain/zero/1.0/'),
|
||||
('CC-BY', 'Attribution', 'http://creativecommons.org/licenses/by/4.0'),
|
||||
('CC-BY-SA', 'Attribution-ShareAlike', 'http://creativecommons.org/licenses/by-sa/3.0'),
|
||||
('CC-BY-ND', 'Attribution-NoDerivs', 'http://creativecommons.org/licenses/by-nd/4.0'),
|
||||
('CC-BY-NC', 'Attribution-NonCommercial', 'http://creativecommons.org/licenses/by-nc/4.0'),
|
||||
('CC-BY-NC-SA', 'Attribution-NonCommercial-ShareAlike', 'http://creativecommons.org/licenses/by-nc-sa/4.0'),
|
||||
('CC-BY-NC-ND', 'Attribution-NonCommercial-NoDerivs', 'http://creativecommons.org/licenses/by-nc-nd/4.0');
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'episodes' - HPR shows
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE SEQUENCE episode_seq;
|
||||
|
||||
ALTER TABLE episode_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE episodes (
|
||||
id integer default nextval('episode_seq') PRIMARY KEY,
|
||||
release_date date NOT NULL,
|
||||
title varchar(100) NOT NULL,
|
||||
summary varchar(100),
|
||||
notes text NOT NULL,
|
||||
explicit smallint NOT NULL DEFAULT '1',
|
||||
license varchar(11) NOT NULL DEFAULT 'CC-BY-SA'
|
||||
REFERENCES licenses (short_name),
|
||||
duration integer NOT NULL DEFAULT 0,
|
||||
downloads integer NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
ALTER TABLE episodes
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE INDEX episode_release_date_key
|
||||
ON episodes
|
||||
USING btree
|
||||
(release_date);
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'hosts' - hosts contributing shows
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE SEQUENCE host_seq;
|
||||
|
||||
ALTER TABLE host_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE hosts (
|
||||
id integer default nextval('host_seq') PRIMARY KEY,
|
||||
host varchar(1024) UNIQUE NOT NULL,
|
||||
-- email varchar(1024) CHECK (email <> ''),
|
||||
email varchar(1024) UNIQUE NOT NULL,
|
||||
profile text,
|
||||
license varchar(11) NOT NULL DEFAULT 'CC-BY-SA'
|
||||
REFERENCES licenses (short_name),
|
||||
local_image smallint NOT NULL DEFAULT '0',
|
||||
gpg text,
|
||||
valid smallint NOT NULL DEFAULT '1',
|
||||
date_added date
|
||||
);
|
||||
|
||||
ALTER TABLE hosts
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'episodes_hosts_xref' - joining table between 'episodes' and 'hosts'
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE TABLE episodes_hosts_xref (
|
||||
episodes_id integer REFERENCES episodes(id)
|
||||
ON DELETE RESTRICT,
|
||||
hosts_id integer REFERENCES hosts(id)
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY (episodes_id, hosts_id)
|
||||
);
|
||||
|
||||
ALTER TABLE episodes_hosts_xref
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'tags' - tags relating to episodes
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE SEQUENCE tag_seq;
|
||||
|
||||
ALTER TABLE tag_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE tags (
|
||||
id integer default nextval('tag_seq') PRIMARY KEY,
|
||||
tag varchar(1024) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE tags
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'episodes_tags_xref' - joining table between 'episodes' and 'tags'
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE TABLE episodes_tags_xref (
|
||||
episodes_id integer REFERENCES episodes(id)
|
||||
ON DELETE RESTRICT,
|
||||
tags_id integer REFERENCES tags(id)
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY (episodes_id, tags_id)
|
||||
);
|
||||
|
||||
ALTER TABLE episodes_tags_xref
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'series' - series grouping for episodes
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE SEQUENCE series_seq;
|
||||
|
||||
ALTER TABLE series_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE series (
|
||||
id integer default nextval('series_seq') PRIMARY KEY,
|
||||
name varchar(100) NOT NULL,
|
||||
description text NOT NULL,
|
||||
private smallint NOT NULL DEFAULT '0',
|
||||
image text,
|
||||
valid smallint NOT NULL DEFAULT '1'
|
||||
);
|
||||
|
||||
ALTER TABLE series
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'episodes_series_xref' - joining table between 'episodes' and 'series'
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE TABLE episodes_series_xref (
|
||||
episodes_id integer REFERENCES episodes(id)
|
||||
ON DELETE RESTRICT,
|
||||
series_id integer REFERENCES series(id)
|
||||
ON DELETE CASCADE,
|
||||
PRIMARY KEY (episodes_id, series_id)
|
||||
);
|
||||
|
||||
ALTER TABLE episodes_series_xref
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Table 'comments' - comments relating to episodes
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE SEQUENCE comment_seq;
|
||||
|
||||
ALTER TABLE comment_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE comments (
|
||||
id integer default nextval('comment_seq') PRIMARY KEY,
|
||||
eps_id integer REFERENCES episodes(id)
|
||||
ON DELETE RESTRICT,
|
||||
comment_timestamp timestamp without time zone NOT NULL,
|
||||
comment_author_name varchar(1024),
|
||||
comment_title varchar(1024),
|
||||
comment_text text,
|
||||
last_changed timestamp without time zone NOT NULL
|
||||
DEFAULT timezone('UTC'::text, now())
|
||||
);
|
||||
|
||||
ALTER TABLE comments
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Tables from "Today with a Techie" for further processing
|
||||
* ------------------------------------------------------------------------------ */
|
||||
CREATE SEQUENCE twat_hosts_seq;
|
||||
|
||||
ALTER TABLE twat_hosts_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE twat_hosts (
|
||||
id integer default nextval('twat_hosts_seq') PRIMARY KEY,
|
||||
host varchar(1024) NOT NULL,
|
||||
email varchar(1024),
|
||||
website varchar(1024),
|
||||
repeat integer NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE twat_hosts
|
||||
OWNER TO hpradmin;
|
||||
|
||||
|
||||
CREATE SEQUENCE twat_episodes_seq;
|
||||
|
||||
ALTER TABLE twat_episodes_seq
|
||||
OWNER TO hpradmin;
|
||||
|
||||
CREATE TABLE twat_episodes (
|
||||
ep_num integer default nextval('twat_episodes_seq') PRIMARY KEY,
|
||||
date integer NOT NULL,
|
||||
host varchar(1024) NOT NULL,
|
||||
topic varchar(1024) NOT NULL,
|
||||
writeup text,
|
||||
url varchar(1024) NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE twat_episodes
|
||||
OWNER TO hpradmin;
|
||||
|
||||
/* ------------------------------------------------------------------------------
|
||||
* Experimental views
|
||||
* ------------------------------------------------------------------------------ */
|
||||
--
|
||||
-- eh_view
|
||||
--
|
||||
CREATE OR REPLACE VIEW eh_view AS
|
||||
SELECT
|
||||
ep.id,
|
||||
ep.release_date,
|
||||
ep.title,
|
||||
(SELECT string_agg(host, ', ' ORDER BY host)
|
||||
FROM hosts h2,
|
||||
episodes_hosts_xref eh2
|
||||
WHERE eh2.hosts_id = h2.id
|
||||
GROUP BY eh2.episodes_id
|
||||
HAVING eh2.episodes_id = ep.id) AS hosts
|
||||
FROM episodes ep
|
||||
GROUP BY ep.id
|
||||
ORDER BY ep.id;
|
||||
|
||||
ALTER TABLE eh_view
|
||||
OWNER TO hpradmin;
|
||||
|
||||
--
|
||||
-- eht_view
|
||||
--
|
||||
CREATE OR REPLACE VIEW eht_view AS
|
||||
SELECT e.*,
|
||||
h.host,
|
||||
t.tag,
|
||||
(SELECT string_agg(tag, ', ')
|
||||
FROM tags t2,
|
||||
episodes_tags_xref et2
|
||||
WHERE et2.tags_id = t2.id
|
||||
GROUP BY et2.episodes_id
|
||||
HAVING et2.episodes_id = e.id) AS tags
|
||||
FROM episodes e,
|
||||
hosts h,
|
||||
episodes_hosts_xref eh,
|
||||
episodes_tags_xref et,
|
||||
tags t
|
||||
WHERE e.id = eh.episodes_id
|
||||
AND h.id = eh.hosts_id
|
||||
AND e.id = et.episodes_id
|
||||
AND et.tags_id = t.id
|
||||
GROUP BY e.id,
|
||||
h.host,
|
||||
t.tag
|
||||
ORDER BY e.id;
|
||||
|
||||
-- CREATE OR REPLACE VIEW eht_view AS
|
||||
-- SELECT
|
||||
-- e.*,
|
||||
-- h.host,
|
||||
-- t.tag,
|
||||
-- (SELECT string_agg(tag, ', ')
|
||||
-- FROM tags t2, episodes_tags_xref et2
|
||||
-- WHERE et2.tags_id = t2.id
|
||||
-- GROUP BY et2.episodes_id
|
||||
-- HAVING et2.episodes_id = e.id) AS tags
|
||||
-- FROM episodes e, hosts h, episodes_hosts_xref eh, episodes_tags_xref et, tags t
|
||||
-- WHERE e.id = eh.episodes_id
|
||||
-- AND h.id = eh.hosts_id
|
||||
-- AND e.id = et.episodes_id
|
||||
-- AND et.tags_id = t.id
|
||||
-- GROUP BY e.id,h.host,t.tag
|
||||
-- ORDER BY e.id;
|
||||
|
||||
ALTER TABLE eht_view
|
||||
OWNER TO hpradmin;
|
||||
|
||||
-- Footer ---------------------------------------------------------------------
|
||||
|
||||
-- vim: syntax=pgsql:ts=8:sw=4:ai:tw=78:et:fo=tcrqn21:nu:rnu
|
118
Database/load_downloads
Executable file
118
Database/load_downloads
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: load_downloads
|
||||
#
|
||||
# USAGE: ./load_downloads infile
|
||||
#
|
||||
# DESCRIPTION: Loads episode downloads from a file into the 'eps' table
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.1
|
||||
# CREATED: 2014-08-30 17:46:47
|
||||
# REVISION: 2014-08-30 17:46:52
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Config::General;
|
||||
use Text::CSV_XS;
|
||||
|
||||
use DBI;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.1';
|
||||
|
||||
#
|
||||
# Script name
|
||||
#
|
||||
( 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";
|
||||
|
||||
my ( $dbh, $sth1, $sth2, $sth3, $sth4, $h1, $h2, $rv );
|
||||
my ( $infile, $row );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Check the input file
|
||||
#
|
||||
$infile = shift;
|
||||
die "Usage: $PROG input_file\n" unless $infile;
|
||||
die "Unable to find/read file '$infile'\n" unless -r $infile;
|
||||
|
||||
#
|
||||
# Load configuration data
|
||||
#
|
||||
my $conf = new Config::General(
|
||||
-ConfigFile => $configfile,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1
|
||||
);
|
||||
my %config = $conf->getall();
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Connect to the database
|
||||
#-------------------------------------------------------------------------------
|
||||
my $dbhost = $config{database}->{host};
|
||||
my $dbname = $config{database}->{name};
|
||||
my $dbuser = $config{database}->{user};
|
||||
my $dbpwd = $config{database}->{password};
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
$sth1 = $dbh->prepare(q{UPDATE eps SET downloads = ? WHERE id = ?});
|
||||
|
||||
#
|
||||
# Open the input file
|
||||
#
|
||||
open( my $in, "<", $infile ) or die "Unable to open $infile: $!\n";
|
||||
|
||||
my $csv = Text::CSV_XS->new;
|
||||
|
||||
#
|
||||
# Process all lines as CSV
|
||||
#
|
||||
while ( $row = $csv->getline($in) ) {
|
||||
$sth1->execute( $row->[1], $row->[0] );
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
}
|
||||
|
||||
close($in);
|
||||
|
||||
exit;
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
518
Database/make_tag_index
Executable file
518
Database/make_tag_index
Executable file
@@ -0,0 +1,518 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: make_tag_index
|
||||
#
|
||||
# USAGE: ./make_tag_index [-help] [-debug=N] [-out=FILE] [-config=FILE]
|
||||
#
|
||||
# DESCRIPTION: Make tag lookup pages for the HPR website
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.2
|
||||
# CREATED: 2022-09-08 11:52:53
|
||||
# REVISION: 2022-09-10 14:59:38
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use v5.16;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
use feature qw{ postderef say signatures state };
|
||||
no warnings qw{ experimental::postderef experimental::signatures };
|
||||
|
||||
use Getopt::Long;
|
||||
use Pod::Usage;
|
||||
|
||||
use Config::General;
|
||||
|
||||
use Template;
|
||||
use Template::Filters;
|
||||
Template::Filters->use_html_entities; # Use HTML::Entities in the template
|
||||
|
||||
use Text::CSV_XS;
|
||||
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.2';
|
||||
|
||||
#
|
||||
# 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";
|
||||
my $template = "$basedir/$PROG.tpl";
|
||||
|
||||
my ( $dbh, $sth1, $h1 );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
my $DEF_DEBUG = 0;
|
||||
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
#
|
||||
# Default help
|
||||
#
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 )
|
||||
if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Full documentation if requested with -doc
|
||||
#
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 2, -exitval => 1 )
|
||||
if ( $options{'doc'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG );
|
||||
my $cfgfile
|
||||
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
||||
|
||||
my $templatefile = $options{template};
|
||||
my $outfile = $options{out};
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Template is the default pre-defined string or a filename
|
||||
#-------------------------------------------------------------------------------
|
||||
if ($templatefile) {
|
||||
die "Unable to find template $templatefile\n" unless ( -e $templatefile );
|
||||
}
|
||||
else {
|
||||
$templatefile = $template;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Open the output file (or STDOUT)
|
||||
#-------------------------------------------------------------------------------
|
||||
my $outfh;
|
||||
if ($outfile) {
|
||||
open( $outfh, ">:encoding(UTF-8)", $outfile )
|
||||
or die "Unable to open $outfile for writing: $!";
|
||||
}
|
||||
else {
|
||||
open( $outfh, ">&", \*STDOUT )
|
||||
or die "Unable to initialise for writing: $!";
|
||||
}
|
||||
|
||||
#
|
||||
# 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};
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Perform a scan of episodes for tags and accumulate them in a hash
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(q{SELECT id,title,tags FROM eps WHERE length(tags) > 0})
|
||||
or die $DBI::errstr;
|
||||
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
my ( $status, @fields, %tag_ids, $lastkey, @tagindex, %showtitles );
|
||||
|
||||
my $csv = Text::CSV_XS->new(
|
||||
{ binary => 1,
|
||||
auto_diag => 1,
|
||||
escape_char => "\\",
|
||||
allow_loose_quotes => 1
|
||||
}
|
||||
);
|
||||
|
||||
#
|
||||
# Loop through the episodes returned by the query
|
||||
#
|
||||
while ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
#
|
||||
# Stash the show title with the show number
|
||||
#
|
||||
$showtitles{ $h1->{id} } = $h1->{title};
|
||||
|
||||
#
|
||||
# Parse the tag list for the current episode
|
||||
#
|
||||
$status = $csv->parse( $h1->{tags} );
|
||||
unless ($status) {
|
||||
#
|
||||
# Report any errors
|
||||
#
|
||||
print "Parse error on episode ", $h1->{id}, "\n";
|
||||
print $csv->error_input(), "\n";
|
||||
next;
|
||||
}
|
||||
@fields = $csv->fields();
|
||||
|
||||
#
|
||||
# Not sure why there are no tags but if not ignore this episode
|
||||
#
|
||||
next unless (@fields);
|
||||
|
||||
#
|
||||
# Trim and lowercase all tags
|
||||
#
|
||||
@fields = map {
|
||||
my $t = $_;
|
||||
$t =~ s/(^\s+|\s+$)//g;
|
||||
lc($t)
|
||||
} @fields;
|
||||
|
||||
#
|
||||
# Loop through the tags. For each tag add the associated episode id to the
|
||||
# %tag_ids hash. The key to this hash is the lower case tag and the value
|
||||
# is an array of episode numbers.
|
||||
#
|
||||
foreach my $tag (@fields) {
|
||||
if ( defined( $tag_ids{$tag} ) ) {
|
||||
#
|
||||
# Add to the existing array
|
||||
#
|
||||
push( @{ $tag_ids{$tag} }, $h1->{id} );
|
||||
}
|
||||
else {
|
||||
#
|
||||
# Create the episode array
|
||||
#
|
||||
$tag_ids{$tag} = [ $h1->{id} ];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Dumps the whole tags table. Warning!
|
||||
#
|
||||
_debug( $DEBUG > 2, '%tag_ids: ' . Dumper( \%tag_ids ) );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Make an alphabetic index of the tags
|
||||
#-------------------------------------------------------------------------------
|
||||
$lastkey = '';
|
||||
foreach my $tag ( sort( keys(%tag_ids) ) ) {
|
||||
if ( substr( $tag, 0, 1 ) ne $lastkey ) {
|
||||
$lastkey = substr( $tag, 0, 1 );
|
||||
push( @tagindex, $tag );
|
||||
}
|
||||
}
|
||||
|
||||
_debug( $DEBUG > 1, '@tagindex: ' . Dumper( \@tagindex ) );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Fill and print the template
|
||||
#-------------------------------------------------------------------------------
|
||||
my $tt = Template->new(
|
||||
{ ABSOLUTE => 1,
|
||||
ENCODING => 'utf8',
|
||||
INCLUDE_PATH => $basedir,
|
||||
OUTPUT_PATH => '.',
|
||||
}
|
||||
);
|
||||
my $vars = {
|
||||
tag_ids => \%tag_ids,
|
||||
tagindex => \@tagindex,
|
||||
titles => \%showtitles,
|
||||
};
|
||||
my $document;
|
||||
$tt->process( $templatefile, $vars, \$document, { binmode => ':utf8' } )
|
||||
|| die $tt->error(), "\n";
|
||||
|
||||
print $outfh $document;
|
||||
close($outfh);
|
||||
|
||||
$dbh->disconnect;
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _debug
|
||||
# PURPOSE: Prints debug reports
|
||||
# PARAMETERS: $active Boolean: 1 for print, 0 for no print
|
||||
# $message Message to print
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: Outputs a message if $active is true. It removes any trailing
|
||||
# newline and then adds one in the 'print' to the caller doesn't
|
||||
# have to bother. Prepends the message with 'D> ' to show it's
|
||||
# a debug message.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub _debug {
|
||||
my ( $active, $message ) = @_;
|
||||
|
||||
chomp($message);
|
||||
print "D> $message\n" if $active;
|
||||
}
|
||||
|
||||
#=== 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", "doc", "debug=i", "template=s", "out=s", "config=s", );
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
=head1 NAME
|
||||
|
||||
make_tag_index - Generate a tag index from the tags in the database
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to make_tag_index version 0.0.2
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
./make_tag_index [-help] [-doc] [-debug=N] [-template=FILE] [-out=FILE]
|
||||
[-config=FILE]
|
||||
|
||||
./make_tag_index -help
|
||||
./make_tag_index -doc
|
||||
./make_tag_index -out=tags.php
|
||||
./make_tag_index -template=MTI_1.tpl -out=tags.php
|
||||
./make_tag_index -config=$HOME/HPR/.hpr_livedb.cfg -out=tags.php
|
||||
|
||||
=head1 OPTIONS
|
||||
|
||||
=over 8
|
||||
|
||||
=item B<-help>
|
||||
|
||||
Prints a brief help message describing the usage of the program, and then exits.
|
||||
|
||||
=item B<-doc>
|
||||
|
||||
Displays the entirety of the documentation (using a pager), and then exits. To
|
||||
generate a PDF version use:
|
||||
|
||||
pod2pdf make_tag_index --out=make_tag_index.pdf
|
||||
|
||||
|
||||
=item B<-debug=N>
|
||||
|
||||
Causes certain debugging information to be displayed.
|
||||
|
||||
0 (the default) no debug output
|
||||
1 N/A
|
||||
2 dumps @tagindex an array containing tags and show numbers for the index
|
||||
3 dumps %tag_ids the data used to build the entire tag list (warning!)
|
||||
|
||||
=item B<-out=FILE>
|
||||
|
||||
This option defines an output file to receive the report. If the option is
|
||||
omitted the report is written to STDOUT, allowing it to be redirected if
|
||||
required.
|
||||
|
||||
=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<-template=FILE>
|
||||
|
||||
This option defines the template used to generate the tag index. The template
|
||||
is written using the B<Template> toolkit language.
|
||||
|
||||
If the option is omitted then the script uses the file
|
||||
B<make_tag_index.tpl> in the same directory as the script. If this file
|
||||
does not exist then the script will exit with an error message.
|
||||
|
||||
=back
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
The script reads all episodes in the HPR database. Each row contains a 'tags'
|
||||
field which contains tags as a comma-separated list. This list is parsed and
|
||||
stored in a Perl hash. The hash is keyed by the lower-case tag and the value
|
||||
part of each hash element contains a Perl arrayref containing a list of show
|
||||
numbers. The tag/show hash is called B<%tag_ids>. There are over 5800 tags in
|
||||
the system in September 2022.
|
||||
|
||||
An array called B<@tagindex> is also created which holds the first tag of each
|
||||
group starting with the same character. So, with a particular tag population,
|
||||
the 'a' group might start with 'aaron newcomb', 'b' with 'b+ tree' and so
|
||||
forth.
|
||||
|
||||
A further hash called B<%showtitles> is indexed by show number and holds the
|
||||
title of the show. This has been added in preparation for producing a tag
|
||||
index pages which have better accessibility features.
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<Unable to find template ...>
|
||||
|
||||
Type: fatal
|
||||
|
||||
The template specified does not exist.
|
||||
|
||||
=item B<Unable to open ... for writing: ...>
|
||||
|
||||
Type: fatal
|
||||
|
||||
The nominated output file cannot be written to.
|
||||
|
||||
=item B<Unable to initialise for writing: ...>;
|
||||
|
||||
Type: fatal
|
||||
|
||||
Trying to write to STDOUT has failed.
|
||||
|
||||
=item B<Unable to find ...>
|
||||
|
||||
Type: fatal
|
||||
|
||||
The nominated configuration file cannot be found
|
||||
|
||||
=item B<various database errors>
|
||||
|
||||
Type: fatal
|
||||
|
||||
Failure while opening the database or preparing a query.
|
||||
|
||||
=item B<Errors from Template Toolkit>
|
||||
|
||||
Type: fatal
|
||||
|
||||
The template could not be processed
|
||||
|
||||
=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
|
||||
Getopt::Long
|
||||
Pod::Usage
|
||||
Template
|
||||
Template::Filters
|
||||
Text::CSV_XS
|
||||
|
||||
=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) 2022 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.
|
||||
|
||||
=cut
|
||||
|
||||
#}}}
|
||||
|
||||
# [zo to open fold, zc to close]
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
85
Database/make_tag_index.tpl
Normal file
85
Database/make_tag_index.tpl
Normal file
@@ -0,0 +1,85 @@
|
||||
[%# make_tag_index.tpl 2022-09-14 -%]
|
||||
[%# version: 0.0.3 -%]
|
||||
[%# Default page summarising tag data from the database, generates PHP -%]
|
||||
[%- USE date -%]
|
||||
[%- DEFAULT title = 'Tag summary' -%]
|
||||
<?php
|
||||
# released under a Creative Commons Attribution-ShareAlike 3.0 Unported license. http://creativecommons.org/licenses/by-sa/3.0/
|
||||
$body="help_out";
|
||||
require "/home/hpr/php/hpr.php";
|
||||
include '/home/hpr/www/header.php';
|
||||
?>
|
||||
|
||||
<main id="maincontent">
|
||||
<style>
|
||||
.columns3 { columns: 3 auto; list-style-type: none }
|
||||
hr.thin {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
p.ralign { text-align: right }
|
||||
</style>
|
||||
<article>
|
||||
<a id="TOP"><h1 class="title">[% title %]</h1></a>
|
||||
<h4 class="date">Page generated on [% date.format(date.now,'%Y-%m-%d at %H:%M:%S UTC','en_GB',1) %]</h4>
|
||||
<p>This section summarises all of the tags currently used throughout the
|
||||
database. The tags are in alphabetical order and each is followed by links to
|
||||
the show numbers where it is used so you can see the context the author used
|
||||
it in. There are currently [% tag_ids.size %] unique tags in the system.</p>
|
||||
<h4>Alphabetical index</h4>
|
||||
<p>This is an index to the initial letters of the tags below.</p>
|
||||
<ul class="columns3">
|
||||
[%# tagindex contains the first tag in an alphabetic list that has a different
|
||||
first letter from the previous one. We use it to build an alphabetic table of
|
||||
anchors linking to the blocks of tags starting with that character. -%]
|
||||
[%- FOREACH index IN tagindex %]
|
||||
<li><a href="#[% index.replace('\s','_') %]"><strong>[% index.substr(0,1) %]</strong></a></li>
|
||||
[%- END %]
|
||||
</ul>
|
||||
<hr/>
|
||||
[%# BLOCK tags -%]
|
||||
<ul>
|
||||
[%# tag_ids is a hash keyed by tags, each containing an array of episode
|
||||
numbers. If a tag matches the one in 'index' place an anchor to it for the
|
||||
alphabetic index above. %]
|
||||
[%- index = tagindex.shift %]
|
||||
[%- FOREACH pair IN tag_ids.pairs %]
|
||||
[%- IF pair.key == index %]
|
||||
</ul>
|
||||
<p class="ralign"><a href="#TOP">🡱 Go to index</a></p>
|
||||
<h3>Tags beginning with '[% index.substr(0,1) %]'</h3>
|
||||
<ul>
|
||||
<li>
|
||||
[%- index = tagindex.shift %]
|
||||
[%- ELSE %]
|
||||
<li>
|
||||
[%- END %]
|
||||
<a id="[% pair.key.replace('\s','_') %]"><strong>[% pair.key FILTER html_entity %]</strong></a>:
|
||||
[%- count = 0 %]
|
||||
[%- FOREACH id IN pair.value.nsort %]
|
||||
[%- count = count + 1 %]
|
||||
<a href="https://hackerpublicradio.org/eps.php?id=[% id FILTER format("%04i") %]"
|
||||
target="_blank" aria-label="[% pair.key FILTER html_entity %] - show [% id %]">[% id %]</a>
|
||||
[%- count < pair.value.size ? ', ' : '' %]
|
||||
[%- END %]
|
||||
</li>
|
||||
[%- END %]
|
||||
</ul>
|
||||
[%# END -%]
|
||||
|
||||
<p>
|
||||
<a href="#TOP">Go to TOP of page</a>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
</main>
|
||||
|
||||
<?php
|
||||
include 'footer.html';
|
||||
?>
|
||||
|
||||
[%#
|
||||
vim: syntax=tt2:ts=8:sw=4:ai:et:tw=78:fo=tcrqn21
|
||||
-%]
|
332
Database/make_tsu_blank
Executable file
332
Database/make_tsu_blank
Executable file
@@ -0,0 +1,332 @@
|
||||
#!/bin/bash -
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: make_tsu_blank
|
||||
#
|
||||
# USAGE: ./make_tsu_blank [-h] [-D] start count
|
||||
#
|
||||
# DESCRIPTION: Make a template for generating a tag and summary update email.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: Now obsolete but retained for reference purposes
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.8
|
||||
# CREATED: 2016-05-28 16:21:22
|
||||
# REVISION: 2021-06-23 13:03:31
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
set -o nounset # Treat unset variables as an error
|
||||
|
||||
SCRIPT=${0##*/}
|
||||
|
||||
VERSION="0.0.8"
|
||||
|
||||
STDOUT="/dev/fd/2"
|
||||
|
||||
#
|
||||
# Load library functions
|
||||
#
|
||||
LIB="$HOME/bin/function_lib.sh"
|
||||
[ -e "$LIB" ] || { echo "$SCRIPT: Unable to source functions"; exit 1; }
|
||||
# shellcheck source=/home/cendjm/bin/function_lib.sh
|
||||
source "$LIB"
|
||||
|
||||
#
|
||||
# Colour codes
|
||||
#
|
||||
define_colours
|
||||
|
||||
#
|
||||
# We need the SSH tunnel (The script to test this and to open it, open_tunnel,
|
||||
# are in ~/bin. This needs to be set up if running this stuff somewhere else)
|
||||
#
|
||||
if ! tunnel_is_open; then
|
||||
echo "$SCRIPT: ${red}The SSH tunnel must be open to do this${reset}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _usage
|
||||
# DESCRIPTION: Report usage
|
||||
# PARAMETERS: None
|
||||
# RETURNS: Nothing
|
||||
#===============================================================================
|
||||
_usage () {
|
||||
cat >$STDOUT <<-endusage
|
||||
Usage: ./${SCRIPT} [-h] [-d] [-D] start count
|
||||
|
||||
Version: $VERSION
|
||||
|
||||
Generates a file of tag and summary updates for shows in the given range which
|
||||
can be edited and submitted to tags@hackerpublicradio.org in order to update
|
||||
the relevant shows.
|
||||
|
||||
Options:
|
||||
-h Print this help
|
||||
-D Select debug mode (works the same; more output)
|
||||
|
||||
Arguments:
|
||||
start starting show number
|
||||
count number of shows (shouldn't exceed 20)
|
||||
|
||||
Examples
|
||||
./${SCRIPT} -h
|
||||
./${SCRIPT} -D 700 10
|
||||
|
||||
endusage
|
||||
exit
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _DEBUG
|
||||
# DESCRIPTION: Writes a message if in DEBUG mode
|
||||
# PARAMETERS: List of messages
|
||||
# RETURNS: Nothing
|
||||
#===============================================================================
|
||||
_DEBUG () {
|
||||
[ "$DEBUG" == 0 ] && return
|
||||
for msg in "$@"; do
|
||||
printf 'D> %s\n' "$msg"
|
||||
done
|
||||
}
|
||||
|
||||
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
#
|
||||
# Option defaults
|
||||
#
|
||||
DEBUG=0
|
||||
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
while getopts :hdD opt
|
||||
do
|
||||
case "${opt}" in
|
||||
h) _usage;;
|
||||
D) DEBUG=1;;
|
||||
?) echo "$SCRIPT: Invalid option; aborting"; exit 1;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
|
||||
#
|
||||
# Directories and files
|
||||
#
|
||||
BASEDIR="$HOME/HPR/Database"
|
||||
TSU="$BASEDIR/tsu"
|
||||
|
||||
PREFIX="tag_summary_updates_"
|
||||
GENERATOR="$BASEDIR/query2tt2"
|
||||
LIVECFG="$BASEDIR/.hpr_livedb.cfg"
|
||||
TEMPLATE="$BASEDIR/query2tt2_taglist.tpl"
|
||||
|
||||
#
|
||||
# Sanity checks
|
||||
#
|
||||
[ -d "$BASEDIR" ] || { echo "Unable to find directory $BASEDIR"; exit 1; }
|
||||
[ -d "$TSU" ] || { echo "Unable to find directory $TSU"; exit 1; }
|
||||
|
||||
for item in $GENERATOR $LIVECFG $TEMPLATE; do
|
||||
[ -e "$item" ] || {
|
||||
echo "Unable to find component: $item"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
#
|
||||
# Maximum number of shows to scan. This is advisory since we might want to
|
||||
# scan 40 and only get 3 which need work!
|
||||
#
|
||||
LIMIT=20
|
||||
|
||||
#
|
||||
# Check arguments
|
||||
#
|
||||
if [[ $# -ne 2 ]]; then
|
||||
_usage
|
||||
fi
|
||||
|
||||
#
|
||||
# Validate arguments and make the END variable
|
||||
#
|
||||
START="$1"
|
||||
COUNT="$2"
|
||||
|
||||
RE='^[0-9]+$'
|
||||
if ! [[ $START =~ $RE ]]; then
|
||||
echo "${red}Invalid starting value: $1${reset}"
|
||||
exit 1
|
||||
fi
|
||||
if ! [[ $COUNT =~ $RE ]]; then
|
||||
echo "${red}Invalid count value: $2${reset}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#
|
||||
# Deal with leading zeroes if any by forcing such numbers to base 10
|
||||
#
|
||||
START=$((10#$START))
|
||||
COUNT=$((10#$COUNT))
|
||||
((END = START + COUNT - 1))
|
||||
|
||||
_DEBUG "Start: $START" "Count: $COUNT" "End: $END"
|
||||
|
||||
#
|
||||
# Argument sanity checks
|
||||
#
|
||||
if [[ $COUNT -gt $LIMIT ]]; then
|
||||
echo "${yellow}Range: $START..$END ($COUNT)${reset}"
|
||||
echo "${yellow}You are asking for a count greater than 20.${reset}"
|
||||
echo "${red}Beware! This could be unmanageable!${reset}"
|
||||
if ! yes_no 'Are you sure you want this? %s ' 'N'; then
|
||||
echo "${red}Request ignored. Please try again.${reset}"
|
||||
exit
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Generate the output file path
|
||||
#
|
||||
printf -v OUTFILE "%s/%s%04d-%04d.txt" "$TSU" "$PREFIX" "$START" "$END"
|
||||
|
||||
_DEBUG "Output: $OUTFILE"
|
||||
|
||||
#
|
||||
# Does the output file exist? If so, can we detect any work having been done
|
||||
# to it?
|
||||
#
|
||||
overwrite=0
|
||||
if [[ -e $OUTFILE ]]; then
|
||||
if [[ -s $OUTFILE ]]; then
|
||||
echo "${yellow}${OUTFILE/$HOME/\~} already exists.${reset}"
|
||||
if grep -E -q "^(summary|tags): ?\w+" "$OUTFILE"; then
|
||||
echo -n "${yellow}** Work has been done on this file"
|
||||
missing=$(grep -E -c "^(summary|tags): *$" "$OUTFILE")
|
||||
if ((missing)); then
|
||||
echo " (there are still tags/summaries to be added).${reset}"
|
||||
else
|
||||
echo ".${reset}"
|
||||
fi
|
||||
else
|
||||
echo "${yellow}This file has not had tags or summaries added.${reset}"
|
||||
fi
|
||||
|
||||
if ! yes_no 'Are you sure you want to replace it? %s ' 'N'; then
|
||||
echo "${red}File not overwritten${reset}"
|
||||
exit
|
||||
else
|
||||
overwrite=1
|
||||
fi
|
||||
else
|
||||
#
|
||||
# This shouldn't happen. An empty file caused by a failed query or
|
||||
# because there's nothing to do should be cleared away immediately
|
||||
# rather than here where the file has been left hanging around.
|
||||
#
|
||||
echo "${yellow}${OUTFILE/$HOME/\~} exists but is empty. Deleting it.${reset}"
|
||||
rm -f "$OUTFILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
_DEBUG "Overwrite: $overwrite"
|
||||
|
||||
#
|
||||
# If we're overwriting no collision check otherwise check check check!
|
||||
#
|
||||
if [[ $overwrite -eq 0 ]]; then
|
||||
#
|
||||
# Check for collisions.
|
||||
#
|
||||
# Look for individual files already created, taking the FROM and TO values
|
||||
# from their names. Look to see if the range START-END is in the range FROM-TO
|
||||
# or the other way round. Print all collisions. Any found mean the script
|
||||
# can't continue.
|
||||
#
|
||||
# Note that we have to force numbers to base 10 in case they have leading
|
||||
# zeroes (and will therefore be treated as octal).
|
||||
#
|
||||
collisions=0
|
||||
FILERE="${PREFIX}([0-9]{4})-([0-9]{4})\\.txt$"
|
||||
for f in "$TSU"/"${PREFIX}"*; do
|
||||
if [[ $f =~ $FILERE ]]; then
|
||||
FROM="${BASH_REMATCH[1]}"
|
||||
FROM=$((10#$FROM))
|
||||
TO="${BASH_REMATCH[2]}"
|
||||
TO=$((10#$TO))
|
||||
if [[ (( $START -ge $FROM && $START -le $TO ) ||\
|
||||
( $END -ge $FROM && $END -le $TO )) || \
|
||||
(( $FROM -ge $START && $FROM -le $END ) ||\
|
||||
( $TO -ge $START && $TO -le $END )) ]]; then
|
||||
printf \
|
||||
'%sCollision: range %04d-%04d overlaps the range %04d-%04d (in '%s')%s\n' \
|
||||
"${red}" "$START" "$END" "$FROM" "$TO" "${f##*/}" "${reset}"
|
||||
((collisions++))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $collisions -gt 0 ]]; then
|
||||
echo "${red}Found $collisions collisions; aborting${reset}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
#
|
||||
# Define the SQL.
|
||||
# 2021-06-20: Now we make a simpler query and rely on a script and template to
|
||||
# format everything.
|
||||
#
|
||||
SQL=$(cat <<ENDSQL
|
||||
SELECT
|
||||
id, summary, tags
|
||||
FROM eps
|
||||
WHERE id BETWEEN $START AND $END
|
||||
AND (length(summary) = 0 OR length(tags) = 0)
|
||||
ORDER BY id
|
||||
ENDSQL
|
||||
)
|
||||
|
||||
_DEBUG "----" "$SQL" "----"
|
||||
|
||||
|
||||
#
|
||||
# Run MySQL with the query.
|
||||
# 2021-06-20: The script below does all we want using the predefined template
|
||||
#
|
||||
$GENERATOR -config="$LIVECFG" -template="$TEMPLATE" "$SQL" > "$OUTFILE"
|
||||
RES=$?
|
||||
|
||||
#
|
||||
# Die if the query failed, and clear up the empty output file if found
|
||||
#
|
||||
[ $RES -eq 0 ] || {
|
||||
echo "${red}Query failed; aborting${reset}"
|
||||
if [[ -e $OUTFILE && ! -s $OUTFILE ]]; then
|
||||
rm -f "$OUTFILE"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
#
|
||||
# An empty file could be "successfully" created. If so we delete it
|
||||
#
|
||||
if [[ -s $OUTFILE ]]; then
|
||||
#
|
||||
# Report the file created.
|
||||
#
|
||||
# 2021-06-20: The original sed call is not needed any more because the
|
||||
# script we ran made the file in the form we want.
|
||||
#
|
||||
echo "${green}Output is in ${OUTFILE/$HOME/\~}${reset}"
|
||||
else
|
||||
rm -f "$OUTFILE"
|
||||
echo "${yellow}No episodes need work in that range${reset}"
|
||||
fi
|
||||
|
||||
exit
|
||||
|
||||
# vim: syntax=sh:ts=8:sw=4:ai:et:tw=78:fo=tcrqn21
|
32
Database/new_hosts_in_last_year.sql
Normal file
32
Database/new_hosts_in_last_year.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
/* -----------------------------------------------------------------------------
|
||||
* Find all new hosts who joined in the last year (ignoring those with queued
|
||||
* shows in the future)
|
||||
*
|
||||
*/
|
||||
SELECT h.hostid,
|
||||
h.host,
|
||||
min(e.date) AS joindate,
|
||||
count(e.id) AS COUNT
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
GROUP BY h.hostid
|
||||
HAVING min(e.date) >= (curdate() - INTERVAL 364 DAY)
|
||||
AND min(e.date) <= curdate()
|
||||
ORDER BY min(e.date);
|
||||
|
||||
/* -----------------------------------------------------------------------------
|
||||
* Total shows produced by the new hosts in the past year
|
||||
*/
|
||||
SELECT sum(COUNT) AS total_shows
|
||||
FROM
|
||||
(SELECT h.hostid,
|
||||
h.host,
|
||||
min(e.date) AS joindate,
|
||||
count(e.id) AS COUNT
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
GROUP BY h.hostid
|
||||
HAVING min(e.date) >= (curdate() - INTERVAL 364 DAY)
|
||||
AND min(e.date) <= curdate()
|
||||
ORDER BY min(e.date)) src;
|
||||
|
114
Database/normalise_tags.sql
Normal file
114
Database/normalise_tags.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Define a function to return a particular element from a comma-delimited
|
||||
* string. There is nothing already present in MySQL to do this.
|
||||
*
|
||||
* Create a table to hold the split tags, storing them in lower- and
|
||||
* upper-case form.
|
||||
*
|
||||
* Define a procedure to do the work of visiting every row in the 'eps' table
|
||||
* to extract the tags and place them in the 'tags' table with the episode id
|
||||
* they are associated with. This could be run on a periodic basis ('call
|
||||
* NormaliseEpisodeTags()') preceded by the statement 'DELETE FROM tags;'. The
|
||||
* 'lctags' column needs to be created with the statement 'UPDATE tags SET
|
||||
* lctag = LOWER(tag);'.
|
||||
*
|
||||
* With the 'tags' table filled then it can be queried for tag information as
|
||||
* shown in the examples below.
|
||||
*
|
||||
* 1. To count tag frequencies (case insensitive) and show the top 50:
|
||||
*
|
||||
* SELECT tag,lctag,COUNT(tag) AS freq FROM tags GROUP BY tag ORDER BY COUNT(tag) DESC LIMIT 50;
|
||||
*
|
||||
* 2. To return the episode numbers of shows tagged with a particular word:
|
||||
*
|
||||
* SELECT e.id,e.date,e.title,h.host FROM eps e JOIN hosts h ON e.hostid = h.hostid
|
||||
* WHERE e.id IN (SELECT id FROM tags WHERE lctag = 'linux');
|
||||
*
|
||||
* ----------------------------------------------------------------------------
|
||||
* (These ideas were based upon the discussions at
|
||||
* https://stackoverflow.com/questions/17942508/sql-split-values-to-multiple-rows)
|
||||
* ----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
/*
|
||||
* Create function 'strSplit'
|
||||
*
|
||||
* Arguments:
|
||||
* x - string to work on
|
||||
* delim - delimiter to split on
|
||||
* pos - starting position
|
||||
*
|
||||
*/
|
||||
DROP FUNCTION IF EXISTS strSplit;
|
||||
|
||||
CREATE FUNCTION strSplit(x VARCHAR(65000), delim VARCHAR(12), pos INTEGER)
|
||||
RETURNS VARCHAR(65000)
|
||||
BEGIN
|
||||
DECLARE output VARCHAR(65000);
|
||||
SET output = TRIM(
|
||||
REPLACE(
|
||||
SUBSTRING(
|
||||
SUBSTRING_INDEX(x, delim, pos),
|
||||
LENGTH(SUBSTRING_INDEX(x, delim, pos - 1)) + 1
|
||||
),
|
||||
delim,
|
||||
''
|
||||
)
|
||||
);
|
||||
IF output = '' THEN
|
||||
SET output = null;
|
||||
END IF;
|
||||
RETURN output;
|
||||
END $$
|
||||
|
||||
/*
|
||||
* Create procedure 'NormaliseEpisodeTags'
|
||||
*
|
||||
*/
|
||||
DROP PROCEDURE IF EXISTS NormaliseEpisodeTags;
|
||||
|
||||
CREATE PROCEDURE NormaliseEpisodeTags()
|
||||
BEGIN
|
||||
DECLARE i INTEGER;
|
||||
|
||||
SET i = 1;
|
||||
REPEAT
|
||||
INSERT INTO tags (id, tag, lctag)
|
||||
SELECT id, strSplit(tags, ',', i), lower(strSplit(tags, ',', i))
|
||||
FROM eps
|
||||
WHERE strSplit(tags, ',', i) IS NOT NULL;
|
||||
SET i = i + 1;
|
||||
UNTIL ROW_COUNT() = 0
|
||||
END REPEAT;
|
||||
END $$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
/*
|
||||
* Create table 'tags'
|
||||
*
|
||||
*/
|
||||
DROP TABLE IF EXISTS tags;
|
||||
|
||||
CREATE TABLE tags (
|
||||
id int(5) NOT NULL,
|
||||
tag varchar(200),
|
||||
lctag varchar(200)
|
||||
);
|
||||
|
||||
-- DROP INDEX tags_all ON tags;
|
||||
CREATE UNIQUE INDEX tags_all ON tags (id,tag,lctag);
|
||||
|
||||
-- DROP INDEX tags_id ON tags;
|
||||
CREATE INDEX tags_id ON tags (id);
|
||||
|
||||
-- DROP INDEX tags_tag ON tags;
|
||||
CREATE INDEX tags_tag ON tags (tag);
|
||||
|
||||
-- DROP INDEX tags_lctag ON tags;
|
||||
CREATE INDEX tags_lctag ON tags (lctag);
|
||||
|
||||
|
||||
-- vim: syntax=sql:ts=8:ai:tw=78:et:fo=tcrqn21:comments+=b\:--
|
1472
Database/process_mail_tags
Executable file
1472
Database/process_mail_tags
Executable file
File diff suppressed because it is too large
Load Diff
137
Database/query2csv
Executable file
137
Database/query2csv
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: query2csv
|
||||
#
|
||||
# USAGE: ./query2csv query
|
||||
#
|
||||
# DESCRIPTION: Runs a query given as the only argument. Caution is needed
|
||||
# since *any* query will be run. The result of the query is
|
||||
# output in CSV form on STDOUT. The CSV is always quoted to
|
||||
# cater for the more simplistic consumers.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: Had to revert to MySQL because of a problem with DBD::MariaDB
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.2
|
||||
# CREATED: 2015-07-11 15:53:01
|
||||
# REVISION: 2022-02-16 23:17:16
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Config::General;
|
||||
use Text::CSV_XS;
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.2';
|
||||
|
||||
#
|
||||
# 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_livedb.cfg";
|
||||
|
||||
my ( $dbh, $sth1, $aref1 );
|
||||
my ( $query, $csv );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load database configuration data
|
||||
#
|
||||
my $conf = Config::General->new(
|
||||
-ConfigFile => $configfile,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1
|
||||
);
|
||||
my %config = $conf->getall();
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
$query = shift;
|
||||
die "Usage: $PROG query\n" unless $query;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 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:MariaDB:host=$dbhost;port=$dbport;database=$dbname",
|
||||
# $dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
# or die $DBI::errstr;
|
||||
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#
|
||||
# Set up the query
|
||||
#
|
||||
$sth1 = $dbh->prepare($query) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Perform the query
|
||||
#
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Prepare to make CSV. Not sure if always quoting is the best idea though
|
||||
#
|
||||
$csv = Text::CSV_XS->new(
|
||||
# { always_quote => 1 }
|
||||
);
|
||||
|
||||
#
|
||||
# Loop through the returned rows making and printing CSV. Each row is returned
|
||||
# as an arrayref to make it easy to join everything.
|
||||
#
|
||||
while ( $aref1 = $sth1->fetchrow_arrayref ) {
|
||||
$csv->combine(@$aref1);
|
||||
print $csv->string(), "\n";
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
134
Database/query2json
Executable file
134
Database/query2json
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: query2json
|
||||
#
|
||||
# USAGE: ./query2json query
|
||||
#
|
||||
# DESCRIPTION: Runs a query given as the only argument. Caution is needed
|
||||
# since *any* query will be run. The result of the query is
|
||||
# output in JSON form on STDOUT.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: Had to revert to MySQL because of a problem with DBD::MariaDB
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.2
|
||||
# CREATED: 2021-06-18 13:24:49
|
||||
# REVISION: 2023-01-05 16:17:24
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Config::General;
|
||||
use JSON;
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.2';
|
||||
|
||||
#
|
||||
# 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_livedb.cfg";
|
||||
|
||||
my ( $dbh, $sth1, $aref1 );
|
||||
my ( $query, $result, $json );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load database configuration data
|
||||
#
|
||||
my $conf = Config::General->new(
|
||||
-ConfigFile => $configfile,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1
|
||||
);
|
||||
my %config = $conf->getall();
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
$query = shift;
|
||||
die "Usage: $PROG query\n" unless $query;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 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:MariaDB:host=$dbhost;port=$dbport;database=$dbname",
|
||||
# $dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
# or die $DBI::errstr;
|
||||
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#
|
||||
# Set up the query
|
||||
#
|
||||
$sth1 = $dbh->prepare($query) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Perform the query
|
||||
#
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Grab everything as an arrayref of hashrefs
|
||||
#
|
||||
$result = $sth1->fetchall_arrayref( {} );
|
||||
|
||||
#
|
||||
# Prepare for JSON, forcing object key sorting (expensive)
|
||||
#
|
||||
$json = JSON->new->utf8->canonical;
|
||||
|
||||
#
|
||||
# Encode the Perl structure to JSON
|
||||
#
|
||||
print $json->encode($result), "\n";
|
||||
|
||||
exit;
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
626
Database/query2tt2
Executable file
626
Database/query2tt2
Executable file
@@ -0,0 +1,626 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: query2tt2
|
||||
#
|
||||
# USAGE: ./query2tt2 [-help] [-debug=N] [-config=FILE] [-query=FILE]
|
||||
# [-template=FILE]
|
||||
# [-dbarg=ARG1 [-dbarg=ARG2] ...]
|
||||
# [-define KEY1=VALUE1 [-define KEY2=VALUE2] ...
|
||||
# [-define KEYn=VALUEn]] [QUERY]
|
||||
#
|
||||
# DESCRIPTION: Built for use with the Hacker Public Radio database, but could
|
||||
# be used in any context with a MariaDB database.
|
||||
# Runs a query given as the only argument (or in a file).
|
||||
# Caution is needed since *any* query will be run, not just
|
||||
# SELECT commands. The result of the query is output in
|
||||
# a specified format defined by a template on STDOUT. The query
|
||||
# can have arguments provided by '-dbarg=ARG' to be used in '?'
|
||||
# placeholders in the SQL. The template can receive variables
|
||||
# through the option '-define KEY=VALUE'. A configuration file
|
||||
# is needed, though there is a default ('.hpr_db.cfg'), which
|
||||
# accesses the local snapshot.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: Had to revert to MySQL because of a problem with DBD::MariaDB
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.4
|
||||
# CREATED: 2021-06-18 13:24:49
|
||||
# REVISION: 2024-01-19 17:15:45
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
use open ':encoding(UTF-8)';
|
||||
|
||||
# Using experimental features, some of which require warnings to be turned off
|
||||
use feature qw{ say try };
|
||||
no warnings qw{
|
||||
experimental::try
|
||||
};
|
||||
|
||||
use Getopt::Long;
|
||||
use Pod::Usage;
|
||||
|
||||
use Config::General;
|
||||
#use Try::Tiny;
|
||||
use File::Slurper qw{ read_text };
|
||||
use Hash::Merge;
|
||||
use Template;
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.4';
|
||||
|
||||
#
|
||||
# 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";
|
||||
|
||||
my ( $dbh, $sth1 );
|
||||
my ( $query, $result, @names, $document );
|
||||
|
||||
#
|
||||
# Default template iterates through all rows in the 'result' matrix and for
|
||||
# each row displays the field name (key) from array 'names', and its value.
|
||||
# There's a blank line after each row.
|
||||
#
|
||||
my $def_template = <<'ENDTPL';
|
||||
[% FOREACH row IN result -%]
|
||||
[% FOREACH key IN names -%]
|
||||
[% key %]: [% row.$key %]
|
||||
[% END -%]
|
||||
|
||||
[% END -%]
|
||||
ENDTPL
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# There should be no need to edit anything after this point
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
#
|
||||
# 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'} );
|
||||
|
||||
#
|
||||
# Full documentation if requested with -doc
|
||||
#
|
||||
pod2usage(
|
||||
-msg => "$PROG version $VERSION\n",
|
||||
-verbose => 2,
|
||||
-exitval => 1,
|
||||
-noperldoc => 0,
|
||||
) if ( $options{'doc'} );
|
||||
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $DEBUG = ( $options{'debug'} ? $options{'debug'} : 0 );
|
||||
|
||||
my $cfgfile
|
||||
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
||||
|
||||
my $queryfile = $options{query};
|
||||
my $template = $options{template};
|
||||
|
||||
my @dbargs = _dbargs( \%options );
|
||||
my %defs = _define( \%options );
|
||||
_debug( $DEBUG >= 3, '@dbargs: ' . join( ',', @dbargs ) );
|
||||
_debug( $DEBUG >= 3, '%defs: ' . Dumper(\%defs) );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Option checks and defaults
|
||||
#-------------------------------------------------------------------------------
|
||||
die "Unable to find configuration file $cfgfile\n" unless ( -e $cfgfile );
|
||||
_debug( $DEBUG >= 3, '$cfgfile: ' . $cfgfile );
|
||||
|
||||
#
|
||||
# Query is an argument string or is in a file
|
||||
#
|
||||
if ($queryfile) {
|
||||
die "Unable to find query file $queryfile\n" unless ( -e $queryfile );
|
||||
$query = read_text($queryfile);
|
||||
}
|
||||
else {
|
||||
$query = shift;
|
||||
pod2usage( -msg => "Please specify a SQL query\n", -exitval => 1 )
|
||||
unless $query;
|
||||
}
|
||||
_debug( $DEBUG >= 3, '$query: ' . Dumper(\$query) );
|
||||
|
||||
#
|
||||
# Template is the default pre-defined string or a filename
|
||||
#
|
||||
if ($template) {
|
||||
die "Unable to find template $template\n" unless ( -e $template );
|
||||
}
|
||||
else {
|
||||
$template = \$def_template;
|
||||
}
|
||||
_debug(
|
||||
$DEBUG >= 3,
|
||||
'$template: '
|
||||
. (ref($template) eq ''
|
||||
? "filename $template"
|
||||
: "reference to string\n$$template")
|
||||
);
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Load database configuration data
|
||||
#-------------------------------------------------------------------------------
|
||||
my $conf = Config::General->new(
|
||||
-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};
|
||||
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#
|
||||
# Set up the query
|
||||
#
|
||||
$sth1 = $dbh->prepare($query) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Perform the query
|
||||
#
|
||||
try {
|
||||
$sth1->execute(@dbargs);
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
}
|
||||
catch ($e) {
|
||||
#
|
||||
# The 'die' above was triggered. The error is in $_.
|
||||
#
|
||||
my $pcount = grep {/\?/} split( '', $query );
|
||||
my $acount = scalar(@dbargs);
|
||||
print STDERR "Failed to execute query.\n";
|
||||
print STDERR "Placeholder/Argument mismatch: $pcount/$acount\n";
|
||||
exit;
|
||||
};
|
||||
|
||||
#
|
||||
# Grab everything from the query as an arrayref of hashrefs
|
||||
#
|
||||
$result = $sth1->fetchall_arrayref( {} );
|
||||
_debug( $DEBUG >= 3, '$result: ' . Dumper($result) );
|
||||
|
||||
#
|
||||
# Collect field names
|
||||
#
|
||||
@names = @{$sth1->{NAME}};
|
||||
_debug( $DEBUG >= 3, '@names: ' . Dumper(\@names) );
|
||||
|
||||
#
|
||||
# Set up the template
|
||||
#
|
||||
my $tt = Template->new(
|
||||
{ ABSOLUTE => 1,
|
||||
ENCODING => 'utf8',
|
||||
INCLUDE_PATH => $basedir,
|
||||
}
|
||||
);
|
||||
|
||||
#
|
||||
# Send collected data to the template
|
||||
#
|
||||
my $vars = { names => \@names, result => $result, };
|
||||
if (%defs) {
|
||||
#
|
||||
# If we have definitions add them to $vars
|
||||
#
|
||||
my $merge = Hash::Merge->new('LEFT_PRECEDENT');
|
||||
my %merged = %{ $merge->merge( $vars, \%defs ) };
|
||||
$vars = \%merged;
|
||||
}
|
||||
_debug( $DEBUG >= 3, '$vars: ' . Dumper($vars) );
|
||||
|
||||
$tt->process( $template, $vars, \$document, { binmode => ':utf8' } )
|
||||
|| die $tt->error(), "\n";
|
||||
print $document;
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _debug
|
||||
# PURPOSE: Prints debug reports
|
||||
# PARAMETERS: $active Boolean: 1 for print, 0 for no print
|
||||
# $message Message to print
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: Outputs a message if $active is true. It removes any trailing
|
||||
# newline and then adds one in the 'print' to the caller doesn't
|
||||
# have to bother. Prepends the message with 'D> ' to show it's
|
||||
# a debug message.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub _debug {
|
||||
my ( $active, $message ) = @_;
|
||||
|
||||
chomp($message);
|
||||
print STDERR "D> $message\n" if $active;
|
||||
}
|
||||
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _dbargs
|
||||
# PURPOSE: Collects database arguments for the main query
|
||||
# PARAMETERS: $opts hash reference holding the options
|
||||
# RETURNS: An array holding all of the arguments
|
||||
# DESCRIPTION: If there are -dbargs options they will be an array in the hash
|
||||
# returned by Getopt::Long. We return the array to the caller.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub _dbargs {
|
||||
my ($opts) = @_;
|
||||
|
||||
my @args;
|
||||
|
||||
if ( defined( $opts->{dbargs} ) ) {
|
||||
@args = @{ $opts->{dbargs} };
|
||||
}
|
||||
|
||||
return (@args);
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _define
|
||||
# PURPOSE: Handles multiple instances of the same option '-define x=42'
|
||||
# PARAMETERS: $opts hash reference holding the options
|
||||
# RETURNS: A hash containing all of the named items (e.g. { 'x' => 42 })
|
||||
# DESCRIPTION: If there are -define options they will be a hashref in the hash
|
||||
# returned by Getopt::Long. We return the internal hash to the
|
||||
# caller. Doesn't handle the issue that we don't want the keys
|
||||
# 'names' and 'result', though perhaps it should.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO:
|
||||
#===============================================================================
|
||||
sub _define {
|
||||
my ($opts) = @_;
|
||||
|
||||
my %defs;
|
||||
|
||||
if ( defined( $opts->{define} ) ) {
|
||||
%defs = %{ $opts->{define} };
|
||||
}
|
||||
|
||||
return (%defs);
|
||||
}
|
||||
|
||||
#=== 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", "doc", "debug=i", "config=s",
|
||||
"query=s", "template=s", "dbargs=s@", "define=s%",
|
||||
);
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "Version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
=head1 NAME
|
||||
|
||||
query2tt2 - A script for formatting a report from database query using a template
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to query2tt2 version 0.0.4
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
query2tt2 [-help] [-debug=N] [-config=FILE] [-query=FILE]
|
||||
[-template=FILE] [QUERY]
|
||||
|
||||
query2tt2 -help
|
||||
|
||||
query2tt2 -query=tag_query_580-589.sql
|
||||
|
||||
query2tt2 -config=.hpr_livedb.cfg -template=query2tt2_taglist.tpl \
|
||||
'select id,summary,tags from eps where id between 580 AND 589 AND (length(summary) = 0 or length(tags) = 0) ORDER BY id'
|
||||
|
||||
query2tt2 -config=.hpr_livedb.cfg -query=hosts_showcount.sql \
|
||||
-dbargs '2021-01-01' -dbargs '2021-12-31' \
|
||||
-def year=2021 -template=~/HPR/Community_News/hosts_list.tpl
|
||||
|
||||
|
||||
=head1 OPTIONS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<-help>
|
||||
|
||||
Prints a brief help message describing the usage of the program, and then exits.
|
||||
|
||||
=item B<-doc>
|
||||
|
||||
Displays the entirety of the documentation (using a pager), and then exits. To
|
||||
generate a PDF version use:
|
||||
|
||||
pod2pdf query2tt2 --out=query2tt2.pdf
|
||||
|
||||
=item B<-debug=N>
|
||||
|
||||
Selects a level of debugging. Debug information consists of a line or series
|
||||
of lines prefixed with the characters 'D>':
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<0>
|
||||
|
||||
No debug output is generated: this is the default
|
||||
|
||||
=item B<3>
|
||||
|
||||
Prints all data structures from options or from the database
|
||||
|
||||
=back
|
||||
|
||||
(The debug levels need work!)
|
||||
|
||||
=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 HPR 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<-query=FILE>
|
||||
|
||||
The script needs an SQL query to be applied to the database. This may be
|
||||
supplied as a file, in which case this option gives the name of the file.
|
||||
|
||||
Alternatively the query can be given as a delimited string on the command
|
||||
line.
|
||||
|
||||
If neither method is used the script aborts with an error message.
|
||||
|
||||
=item B<-dbarg=ARG> [ B<-dbarg=ARG> ... ]
|
||||
|
||||
The query can have place holders ('?') in it and the corresponding values can
|
||||
be passed to the script through the B<-dbarg=ARG> option. The option can be
|
||||
repeated as many times as required and the order of B<ARG> values is
|
||||
preserved.
|
||||
|
||||
=item B<-template=FILE>
|
||||
|
||||
The results of the query are fed to the Template Toolkit system for
|
||||
reformatting. This option provides the name of the template definition file.
|
||||
If this option is omitted then the script uses a very simple internal template
|
||||
which is roughly equivalent to the effect in MySQL/MariaDB of ending a query
|
||||
with I<\G>.
|
||||
|
||||
See below in the B<DESCRIPTION> section for the constraints imposed on the
|
||||
contents of the template.
|
||||
|
||||
Output from the template is written to STDOUT.
|
||||
|
||||
=item B<-define KEY1=VALUE1> [ B<-define KEY2=VALUE2> ... B<-define KEYn=VALUEn> ]
|
||||
|
||||
The Template Toolkit (TT2) template may receive values from the command line
|
||||
using this option. The argument to the B<-define> option is a B<key=value>
|
||||
pair. Keys should be unique otherwise they will overwrite one another. The
|
||||
keys will become TT2 variables and the values will be assigned to them.
|
||||
|
||||
=back
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
The purpose of the script is to run a query against the HPR database (a local
|
||||
copy or the live one on the server over an SSH tunnel). The database choice is
|
||||
made via a configuration file. The default file points to the local database,
|
||||
but the alternative (discussed later) accesses the live database.
|
||||
|
||||
The data returned from the query is then passed through a Template Toolkit
|
||||
template so that it can be formatted. There are many ways in which this can be
|
||||
done. A default template is built into the script which displays the data in
|
||||
a very simple form.
|
||||
|
||||
A knowledge of the Template Toolkit package is required to write templates.
|
||||
|
||||
The template receives two data structures:
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<names>
|
||||
|
||||
This is an array of the field (column) names used in the query in the order
|
||||
they are referenced. This is to help with writing out fields in the same order
|
||||
as the query, if this is required.
|
||||
|
||||
=item B<result>
|
||||
|
||||
This is an array of hashes returned from the query. Relational databases
|
||||
return sets which are effectively tables or matrices of information. Perl
|
||||
represents this structure as an array of hashes where each array element
|
||||
corresponds to a row in the returned table, and each hash contains the fields
|
||||
or columns. Perl does not guarantee hash key ordering, so the B<names> array
|
||||
(above) is provided to ensure order is preserved.
|
||||
|
||||
=back
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
=over 4
|
||||
|
||||
=item B<Unable to find configuration file ...>
|
||||
|
||||
The nominated (or default) configuration file could not be found.
|
||||
|
||||
=item B<Unable to find query file ...>
|
||||
|
||||
The nominated query file could not be found.
|
||||
|
||||
=item B<Couldn't open ...: ...>
|
||||
|
||||
The nominated query file could not be opened.
|
||||
|
||||
=item B<Unable to find template file ...>
|
||||
|
||||
The nominated template file could not be found.
|
||||
|
||||
=item B<various database errors>
|
||||
|
||||
An error has occurred while performing a database operation.
|
||||
|
||||
=item B<Failed to execure query.>
|
||||
|
||||
There is a mismatch between the number of placeholders in the query ('?'
|
||||
characters) and the number of arguments provided through the B<-dbargs=ARG>
|
||||
option. The script will attempt to analyse whether there are too many or too
|
||||
few arguments
|
||||
|
||||
There is a mismatch between the number of placeholders in the query ('?'
|
||||
characters) and the number of arguments provided through the B<-dbargs=ARG>
|
||||
option. The script will attempt to analyse whether there are too many or too
|
||||
few arguments
|
||||
|
||||
=item B<Template Toolkit error>
|
||||
|
||||
An error has occurred while processing the template.
|
||||
|
||||
=back
|
||||
|
||||
=head1 CONFIGURATION AND ENVIRONMENT
|
||||
|
||||
The script obtains the credentials it requires to open the MariaDB database
|
||||
from a configuration file. The name of the file it expects is B<.hpr_db.cfg>
|
||||
in the directory holding the script. This configuration file can be overridden
|
||||
using the B<-config=FILE> option as described above.
|
||||
|
||||
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
|
||||
Getopt::Long
|
||||
Hash::Merge
|
||||
Pod::Usage
|
||||
Template
|
||||
|
||||
=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) 2021, 2022, 2024 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
|
20
Database/query2tt2_taglist.tpl
Normal file
20
Database/query2tt2_taglist.tpl
Normal file
@@ -0,0 +1,20 @@
|
||||
[%# query2tt2_taglist.tpl 2021-12-31 -%]
|
||||
[%# Template to make a template for missing summaries and tags -%]
|
||||
[%# Now obsolete; the tag/summary project has finished -%]
|
||||
[% FOREACH row IN result -%]
|
||||
show: [% row.id %]
|
||||
[% IF row.summary.length > 0 -%]
|
||||
#summary: [% row.summary %]
|
||||
[% ELSE -%]
|
||||
summary:
|
||||
[% END -%]
|
||||
[% IF row.tags.length > 0 -%]
|
||||
#tags: [% row.tags %]
|
||||
[% ELSE -%]
|
||||
tags:
|
||||
[% END -%]
|
||||
|
||||
[% END -%]
|
||||
[%#
|
||||
vim: syntax=tt2:ts=8:sw=4:ai:et:tw=78:fo=tcrqn21
|
||||
-%]
|
613
Database/refresh_tags
Executable file
613
Database/refresh_tags
Executable file
@@ -0,0 +1,613 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: refresh_tags
|
||||
#
|
||||
# USAGE: ./refresh_tags
|
||||
#
|
||||
# DESCRIPTION: Parse tags from the eps.tags field and use them to populate
|
||||
# the tags table. The eps tag list is definitive (though it's
|
||||
# quite limited since it's only 200 characters long), and so the
|
||||
# tags table is kept in step by adding and deleting.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.3
|
||||
# CREATED: 2016-07-17 15:59:24
|
||||
# REVISION: 2017-01-30 17:13:28
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Carp;
|
||||
use Getopt::Long;
|
||||
use Config::General;
|
||||
use Text::CSV;
|
||||
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";
|
||||
|
||||
my ( $dbh, $sth1, $h1 );
|
||||
my ( $status, @fields );
|
||||
my ( %eps_tags, %tags_tags, %diffs );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load configuration data
|
||||
#
|
||||
my $conf = Config::General->new(
|
||||
-ConfigFile => $configfile,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1,
|
||||
);
|
||||
my %config = $conf->getall();
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
Usage() if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $verbose = ( defined( $options{verbose} ) ? $options{verbose} : 0 );
|
||||
my $dry_run = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 1 );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 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;
|
||||
|
||||
my $csv = Text::CSV_XS->new;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Collect and process the id numbers and tags from the 'eps' table
|
||||
#-------------------------------------------------------------------------------
|
||||
%eps_tags = %{ collect_eps_tags( $dbh, $verbose ) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Collect any tags we've already stashed in the database
|
||||
#-------------------------------------------------------------------------------
|
||||
%tags_tags = %{ collect_db_tags( $dbh, $verbose ) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Now compare the two sources to look for differences
|
||||
#-------------------------------------------------------------------------------
|
||||
%diffs = %{ find_differences(\%eps_tags,\%tags_tags) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Perform the updates if there are any
|
||||
#-------------------------------------------------------------------------------
|
||||
if (%diffs) {
|
||||
print "Differences found\n";
|
||||
unless ($dry_run) {
|
||||
#
|
||||
# Loop through all of the actions by episode number
|
||||
#
|
||||
foreach my $id ( sort { $a <=> $b } keys(%diffs) ) {
|
||||
|
||||
#
|
||||
# Do deletions before additions
|
||||
#
|
||||
if ( exists( $diffs{$id}->{deletions} ) ) {
|
||||
do_deletions( $dbh, $verbose, $id, $diffs{$id}->{deletions} );
|
||||
}
|
||||
|
||||
#
|
||||
# Do additions after deletions
|
||||
#
|
||||
if ( exists( $diffs{$id}->{additions} ) ) {
|
||||
do_additions( $dbh, $sth1, $verbose, $id,
|
||||
$diffs{$id}->{additions} );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "No changes made - dry run\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "No differences found\n";
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: collect_eps_tags
|
||||
# PURPOSE: Collects the tags from the eps.tags field
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $verbose Verbosity level
|
||||
# RETURNS: A reference to the hash created by collecting all the tags
|
||||
# DESCRIPTION:
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub collect_eps_tags {
|
||||
my ( $dbh, $verbose ) = @_;
|
||||
|
||||
my ( $status, @fields, %hash );
|
||||
my ( $sth, $h );
|
||||
|
||||
#
|
||||
# For parsing the field as CSV
|
||||
# NOTE: Unexplained error in [E. E. "Doc" Smith] (show 2462). Works with
|
||||
# double replaced by single quote, but doesn't work if quotes escaped (by
|
||||
# doubling) whether all tags are quoted or not. With 'auto_diag' enabled
|
||||
# get the error:
|
||||
# CSV_XS ERROR: 2034 - EIF - Loose unescaped quote @ rec 1632 pos 40 field 3
|
||||
#
|
||||
# NOTE: Adding 'allow_loose_quotes' avoids the issue
|
||||
#
|
||||
my $csv = Text::CSV_XS->new(
|
||||
{ binary => 1, auto_diag => 1, allow_loose_quotes => 1 } );
|
||||
|
||||
#
|
||||
# Query the eps table for all the id and tags
|
||||
#
|
||||
$sth = $dbh->prepare(
|
||||
q{SELECT id,tags FROM eps
|
||||
WHERE length(tags) > 0
|
||||
ORDER BY id}
|
||||
) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
$sth->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Loop through what we got
|
||||
#
|
||||
while ( $h = $sth->fetchrow_hashref ) {
|
||||
#
|
||||
# Parse the tag list
|
||||
#
|
||||
$status = $csv->parse( $h->{tags} );
|
||||
unless ($status) {
|
||||
#
|
||||
# Report any errors
|
||||
#
|
||||
print "Parse error on episode ", $h->{id}, "\n";
|
||||
print $csv->error_input(), "\n";
|
||||
next;
|
||||
}
|
||||
@fields = $csv->fields();
|
||||
|
||||
next unless (@fields);
|
||||
|
||||
#
|
||||
# Trim all tags (don't alter $_ when doing it)
|
||||
#
|
||||
@fields = map {
|
||||
my $t = $_;
|
||||
$t =~ s/(^\s+|\s+$)//g;
|
||||
$t;
|
||||
} @fields;
|
||||
|
||||
#print "$h->{id}: ",join(",",@fields),"\n";
|
||||
|
||||
#
|
||||
# Save the id and its tags, sorted for comparison, with empty elements
|
||||
# removed too
|
||||
#
|
||||
$hash{ $h->{id} } = [ sort grep {!/^$/} @fields ];
|
||||
|
||||
}
|
||||
|
||||
#print Dumper(\%hash),"\n";
|
||||
|
||||
#
|
||||
# Dump all id numbers and tags if the verbose level is high enough
|
||||
#
|
||||
if ( $verbose >= 2 ) {
|
||||
print "\nTags collected from the 'eps' table\n\n";
|
||||
foreach my $id ( sort { $a <=> $b } keys(%hash) ) {
|
||||
printf "%04d: %s\n", $id, join( ",", @{ $hash{$id} } );
|
||||
}
|
||||
}
|
||||
|
||||
return \%hash;
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: collect_db_tags
|
||||
# PURPOSE: Collects the tags already stored in the database
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $verbose Verbosity level
|
||||
# RETURNS: A reference to the hash created by collecting all the tags
|
||||
# DESCRIPTION:
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub collect_db_tags {
|
||||
my ( $dbh, $verbose ) = @_;
|
||||
|
||||
my %hash;
|
||||
my ( $sth, $h );
|
||||
|
||||
#
|
||||
# Query the database for tag data
|
||||
#
|
||||
|
||||
$sth = $dbh->prepare(q{SELECT * FROM tags ORDER BY id})
|
||||
or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
$sth->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Loop through what we got building an array of tags per episode number
|
||||
#
|
||||
while ( $h = $sth->fetchrow_hashref ) {
|
||||
if ( defined( $hash{ $h->{id} } ) ) {
|
||||
push( @{ $hash{ $h->{id} } }, $h->{tag} );
|
||||
}
|
||||
else {
|
||||
$hash{ $h->{id} } = [ $h->{tag} ];
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Sort all the tag arrays for comparison
|
||||
#
|
||||
foreach my $id ( keys(%hash) ) {
|
||||
$hash{$id} = [ sort @{ $hash{$id} } ];
|
||||
}
|
||||
|
||||
#
|
||||
# Dump all id numbers and tags if the verbose level is high enough
|
||||
#
|
||||
if ( $verbose >= 2 ) {
|
||||
print "\nTags collected from the 'tags' table\n\n";
|
||||
foreach my $id ( sort { $a <=> $b } keys(%hash) ) {
|
||||
printf "%04d: %s\n", $id, join( ",", @{ $hash{$id} } );
|
||||
}
|
||||
print '=-' x 40,"\n";
|
||||
}
|
||||
|
||||
return \%hash;
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: find_differences
|
||||
# PURPOSE: Find the differences between two hashes containing tags
|
||||
# PARAMETERS: $master Reference to the master hash
|
||||
# $slave Reference to the slave hash
|
||||
# RETURNS: A reference to the hash created checking for differences
|
||||
# DESCRIPTION:
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub find_differences {
|
||||
my ($master,$slave) = @_;
|
||||
|
||||
my %hash;
|
||||
|
||||
foreach my $id ( sort { $a <=> $b } keys(%$master) ) {
|
||||
my %iddiffs = array_compare( $master->{$id}, $slave->{$id} );
|
||||
if (%iddiffs) {
|
||||
if ( $verbose >= 1 ) {
|
||||
#
|
||||
# Report what was found if asked to
|
||||
#
|
||||
print "Episode: $id\n";
|
||||
print "Update:\n\teps: ", join( ",", @{ $master->{$id} } ), "\n";
|
||||
print "\ttags: ",
|
||||
(
|
||||
defined( $slave->{$id} )
|
||||
? join( ",", @{ $slave->{$id} } )
|
||||
: '--None--' ), "\n";
|
||||
print '-' x 80,"\n";
|
||||
}
|
||||
$hash{$id} = {%iddiffs};
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Report differences and actions if the verbose level is high enough
|
||||
#
|
||||
if ( $verbose >= 2 ) {
|
||||
print "\nDifferences and actions\n\n";
|
||||
foreach my $id ( sort { $a <=> $b } keys(%hash) ) {
|
||||
print "Episode: $id\n";
|
||||
if ( exists( $hash{$id}->{deletions} ) ) {
|
||||
print "Deletions: ";
|
||||
print join( ",", @{ $hash{$id}->{deletions} } ), "\n";
|
||||
}
|
||||
if ( exists( $hash{$id}->{additions} ) ) {
|
||||
print "Additions: ";
|
||||
print join( ",", @{ $hash{$id}->{additions} } ), "\n";
|
||||
}
|
||||
print '-' x 80, "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return \%hash;
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: do_deletions
|
||||
# PURPOSE: Perform any deletions indicated in an array for a given
|
||||
# episode
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $verbose Verbosity level
|
||||
# $id Episode number
|
||||
# $tags Reference to an array of tags for this episode
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION:
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub do_deletions {
|
||||
my ( $dbh, $verbose, $id, $tags ) = @_;
|
||||
|
||||
my ( $stmt, @bind, %data, %where );
|
||||
|
||||
#
|
||||
# We will dynamically build SQL as we go
|
||||
#
|
||||
my $sql = SQL::Abstract->new;
|
||||
|
||||
#
|
||||
# Process the list of tags we have been given
|
||||
#
|
||||
for my $i ( 0 .. $#$tags ) {
|
||||
#
|
||||
# Set up a deletion '... where id = ? and tag = ?'
|
||||
#
|
||||
%where = ( id => $id, tag => $tags->[$i] );
|
||||
|
||||
( $stmt, @bind ) = $sql->delete( 'tags', \%where );
|
||||
|
||||
my $sth = $dbh->prepare($stmt);
|
||||
my $rv = $sth->execute(@bind);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
$rv = 0 if ( $rv eq '0E0' );
|
||||
|
||||
#
|
||||
# Report the action
|
||||
#
|
||||
if ($rv) {
|
||||
print "Deleted tag for show $id ($tags->[$i])\n";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
print "Deleted ", scalar(@$tags), " row",
|
||||
( scalar(@$tags) != 1 ? 's' : '' ), "\n";
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: do_additions
|
||||
# PURPOSE: Perform any additions indicated in an array for a given
|
||||
# episode
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $sth A prepared database handle with a query to
|
||||
# search for the target tag
|
||||
# $verbose Verbosity level
|
||||
# $id Episode number
|
||||
# $tags Reference to an array of tags for this episode
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: FIXME
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub do_additions {
|
||||
my ( $dbh, $sth, $verbose, $id, $tags ) = @_;
|
||||
|
||||
my ( $sth1, $rv, $h, $tid, $stmt, @bind, %data );
|
||||
|
||||
#
|
||||
# We will dynamically build SQL as we go
|
||||
#
|
||||
my $sql = SQL::Abstract->new;
|
||||
|
||||
my @lctags = map { lc($_) } @$tags;
|
||||
|
||||
for my $i ( 0 .. $#$tags ) {
|
||||
#
|
||||
# Build the row we're going to add
|
||||
#
|
||||
%data = (
|
||||
id => $id,
|
||||
tag => $tags->[$i],
|
||||
lctag => $lctags[$i]
|
||||
);
|
||||
|
||||
( $stmt, @bind ) = $sql->insert( 'tags', \%data );
|
||||
|
||||
my $sth = $dbh->prepare($stmt);
|
||||
my $rv = $sth->execute(@bind);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
$rv = 0 if ( $rv eq '0E0' );
|
||||
|
||||
#
|
||||
# Report the action
|
||||
#
|
||||
if ($rv) {
|
||||
print "Added tag for show $id ($tags->[$i])\n";
|
||||
}
|
||||
}
|
||||
|
||||
print "Added ", scalar(@$tags), " row",
|
||||
( scalar(@$tags) != 1 ? 's' : '' ), "\n";
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: array_compare
|
||||
# PURPOSE: Compares the elements of two arrays to see if an element
|
||||
# present in the master is also present in the slave
|
||||
# PARAMETERS: $arr1 A reference to the first array; the MASTER
|
||||
# $arr2 A reference to the second array; the SLAVE
|
||||
# RETURNS: A hash containing arrays of additions and deletions of the
|
||||
# elements that are different. The structure is:
|
||||
# {
|
||||
# additions => [ tag1, tag2 .. tagn ],
|
||||
# deletions => [ tag1, tag2 .. tagn ],
|
||||
# }
|
||||
# The returned hash will be empty if there are no differences.
|
||||
# DESCRIPTION: The requirement is to find if there are differences, then to
|
||||
# find what they are so that other code can make the slave array
|
||||
# match the master. The two arrays come from a database, so
|
||||
# we're trying to make a second source (slave) equal the first
|
||||
# (master).
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub array_compare {
|
||||
my ( $arr1, $arr2 ) = @_;
|
||||
|
||||
my %res;
|
||||
my ( @additions, @deletions );
|
||||
my %h1 = map { $_ => 1 } @$arr1;
|
||||
my %h2 = map { $_ => 1 } @$arr2;
|
||||
|
||||
#
|
||||
# Find additions
|
||||
#
|
||||
for my $key ( keys(%h1) ) {
|
||||
unless ( exists( $h2{$key} ) ) {
|
||||
push( @additions, $key );
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Find deletions
|
||||
#
|
||||
for my $key ( keys(%h2) ) {
|
||||
unless ( exists( $h1{$key} ) ) {
|
||||
push( @deletions, $key );
|
||||
}
|
||||
}
|
||||
|
||||
$res{additions} = [@additions] if @additions;
|
||||
$res{deletions} = [@deletions] if @deletions;
|
||||
|
||||
return %res;
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: Usage
|
||||
# PURPOSE: Display a usage message and exit
|
||||
# PARAMETERS: None
|
||||
# RETURNS: To command line level with exit value 1
|
||||
# DESCRIPTION: Builds the usage message using global values
|
||||
# THROWS: no exceptions
|
||||
# COMMENTS: none
|
||||
# SEE ALSO: n/a
|
||||
#===============================================================================
|
||||
sub Usage {
|
||||
print STDERR <<EOD;
|
||||
Usage: $PROG [options] project
|
||||
|
||||
$PROG v$VERSION
|
||||
|
||||
-help Display this information
|
||||
-[no]dry-run Display what would have been done but make no changes.
|
||||
Default is -dry-run.
|
||||
-verbose A repeatable option which turns up the verbosity from
|
||||
0 (silent) to 2 (lots of stuff). Default is 0.
|
||||
|
||||
EOD
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#=== 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", "verbose+", "dry-run!", );
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
Usage();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
775
Database/refresh_tags_2
Executable file
775
Database/refresh_tags_2
Executable file
@@ -0,0 +1,775 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: refresh_tags_2
|
||||
#
|
||||
# USAGE: ./refresh_tags_2
|
||||
#
|
||||
# DESCRIPTION: Parse tags from the eps.tags field and use them to populate
|
||||
# the eps_tags2_xref and tags2 tables. The eps tag list is
|
||||
# definitive (though it's quite limited since it's only 200
|
||||
# characters long), and so the junction table eps_tags2_xref and
|
||||
# the normalised tags table tags2 are kept in step by adding
|
||||
# and deleting.
|
||||
# This script is for demonstration purposes. It is not the
|
||||
# definitive answer to the tag management problem in the HPR
|
||||
# database, though it's close :-)
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.3
|
||||
# CREATED: 2016-07-22 16:48:49
|
||||
# REVISION: 2017-03-14 21:11:33
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Carp;
|
||||
use Getopt::Long;
|
||||
use Config::General;
|
||||
use Text::CSV;
|
||||
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";
|
||||
|
||||
my ( $dbh, $sth1, $h1, $rv );
|
||||
my ( %eps_tags, %tags_tags, %diffs );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load configuration data
|
||||
#
|
||||
my $conf = Config::General->new(
|
||||
-ConfigFile => $configfile,
|
||||
-InterPolateVars => 1,
|
||||
-ExtendedAccess => 1,
|
||||
);
|
||||
my %config = $conf->getall();
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
Usage() if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $verbose = ( defined( $options{verbose} ) ? $options{verbose} : 0 );
|
||||
my $dry_run = ( defined( $options{'dry-run'} ) ? $options{'dry-run'} : 1 );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# 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;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Collect and process the id numbers and tags from the 'eps' table
|
||||
#-------------------------------------------------------------------------------
|
||||
%eps_tags = %{ collect_eps_tags( $dbh, $verbose ) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Collect any tags we've already stashed in the database.
|
||||
#-------------------------------------------------------------------------------
|
||||
%tags_tags = %{ collect_db_tags( $dbh, $verbose ) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Now compare the two sources to look for differences
|
||||
#-------------------------------------------------------------------------------
|
||||
%diffs = %{ find_differences(\%eps_tags,\%tags_tags) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Perform the updates if there are any
|
||||
#-------------------------------------------------------------------------------
|
||||
if (%diffs) {
|
||||
print "Differences found\n\n";
|
||||
unless ($dry_run) {
|
||||
#
|
||||
# Scan for all deletions in the %diffs hash by traversing it by sorted
|
||||
# episode number. If deletions are found for an episode they are
|
||||
# performed.
|
||||
#
|
||||
foreach my $id ( sort { $a <=> $b } keys(%diffs) ) {
|
||||
if ( exists( $diffs{$id}->{deletions} ) ) {
|
||||
do_deletions( $dbh, $verbose, $id, $diffs{$id}->{deletions} );
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Prepare to search for tags
|
||||
#
|
||||
$sth1 = $dbh->prepare(q{SELECT * FROM tags2 WHERE tag = ?})
|
||||
or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Scan for all additions in the %diffs hash
|
||||
#
|
||||
foreach my $id ( sort { $a <=> $b } keys(%diffs) ) {
|
||||
if ( exists( $diffs{$id}->{additions} ) ) {
|
||||
do_additions( $dbh, $sth1, $verbose, $id,
|
||||
$diffs{$id}->{additions} );
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Having deleted all the requested rows from the junction table remove
|
||||
# any tags that are "orphaned" as a consequence. If we were using
|
||||
# foreign keys we could let the database do this.
|
||||
#
|
||||
$sth1 = $dbh->prepare(
|
||||
q{DELETE FROM tags2
|
||||
WHERE id NOT IN (SELECT DISTINCT tags2_id FROM eps_tags2_xref)}
|
||||
) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
$rv = $sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
$rv = 0 if ( $rv eq '0E0' );
|
||||
|
||||
#
|
||||
# Report the action
|
||||
#
|
||||
if ($rv) {
|
||||
print "Deleted ", $rv, " orphan tag", ( $rv != 1 ? 's' : '' ),
|
||||
"\n";
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
print "No changes made - dry run\n";
|
||||
}
|
||||
}
|
||||
else {
|
||||
print "No differences found\n";
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: collect_eps_tags
|
||||
# PURPOSE: Collects the tags from the eps.tags field
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $verbose Verbosity level
|
||||
# RETURNS: A reference to the hash created by collecting all the tags
|
||||
# DESCRIPTION: FIXME
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub collect_eps_tags {
|
||||
my ( $dbh, $verbose ) = @_;
|
||||
|
||||
my ( $status, @fields, %hash );
|
||||
my ( $sth, $h );
|
||||
|
||||
#
|
||||
# For parsing the field as CSV
|
||||
#
|
||||
my $csv = Text::CSV_XS->new(
|
||||
{ binary => 1,
|
||||
auto_diag => 1,
|
||||
allow_loose_quotes => 1
|
||||
}
|
||||
);
|
||||
|
||||
#
|
||||
# Query the eps table for all the id and tags
|
||||
#
|
||||
$sth = $dbh->prepare(
|
||||
q{SELECT id,tags FROM eps
|
||||
WHERE length(tags) > 0
|
||||
ORDER BY id}
|
||||
) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
$sth->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Loop through what we got
|
||||
#
|
||||
while ( $h = $sth->fetchrow_hashref ) {
|
||||
#
|
||||
# Parse the tag list
|
||||
#
|
||||
$status = $csv->parse( $h->{tags} );
|
||||
unless ($status) {
|
||||
#
|
||||
# Report any errors
|
||||
#
|
||||
print "Parse error on episode ", $h->{id}, "\n";
|
||||
print $csv->error_input(), "\n";
|
||||
next;
|
||||
}
|
||||
@fields = $csv->fields();
|
||||
|
||||
next unless (@fields);
|
||||
|
||||
#
|
||||
# Trim all tags (don't alter $_ when doing it)
|
||||
#
|
||||
@fields = map {
|
||||
my $t = $_;
|
||||
$t =~ s/(^\s+|\s+$)//g;
|
||||
$t;
|
||||
} @fields;
|
||||
|
||||
#print "$h->{id}: ",join(",",@fields),"\n";
|
||||
|
||||
#
|
||||
# Save the id and its tags, sorted for comparison, with empty elements
|
||||
# removed too
|
||||
#
|
||||
$hash{ $h->{id} } = [ sort grep {!/^$/} @fields ];
|
||||
|
||||
}
|
||||
|
||||
#print Dumper(\%hash),"\n";
|
||||
|
||||
#
|
||||
# Dump all id numbers and tags if the verbose level is high enough
|
||||
#
|
||||
if ( $verbose >= 3 ) {
|
||||
print "\nTags collected from the 'eps' table\n\n";
|
||||
foreach my $id ( sort { $a <=> $b } keys(%hash) ) {
|
||||
printf "%04d: %s\n", $id, join( ",", @{ $hash{$id} } );
|
||||
}
|
||||
}
|
||||
|
||||
return \%hash;
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: collect_db_tags
|
||||
# PURPOSE: Collects the tags already stored in the database
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $verbose Verbosity level
|
||||
# RETURNS: A reference to the hash created by collecting all the tags
|
||||
# DESCRIPTION:
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub collect_db_tags {
|
||||
my ( $dbh, $verbose ) = @_;
|
||||
|
||||
my %hash;
|
||||
my ( $sth, $h );
|
||||
|
||||
#
|
||||
# Query the database for tag data
|
||||
#
|
||||
# We use the junction table (eps_tags2_xref), traversing it by episode number
|
||||
# and linking the table of tags (tags2). This results in a list of the tags
|
||||
# relating to an episode, which should be similar to (if not the same as) the
|
||||
# 'tags' field in the 'eps' table.
|
||||
#
|
||||
$sth = $dbh->prepare(
|
||||
q{SELECT et.eps_id AS id,t.tag,t.lctag
|
||||
FROM eps_tags2_xref et
|
||||
JOIN tags2 t ON et.tags2_id = t.id
|
||||
ORDER BY et.eps_id}
|
||||
) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
$sth->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Loop through what we got, building an array of tags per episode number
|
||||
#
|
||||
while ( $h = $sth->fetchrow_hashref ) {
|
||||
if ( defined( $hash{ $h->{id} } ) ) {
|
||||
push( @{ $hash{ $h->{id} } }, $h->{tag} );
|
||||
}
|
||||
else {
|
||||
$hash{ $h->{id} } = [ $h->{tag} ];
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Sort all the tag arrays for comparison
|
||||
#
|
||||
foreach my $id ( keys(%hash) ) {
|
||||
$hash{$id} = [ sort @{ $hash{$id} } ];
|
||||
}
|
||||
|
||||
#
|
||||
# Dump all id numbers and tags if the verbose level is high enough
|
||||
#
|
||||
if ( $verbose >= 3 ) {
|
||||
print "\nTags collected from the 'tags2' table\n\n";
|
||||
foreach my $id ( sort { $a <=> $b } keys(%hash) ) {
|
||||
printf "%04d: %s\n", $id, join( ",", @{ $hash{$id} } );
|
||||
}
|
||||
print '=-' x 40,"\n";
|
||||
}
|
||||
|
||||
return \%hash;
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: find_differences
|
||||
# PURPOSE: Find the differences between two hashes containing tags
|
||||
# PARAMETERS: $master Reference to the master hash
|
||||
# $slave Reference to the slave hash
|
||||
# RETURNS: A reference to the hash created checking for differences
|
||||
# DESCRIPTION: The function is presented with two hashes. The 'master' hash
|
||||
# has come from the CSV string in the 'eps' table. The 'slave'
|
||||
# hash has come from the table of tags 'tags2'. These hashes are
|
||||
# keyed by episode number and each element contains a reference
|
||||
# to a sorted array of tags.
|
||||
# This function compares two tag arrays for an episode using
|
||||
# function 'array_compare' and receives back a hash of additions
|
||||
# and deletions:
|
||||
# {
|
||||
# additions => [ tag1, tag2 .. tagn ],
|
||||
# deletions => [ tag1, tag2 .. tagn ],
|
||||
# }
|
||||
# These are stored in a result hash keyed by episode number, and
|
||||
# a reference to this hash is returned to the caller.
|
||||
# This function can report a lot of details about what has been
|
||||
# found if the level of verbosity is high enough.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub find_differences {
|
||||
my ($master,$slave) = @_;
|
||||
|
||||
my %hash;
|
||||
|
||||
foreach my $id ( sort { $a <=> $b } keys(%$master) ) {
|
||||
my %iddiffs = array_compare( $master->{$id}, $slave->{$id} );
|
||||
if (%iddiffs) {
|
||||
if ( $verbose >= 1 ) {
|
||||
#
|
||||
# Report what was found if asked to
|
||||
#
|
||||
print "Episode: $id\n";
|
||||
print "Update:\n\teps: ", join( ",", @{ $master->{$id} } ), "\n";
|
||||
print "\ttags: ",
|
||||
(
|
||||
defined( $slave->{$id} )
|
||||
? join( ",", @{ $slave->{$id} } )
|
||||
: '--None--' ), "\n";
|
||||
print '-' x 80,"\n";
|
||||
}
|
||||
$hash{$id} = {%iddiffs};
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Report differences and actions if the verbose level is high enough
|
||||
#
|
||||
if ( $verbose >= 2 ) {
|
||||
print "\nDifferences and actions\n\n";
|
||||
foreach my $id ( sort { $a <=> $b } keys(%hash) ) {
|
||||
print "Episode: $id\n";
|
||||
if ( exists( $hash{$id}->{deletions} ) ) {
|
||||
print "Deletions: ";
|
||||
print join( ",", @{ $hash{$id}->{deletions} } ), "\n";
|
||||
}
|
||||
if ( exists( $hash{$id}->{additions} ) ) {
|
||||
print "Additions: ";
|
||||
print join( ",", @{ $hash{$id}->{additions} } ), "\n";
|
||||
}
|
||||
print '-' x 80, "\n";
|
||||
}
|
||||
}
|
||||
|
||||
return \%hash;
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: do_deletions
|
||||
# PURPOSE: Perform any deletions indicated in an array for a given
|
||||
# episode
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $verbose Verbosity level
|
||||
# $id Episode number
|
||||
# $tags Reference to an array of tags for this episode
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: A tag deletion consists of its removal from the joining table.
|
||||
# Only when there are no more references to the actual tag can
|
||||
# it then be deleted. If the tables were in a database with
|
||||
# foreign keys then we could leave the database itself to handle
|
||||
# this (MariaDB could do it but we'd need to redefine the tables
|
||||
# to use InnoDB rather than MyISAM. The latter is the legacy
|
||||
# table structure from the days when MySQL didn't have foreign
|
||||
# keys).
|
||||
# This function does not perform the tag deletion since this
|
||||
# easier to leave until all deletions have finished.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub do_deletions {
|
||||
my ( $dbh, $verbose, $id, $tags ) = @_;
|
||||
|
||||
my ( $stmt, @bind, %data, %where );
|
||||
|
||||
#
|
||||
# We will dynamically build SQL as we go
|
||||
#
|
||||
my $sql = SQL::Abstract->new;
|
||||
|
||||
#
|
||||
# Process the list of tags we have been given
|
||||
#
|
||||
for my $i ( 0 .. $#$tags ) {
|
||||
#
|
||||
# Set up a deletion '... where eps_id = ? and
|
||||
# tags2 = (select id from tags2 where tag = ?)'
|
||||
#
|
||||
my ( $sub_stmt, @sub_bind )
|
||||
= ( "SELECT id FROM tags2 WHERE tag = ?", $tags->[$i] );
|
||||
|
||||
%where = (
|
||||
eps_id => $id,
|
||||
tags2_id => \[ "= ($sub_stmt)" => @sub_bind ]
|
||||
);
|
||||
|
||||
( $stmt, @bind ) = $sql->delete( 'eps_tags2_xref', \%where );
|
||||
if ( $verbose >= 2 ) {
|
||||
print "Statement: $stmt\n";
|
||||
print "Bind: ", join( ",", @bind ), "\n";
|
||||
}
|
||||
|
||||
#
|
||||
# Do the deletion
|
||||
#
|
||||
my $sth = $dbh->prepare($stmt);
|
||||
my $rv = $sth->execute(@bind);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
$rv = 0 if ( $rv eq '0E0' );
|
||||
|
||||
#
|
||||
# Report the action
|
||||
#
|
||||
if ($rv) {
|
||||
print "Deleted tag for show $id ($tags->[$i])\n";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
print "Deleted ", scalar(@$tags), " row",
|
||||
( scalar(@$tags) != 1 ? 's' : '' ), "\n";
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: do_additions
|
||||
# PURPOSE: Perform any additions indicated in an array for a given
|
||||
# episode
|
||||
# PARAMETERS: $dbh Database handle
|
||||
# $sth A prepared database handle with a query to
|
||||
# search for the target tag
|
||||
# $verbose Verbosity level
|
||||
# $id Episode number
|
||||
# $tags Reference to an array of tags for this episode
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: The addition of a tag for an episode consists of creating the
|
||||
# tag in the 'tags2' table (unless it already exists) and
|
||||
# making a joining table entry for it. This what this function
|
||||
# does.
|
||||
# FIXME: Not very resilient to failure.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub do_additions {
|
||||
my ( $dbh, $sth, $verbose, $id, $tags ) = @_;
|
||||
|
||||
my ( $sth1, $rv, $h, $tid, $stmt, @bind, %data );
|
||||
|
||||
#
|
||||
# We will dynamically build SQL as we go
|
||||
#
|
||||
my $sql = SQL::Abstract->new;
|
||||
|
||||
my @lctags = map { lc($_) } @$tags;
|
||||
|
||||
#
|
||||
# Loop through the array of tags (using an integer so we can index the
|
||||
# current tag)
|
||||
#
|
||||
for my $i ( 0 .. $#$tags ) {
|
||||
#
|
||||
# Look to see if this tag exists
|
||||
#
|
||||
$sth->execute( $tags->[$i] );
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# If it's already in the table just store the id otherwise
|
||||
# add a new entry
|
||||
#
|
||||
if ( $h = $sth->fetchrow_hashref ) {
|
||||
$tid = $h->{id};
|
||||
}
|
||||
else {
|
||||
#
|
||||
# Build the row we're going to add
|
||||
#
|
||||
%data = (
|
||||
tag => $tags->[$i],
|
||||
lctag => $lctags[$i]
|
||||
);
|
||||
|
||||
#
|
||||
# Build the SQL, reporting the result if asked
|
||||
#
|
||||
( $stmt, @bind ) = $sql->insert( 'tags2', \%data );
|
||||
if ( $verbose >= 2 ) {
|
||||
print "Statement: $stmt\n";
|
||||
print "Bind: ", join( ",", @bind ), "\n";
|
||||
}
|
||||
|
||||
#
|
||||
# Add the tag to 'tags2'
|
||||
#
|
||||
$sth1 = $dbh->prepare($stmt);
|
||||
$rv = $sth1->execute(@bind);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
$rv = 0 if ( $rv eq '0E0' );
|
||||
|
||||
#
|
||||
# Ask the database for the id we just added
|
||||
# FIXME: what if it failed?
|
||||
#
|
||||
$tid = $sth1->{mysql_insertid};
|
||||
|
||||
#
|
||||
# Report the action
|
||||
#
|
||||
if ($rv) {
|
||||
print "Added new tag '$tags->[$i]' ($tid)\n";
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Now we know we have a tag in the tags2 table so now we can create
|
||||
# the eps_tags2_xref entry
|
||||
#
|
||||
%data = (
|
||||
eps_id => $id,
|
||||
tags2_id => $tid
|
||||
);
|
||||
|
||||
#
|
||||
# Build the SQL, reporting the result if asked
|
||||
#
|
||||
( $stmt, @bind ) = $sql->insert( 'eps_tags2_xref', \%data );
|
||||
if ( $verbose >= 2 ) {
|
||||
print "Statement: $stmt\n";
|
||||
print "Bind: ", join( ",", @bind ), "\n";
|
||||
}
|
||||
|
||||
#
|
||||
# Add the row
|
||||
#
|
||||
$sth1 = $dbh->prepare($stmt);
|
||||
$rv = $sth1->execute(@bind);
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
$rv = 0 if ( $rv eq '0E0' );
|
||||
|
||||
#
|
||||
# Report the action
|
||||
#
|
||||
if ($rv) {
|
||||
printf "Added new junction row (eps_id=%s,tags2_id=%s -> %s)\n",
|
||||
$id, $tid, $tags->[$i];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
print "Added ", scalar(@$tags), " row",
|
||||
( scalar(@$tags) != 1 ? 's' : '' ), "\n";
|
||||
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: array_compare
|
||||
# PURPOSE: Compares the elements of two arrays to see if an element
|
||||
# present in the master is also present in the slave
|
||||
# PARAMETERS: $arr1 A reference to the first array; the MASTER
|
||||
# $arr2 A reference to the second array; the SLAVE
|
||||
# RETURNS: A hash containing arrays of additions and deletions of the
|
||||
# elements that are different. The structure is:
|
||||
# {
|
||||
# additions => [ tag1, tag2 .. tagn ],
|
||||
# deletions => [ tag1, tag2 .. tagn ],
|
||||
# }
|
||||
# The returned hash will be empty if there are no differences.
|
||||
# DESCRIPTION: The requirement is to find if there are differences, then to
|
||||
# find what they are so that other code can make the slave array
|
||||
# match the master. The two arrays come from a database, so
|
||||
# we're trying to make a second source (slave) equal the first
|
||||
# (master).
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub array_compare {
|
||||
my ( $arr1, $arr2 ) = @_;
|
||||
|
||||
my %res;
|
||||
my ( @additions, @deletions );
|
||||
|
||||
#
|
||||
# Use hashes to make it easier to find existence of stuff
|
||||
#
|
||||
my %h1 = map { lc($_) => 1 } @$arr1;
|
||||
my %h2 = map { lc($_) => 1 } @$arr2;
|
||||
|
||||
#
|
||||
# Find additions
|
||||
#
|
||||
for my $key ( keys(%h1) ) {
|
||||
unless ( exists( $h2{$key} ) ) {
|
||||
push( @additions, $key );
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Find deletions
|
||||
#
|
||||
for my $key ( keys(%h2) ) {
|
||||
unless ( exists( $h1{$key} ) ) {
|
||||
push( @deletions, $key );
|
||||
}
|
||||
}
|
||||
|
||||
$res{additions} = [@additions] if @additions;
|
||||
$res{deletions} = [@deletions] if @deletions;
|
||||
|
||||
return %res;
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: Usage
|
||||
# PURPOSE: Display a usage message and exit
|
||||
# PARAMETERS: None
|
||||
# RETURNS: To command line level with exit value 1
|
||||
# DESCRIPTION: Builds the usage message using global values
|
||||
# THROWS: no exceptions
|
||||
# COMMENTS: none
|
||||
# SEE ALSO: n/a
|
||||
#===============================================================================
|
||||
sub Usage {
|
||||
print STDERR <<EOD;
|
||||
Usage: $PROG [options] project
|
||||
|
||||
$PROG v$VERSION
|
||||
|
||||
-help Display this information
|
||||
-[no]dry-run Display what would have been done but make no changes.
|
||||
Default is -dry-run.
|
||||
-verbose A repeatable option which turns up the verbosity from
|
||||
0 (silent) to 3 (lots and lots of stuff). Default is 0.
|
||||
|
||||
EOD
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#=== 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", "verbose+", "dry-run!", );
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
Usage();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
354
Database/remodel_db_hosts_eps
Executable file
354
Database/remodel_db_hosts_eps
Executable file
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: remodel_db_host_eps
|
||||
#
|
||||
# USAGE: ./remodel_db_host_eps
|
||||
#
|
||||
# DESCRIPTION: Remodel the 'hosts' and 'eps' tables in the HPR database so
|
||||
# that a many-to-many relationship between host and episode can
|
||||
# be established.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.2
|
||||
# CREATED: 2014-05-08 10:55:28
|
||||
# REVISION: 2015-06-26 13:33:20
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use YAML::Syck;
|
||||
use List::Util qw{max};
|
||||
use List::MoreUtils qw{uniq};
|
||||
use DBI;
|
||||
#use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.2';
|
||||
|
||||
#
|
||||
# Script name
|
||||
#
|
||||
( 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.yml";
|
||||
|
||||
my ( $dbh, $sth1, $sth2, $sth3, $sth4, $h1, $h2, $rv );
|
||||
my ( %hosts_by_name, %hosts_by_id, %eps, @names, $hostid, $hid, $max_hostid );
|
||||
|
||||
#
|
||||
# Names of fields in the 'hosts' table in the appropriate order for the later
|
||||
# INSERT statement
|
||||
#
|
||||
my @host_flds = qw{
|
||||
hostid
|
||||
host
|
||||
email
|
||||
profile
|
||||
license
|
||||
local_image
|
||||
valid
|
||||
};
|
||||
|
||||
#
|
||||
# Names of fields in the 'eps' table in the appropriate order for the later
|
||||
# INSERT statement. Note that it omits the 'hostid' field.
|
||||
#
|
||||
my @eps_flds = qw{
|
||||
id
|
||||
date
|
||||
title
|
||||
summary
|
||||
notes
|
||||
series
|
||||
explicit
|
||||
license
|
||||
tags
|
||||
version
|
||||
valid
|
||||
};
|
||||
|
||||
#
|
||||
# Enable Unicode output mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load configuration data
|
||||
#
|
||||
my %config = %{ LoadFile($configfile) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Connect to the database
|
||||
#-------------------------------------------------------------------------------
|
||||
my $dbname = $config{database}->{name};
|
||||
my $dbuser = $config{database}->{user};
|
||||
my $dbpwd = $config{database}->{password};
|
||||
$dbh
|
||||
= DBI->connect( "dbi:mysql:dbname=$dbname", $dbuser, $dbpwd,
|
||||
{ AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
||||
# The expectation is that we have the following original tables:
|
||||
# hosts - the details of each host (contains some double entries
|
||||
# "hostA and hostB")
|
||||
# eps - the details of all episodes, currently with a host id number
|
||||
# against each one
|
||||
#
|
||||
# We also have the following new tables for the transition:
|
||||
# new_hosts - an empty copy of the 'hosts' table, InnoDB
|
||||
# new_eps - an empty copy of the 'eps' table without the 'hostid'
|
||||
# column, InnoDB
|
||||
# hosts_eps - a mapping table for joining together the 'new_hosts' and
|
||||
# 'new_eps' tables, InnoDB with foreign keys
|
||||
#
|
||||
# See the file 'hosts_eps.sql' for the DDL which creates these tables.
|
||||
#=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Collect the entire 'hosts' table
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(q{SELECT * FROM hosts ORDER BY hostid DESC});
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Grab the data as an arrayref of hashrefs
|
||||
#
|
||||
my $hosts = $sth1->fetchall_arrayref( {} );
|
||||
|
||||
#
|
||||
# Make hashes keyed on the host name and on the id
|
||||
#
|
||||
%hosts_by_name = map { $_->{host} => $_ } @{$hosts};
|
||||
%hosts_by_id = map { $_->{hostid} => $_ } @{$hosts};
|
||||
|
||||
$max_hostid = max( map { $_->{hostid} } values(%hosts_by_name) );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Collect the entire 'eps' table
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(q{SELECT * FROM eps ORDER BY id DESC});
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Grab the data as an arrayref of hashrefs
|
||||
#
|
||||
my $eps = $sth1->fetchall_arrayref( {} );
|
||||
|
||||
#
|
||||
# Make a hash keyed on the episode number
|
||||
#
|
||||
%eps = map { $_->{id} => $_ } @{$eps};
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Walk the hash of hosts by name, finding double host entries. Stash the
|
||||
# episode numbers against the hosts (but do it messily resulting in duplicates
|
||||
# as a side effect)
|
||||
#-------------------------------------------------------------------------------
|
||||
foreach my $key ( keys(%hosts_by_name) ) {
|
||||
$hostid = $hosts_by_name{$key}->{hostid};
|
||||
|
||||
#
|
||||
# Is this a double ("HostA and HostB") entry?
|
||||
#
|
||||
if ( @names = ( $key =~ /^([[:print:]]+) and ([[:print:]]+)$/ ) ) {
|
||||
printf "%3d: %s\n", $hosts_by_name{$key}->{hostid}, $key;
|
||||
|
||||
#
|
||||
# Process the names picked out of the 'host' field
|
||||
#
|
||||
foreach my $name (@names) {
|
||||
if ( exists( $hosts_by_name{$name} ) ) {
|
||||
#
|
||||
# Known name, report it
|
||||
#
|
||||
printf "\t%3d: %s\n", $hosts_by_name{$name}->{hostid}, $name;
|
||||
printf "Replace %d with %d\n",
|
||||
$hosts_by_name{$key}->{hostid},
|
||||
$hosts_by_name{$name}->{hostid};
|
||||
|
||||
#
|
||||
# Collect all episodes relating to the double id ($hostid) and
|
||||
# add them to the known id ($hid)
|
||||
#
|
||||
$hid = $hosts_by_name{$name}->{hostid};
|
||||
$hosts_by_id{$hid}->{eps}
|
||||
= collect_eps( $hostid, \%eps,
|
||||
$hosts_by_id{$hid}->{eps} );
|
||||
|
||||
#
|
||||
# Mark the double id as not valid
|
||||
#
|
||||
$hosts_by_id{$hostid}->{valid} = 0;
|
||||
}
|
||||
else {
|
||||
#
|
||||
# Unknown name, make a new host entry
|
||||
#
|
||||
print "\t'$name' not known\n";
|
||||
$max_hostid++;
|
||||
$hosts_by_id{$max_hostid} = {
|
||||
'profile' => '',
|
||||
'local_image' => '0',
|
||||
'hostid' => $max_hostid,
|
||||
'license' => 'CC-BY-SA',
|
||||
'host' => $name,
|
||||
'valid' => '1',
|
||||
'email' => ''
|
||||
};
|
||||
|
||||
#
|
||||
# Save all episodes for this name
|
||||
#
|
||||
$hid = $hosts_by_name{$key}->{hostid};
|
||||
$hosts_by_id{$max_hostid}->{eps}
|
||||
= collect_eps( $hid, \%eps,
|
||||
$hosts_by_id{$max_hostid}->{eps} );
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
#
|
||||
# Single host, just collect all their episodes
|
||||
#
|
||||
$hosts_by_id{$hostid}->{eps}
|
||||
= collect_eps( $hostid, \%eps, $hosts_by_id{$hostid}->{eps} );
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Report on the structure we built, de-duplicating as we go
|
||||
#-------------------------------------------------------------------------------
|
||||
foreach my $hid ( sort { $a <=> $b } keys(%hosts_by_id) ) {
|
||||
if ( exists( $hosts_by_id{$hid}->{eps} ) ) {
|
||||
#
|
||||
# De-duplicate the episode list
|
||||
#
|
||||
@{ $hosts_by_id{$hid}->{eps} }
|
||||
= sort { $a <=> $b } uniq( @{ $hosts_by_id{$hid}->{eps} } );
|
||||
|
||||
#
|
||||
# Print the host details followed by the episodes
|
||||
#
|
||||
printf "Hostid: %d [%s,%d] (%d)\n", $hid,
|
||||
$hosts_by_id{$hid}->{host},
|
||||
$hosts_by_id{$hid}->{hostid},
|
||||
scalar( @{ $hosts_by_id{$hid}->{eps} } );
|
||||
foreach my $ep ( @{ $hosts_by_id{$hid}->{eps} } ) {
|
||||
printf " Episode: %d\n", $ep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Turn the %hosts_by_id hash into database insert statements
|
||||
#-------------------------------------------------------------------------------
|
||||
my $sql1 = sprintf( "INSERT INTO new_hosts VALUES(%s)",
|
||||
join( ",", map { '?' } @host_flds ) );
|
||||
$sth1 = $dbh->prepare($sql1);
|
||||
|
||||
my $sql2 = sprintf( "INSERT INTO new_eps VALUES(%s)",
|
||||
join( ",", map { '?' } @eps_flds ) );
|
||||
$sth2 = $dbh->prepare($sql2);
|
||||
|
||||
$sth3 = $dbh->prepare(q{INSERT INTO hosts_eps VALUES(?,?)});
|
||||
|
||||
#
|
||||
# The 'new_hosts' table
|
||||
#
|
||||
foreach my $hid ( sort { $a <=> $b } keys(%hosts_by_id) ) {
|
||||
$sth1->execute( @{ $hosts_by_id{$hid} }{@host_flds} );
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# The 'new_eps' table
|
||||
#
|
||||
foreach my $eid ( sort { $a <=> $b } keys(%eps) ) {
|
||||
$sth2->execute( @{ $eps{$eid} }{@eps_flds} );
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# The 'hosts_eps' table
|
||||
#
|
||||
foreach my $hid ( sort { $a <=> $b } keys(%hosts_by_id) ) {
|
||||
if ( exists( $hosts_by_id{$hid}->{eps} ) ) {
|
||||
foreach my $ep ( @{ $hosts_by_id{$hid}->{eps} } ) {
|
||||
$sth3->execute( $hosts_by_id{$hid}->{hostid}, $ep );
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: collect_eps
|
||||
# PURPOSE: Collect all the episodes relating to a hostid and return them,
|
||||
# along with the contents of $current as an array of hashrefs
|
||||
# PARAMETERS: $hostid the host id we're interested in
|
||||
# $eps hashref containing anonymous hashes keyed by
|
||||
# episode number
|
||||
# $current a reference to any existing array of episodes
|
||||
# for this host id
|
||||
# RETURNS: A reference to the resulting array of anonymous hashes
|
||||
# DESCRIPTION:
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub collect_eps {
|
||||
my ( $hostid, $eps, $current ) = @_;
|
||||
my @host_eps;
|
||||
|
||||
@host_eps = @{$current} if $current;
|
||||
|
||||
foreach my $epsid ( keys(%$eps) ) {
|
||||
if ( $eps->{$epsid}->{hostid} == $hostid ) {
|
||||
push( @host_eps, $epsid );
|
||||
}
|
||||
}
|
||||
|
||||
return \@host_eps;
|
||||
}
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
110
Database/remodel_db_series_eps
Executable file
110
Database/remodel_db_series_eps
Executable file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: remodel_db_series_eps
|
||||
#
|
||||
# USAGE: ./remodel_db_series _eps
|
||||
#
|
||||
# DESCRIPTION: Script to perform the steps necessary to remodel the series
|
||||
# information in the hpr_hpr database. We want to have
|
||||
# a many-to-many relationship between episodes and series.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: This code is extremely trivial. It could be done just as well
|
||||
# with plain SQL. However, having a script will potentially
|
||||
# allow other things to be done during the table load, such as
|
||||
# reading other series associations from a file.
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.0.1
|
||||
# CREATED: 2015-06-26 12:30:27
|
||||
# REVISION: 2015-06-26 15:28:12
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use YAML::Syck;
|
||||
use DBI;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.0.1';
|
||||
|
||||
#
|
||||
# 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.yml";
|
||||
|
||||
my ( $dbh, $sth1, $sth2, $sth3, $sth4, $h1, $h2, $rv );
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#
|
||||
# Load configuration data
|
||||
#
|
||||
my %config = %{ LoadFile($configfile) };
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Connect to the database
|
||||
#-------------------------------------------------------------------------------
|
||||
my $dbname = $config{database}->{name};
|
||||
my $dbuser = $config{database}->{user};
|
||||
my $dbpwd = $config{database}->{password};
|
||||
$dbh
|
||||
= DBI->connect( "dbi:mysql:dbname=$dbname", $dbuser, $dbpwd,
|
||||
{ AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Set up the SQL and query the 'eps' table since everything has a series, even
|
||||
# if it's 0
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(q{SELECT * FROM eps ORDER BY id DESC});
|
||||
$sth2 = $dbh->prepare(q{INSERT INTO series_eps VALUES(?,?)});
|
||||
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Walk the entire 'eps' table simply adding rows into the 'series_eps' table
|
||||
#-------------------------------------------------------------------------------
|
||||
while ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
$sth2->execute($h1->{series},$h1->{id});
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
}
|
||||
|
||||
exit;
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
||||
|
||||
|
750
Database/report_missing_tags
Executable file
750
Database/report_missing_tags
Executable file
@@ -0,0 +1,750 @@
|
||||
#!/usr/bin/env perl
|
||||
#===============================================================================
|
||||
#
|
||||
# FILE: report_missing_tags
|
||||
#
|
||||
# USAGE: ./report_missing_tags [-help] [-out=FILE]
|
||||
# [-sort=FIELD1[,FIELD2...]] [-action-csv=FILE] [-tags-csv=FILE]
|
||||
# [-json=FILE] [-config=FILE]
|
||||
#
|
||||
# DESCRIPTION: Generate a report of shows which are missing tags and
|
||||
# summaries in the HPR database.
|
||||
#
|
||||
# OPTIONS: ---
|
||||
# REQUIREMENTS: ---
|
||||
# BUGS: ---
|
||||
# NOTES: ---
|
||||
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
||||
# VERSION: 0.1.4
|
||||
# CREATED: 2015-08-03 21:20:53
|
||||
# REVISION: 2022-08-03 23:00:42
|
||||
#
|
||||
#===============================================================================
|
||||
|
||||
use 5.010;
|
||||
use strict;
|
||||
use warnings;
|
||||
use utf8;
|
||||
|
||||
use Getopt::Long;
|
||||
use Pod::Usage;
|
||||
|
||||
use Config::General;
|
||||
|
||||
use Template;
|
||||
use Template::Filters;
|
||||
Template::Filters->use_html_entities; # Use HTML::Entities in the template
|
||||
|
||||
use Text::CSV_XS;
|
||||
|
||||
use JSON;
|
||||
|
||||
use DBI;
|
||||
|
||||
use Data::Dumper;
|
||||
|
||||
#
|
||||
# Version number (manually incremented)
|
||||
#
|
||||
our $VERSION = '0.1.4';
|
||||
|
||||
#
|
||||
# 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";
|
||||
my $template = "$basedir/$PROG.tpl";
|
||||
|
||||
my ( $dbh, $sth1, $h1 );
|
||||
my ( $s_count, $t_count, $st_count, $missing, $showsbyhosts );
|
||||
my @order_by;
|
||||
|
||||
#
|
||||
# Enable Unicode mode
|
||||
#
|
||||
binmode STDOUT, ":encoding(UTF-8)";
|
||||
binmode STDERR, ":encoding(UTF-8)";
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Options and arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
my $DEF_DEBUG = 0;
|
||||
|
||||
#
|
||||
# Process options
|
||||
#
|
||||
my %options;
|
||||
Options( \%options );
|
||||
|
||||
#
|
||||
# Default help
|
||||
#
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 )
|
||||
if ( $options{'help'} );
|
||||
|
||||
#
|
||||
# Collect options
|
||||
#
|
||||
my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG );
|
||||
my $cfgfile
|
||||
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
||||
|
||||
my $outfile = $options{out};
|
||||
|
||||
if ( exists( $options{sort} ) ) {
|
||||
@order_by = getMultiOpts( $options{sort}, ',', 1,
|
||||
[ 'id', 'date', 'title', 'hostid', 'host' ] );
|
||||
}
|
||||
else {
|
||||
@order_by = qw{ id };
|
||||
}
|
||||
_debug( $DEBUG > 1, '@order_by: ' . join( "/", @order_by ) );
|
||||
|
||||
#
|
||||
# Handle the optional Action CSV output file
|
||||
#
|
||||
my $acsvfile = $options{'action-csv'};
|
||||
my $acsvfh;
|
||||
if ($acsvfile) {
|
||||
open( $acsvfh, ">:encoding(UTF-8)", $acsvfile )
|
||||
or die "Unable to open $acsvfile for writing: $!";
|
||||
}
|
||||
|
||||
#
|
||||
# Handle the optional Tags CSV output file
|
||||
#
|
||||
my $tcsvfile = $options{'tags-csv'};
|
||||
my $tcsvfh;
|
||||
if ($tcsvfile) {
|
||||
open( $tcsvfh, ">:encoding(UTF-8)", $tcsvfile )
|
||||
or die "Unable to open $tcsvfile for writing: $!";
|
||||
}
|
||||
|
||||
#
|
||||
# Handle the optional JSON output file
|
||||
#
|
||||
my $jsonfile = $options{json};
|
||||
my $jsonfh;
|
||||
if ($jsonfile) {
|
||||
open( $jsonfh, ">:encoding(UTF-8)", $jsonfile )
|
||||
or die "Unable to open $jsonfile for writing: $!";
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Open the output file (or STDOUT) - we may need the date to do it
|
||||
#-------------------------------------------------------------------------------
|
||||
my $outfh;
|
||||
if ($outfile) {
|
||||
open( $outfh, ">:encoding(UTF-8)", $outfile )
|
||||
or die "Unable to open $outfile for writing: $!";
|
||||
}
|
||||
else {
|
||||
open( $outfh, ">&", \*STDOUT )
|
||||
or die "Unable to initialise for writing: $!";
|
||||
}
|
||||
|
||||
#
|
||||
# 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};
|
||||
$dbh = DBI->connect( "dbi:mysql:host=$dbhost;port=$dbport;database=$dbname",
|
||||
$dbuser, $dbpwd, { AutoCommit => 1 } )
|
||||
or die $DBI::errstr;
|
||||
|
||||
#
|
||||
# Enable client-side UTF8
|
||||
#
|
||||
$dbh->{mysql_enable_utf8} = 1;
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Get the counts of missing items
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(
|
||||
q{
|
||||
SELECT
|
||||
(SELECT count(*) FROM eps
|
||||
WHERE length(summary) = 0 AND DATEDIFF(date,CURDATE()) <= 0) AS s_count,
|
||||
(SELECT count(*) FROM eps
|
||||
WHERE length(tags) = 0 AND DATEDIFF(date,CURDATE()) <= 0) AS t_count,
|
||||
(SELECT count(*) FROM eps
|
||||
WHERE length(summary) = 0 AND length(tags) = 0
|
||||
AND DATEDIFF(date,CURDATE()) <= 0) AS st_count
|
||||
}
|
||||
) or die $DBI::errstr;
|
||||
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Fetch the data from the query and use hashref slicing to extract
|
||||
#
|
||||
$h1 = $sth1->fetchrow_hashref();
|
||||
( $s_count, $t_count, $st_count )
|
||||
= @{$h1}{ 's_count', 't_count', 'st_count' };
|
||||
|
||||
_debug( $DEBUG > 1, '$s_count, $t_count, $st_count: ' .
|
||||
Dumper(\$s_count, \$t_count, \$st_count));
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Create the main query with selected sort arguments
|
||||
#-------------------------------------------------------------------------------
|
||||
my $sql = q{
|
||||
SELECT
|
||||
e.id,
|
||||
date_format(e.date,'00:00:00 %d/%m/%Y') AS date,
|
||||
e.title,
|
||||
h.host,
|
||||
e.hostid AS hostid,
|
||||
if(length(e.summary) = 0,0,1) AS summary,
|
||||
if(length(e.tags) = 0,0,1) AS tags
|
||||
FROM eps e JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE (length(e.summary) = 0
|
||||
OR length(e.tags) = 0)
|
||||
AND DATEDIFF(e.date,CURDATE()) <= 0
|
||||
};
|
||||
|
||||
$sql .= 'ORDER BY ' . join( ",", @order_by );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Perform the main query and grab the results as an array of hashes
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare($sql) or die $DBI::errstr;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
die $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Grab the data as an arrayref of hashrefs
|
||||
#
|
||||
$missing = $sth1->fetchall_arrayref( {} );
|
||||
|
||||
#
|
||||
# If asked for an action CSV file deal with it here
|
||||
#
|
||||
if ($acsvfile) {
|
||||
foreach my $row (@{$missing}) {
|
||||
printf $acsvfh "%s,%s,%s\n", $row->{id},$row->{summary},$row->{tags};
|
||||
}
|
||||
close($acsvfh);
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Perform a query relating hosts to shows. Each host who has shows in the
|
||||
# database without tags or a summary is returned with the list of shows
|
||||
# needing attention.
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(
|
||||
q{
|
||||
SELECT
|
||||
e.hostid,
|
||||
h.host,
|
||||
group_concat(e.id ORDER BY e.id) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE (length(e.summary) = 0
|
||||
OR length(e.tags) = 0)
|
||||
AND DATEDIFF(e.date,CURDATE()) <= 0
|
||||
GROUP BY e.hostid
|
||||
ORDER BY h.host
|
||||
}
|
||||
) or die $DBI::errstr;
|
||||
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
#
|
||||
# Grab the data as an arrayref of hashrefs
|
||||
#
|
||||
$showsbyhosts = $sth1->fetchall_arrayref( {} );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Perform a scan of episodes for tags and accumulate them in a hash
|
||||
#-------------------------------------------------------------------------------
|
||||
$sth1 = $dbh->prepare(
|
||||
q{SELECT id,title,tags FROM eps WHERE length(tags) > 0}
|
||||
) or die $DBI::errstr;
|
||||
|
||||
$sth1->execute;
|
||||
if ( $dbh->err ) {
|
||||
warn $dbh->errstr;
|
||||
}
|
||||
|
||||
my ( $status, @fields, %tag_ids, $lastkey, @tagindex, %showtitles );
|
||||
|
||||
my $csv = Text::CSV_XS->new(
|
||||
{ binary => 1, auto_diag => 1, allow_loose_quotes => 1 } );
|
||||
|
||||
#
|
||||
# Loop through the episodes returned by the query
|
||||
#
|
||||
while ( $h1 = $sth1->fetchrow_hashref ) {
|
||||
#
|
||||
# Stash the show title with the show number
|
||||
#
|
||||
$showtitles{$h1->{id}} = $h1->{title};
|
||||
|
||||
#
|
||||
# Parse the tag list for the current episode
|
||||
#
|
||||
$status = $csv->parse( $h1->{tags} );
|
||||
unless ($status) {
|
||||
#
|
||||
# Report any errors
|
||||
#
|
||||
print "Parse error on episode ", $h1->{id}, "\n";
|
||||
print $csv->error_input(), "\n";
|
||||
next;
|
||||
}
|
||||
@fields = $csv->fields();
|
||||
|
||||
#
|
||||
# Not sure why there are no tags but if not ignore this episode
|
||||
#
|
||||
next unless (@fields);
|
||||
|
||||
#
|
||||
# Trim and lowercase all tags
|
||||
#
|
||||
@fields = map {
|
||||
my $t = $_;
|
||||
$t =~ s/(^\s+|\s+$)//g;
|
||||
lc($t)
|
||||
} @fields;
|
||||
|
||||
#
|
||||
# Loop through the tags. For each tag add the associated episode id to the
|
||||
# %tag_ids hash. The key to this hash is the lower case tag and the value
|
||||
# is an array of episode numbers.
|
||||
#
|
||||
foreach my $tag (@fields) {
|
||||
if ( defined( $tag_ids{$tag} ) ) {
|
||||
#
|
||||
# Add to the existing array
|
||||
#
|
||||
push( @{$tag_ids{$tag}}, $h1->{id} );
|
||||
}
|
||||
else {
|
||||
#
|
||||
# Create the episode array
|
||||
#
|
||||
$tag_ids{$tag} = [$h1->{id}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Dumps the whole tags table. Warning!
|
||||
#
|
||||
_debug( $DEBUG > 2, '%tag_ids: ' . Dumper( \%tag_ids ) );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Make an alphabetic index of the tags
|
||||
#-------------------------------------------------------------------------------
|
||||
$lastkey = '';
|
||||
foreach my $tag ( sort( keys(%tag_ids) ) ) {
|
||||
if (substr($tag,0,1) ne $lastkey) {
|
||||
$lastkey = substr($tag,0,1);
|
||||
push(@tagindex,$tag);
|
||||
}
|
||||
}
|
||||
|
||||
_debug( $DEBUG > 1, '@tagindex: ' . Dumper( \@tagindex ) );
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Output tags and show numbers in CSV form if requested
|
||||
#-------------------------------------------------------------------------------
|
||||
if ($tcsvfile) {
|
||||
my @line;
|
||||
foreach my $tag ( sort( keys(%tag_ids) ) ) {
|
||||
push(@line,$tag);
|
||||
|
||||
foreach my $show (@{$tag_ids{$tag}}) {
|
||||
push(@line,$show);
|
||||
}
|
||||
|
||||
print $tcsvfh join(",",@line), "\n";
|
||||
@line = ();
|
||||
}
|
||||
close($tcsvfh);
|
||||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Output a JSON report of the TSU (Tag and Summary Update) project if
|
||||
# requested. Now no longer needed since the TSU project is finished.
|
||||
#-------------------------------------------------------------------------------
|
||||
if ($jsonfile) {
|
||||
my $jvars = {
|
||||
without_summaries => $s_count,
|
||||
without_tags => $t_count,
|
||||
without_either => $st_count,
|
||||
need_work => $s_count + $t_count - $st_count,
|
||||
};
|
||||
|
||||
my $json = JSON->new->utf8;
|
||||
print $jsonfh $json->encode($jvars);
|
||||
close($jsonfh);
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Fill and print the template
|
||||
#-------------------------------------------------------------------------------
|
||||
my $tt = Template->new(
|
||||
{ ABSOLUTE => 1,
|
||||
ENCODING => 'utf8',
|
||||
INCLUDE_PATH => $basedir,
|
||||
OUTPUT_PATH => '.',
|
||||
}
|
||||
);
|
||||
my $vars = {
|
||||
title => 'Shows without a summary and/or tags',
|
||||
order_by => \@order_by,
|
||||
s_count => $s_count,
|
||||
t_count => $t_count,
|
||||
st_count => $st_count,
|
||||
shows => $missing,
|
||||
byhost => $showsbyhosts,
|
||||
tag_ids => \%tag_ids,
|
||||
tagindex => \@tagindex,
|
||||
titles => \%showtitles,
|
||||
};
|
||||
my $document;
|
||||
$tt->process( $template, $vars, \$document, { binmode => ':utf8' } )
|
||||
|| die $tt->error(), "\n";
|
||||
|
||||
print $outfh $document;
|
||||
close($outfh);
|
||||
|
||||
$dbh->disconnect;
|
||||
|
||||
exit;
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: _debug
|
||||
# PURPOSE: Prints debug reports
|
||||
# PARAMETERS: $active Boolean: 1 for print, 0 for no print
|
||||
# $message Message to print
|
||||
# RETURNS: Nothing
|
||||
# DESCRIPTION: Outputs a message if $active is true. It removes any trailing
|
||||
# newline and then adds one in the 'print' to the caller doesn't
|
||||
# have to bother. Prepends the message with 'D> ' to show it's
|
||||
# a debug message.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub _debug {
|
||||
my ( $active, $message ) = @_;
|
||||
|
||||
chomp($message);
|
||||
print "D> $message\n" if $active;
|
||||
}
|
||||
|
||||
#=== FUNCTION ================================================================
|
||||
# NAME: getMultiOpts
|
||||
# PURPOSE: For use with a Getopt::Long option which is defined as "opt=s@"
|
||||
# PARAMETERS: $rargs reference to an array of arguments from
|
||||
# GetOpt::Long
|
||||
# $delim delimiter to be used to split arguments
|
||||
# $lc 1 -> lowercase the arguments, 0 -> leave alone
|
||||
# $rallowed reference to an array of allowed values (use
|
||||
# undef if anything is allowed)
|
||||
# RETURNS: A list containing all of the individual arguments
|
||||
# DESCRIPTION: With "opt=s@" in the option definition the script will only
|
||||
# accept the repetition of the option with different values. You
|
||||
# can present "-opt=a,b,c" but no special parsing is done on
|
||||
# this. This function extends option parsing to cater for such
|
||||
# lists. The list is made unique and care is taken to keep the
|
||||
# original order.
|
||||
# THROWS: No exceptions
|
||||
# COMMENTS: None
|
||||
# SEE ALSO: N/A
|
||||
#===============================================================================
|
||||
sub getMultiOpts {
|
||||
my ( $rargs, $delim, $lc, $rallowed ) = @_;
|
||||
|
||||
#
|
||||
# Defaults
|
||||
#
|
||||
$delim = ',' unless $delim;
|
||||
$lc = 0 unless $lc;
|
||||
|
||||
#
|
||||
# Walk through the array of arguments and check them, lower case them,
|
||||
# save them or split them
|
||||
#
|
||||
my @args;
|
||||
foreach my $arg (@$rargs) {
|
||||
$arg = lc($arg) if $lc;
|
||||
if ( $arg =~ /$delim/ ) {
|
||||
push( @args, split( /$delim/, $arg ) );
|
||||
}
|
||||
else {
|
||||
push( @args, $arg );
|
||||
}
|
||||
}
|
||||
|
||||
#
|
||||
# Remove the elements that aren't allowed
|
||||
#
|
||||
if ($rallowed) {
|
||||
my @new;
|
||||
foreach my $elem (@args) {
|
||||
push( @new, $elem ) if grep( /^$elem$/, @$rallowed );
|
||||
}
|
||||
@args = @new;
|
||||
}
|
||||
|
||||
#
|
||||
# De-duplicate the result (without losing the original order)
|
||||
#
|
||||
my @arr1;
|
||||
foreach my $elem (@args) {
|
||||
push( @arr1, $elem ) if !grep( /^$elem$/, @arr1 );
|
||||
}
|
||||
@args = @arr1;
|
||||
|
||||
#
|
||||
# Return the array as a list
|
||||
#
|
||||
return @args;
|
||||
}
|
||||
|
||||
#=== 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", "out=s", "sort=s@",
|
||||
"action-csv=s", "tags-csv=s", "json=s", "config=s",
|
||||
);
|
||||
|
||||
if ( !GetOptions( $optref, @options ) ) {
|
||||
pod2usage( -msg => "$PROG version $VERSION\n", -exitval => 1 );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
__END__
|
||||
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
# Application Documentation
|
||||
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
#{{{
|
||||
|
||||
=head1 NAME
|
||||
|
||||
report_missing_tags - Report missing HPR summaries and/or tags
|
||||
|
||||
=head1 VERSION
|
||||
|
||||
This documentation refers to B<report_missing_tags> version 0.1.4
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
./report_missing_tags [-help] [-debug=N] [-out=FILE]
|
||||
[-sort=FIELD1[,FIELD2...]] [-action-csv=FILE] [-tags-csv=FILE]
|
||||
[-json=FILE] [-config=FILE]
|
||||
|
||||
Examples:
|
||||
|
||||
./report_missing_tags
|
||||
./report_missing_tags -help
|
||||
./report_missing_tags -out=missing_tags.html
|
||||
./report_missing_tags -out=missing_tags.html -action-csv=tag_summary_actions.csv
|
||||
./report_missing_tags -out=missing_tags.html -tags-csv=tag_summary_actions.csv
|
||||
./report_missing_tags -out=missing_tags.html -json=tag_data.json
|
||||
./report_missing_tags -out=missing_tags.html -config=$HOME/HPR/.hpr_livedb.cfg
|
||||
|
||||
=head1 OPTIONS
|
||||
|
||||
=over 8
|
||||
|
||||
=item B<-help>
|
||||
|
||||
Prints a brief help message describing the usage of the program, and then exits.
|
||||
|
||||
=item B<-debug=N>
|
||||
|
||||
Causes certain debugging information to be displayed.
|
||||
|
||||
0 (the default) no debug output
|
||||
1 N/A
|
||||
2 reports the chosen sort order and the counts of work yet to be done
|
||||
(now obsolete). Also dumps:
|
||||
- @tagindex an array containing tags for the index
|
||||
3 dumps data structures:
|
||||
- %tag_ids: the data used to build the entire tag list (warning!)
|
||||
|
||||
=item B<-out=FILE>
|
||||
|
||||
This option defines an output file to receive the report. If the option is
|
||||
omitted the report is written to STDOUT, allowing it to be redirected if
|
||||
required.
|
||||
|
||||
=item B<-sort=FIELD1[,FIELD2...]>
|
||||
|
||||
Changes the sort order of the report. The default is to sort by the I<id>
|
||||
column, but other columns may be used, one or more.
|
||||
|
||||
=item B<-action-csv=FILE>
|
||||
|
||||
This optionally defines a file into which simple CSV data is written showing
|
||||
the actions needed for shows missing tags or summarise. The CSV rows consist
|
||||
of: the show number, a 0/1 value for the summary, and a 0/1 value for the
|
||||
tags. This is for the use of other tools that need to know if it's OK to
|
||||
present a show in a list for editing in order to add new summary and/or tags.
|
||||
See the way B<make_tsu_blank> and B<edit_tsu_blank> work.
|
||||
|
||||
=item B<-tags-csv=FILE>
|
||||
|
||||
This optionally defines a file to contain CSV for tags and shows which iuse
|
||||
them.
|
||||
|
||||
=item B<-json=FILE>
|
||||
|
||||
This optionally defines file into which some of the tag counts are written in
|
||||
JSON format. The items written are:
|
||||
|
||||
{
|
||||
"without_tags": 341,
|
||||
"without_summaries": 363,
|
||||
"need_work": 398,
|
||||
"without_either": 306
|
||||
}
|
||||
|
||||
=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
|
||||
|
||||
A full description of the application and its features.
|
||||
May include numerous subsections (i.e. =head2, =head3, etc.)
|
||||
|
||||
|
||||
=head1 DIAGNOSTICS
|
||||
|
||||
A list of every error and warning message that the application can generate
|
||||
(even the ones that will "never happen"), with a full explanation of each
|
||||
problem, one or more likely causes, and any suggested remedies. If the
|
||||
application generates exit status codes (e.g. under Unix) then list the exit
|
||||
status associated with each error.
|
||||
|
||||
|
||||
=head1 CONFIGURATION AND ENVIRONMENT
|
||||
|
||||
A full explanation of any configuration system(s) used by the application,
|
||||
including the names and locations of any configuration files, and the
|
||||
meaning of any environment variables or properties that can be set. These
|
||||
descriptions must also include details of any configuration language used
|
||||
|
||||
|
||||
=head1 DEPENDENCIES
|
||||
|
||||
A list of all the other modules that this module relies upon, including any
|
||||
restrictions on versions, and an indication whether these required modules are
|
||||
part of the standard Perl distribution, part of the module's distribution,
|
||||
or must be installed separately.
|
||||
|
||||
|
||||
=head1 INCOMPATIBILITIES
|
||||
|
||||
A list of any modules that this module cannot be used in conjunction with.
|
||||
This may be due to name conflicts in the interface, or competition for
|
||||
system or program resources, or due to internal limitations of Perl
|
||||
(for example, many modules that use source code filters are mutually
|
||||
incompatible).
|
||||
|
||||
|
||||
=head1 BUGS AND LIMITATIONS
|
||||
|
||||
A list of known problems with the module, together with some indication
|
||||
whether they are likely to be fixed in an upcoming release.
|
||||
|
||||
Also a list of restrictions on the features the module does provide:
|
||||
data types that cannot be handled, performance issues and the circumstances
|
||||
in which they may arise, practical limitations on the size of data sets,
|
||||
special cases that are not (yet) handled, etc.
|
||||
|
||||
The initial template usually just has:
|
||||
|
||||
There are no known bugs in this module.
|
||||
Please report problems to <Maintainer name(s)> (<contact address>)
|
||||
Patches are welcome.
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Dave Morriss (Dave.Morriss@gmail.com)
|
||||
|
||||
=head1 LICENCE AND COPYRIGHT
|
||||
|
||||
Copyright (c) 2015 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.
|
||||
|
||||
=cut
|
||||
|
||||
#}}}
|
||||
|
||||
# [zo to open fold, zc to close]
|
||||
|
||||
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|
1
Database/report_missing_tags.tpl
Symbolic link
1
Database/report_missing_tags.tpl
Symbolic link
@@ -0,0 +1 @@
|
||||
report_missing_tags_0.1.1.tpl
|
254
Database/report_missing_tags_0.1.1.tpl
Normal file
254
Database/report_missing_tags_0.1.1.tpl
Normal file
@@ -0,0 +1,254 @@
|
||||
[%# report_missing_tags.tpl 2022-08-03 -%]
|
||||
[%# version: 0.1.1 -%]
|
||||
[%# Page summarising data from the database, generates PHP -%]
|
||||
[%# USE dumper -%]
|
||||
[%- USE date -%]
|
||||
[%- DEFAULT title = 'Test' -%]
|
||||
[%- DEFAULT htmltags = 'tags.html' -%]
|
||||
[%- table_size = 37 count = 0 anchors = [] -%]
|
||||
<?php
|
||||
# released under a Creative Commons Attribution-ShareAlike 3.0 Unported license. http://creativecommons.org/licenses/by-sa/3.0/
|
||||
$body="help_out";
|
||||
require "/home/hpr/php/hpr.php";
|
||||
include '/home/hpr/www/header.php';
|
||||
?>
|
||||
|
||||
<main id="maincontent">
|
||||
<style>
|
||||
.columns3 { columns: 3 auto; list-style-type: none }
|
||||
hr.thin {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
p.ralign { text-align: right }
|
||||
</style>
|
||||
<article>
|
||||
<a id="TOP"><h1 class="title">[% title %]</h1></a>
|
||||
<h3 class="date">Page generated on [% date.format(date.now,'%Y-%m-%d at %H:%M:%S UTC','en_GB',1) %]</h3>
|
||||
<h4>Sort order: [%- order_by.join(', ') %]</h4>
|
||||
<h4>Current counts</h4>
|
||||
<ul>
|
||||
<li><b>[% s_count %]</b> shows without summaries</li>
|
||||
<li><b>[% t_count %]</b> shows without tags</li>
|
||||
<li><b>[% st_count %]</b> shows with neither summaries nor tags</li>
|
||||
<li><b>[% needwork = s_count+t_count-st_count; needwork %]</b> shows which need work</li>
|
||||
</ul>
|
||||
<h4>Instructions</h4>
|
||||
<ol>
|
||||
<li>Find a show in the list below</li>
|
||||
<li>Check in the list which attributes are missing: summary and/or tags</li>
|
||||
<li>Click the show number or title to visit the show page</li>
|
||||
<li>Read the show notes and listen to the show to determine the missing information</li>
|
||||
<li>Submit your updates by email to <em>tags at hackerpublicradio.org</em></li>
|
||||
</ol>
|
||||
<p>Please send simple ASCII email. No HTML please, and no multipart, encrypted
|
||||
or signed messages; the script can't handle them at the moment! (We are
|
||||
working on a solution to some of this though). Remember, the internals of an
|
||||
email are <em>complex</em> and the script isn't clever enough to deal with all
|
||||
the many possible formats. <strong>Please be gentle with it!</strong></p>
|
||||
<p>Format the message as follows:</p>
|
||||
<pre>
|
||||
show:12345
|
||||
summary:Using Linux at Christmas to make tomato soup in a sporran
|
||||
tags:linux,christmas,sporran,tomato soup
|
||||
|
||||
show: 12346
|
||||
tags: sausage,clothing,hairpiece
|
||||
</pre>
|
||||
<ul>
|
||||
<li>Start with the <code>show:XXXX</code> line (just the show number, no
|
||||
<code>'hpr'</code>)</li>
|
||||
<li>If either the summary or the tags are already present on the show you can omit them from the group</li>
|
||||
<li>It's not possible to change existing summaries or tags by this route, only to add missing ones</li>
|
||||
<li>Ensure the summary text isn't longer than 100 characters</li>
|
||||
<li>The tags need to be separated by commas</li>
|
||||
<li>If you need to add a tag with a comma in it enclose the tag in double quotes</li>
|
||||
<li>The length of the tag list can't exceed 200 characters</li>
|
||||
<li>You can update more than one show per email if you want</li>
|
||||
<li>Blank lines between the groups of <em>show</em>/<em>summary</em>/<em>tags</em> lines are fine (as shown), as are comment lines beginning with '#'</li>
|
||||
</ul>
|
||||
<p>Updates will be processed with a script, which is run manually, and this page
|
||||
will be refreshed once the changes have been made. The timestamp above shows
|
||||
when it was last refreshed.</p>
|
||||
<hr/>
|
||||
</article>
|
||||
|
||||
[% IF needwork > 0 -%]
|
||||
|
||||
[%- BLOCK table_head -%]
|
||||
<p>
|
||||
<a href="#END">Go to END of section</a>
|
||||
</p>
|
||||
<table id="t01" style="width:100%">
|
||||
<tr>
|
||||
<th style="width:5%">Id</th>
|
||||
<th style="width:12%">Date</th>
|
||||
<th style="width:50%">Title</th>
|
||||
<th style="width:17%">Host</th>
|
||||
<th style="width:8%">Summary</th>
|
||||
<th style="width:8%">Tags</th>
|
||||
</tr>
|
||||
[%- END -%]
|
||||
|
||||
[%- BLOCK table_foot -%]
|
||||
</table>
|
||||
<p>
|
||||
<a href="#TOP">Go to TOP of page</a>
|
||||
</p>
|
||||
[%- END -%]
|
||||
|
||||
<article>
|
||||
<h3>Section Index</h3>
|
||||
<ul>
|
||||
<li><a href="#Shows_by_host">Shows by host</a></li>
|
||||
<li><a href="#Tag_summary">Tag summary</a></li>
|
||||
</ul>
|
||||
<hr/>
|
||||
</article>
|
||||
|
||||
[%# Make the index. Point to the first show and every $table_size shows -%]
|
||||
[%# thereafter. Keep the id numbers in a list for use later. -%]
|
||||
<article>
|
||||
<h3>Tables of shows requiring attention</h3>
|
||||
<p>The following index is to help you find shows in need of attention.</p>
|
||||
|
||||
<h4>Index of tables</h4>
|
||||
<ul>
|
||||
[%- count = 0 %]
|
||||
[%- FOREACH row IN shows %]
|
||||
[%- IF count % table_size == 0 && count < shows.size %]
|
||||
<li><a href="#show[% row.id %]">Go to show number [% row.id %]</a></li>
|
||||
[%- anchors.push(row.id) %]
|
||||
[%- END %]
|
||||
[%- count = count + 1 %]
|
||||
[%- END %]
|
||||
</ul>
|
||||
<hr/>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
[% count = 0 -%]
|
||||
<small>
|
||||
<a id="show[% anchors.shift %]"></a>
|
||||
[%- PROCESS table_head %]
|
||||
[%- FOREACH row IN shows %]
|
||||
<tr valign="top">
|
||||
<td align="left"><strong><a href="http://hackerpublicradio.org/eps.php?id=[% row.id FILTER format("%04i") %]" target="_blank">[% row.id %]</a></strong></td>
|
||||
<td align="left">[% date.format(row.date,'%Y-%m-%d') %]</td>
|
||||
<td align="left"><a href="http://hackerpublicradio.org/eps.php?id=[% row.id FILTER format("%04i") %]" target="_blank">[% row.title %]</a></td>
|
||||
<td align="left">[% row.host FILTER html_entity %]</a></td>
|
||||
<td align="left">[% row.summary == 0 ? 'No' : 'Yes' %]</a></td>
|
||||
<td align="left">[% row.tags == 0 ? 'No' : 'Yes' %]</a></td>
|
||||
</tr>
|
||||
[%- count = count + 1 -%]
|
||||
[%- IF count % table_size == 0 && count < shows.size -%]
|
||||
[%- PROCESS table_foot %]
|
||||
<a id="show[% anchors.shift %]"></a>
|
||||
[%- PROCESS table_head %]
|
||||
[%- END -%]
|
||||
[%- END %]
|
||||
[%- PROCESS table_foot %]
|
||||
</small>
|
||||
<a id="END"></a>
|
||||
<p>Total shows = [% count %]</p>
|
||||
<hr/>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<a id="Shows_by_host"><h3>Shows by host</h3></a>
|
||||
<p>This section lists all HPR hosts who have shows without summaries and/or tags. The show numbers in question are listed against each host and can be clicked to open the show. </p>
|
||||
<p> If you are one of the hosts below it would be very much appreciated if you could help out by adding the summary and or tags to all of your shows that do not have them. You are the person who is best able to add these missing items to your shows!</p>
|
||||
<ul>
|
||||
[%- FOREACH row IN byhost %]
|
||||
<li><strong><a href="http://hackerpublicradio.org/correspondents.php?hostid=[% row.hostid %]" target="_blank">[% row.host %]</a></strong>:
|
||||
[%- showlist = row.shows.split(',') %]
|
||||
[%- count = 0 %]
|
||||
[%- FOREACH show IN showlist %]
|
||||
[%- count = count + 1 %]
|
||||
<a href="http://hackerpublicradio.org/eps.php?id=[% show FILTER format("%04i") %]" target="_blank">[% show %]</a>
|
||||
[%- count < showlist.size ? ', ' : '' %]
|
||||
[%- END %]
|
||||
</li>
|
||||
[%- END %]
|
||||
</ul>
|
||||
<p>
|
||||
<a href="#TOP">Go to TOP of page</a>
|
||||
</p>
|
||||
<hr/>
|
||||
</article>
|
||||
|
||||
[% ELSE -%]
|
||||
<article>
|
||||
<h3>The project is finished!</h3>
|
||||
<p>All shows have been processed! Thanks to everyone who has helped!</p>
|
||||
<hr/>
|
||||
</article>
|
||||
[% END -%]
|
||||
|
||||
<article>
|
||||
[% BLOCK tags -%]
|
||||
<a id="Tag_summary"><h3>Tag summary</h3></a>
|
||||
<h4 class="date">Page generated on [% date.format(date.now,'%Y-%m-%d at %H:%M:%S UTC','en_GB',1) %]</h4>
|
||||
<p>This section summarises all of the tags currently used throughout the
|
||||
database. The tags are in alphabetical order and each is followed by links to
|
||||
the show numbers where it is used so you can see the context the author used
|
||||
it in. There are currently [% tag_ids.size %] unique tags in the system.</p>
|
||||
<h4>Alphabetical index</h4>
|
||||
<p>This is an index to the initial letters of the tags below.</p>
|
||||
<ul class="columns3">
|
||||
[%# tagindex contains the first tag in an alphabetic list that has a different
|
||||
first letter from the previous one. We use it to build an alphabetic table of
|
||||
anchors linking to the blocks of tags starting with that character. -%]
|
||||
[%- FOREACH index IN tagindex %]
|
||||
<li><a href="#[% index.replace('\s','_') %]"><strong>[% index.substr(0,1) %]</strong></a></li>
|
||||
[%- END %]
|
||||
</ul>
|
||||
<hr/>
|
||||
[%# BLOCK tags -%]
|
||||
<ul>
|
||||
[%# tag_ids is a hash keyed by tags, each containing an array of episode
|
||||
numbers. If a tag matches the one in 'index' place an anchor to it for the
|
||||
alphabetic index above. %]
|
||||
[%- index = tagindex.shift %]
|
||||
[%- FOREACH pair IN tag_ids.pairs %]
|
||||
[%- IF pair.key == index %]
|
||||
</ul>
|
||||
<p class="ralign"><a href="#Tag_summary">🡱 Go to index</a></p>
|
||||
<h3>Tags beginning with '[% index.substr(0,1) %]'</h3>
|
||||
<ul>
|
||||
<li>
|
||||
[%- index = tagindex.shift %]
|
||||
[%- ELSE %]
|
||||
<li>
|
||||
[%- END %]
|
||||
<a id="[% pair.key.replace('\s','_') %]"><strong>[% pair.key FILTER html_entity %]</strong></a>:
|
||||
[%- count = 0 %]
|
||||
[%- FOREACH id IN pair.value.nsort %]
|
||||
[%- count = count + 1 %]
|
||||
<a href="http://hackerpublicradio.org/eps.php?id=[% id FILTER format("%04i") %]"
|
||||
target="_blank" aria-label="Tag [% pair.key FILTER html_entity %]: show [% id %]">[% id %]</a>
|
||||
[%- count < pair.value.size ? ', ' : '' %]
|
||||
[%- END %]
|
||||
</li>
|
||||
[%- END %]
|
||||
</ul>
|
||||
[% END -%]
|
||||
[% INCLUDE tags FILTER redirect("$htmltags") -%]
|
||||
[% INSERT $htmltags -%]
|
||||
<p>
|
||||
<a href="#TOP">Go to TOP of page</a>
|
||||
</p>
|
||||
</article>
|
||||
|
||||
</main>
|
||||
|
||||
|
||||
<?php
|
||||
include 'footer.html';
|
||||
?>
|
||||
|
||||
[%#
|
||||
vim: syntax=tt2:ts=8:sw=4:ai:et:tw=78:fo=tcrqn21
|
||||
-%]
|
47
Database/reservations.sql
Normal file
47
Database/reservations.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- MySQL dump 10.13 Distrib 5.6.30, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: hpr_hpr
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 5.6.30-1
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!40101 SET NAMES utf8 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `reservations`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `reservations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `reservations` (
|
||||
`ip` varchar(45) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`timestamp` datetime NOT NULL,
|
||||
`key` varchar(46) COLLATE utf8_unicode_ci NOT NULL,
|
||||
`ep_num` int(5) NOT NULL,
|
||||
`ep_date` date NOT NULL,
|
||||
`email` text COLLATE utf8_unicode_ci NOT NULL,
|
||||
`verified` tinyint(1) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`key`),
|
||||
UNIQUE KEY `key` (`key`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT='To keep track of reservations';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2016-08-07 22:06:16
|
56
Database/series_eps.sql
Normal file
56
Database/series_eps.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
--
|
||||
-- Set up a many-to-many relationship between tables (mini)series and eps
|
||||
-- -----------------------------------------------------------------------------
|
||||
--
|
||||
|
||||
--
|
||||
-- Table structure for the mapping table 'series_eps'
|
||||
--
|
||||
DROP TABLE IF EXISTS series_eps;
|
||||
CREATE TABLE IF NOT EXISTS series_eps (
|
||||
series_id int(5) NOT NULL
|
||||
REFERENCES miniseries(id),
|
||||
eps_id int(5) NOT NULL
|
||||
REFERENCES eps(id),
|
||||
PRIMARY KEY series_eps_pk (series_id,eps_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
SHOW warnings;
|
||||
|
||||
--
|
||||
-- Make a view to simplify access to eps and (mini)series. This simply
|
||||
-- joins the two tables through the 'series_eps' table. Some fields need
|
||||
-- renaming to avoid clashes.
|
||||
--
|
||||
DROP VIEW IF EXISTS eps_with_series;
|
||||
CREATE VIEW eps_with_series AS
|
||||
SELECT
|
||||
e.id AS eps_id,
|
||||
e.date,
|
||||
e.title,
|
||||
e.duration,
|
||||
e.summary,
|
||||
e.notes,
|
||||
e.hostid,
|
||||
e.explicit,
|
||||
e.license,
|
||||
e.tags,
|
||||
e.version,
|
||||
e.downloads,
|
||||
e.valid AS eps_valid,
|
||||
ms.id AS series_id,
|
||||
ms.name,
|
||||
ms.description,
|
||||
ms.private,
|
||||
ms.image,
|
||||
ms.valid AS series_valid
|
||||
FROM eps e
|
||||
JOIN series_eps se ON (e.id = se.eps_id)
|
||||
JOIN miniseries ms ON (ms.id = se.series_id)
|
||||
ORDER BY e.id, ms.id;
|
||||
|
||||
SHOW warnings;
|
||||
|
||||
/*
|
||||
vim: syntax=sql ai tw=75:
|
||||
*/
|
212
Database/shows_per_host_in_last_year.sql
Normal file
212
Database/shows_per_host_in_last_year.sql
Normal file
@@ -0,0 +1,212 @@
|
||||
--
|
||||
-- What's the latest show today?
|
||||
--
|
||||
|
||||
SELECT concat('Latest show number on this date: ',curdate()) AS '';
|
||||
|
||||
SELECT max(id) AS latest_show
|
||||
FROM eps
|
||||
WHERE date <= curdate();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- How many hosts are there up to and including today? Note there are 49 hosts
|
||||
-- with no shows at all, imported from the TWAT data, so a simple count of
|
||||
-- rows is not going to be correct.
|
||||
--
|
||||
|
||||
SELECT count(*) AS total_users_to_today
|
||||
FROM
|
||||
( SELECT e.id
|
||||
FROM hosts h,
|
||||
eps e
|
||||
WHERE e.hostid = h.hostid
|
||||
AND e.date <= curdate()
|
||||
GROUP BY h.host) AS ttab;
|
||||
|
||||
--
|
||||
-- How many new hosts are there in the (future) queue?
|
||||
--
|
||||
|
||||
SELECT h.host AS upcoming_new_host,
|
||||
min(e.date) AS joindate
|
||||
FROM hosts h,
|
||||
eps e
|
||||
WHERE e.hostid = h.hostid
|
||||
GROUP BY h.host
|
||||
HAVING min(e.date) > curdate();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Find all new hosts who joined in the last year (ignoring those with queued
|
||||
-- shows in the future)
|
||||
--
|
||||
|
||||
SELECT 'Hosts joining in the last year with number of shows done' AS '';
|
||||
|
||||
SELECT h.hostid,
|
||||
h.host,
|
||||
min(e.date) AS joindate,
|
||||
count(e.id) AS COUNT
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
GROUP BY h.hostid
|
||||
HAVING min(e.date) >= (curdate() - INTERVAL 364 DAY)
|
||||
AND min(e.date) <= curdate()
|
||||
ORDER BY min(e.date);
|
||||
|
||||
SELECT COUNT(*) AS new_host_count,
|
||||
sum(COUNT) AS total_shows
|
||||
FROM
|
||||
( SELECT h.hostid,
|
||||
h.host,
|
||||
min(e.date) AS joindate,
|
||||
count(e.id) AS COUNT
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
GROUP BY h.hostid
|
||||
HAVING min(e.date) >= (curdate() - INTERVAL 364 DAY)
|
||||
AND min(e.date) <= curdate()
|
||||
ORDER BY min(e.date)) src;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Display all hosts contributing > 10 shows in the last year then count the
|
||||
-- number of shows
|
||||
--
|
||||
|
||||
SELECT 'Hosts contributing > 10 shows in the last year' AS '';
|
||||
|
||||
SELECT e.hostid,
|
||||
h.host,
|
||||
COUNT(e.hostid) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE e.date <= curdate()
|
||||
AND e.date >= (curdate() - interval 364 DAY)
|
||||
GROUP BY e.hostid
|
||||
HAVING COUNT(e.hostid) > 10
|
||||
ORDER BY COUNT(e.hostid) DESC;
|
||||
|
||||
|
||||
SELECT COUNT(*) AS no_of_hosts,
|
||||
sum(shows) AS sum_of_shows_by_group
|
||||
FROM
|
||||
( SELECT e.hostid,
|
||||
h.host,
|
||||
COUNT(e.hostid) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE e.date <= curdate()
|
||||
AND e.date >= (curdate() - interval 364 DAY)
|
||||
GROUP BY e.hostid
|
||||
HAVING COUNT(e.hostid) > 10
|
||||
ORDER BY COUNT(e.hostid) DESC) AS ttab;
|
||||
|
||||
--
|
||||
-- Display all hosts contributing > 5 shows in the last year then count the
|
||||
-- number of shows. Remember this includes the > 10 group too!
|
||||
--
|
||||
|
||||
SELECT 'Hosts contributing > 5 shows in the last year' AS '';
|
||||
|
||||
SELECT e.hostid,
|
||||
h.host,
|
||||
COUNT(e.hostid) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE e.date <= curdate()
|
||||
AND e.date >= (curdate() - interval 364 DAY)
|
||||
GROUP BY e.hostid
|
||||
HAVING COUNT(e.hostid) > 5
|
||||
ORDER BY COUNT(e.hostid) DESC;
|
||||
|
||||
|
||||
SELECT COUNT(*) AS no_of_hosts,
|
||||
sum(shows) AS sum_of_shows_by_group
|
||||
FROM
|
||||
( SELECT e.hostid,
|
||||
h.host,
|
||||
COUNT(e.hostid) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE e.date <= curdate()
|
||||
AND e.date >= (curdate() - interval 364 DAY)
|
||||
GROUP BY e.hostid
|
||||
HAVING COUNT(e.hostid) > 5
|
||||
ORDER BY COUNT(e.hostid) DESC) AS ttab;
|
||||
|
||||
--
|
||||
-- How many hosts contributed shows in the last year
|
||||
--
|
||||
|
||||
SELECT 'Number of hosts contributing shows in the last year' AS '';
|
||||
|
||||
|
||||
SELECT COUNT(*) AS hosts_contributing_last_year,
|
||||
sum(shows) AS sum_of_shows_by_group
|
||||
FROM
|
||||
( SELECT e.hostid,
|
||||
h.host,
|
||||
COUNT(e.hostid) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE e.date <= curdate()
|
||||
AND e.date >= (curdate() - interval 364 DAY)
|
||||
GROUP BY e.hostid
|
||||
ORDER BY COUNT(e.hostid) DESC ) AS hly;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- How many new hosts were there per year and how many shows have they
|
||||
-- contributed overall?
|
||||
--
|
||||
|
||||
SELECT 'Hosts per year and their contributions' AS '';
|
||||
|
||||
SELECT joinyear,
|
||||
COUNT(*) AS new_hosts,
|
||||
sum(shows) AS shows
|
||||
FROM
|
||||
( SELECT extract(YEAR
|
||||
FROM min(e.date)) AS joinyear,
|
||||
count(e.id) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE e.date <= curdate()
|
||||
GROUP BY h.hostid
|
||||
ORDER BY min(e.date)) AS ttab
|
||||
GROUP BY joinyear;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Hosts who joined, did a show but haven't been seen since January of the
|
||||
-- year 3 years ago.
|
||||
--
|
||||
|
||||
SELECT 'Hosts not seen for the past three years' AS '';
|
||||
|
||||
SELECT COUNT(*) AS departed_hosts
|
||||
FROM
|
||||
( SELECT h.hostid,
|
||||
h.host,
|
||||
min(e.date) AS joindate,
|
||||
max(e.date) AS lastshow,
|
||||
count(e.id) AS shows
|
||||
FROM eps e
|
||||
JOIN hosts h ON e.hostid = h.hostid
|
||||
WHERE e.date <= curdate()
|
||||
GROUP BY h.hostid
|
||||
HAVING lastshow < date_format(date_sub(curdate(),INTERVAL 3 YEAR),'%Y-01-01')
|
||||
ORDER BY shows,
|
||||
min(e.date)) ttab;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- Show hosts and shows per month throughout the HPR history
|
||||
--
|
||||
|
||||
SELECT 'Hosts and shows per month throughout the HPR history' AS '';
|
||||
|
||||
SELECT extract(year_month
|
||||
FROM e.date) AS MONTH,
|
||||
COUNT(e.id) AS shows,
|
||||
COUNT(DISTINCT e.hostid) AS hosts
|
||||
FROM eps e
|
||||
WHERE e.date < date_format(curdate(),"%Y-%c-01")
|
||||
GROUP BY MONTH;
|
||||
|
0
Database/tag_summary_actions.csv
Normal file
0
Database/tag_summary_actions.csv
Normal file
|
38
Database/triggers.sql
Normal file
38
Database/triggers.sql
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Trigger declarations for hpr_hpr
|
||||
*
|
||||
*/
|
||||
|
||||
--
|
||||
-- Table 'log' to hold details of certain background activities invoked by
|
||||
-- stored procedures and triggers
|
||||
--
|
||||
DROP TABLE IF EXISTS log;
|
||||
CREATE TABLE log (
|
||||
id int(5) NOT NULL AUTO_INCREMENT,
|
||||
stamp timestamp DEFAULT now(),
|
||||
message text NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
--
|
||||
-- When an episode is added to the eps table check whether there is an
|
||||
-- entry in the reservations table with the same id. If there is, delete
|
||||
-- it. Log the deletion in the log table.
|
||||
--
|
||||
DROP TRIGGER IF EXISTS check_reservations;
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER check_reservations BEFORE INSERT ON eps
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF EXISTS(SELECT id FROM reservations WHERE id = NEW.id) THEN
|
||||
DELETE FROM reservations WHERE id = NEW.id;
|
||||
INSERT INTO log (message) VALUES(concat('DELETE FROM reservations WHERE id = ',NEW.id));
|
||||
END IF;
|
||||
END;
|
||||
$$
|
||||
DELIMITER ;
|
||||
|
||||
/*
|
||||
vim: syntax=sql ai tw=75:
|
||||
*/
|
56
Database/tsu/X_tag_summary_updates_1060-1079.txt
Normal file
56
Database/tsu/X_tag_summary_updates_1060-1079.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
show: 1063
|
||||
summary: Following an interview with Richard Stallman on the Linux Action Show
|
||||
tags: software freedom,GPL,BSD licence
|
||||
|
||||
show: 1064
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1065
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1067
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1068
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1069
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1071
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1073
|
||||
summary: An introduction to the concept of presentation versus content
|
||||
tags: presentation,content,css
|
||||
|
||||
show: 1074
|
||||
summary: Interview with Alan Pope
|
||||
tags: interview,oggcamp,oggcamp11
|
||||
|
||||
show: 1075
|
||||
summary: A newscast from Talk Geek to Me
|
||||
tags: newscast,TGTM
|
||||
|
||||
show: 1076
|
||||
summary: Describing the upcoming Ohio LinuxFest conference and expo
|
||||
tags: interview,OLF 2012,Ohio LinuxFest
|
||||
|
||||
show: 1077
|
||||
summary: Interview with Christina Haralanova from Canada
|
||||
tags: interview,FSCONS 2011,freedom,hacking
|
||||
|
||||
show: 1078
|
||||
summary: The European Parliament and the issue of software patentability
|
||||
tags: patent,software patent,unitary patent,Free Software
|
||||
|
||||
show: 1079
|
||||
summary: Episode 12: LMAX
|
||||
tags: Distributed Systems Podcast,LMAX Disruptor,Java,C,C++,Fedora,Red Hat
|
||||
|
56
Database/tsu/X_tag_summary_updates_1080-1099.txt
Normal file
56
Database/tsu/X_tag_summary_updates_1080-1099.txt
Normal file
@@ -0,0 +1,56 @@
|
||||
show: 1081
|
||||
summary: Preparing photographs for posting on a website
|
||||
tags: GIMP,photography
|
||||
|
||||
show: 1083
|
||||
summary: The concept of a compiler
|
||||
tags: compiler,neocortex,lexical analysis,preprocessing,parsing,semantic analysis,code generation,assembling,linking
|
||||
|
||||
show: 1084
|
||||
summary: A Full Circle Podcast interview with Paul Levy
|
||||
tags: Interview,Full Circle Podcast
|
||||
|
||||
show: 1085
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1087
|
||||
summary:
|
||||
tags: FSCONS
|
||||
|
||||
show: 1088
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1089
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1092
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1093
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1094
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1096
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1097
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1098
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 1099
|
||||
summary:
|
||||
tags:
|
||||
|
43
Database/tsu/shows_from_host30_part_1.txt
Normal file
43
Database/tsu/shows_from_host30_part_1.txt
Normal file
@@ -0,0 +1,43 @@
|
||||
show: 0036
|
||||
tags: LPI, bus, System Bus, PCI Bus
|
||||
summary: Ken covers computer buses and system resources
|
||||
|
||||
show: 0056
|
||||
tags: Open Street Map, GPS, POI
|
||||
summary: Ken encourages people to add to Open Street Map
|
||||
|
||||
show: 0057
|
||||
tags: LPI, hard disks
|
||||
summary: Ken covers how disks are dealt with in Linux
|
||||
|
||||
show: 0102
|
||||
tags: LPI, SCSI
|
||||
summary: Ken covers SCSI skipping over modem and sound
|
||||
|
||||
show: 0115
|
||||
tags: Linux Promotion
|
||||
summary: Ken Fallon discusses ways to promote linux
|
||||
|
||||
show: 0135
|
||||
tags: LPI, lspci
|
||||
summary: Setup different PC expansion cards
|
||||
|
||||
show: 0140
|
||||
tags: LPI, modems, ISDN, DSL
|
||||
summary: LPIC topic 1.101.6 — Configure Communication Devices
|
||||
|
||||
show: 0160
|
||||
tags: dvgrab, archiving
|
||||
summary: Ken walks us through moving off DV tapes to disk
|
||||
|
||||
show: 0185
|
||||
tags: sox, sleep, tar
|
||||
summary: Ken gives us quick bash tips
|
||||
|
||||
show: 0206
|
||||
tags: promotion
|
||||
summary: Ken's failed attempt to set up a site that promotes devices that run linux
|
||||
|
||||
show: 0227
|
||||
tags: Squid, proxy server
|
||||
summary: Ken explains how to install and run a local squid proxy
|
28
Database/tsu/tag_summary_updates_0530-0539.txt
Normal file
28
Database/tsu/tag_summary_updates_0530-0539.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
#show: 533
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
show: 534
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 535
|
||||
summary:
|
||||
tags:
|
||||
|
||||
show: 536
|
||||
summary:
|
||||
tags:
|
||||
|
||||
#show: 537
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 538
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 539
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
16
Database/tsu/tag_summary_updates_0540-0544.txt
Normal file
16
Database/tsu/tag_summary_updates_0540-0544.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
#show: 540
|
||||
##summary: Uber Leet Hacker Force Radio issue 4
|
||||
#tags:
|
||||
#
|
||||
#show: 541
|
||||
##summary: Moxie Marlinspike interview
|
||||
#tags:
|
||||
#
|
||||
#show: 542
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 544
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
20
Database/tsu/tag_summary_updates_0545-0549.txt
Normal file
20
Database/tsu/tag_summary_updates_0545-0549.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
#show: 545
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 546
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 547
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 548
|
||||
#summary:
|
||||
##tags: Spam
|
||||
#
|
||||
#show: 549
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
40
Database/tsu/tag_summary_updates_0550-0559.txt
Normal file
40
Database/tsu/tag_summary_updates_0550-0559.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
#show: 550
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 551
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 552
|
||||
##summary: Uber Leet Hacker Force Radio issue 5
|
||||
#tags:
|
||||
#
|
||||
#show: 553
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 554
|
||||
#summary:
|
||||
##tags: Wifi,Wireless,WAP
|
||||
#
|
||||
#show: 555
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 556
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 557
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 558
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
||||
#show: 559
|
||||
#summary:
|
||||
#tags:
|
||||
#
|
32
Database/tsu/tag_summary_updates_0560-0569.txt
Normal file
32
Database/tsu/tag_summary_updates_0560-0569.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 560
|
||||
summary: A discussion of "podfading" - the fading away of once-active podcasts
|
||||
tags: Podcasting,Podfading
|
||||
|
||||
show: 561
|
||||
#summary: Hacker Radio Live discusses vulnerabilities in WEP (Wired equivalent privacy) encryption
|
||||
tags: encryption,WEP,Wired equivalent privacy,stream cipher
|
||||
|
||||
show: 564
|
||||
summary: An innovative mobile computing platform, the "Robocop mobile computing fortissimo", aka robomofo
|
||||
tags: mobile computing,tablet,video glasses,Vuzix,Pandora,mini-ITX
|
||||
|
||||
show: 565
|
||||
summary: Hacker Radio Live discusses and demonstrates radio scanners
|
||||
tags: scanner,radio
|
||||
|
||||
show: 566
|
||||
summary: An experimental scheme to micro-manage personal free time
|
||||
tags: time management,productivity,gopher,phlog
|
||||
|
||||
show: 567
|
||||
summary: Visiting the University of Minnesota Supercomputing Institute
|
||||
tags: Minnesota,supercomputer,HPC,Cray,Beowulf Cluster,PVM
|
||||
|
||||
show: 568
|
||||
#summary: Hacker Radio Live discusses MythTV and how Mr. E. Nigma uses it at home
|
||||
tags: MythTV,MythBuntu,capture card,Linux MCE,XBMC
|
||||
|
||||
show: 569
|
||||
summary: Installing Windows 7 Ultimate under Virtual Box
|
||||
#tags: Windows,Install,Virtual
|
||||
|
36
Database/tsu/tag_summary_updates_0570-0579.txt
Normal file
36
Database/tsu/tag_summary_updates_0570-0579.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
show: 570
|
||||
summary: The upcoming Google Privacy Policy is read by espeak
|
||||
tags: policy,privacy,Google
|
||||
|
||||
show: 572
|
||||
#summary: In this episode Klaatu talks to Mark Terranova from Zareason
|
||||
tags: Zareason,Linux computer,SELF 2010
|
||||
|
||||
show: 573
|
||||
#summary: Episode 39 of "Linux In The Ham Shack" syndicated on HPR
|
||||
tags: Ubuntu 10.04,Crunchbang,morse code,SELF 2010
|
||||
|
||||
show: 574
|
||||
summary: Maco and her new Sign Language Tutor application
|
||||
tags: Qt,KDE,Ubuntu Women
|
||||
|
||||
show: 575
|
||||
summary: A recording of a presentation by Robert McWilliam from Software Freedom Day Event 2009
|
||||
tags: FOSS,Windows
|
||||
|
||||
show: 576
|
||||
summary: HeathenX from the screencasters speaks about art on Linux
|
||||
#tags: OLF 2009,interview
|
||||
|
||||
show: 577
|
||||
summary: An interview recorded at PyCon 2010, Atlanta, with Antoine Pitrou
|
||||
tags: Python,twisted,wxWidgets,CPython,Global interpreter lock,GIL
|
||||
|
||||
show: 578
|
||||
summary: A recording of a presentation by Nick Walker from Software Freedom Day Event 2009
|
||||
tags: open source security
|
||||
|
||||
show: 579
|
||||
summary: Jeff, a student, sponsor of SELF and volunteer and Loafy, a volunteer, first time at SELF
|
||||
tags: SELF 2010,volunteer,interview
|
||||
|
20
Database/tsu/tag_summary_updates_0580-0589.txt
Normal file
20
Database/tsu/tag_summary_updates_0580-0589.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
show: 580
|
||||
summary: Several well-known HPR contributors are recorded in discussion
|
||||
tags: recording methods,HPR history,audio editing,mono,encoding
|
||||
|
||||
show: 581
|
||||
summary: A talk by Phillip Geyer at Software Freedom Day Dundee 2009 about Open Source Games
|
||||
tags: Dundee,gaming,game development,game engine,game development community
|
||||
|
||||
show: 582
|
||||
summary: deepgeek interviews Ken Fallon about the future of HPR
|
||||
tags: podcast,community,Today with a Techie,podfade
|
||||
|
||||
show: 583
|
||||
summary: Alan Hicks at SouthEast LinuxFest 2010, the second annual festival
|
||||
#tags: SELF, interview
|
||||
|
||||
show: 588
|
||||
summary: Brian Smith at SouthEast LinuxFest 2010, the second annual festival
|
||||
#tags: SELF, interview
|
||||
|
8
Database/tsu/tag_summary_updates_0590-0599.txt
Normal file
8
Database/tsu/tag_summary_updates_0590-0599.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
show: 593
|
||||
summary: A show from a new host about using Linux at home and at work
|
||||
tags: Unix,Mandrake,Ubuntu,Mint,GIMP,OpenOffice
|
||||
|
||||
show: 595
|
||||
summary: The first episode of the Read 'n Code podcast from a new host
|
||||
tags: Seneca,Stoicism,Python,Zen
|
||||
|
11
Database/tsu/tag_summary_updates_0600-0609.txt
Normal file
11
Database/tsu/tag_summary_updates_0600-0609.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
show: 607
|
||||
summary: Rebecca is in sales at bueda.com
|
||||
tags: semantic web,social network,hipster boxing,privacy
|
||||
|
||||
show: 608
|
||||
summary: Sampling Linux distributions and learning the command line
|
||||
tags: DSL,Ubuntu,Mandrake,CentOS,Mint,command line
|
||||
|
||||
show: 609
|
||||
summary: A new host speaks of his first inklings of a Windows-free world
|
||||
tags: Ubuntu,IBM mainframe,Tom Merritt
|
8
Database/tsu/tag_summary_updates_0610-0619.txt
Normal file
8
Database/tsu/tag_summary_updates_0610-0619.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
show: 613
|
||||
summary: Some reflections on investing and saving
|
||||
tags: investment,E-Trade,microfinance,MicroPlace,share,hard asset
|
||||
|
||||
show: 614
|
||||
summary: We look at what sound is, and how we represent it digitally
|
||||
tags: frequency,amplitude,bit rate, bit depth,Ardour,codec
|
||||
|
20
Database/tsu/tag_summary_updates_0620-0629.txt
Normal file
20
Database/tsu/tag_summary_updates_0620-0629.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
show: 623
|
||||
summary: GNU nano is a simple editor, inspired by Pico
|
||||
tags: editor,command line,Pine,Pico
|
||||
|
||||
show: 624
|
||||
summary: Episode 3: personal hygiene
|
||||
tags: stairwell,yurt,shower,gym membership,urban camping,
|
||||
|
||||
show: 626
|
||||
summary: Episode 4: organizing your gear
|
||||
tags: backpack,locker,ziplock bag,travel mug,urban camping
|
||||
|
||||
show: 627
|
||||
summary: Ruji's journey to Linux
|
||||
tags: Mac OS X,Windows XP,freeware,shareware,FOSS,Mandriva,Ubuntu,Sabayon
|
||||
|
||||
show: 628
|
||||
summary: brother mouse speaks of Tasker in his first show for HPR
|
||||
tags: Tasker,Android,automation
|
||||
|
12
Database/tsu/tag_summary_updates_0630-0639.txt
Normal file
12
Database/tsu/tag_summary_updates_0630-0639.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
show: 634
|
||||
summary: Episode 5: where to find food
|
||||
tags: free food,dumpster diving,stealing,bartering
|
||||
|
||||
show: 635
|
||||
summary: A new contributor talks about the downside of external cloud solutions
|
||||
tags: del.icio.us,atlassian.com,insipid
|
||||
|
||||
show: 638
|
||||
summary: Episode 6: making money
|
||||
tags: part-time job,odd job,business card,freelance,street performing
|
||||
|
12
Database/tsu/tag_summary_updates_0640-0649.txt
Normal file
12
Database/tsu/tag_summary_updates_0640-0649.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
show: 641
|
||||
summary: Episode 7: what to do all day!
|
||||
tags: job,cafe,library,university,parks,friends,streets,community establishment
|
||||
|
||||
show: 645
|
||||
summary: Curbuntu interviews Baylee Juran, a public-school teacher
|
||||
tags: education,Second Life,HTML,CSS
|
||||
|
||||
show: 646
|
||||
summary: Using Android without a phone or data plan, just WiFi
|
||||
tags: unlocked phone,SIP,Session Initiation Protocol,GPS
|
||||
|
20
Database/tsu/tag_summary_updates_0660-0669.txt
Normal file
20
Database/tsu/tag_summary_updates_0660-0669.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
show: 661
|
||||
summary: Finding open wireless networks in a neighbourhood using a smartphone
|
||||
tags: Wardriving,Warwalking,Warchalking,GPS
|
||||
|
||||
show: 662
|
||||
summary: Xoke records an HPR episode while installing DD-WRT
|
||||
#tags: WiFi,Wireless,DDWRT,Hardware
|
||||
|
||||
show: 663
|
||||
summary: Podcast listening, and some suggestions of what to listen to
|
||||
tags: Sansa E200,Rockbox,iPod Mini,hpodder,Android,Cyanogenmod
|
||||
|
||||
show: 668
|
||||
summary: Kurt Vonnegut's novel Slaughterhouse-five considered with the Erlang language
|
||||
tags: Kurt Vonnegut,postmodernism,Erlang,concurrency,fault tolerance
|
||||
|
||||
show: 669
|
||||
summary: The audio recording of KFive's talk at Ohio Linux Fest 2010
|
||||
tags: Open Source
|
||||
|
15
Database/tsu/tag_summary_updates_0670-0679.txt
Normal file
15
Database/tsu/tag_summary_updates_0670-0679.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
show: 673
|
||||
summary: droops voices regrets over the lack of imagination in the placement of some caches
|
||||
tags: geocaching,gps
|
||||
|
||||
show: 334
|
||||
summary: Theater of the Imagination - part 1
|
||||
|
||||
show: 677
|
||||
summary: Some descriptions of the base concepts
|
||||
tags: audiocast,broadcast,audio drama,RSS
|
||||
|
||||
show: 679
|
||||
summary: An interview with Christian Tismer after PyCon 2010
|
||||
tags: Pycon,Stackless,psyco,PyPy,Unladen Swallow
|
||||
|
8
Database/tsu/tag_summary_updates_0680-0689.txt
Normal file
8
Database/tsu/tag_summary_updates_0680-0689.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
show: 680
|
||||
summary: Broam talks of Auctions yard sales and flea markets
|
||||
tags: auction,yard sale,flea market
|
||||
|
||||
show: 688
|
||||
summary: The first HPR Audio Book Club show: Badge Of Infamy written by Lester Del Rey
|
||||
tags: HPR AudioBookClub,Badge Of Infamy
|
||||
|
8
Database/tsu/tag_summary_updates_0690-0699.txt
Normal file
8
Database/tsu/tag_summary_updates_0690-0699.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
show: 691
|
||||
summary: MrGadgets talks more about early computers (before the PC)
|
||||
tags: s-100 bus,CP/M,Ohio Scientific Inc.,Motorola 6800,Commodore VIC-20
|
||||
|
||||
show: 696
|
||||
summary: Mr Gadgets talks about how he got to Linux
|
||||
tags: 6502,Z80,6800A,Moog Synthesizer,Commodore PET,TRS-80 Color Computer,KIM-1
|
||||
|
32
Database/tsu/tag_summary_updates_0700-0739.txt
Normal file
32
Database/tsu/tag_summary_updates_0700-0739.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 705
|
||||
summary: A new host's first show - about Linux experiences
|
||||
tags: RedHat,Arch Linux,Linux Fest North West
|
||||
|
||||
show: 706
|
||||
summary: Some advice for sighted people when encountering a blind person
|
||||
tags: accessibility
|
||||
|
||||
show: 708
|
||||
summary: A view of Linux in the Enterprise
|
||||
tags: SAP,Enterprise Resource Planning,ERP,Oracle Enterprise Linux,Novell
|
||||
|
||||
show: 719
|
||||
summary: What language reveals about you; linguistics; dead languages
|
||||
tags: language,neologism,accent,spelling,linguistics,dialect
|
||||
|
||||
show: 721
|
||||
summary: lostnbronx describes Audio Drama and how to make your own
|
||||
tags: audio drama,script
|
||||
|
||||
show: 727
|
||||
summary: A description of how to get the control you should already have over an electronic device you own
|
||||
tags: Motorola cliq,cyanogenmod,root access
|
||||
|
||||
show: 728
|
||||
summary: Two HPR hosts, Dismal Science and Sunzofman1, discuss equality in computing culture
|
||||
tags: privilege,minorities,wage discrimination
|
||||
|
||||
show: 734
|
||||
summary: Syndicated Thursday Presents: The Language Frontier Episode 4.5
|
||||
tags: language,obscurity
|
||||
|
32
Database/tsu/tag_summary_updates_0740-0749.txt
Normal file
32
Database/tsu/tag_summary_updates_0740-0749.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 740
|
||||
summary: Distributed Denial of Service attacks
|
||||
tags: DDoS,Distributed Denial of Service,botnet,LOIC,Low Orbit Ion Cannon
|
||||
|
||||
show: 742
|
||||
summary: Ken Fallon interviews Dave over Mumble about his use of Linux
|
||||
tags: Interview,PC repair,Linux Mint,Mumble
|
||||
|
||||
show: 744
|
||||
summary: Skirlet's penultimate show in the series
|
||||
tags: language,movie subtitle,Esperanto
|
||||
|
||||
show: 745
|
||||
summary: MrGadgets speaks of lessons learned with a product called Wingz
|
||||
tags: COMDEX,Innovative Software,Wingz,spreadsheet,Informix
|
||||
|
||||
show: 746
|
||||
summary: Ken Fallon interviews Tony Whitmore of the Ubuntu-UK Podcast about OggCamp11
|
||||
#tags: oggcamp,oggcamp11
|
||||
|
||||
show: 747
|
||||
summary: A discussion between two HPR hosts, one in Dundee and the other in Vancouver
|
||||
tags: Botnet,DNS Tunneling
|
||||
|
||||
show: 748
|
||||
#summary: Today I share with you my list of favorite audiocasts w/ratings and reviews
|
||||
tags: podcast,audiocast
|
||||
|
||||
show: 749
|
||||
summary: The process of preparing the Full Circle Podcast: preparation
|
||||
#tags: Full Circle Podcast
|
||||
|
36
Database/tsu/tag_summary_updates_0750-0759.txt
Normal file
36
Database/tsu/tag_summary_updates_0750-0759.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
show: 750
|
||||
summary: In his first episode NewAgeTechnoHippie describes his Linux journey
|
||||
tags: Redhat Linux, LILO, Fedora, Arch Linux, Maemo, Nokia N900
|
||||
|
||||
show: 751
|
||||
summary: A submission to HPR by Lord Drachenblut and Downer
|
||||
tags: Anonymous, LulzSec, Cloudflare
|
||||
|
||||
show: 752
|
||||
summary: Another part of Mr Gadgets' journey to daily use of Linux
|
||||
tags: Knoppix, live CD, microdrive, compact flash
|
||||
|
||||
show: 753
|
||||
#summary: KFive interviews klaatu of Slackermedia
|
||||
tags: Indiana Linux Fest, ILF, Slackware, multimedia studio
|
||||
|
||||
show: 754
|
||||
summary: The last episode in the series
|
||||
tags: Noam Chomsky,Esperanto
|
||||
|
||||
show: 755
|
||||
summary: Bariman talks about changes to his Linux audio setup
|
||||
tags: Jack Audio Driver,limits.conf, audio mixer, Ardour, Audacity, Kid3Tag, MMA, Muse, Lilypond
|
||||
|
||||
show: 756
|
||||
summary: An introduction to Radio Frequencies
|
||||
tags: electromagnetic radiation, packet radio
|
||||
|
||||
show: 758
|
||||
#summary: Ken interviews Jon Spriggs of CCHits.net
|
||||
tags: CCHits.net, music, Creative Commons, PHP
|
||||
|
||||
show: 759
|
||||
#summary: A weekly meeting to revise for the LPI exams
|
||||
tags: LPI exam,www.linuxbasix.com,PS1,export
|
||||
|
32
Database/tsu/tag_summary_updates_0760-0769.txt
Normal file
32
Database/tsu/tag_summary_updates_0760-0769.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 760
|
||||
summary: Klaatu interviews /dev/Rob0 at the South East Linux Fest 2011
|
||||
#tags: SELF, interview
|
||||
|
||||
show: 762
|
||||
summary: lostnbronx speaks about dramatic audio
|
||||
tags: condenser microphone,XLR,phantom power,pop filter,audio drama
|
||||
|
||||
show: 763
|
||||
summary: MrGadgets talks about what are in his opinion some very terrible movies
|
||||
tags: movies,bad movies
|
||||
|
||||
show: 764
|
||||
summary: Granola is software that improves the energy efficiency of your PC or laptop
|
||||
#tags: Full Circle Podcast
|
||||
|
||||
show: 765
|
||||
summary: Klaatu interviews Dave S. Yates and Jeremy Sands at the South East Linux Fest 2011
|
||||
#tags: SELF 2011, interview
|
||||
|
||||
show: 766
|
||||
summary: A personal account of technological history
|
||||
tags: OS/2,IBM PS/2,Microchannel,SCSI,Windows 98,Knoppix
|
||||
|
||||
show: 767
|
||||
summary: Klaatu interviews John "Maddog" Hall at the South East Linux Fest 2011
|
||||
#tags: SELF 2011, interview
|
||||
|
||||
show: 768
|
||||
summary: Ken describes the use of the GNU 'sort' command
|
||||
tags: GNU sort
|
||||
|
20
Database/tsu/tag_summary_updates_0770-0779.txt
Normal file
20
Database/tsu/tag_summary_updates_0770-0779.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
#show: 772
|
||||
#summary: Adding volume control to children's electronic toys
|
||||
#tags: electronics, resistor, capacitor, soldering
|
||||
#
|
||||
#show: 773
|
||||
#summary: Interview with Gabriel Weinberg of DuckDuckGo
|
||||
#tags: search engine,privacy
|
||||
#
|
||||
#show: 774
|
||||
#summary: The process of preparing the Full Circle Podcast: recording
|
||||
#tags: Behringer microphone,pop filter,Skype,Skype call recorder
|
||||
|
||||
show: 776
|
||||
summary: Open Source and Hackable Hardware
|
||||
tags: Linksys NSLU2,Radio Shack,Heathkit,Microcentre,soldering
|
||||
|
||||
show: 777
|
||||
summary: A look at aspects of the services jointly referred to as "The Cloud"
|
||||
tags: hypervisor,virtual environment,container,security,network storage,SAS,PAAS
|
||||
|
12
Database/tsu/tag_summary_updates_0780-0789.txt
Normal file
12
Database/tsu/tag_summary_updates_0780-0789.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
show: 780
|
||||
summary: klaatu interviews Jason DeRose about a Kickstarter campaign for NovaCut, a video editor
|
||||
tags: Kickstarter,NovaCut,video editor
|
||||
|
||||
show: 782
|
||||
summary: MrGadgets discusses Open Source Software versus closed and proprietary options
|
||||
tags: Open Source Software,walled garden,user experience
|
||||
|
||||
show: 786
|
||||
summary: Designing a system to live stream video from multiple cameras at a sporting event
|
||||
tags: video,live stream,camera
|
||||
|
32
Database/tsu/tag_summary_updates_0790-0799.txt
Normal file
32
Database/tsu/tag_summary_updates_0790-0799.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 791
|
||||
summary: Ken talks to Moose, one of the organizers of Ohio LinuxFest
|
||||
tags: Ohio LinuxFest, OLF 2011
|
||||
|
||||
show: 792
|
||||
summary: A response to episode 785 by Quvmoh on binaural recording
|
||||
tags: audio,binaural recording,matrix microphone
|
||||
|
||||
show: 793
|
||||
summary: Part 6 of Klaatu's Networking Basics miniseries
|
||||
tags: networking, server, client, DHCP, router
|
||||
|
||||
show: 794
|
||||
summary: A report from the U-Cubed unconference at Mad-Lab, Manchester, UK
|
||||
#tags: Full Circle Podcast
|
||||
|
||||
show: 795
|
||||
summary: An interview with John Uren at OggCamp 2011
|
||||
tags: Civil Service, open-source software, EtherPad
|
||||
|
||||
show: 796
|
||||
summary: An interview with Shane Marks from the Nexus maker space in Cork, Ireland
|
||||
tags: hackerspace,Ireland
|
||||
|
||||
show: 797
|
||||
summary: Mike Hingley's first HPR episode where he describes how he got into Linux
|
||||
tags: Ubuntu
|
||||
|
||||
show: 799
|
||||
summary: Details of how the Full Circle Podcast is prepared, part four
|
||||
#tags: Full Circle Podcast
|
||||
|
36
Database/tsu/tag_summary_updates_0800-0809.txt
Normal file
36
Database/tsu/tag_summary_updates_0800-0809.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
#show: 800
|
||||
#summary: The danger of a open source monoculture in the mobile OS space
|
||||
#tags: webOS,Android,IOS,Windows Phone 7,Nokia N900
|
||||
#
|
||||
#show: 801
|
||||
#summary: Part 1 of the series on packaging applications
|
||||
#tags: package,packaging,yesplz,Slackware,SlackBuild
|
||||
#
|
||||
#show: 802
|
||||
#summary: An interview at OggCamp 11 with Ana Nelson about Dexy, a software documentation tool
|
||||
#tags: Dexy,language-specific documentation
|
||||
|
||||
show: 803
|
||||
summary: The NovaCut video editor was a Kickstarter project in 2011
|
||||
tags: NovaCut,video,editor,Kickstarter
|
||||
|
||||
show: 804
|
||||
summary: Ken Fallon interviews Wayne Myers at OggCamp 11
|
||||
#tags: oggcamp,oggcamp11
|
||||
|
||||
show: 805
|
||||
summary: Mr Gadgets phones in to talk about the Monster Cable company
|
||||
tags: HiFi,audiophile,decibel,skin effect
|
||||
|
||||
show: 807
|
||||
summary: Part 6 of the Networking series: How to set up a simple DNS server
|
||||
tags: networking,DNS,Domain Name System,MaraDNS
|
||||
|
||||
show: 808
|
||||
summary: Interview at SELF about a PC recycling project with the Carolina Free PC Foundation
|
||||
tags: SELF,Carolina Free PC Foundation,Upstate Carolina Linux User Group
|
||||
|
||||
show: 809
|
||||
summary: DeepGeek explains Segmented Downloading
|
||||
tags: Segmented Downloading
|
||||
|
40
Database/tsu/tag_summary_updates_0810-0819.txt
Normal file
40
Database/tsu/tag_summary_updates_0810-0819.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
show: 810
|
||||
summary: An introduction from a new host
|
||||
tags: Knoppix,Slackware,Fedora,Debian,Ubuntu,Arch Linux
|
||||
|
||||
show: 811
|
||||
summary: Plans to create a Drupal CMS with a Creative Commons torrent tracker
|
||||
tags: torrent,torrent seeder,creative commons,Drupal,EFF,GNU
|
||||
|
||||
show: 812
|
||||
summary: Useful tips on how to determine if someone is a patent troll or not
|
||||
tags: patent,patent troll
|
||||
|
||||
show: 813
|
||||
summary: Ken talks to Gemma Cameron aka @ruby_gem about Barcamp Blackpool
|
||||
tags: Barcamp Blackpool,unconference
|
||||
|
||||
show: 814
|
||||
#summary: A visit to the Knightcast podcast
|
||||
tags: Amahi
|
||||
|
||||
show: 815
|
||||
summary: An event to celebrate and promote the use of free and open source software
|
||||
tags: Tayside Linux User Group,Dundee
|
||||
|
||||
show: 816
|
||||
#summary: Modern Survivalism - part 1 of 2
|
||||
tags: recipe
|
||||
|
||||
show: 817
|
||||
summary: Script to install Windows to an external USB hard drive
|
||||
tags: USB hard disk,Windows 7,swapfile,pagefile
|
||||
|
||||
show: 818
|
||||
summary: Using a Sansa player as a podcast recorder
|
||||
tags: Sansa Clip+,recording,RockBox
|
||||
|
||||
show: 819
|
||||
summary: The process of editing the Full Circle podcast audio, part 5
|
||||
#tags: Full Circle Podcast
|
||||
|
36
Database/tsu/tag_summary_updates_0820-0829.txt
Normal file
36
Database/tsu/tag_summary_updates_0820-0829.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
show: 820
|
||||
summary: Part 9 - configuring a web server and a MySQL server
|
||||
tags: DHCP,DNS,web server,MySQL server,apache,nginx,lighttpd,virtual host
|
||||
|
||||
show: 821
|
||||
summary: Mr. Gadgets discusses his experiences with various Android tablets
|
||||
tags: resistive screen,Android apps,Google marketplace,Amazon marketplace
|
||||
|
||||
show: 822
|
||||
summary: The GiffGaff Community Phone project
|
||||
tags: giffgaff,community phone project
|
||||
|
||||
show: 823
|
||||
summary: Phonon's Gstreamer backend
|
||||
tags: KDE,Phonon,Gstreamer,Amarok,Dragon Player
|
||||
|
||||
show: 824
|
||||
summary: Fossbox at OpenTech 2011
|
||||
#tags: Full Circle Podcast
|
||||
|
||||
show: 825
|
||||
summary: Interview with Jamey Sharp at XDC 2011
|
||||
tags: X.Org Developer Conference,XDC,XCB
|
||||
|
||||
show: 827
|
||||
summary: HPR at OLF
|
||||
tags: Ohio Linux Fest,OLF,lock picking,HostGator.com
|
||||
|
||||
show: 828
|
||||
summary: The demise of physical retail stores
|
||||
tags: online store,physical store,book reader
|
||||
|
||||
show: 829
|
||||
summary: Prof Jocelyn Bell-Burnell, discoverer of pulsars, on the Jodcast podcast
|
||||
tags: astronomy,quasar,pulsar,radio telescope,neutron star,transient pulsar
|
||||
|
40
Database/tsu/tag_summary_updates_0830-0839.txt
Normal file
40
Database/tsu/tag_summary_updates_0830-0839.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
show: 830
|
||||
summary: Interview with Peter Hutterer at XDC 2011
|
||||
tags: X.org,xinput,multitouch
|
||||
|
||||
show: 831
|
||||
summary: Klaatu interviews Chris from Sourceforge.net at OLF
|
||||
tags: OLF,Sourceforge,Freshmeat,Slashdot,Geeknet
|
||||
|
||||
show: 832
|
||||
summary: A round-up of OggCamp 11
|
||||
#tags: oggcamp,oggcamp11
|
||||
|
||||
show: 833
|
||||
summary: Interview with Ian Romanick at XDC 2011
|
||||
tags: X.org,Mesa,OpenGL
|
||||
|
||||
show: 834
|
||||
#summary: A visit to the Knightcast podcast
|
||||
tags: KWTV Live, OSX Lion, Windows 8
|
||||
|
||||
show: 835
|
||||
summary: Speculating about the Amazon Fire tablet
|
||||
tags: Kindle Fire,Amazon Fire,camera
|
||||
|
||||
show: 836
|
||||
summary: Klaatu interviews Jeff from No Machine
|
||||
tags: OLF,No Machine,VNC,Citrix,ssh
|
||||
|
||||
show: 837
|
||||
summary: Ken talks to Juergen Schinker about the OWN Open wireless network
|
||||
tags: oggcamp,oggcamp11
|
||||
|
||||
show: 838
|
||||
summary: Interview with Martin Peres at XDC 2011
|
||||
tags: X.org,nVidia card,nouveau driver,nvacounter,Arduino,arduide
|
||||
|
||||
show: 839
|
||||
summary: An interview with the team behind Lubuntu
|
||||
#tags: Full Circle Podcast
|
||||
|
36
Database/tsu/tag_summary_updates_0840-0849.txt
Normal file
36
Database/tsu/tag_summary_updates_0840-0849.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
#show: 840
|
||||
#summary: Some advice about shopping for an Android device
|
||||
#tags: android,cyanogenmod
|
||||
|
||||
#show: 841
|
||||
#summary: Klaatu interviews Jonathan Nadeau
|
||||
#tags: Free Software Foundation, FSF, Trisquel Linux, accessibility
|
||||
|
||||
#show: 842
|
||||
#summary: An interview from OggCamp
|
||||
#tags: The H, opensource, security
|
||||
|
||||
#show: 843
|
||||
#summary: Shopping for technical items in November and December
|
||||
#tags: Thanksgiving,Black Friday,microSD
|
||||
|
||||
#show: 844
|
||||
#summary: An impromptu podcast from Barcamp Blackpool
|
||||
#tags: Blackpool, Barcamp
|
||||
|
||||
show: 845
|
||||
summary: AukonDK leads us on a tour of Open Source Radio Software
|
||||
tags: icecast,BUTT,Broadcast Using This Tool,IDJC,Internet DJ Console,Liquidsoap,Airtime,Rivendell,RAAbuntu
|
||||
|
||||
show: 846
|
||||
summary: An interview with Jared Smith, the project manager of Fedora Linux
|
||||
tags: Ohio Linux Fest,OLF,Fedora Project
|
||||
|
||||
show: 847
|
||||
summary: Interviews from FSCONS relating to MeeGo and Tizen
|
||||
#tags: FSCONS
|
||||
|
||||
show: 848
|
||||
summary: An interview with Alan Cocks at Oggcamp 11
|
||||
tags: Info Point Project,Bracknell Computer Fair
|
||||
|
36
Database/tsu/tag_summary_updates_0850-0859.txt
Normal file
36
Database/tsu/tag_summary_updates_0850-0859.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
show: 850
|
||||
summary: Remembering some pioneering greats in the tech field
|
||||
tags: Edgar Villchur, Henry Kloss, Acoustic Research
|
||||
|
||||
show: 852
|
||||
summary: Part 1 of a mini series on GNU Emacs
|
||||
tags: GNU Emacs,text editor,Emacs Lisp
|
||||
|
||||
show: 853
|
||||
summary: Pat Volkerding of Slackware Linux at the SELF afterparty
|
||||
tags: Slackware
|
||||
|
||||
show: 854
|
||||
summary: Syndicated Thursday welcomes All In IT Radio
|
||||
tags: Intelligent cars,Nokia,Meego
|
||||
|
||||
show: 855
|
||||
summary: The advantages of packaging content for your distro
|
||||
tags: OggCamp,Ubuntu,PPA
|
||||
|
||||
show: 856
|
||||
summary: Part 2 of a mini series on GNU Emacs
|
||||
tags: GNU Emacs,.emacs,buffer,frame
|
||||
|
||||
show: 857
|
||||
summary: An interview wth Sam Tuke the British Team Coordinator and Editorial Team co-ordinator for the FSFE
|
||||
tags: OggCamp,Free Software Foundation Europe,FSFE,FSF
|
||||
|
||||
show: 858
|
||||
summary: Children's access to science in the 1960s
|
||||
tags: Chemistry,The Golden Book of Chemistry Experiments
|
||||
|
||||
show: 859
|
||||
summary: Syndicated Thursday welcomes Sourcetrunk
|
||||
tags: OwnCloud
|
||||
|
35
Database/tsu/tag_summary_updates_0860-0869.txt
Normal file
35
Database/tsu/tag_summary_updates_0860-0869.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
show: 860
|
||||
summary: Two interviews from FSCONS 2010
|
||||
#tags: FSCONS
|
||||
|
||||
show: 861
|
||||
summary: Third and final episode of a mini series on GNU Emacs
|
||||
tags: GNU Emacs,buffer,minibuffer,frame,window
|
||||
|
||||
show: 862
|
||||
summary: TFTP, what it's good for and what makes it tick
|
||||
tags: Trivial File Transfer Protocol,TFTP,Preboot Execution Environment,PXE
|
||||
|
||||
show: 863
|
||||
#summary: In today's show Ken talks to Tony Hughes about how he got into Linux
|
||||
tags: Free Cycle,Xubuntu,Ucubed,Software Freedom Day,BLACKPOOL LUG
|
||||
|
||||
show: 864
|
||||
summary: Opentech Conference in London, interview with Glen Mehn of Social Innovation Camp
|
||||
#tags: Full Circle Podcast
|
||||
|
||||
show: 865
|
||||
summary: The history of Desktop Transparency
|
||||
tags: free software,rxvt,background image,transparent background,Enlightenment,translucent window,Berlin
|
||||
|
||||
show: 866
|
||||
summary: Publican is a tool for publishing material authored in DocBook XML
|
||||
tags: Perl,XML schema,DocBook,GNU Emacs,Vim
|
||||
|
||||
show: 867
|
||||
summary: Encouraging young people to get interested in technology
|
||||
tags: Radio Shack,Tandy Co.,arduino,AdaFruit
|
||||
|
||||
show: 868
|
||||
summary: As a keen nano user, JWP tries out EMACS on his NSLU2 "SLUG"
|
||||
tags: EMACS,nano,Pine,Linksys NSLU2
|
32
Database/tsu/tag_summary_updates_0870-0879.txt
Normal file
32
Database/tsu/tag_summary_updates_0870-0879.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 870
|
||||
summary: Deltaray looks back at his early computer experiences
|
||||
tags: Apple Lisa, TRS-80, Commodore 128, Amiga 2000
|
||||
|
||||
show: 872
|
||||
summary: The YUM package manager
|
||||
tags: package manager, yum
|
||||
|
||||
show: 873
|
||||
summary: CrunchBang Linux
|
||||
tags: CrunchBang Linux, Debian, Openbox
|
||||
|
||||
show: 874
|
||||
summary: Full Circle Podcast, interview
|
||||
#tags: Full Circle Podcast
|
||||
|
||||
show: 875
|
||||
summary: Replacing old AMD systems
|
||||
tags: Intel Pentium 4, HDMI
|
||||
|
||||
show: 876
|
||||
summary: FreeBSD Ports collection is a package management system
|
||||
tags: package management,BSD, FreeBSD, Ports
|
||||
|
||||
show: 878
|
||||
summary: Computer-controlled manufacturing - 3D printers, CNC
|
||||
tags: 3D printer,RepRap,CandyFab,MakerBot,CNC
|
||||
|
||||
show: 879
|
||||
summary: SMLR episode 9
|
||||
tags: SMLR,Sunday Morning Linux Review
|
||||
|
40
Database/tsu/tag_summary_updates_0880-0889.txt
Normal file
40
Database/tsu/tag_summary_updates_0880-0889.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
show: 880
|
||||
summary: A discussion of "Handbook for the Criminally Insane" by Brian Holtz
|
||||
tags: audiobook,HPR AudioBookClub
|
||||
|
||||
show: 881
|
||||
summary: Details of this ultra-low-voltage processor
|
||||
tags: Intel, Intel Atom
|
||||
|
||||
show: 882
|
||||
summary: The RPM package format
|
||||
tags: package management, RPM, rpmbuild
|
||||
|
||||
show: 883
|
||||
summary: An interview from OggCamp 2011
|
||||
tags: OggCamp,interview
|
||||
|
||||
show: 884
|
||||
summary: A visit to the Knightwise podcast: the Knightcast
|
||||
tags: streaming media, DLNA, Amahi
|
||||
|
||||
show: 885
|
||||
summary: A free, open source, backup and recovery tool
|
||||
tags: backup,restore,recovery
|
||||
|
||||
show: 886
|
||||
summary: An discussion with Alister Munroe from OggCamp 2011
|
||||
tags: product lifecycle management,PLM
|
||||
|
||||
show: 887
|
||||
summary: NYbill and Windigo and their Linux stories
|
||||
tags: Yggdrasil
|
||||
|
||||
show: 888
|
||||
summary: Where to get help on using EMACS
|
||||
tags: EMACS
|
||||
|
||||
show: 889
|
||||
summary: Preparing for the upcoming New Year's Eve show
|
||||
#tags: New Year,2012
|
||||
|
36
Database/tsu/tag_summary_updates_0890-0899.txt
Normal file
36
Database/tsu/tag_summary_updates_0890-0899.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
show: 890
|
||||
summary: What happened to the promise of science and technology?
|
||||
tags: home automation
|
||||
|
||||
show: 891
|
||||
summary: HPR New Year 2012 #1
|
||||
#tags: New Year,2012
|
||||
|
||||
show: 892
|
||||
summary: HPR New Year 2012 #2
|
||||
#tags: New Year,2012
|
||||
|
||||
show: 893
|
||||
summary: HPR New Year 2012 #3
|
||||
#tags: New Year,2012
|
||||
|
||||
show: 894
|
||||
summary: HPR New Year 2012 #4
|
||||
#tags: New Year,2012
|
||||
|
||||
show: 895
|
||||
summary: HPR New Year 2012 #5
|
||||
#tags: New Year,2012
|
||||
|
||||
show: 896
|
||||
summary: HPR New Year 2012 #6
|
||||
#tags: New Year,2012
|
||||
|
||||
show: 897
|
||||
summary: HPR New Year 2012 #7
|
||||
#tags: New Year,2012
|
||||
|
||||
show: 898
|
||||
summary: HPR New Year 2012 #8
|
||||
#tags: New Year,2012
|
||||
|
44
Database/tsu/tag_summary_updates_0900-0910.txt
Normal file
44
Database/tsu/tag_summary_updates_0900-0910.txt
Normal file
@@ -0,0 +1,44 @@
|
||||
show: 900
|
||||
summary: A first podcast and introduction
|
||||
tags: Commodore,BASIC,ZX Spectrum 48k,assembler,Pascal,C,C++,Debian,Red Hat
|
||||
|
||||
show: 901
|
||||
#summary: An introduction from a new host
|
||||
tags: mainframe,punched card,teletype,Sinclair ZX80,PC XT,DOS,Linux,KDE
|
||||
|
||||
show: 902
|
||||
summary: A newscast from Talk Geek to Me
|
||||
tags: newscast,TGTM
|
||||
|
||||
show: 903
|
||||
summary: The HPR domain is redirected to http://www.nosopa.org/ in protest
|
||||
tags: SOPA,Stop Online Piracy Act,protest
|
||||
|
||||
show: 904
|
||||
summary: A show from Frostcast about NELF
|
||||
tags: Northeast GNU/Linux Fest,NELF 2012,Worcester State University
|
||||
|
||||
show: 905
|
||||
summary: An introductory show from a new host
|
||||
tags: Smoothwall,Puppy Linux,Ubuntu,Arch,Debian,Bash,Bashpodder
|
||||
|
||||
show: 906
|
||||
summary: Ken interviews Pascal Bleser, FOSDEM organisation team member
|
||||
tags: Interview,FOSDEM,FOSDEM 2012
|
||||
|
||||
show: 907
|
||||
summary: Using web resources to become an autodidact
|
||||
tags: espeak,text to speech,free course
|
||||
|
||||
show: 908
|
||||
summary: TED - Torrent Episode Downloader
|
||||
tags: DVR,bittorrent,SAMBA
|
||||
|
||||
show: 909
|
||||
summary: Managing your information feeds
|
||||
tags: RSS,Google Reader,Instapaper
|
||||
|
||||
show: 910
|
||||
summary: Software that gives your localhost servers names and makes them globally visible
|
||||
tags: reverse proxy
|
||||
|
24
Database/tsu/tag_summary_updates_0911-0920.txt
Normal file
24
Database/tsu/tag_summary_updates_0911-0920.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
show: 911
|
||||
summary: Mr X talks about hobbies he's had over the years
|
||||
tags: playing music,electronics,amateur radio,computing
|
||||
|
||||
show: 912
|
||||
summary: A new host speaks about cable cutting
|
||||
tags: Cable TV,internet streaming,Roku,
|
||||
|
||||
show: 913
|
||||
summary: An introduction to the data formats available when talking to web services
|
||||
tags: open schedule,exchanging data,XML,DTD,XML schema,JSON,JSONP
|
||||
|
||||
show: 914
|
||||
summary: SMLR episode 14
|
||||
tags: SMLR,Sunday Morning Linux Review
|
||||
|
||||
show: 917
|
||||
#summary: Uber Leet Hacker Force Radio issue 6
|
||||
tags: concr,yesplz
|
||||
|
||||
show: 919
|
||||
summary: A 200 kilometre skating tour in the province of Friesland in the north of the Netherlands
|
||||
tags: Interview,Netherlands,Friesland,skating
|
||||
|
16
Database/tsu/tag_summary_updates_0921-0930.txt
Normal file
16
Database/tsu/tag_summary_updates_0921-0930.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
show: 922
|
||||
summary: Installing a 2012 North American map on a Garmin
|
||||
tags: garmin,gps,download
|
||||
|
||||
show: 923
|
||||
summary: Jezra and NYbill discuss various topics
|
||||
tags: Ubuntu,Unity,NELF,SCALE,Raspberry Pi,Teensy,microcontroller,
|
||||
|
||||
show: 926
|
||||
summary: Thoughts about the evolution of high-level languages from machine language
|
||||
tags: machine code,assembly language,Grace Hopper,COBOL,debugging,C,Python,Perl,Vala
|
||||
|
||||
show: 929
|
||||
#summary: Another visit to the Knightcast podcast
|
||||
tags: email,productivity,IT technician
|
||||
|
23
Database/tsu/tag_summary_updates_0931-0940.txt
Normal file
23
Database/tsu/tag_summary_updates_0931-0940.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
show: 931
|
||||
summary: An efficient and minimalist window manager
|
||||
tags: window manager,ratpoison
|
||||
|
||||
show: 932
|
||||
summary: An introduction to programming languages
|
||||
tags: programming,Python,C,C++,Lisp,Java,Perl,PHP,Smalltalk,Haskell,FORTRAN,COBOL,Algol60,Algol68,BASIC,Pascal
|
||||
|
||||
show: 933
|
||||
summary: Part 1 of the "Freedom is not Free" series
|
||||
tags: FOSS,FLOSS,Free Software Foundation,GNU Project
|
||||
|
||||
show: 935
|
||||
summary: Ken talks to Lord Drachenblut about the upcoming Indiana LinuxFest 2012
|
||||
tags: Indiana Linuxfest,ILF,ILF 2012
|
||||
|
||||
show: 938
|
||||
summary: Some tips on configuring Windows networking and installing Skype on 64-bit Fedora 15
|
||||
tags: Windows,netsh,Fedora,Skype
|
||||
|
||||
show: 939
|
||||
summary: SMLR episode 21
|
||||
tags: SMLR,Sunday Morning Linux Review
|
24
Database/tsu/tag_summary_updates_0941-0950.txt
Normal file
24
Database/tsu/tag_summary_updates_0941-0950.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
show: 941
|
||||
summary: An introductory show from a new host
|
||||
tags: screwdriver,solder,flash drive,Windows,portable apps
|
||||
|
||||
show: 942
|
||||
summary: A small business solution based on Linux
|
||||
tags: Gateway, Infrastructure Manager, Unified Threat Manager, Office Server, Unified Communication Server
|
||||
|
||||
show: 943
|
||||
summary: Part 2 of the "Freedom is not Free" series
|
||||
tags: FOSS,FLOSS,bug reporting,bug tracking
|
||||
|
||||
show: 947
|
||||
summary: FOSS Distros and Communities
|
||||
tags: Fedora,community
|
||||
|
||||
show: 948
|
||||
summary: More about the example restaurant web application from the last episode
|
||||
tags: XML,JSON,JSONP,SOAP,WSDL,REST,Java,Spring Framework
|
||||
|
||||
show: 949
|
||||
summary: 2011 Year Overview Show from CCHits.net
|
||||
tags: CCHits.net,music,creative commons
|
||||
|
28
Database/tsu/tag_summary_updates_0951-0960.txt
Normal file
28
Database/tsu/tag_summary_updates_0951-0960.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
show: 951
|
||||
summary: A review of the Roku XD digital media player device
|
||||
tags: Roku,digital media player
|
||||
|
||||
show: 952
|
||||
summary: Part 2 of the cable cutting series
|
||||
tags: internet streaming,antenna
|
||||
|
||||
show: 954
|
||||
summary: The Chromebook and ChromeOS
|
||||
#tags: Full Circle Podcast
|
||||
|
||||
show: 955
|
||||
summary: A no-holds-barred discussion that might be a series pilot
|
||||
tags: zombies
|
||||
|
||||
show: 957
|
||||
summary: Part 3 of the "Freedom is not Free" series
|
||||
tags: FOSS,FLOSS, technical documentation, end-user documentation, translation
|
||||
|
||||
show: 958
|
||||
summary: An interview with Carl Symons and John Blanford: all things KDE
|
||||
tags: KDE, Akademy, Plasma Workspace, Krita, Calligra
|
||||
|
||||
show: 959
|
||||
#summary: Joanmarie Diggs talk from NELF about the Orca Screen Reader
|
||||
tags: Orca screen reader,NELF
|
||||
|
24
Database/tsu/tag_summary_updates_0961-0970.txt
Normal file
24
Database/tsu/tag_summary_updates_0961-0970.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
show: 961
|
||||
summary: A personal log of time spent in a mental hospital
|
||||
tags: mental health,paranoia
|
||||
|
||||
show: 963
|
||||
summary: Part 3 of the cable cutting series
|
||||
tags: internet streaming, set-top box
|
||||
|
||||
show: 966
|
||||
summary: A first episode from a new (to HPR) host
|
||||
tags: personal experiences,history
|
||||
|
||||
show: 967
|
||||
summary: A discussion of the capabilities of the Raspberry Pi
|
||||
tags: Broadcom BCM2835,HDMI,GPIO,UART,i2c,SPI
|
||||
|
||||
show: 968
|
||||
summary: Using <em>ffmpeg</em> to convert videos
|
||||
tags: video,conversion,ffmpeg,winff
|
||||
|
||||
show: 969
|
||||
summary: Crivins - episode 10
|
||||
tags: crivins, audiocast
|
||||
|
24
Database/tsu/tag_summary_updates_0971-0980.txt
Normal file
24
Database/tsu/tag_summary_updates_0971-0980.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
show: 971
|
||||
summary: Episode zero of the /dev/random podcast
|
||||
tags: podcast
|
||||
|
||||
show: 973
|
||||
summary: Part 4 of the "Freedom is not Free" series
|
||||
tags: FOSS,FLOSS, financial support
|
||||
|
||||
show: 974
|
||||
summary: A presentation from the North East Linux Fest about FreeNAS
|
||||
tags: NELF,NAS,FreeBSD,FreeNAS
|
||||
|
||||
show: 975
|
||||
summary: Do modern workstations need as many as 16 cores?
|
||||
tags: workstation,CPU,cores
|
||||
|
||||
show: 978
|
||||
summary: A discussion of the book "Dead Hunt" written by Kenn Crawford
|
||||
tags: audiobook,HPR AudioBookClub
|
||||
|
||||
show: 980
|
||||
summary: The story of a Lancashire community and their high-speed network
|
||||
tags: broadband,fibre optic,fiber optic,B4RN
|
||||
|
36
Database/tsu/tag_summary_updates_0981-0990.txt
Normal file
36
Database/tsu/tag_summary_updates_0981-0990.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
show: 981
|
||||
summary: Ahuka speaks of his experiences attending the second of these conferences
|
||||
tags: Indiana LinuxFest
|
||||
|
||||
show: 983
|
||||
summary: Part 5 of the "Freedom is not Free" series
|
||||
tags: FOSS,FLOSS,involvement
|
||||
|
||||
show: 984
|
||||
summary: A visit to the "Going Linux" podcast
|
||||
tags: podcast recording
|
||||
|
||||
show: 985
|
||||
summary: An interview with Thomas Stover at Linux Fest North West
|
||||
tags: Linux Fest North West,LFNW,C
|
||||
|
||||
show: 986
|
||||
#summary: In this episode David Whitman interviews Scott Newlon of MintCast
|
||||
tags: mintCast,Linux Mint,Linux Fest North West,LFNW
|
||||
|
||||
show: 987
|
||||
summary: An interview with Larry Cafiero at Linux Fest North West
|
||||
tags: CrunchBang,Linux Fest North West,LFNW
|
||||
|
||||
show: 988
|
||||
summary: An interview with Dawn McKenna at Linux Fest North West
|
||||
tags: sign language,interpreting,Linux Fest North West,LFNW
|
||||
|
||||
show: 989
|
||||
summary: A visit to the "Juiced Penguin" podcast
|
||||
tags: PodSafe music,Oggcast
|
||||
|
||||
show: 990
|
||||
summary: JWP talks about portable applications
|
||||
tags: CrunchBang,portableapps.com
|
||||
|
16
Database/tsu/tag_summary_updates_0991-0999.txt
Normal file
16
Database/tsu/tag_summary_updates_0991-0999.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
show: 996
|
||||
summary: A Unix command cheat sheet written by FossWire
|
||||
tags: command line,date,cal,ncal,uptime,uname,du,df
|
||||
|
||||
show: 997
|
||||
summary: Rural communications problems versus the urban equivalent
|
||||
tags: mobile computing,DSL,land line,cellphone,coverage
|
||||
|
||||
show: 998
|
||||
summary: Setting up a Status.net instance
|
||||
tags: microblogging, status.net, identi.ca
|
||||
|
||||
show: 999
|
||||
summary: The Full Circle Podcast with a recording of Simon Phipps at OggCamp 11
|
||||
tags: OggCamp
|
||||
|
27
Database/tsu/tag_summary_updates_1000-1010.txt
Normal file
27
Database/tsu/tag_summary_updates_1000-1010.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
show: 1000
|
||||
summary: Thoughts and wishes for the 1000th episode of HPR
|
||||
tags: congratulations
|
||||
|
||||
show: 1003
|
||||
summary: The recording equipment of a new contributor
|
||||
tags: microphone,mixer,sound card, Ardour
|
||||
|
||||
show: 1004
|
||||
summary: SMLR episode 34
|
||||
tags: SMLR
|
||||
|
||||
show: 1006
|
||||
summary: Recovering from Mental Health issues
|
||||
tags: schizophrenia,mental hospital
|
||||
|
||||
show: 1008
|
||||
summary: A way to correct a Minecraft bug
|
||||
tags:Minecraft,LWJGL libraries
|
||||
|
||||
show: 1009
|
||||
summary:A talk from John Sullivan of the Free Software Foundation
|
||||
tags:NELF,FSF
|
||||
|
||||
show: 1010
|
||||
summary:An account of being sued for copyright infringement
|
||||
tags:copyright,BitTorrent,subpoena
|
27
Database/tsu/tag_summary_updates_1011-1019.txt
Normal file
27
Database/tsu/tag_summary_updates_1011-1019.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
show: 1011
|
||||
summary: Interview with Robert Schweikert at NELF
|
||||
tags: Interview,NELF,Open Suse
|
||||
|
||||
show: 1013
|
||||
summary: How to make copies of programs from TiVo for long-term use
|
||||
tags: TiVo,kmttg,tivodecode,curl,mencoder,ffmpeg,Handbrake,Comskip,AtomicParsley,kdenlive
|
||||
|
||||
show: 1014
|
||||
summary: An RFA show from 2002, with historical notes from Wikipedia
|
||||
tags: Radio FreeK America,RFA,hacker,hacking,phreaking,internet radio,urban exploration,wardriving
|
||||
|
||||
show: 1016
|
||||
summary: The Nix package manager and related projects such as NixOS
|
||||
tags: NixOS,Nix,package manager,Nixpkgs,Hydra,Disnix
|
||||
|
||||
show: 1017
|
||||
summary: Attempting to restore a damaged smartphone
|
||||
tags: Samsung Admire,water damage,
|
||||
|
||||
show: 1018
|
||||
tags: Interview,Oggcamp,Freenode
|
||||
|
||||
show: 1019
|
||||
summary: Rob Reid, creator of "Rhapsody", gives a TED talk
|
||||
tags: TED,"Technology, Education and Design",Rhapsody
|
||||
|
58
Database/tsu/tag_summary_updates_1020-1039.txt
Normal file
58
Database/tsu/tag_summary_updates_1020-1039.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
show: 1023
|
||||
summary: Rivendell Radio Automation software
|
||||
tags: Rivendell,radio,automation
|
||||
|
||||
show: 1024
|
||||
summary: Celebration of Hacker Public Radio's first 1K episodes
|
||||
|
||||
show: 1025
|
||||
summary: Episode 51 of the Infonomicon podcast
|
||||
tags: Infonomicon, RFA
|
||||
|
||||
show: 1026
|
||||
summary: Episode four on setting up a WordPress blog
|
||||
tags: WordPress,MySQL,phpMyAdmin
|
||||
|
||||
show: 1027
|
||||
summary: An alternative to Google Reader for managing feeds
|
||||
tags: Google, Google Reader, OPML, IMAP, Feed2Imap, opml2feed
|
||||
|
||||
show: 1028
|
||||
summary: A discussion of what technology can be found at Goodwill
|
||||
tags: Goodwill, second-hand, IT
|
||||
|
||||
show: 1029
|
||||
summary: Karen Sandler's OggCamp talk about closed source medical devices
|
||||
tags: OggCamp, Full Circle Podcast
|
||||
|
||||
show: 1030
|
||||
summary: A talk from NELF 2012 about Trisquel
|
||||
tags: NELF 2012,Trisquel Linux
|
||||
|
||||
show: 1031
|
||||
summary: Using mencoder from the command line
|
||||
tags: mencoder,dvd
|
||||
|
||||
show: 1033
|
||||
summary: Asking for help from others versus trying to find the answer yourself
|
||||
tags: RTFM, helpfulness, self-help
|
||||
|
||||
show: 1034
|
||||
summary: Setting up an information display with a thin client and spare monitor
|
||||
tags: PXE boot, thin client, DHCP, TFTP
|
||||
|
||||
show: 1035
|
||||
summary: Fourth in a series of OggCamp 11 highlights from the Full Circle Podcast
|
||||
|
||||
show: 1036
|
||||
summary: Advice on starting with Amateur Radio
|
||||
tags: Amateur radio,Ham radio
|
||||
|
||||
show: 1037
|
||||
summary: A show about the tools needed for soldering
|
||||
tags: solder,soldering,soldering iron,soldering sponge,dry joint
|
||||
|
||||
show: 1039
|
||||
summary: A recording of a talk from the Northeast GNU/Linux Fest by Matt Lee
|
||||
tags: Libre.fm, GNU FM, Audioscrobbler
|
||||
|
51
Database/tsu/tag_summary_updates_1040-1059.txt
Normal file
51
Database/tsu/tag_summary_updates_1040-1059.txt
Normal file
@@ -0,0 +1,51 @@
|
||||
show: 1040
|
||||
summary: A discussion of the future of Steam on Linux
|
||||
tags: Steam,Linux,game,gaming
|
||||
|
||||
show: 1041
|
||||
summary: A discussion between attendees after HOPE Number Nine
|
||||
tags: Hackers On Planet Earth,H.O.P.E.,2600
|
||||
|
||||
show: 1043
|
||||
summary: Where to go to get old tech and things you should know before buying
|
||||
tags: second hand,yard sale,flea market,rummage sale,auction,thrift store,trash picking
|
||||
|
||||
show: 1044
|
||||
summary: An interview with JWP followed by the talk he gave
|
||||
tags: oggcamp,oggcamp11,Oracle Linux
|
||||
|
||||
show: 1045
|
||||
summary: A look at the process of researching a UK family tree
|
||||
tags: genealogy,Gramps,FreeBMD,census,ancestry.co.uk
|
||||
|
||||
show: 1047
|
||||
summary: More about the process of soldering
|
||||
tags: multicore solder,flux,perf board,strip board,bread board
|
||||
|
||||
show: 1048
|
||||
summary: Mr Gadgets talks about Space travel and living on other planets
|
||||
tags: space,space travel,SpaceX
|
||||
|
||||
show: 1049
|
||||
summary: Fifth in a series of OggCamp 11 highlights from the Full Circle Podcast
|
||||
tags: OggCamp,Full Circle Podcast,IRC
|
||||
|
||||
show: 1053
|
||||
summary: Thoughts on funding for Linux podcasts
|
||||
tags: Linux Outlaws,podcast,Linux,donations
|
||||
|
||||
show: 1054
|
||||
summary: An interview with journalist and author Becky Hogge
|
||||
tags: Full Circle Podcast,interview
|
||||
|
||||
show: 1056
|
||||
summary: Interviews from OggCamp 12 on day 1
|
||||
|
||||
show: 1057
|
||||
summary: A brief look at the UK Communications Data Bill
|
||||
|
||||
show: 1058
|
||||
summary: More interviews from OggCamp 12 on day 1
|
||||
|
||||
show: 1059
|
||||
summary: Yet more interviews from OggCamp 12 on day 2
|
23
Database/tsu/tag_summary_updates_1060-1069.txt
Normal file
23
Database/tsu/tag_summary_updates_1060-1069.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
show: 1063
|
||||
summary: Following an interview with Richard Stallman on the Linux Action Show
|
||||
tags: software freedom,GPL,BSD licence
|
||||
|
||||
show: 1064
|
||||
summary: Recording of the panel discussion at OggCamp11
|
||||
tags: oggcamp11,panel discussion
|
||||
|
||||
show: 1065
|
||||
summary: Using an Android phone as a wireless hotspot
|
||||
tags: android,wireless,wifi,tether
|
||||
|
||||
show: 1067
|
||||
summary: An episode of the /dev/random podcast
|
||||
tags: podcast
|
||||
|
||||
show: 1068
|
||||
summary: Thoughts on reformatting CC content for emergency redistibution
|
||||
tags: creative commons,emergency
|
||||
|
||||
show: 1069
|
||||
summary: A recording of a talk at the Central Philadelphia LUG
|
||||
tags: hardware driver,Ground-truth document,CIA,irker
|
32
Database/tsu/tag_summary_updates_1070-1079.txt
Normal file
32
Database/tsu/tag_summary_updates_1070-1079.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 1071
|
||||
summary: Consuming over the air TV rather than cable TV
|
||||
tags: cable tv,antenna,netgear digital entertainer,roku,playon server
|
||||
|
||||
show: 1073
|
||||
summary: An introduction to the concept of presentation versus content
|
||||
tags: presentation,content,css
|
||||
|
||||
show: 1074
|
||||
summary: Interview with Alan Pope
|
||||
tags: interview,oggcamp,oggcamp11
|
||||
|
||||
show: 1075
|
||||
summary: A newscast from Talk Geek to Me
|
||||
tags: newscast,TGTM
|
||||
|
||||
show: 1076
|
||||
summary: Describing the upcoming Ohio LinuxFest conference and expo
|
||||
tags: interview,OLF 2012,Ohio LinuxFest
|
||||
|
||||
show: 1077
|
||||
summary: Interview with Christina Haralanova from Canada
|
||||
tags: interview,FSCONS 2011,freedom,hacking
|
||||
|
||||
show: 1078
|
||||
summary: The European Parliament and the issue of software patentability
|
||||
tags: patent,software patent,unitary patent,Free Software
|
||||
|
||||
show: 1079
|
||||
summary: Episode 12: LMAX
|
||||
tags: Distributed Systems Podcast,LMAX Disruptor,Java,C,C++,Fedora,Red Hat
|
||||
|
27
Database/tsu/tag_summary_updates_1081-1090.txt
Normal file
27
Database/tsu/tag_summary_updates_1081-1090.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
show: 1081
|
||||
summary: Preparing photographs for posting on a website
|
||||
tags: GIMP,GNU Image Manipulation Program,photography
|
||||
|
||||
show: 1083
|
||||
summary: The concept of a compiler
|
||||
tags: compiler,neocortex,lexical analysis,preprocessing,parsing,semantic analysis,code generation,assembling,linking
|
||||
|
||||
show: 1084
|
||||
summary: A Full Circle Podcast interview with Paul Levy
|
||||
tags: Interview,Full Circle Podcast
|
||||
|
||||
show: 1085
|
||||
summary: An ambient recording of a stream
|
||||
tags: ambient sound
|
||||
|
||||
show: 1087
|
||||
summary: Two interviews from FSCONS 2011
|
||||
tags: Interview,FSCONS,FSCONS 2011,Qt
|
||||
|
||||
show: 1088
|
||||
summary: A report from Penguicon 2012 - a Linux Fest and Sci Fi convention
|
||||
tags: Penguicon 2012
|
||||
|
||||
show: 1089
|
||||
summary: A recording from the Northeast GNU/Linux Fest 2012
|
||||
tags: database,MySQL,MariaDB
|
28
Database/tsu/tag_summary_updates_1091-1099.txt
Normal file
28
Database/tsu/tag_summary_updates_1091-1099.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
show: 1092
|
||||
summary: Mr Gadgets talks about his interest and long experience in Amateur Radio
|
||||
tags: HAM radio,amateur radio,CB radio,Morse code
|
||||
|
||||
show: 1093
|
||||
summary: A continuation of the topic of presentation versus content
|
||||
tags: presentation,content,office software,semantic encoding
|
||||
|
||||
show: 1094
|
||||
summary: A recording of an impromptu podcast session
|
||||
tags: podcast,audiocast
|
||||
|
||||
show: 1096
|
||||
summary: A discussion of KeepassX, a cross platform password manager
|
||||
tags: KeepassX,password,password vault,
|
||||
|
||||
show: 1097
|
||||
summary: Exploring the intersection between trade union organising and new technology
|
||||
tags: trade union,media freedom
|
||||
|
||||
show: 1098
|
||||
summary: A personal reminiscence
|
||||
tags: BBC Micro,ZX Spectrum 48k,RAF,Crunchbang,Lincoln LUG
|
||||
|
||||
show: 1099
|
||||
summary: About the parsing stage of a compiler
|
||||
tags: compiler,parsing
|
||||
|
32
Database/tsu/tag_summary_updates_1100-1110.txt
Normal file
32
Database/tsu/tag_summary_updates_1100-1110.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
show: 1100
|
||||
tags: Android,tablet
|
||||
|
||||
show: 1101
|
||||
tags: encryption,Ubuntu,recovery
|
||||
|
||||
show: 1102
|
||||
tags: speech impediment,stuttering
|
||||
|
||||
show: 1103
|
||||
tags: media format,freedom,Opus, Ogg Vorbis, WebM, MP3, Flash Video, MP4
|
||||
|
||||
show: 1104
|
||||
tags: TuxJam, music
|
||||
|
||||
show: 1105
|
||||
tags: newscast, TGTM
|
||||
|
||||
show: 1106
|
||||
tags: linux, distributions
|
||||
|
||||
show: 1107
|
||||
tags: compiler,semantic analysis
|
||||
|
||||
show: 1108
|
||||
tags: backpack, Acer Aspire, Kindle, iPod, Rockbox
|
||||
|
||||
show: 1109
|
||||
tags: Astricon, Asterisk, VOIP
|
||||
|
||||
show: 1110
|
||||
tags: Doctor Who
|
24
Database/tsu/tag_summary_updates_1110-1120.txt
Normal file
24
Database/tsu/tag_summary_updates_1110-1120.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
show: 1113
|
||||
tags: aptsh,screen,guake
|
||||
|
||||
show: 1114
|
||||
tags: Dudmanovi podcast
|
||||
|
||||
show: 1115
|
||||
tags: newscast,TGTM
|
||||
|
||||
show: 1116
|
||||
tags: interview,Richard Stallman
|
||||
|
||||
show: 1117
|
||||
tags: sdf.org,Unix,Gopher
|
||||
|
||||
show: 1118
|
||||
tags: FLOSS, Doom, Freedoom
|
||||
|
||||
show: 1119
|
||||
tags: FLOSS, Creative Commons, Cory Doctorow, Science Fiction
|
||||
|
||||
show: 1120
|
||||
tags: Qt, desktop environment, Razor-qt
|
||||
|
26
Database/tsu/tag_summary_updates_1121-1130.txt
Normal file
26
Database/tsu/tag_summary_updates_1121-1130.txt
Normal file
@@ -0,0 +1,26 @@
|
||||
show: 1121
|
||||
tags: networking,SMB,CIFS,SAMBA,file server,NFS,AFP
|
||||
|
||||
show: 1123
|
||||
tags: Android,GPS
|
||||
|
||||
show: 1124
|
||||
tags: brewing,beer
|
||||
|
||||
show: 1125
|
||||
tags: newscast,TGTM
|
||||
|
||||
show: 1126
|
||||
tags: Drupal, DrupalCamp
|
||||
|
||||
show: 1127
|
||||
tags: networking,AFP,Apple Filing Protocol,Netatalk
|
||||
|
||||
show: 1128
|
||||
tags: compiler,lexical analysis,parsing
|
||||
|
||||
show: 1129
|
||||
tags: Windows XP,Ubuntu,Unity,Debian,Gnome,CrunchBang
|
||||
|
||||
show: 1130
|
||||
tags: newscast,TGTM
|
15
Database/tsu/tag_summary_updates_1131-1140.txt
Normal file
15
Database/tsu/tag_summary_updates_1131-1140.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
show: 1133
|
||||
tags: ZX Spectrum,Corel Linux,Debian,Gentoo,Ubuntu
|
||||
|
||||
show: 1134
|
||||
tags: Science Fiction,interview
|
||||
|
||||
show: 1137
|
||||
tags: OpenStreetMap,GPS
|
||||
|
||||
show: 1138
|
||||
tags: programming, Python
|
||||
|
||||
show: 1139
|
||||
tags:history,computers,IT
|
||||
|
24
Database/tsu/tag_summary_updates_1141-1150.txt
Normal file
24
Database/tsu/tag_summary_updates_1141-1150.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
show: 1141
|
||||
#summary: A brief introduction to the Mumble client
|
||||
tags: mumble,SSL certificate,audio wizard
|
||||
|
||||
show: 1143
|
||||
#summary: Intro to Recreational Math Part Zero
|
||||
tags: mathematics, calendar counting
|
||||
|
||||
show: 1144
|
||||
#summary: Ahuka discusses the aftermath of the wiping a Norwegian user's Kindle by Amazon
|
||||
tags: DRM, digital rights management
|
||||
|
||||
show: 1146
|
||||
#summary: An introduction to Wireshark
|
||||
tags: communication protocol,tcpip,udp,wireshark
|
||||
|
||||
show: 1147
|
||||
#summary: Memories of the ASUS Eee PC 701 superseded by the rise of the tablet
|
||||
tags: netbook,Dell Mini 9,Dell Mini 10,tablet
|
||||
|
||||
show: 1150
|
||||
#summary: DeepGeek shares his first speech concerned with Mysticism
|
||||
tags: mysticism
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user