forked from HPR/hpr-tools
		
	
		
			
	
	
		
			882 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
		
		
			
		
	
	
			882 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
|   | #!/usr/bin/env perl | ||
|  | #=============================================================================== | ||
|  | # | ||
|  | #         FILE: edit_shownotes | ||
|  | # | ||
|  | #        USAGE: ./edit_shownotes [-help] [-debug=N] [-field=NAME] -episode=N | ||
|  | # | ||
|  | #  DESCRIPTION: Perform edits on metadata in show notes | ||
|  | # | ||
|  | #      OPTIONS: --- | ||
|  | # REQUIREMENTS: --- | ||
|  | #         BUGS: --- | ||
|  | #        NOTES: 2022-12-20: originally written to update the shownotes.txt | ||
|  | #               file as well as shownotes.json. Now we are phasing out the | ||
|  | #               former and using just the JSON, so the former file is not | ||
|  | #               being edited. | ||
|  | #       AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com | ||
|  | #      VERSION: 0.0.5 | ||
|  | #      CREATED: 2022-12-07 13:05:40 | ||
|  | #     REVISION: 2024-04-27 22:41:24 | ||
|  | # | ||
|  | #=============================================================================== | ||
|  | 
 | ||
|  | use v5.36; | ||
|  | use strict; | ||
|  | use warnings; | ||
|  | use utf8; | ||
|  | use feature qw{ postderef say signatures state try }; | ||
|  | no warnings | ||
|  |     qw{ experimental::postderef experimental::signatures experimental::try }; | ||
|  | 
 | ||
|  | use Getopt::Long; | ||
|  | use Pod::Usage qw{pod2usage}; | ||
|  | 
 | ||
|  | use Term::ANSIColor; | ||
|  | use File::Temp; | ||
|  | use File::Slurper qw{ read_text write_text }; | ||
|  | use File::Copy; | ||
|  | 
 | ||
|  | use JSON; | ||
|  | 
 | ||
|  | use Log::Handler; | ||
|  | 
 | ||
|  | 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/Show_Submission"; | ||
|  | my $cache   = "$basedir/shownotes"; | ||
|  | my $logdir  = "$basedir/logs"; | ||
|  | my $logfile = "$logdir/${PROG}.log"; | ||
|  | 
 | ||
|  | my ( $fh,       $showno, $showid ); | ||
|  | my ( $jsonfile, $jsonfile_bk, $json_text, $JSON_content ); | ||
|  | my ( $before,   $field_contents ); | ||
|  | #my ( $txtfile,  $txtfile_bk, $txtstring, @txtbuffer ); | ||
|  | 
 | ||
|  | # | ||
|  | # Text colours | ||
|  | # | ||
|  | my $red     = color('red'); | ||
|  | my $green   = color('green'); | ||
|  | my $yellow  = color('yellow'); | ||
|  | my $magenta = color('magenta'); | ||
|  | my $bold    = color('bold'); | ||
|  | my $reset   = color('reset'); | ||
|  | 
 | ||
|  | # | ||
|  | # How to find (and edit) the fields in shownotes.{txt,json}. | ||
|  | # The fields are 'tags', 'title' and 'summary'. | ||
|  | # If the JSON file is being edited the primary field offered to the editor is | ||
|  | # the first in the array (e.g. 'episode.Tags') and the other one simply echoes | ||
|  | # it. | ||
|  | # NOTE: 2022-12-20 - stopped modifying the shownotes.txt file | ||
|  | # | ||
|  | # If the TXT file is being updated (from the primary JSON version edited | ||
|  | # earlier) we find it in the array holding the text file using the 'regex' | ||
|  | # value, and build a new line using the 'label' value. | ||
|  | # This will be cleaned up when the TXT variant is dropped. | ||
|  | # | ||
|  | my %fields = ( | ||
|  |     'tags' => { | ||
|  |         'json' => [ 'episode.Tags', 'metadata.POST.tags' ], | ||
|  | #        'txt'  => { regex => qr{^Tags:}, label => 'Tags', }, | ||
|  |     }, | ||
|  |     'title' => { | ||
|  |         'json' => [ 'episode.Title', 'metadata.POST.title' ], | ||
|  | #        'txt'  => { regex => qr{^Title:}, label => 'Title', }, | ||
|  |     }, | ||
|  |     'summary' => { | ||
|  |         'json' => [ 'episode.Summary', 'metadata.POST.summary' ], | ||
|  | #        'txt'  => { regex => qr{^Summary:}, label => 'Summary', }, | ||
|  |     }, | ||
|  | ); | ||
|  | 
 | ||
