2024-06-04 16:35:44 +01:00
|
|
|
#!/usr/bin/env perl
|
|
|
|
#===============================================================================
|
|
|
|
#
|
|
|
|
# FILE: make_email
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
# USAGE: ./make_email [-debug=N] [-month=DATE] [-date=DATE]
|
|
|
|
# [-start=START_TIME] [-end=END_TIME] [-output[=FILE]]
|
2025-04-03 16:57:57 +01:00
|
|
|
# [-[no]force] [-config=FILE]
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
# DESCRIPTION: Make an invitation email for the next Community News
|
2024-06-04 16:35:44 +01:00
|
|
|
# with times per timezone.
|
|
|
|
#
|
|
|
|
# The configuration file (.make_email.cfg) defines the name of
|
|
|
|
# the email template and the defaults used when generating the
|
|
|
|
# message. The date of the recording is computed from the
|
2025-03-31 21:59:14 +01:00
|
|
|
# current month (Friday before the first Monday of the month
|
2024-06-04 16:35:44 +01:00
|
|
|
# when the show will be posted). It can also be specified
|
|
|
|
# through the -date=DATE option.
|
|
|
|
#
|
|
|
|
# The month the email relates to can be changed through the
|
2025-03-31 21:59:14 +01:00
|
|
|
# -month=DATE option, though this is rarely used. Use a date of
|
2024-06-04 16:35:44 +01:00
|
|
|
# the format 'YYYY-MM-DD' here. The day is ignored but the year
|
|
|
|
# and month are used in the computation. The month specified
|
|
|
|
# must be in the future.
|
|
|
|
#
|
|
|
|
# OPTIONS: ---
|
|
|
|
# REQUIREMENTS: ---
|
|
|
|
# BUGS: ---
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
# NOTES: Extensive rewrite. No longer needs a database and computes
|
|
|
|
# review months and recording dates itself.
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
|
|
|
# AUTHOR: Dave Morriss (djm), Dave.Morriss@gmail.com
|
2025-04-07 21:03:37 +01:00
|
|
|
# VERSION: 0.3.4
|
2024-06-04 16:35:44 +01:00
|
|
|
# CREATED: 2013-10-28 20:35:22
|
2025-04-07 21:03:37 +01:00
|
|
|
# REVISION: 2025-04-07 18:15:30
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
|
|
|
#===============================================================================
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
use v5.36;
|
|
|
|
use utf8;
|
|
|
|
use feature qw{ try };
|
|
|
|
no warnings qw{ experimental::try };
|
|
|
|
|
|
|
|
use open ':std', ':encoding(UTF-8)'; # Make all IO UTF-8
|
|
|
|
|
|
|
|
use Cwd qw( abs_path );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
use Getopt::Long;
|
2025-04-16 17:14:26 +01:00
|
|
|
BEGIN { $ENV{PERLDOC} = '-MPod::Text::Color'; }
|
|
|
|
use Pod::Usage qw(pod2usage); # Use colour-capable Pod::Text
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
use Config::General;
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
use File::Copy;
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
use Date::Parse;
|
|
|
|
|
|
|
|
use DateTime;
|
|
|
|
use DateTime::TimeZone;
|
|
|
|
use DateTime::Format::Duration;
|
|
|
|
|
|
|
|
use Date::Calc qw{:all};
|
|
|
|
|
|
|
|
use Template;
|
|
|
|
|
|
|
|
use DBI;
|
|
|
|
|
|
|
|
use Data::Dumper;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Version number (manually incremented)
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
our $VERSION = '0.3.4';
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Script name
|
|
|
|
#
|
|
|
|
( my $PROG = $0 ) =~ s|.*/||mx;
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
# Declarations
|
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
#
|
|
|
|
# Constants and other declarations
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
( my $basedir = abs_path($0) ) =~ s|/?[^/]*$||mx;
|
|
|
|
my $configfile = "$basedir/.${PROG}.cfg";
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
my ( %recdates, $rdfh );
|
|
|
|
|
|
|
|
#
|
|
|
|
# Run in the script's directory
|
|
|
|
#
|
|
|
|
chdir($basedir);
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# The timezones we want to report. These were generated with
|
|
|
|
# DateTime::TimeZone->all_names(). Just uncomment the desired elements.
|
|
|
|
#
|
|
|
|
my @zones = (
|
2025-04-16 17:14:26 +01:00
|
|
|
#{{{ --- time zones ---
|
2024-06-04 16:35:44 +01:00
|
|
|
#'Africa/Abidjan',
|
|
|
|
#'Africa/Accra',
|
|
|
|
#'Africa/Addis_Ababa',
|
|
|
|
#'Africa/Algiers',
|
|
|
|
#'Africa/Asmara',
|
|
|
|
#'Africa/Bamako',
|
|
|
|
#'Africa/Bangui',
|
|
|
|
#'Africa/Banjul',
|
|
|
|
#'Africa/Bissau',
|
|
|
|
#'Africa/Blantyre',
|
|
|
|
#'Africa/Brazzaville',
|
|
|
|
#'Africa/Bujumbura',
|
|
|
|
#'Africa/Cairo',
|
|
|
|
#'Africa/Casablanca',
|
|
|
|
#'Africa/Ceuta',
|
|
|
|
#'Africa/Conakry',
|
|
|
|
#'Africa/Dakar',
|
|
|
|
#'Africa/Dar_es_Salaam',
|
|
|
|
#'Africa/Djibouti',
|
|
|
|
#'Africa/Douala',
|
|
|
|
#'Africa/El_Aaiun',
|
|
|
|
#'Africa/Freetown',
|
|
|
|
#'Africa/Gaborone',
|
|
|
|
#'Africa/Harare',
|
|
|
|
#'Africa/Johannesburg',
|
|
|
|
#'Africa/Kampala',
|
|
|
|
#'Africa/Khartoum',
|
|
|
|
#'Africa/Kigali',
|
|
|
|
#'Africa/Kinshasa',
|
|
|
|
#'Africa/Lagos',
|
|
|
|
#'Africa/Libreville',
|
|
|
|
#'Africa/Lome',
|
|
|
|
#'Africa/Luanda',
|
|
|
|
#'Africa/Lubumbashi',
|
|
|
|
#'Africa/Lusaka',
|
|
|
|
#'Africa/Malabo',
|
|
|
|
#'Africa/Maputo',
|
|
|
|
#'Africa/Maseru',
|
|
|
|
#'Africa/Mbabane',
|
|
|
|
#'Africa/Mogadishu',
|
|
|
|
#'Africa/Monrovia',
|
|
|
|
#'Africa/Nairobi',
|
|
|
|
#'Africa/Ndjamena',
|
|
|
|
#'Africa/Niamey',
|
|
|
|
#'Africa/Nouakchott',
|
|
|
|
#'Africa/Ouagadougou',
|
|
|
|
#'Africa/Porto-Novo',
|
|
|
|
#'Africa/Sao_Tome',
|
|
|
|
#'Africa/Tripoli',
|
|
|
|
#'Africa/Tunis',
|
|
|
|
#'Africa/Windhoek',
|
|
|
|
#'America/Adak',
|
|
|
|
#'America/Anchorage',
|
|
|
|
#'America/Antigua',
|
|
|
|
#'America/Araguaina',
|
|
|
|
#'America/Argentina/Buenos_Aires',
|
|
|
|
#'America/Argentina/Catamarca',
|
|
|
|
#'America/Argentina/Cordoba',
|
|
|
|
#'America/Argentina/Jujuy',
|
|
|
|
#'America/Argentina/La_Rioja',
|
|
|
|
#'America/Argentina/Mendoza',
|
|
|
|
#'America/Argentina/Rio_Gallegos',
|
|
|
|
#'America/Argentina/Salta',
|
|
|
|
#'America/Argentina/San_Juan',
|
|
|
|
#'America/Argentina/San_Luis',
|
|
|
|
#'America/Argentina/Tucuman',
|
|
|
|
#'America/Argentina/Ushuaia',
|
|
|
|
#'America/Asuncion',
|
|
|
|
#'America/Atikokan',
|
|
|
|
#'America/Bahia',
|
|
|
|
#'America/Bahia_Banderas',
|
|
|
|
#'America/Barbados',
|
|
|
|
#'America/Belem',
|
|
|
|
#'America/Belize',
|
|
|
|
#'America/Blanc-Sablon',
|
|
|
|
#'America/Boa_Vista',
|
|
|
|
#'America/Bogota',
|
|
|
|
#'America/Boise',
|
|
|
|
#'America/Cambridge_Bay',
|
|
|
|
#'America/Campo_Grande',
|
|
|
|
#'America/Cancun',
|
|
|
|
#'America/Caracas',
|
|
|
|
#'America/Cayenne',
|
|
|
|
#'America/Cayman',
|
|
|
|
'America/Chicago',
|
|
|
|
#'America/Chihuahua',
|
|
|
|
#'America/Costa_Rica',
|
|
|
|
#'America/Creston',
|
|
|
|
#'America/Cuiaba',
|
|
|
|
#'America/Curacao',
|
|
|
|
#'America/Danmarkshavn',
|
|
|
|
#'America/Dawson',
|
|
|
|
#'America/Dawson_Creek',
|
|
|
|
#'America/Denver',
|
|
|
|
#'America/Detroit',
|
|
|
|
#'America/Edmonton',
|
|
|
|
#'America/Eirunepe',
|
|
|
|
#'America/El_Salvador',
|
|
|
|
#'America/Fortaleza',
|
|
|
|
#'America/Glace_Bay',
|
|
|
|
#'America/Godthab',
|
|
|
|
#'America/Goose_Bay',
|
|
|
|
#'America/Grand_Turk',
|
|
|
|
#'America/Guatemala',
|
|
|
|
#'America/Guayaquil',
|
|
|
|
#'America/Guyana',
|
|
|
|
#'America/Halifax',
|
|
|
|
#'America/Havana',
|
|
|
|
#'America/Hermosillo',
|
|
|
|
#'America/Indiana/Indianapolis',
|
|
|
|
#'America/Indiana/Knox',
|
|
|
|
#'America/Indiana/Marengo',
|
|
|
|
#'America/Indiana/Petersburg',
|
|
|
|
#'America/Indiana/Tell_City',
|
|
|
|
#'America/Indiana/Vevay',
|
|
|
|
#'America/Indiana/Vincennes',
|
|
|
|
#'America/Indiana/Winamac',
|
|
|
|
#'America/Inuvik',
|
|
|
|
#'America/Iqaluit',
|
|
|
|
#'America/Jamaica',
|
|
|
|
#'America/Juneau',
|
|
|
|
#'America/Kentucky/Louisville',
|
|
|
|
#'America/Kentucky/Monticello',
|
|
|
|
#'America/La_Paz',
|
|
|
|
#'America/Lima',
|
|
|
|
'America/Los_Angeles',
|
|
|
|
#'America/Maceio',
|
|
|
|
#'America/Managua',
|
|
|
|
#'America/Manaus',
|
|
|
|
#'America/Martinique',
|
|
|
|
#'America/Matamoros',
|
|
|
|
#'America/Mazatlan',
|
|
|
|
#'America/Menominee',
|
|
|
|
#'America/Merida',
|
|
|
|
#'America/Metlakatla',
|
|
|
|
#'America/Mexico_City',
|
|
|
|
#'America/Miquelon',
|
|
|
|
#'America/Moncton',
|
|
|
|
#'America/Monterrey',
|
|
|
|
#'America/Montevideo',
|
|
|
|
#'America/Montreal',
|
|
|
|
#'America/Nassau',
|
|
|
|
'America/New_York',
|
|
|
|
#'America/Nipigon',
|
|
|
|
#'America/Nome',
|
|
|
|
#'America/Noronha',
|
|
|
|
#'America/North_Dakota/Beulah',
|
|
|
|
#'America/North_Dakota/Center',
|
|
|
|
#'America/North_Dakota/New_Salem',
|
|
|
|
#'America/Ojinaga',
|
|
|
|
#'America/Panama',
|
|
|
|
#'America/Pangnirtung',
|
|
|
|
#'America/Paramaribo',
|
|
|
|
#'America/Phoenix',
|
|
|
|
#'America/Port-au-Prince',
|
|
|
|
#'America/Port_of_Spain',
|
|
|
|
#'America/Porto_Velho',
|
|
|
|
#'America/Puerto_Rico',
|
|
|
|
#'America/Rainy_River',
|
|
|
|
#'America/Rankin_Inlet',
|
|
|
|
#'America/Recife',
|
|
|
|
#'America/Regina',
|
|
|
|
#'America/Resolute',
|
|
|
|
#'America/Rio_Branco',
|
|
|
|
#'America/Santa_Isabel',
|
|
|
|
#'America/Santarem',
|
|
|
|
#'America/Santiago',
|
|
|
|
#'America/Santo_Domingo',
|
|
|
|
#'America/Sao_Paulo',
|
|
|
|
#'America/Scoresbysund',
|
|
|
|
#'America/Sitka',
|
|
|
|
#'America/St_Johns',
|
|
|
|
#'America/Swift_Current',
|
|
|
|
#'America/Tegucigalpa',
|
|
|
|
#'America/Thule',
|
|
|
|
#'America/Thunder_Bay',
|
|
|
|
#'America/Tijuana',
|
|
|
|
#'America/Toronto',
|
|
|
|
#'America/Vancouver',
|
|
|
|
#'America/Whitehorse',
|
|
|
|
#'America/Winnipeg',
|
|
|
|
#'America/Yakutat',
|
|
|
|
#'America/Yellowknife',
|
|
|
|
#'Antarctica/Casey',
|
|
|
|
#'Antarctica/Davis',
|
|
|
|
#'Antarctica/DumontDUrville',
|
|
|
|
#'Antarctica/Macquarie',
|
|
|
|
#'Antarctica/Mawson',
|
|
|
|
#'Antarctica/Palmer',
|
|
|
|
#'Antarctica/Rothera',
|
|
|
|
#'Antarctica/Syowa',
|
|
|
|
#'Antarctica/Vostok',
|
|
|
|
#'Asia/Aden',
|
|
|
|
#'Asia/Almaty',
|
|
|
|
#'Asia/Amman',
|
|
|
|
#'Asia/Anadyr',
|
|
|
|
#'Asia/Aqtau',
|
|
|
|
#'Asia/Aqtobe',
|
|
|
|
#'Asia/Ashgabat',
|
|
|
|
#'Asia/Baghdad',
|
|
|
|
#'Asia/Bahrain',
|
|
|
|
#'Asia/Baku',
|
|
|
|
#'Asia/Bangkok',
|
|
|
|
#'Asia/Beirut',
|
|
|
|
#'Asia/Bishkek',
|
|
|
|
#'Asia/Brunei',
|
|
|
|
#'Asia/Choibalsan',
|
|
|
|
#'Asia/Chongqing',
|
|
|
|
#'Asia/Colombo',
|
|
|
|
#'Asia/Damascus',
|
|
|
|
#'Asia/Dhaka',
|
|
|
|
#'Asia/Dili',
|
|
|
|
#'Asia/Dubai',
|
|
|
|
#'Asia/Dushanbe',
|
|
|
|
#'Asia/Gaza',
|
|
|
|
#'Asia/Harbin',
|
|
|
|
#'Asia/Hebron',
|
|
|
|
#'Asia/Ho_Chi_Minh',
|
|
|
|
'Asia/Hong_Kong',
|
|
|
|
#'Asia/Hovd',
|
|
|
|
#'Asia/Irkutsk',
|
|
|
|
#'Asia/Jakarta',
|
|
|
|
#'Asia/Jayapura',
|
|
|
|
#'Asia/Jerusalem',
|
|
|
|
#'Asia/Kabul',
|
|
|
|
#'Asia/Kamchatka',
|
|
|
|
#'Asia/Karachi',
|
|
|
|
#'Asia/Kashgar',
|
|
|
|
#'Asia/Kathmandu',
|
|
|
|
#'Asia/Khandyga',
|
|
|
|
#'Asia/Kolkata',
|
|
|
|
#'Asia/Krasnoyarsk',
|
|
|
|
#'Asia/Kuala_Lumpur',
|
|
|
|
#'Asia/Kuching',
|
|
|
|
#'Asia/Kuwait',
|
|
|
|
#'Asia/Macau',
|
|
|
|
#'Asia/Magadan',
|
|
|
|
#'Asia/Makassar',
|
|
|
|
#'Asia/Manila',
|
|
|
|
#'Asia/Muscat',
|
|
|
|
#'Asia/Nicosia',
|
|
|
|
#'Asia/Novokuznetsk',
|
|
|
|
#'Asia/Novosibirsk',
|
|
|
|
#'Asia/Omsk',
|
|
|
|
#'Asia/Oral',
|
|
|
|
#'Asia/Phnom_Penh',
|
|
|
|
#'Asia/Pontianak',
|
|
|
|
#'Asia/Pyongyang',
|
|
|
|
#'Asia/Qatar',
|
|
|
|
#'Asia/Qyzylorda',
|
|
|
|
#'Asia/Rangoon',
|
|
|
|
#'Asia/Riyadh',
|
|
|
|
#'Asia/Sakhalin',
|
|
|
|
#'Asia/Samarkand',
|
|
|
|
#'Asia/Seoul',
|
|
|
|
#'Asia/Shanghai',
|
|
|
|
#'Asia/Singapore',
|
|
|
|
#'Asia/Taipei',
|
|
|
|
#'Asia/Tashkent',
|
|
|
|
#'Asia/Tbilisi',
|
|
|
|
#'Asia/Tehran',
|
|
|
|
#'Asia/Thimphu',
|
|
|
|
#'Asia/Tokyo',
|
|
|
|
#'Asia/Ulaanbaatar',
|
|
|
|
#'Asia/Urumqi',
|
|
|
|
#'Asia/Ust-Nera',
|
|
|
|
#'Asia/Vientiane',
|
|
|
|
#'Asia/Vladivostok',
|
|
|
|
#'Asia/Yakutsk',
|
|
|
|
#'Asia/Yekaterinburg',
|
|
|
|
#'Asia/Yerevan',
|
|
|
|
#'Atlantic/Azores',
|
|
|
|
#'Atlantic/Bermuda',
|
|
|
|
#'Atlantic/Canary',
|
|
|
|
#'Atlantic/Cape_Verde',
|
|
|
|
#'Atlantic/Faroe',
|
|
|
|
#'Atlantic/Madeira',
|
|
|
|
#'Atlantic/Reykjavik',
|
|
|
|
#'Atlantic/South_Georgia',
|
|
|
|
#'Atlantic/St_Helena',
|
|
|
|
#'Atlantic/Stanley',
|
|
|
|
#'Australia/Adelaide',
|
|
|
|
'Australia/Brisbane',
|
|
|
|
#'Australia/Broken_Hill',
|
|
|
|
#'Australia/Currie',
|
|
|
|
#'Australia/Darwin',
|
|
|
|
#'Australia/Eucla',
|
|
|
|
#'Australia/Hobart',
|
|
|
|
#'Australia/Lindeman',
|
|
|
|
#'Australia/Lord_Howe',
|
|
|
|
#'Australia/Melbourne',
|
|
|
|
'Australia/Perth',
|
|
|
|
'Australia/Sydney',
|
|
|
|
#'CET',
|
|
|
|
#'CST6CDT',
|
|
|
|
#'EET',
|
|
|
|
#'EST',
|
|
|
|
#'EST5EDT',
|
|
|
|
'Europe/Amsterdam',
|
|
|
|
#'Europe/Andorra',
|
|
|
|
#'Europe/Athens',
|
|
|
|
#'Europe/Belgrade',
|
|
|
|
#'Europe/Berlin',
|
|
|
|
#'Europe/Brussels',
|
|
|
|
#'Europe/Bucharest',
|
|
|
|
#'Europe/Budapest',
|
|
|
|
#'Europe/Chisinau',
|
|
|
|
#'Europe/Copenhagen',
|
|
|
|
#'Europe/Dublin',
|
|
|
|
#'Europe/Gibraltar',
|
|
|
|
#'Europe/Helsinki',
|
|
|
|
#'Europe/Istanbul',
|
|
|
|
#'Europe/Kaliningrad',
|
|
|
|
#'Europe/Kiev',
|
|
|
|
#'Europe/Lisbon',
|
|
|
|
'Europe/London',
|
|
|
|
#'Europe/Luxembourg',
|
|
|
|
#'Europe/Madrid',
|
|
|
|
#'Europe/Malta',
|
|
|
|
#'Europe/Minsk',
|
|
|
|
#'Europe/Monaco',
|
|
|
|
#'Europe/Moscow',
|
|
|
|
#'Europe/Oslo',
|
|
|
|
#'Europe/Paris',
|
|
|
|
#'Europe/Prague',
|
|
|
|
#'Europe/Riga',
|
|
|
|
#'Europe/Rome',
|
|
|
|
#'Europe/Samara',
|
|
|
|
#'Europe/Simferopol',
|
|
|
|
#'Europe/Sofia',
|
|
|
|
#'Europe/Stockholm',
|
|
|
|
#'Europe/Tallinn',
|
|
|
|
#'Europe/Tirane',
|
|
|
|
#'Europe/Uzhgorod',
|
|
|
|
#'Europe/Vienna',
|
|
|
|
#'Europe/Vilnius',
|
|
|
|
#'Europe/Volgograd',
|
|
|
|
#'Europe/Warsaw',
|
|
|
|
#'Europe/Zaporozhye',
|
|
|
|
'Europe/Zurich',
|
|
|
|
#'HST',
|
|
|
|
#'Indian/Antananarivo',
|
|
|
|
#'Indian/Chagos',
|
|
|
|
#'Indian/Christmas',
|
|
|
|
#'Indian/Cocos',
|
|
|
|
#'Indian/Comoro',
|
|
|
|
#'Indian/Kerguelen',
|
|
|
|
#'Indian/Mahe',
|
|
|
|
#'Indian/Maldives',
|
|
|
|
#'Indian/Mauritius',
|
|
|
|
#'Indian/Mayotte',
|
|
|
|
#'Indian/Reunion',
|
|
|
|
#'MET',
|
|
|
|
#'MST',
|
|
|
|
#'MST7MDT',
|
|
|
|
#'PST8PDT',
|
|
|
|
#'Pacific/Apia',
|
|
|
|
'Pacific/Auckland',
|
|
|
|
#'Pacific/Chatham',
|
|
|
|
#'Pacific/Chuuk',
|
|
|
|
#'Pacific/Easter',
|
|
|
|
#'Pacific/Efate',
|
|
|
|
#'Pacific/Enderbury',
|
|
|
|
#'Pacific/Fakaofo',
|
|
|
|
#'Pacific/Fiji',
|
|
|
|
#'Pacific/Funafuti',
|
|
|
|
#'Pacific/Galapagos',
|
|
|
|
#'Pacific/Gambier',
|
|
|
|
#'Pacific/Guadalcanal',
|
|
|
|
#'Pacific/Guam',
|
|
|
|
#'Pacific/Honolulu',
|
|
|
|
#'Pacific/Kiritimati',
|
|
|
|
#'Pacific/Kosrae',
|
|
|
|
#'Pacific/Kwajalein',
|
|
|
|
#'Pacific/Majuro',
|
|
|
|
#'Pacific/Marquesas',
|
|
|
|
#'Pacific/Midway',
|
|
|
|
#'Pacific/Nauru',
|
|
|
|
#'Pacific/Niue',
|
|
|
|
#'Pacific/Norfolk',
|
|
|
|
#'Pacific/Noumea',
|
|
|
|
#'Pacific/Pago_Pago',
|
|
|
|
#'Pacific/Palau',
|
|
|
|
#'Pacific/Pitcairn',
|
|
|
|
#'Pacific/Pohnpei',
|
|
|
|
#'Pacific/Port_Moresby',
|
|
|
|
#'Pacific/Rarotonga',
|
|
|
|
#'Pacific/Saipan',
|
|
|
|
#'Pacific/Tahiti',
|
|
|
|
#'Pacific/Tarawa',
|
|
|
|
#'Pacific/Tongatapu',
|
|
|
|
#'Pacific/Wake',
|
|
|
|
#'Pacific/Wallis',
|
|
|
|
'UTC',
|
|
|
|
#'WET',
|
|
|
|
#}}}
|
|
|
|
);
|
|
|
|
|
|
|
|
#
|
|
|
|
# Defaults for options
|
|
|
|
#
|
|
|
|
my $DEF_DEBUG = 0;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Options and arguments
|
|
|
|
#
|
|
|
|
my %options;
|
|
|
|
Options( \%options );
|
|
|
|
|
|
|
|
#
|
|
|
|
# Default help
|
|
|
|
#
|
|
|
|
pod2usage( -msg => "$PROG version $VERSION\n", -verbose => 0, -exitval => 1 )
|
|
|
|
if ( $options{'help'} );
|
|
|
|
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
# Full documentation if requested with -doc[umentation] or -man
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
|
|
|
pod2usage(
|
|
|
|
-msg => "$PROG version $VERSION\n",
|
|
|
|
-verbose => 2,
|
|
|
|
-exitval => 1,
|
|
|
|
) if ( $options{'documentation'} );
|
|
|
|
|
|
|
|
#
|
|
|
|
# Collect options
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
my $DEBUG = ( defined( $options{debug} ) ? $options{debug} : $DEF_DEBUG );
|
|
|
|
my $month = $options{month};
|
|
|
|
my $date = $options{date};
|
|
|
|
my $start = $options{starttime};
|
|
|
|
my $end = $options{endtime};
|
|
|
|
my $outfile = $options{output};
|
2025-04-03 16:57:57 +01:00
|
|
|
my $force = ( defined( $options{force} ) ? $options{force} : 0 );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
my $cfgfile
|
2025-03-31 21:59:14 +01:00
|
|
|
= ( defined( $options{config} ) ? $options{config} : $configfile );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Sanity checking the options
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
die "Unable to find config file '$cfgfile'\n" unless ( -e $cfgfile );
|
2024-06-04 16:35:44 +01:00
|
|
|
die "Use only one of -month=MONTH or -date=DATE\n"
|
|
|
|
if (defined($month) && defined($date));
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
2025-03-31 21:59:14 +01:00
|
|
|
# Load configuration data
|
2024-06-04 16:35:44 +01:00
|
|
|
#-------------------------------------------------------------------------------
|
2025-03-31 21:59:14 +01:00
|
|
|
my $conf = Config::General->new(
|
2024-06-04 16:35:44 +01:00
|
|
|
-ConfigFile => $cfgfile,
|
|
|
|
-InterPolateVars => 1,
|
|
|
|
-ExtendedAccess => 1
|
|
|
|
);
|
|
|
|
my %config = $conf->getall();
|
2025-03-31 21:59:14 +01:00
|
|
|
_debug( $DEBUG >= 2, '%config: ' . Dumper( \%config ) );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
# Configuration file values for the email text with defaults and/or checks
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
my $cfg_server = $config{email}->{server} // 'chatter.skyehaven.net';
|
|
|
|
my $cfg_port = $config{email}->{port} // 64738;
|
|
|
|
my $cfg_room = $config{email}->{room} // 'Hacker Public Radio';
|
|
|
|
my $cfg_duration = $config{email}->{duration} // 2; # Hours
|
|
|
|
my $cfg_dayname = $config{email}->{dayname} // 'Friday';
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# If we had a start time specified then check it and ensure the end time makes
|
|
|
|
# sense. Otherwise use the configuration file defaults, but don't check them.
|
|
|
|
#
|
|
|
|
if ($start) {
|
|
|
|
#
|
|
|
|
# Check start time and add a seconds field if needed
|
|
|
|
#
|
|
|
|
$start = validate_time($start);
|
|
|
|
|
|
|
|
#
|
|
|
|
# The end time usually needs to be 2 hours from the start if not
|
|
|
|
# specified. The actual duration is specified in the configuration file
|
|
|
|
# (with a default above).
|
|
|
|
#
|
|
|
|
unless ($end) {
|
|
|
|
my @end = split( ':', $start );
|
2025-04-07 21:03:37 +01:00
|
|
|
$end[0] += $cfg_duration;
|
2024-06-04 16:35:44 +01:00
|
|
|
$end = join(':',@end);
|
|
|
|
}
|
|
|
|
|
|
|
|
#
|
|
|
|
# Check and add a seconds field if needed
|
|
|
|
#
|
|
|
|
$end = validate_time($end);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
$start = $config{email}->{starttime};
|
|
|
|
$end = $config{email}->{endtime};
|
|
|
|
}
|
|
|
|
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
# Recording start and end times from options or the configuration file
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
|
|
|
my @starttime = split( ':', $start );
|
|
|
|
my @endtime = split( ':', $end );
|
|
|
|
die "Missing start/end time(s)\n" unless ( @starttime && @endtime );
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
#
|
|
|
|
# The template from the configuration file
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
my $cfg_template = $config{email}->{template};
|
|
|
|
die "Missing template file '$cfg_template'\n" unless (-e $cfg_template);
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
#
|
|
|
|
# Recording date cache filename in the configuration file
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
my $cfg_recdatefile = $config{recdates}->{name};
|
|
|
|
unless ($cfg_recdatefile) {
|
2025-03-31 21:59:14 +01:00
|
|
|
warn "No recording date file defined in configuration";
|
|
|
|
say STDERR "Continuing without this file";
|
|
|
|
}
|
2025-04-07 21:03:37 +01:00
|
|
|
elsif ( ! -e $cfg_recdatefile) {
|
|
|
|
warn "Can't find recording date file '$cfg_recdatefile'";
|
2025-03-31 21:59:14 +01:00
|
|
|
say STDERR "Continuing without this file";
|
2025-04-07 21:03:37 +01:00
|
|
|
$cfg_recdatefile = undef;
|
2025-03-31 21:59:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#
|
|
|
|
# Load the recording dates
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
if ($cfg_recdatefile) {
|
|
|
|
%recdates = load_cache($cfg_recdatefile, $rdfh);
|
2025-03-31 21:59:14 +01:00
|
|
|
}
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
_debug($DEBUG >= 2,
|
|
|
|
'$start: ' . coalesce($start,''),
|
|
|
|
'$end: ' . coalesce($end,''),
|
|
|
|
'--'
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($DEBUG >= 1) {
|
|
|
|
report_settings();
|
|
|
|
}
|
|
|
|
|
|
|
|
#-------------------------------------------------------------------------------
|
2025-04-07 21:03:37 +01:00
|
|
|
# Use -date=DATE, -month=DATE or default to today's date to compute or collect
|
|
|
|
# the review month and the recording date. Use Date::Calc format.
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
# If there's a -date=DATE option then it'll be an override for the recording
|
|
|
|
# date otherwise we'll compute it.
|
|
|
|
#-------------------------------------------------------------------------------
|
2024-06-04 16:35:44 +01:00
|
|
|
my @today = Today();
|
2025-04-07 21:03:37 +01:00
|
|
|
my @recordingdate;
|
|
|
|
my @reviewmonth;
|
2025-03-31 21:59:14 +01:00
|
|
|
my $monday = 1; # Day of week number 1-7, Monday-Sunday
|
2025-04-07 21:03:37 +01:00
|
|
|
my $offset = day_offset($cfg_dayname)->{offset};
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2024-06-04 16:35:44 +01:00
|
|
|
if ( defined($date) ) {
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
# Parse and perform rudimentary validation on the argument. Don't change
|
|
|
|
# the day in the date. Allow past dates it -force is on.
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
@recordingdate = convert_date( $date, 0, $force );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
#
|
|
|
|
# Determine the review month
|
|
|
|
#
|
|
|
|
if (week_of_month(@recordingdate) >= 4) {
|
|
|
|
#
|
|
|
|
# Use the month of the date
|
|
|
|
#
|
|
|
|
@reviewmonth = ((@recordingdate)[0,1],1);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
#
|
|
|
|
# Go backwards a month from the month of the recording date
|
|
|
|
#
|
|
|
|
@reviewmonth = Add_Delta_YM((@recordingdate)[0,1],1,0,-1);
|
|
|
|
}
|
2024-06-04 16:35:44 +01:00
|
|
|
}
|
2025-04-07 21:03:37 +01:00
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2024-06-04 16:35:44 +01:00
|
|
|
elsif ( defined($month) ) {
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
# Parse the month out of the -month=DATE option. The day is forced to 1,
|
|
|
|
# but cannot be omitted from the option. This defines the review month.
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
@reviewmonth = convert_date( $month, 1, $force );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Compute the next meeting date from now (by finding the next first Monday
|
2025-03-31 21:59:14 +01:00
|
|
|
# of the month then backing up two days to the desired day - default
|
|
|
|
# Friday).
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
my @nextmonth = Add_Delta_YM(@reviewmonth,0,1);
|
|
|
|
@recordingdate = make_recording_date( \@nextmonth, $monday, 1, $offset );
|
2024-06-04 16:35:44 +01:00
|
|
|
}
|
2025-04-07 21:03:37 +01:00
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
2024-06-04 16:35:44 +01:00
|
|
|
else {
|
2025-04-07 21:03:37 +01:00
|
|
|
#
|
|
|
|
# This is equivalent to the -month=DATE option except we use the current
|
|
|
|
# month which we get from @today and save it as @reviewmonth. We add
|
|
|
|
# a month to this to get the next month.
|
|
|
|
#
|
|
|
|
@reviewmonth = ((@today)[0,1],1);
|
|
|
|
my @nextmonth = Add_Delta_YM(@reviewmonth,0,1);
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
|
|
|
# Compute the next meeting date from now (by finding the next first Monday
|
|
|
|
# of the month then backing up a number of days to the required date).
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
@recordingdate = make_recording_date( \@nextmonth, $monday, 1, $offset );
|
2024-06-04 16:35:44 +01:00
|
|
|
}
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
_debug($DEBUG >= 2, '$offset: ' . $offset);
|
|
|
|
_debug($DEBUG >= 2, '@recordingdate: ' . join(',',@recordingdate));
|
|
|
|
_debug($DEBUG >= 2, '@reviewmonth: ' . join(',',@reviewmonth));
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
#-------------------------------------------------------------------------------
|
2025-04-03 16:57:57 +01:00
|
|
|
# Transfer Date::Calc values into DateTime objects so
|
|
|
|
# we can get better formatting.
|
2025-04-07 21:03:37 +01:00
|
|
|
#-------------------------------------------------------------------------------
|
2025-04-03 16:57:57 +01:00
|
|
|
my ( $dtrevmonth, $dtstart, $dtend );
|
2025-04-07 21:03:37 +01:00
|
|
|
$dtrevmonth = dc_to_dt( \@reviewmonth );
|
|
|
|
$dtstart = dc_to_dt( [ @recordingdate, @starttime ] );
|
|
|
|
$dtend = dc_to_dt( [ @recordingdate, @endtime ] );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Compute the number of days until the recording
|
|
|
|
#
|
|
|
|
my $dtnow = DateTime->now( time_zone => 'UTC' );
|
|
|
|
my $dtoffset = $dtstart->delta_days($dtnow);
|
|
|
|
my $dtf = DateTime::Format::Duration->new( pattern => '%e' );
|
|
|
|
my $days = $dtf->format_duration($dtoffset);
|
|
|
|
|
|
|
|
#
|
2025-04-03 16:57:57 +01:00
|
|
|
# Formatted datetime-related values for the mail message body
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-04-03 16:57:57 +01:00
|
|
|
my ( $revyear, $revmonthno, $revmonthname ) = (
|
|
|
|
$dtrevmonth->year,
|
|
|
|
$dtrevmonth->month,
|
|
|
|
$dtrevmonth->month_name
|
|
|
|
);
|
|
|
|
|
|
|
|
my ( $year, $monthno, $monthname ) = (
|
|
|
|
$dtstart->year,
|
|
|
|
$dtstart->month,
|
|
|
|
$dtstart->month_name
|
|
|
|
);
|
|
|
|
|
|
|
|
my ( $nicedate, $starttime, $endtime ) = (
|
|
|
|
$dtstart->strftime("%A, %B %d %Y"),
|
|
|
|
$dtstart->strftime("%R (%Z)"),
|
|
|
|
$dtend->strftime("%R (%Z)"),
|
2024-06-04 16:35:44 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
_debug($DEBUG >= 2,
|
2025-04-03 16:57:57 +01:00
|
|
|
"----",
|
|
|
|
"\$revyear: $revyear",
|
|
|
|
"\$revmonthno: $revmonthno",
|
|
|
|
"\$revmonthname: $revmonthname",
|
2024-06-04 16:35:44 +01:00
|
|
|
"\$year: $year",
|
2025-03-31 21:59:14 +01:00
|
|
|
"\$monthno: $monthno",
|
2024-06-04 16:35:44 +01:00
|
|
|
"\$monthname: $monthname",
|
|
|
|
"\$nicedate: $nicedate",
|
|
|
|
"\$starttime: $starttime",
|
2025-04-07 21:03:37 +01:00
|
|
|
"\$endtime: $endtime",
|
|
|
|
"ISO date: ".$dtstart->ymd
|
2024-06-04 16:35:44 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
#
|
2025-04-03 16:57:57 +01:00
|
|
|
# Build the subject line (with the recording date)
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
my $waittime = ( $days > 6 ? "in $days days" : "next %A" );
|
|
|
|
my $next = ( $days > 6 ? '' : 'next ' );
|
|
|
|
my $forspec = ( $days > 6 ? "for $revmonthname " : "" );
|
2024-06-04 16:35:44 +01:00
|
|
|
my $subject = $dtstart->strftime(
|
|
|
|
"HPR Community News ${forspec}- $waittime on %FT%TZ");
|
|
|
|
|
|
|
|
_debug( $DEBUG >= 2, "\$subject: $subject" );
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
# Open the output file (or STDOUT) - we may need the year and month number to
|
|
|
|
# do it, if the file name contains '%s'.
|
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
my $outfh;
|
|
|
|
if ($outfile) {
|
2025-04-03 16:57:57 +01:00
|
|
|
$outfile = sprintf( $outfile, sprintf( "%d-%02d", $revyear, $revmonthno ) )
|
2025-03-31 21:59:14 +01:00
|
|
|
if ( $outfile =~ /%s/ );
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
open( $outfh, ">:encoding(UTF-8)", $outfile )
|
|
|
|
or die "Unable to open $outfile for writing: $!\n";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
open( $outfh, ">&", \*STDOUT )
|
|
|
|
or die "Unable to initialise for writing: $!\n";
|
|
|
|
}
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Build an array of timezone data for the template
|
|
|
|
#
|
|
|
|
my @timezones;
|
|
|
|
for my $tz (@zones) {
|
2025-03-31 21:59:14 +01:00
|
|
|
push( @timezones, storeTZ( $dtstart, $dtend, $tz ) );
|
2024-06-04 16:35:44 +01:00
|
|
|
}
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
# Update the date cache now we have the date and time details we need.
|
|
|
|
#-------------------------------------------------------------------------------
|
2025-04-03 16:57:57 +01:00
|
|
|
( my $monthkey = $dtrevmonth->ymd ) =~ s/\d+$/01/;
|
2025-03-31 21:59:14 +01:00
|
|
|
my $datestamp = $dtstart->strftime("%F %T");
|
|
|
|
|
|
|
|
if (exists($recdates{$monthkey})) {
|
|
|
|
#
|
|
|
|
# It exists. Is it different?
|
|
|
|
#
|
|
|
|
unless ( $recdates{$monthkey} eq $datestamp ) {
|
|
|
|
#
|
|
|
|
# Save the new data (assuming it's correct)
|
|
|
|
#
|
|
|
|
$recdates{$monthkey} = $datestamp;
|
2025-04-07 21:03:37 +01:00
|
|
|
update_cache( $cfg_recdatefile, \%recdates );
|
2025-03-31 21:59:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
#
|
|
|
|
# Add a new record to the end of the cache file
|
|
|
|
#
|
2025-04-07 21:03:37 +01:00
|
|
|
append_cache( $cfg_recdatefile, sprintf( "%s,%s", $monthkey, $datestamp ) );
|
2025-03-31 21:59:14 +01:00
|
|
|
}
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
# Fill the template
|
|
|
|
#-------------------------------------------------------------------------------
|
|
|
|
my $tt = Template->new(
|
|
|
|
{ ABSOLUTE => 1,
|
|
|
|
ENCODING => 'utf8',
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
my $vars = {
|
2025-04-07 21:03:37 +01:00
|
|
|
server => $cfg_server,
|
|
|
|
port => $cfg_port,
|
|
|
|
room => $cfg_room,
|
2025-03-31 21:59:14 +01:00
|
|
|
subject => $subject,
|
2024-06-04 16:35:44 +01:00
|
|
|
timezones => \@timezones,
|
|
|
|
utc => {
|
2025-04-03 16:57:57 +01:00
|
|
|
days => $days,
|
|
|
|
revmonth => $revmonthname,
|
|
|
|
revyear => $revyear,
|
|
|
|
month => $monthname,
|
|
|
|
year => $year,
|
|
|
|
date => $nicedate,
|
|
|
|
start => $starttime,
|
|
|
|
end => $endtime,
|
2024-06-04 16:35:44 +01:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
my $document;
|
2025-04-07 21:03:37 +01:00
|
|
|
$tt->process( $cfg_template,
|
2024-06-04 16:35:44 +01:00
|
|
|
$vars, \$document, { binmode => ':utf8' } )
|
|
|
|
|| die $tt->error(), "\n";
|
|
|
|
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
# Write to the output file
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
print $outfh $document;
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
# Report the output file name if there is one
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
if ($outfile) {
|
|
|
|
say "Output is in $outfile";
|
2024-06-04 16:35:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
exit;
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: load_cache
|
|
|
|
# PURPOSE: Load the date cache into a hash
|
|
|
|
# PARAMETERS: $cache_name Name of file holding the cache
|
|
|
|
# RETURNS: Contents of cache as a hash
|
|
|
|
# DESCRIPTION: Opens the nominated file, parses each record, and adds the
|
|
|
|
# data to a hash. The record should contain the following:
|
|
|
|
# * 'YYYY-MM-01' the month for which the details are being
|
|
|
|
# recorded
|
|
|
|
# * ',' comma field separator
|
|
|
|
# * 'YYYY-MM-DD HH:MM:SS' timestamp of the recording
|
|
|
|
# The file is closed once it has been scanned. The function
|
|
|
|
# returns the completed hash.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub load_cache {
|
|
|
|
my ($cache_name) = @_;
|
|
|
|
|
|
|
|
my ( $month, $datetime, %result );
|
|
|
|
|
|
|
|
#
|
|
|
|
# Open the file in read mode
|
|
|
|
#
|
|
|
|
open( my $dcfh, '<', $cache_name )
|
|
|
|
or die "$PROG: failed to open '$cache_name': $!\n";
|
|
|
|
|
|
|
|
while ( my $line = <$dcfh> ) {
|
|
|
|
chomp($line);
|
|
|
|
if ( ( $month, $datetime )
|
|
|
|
= ( $line =~ /^(\d{4}-\d{2}-\d{2}),(.*)$/ ) )
|
|
|
|
{
|
|
|
|
$result{$month} = $datetime;
|
|
|
|
}
|
|
|
|
# TODO: Report any errors found in the file
|
|
|
|
}
|
|
|
|
|
|
|
|
close($dcfh)
|
|
|
|
or warn "$PROG: failed to close '$cache_name': $!\n";
|
|
|
|
|
|
|
|
return %result;
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: append_cache
|
|
|
|
# PURPOSE: Append a new line to the cache
|
|
|
|
# PARAMETERS: $cache_name Name of file holding the cache
|
|
|
|
# $line New record to add
|
|
|
|
# RETURNS: Nothing
|
|
|
|
# DESCRIPTION: Opens the nominated file and appends the new record in $line.
|
|
|
|
# The file is closed once it has been updated.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub append_cache {
|
|
|
|
my ( $cache_name, $line ) = @_;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Open the file in append mode
|
|
|
|
#
|
|
|
|
open( my $dcfh, '>>', $cache_name )
|
|
|
|
or die "$PROG: failed to open '$cache_name': $!\n";
|
|
|
|
|
|
|
|
say $dcfh $line;
|
|
|
|
|
|
|
|
close($dcfh)
|
|
|
|
or warn "$PROG: failed to close '$cache_name': $!\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: update_cache
|
|
|
|
# PURPOSE: Make changes to an existing line in the cache
|
|
|
|
# PARAMETERS: $cache_name Name of file holding the cache
|
|
|
|
# $rhash Hashref holding the updated cache contents
|
|
|
|
# RETURNS: Nothing
|
|
|
|
# DESCRIPTION: Makes a backup of the nominated file. Opens, truncates it and
|
|
|
|
# positions for writing (using 'seek'). The now empty file is
|
|
|
|
# filled with data from the hash and closed.
|
|
|
|
# THROWS: No exceptions
|
2025-04-03 16:57:57 +01:00
|
|
|
# COMMENTS: Uses 'copy' from File::Copy
|
2025-03-31 21:59:14 +01:00
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub update_cache {
|
|
|
|
my ( $cache_name, $rhash ) = @_;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Copy the cache file to a backup
|
|
|
|
#
|
|
|
|
copy($cache_name,"${cache_name}~")
|
2025-04-07 21:03:37 +01:00
|
|
|
or die "Unable to back up '$cache_name'\n";
|
2025-03-31 21:59:14 +01:00
|
|
|
|
|
|
|
#
|
|
|
|
# Open the original file in write mode
|
|
|
|
#
|
|
|
|
open( my $dcfh, '>', $cache_name )
|
|
|
|
or die "$PROG: failed to open '$cache_name': $!\n";
|
|
|
|
|
|
|
|
#
|
|
|
|
# Truncate the file and seek to the start again
|
|
|
|
#
|
|
|
|
truncate($dcfh,0)
|
|
|
|
or die "$PROG: failed to truncate '$cache_name': $!\n";
|
|
|
|
seek($dcfh,0,0)
|
|
|
|
or die "$PROG: failed to seek in '$cache_name': $!\n";
|
|
|
|
|
|
|
|
#
|
|
|
|
# Write the cache data to the file
|
|
|
|
#
|
|
|
|
for my $key (sort(keys(%$rhash))) {
|
|
|
|
say $dcfh sprintf("%s,%s",$key, $rhash->{$key});
|
|
|
|
}
|
|
|
|
|
|
|
|
close($dcfh)
|
|
|
|
or warn "$PROG: failed to close '$cache_name': $!\n";
|
|
|
|
}
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: report_settings
|
|
|
|
# PURPOSE: Report settings from options (or defaults)
|
|
|
|
# PARAMETERS: None
|
|
|
|
# RETURNS: Nothing
|
|
|
|
# DESCRIPTION:
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub report_settings {
|
|
|
|
my $fmt = "D> %-14s = %s\n";
|
|
|
|
print "D> Settings from options or default values:\n";
|
|
|
|
printf $fmt, "Month", coalesce($month,'undef');
|
|
|
|
printf $fmt, "Meeting date", coalesce($date,'undef');
|
|
|
|
printf $fmt, "Start time", join(':',@starttime);
|
|
|
|
printf $fmt, "End time", join(':',@endtime);
|
|
|
|
printf $fmt, "Config file", coalesce($cfgfile,'undef');
|
2025-04-07 21:03:37 +01:00
|
|
|
printf $fmt, "Server", coalesce($cfg_server,'undef');
|
|
|
|
printf $fmt, "Port", coalesce($cfg_port,'undef');
|
|
|
|
printf $fmt, "Room", coalesce($cfg_room,'undef');
|
|
|
|
printf $fmt, "Template", coalesce($cfg_template,'undef');
|
|
|
|
printf $fmt, "Recording date file", coalesce($cfg_recdatefile,'undef');
|
2024-06-04 16:35:44 +01:00
|
|
|
print "D> ----\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: validate_time
|
|
|
|
# PURPOSE: Validates a time in HH:MM:SS format
|
|
|
|
# PARAMETERS: $time Time string
|
|
|
|
# RETURNS: The input string with any missing fields added in
|
|
|
|
# DESCRIPTION: The input time needs to be in the format HH:MM[:SS] where
|
|
|
|
# a missing seconds value is replaced with '00'. A regex check
|
|
|
|
# on the format is performed, and if that passes a check is made
|
|
|
|
# on the time values (using 'check_time').
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub validate_time {
|
|
|
|
my ($time) = (@_);
|
|
|
|
|
|
|
|
if ( defined($time) ) {
|
|
|
|
if ( ( my @fields )
|
|
|
|
= ( $time =~ /(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?/ ) )
|
|
|
|
{
|
|
|
|
@fields = map { defined($_) ? sprintf('%02d',$_) : "00" } @fields;
|
|
|
|
$time = join( ':', @fields );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
die "Invalid time: $time\n";
|
|
|
|
}
|
|
|
|
|
|
|
|
unless ( check_time(split(':',$time)) ) {
|
|
|
|
die "Invalid time: $time\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $time;
|
|
|
|
}
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: valid_month
|
|
|
|
# PURPOSE: Determines if the requested month is valid, meaning the
|
|
|
|
# current one or a future one
|
|
|
|
# PARAMETERS: $refmonth Arrayref holding the requested month
|
|
|
|
# RETURNS: True if a past month, otherwise false
|
|
|
|
# DESCRIPTION: The day of the requested month is forced to 1. Four Julian
|
|
|
|
# dates are computed: the start of the requested month, the end of the
|
|
|
|
# requested month, the current day and the start of the month
|
|
|
|
# after this one. The month is a current or future
|
|
|
|
# one if today's date is greater than the start of the month, or
|
|
|
|
# the end of the month.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub valid_month {
|
|
|
|
my ($refmonth) = @_;
|
|
|
|
|
|
|
|
my @month = @$refmonth;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Force the date to first of the month
|
|
|
|
#
|
|
|
|
$month[2] = 1;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Make Julian dates:
|
|
|
|
# $jstartmonth - start of the requested month
|
|
|
|
# $jendmonth - end of the requested month
|
|
|
|
# $jtoday - today
|
|
|
|
# $jstartnextmonth - the end of the current month plus 1 day
|
|
|
|
#
|
|
|
|
my $jstartmonth = Date_to_Days(@month);
|
|
|
|
my $jendmonth = Date_to_Days(
|
|
|
|
Add_Delta_Days( @month, Days_in_Month( @month[ 0, 1 ] ) )
|
|
|
|
);
|
|
|
|
my $jtoday = Date_to_Days( Today() );
|
|
|
|
my $jstartnextmonth = Date_to_Days(
|
|
|
|
Add_Delta_Days(
|
|
|
|
( ( Today() )[ 0, 1 ], 1 ), # Start of this month
|
|
|
|
Days_in_Month( ( Today() )[ 0, 1 ] ) # Days this month
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
_debug($DEBUG >= 2,
|
|
|
|
"\$jstartmonth: $jstartmonth",
|
|
|
|
"\$jendmonth: $jendmonth",
|
|
|
|
"\$jtoday: $jtoday",
|
|
|
|
"\$jstartnextmonth $jstartnextmonth",
|
|
|
|
"----"
|
|
|
|
);
|
|
|
|
|
|
|
|
#
|
|
|
|
# It's valid if this month or a later one
|
|
|
|
#
|
|
|
|
return ( $jtoday >= $jstartmonth && $jtoday <= $jendmonth ) ||
|
|
|
|
($jstartmonth >= $jstartnextmonth);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2025-04-03 16:57:57 +01:00
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: dc_to_dt
|
|
|
|
# PURPOSE: Converts a Date::Calc datetime into a DateTime equivalent
|
|
|
|
# PARAMETERS: $refdt Reference to an array holding a Date::Calc
|
|
|
|
# date and time
|
|
|
|
# RETURNS: Returns a DateTime object converted from the input
|
|
|
|
# DESCRIPTION: Takes an arrayref which is expected to have a Date::Calc date
|
|
|
|
# and time. If the referenced array is too short it has three
|
|
|
|
# zero elements added to it and is checked again, aborting if
|
|
|
|
# it's still the wrong length.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub dc_to_dt {
|
|
|
|
my ($refdt) = @_;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Copy the incoming arrayref into an array to avoid writing data back
|
|
|
|
#
|
|
|
|
my @dt = @$refdt;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Check we got a 6-element array and add a default time if not
|
|
|
|
#
|
|
|
|
if (scalar(@dt) < 6) {
|
|
|
|
push(@dt,0,0,0);
|
|
|
|
}
|
|
|
|
|
|
|
|
#
|
|
|
|
# Should be 6 elements now
|
|
|
|
#
|
|
|
|
die "Invalid Date::Calc date and time (@dt) in dc_to_dt\n"
|
|
|
|
unless (scalar(@dt) == 6);
|
|
|
|
|
|
|
|
#
|
|
|
|
# Convert to DateTime to get access to formatting stuff, default to UTC.
|
|
|
|
# (Note: %dtargs is a hash and we're using hash slicing to initialise it).
|
|
|
|
#
|
|
|
|
my ( %dtargs, $dt );
|
|
|
|
@dtargs{ 'year', 'month', 'day', 'hour', 'minute', 'second', 'time_zone' }
|
|
|
|
= ( @dt, 'UTC' );
|
|
|
|
$dt = DateTime->new(%dtargs);
|
|
|
|
|
|
|
|
#
|
|
|
|
# Return the date and time as a DateTime object
|
|
|
|
#
|
|
|
|
return $dt;
|
|
|
|
}
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
#=== FUNCTION ================================================================
|
2025-04-07 21:03:37 +01:00
|
|
|
# NAME: week_of_month
|
|
|
|
# PURPOSE: Determine the week number of the month a date is in
|
|
|
|
# PARAMETERS: $year, $month, $day fields of a Date::Calc date
|
|
|
|
# RETURNS: The computed week number within the month, in the range 1-5
|
|
|
|
# DESCRIPTION: This is the algorithm provided in example 5 of the RECIPES
|
|
|
|
# section of the Date::Calc manpage. It uses the incoming day
|
|
|
|
# number, added to the day number of the first day of the month,
|
|
|
|
# subtracts 2 and divides by 7 then adds 1 to the result.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub week_of_month {
|
|
|
|
my ( $year, $month, $day ) = @_;
|
|
|
|
|
|
|
|
return int( ( $day + Day_of_Week( $year, $month, 1 ) - 2 ) / 7 ) + 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: convert_date
|
|
|
|
# PURPOSE: Convert a textual date (ideally YYYY-MM-DD) to a Date::Calc
|
|
|
|
# date for the start of the given month.
|
|
|
|
# PARAMETERS: $textdate date in text form
|
|
|
|
# $ismonth Boolean controlling whether the date is to be
|
|
|
|
# forced to the start of the month or not.
|
|
|
|
# $force Boolean defining whether to skip validating
|
|
|
|
# the date
|
|
|
|
# RETURNS: The date or the start of the month from the textual date
|
|
|
|
# ($textdate) in Date::Calc format
|
|
|
|
# DESCRIPTION: Parses the date string and makes a Date::Calc date from the
|
|
|
|
# result making the day part is 1 if $override is true.
|
|
|
|
# Optionally checks that the date isn't in the past, though
|
|
|
|
# $force = 1 ignores this check.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: Requires Date::Calc and Date::Parse
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub convert_date {
|
|
|
|
my ( $textdate, $ismonth, $allow_past ) = @_;
|
|
|
|
|
|
|
|
my ( @today, @parsed, @dcdate );
|
|
|
|
|
|
|
|
#
|
|
|
|
# Reference date
|
|
|
|
#
|
|
|
|
@today = Today();
|
|
|
|
|
|
|
|
#
|
|
|
|
# Parse and perform rudimentary validation on the $textdate date.
|
|
|
|
# Using Date::Parse routine 'strptime' which returns:
|
|
|
|
# ($ss,$mm,$hh,$day,$month,$year,$zone) = strptime($date)
|
|
|
|
# 0 1 2 3 4 5 6
|
|
|
|
#
|
|
|
|
# The Date::Calc date $startdate[0] gets the returned year or the current
|
|
|
|
# year if no year was parsed, $startdate[1] gets the parsed month or the
|
|
|
|
# current month if no month was parsed, and $startdate[2] gets a day of 1.
|
|
|
|
#
|
|
|
|
@parsed = strptime($textdate);
|
|
|
|
die "Unable to parse date '$textdate'\n" unless @parsed;
|
|
|
|
|
|
|
|
@dcdate = (
|
|
|
|
( defined( $parsed[5] ) ? $parsed[5] + 1900 : $today[0] ), # year
|
|
|
|
( defined( $parsed[4] ) ? $parsed[4] + 1 : $today[1] ), # month
|
|
|
|
( $ismonth ? 1 : $parsed[3] ) # day
|
|
|
|
);
|
|
|
|
|
|
|
|
#
|
|
|
|
# Unless we've overridden then perform checks:
|
|
|
|
# - if we're handling a month call validmonth
|
|
|
|
# - otherwise there should be a positive or zero difference in days
|
|
|
|
# between the target date and today's date
|
|
|
|
# this is to prevent going backwards in time.
|
|
|
|
#
|
|
|
|
unless ($allow_past) {
|
|
|
|
unless (
|
|
|
|
( $ismonth && ( valid_month( \@dcdate ) ) )
|
|
|
|
||
|
|
|
|
( !$ismonth && ( Date_to_Days(@dcdate) > Date_to_Days(@today) ) )
|
|
|
|
)
|
|
|
|
{
|
|
|
|
warn "convert_date: Invalid date $textdate (in the past)\n";
|
|
|
|
die "Use -force to create a back-dated calendar\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return @dcdate;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: make_recording_date
|
|
|
|
# PURPOSE: Makes a recording date for a given review month
|
2024-06-04 16:35:44 +01:00
|
|
|
# PARAMETERS: $refdate
|
|
|
|
# An arrayref to the reference date array (usually
|
2025-04-07 21:03:37 +01:00
|
|
|
# the start of the target month)
|
2024-06-04 16:35:44 +01:00
|
|
|
# $dow Day of week for the event date (1-7, 1=Monday)
|
|
|
|
# $n The nth day of the week in the given month required
|
|
|
|
# for the event date
|
|
|
|
# $offset Number of days to offset the computed date
|
2025-04-07 21:03:37 +01:00
|
|
|
# RETURNS: The resulting date as a Date::Calc list
|
2024-06-04 16:35:44 +01:00
|
|
|
# DESCRIPTION: We want to compute a simple date with an offset, such as
|
2025-04-07 21:03:37 +01:00
|
|
|
# "the Friday before the first Monday of the month". We do
|
2024-06-04 16:35:44 +01:00
|
|
|
# this by computing a pre-offset date (first Monday of month)
|
2025-04-07 21:03:37 +01:00
|
|
|
# then apply the offset (Friday before).
|
2024-06-04 16:35:44 +01:00
|
|
|
# THROWS: No exceptions
|
2025-04-07 21:03:37 +01:00
|
|
|
# COMMENTS: Derived from make_date, originally written for my HPR episode on
|
|
|
|
# iCalendar. It then got used in reserve_cnews, and was
|
|
|
|
# originally used here. It was really for generating a series of
|
|
|
|
# dates or reserving a seires of episode slots, and wasn't
|
|
|
|
# really suited for this script.
|
|
|
|
# Rather than using a date array reference it might be better to
|
|
|
|
# use a year, month, date sequence like Date::Calc does.
|
|
|
|
# SEE ALSO: N/A
|
2024-06-04 16:35:44 +01:00
|
|
|
#===============================================================================
|
2025-04-07 21:03:37 +01:00
|
|
|
sub make_recording_date {
|
2024-06-04 16:35:44 +01:00
|
|
|
my ( $refdate, $dow, $n, $offset ) = @_;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Compute the required date: the "$n"th day of week "$dow" in the year and
|
|
|
|
# month in @$refdate. This could be a date in the past.
|
|
|
|
#
|
|
|
|
my @date = Nth_Weekday_of_Month_Year( @$refdate[ 0, 1 ], $dow, $n );
|
|
|
|
|
|
|
|
#
|
2025-03-31 21:59:14 +01:00
|
|
|
# Apply any day offset
|
2024-06-04 16:35:44 +01:00
|
|
|
#
|
|
|
|
@date = Add_Delta_Days( @date, $offset ) if $offset;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Return a list
|
|
|
|
#
|
|
|
|
return (@date);
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: storeTZ
|
|
|
|
# PURPOSE: Store start/end times for a timezone
|
|
|
|
# PARAMETERS: $start DateTime object containing the starting
|
|
|
|
# datetime as UTC
|
|
|
|
# $end DateTime object containing the ending datetime
|
|
|
|
# as UTC
|
|
|
|
# $tz The textual time zone (need this to be valid)
|
2025-03-31 21:59:14 +01:00
|
|
|
# RETURNS: Reference to a hash containing the timezone name and start and
|
|
|
|
# end times
|
2024-06-04 16:35:44 +01:00
|
|
|
# DESCRIPTION: Relies on DateTime::TimeZone to do the work to turn a UTC time
|
|
|
|
# into a time in a different time zone. Uses the DateTime
|
|
|
|
# strftime method to format the dates and times for printing.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub storeTZ {
|
|
|
|
my ( $start, $end, $tz ) = @_;
|
|
|
|
|
|
|
|
my %result;
|
|
|
|
|
|
|
|
#
|
|
|
|
# Adjust time zone
|
|
|
|
#
|
|
|
|
$start->set_time_zone($tz);
|
|
|
|
$end->set_time_zone($tz);
|
|
|
|
|
|
|
|
#
|
|
|
|
# Print time zone and start/end times in that zone
|
|
|
|
#
|
|
|
|
$result{name} = "$tz";
|
|
|
|
$result{start} = $start->strftime("%H:%S %a, %b %d %Y");
|
|
|
|
$result{end} = $end->strftime("%H:%S %a, %b %d %Y");
|
|
|
|
|
|
|
|
return \%result;
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: day_offset
|
|
|
|
# PURPOSE: Given a day name computes day attributes including the
|
2025-03-31 21:59:14 +01:00
|
|
|
# (negative) offset in days from the target release day (Monday)
|
|
|
|
# to the recording date.
|
2024-06-04 16:35:44 +01:00
|
|
|
# PARAMETERS: $dayname Name of a day of the week
|
2025-03-31 21:59:14 +01:00
|
|
|
# RETURNS: Hashref containing the full day name (dayname), the weekday
|
|
|
|
# number (wday) and the integer offset (offset) from Monday to
|
|
|
|
# the recording day, or undef.
|
2024-06-04 16:35:44 +01:00
|
|
|
# DESCRIPTION: Uses the hash '%matches' keyed by regular expressions matching
|
2025-04-07 21:03:37 +01:00
|
|
|
# day names. The argument '$cfg_dayname' is matched against each
|
2024-06-04 16:35:44 +01:00
|
|
|
# regex in turn and if it matches the sub-hash is returned. This
|
2025-04-07 21:03:37 +01:00
|
|
|
# allows the caller to use 'day_offset($cfg_dayname)->{dayname}' to
|
2024-06-04 16:35:44 +01:00
|
|
|
# get the full name of the day if needed, or the offset as
|
2025-04-07 21:03:37 +01:00
|
|
|
# 'day_offset($cfg_dayname)->{offset}'. If there is no match
|
2024-06-04 16:35:44 +01:00
|
|
|
# 'undef' is returned.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: None
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub day_offset {
|
|
|
|
my ( $dayname ) = @_;
|
|
|
|
|
|
|
|
my %matches = (
|
|
|
|
qr{^(?i)Sun(day)?$} => {
|
|
|
|
dayname => 'Sunday',
|
|
|
|
wday => 7,
|
|
|
|
offset => -1,
|
|
|
|
},
|
|
|
|
qr{^(?i)Sat(urday)?$} => {
|
|
|
|
dayname => 'Saturday',
|
|
|
|
wday => 6,
|
|
|
|
offset => -2,
|
|
|
|
},
|
|
|
|
qr{^(?i)Fri(day)?$} => {
|
|
|
|
dayname => 'Friday',
|
|
|
|
wday => 5,
|
|
|
|
offset => -3,
|
|
|
|
},
|
|
|
|
qr{^(?i)Thu(rsday)?$} => {
|
|
|
|
dayname => 'Thursday',
|
|
|
|
wday => 4,
|
|
|
|
offset => -4,
|
|
|
|
},
|
|
|
|
qr{^(?i)Wed(nesday)?$} => {
|
|
|
|
dayname => 'Wednesday',
|
|
|
|
wday => 3,
|
|
|
|
offset => -5,
|
|
|
|
},
|
|
|
|
qr{^(?i)Tue(sday)?$} => {
|
|
|
|
dayname => 'Tuesday',
|
|
|
|
wday => 2,
|
|
|
|
offset => -6,
|
|
|
|
},
|
|
|
|
qr{^(?i)Mon(day)?$} => {
|
|
|
|
dayname => 'Monday',
|
|
|
|
wday => 1,
|
|
|
|
offset => -7,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
my $match;
|
|
|
|
|
|
|
|
foreach my $re (keys(%matches)) {
|
|
|
|
if ($dayname =~ $re) {
|
|
|
|
$match = $matches{$re};
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $match;
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== FUNCTION ================================================================
|
|
|
|
# NAME: _debug
|
|
|
|
# PURPOSE: Prints debug reports
|
|
|
|
# PARAMETERS: $active Boolean: 1 for print, 0 for no print
|
|
|
|
# $messages... Arbitrary list of messages to print
|
|
|
|
# RETURNS: Nothing
|
|
|
|
# DESCRIPTION: Outputs messages if $active is true. It removes any trailing
|
|
|
|
# newline from each one and then adds one in the 'print' to the
|
|
|
|
# caller doesn't have to bother. Prepends each message with 'D>'
|
|
|
|
# to show it's a debug message.
|
|
|
|
# THROWS: No exceptions
|
|
|
|
# COMMENTS: Differs from other functions of the same name
|
|
|
|
# SEE ALSO: N/A
|
|
|
|
#===============================================================================
|
|
|
|
sub _debug {
|
|
|
|
my $active = shift;
|
|
|
|
|
|
|
|
my $message;
|
|
|
|
return unless $active;
|
|
|
|
|
|
|
|
while ($message = shift) {
|
|
|
|
chomp($message);
|
|
|
|
print STDERR "D> $message\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#=== 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: 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 = (
|
2025-03-31 21:59:14 +01:00
|
|
|
"help", "documentation|man",
|
|
|
|
"debug=i", "date=s",
|
|
|
|
"starttime=s", "endtime=s",
|
|
|
|
"month=s", "output:s",
|
2025-04-03 16:57:57 +01:00
|
|
|
"config=s", "force!",
|
2024-06-04 16:35:44 +01:00
|
|
|
);
|
2025-03-31 21:59:14 +01:00
|
|
|
# "mail!", "fromaddress=s", "toaddress=s", "duration=s",
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
if ( !GetOptions( $optref, @options ) ) {
|
|
|
|
pod2usage( -msg => "Version $VERSION\n", -verbose => 0, -exitval => 1 );
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
__END__
|
|
|
|
|
|
|
|
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
# Application Documentation
|
|
|
|
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
#{{{
|
|
|
|
|
|
|
|
=head1 NAME
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
make_email - generates the text of an HPR Community News recording invitation
|
|
|
|
email
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
=head1 VERSION
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
This documentation refers to make_email version 0.3.4
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
|
|
|
|
=head1 USAGE
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
make_email [-help] [-documentation|-man]
|
|
|
|
[-month=DATE] [-date=DATE] [-start=START_TIME] [-end=END_TIME]
|
|
|
|
[-[no]force] [-config=FILE] [-debug=N]
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
=head1 OPTIONS
|
|
|
|
|
|
|
|
=over 8
|
|
|
|
|
|
|
|
=item B<-help>
|
|
|
|
|
|
|
|
Prints a brief help message describing the usage of the program, and then exits.
|
|
|
|
|
|
|
|
=item B<-documentation> B<-man>
|
|
|
|
|
|
|
|
Prints the entire embedded documentation for the program, then exits.
|
|
|
|
|
|
|
|
Another way to see the full documentation use:
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
perldoc ./make_email
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Run this in the directory where B<make_email> is kept.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
=item B<-month=DATE>
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Defines the month to be reviewed in the Community News recording for which the
|
|
|
|
email is being generated. Normally (without this option) the current month is
|
|
|
|
chosen and the date of recording computed within it. The month specified here
|
|
|
|
is provided as a ISO8601 date such as 2014-03-08 (meaning March 2014) or
|
|
|
|
1-Jan-2017 (meaning January 2017). Only the year and month parts are used but
|
|
|
|
a valid day must be present.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
=item B<-date=DATE>
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
This option is a way of providing a non-default date for the recording.
|
|
|
|
Normally the script computes the next scheduled date based on the algorithm
|
|
|
|
"DAY_OF_WEEK before the first Monday of the next month" (where DAY_OF_WEEK is
|
|
|
|
the value defined in the configuration file as B<dayname>) starting from the
|
|
|
|
current date or the start of the month given in the B<-month=DATE> option. If
|
|
|
|
for any reason a different date is required then this may be specified via
|
|
|
|
this option.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
The recording date should be given as an ISO8601 date (such as 2014-03-08).
|
|
|
|
|
|
|
|
=item B<-start=START_TIME>
|
|
|
|
|
|
|
|
The default start time is defined in the configuration file, but if it is
|
|
|
|
necessary to change it, this option can be used to do it. The B<START_TIME>
|
|
|
|
value must be a valid B<HH:MM> time specification.
|
|
|
|
|
|
|
|
A change to the start time in the configuration file also implies that the end
|
|
|
|
time should change. If the B<-start=START_TIME> option is present but
|
|
|
|
B<-end=END_TIME> is not, then the end time is computed by adding a number of
|
|
|
|
hours to the start time this number is defined in the configuration file as
|
|
|
|
B<duration>.
|
|
|
|
|
|
|
|
=item B<-end=END_TIME>
|
|
|
|
|
|
|
|
The default end time is defined in the configuration file, but if it is
|
|
|
|
necessary to change it temporarily, this option can be used to do it. The
|
|
|
|
B<END_TIME> value must be a valid B<HH:MM> time specification.
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
=item B<-output=FILE>
|
|
|
|
|
|
|
|
This option defines an output file to receive the mail message text. If the option is
|
|
|
|
omitted the notes are written to STDOUT, allowing them to be redirected if
|
|
|
|
required.
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
The output file name may contain one instance of the characters 'B<%s>'. This
|
|
|
|
denotes the point at which the year and the review month in the format
|
|
|
|
B<YYYY-MM> are inserted. For example if the script is being run for reviewing
|
|
|
|
February 2025 the option:
|
2025-03-31 21:59:14 +01:00
|
|
|
|
|
|
|
-out=HPR_email_%s.txt
|
|
|
|
|
|
|
|
will cause the generation of the file:
|
|
|
|
|
|
|
|
HPR_email_2025-02.txt
|
|
|
|
|
2025-04-03 16:57:57 +01:00
|
|
|
=item B<-[no]force>
|
|
|
|
|
|
|
|
Sometimes the recording of the Community News episode for a month takes place
|
|
|
|
on the first Friday of the next month. If an attempt is made to run this
|
|
|
|
script to make the email for such a recording in the month after the review
|
|
|
|
month, safety checks will prevent it. This option, which is normally off, will
|
|
|
|
allow the script to run and generate the mail message.
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Only use this if an apparent anomaly with the dates is detected. This may
|
|
|
|
happen if the recording is to be made in the month after the month being
|
|
|
|
reviewed.
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
=item B<-config=FILE>
|
|
|
|
|
|
|
|
This option defines a configuration file other than the default
|
|
|
|
B<.make_email.cfg>. The file must be formatted as described below in the
|
|
|
|
section I<CONFIGURATION AND ENVIRONMENT>.
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
=item B<-debug=N>
|
|
|
|
|
|
|
|
Enables debugging mode when N > 0 (zero is the default, no debugging output).
|
|
|
|
The levels are:
|
|
|
|
|
|
|
|
Values are:
|
|
|
|
|
|
|
|
=over 4
|
|
|
|
|
|
|
|
=item 1
|
|
|
|
|
|
|
|
Reports all of the settings taken from the configuration file, the provided
|
|
|
|
command line options or their default values. The report is generated early on
|
|
|
|
in the processing of these values. Use B<-debug=2> for information about the
|
|
|
|
next stages.
|
|
|
|
|
|
|
|
=item 2
|
|
|
|
|
|
|
|
Reports the following (as well as the data for level 1):
|
|
|
|
|
|
|
|
=over 4
|
|
|
|
|
|
|
|
=item .
|
|
|
|
|
|
|
|
Details of the start date chosen
|
|
|
|
|
|
|
|
=item .
|
|
|
|
|
|
|
|
Details of the year, name of month, readable date, and recording start and end
|
|
|
|
times.
|
|
|
|
|
|
|
|
=item .
|
|
|
|
|
|
|
|
The subject line chosen for the email.
|
|
|
|
|
|
|
|
=item .
|
|
|
|
|
|
|
|
The date of the show being searched for in the database.
|
|
|
|
|
|
|
|
=item .
|
|
|
|
|
|
|
|
The number of the show found in the database.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
=back
|
|
|
|
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
|
2025-04-16 17:14:26 +01:00
|
|
|
=head2 B<Overview>
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
The script makes an invitation email for the next Community News recording
|
|
|
|
with times per timezone. The message is structured by a Template Toolkit
|
|
|
|
template, so its contents can be adjusted without changing this script.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
In normal operation the script computes the date of the next recording using
|
2025-04-07 21:03:37 +01:00
|
|
|
the algorithm "DAY before the first Monday of the next month" starting
|
2024-06-04 16:35:44 +01:00
|
|
|
from the current date or the start of the month (and year) given in the
|
|
|
|
B<-month=DATE> option.
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Alternatively it can use the B<-date=DATE> option to define a non-default
|
|
|
|
recording date (and time if B<-start=START_TIME> is provided). This allows the
|
|
|
|
defaults in the configuration file to be overridden.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Only one of B<-month=DATE> and B<-date=DATE> is allowed.
|
|
|
|
|
|
|
|
The script creates the text of the body of an email message. It is necessary
|
|
|
|
to use it with an email client to construct the message to be sent to the HPR
|
|
|
|
mailing list, usually on the Monday prior to the weekend of the recording. The
|
|
|
|
script does not send the message itself.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-16 17:14:26 +01:00
|
|
|
=head2 B<Required files>
|
|
|
|
|
|
|
|
The B<make_email> script requires the presence of two files, which are
|
|
|
|
described in this section. These files are expected to be located in the same
|
|
|
|
directory used to store the script.
|
|
|
|
|
|
|
|
The script also requires a configuration file which is described in the
|
|
|
|
B<CONFIGURATION AND ENVIRONMENT> section below:
|
|
|
|
|
|
|
|
=over 4
|
|
|
|
|
|
|
|
=item B<Date Cache>
|
|
|
|
|
|
|
|
This is a file containing the recording dates of previous Community News
|
|
|
|
shows. The file is managed by B<make_email>, and it exists to aid the script
|
|
|
|
B<make_shownotes> when it is generating the HTML notes for distribution to the
|
|
|
|
volunteers who are contributing to the recording.
|
|
|
|
|
|
|
|
When B<make_shownotes> is generating notes for the volunteers it tries to work
|
|
|
|
out whether any comments were missed for that month, or whether any for the
|
|
|
|
current review month were read. It highlights any such cases to assist with
|
|
|
|
continuity. The date and time of the last recording are needed to achieve
|
|
|
|
this.
|
|
|
|
|
|
|
|
The file name is defined in the configuration file, and the default is
|
|
|
|
B<recording_dates.dat>.
|
|
|
|
|
|
|
|
The format of the file is described in the B<DATE CACHE> part of the
|
|
|
|
B<CONFIGURATION AND ENVIRONMENT> section below.
|
|
|
|
|
|
|
|
=item B<Template>
|
|
|
|
|
|
|
|
This is a file written in the Template Toolkit syntax which defines the format
|
|
|
|
of the email text. The name of this file is defined in the configuration file.
|
|
|
|
By default it is B<make_email_template.tpl>.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
=head1 DIAGNOSTICS
|
|
|
|
|
|
|
|
=over 8
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
=item B<Unable to find config file '...'>
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
The configuration file specified in B<-config=FILE> (or the default file)
|
|
|
|
could not be found.
|
|
|
|
|
|
|
|
=item B<Use only one of -month=MONTH or -date=DATE>
|
|
|
|
|
|
|
|
These options are mutually exclusive. See their specifications earlier in this
|
|
|
|
document.
|
|
|
|
|
|
|
|
=item B<Missing start/end time(s)>
|
|
|
|
|
|
|
|
One or both of the start and end times is missing, either from the configuration file or
|
|
|
|
from the command line options.
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
=item B<Missing template file '...'>
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
The template file specified in the configuration file could not be found.
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
=item B<No recording date file defined in configuration>
|
|
|
|
|
|
|
|
The script reads and writes a file, usually called B<recording_dates.dat>,
|
|
|
|
which is defined in the configuration file. This is used to record the date
|
|
|
|
and time of the recording for a given month for use by othger scripts. This
|
|
|
|
warning is reporting that no such file specification has been found in the
|
|
|
|
configuration file. The script will continue without it.
|
|
|
|
|
|
|
|
=item B<Can't find recording date file '...'>
|
|
|
|
|
|
|
|
The file for holding recording dates, which is defined in the configuration
|
|
|
|
file, cannot be found. The script will continue without it.
|
|
|
|
|
|
|
|
=item B<Unable to open '...' for writing: ...>
|
|
|
|
|
|
|
|
The script cannot open the nominated output file for writing.
|
|
|
|
|
|
|
|
=item B<Unable to initialise for writing: ...>
|
|
|
|
|
|
|
|
No output file has been nominated, so the script witll write to STDOUT. It has
|
|
|
|
failed to open this channel however.
|
|
|
|
|
|
|
|
=item B<...: failed to open '...': ...>
|
|
|
|
|
|
|
|
The date cache file could not be opened.
|
|
|
|
|
|
|
|
=item B<...: failed to close '...': ...>
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Type: warning
|
|
|
|
|
|
|
|
The date cache file could not be closed.
|
|
|
|
|
|
|
|
=item B<Unable to back up '...'>
|
|
|
|
|
|
|
|
The script needs to append to the date cache and it is trying to make a backup
|
|
|
|
of it before it does so. This process has failed.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
=item B<Invalid -date=DATE option '...'>
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Type: fatal
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
An invalid date has been supplied via this option.
|
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
=item B<...: failed to truncate '...': ...> or
|
|
|
|
B<...: failed to seek in '...': ...>
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Type: fatal
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
The script is attempting to update a line in the date cache, but has failed to
|
|
|
|
perform an operation on it.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
=item B<Invalid time: ...>
|
|
|
|
|
|
|
|
Type: fatal
|
|
|
|
|
|
|
|
The script is checking one of the times provided and has found it to be
|
|
|
|
invalid.
|
|
|
|
|
|
|
|
=item B<Invalid Date::Calc date and time (...) in dc_to_dt>
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
Type: fatal
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
The script has failed to validate a date and time value (built from options
|
|
|
|
such as B<-date=DATE> and B<-start=TIME>).
|
|
|
|
|
|
|
|
=item B<Unable to parse date '...'>
|
|
|
|
|
|
|
|
Type: fatal
|
|
|
|
|
|
|
|
The script has failed to parse a date from B<-date=DATE> or B<-month=DATE>.
|
|
|
|
|
|
|
|
=item B<"convert_date: Invalid date ... (in the past)>
|
|
|
|
|
|
|
|
Type: fatal
|
|
|
|
|
|
|
|
The date specified in B<-date=DATE> or B<-month=DATE> is in the past. There is
|
|
|
|
a supplementary message: I<Use -force to create a back-dated calendar>
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
=item B<Various Template Toolkit messages>
|
|
|
|
|
|
|
|
The program can generate warning messages from the Template.
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
|
|
|
=head1 CONFIGURATION AND ENVIRONMENT
|
|
|
|
|
|
|
|
=head2 EMAIL CONFIGURATION
|
|
|
|
|
|
|
|
The program obtains the settings it requires for preparing the email from
|
|
|
|
a configuration file, which by default is called B<.make_email.cfg>. This file
|
|
|
|
needs to contain the following data:
|
|
|
|
|
|
|
|
<email>
|
|
|
|
server = MUMBLE_SERVER_NAME
|
|
|
|
port = MUMBLE_PORT
|
|
|
|
room = NAME_OF_ROOM
|
|
|
|
dayname = DAY_OF_WEEK_OF_RECORDING
|
2025-04-07 21:03:37 +01:00
|
|
|
starttime = 16:00:00
|
|
|
|
endtime = 18:00:00
|
2024-06-04 16:35:44 +01:00
|
|
|
duration = 02 # hours
|
|
|
|
template = NAME_OF_TEMPLATE
|
|
|
|
</email>
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
=head2 DATE CACHE
|
|
|
|
|
|
|
|
<recdates>
|
|
|
|
name = recording_dates.dat
|
|
|
|
</recdates>
|
|
|
|
|
|
|
|
The program will update a cache of recording dates and times per month. This
|
|
|
|
is useful for the script B<make_shownotes> which needs to know about the
|
|
|
|
Community News show recording time so it can determine how to display
|
|
|
|
information about comments. See the details for this script.
|
|
|
|
|
|
|
|
The format of the lines in the file is:
|
|
|
|
|
|
|
|
MONTH,TIMESTAMP
|
|
|
|
|
|
|
|
Note the separating comma. The month is an ISO8601 date where the day part is
|
|
|
|
always B<01>. The timestamp part is the date and time of the recording in the
|
|
|
|
format:
|
|
|
|
|
|
|
|
YYYY-MM-DD HH:MM:SS
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
2024-12-01,2025-01-03 15:00:00
|
|
|
|
2025-01-01,2025-01-31 15:00:00
|
|
|
|
2025-02-01,2025-02-28 16:00:00
|
|
|
|
|
|
|
|
The dates and times are derived from the configuration file defaults (and those
|
|
|
|
computed in the script), or the options given when running the script.
|
|
|
|
|
|
|
|
The contents of this cache are loaded into the B<make_email> script. If there
|
|
|
|
is no record for the month being processed, one is appended to the file. If
|
|
|
|
the details already exist they are updated unless they are the same as those
|
|
|
|
stored.
|
|
|
|
|
2025-04-16 17:14:26 +01:00
|
|
|
A backup of the file is made if the data in a record is being updated. This
|
|
|
|
file has the same name as the cache file with a B<~> character appended to it.
|
2024-06-04 16:35:44 +01:00
|
|
|
|
2025-04-07 21:03:37 +01:00
|
|
|
=head1 EXAMPLES
|
|
|
|
|
|
|
|
=over 4
|
|
|
|
|
2025-04-16 17:14:26 +01:00
|
|
|
=item B<Specify a future recording date>
|
2025-04-07 21:03:37 +01:00
|
|
|
|
|
|
|
make_email -date=2025-08-01
|
|
|
|
|
|
|
|
Generates the text of an email for a specific date (expected to be in the
|
2025-04-16 17:14:26 +01:00
|
|
|
future). The first Monday of August 2025 is 2025-08-04. The date used in the
|
2025-04-07 21:03:37 +01:00
|
|
|
example is the Friday before that. The script will compute the month to be
|
|
|
|
reviewed (July 2025) from this date and write the email message text
|
|
|
|
accordingly, to STDOUT.
|
|
|
|
|
2025-04-16 17:14:26 +01:00
|
|
|
This would be a way of testing the generation of the email text for a future
|
|
|
|
date, as opposed to the next one. It will cause the writing of a record in the
|
|
|
|
data cache file, but, since this will be adjusted if there are any changes in
|
|
|
|
the future, this should not be a problem.
|
|
|
|
|
|
|
|
=item B<Specify and force a review month in the recent past>
|
2025-04-07 21:03:37 +01:00
|
|
|
|
|
|
|
make_email -force -month=2025-03-01 -start=16:00 -out=HPR_email_%s.txt
|
|
|
|
|
|
|
|
Assume this is run in early April 2025 to review the episodes for March. The
|
|
|
|
recording date for that month will be computed to be 2025-04-04, with the
|
|
|
|
result being released on 2025-04-07.
|
|
|
|
|
|
|
|
Normally, generating email for March would not be allowed since it's in the
|
|
|
|
past, so B<-force> is needed to override the checks. The start day is the
|
|
|
|
default Friday, and the start time is set to 16:00. The message (email body)
|
|
|
|
is written to B<HPR_email_2025-03.txt>. The date cache
|
2025-04-16 17:14:26 +01:00
|
|
|
(B<recording_dates.dat>) will be updated with the line:
|
2025-04-07 21:03:37 +01:00
|
|
|
|
|
|
|
2025-03-01,2025-04-04 16:00:00
|
|
|
|
|
|
|
|
=back
|
|
|
|
|
2024-06-04 16:35:44 +01:00
|
|
|
=head1 DEPENDENCIES
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
Config::General
|
|
|
|
Cwd
|
2024-06-04 16:35:44 +01:00
|
|
|
DBI
|
2025-03-31 21:59:14 +01:00
|
|
|
Data::Dumper
|
2024-06-04 16:35:44 +01:00
|
|
|
Date::Calc
|
|
|
|
Date::Parse
|
|
|
|
DateTime
|
|
|
|
DateTime::Format::Duration
|
|
|
|
DateTime::TimeZone
|
2025-03-31 21:59:14 +01:00
|
|
|
File::Copy
|
2024-06-04 16:35:44 +01:00
|
|
|
Getopt::Long
|
|
|
|
Pod::Usage
|
|
|
|
Template
|
|
|
|
|
|
|
|
=head1 BUGS AND LIMITATIONS
|
|
|
|
|
|
|
|
There are no known bugs in this script.
|
|
|
|
Please report problems to Dave Morriss (Dave.Morriss@gmail.com)
|
|
|
|
Patches are welcome.
|
|
|
|
|
|
|
|
=head1 AUTHOR
|
|
|
|
|
2025-03-31 21:59:14 +01:00
|
|
|
Dave Morriss (Dave.Morriss@gmail.com) 2013 - 2025
|
2024-06-04 16:35:44 +01:00
|
|
|
|
|
|
|
=head1 LICENCE AND COPYRIGHT
|
|
|
|
|
|
|
|
Copyright (c) Dave Morriss (Dave.Morriss@gmail.com). All rights reserved.
|
|
|
|
|
|
|
|
This program is free software. You can redistribute it and/or modify it under
|
|
|
|
the same terms as perl itself.
|
|
|
|
|
|
|
|
=cut
|
|
|
|
|
|
|
|
#}}}
|
|
|
|
|
|
|
|
# [zo to open fold, zc to close]
|
|
|
|
|
|
|
|
# vim: syntax=perl:ts=8:sw=4:et:ai:tw=78:fo=tcrqn21:fdm=marker
|