|  | # | ||
|  | # Enable Unicode mode | ||
|  | # | ||
|  | binmode STDOUT, ":encoding(UTF-8)"; | ||
|  | binmode STDERR, ":encoding(UTF-8)"; | ||
|  | 
 | ||
|  | #------------------------------------------------------------------------------- | ||
|  | # Options and arguments | ||
|  | #------------------------------------------------------------------------------- | ||
|  | my $DEF_DEBUG = 0; | ||
|  | my $DEF_FIELD = 'tags'; | ||
|  | 
 | ||
|  | # | ||
|  | # Process options | ||
|  | # | ||
|  | my %options; | ||
|  | Options( \%options ); | ||
|  | 
 | ||
|  | # | ||
|  | # Default help | ||
|  | # | ||
|  | pod2usage( | ||
|  |     -msg     => "$PROG version $VERSION\n", | ||
|  |     -verbose => 0, | ||
|  |     -exitval => 1 | ||
|  | ) if ( $options{'help'} ); | ||
|  | 
 | ||
|  | # | ||
|  | # Detailed help | ||
|  | # | ||
|  | pod2usage( | ||
|  |     -msg       => "$PROG version $VERSION\n", | ||
|  |     -verbose   => 2, | ||
|  |     -exitval   => 1, | ||
|  | ) if ( $options{'documentation'} ); | ||
|  | 
 | ||
|  | # | ||
|  | # Collect options | ||
|  | # | ||
|  | my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG ); | ||
|  | my $field = ( defined( $options{field} ) ? $options{field} : $DEF_FIELD ); | ||
|  | 
 | ||
|  | my $test_dir = $options{test_dir}; | ||
|  | 
 | ||
|  | $showno = $options{episode}; | ||
|  | pod2usage( | ||
|  |     -msg     => "$PROG version $VERSION\nMissing mandatory option -episode\n", | ||
|  |     -verbose => 0, | ||
|  |     -exitval => 1 | ||
|  | ) unless $showno; | ||
|  | 
 | ||
|  | # | ||
|  | # Make an id in 'hpr1234' format | ||
|  | # | ||
|  | $showid = sprintf('hpr%04d',$showno); | ||
|  | 
 | ||
|  | # | ||
|  | # Check that the -field=FIELD option is valid | ||
|  | # | ||
|  | die "Invalid field specification: $field\n" | ||
|  |     unless ($field =~ /^(tags|title|summary)$/); | ||
|  | 
 | ||
|  | # | ||
|  | # The files we'll parse and/or edit (and that we'll backup to). Check the | ||
|  | # former exist. | ||
|  | # If we've received a test directory we'll run in test mode | ||
|  | # | ||
|  | if ($test_dir) { | ||
|  |     # | ||
|  |     # We need a directory here so remove any file component. | ||
|  |     # | ||
|  |     die "Missing path $test_dir\n" unless ( -e $test_dir ); | ||
|  |     unless (-d $test_dir) { | ||
|  |         # | ||
|  |         # Trim a file path to what is likely to be a directory only, and test | ||
|  |         # it exists. Add a trailing '/' if none. | ||
|  |         # | ||
|  |         $test_dir =~ s|/?[^/]*$||mx; | ||
|  |         die "Missing directory $test_dir\n" unless ( -d $test_dir ); | ||
|  |         $test_dir .= '/' unless ($test_dir =~ qr{/$}); | ||
|  |     } | ||
|  | 
 | ||
|  |     # | ||
|  |     # Look in the test place | ||
|  |     # | ||
|  |     $jsonfile = "$test_dir/shownotes.json"; | ||
|  | #    $txtfile  = "$test_dir/shownotes.txt"; | ||
|  | 
 | ||
|  |     $jsonfile_bk = "$test_dir/shownotes.json.orig"; | ||
|  | #    $txtfile_bk  = "$test_dir/shownotes.txt.orig"; | ||
|  | } | ||
|  | else { | ||
|  |     # | ||
|  |     # Look in the default place | ||
|  |     # | ||
|  |     $jsonfile = "$cache/$showid/shownotes.json"; | ||
|  | #    $txtfile  = "$cache/$showid/shownotes.txt"; | ||
|  | 
 | ||
|  |     $jsonfile_bk = "$cache/$showid/shownotes.json.orig"; | ||
|  | #    $txtfile_bk  = "$cache/$showid/shownotes.txt.orig"; | ||
|  | } | ||
|  | 
 | ||
|  | die colored( "Unable to find JSON file $jsonfile", 'red' ) . "\n" | ||
|  |     unless ( -e $jsonfile ); | ||
|  | #die colored( "Unable to find text file $txtfile", 'red' ) . "\n" | ||
|  | #    unless ( -e $txtfile ); | ||
|  | 
 | ||
|  | #------------------------------------------------------------------------------- | ||
|  | # Set up logging keeping the default log layout except for the date. The format | ||
|  | # is "%T [%L] %m" where '%T' is the timestamp, '%L' is the log level and '%m is | ||
|  | # the message. We fiddle with this to implement a 'Test' mode. | ||
|  | #------------------------------------------------------------------------------- | ||
|  | my $log = Log::Handler->new(); | ||
|  | 
 | ||
|  | $log->add( | ||
|  |     file => { | ||
|  |         timeformat     => "%Y/%m/%d %H:%M:%S", | ||
|  |         message_layout => (defined($test_dir) ? "%T [%L] TEST %m" : "%T [%L] %m"), | ||
|  |         filename       => $logfile, | ||
|  |         minlevel       => 0, | ||
|  |         maxlevel       => 7, | ||
|  |     } | ||
|  | ); | ||
|  | 
 | ||
|  | #------------------------------------------------------------------------------- | ||
|  | #  Read the JSON input file and parse it into $JSON_content (hashref) | ||
|  | #------------------------------------------------------------------------------- | ||
|  | my $json = JSON->new->utf8; | ||
|  | open( $fh, '<:encoding(UTF-8)', $jsonfile ); | ||
|  | $json_text = <$fh>; | ||
|  | close($fh); | ||
|  | 
 | ||
|  | # | ||
|  | # Careful! Bad JSON can crash the script here! | ||
|  | # | ||
|  | try { | ||
|  |     $JSON_content = decode_json($json_text); | ||
|  | } | ||
|  | catch ($e) { | ||
|  |     die colored( "Failed to decode the JSON in $jsonfile", 'red' ) . "\n" | ||
|  | } | ||
|  | 
 | ||
|  | $log->info( $showno, "Collecting $field from JSON" ); | ||
|  | 
 | ||
|  | _debug( $DEBUG > 2, Dumper($JSON_content) ); | ||
|  | #_debug( $DEBUG > 2, "\$field: $field" ); | ||
|  | #_debug( $DEBUG > 2, Dumper(\%fields) ); | ||
|  | 
 | ||
|  | 
 | ||
|  | #------------------------------------------------------------------------------- | ||
|  | # Get the contents of the primary JSON field. The group will all be the same, | ||
|  | # as will the text versions, so we edit the primary and propagate to the | ||
|  | # others. | ||
|  | #------------------------------------------------------------------------------- | ||
|  | $log->info( $showno, | ||
|  |     "Collecting from JSON with path '$fields{$field}->{json}->[0]'" ); | ||
|  | 
 | ||
|  | # | ||
|  | # Use the path in the %fields hash to get the prinmary node in the JSON hash. | ||
|  | # We are using 'get_JSON' rather than going to the field directly because | ||
|  | # we're hunting for different parts of the JSON structure depending on the | ||
|  | # -field=NAME option and using a jq-like path stored in %fields. | ||
|  | # | ||
|  | $field_contents = get_JSON( $JSON_content, $fields{$field}->{json}->[0] ); | ||
|  | #say( defined($field_contents) ? $field_contents : 'undefined' ); | ||
|  | die colored( "No contents in field '$field'. Can't continue", 'red' ) . "\n" | ||
|  |     unless ( defined($field_contents) ); | ||
|  | 
 | ||
|  | $log->info( $showno, "Contents of field: $field_contents" ); | ||
|  | $before = $field_contents; | ||
|  | 
 | ||
|  | # | ||
|  | # Run the editor on what we collected | ||
|  | # | ||
|  | if ( $field eq 'tags' ) { | ||
|  |     $field_contents | ||
|  |         = check_field( $field, run_editor($field_contents), 200, qr{(\n)} ); | ||
|  | } | ||
|  | elsif ( $field eq 'title' ) { | ||
|  |     $field_contents | ||
|  |         = check_field( $field, run_editor($field_contents), 100, qr{(\n)} ); | ||
|  | } | ||
|  | elsif ( $field eq 'summary' ) { | ||
|  |     $field_contents | ||
|  |         = check_field( $field, run_editor($field_contents), 100, qr{(\n)} ); | ||
|  | } | ||
|  | 
 | ||
|  | die colored( "Nothing was changed. Exiting", 'red' ) . "\n" | ||
|  |     if ( $field_contents eq $before ); | ||
|  | 
 | ||
|  | $log->info( $showno, "New contents of field: $field_contents" ); | ||
|  | 
 | ||
|  | #------------------------------------------------------------------------------- | ||
|  | # Load the text version of the metadata | ||
|  | # NOTE: 2022-12-20 - stopped modifying the shownotes.txt file | ||
|  | #------------------------------------------------------------------------------- | ||
|  | #open( $fh, '<:encoding(UTF-8)', $txtfile ); | ||
|  | #@txtbuffer = <$fh>; | ||
|  | #close($fh); | ||
|  | 
 | ||
|  | #------------------------------------------------------------------------------- | ||
|  | # Make the changes to all the relevant points in the two data structures. | ||
|  | # NOTE: 2022-12-20 - stopped modifying the shownotes.txt file | ||
|  | #------------------------------------------------------------------------------- | ||
|  | put_JSON( $JSON_content, $fields{$field}->{json}->[0], $field_contents ); | ||
|  | put_JSON( $JSON_content, $fields{$field}->{json}->[1], $field_contents ); | ||
|  | #_debug( $DEBUG > 1, Dumper($JSON_content) ); | ||
|  | 
 | ||
|  | $log->info( $showno, "Updated JSON '$field' fields" ); | ||
|  | 
 | ||
|  | #@txtbuffer = update_txt( \@txtbuffer, $fields{$field}->{txt}->{regex}, | ||
|  | #    $fields{$field}->{txt}->{label} . ":\t$field_contents\n" ); | ||
|  | #_debug( $DEBUG > 1, "Text: " . Dumper( \@txtbuffer ) ); | ||
|  | 
 | ||
|  | #$log->info( $showno, "Updated text '$field' fields" ); | ||
|  | 
 | ||
|  | #------------------------------------------------------------------------------- | ||
|  | # Turn the data structures back into files, backing up the originals and | ||
|  | # writing the new. | ||
|  | #------------------------------------------------------------------------------- | ||
|  | unless ( -e $jsonfile_bk ) { | ||
|  |     copy ($jsonfile, $jsonfile_bk) or die "Unable to backup $jsonfile\n"; | ||
|  |     $log->info( $showno, "JSON backup created" ); | ||
|  | } | ||
|  | $json_text = encode_json($JSON_content); | ||
|  | write_text($jsonfile,$json_text); | ||
|  | 
 | ||
|  | $log->info( $showno, "JSON file updated" ); | ||
|  | 
 | ||
|  | #unless ( -e $txtfile_bk ) { | ||
|  | #    copy ($txtfile, $txtfile_bk) or die "Unable to backup $txtfile\n"; | ||
|  | #    $log->info( $showno, "Text backup created" ); | ||
|  | #} | ||
|  | #$txtstring = join('',@txtbuffer); | ||
|  | #write_text($txtfile,$txtstring); | ||
|  | # | ||
|  | #$log->info( $showno, "Text file updated" ); | ||
|  | 
 | ||
|  | exit; | ||
|  | 
 | ||
|  | #===  FUNCTION  ================================================================ | ||
|  | #         NAME: get_JSON | ||
|  | #      PURPOSE: Traverse a data structure using dotted notation and return the | ||
|  | #               value at the end point | ||
|  | #   PARAMETERS: $hash           hashref to traverse | ||
|  | #               $path           a path in the form of '.key1.key2.[0]' which | ||
|  | #                               should walk to the key 'key1', then 'key2' | ||
|  | #                               within it, and open the array expected to be | ||
|  | #                               there and get element 0. | ||
|  | #      RETURNS: The value found or undef | ||
|  | #  DESCRIPTION: Uses 'traverse_JSON' to find the $path in the $hash, returning | ||
|  | #               a pointer (reference) to it. This can then be de-referenced to | ||
|  | #               return the item being referenced - unless it's undefined in | ||
|  | #               which case that value is returned. Being undefined means that | ||
|  | #               the path did not match the structure of the hash. | ||
|  | #       THROWS: No exceptions | ||
|  | #     COMMENTS: None | ||
|  | #     SEE ALSO: N/A | ||
|  | #=============================================================================== | ||
|  | sub get_JSON { | ||
|  |     my ( $hash, $path ) = @_; | ||
|  | 
 | ||
|  |     my $pointer = traverse_JSON( $hash, $path ); | ||
|  | 
 | ||
|  |     # | ||
|  |     # If undef then need to return undef, not try and deref it! | ||
|  |     # | ||
|  |     if (defined($pointer)) { | ||
|  |         return $$pointer; | ||
|  |     } | ||
|  |     return; | ||
|  | } | ||
|  | 
 | ||
|  | #===  FUNCTION  ================================================================ | ||
|  | #         NAME: put_JSON | ||
|  | #      PURPOSE: Traverse a data structure using dotted notation and update the | ||
|  | #               value at the end point | ||
|  | #   PARAMETERS: $hash           hashref to traverse | ||
|  | #               $path           a path in the form of '.key1.key2.[0]' which | ||
|  | #                               should walk to the key 'key1', then 'key2' | ||
|  | #                               within it, and open the array expected to be | ||
|  | #                               there and rewrite element 0. | ||
|  | #               $new_value      the new value to place at the point in the | ||
|  | #                               hash | ||
|  | #      RETURNS: The previous value if found, or undef | ||
|  | #  DESCRIPTION: Uses 'traverse_JSON' to find the $path in the $hash, returning | ||
|  | #               a pointer (reference) to it. This can then be de-referenced to | ||
|  | #               return the item being referenced - unless it's undefined in | ||
|  | #               which case that value is returned. Being undefined means that | ||
|  | #               the path did not match the structure of the hash. Finally, if | ||
|  | #               the reference is valid then it's used to save the contents of | ||
|  | #               $new_value. | ||
|  | #       THROWS: No exceptions | ||
|  | #     COMMENTS: None | ||
|  | #     SEE ALSO: N/A | ||
|  | #=============================================================================== | ||
|  | sub put_JSON { | ||
|  |     my ( $hash, $path, $new_value ) = @_; | ||
|  | 
 | ||
|  |     my $pointer = traverse_JSON( $hash, $path ); | ||
|  |     my $old_value; | ||
|  | 
 | ||
|  |     # | ||
|  |     # If $pointer is defined then dereference it as the return value. Also, | ||
|  |     # use it to write the new value. | ||
|  |     # | ||
|  |     if (defined($pointer)) { | ||
|  |         $old_value = $$pointer; | ||
|  |         $$pointer = $new_value; | ||
|  |     } | ||
|  | 
 | ||
|  |     return $old_value; | ||
|  | } | ||
|  | 
 | ||
|  | #===  FUNCTION  ================================================================ | ||
|  | #         NAME: traverse_JSON | ||
|  | #      PURPOSE: Traverse a data structure using dotted notation | ||
|  | #   PARAMETERS: $hash           hashref to traverse | ||
|  | #               $path           a path in the form of '.key1.key2.[0]' which | ||
|  | #                               should walk to the key 'key1', then 'key2' | ||
|  | #                               within it, and open the array expected to be | ||
|  | #                               there and return a pointer to element 0. | ||
|  | #      RETURNS: A reference to the end point of the path, or undef if not | ||
|  | #               found. | ||
|  | #  DESCRIPTION: Given a path like '.metadata.POST.title' (where the leading | ||
|  | #               '.' is optional because it's ignored), this is used to | ||
|  | #               traverse a Perl version of a JSON data structure to return | ||
|  | #               a *pointer* (reference) to a node. | ||
|  | # | ||
|  | #               This is done by processing the path step by step and using | ||
|  | #               'name' elements as hash keys to get to the next level or | ||
|  | #               return a terminal hash element. We also cater for '.[0]' | ||
|  | #               format path elements which means to use the zeroth member of | ||
|  | #               an array. Thus, if the tags were represented as an array rather | ||
|  | #               than a scalar CSV string, we'd be able to use a path like: | ||
|  | #               'episode.Tags.[0]' to return the first tag. | ||
|  | # | ||
|  | #               The data structure traversal is performed by using a pointer | ||
|  | #               to a part of it, and since nested hashes are constructed by | ||
|  | #               using hashrefs as hash element values, we get back references | ||
|  | #               by default. | ||
|  | # | ||
|  | #               Once the terminal node has been reached it may be a scalar | ||
|  | #               (very likely in this application), so we make and return | ||
|  | #               a reference to it so that the caller can use this reference in | ||
|  | #               a standard way to return or change the contents. | ||
|  | # | ||
|  | #               This function is meant to be more generic than this | ||
|  | #               application requires. The JSON we're dealing with here doesn't | ||
|  | #               have arrays (at the moment). This description is | ||
|  | #               application-specifc however! | ||
|  | #       THROWS: No exceptions | ||
|  | #     COMMENTS: None | ||
|  | #     SEE ALSO: N/A | ||
|  | #=============================================================================== | ||
|  | sub traverse_JSON { | ||
|  |     my ( $hash, $path ) = @_; | ||
|  | 
 | ||
|  |     my ( $pointer, $index ); | ||
|  |     my @elements = split( '\.', $path ); | ||
|  | 
 | ||
|  |     # | ||
|  |     # Because $hash is a hashref we can just copy it | ||
|  |     # | ||
|  |     $pointer = $hash; | ||
|  | 
 | ||
|  |     # | ||
|  |     # Traverse the hash using the path | ||
|  |     # | ||
|  |     foreach my $element (@elements) { | ||
|  |         next unless defined($element); | ||
|  | 
 | ||
|  |         # | ||
|  |         # Plain names are keys or scalar end nodes, bracketed things are | ||
|  |         # arrays (which can be scalars of course) | ||
|  |         # | ||
|  |         unless ( ($index) = ( $element =~ /\[([^]])\]/ ) ) { | ||
|  |             # Hash key | ||
|  |             if ( exists( $pointer->{$element} ) ) { | ||
|  |                 if ( ref( $pointer->{$element} ) eq '' ) { | ||
|  |                     # | ||
|  |                     # It's not a refererence so make a reference to it | ||
|  |                     # | ||
|  |                     $pointer = \$pointer->{$element}; | ||
|  |                 } | ||
|  |                 else { | ||
|  |                     # | ||
|  |                     # It's a reference | ||
|  |                     # | ||
|  |                     $pointer = $pointer->{$element}; | ||
|  |                 } | ||
|  |             } | ||
|  |             else { | ||
|  |                 # | ||
|  |                 # Doesn't exist! | ||
|  |                 # | ||
|  |                 return;    #undef | ||
|  |             } | ||
|  |         } | ||
|  |         else { | ||
|  |             # Array | ||
|  |             if ( exists( $pointer->[$index] ) ) { | ||
|  |                 if ( ref( $pointer->[$index] ) eq '' ) { | ||
|  |                     # | ||
|  |                     # It's not a refererence so make a reference to it | ||
|  |                     # | ||
|  |                     $pointer = \$pointer->[$index]; | ||
|  |                 } | ||
|  |                 else { | ||
|  |                     # | ||
|  |                     # It's a reference | ||
|  |                     # | ||
|  |                     $pointer = $pointer->[$index]; | ||
|  |                 } | ||
|  |             } | ||
|  |             else { | ||
|  |                 # | ||
|  |                 # Doesn't exist! | ||
|  |                 # | ||
|  |                 return;    # undef | ||
|  |             } | ||
|  |         } | ||
|  |     } | ||
|  |     return $pointer; | ||
|  | } | ||
|  | 
 | ||
|  | #===  FUNCTION  ================================================================ | ||
|  | #         NAME: update_txt | ||
|  | #      PURPOSE: Update the data structure from 'shownotes.txt' in the light of | ||
|  | #               requested changes. | ||
|  | #   PARAMETERS: $array          arrayref to the contents of the shownotes.txt | ||
|  | #                               file | ||
|  | #               $regex          regular expression to find the line that needs | ||
|  | #                               to be changed (there's just one main one). | ||
|  | #               $new_value      new value to replace the original line | ||
|  | #      RETURNS: Modified array | ||
|  | #  DESCRIPTION: Finds the element in the arrayref ($array) which matches the | ||
|  | #               regular expression ($regex). When found it is replaced by the | ||
|  | #               new value ($new_value). The modified buffer is returned as | ||
|  | #               a list. Uses $result to determine the value of each line since | ||
|  | #               it is not permitted to change the $_ variable in the 'map' | ||
|  | #               statement. | ||
|  | #       THROWS: No exceptions | ||
|  | #     COMMENTS: None | ||
|  | #     SEE ALSO: N/A | ||
|  | #=============================================================================== | ||
|  | sub update_txt { | ||
|  |     my ( $array, $regex, $new_value ) = @_; | ||
|  | 
 | ||
|  |     my $result; | ||
|  |     my @newbuffer | ||
|  |         = map { $result = ( $_ =~ /$regex/ ? $new_value : $_ ); $result } | ||
|  |         @$array; | ||
|  | 
 | ||
|  |     return @newbuffer; | ||
|  | } | ||
|  | 
 | ||
|  | #===  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: coalesce | ||
|  | #      PURPOSE: To find the first defined argument and return it | ||
|  | #   PARAMETERS: Arbitrary number of arguments | ||
|  | #      RETURNS: The first defined argument or undef if there are none | ||
|  | #  DESCRIPTION: | ||
|  | #       THROWS: No exceptions | ||
|  | #     COMMENTS: None | ||
|  | #     SEE ALSO: N/A | ||
|  | #=============================================================================== | ||
|  | sub coalesce { | ||
|  |     foreach (@_) { | ||
|  |         return $_ if defined($_); | ||
|  |     } | ||
|  |     return undef;    ## no critic | ||
|  | } | ||
|  | 
 | ||
|  | #===  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: Options | ||
|  | #      PURPOSE: Processes command-line options | ||
|  | #   PARAMETERS: $optref     Hash reference to hold the options | ||
|  | #      RETURNS: Undef | ||
|  | #  DESCRIPTION: | ||
|  | #       THROWS: no exceptions | ||
|  | #     COMMENTS: none | ||
|  | #     SEE ALSO: n/a | ||
|  | #=============================================================================== | ||
|  | sub Options { | ||
|  |     my ($optref) = @_; | ||
|  | 
 | ||
|  |     my @options = ( | ||
|  |         "help",      "documentation|manpage", | ||
|  |         "debug=i",   "field=s", | ||
|  |         "episode=i", "test_dir=s", | ||
|  |     ); | ||
|  | 
 | ||
|  |     if ( !GetOptions( $optref, @options ) ) { | ||
|  |         pod2usage( | ||
|  |             -msg     => "$PROG version $VERSION\n", | ||
|  |             -verbose => 0, | ||
|  |             -exitval => 1 | ||
|  |         ); | ||
|  |     } | ||
|  | 
 | ||
|  |     return; | ||
|  | } | ||
|  | 
 | ||
|  | __END__ | ||
|  | 
 | ||
|  | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | ||
|  | #  Application Documentation | ||
|  | #%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | ||
|  | #{{{ | ||
|  | 
 | ||
|  | =head1 NAME | ||
|  | 
 | ||
|  | edit_shownotes - edit JSON and text shownote metadata | ||
|  | 
 | ||
|  | =head1 VERSION | ||
|  | 
 | ||
|  | This documentation refers to edit_shownotes version 0.0.5 | ||
|  | 
 | ||
|  | =head1 USAGE | ||
|  | 
 | ||
|  |     ./edit_shownotes [-help] [-documentation|manpage] [-debug=N] | ||
|  |         [-field=FIELD] -episode=N | ||
|  | 
 | ||
|  | 
 | ||
|  |     ./edit_shownotes -field=tags -episode=3754 | ||
|  |     ./edit_shownotes -field=summary -episode=3762 \ | ||
|  |         -test_dir=$PWD/tests/shownotes/hpr3762 | ||
|  | 
 | ||
|  | =head1 OPTIONS | ||
|  | 
 | ||
|  | =over 8 | ||
|  | 
 | ||
|  | =item B<-help> | ||
|  | 
 | ||
|  | Prints a brief help message describing the usage of the program, and then exits. | ||
|  | 
 | ||
|  | =item B<-manpage> or B<-documentation> | ||
|  | 
 | ||
|  | Prints the entire documentation for the script. | ||
|  | 
 | ||
|  | =item B<-field=FIELD> | ||
|  | 
 | ||
|  | This optional parameter defaults to B<-field=tags>. The only permitted fields | ||
|  | are 'tags', 'title' and 'summary'. | ||
|  | 
 | ||
|  | =item B<-episode=N> | ||
|  | 
 | ||
|  | This mandatory option specifies the number of the HPR episode that is being | ||
|  | parsed. It needs to be numeric even though it is often used with an 'hpr' | ||
|  | prefix internally. Leading zeroes are not required (when these are | ||
|  | appropriate). | ||
|  | 
 | ||
|  | =item B<-test_dir=PATH> | ||
|  | 
 | ||
|  | This option is for testing. Normally the script determines the path to the | ||
|  | file(s) it will parse and update. It actually finds the field specified by the | ||
|  | B<-field-FIELD> option in the JSON file B<shownotes.json> and calls the editor | ||
|  | to change this, then propagates any changes to the other JSON instance and the | ||
|  | one in B<shownotes.txt>. | ||
|  | 
 | ||
|  | This option has been provided to make it simpler to make copies of the files | ||
|  | relating to an incoming show and use them to test changes to the script. | ||
|  | 
 | ||
|  | The current workflow expects the "live" files in | ||
|  | B<$HOME/HPR/Show_Submission/shownotes/hprSHOW>, but there is another area | ||
|  | where show files are placed for testing: | ||
|  | B<$HOME/HPR/Show_Submission/tests/shownotes/hprSHOW>. | ||
|  | 
 | ||
|  | It is simply a matter of making a copy and of referencing the directory path | ||
|  | to this option. Changes will be made to this copy and log records written, but | ||
|  | these will contain the word 'TEST' to show that the script is being run in | ||
|  | this mode. | ||
|  | 
 | ||
|  | =item B<-debug=N> | ||
|  | 
 | ||
|  | Causes certain debugging information to be displayed. | ||
|  | 
 | ||
|  |     0   (the default) no debug output | ||
|  |     1   N/A | ||
|  |     2   TBA | ||
|  |     3   TBA | ||
|  | 
 | ||
|  | =back | ||
|  | 
 | ||
|  | =head1 DESCRIPTION | ||
|  | 
 | ||
|  | This script is meant to be a component in a workflow dealing with the notes | ||
|  | submitted by HPR hosts with the shows they are uploading. | ||
|  | 
 | ||
|  | The data from the web form which is used by hosts submitting shows is received | ||
|  | in two formats, with fixed names. | ||
|  | 
 | ||
|  | The file B<shownotes.txt> contains the form data in a label/value format, but | ||
|  | also uses PHP dumps to show some of the metadata. It is no longer used by the | ||
|  | current workflow, but is retained for historical reasons. | ||
|  | 
 | ||
|  | The alternative file format is JSON, which is stored in B<shownotes.json>. | ||
|  | This contains the same data as the other file but in a more standardised | ||
|  | format. | ||
|  | 
 | ||
|  | This script is designed to allow the editing of three specific fields in these | ||
|  | two files. This is because these fields are not used in the show note | ||
|  | preparation workflow (though they are referenced), but any changes can be | ||
|  | propagated back to the HPR server to allow workflows there to use the | ||
|  | corrected version(s). | ||
|  | 
 | ||
|  | The fields which can be edited are duplicated in the two files. One of these | ||
|  | is edited, and if appropriate, is propagated to the other duplicates. This | ||
|  | simplifies the editing of the text format, and the JSON format, which is not | ||
|  | simple to edit in a conventional way. | ||
|  | 
 | ||
|  | =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 | ||
|  | 
 | ||
|  | Data::Dumper | ||
|  | File::Slurper | ||
|  | File::Temp | ||
|  | Getopt::Long | ||
|  | JSON | ||
|  | Log::Handler | ||
|  | Pod::Usage | ||
|  | Term::ANSIColor | ||
|  | 
 | ||
|  | =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. | ||
|  | 
 | ||
|  | 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 | ||
|  | 
 |