When making a human readable disk size, keep the original.
[mythtv-status.git] / bin / mythtv-status
1 #!/usr/bin/perl -w
2 # Copyright (c) 2007-2018 Andrew Ruthven <andrew@etc.gen.nz>
3 # This code is hereby licensed for public consumption under the GNU GPL v3.
4 #
5 # You should have received a copy of the GNU General Public License along
6 # with this program; if not, write to the Free Software Foundation, Inc.,
7 # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
8
9 # Display the current status of a MythTV system.
10
11 # While I would love to enable the 6.xx interface for Date::Manip we may
12 # still need to run on platforms that only have version 5.x.  So we'll
13 # turn on backwards compatible mode for now.
14 {
15   no warnings 'once';
16   $Date::Manip::Backend = 'DM5';
17 }
18
19 use LWP::UserAgent;
20 use XML::LibXML;
21 use Date::Manip;
22 use Date::Manip::Date;
23 use Getopt::Long;
24 use Text::Wrap;
25 use POSIX qw/strftime :sys_wait_h :signal_h/;
26
27 use MIME::Entity;
28 use Pod::Usage;
29 use Encode;
30
31 # Try and load a config file first, and then allow the command line
32 # to override what is in the config file.
33 my $c;
34
35 if (eval("{ local \$SIG{__DIE__}; require Config::Auto; }")) {
36   $c = eval {
37     Config::Auto::parse("mythtv-status.yml", format => 'yaml')
38   };
39
40   if ($@) {
41     # Die on any issues loading the config file, apart from it not existing.
42     if ($@ =~ /^No config file found/) {
43       $c->{'config_file_loaded'} = 0;
44     } else {
45       die $@;
46     }
47   } else {
48     $c->{'config_file_loaded'} = 1;
49   }
50 }
51
52 # Some sane defaults.
53 $c->{'host'}    ||= "localhost";
54 $c->{'port'}    ||= "6544";
55 $c->{'colour'}  ||= 0;
56 $c->{'episode'} ||= 0;
57 $c->{'description'} ||= 0;
58 $c->{'encoder_details'}     ||= 0;
59 $c->{'encoder_skip_idle'}   //= 1;
60 $c->{'email_only_on_alert'} ||= 0;
61 my $help = 0;
62 my $verbose = 0;
63 $c->{'disk_space_warn'} ||= 95;  # Percent to warn at.
64 $c->{'guide_days_warn'} ||= 2;   # How many days we require.
65 $c->{'auto_expire_count'} ||= 10;      # How many auto expire shows to display.
66 $c->{'recording_in_warn'} ||= 60 * 60; # When is the next recording considered critical? (seconds)
67 $c->{'save_file'} ||= undef; # File to save the XML from the BE to.
68 $c->{'xml_file'}  ||= undef; # Load the BE XML from this file.
69
70 # We allow a hack for MS Outlook which normally recognises the
71 # oneliners block as a paragraph and wraps it.  If it sees "bullets"
72 # it believes it is a bulleted list and leaves it alone.
73 $c->{'oneliner_bullets'} ||= 0;
74
75 # What units we have available for converting diskspaces.
76 # The threshold is at what level we switch to using that unit.
77 my @size_thresholds = (
78   {
79     'unit' => 'TB',
80     'threshold' => 2 * 1024 * 1024,
81     'conversion' => 1024 * 1024,
82   },
83   {
84     'unit' => 'GB',
85     'threshold' => 50 * 1024,        # 50GB seems like a good threshold.
86     'conversion' => 1024,
87   },
88   {
89     'unit' => 'MB',
90   },
91 );
92
93 my $return_code_only = 0;
94
95 my $VERSION = '0.10.8';
96
97 # Some display blocks are disabled by default:
98 $c->{'display'}{'Shows due to Auto Expire'} = 0;
99
100 GetOptions(
101   'c|colour|color!' => \$c->{'colour'},
102   'd|description!' => \$c->{'description'},
103   'e|episode!'    => \$c->{'episode'},
104   'encoder-details!' => \$c->{'encoder_details'},
105   'h|host=s'     => \$c->{'host'},
106   'p|port=i'     => \$c->{'port'},
107   'v|version'    => \&print_version,
108   'email=s@'     => \@{ $c->{'email'} },
109   'email-only-on-conflict|email-only-on-alert|email-only-on-alerts'
110     => \$c->{'email_only_on_alert'},
111   'disk-space-warn=i'     => \$c->{'disk_space_warn'},
112   'guide-days-warn=i'     => \$c->{'guide_days_warn'},
113   'auto-expire-count=i'   => \$c->{'auto_expire_count'},
114   'recording-in-warn=i'   => \$c->{'recording_in_warn'},
115   'encoder-skip-idle!'    => \$c->{'encoder_skip_idle'},
116   'oneliner-bullets!'     => \$c->{'oneliner_bullets'},
117
118   'status!'               => \$c->{'display'}{'Status'},
119   'encoders!'             => \$c->{'display'}{'Encoders'},
120   'recording-now!'        => \$c->{'display'}{'Recording Now'},
121   'scheduled-recordings!' => \$c->{'display'}{'Scheduled Recordings'},
122   'schedule-conflicts!'   => \$c->{'display'}{'Schedule Conflicts'},
123   'next-recording!'       => \$c->{'display'}{'Next Recording In'},
124   'total-disk-space!'     => \$c->{'display'}{'Total Disk Space'},
125   'disk-space!'           => \$c->{'display'}{'Disk Space'},
126   'guide-data!'           => \$c->{'display'}{'Guide Data'},
127   'auto-expire!'          => \$c->{'display'}{'Shows due to Auto Expire'},
128
129   'return-code-only'      => \$return_code_only,
130
131   'file=s'                => \$c->{'xml_file'},
132   's|save-file=s'         => \$c->{'save_file'},
133
134   'date=s'  => \$c->{'date'},
135   'verbose' => \$verbose,
136   'help|?'  => \$help,
137   ) || pod2usage("\nUse --help for help.\n");
138
139 pod2usage(verbose => 1)
140   if $help;
141
142 $0 = "mythtv-status - parent";
143
144 # Get the email address into a format we can use.
145 @{ $c->{'email'} } = split(',', join(',', @{ $c->{'email'} }));
146
147 # Default to not showing some blocks if we're sending email, but let the
148 # user override us.
149 if (scalar(@{ $c->{'email'} }) > 0) {
150   for my $block ('Encoders', 'Recording Now', 'Next Recording In') {
151     if (! defined $c->{'display'}{$block}) {
152       $c->{'display'}{$block} = 0;
153     }
154   }
155 }
156
157 # Possibly use some colour, but not in emails.
158 my $safe = '';
159 my $warning = '';
160 my $normal = '';
161
162 if ($c->{'colour'} && scalar(@{ $c->{'email'} }) == 0) {
163   $safe    = "\033[0;32m";
164   $warning = "\033[1;31m";
165   $normal  = "\033[0m";
166 }
167
168 # Is a warning present?
169 my $warn_present = 0;
170
171 # Allow setting some defaults for the output blocks.
172 my %defaults = (
173   'schedule' => {
174     'attrs' => [ qw/title startTime NODE_TEXT subTitle channelName:.\/Channel[@channelName] chanNum:.\/Channel[@chanId] inputId:.\/Channel[@inputId]/ ],
175     'template' => "__startTime__"
176       . " - __title__"
177       . ($c->{'episode'} ? " - __subTitle__" : '')
178       . " (__channelName__)"
179       . ($c->{'encoder_details'} ? " - Enc: __inputId__, Chan: __chanNum__" : '')
180       . ($c->{'description'} ? "\n__NODE_TEXT__" : ''),
181
182     'filter' =>  {
183
184       # Only show recordings for today and tomorrow.
185       'startTime' => sub {
186         my $date = substr(ParseDate($_[0]), 0, 8);
187         return ! (($date cmp $today) == 0
188           || ($date cmp $tomorrow) == 0) }
189       },
190     'rewrite' => {
191       '&startTime' => sub { return process_iso_date($_[0]); }
192     }
193   }
194 );
195
196 # The time of the next scheduled recording.
197 my $next_time = 'Never';
198
199 # Are there any alerts that should be notified via email?
200 my @alerts = ();
201
202 # The blocks of output which we might generate.
203 #
204 # Generally, these blocks of output are generated in the order that they
205 # appear below.  However, if a block has the tag "format", who's value is
206 # "one line" it will appear at the start of the output, on a single line,
207 # all one liners grouped together.
208 my @blocks = (
209
210   # All the one liners together
211   {
212     'name' => 'One Liners',
213     'type' => 'sub',
214     'template' => '',
215     'sub' => sub { return 'Place holder' },
216   },
217
218   # Date/Time from server
219   {
220     'name'  => 'Status as of',
221     'type'  => 'xpath',
222     'xpath' => "//Status",
223     'attrs' => [ qw/ISODate time date/ ],
224     'template' => "__date__",
225     'format' => 'one line',
226     'rewrite' => {
227       '&date' => sub {
228         my ($value, $vars) = @_;
229
230         if (defined $vars->{ISODate} && $vars->{ISODate} =~ /Z$/) {
231           return process_iso_date($vars->{ISODate});
232         } else {
233           return $vars->{date} . ", " . $vars->{time};
234         }
235       },
236     },
237   },
238
239   # Info about the encoders before TV OSD Declutter (Changeset 20037).
240   {
241     'name'  => 'Encoders',
242     'type'  => 'xpath',
243     'xpath' => "//Status/Encoders/Encoder",
244     'protocol_version' => [ "<= 43" ],
245     'attrs' => [ qw/hostname id state connected/ ],
246     'template' => "__hostname__ (__id__) - __state____connected__",
247     'rewrite' => {
248       '/connected/' => { '1' => '', '0' => "${warning}(Disconnected)${normal}" },
249       '/state/' => {
250         '^0$' => "${safe}Idle${normal}",
251         '^1$' => "${warning}Watching LiveTV${normal}",
252         '^2$' => "${warning}Watching Pre-recorded${normal}",
253         '^3$' => "${warning}Watching Recording${normal}",
254         '^4$' => "${warning}Recording${normal}",
255       },
256     },
257     'filter' => {
258       'state' => sub { return $c->{'encoder_skip_idle'} && $_[0] == 0 },
259     },
260   },
261
262   # Info about the encoders after TV OSD Declutter (Changeset 20037).
263   {
264     'name'  => 'Encoders',
265     'type'  => 'xpath',
266     'xpath' => "//Status/Encoders/Encoder",
267     'protocol_version' => [ ">= 44", "< 58" ],
268     'attrs' => [ qw/hostname id state connected/ ],
269     'template' => "__hostname__ (__id__) - __state____connected__",
270     'rewrite' => {
271       '/connected/' => { '1' => '', '0' => "${warning}(Disconnected)${normal}" },
272       '/state/' => {
273          '^-1$' => "${warning}Error${normal}",
274          '^0$' => "${safe}Idle${normal}",
275          '^1$' => "${warning}Watching LiveTV${normal}",
276          '^2$' => "${warning}Watching Pre-recorded${normal}",
277          '^3$' => "${warning}Watching DVD${normal}",
278          '^4$' => "${warning}Watching Video${normal}",
279          '^5$' => "${warning}Watching Recording${normal}",
280          '^6$' => "${warning}Recording${normal}",
281        },
282     },
283     'filter' => {
284       'state' => sub { return $c->{'encoder_skip_idle'} && $_[0] == 0 },
285     },
286   },
287
288   # Info about the encoders after adding Blu-ray (Changeset 25058).
289   #  The protocol version is from svn commit 25362 but is the closest commit
290   #  for mythtv/libs/libmythdb/mythversion.h.
291   {
292     'name'  => 'Encoders',
293     'type'  => 'xpath',
294     'xpath' => "//Status/Encoders/Encoder",
295     'protocol_version' => [ ">= 58" ],
296     'attrs' => [ qw/hostname id state devlabel connected/ ],
297     'template' => "__hostname__ (__id____devtype__) - __state____connected__",
298     'rewrite' => {
299       '/connected/' => { '1' => '', '0' => "${warning}(Disconnected)${normal}" },
300       '/state/' => {
301          '^-1$' => "${warning}Error${normal}",
302          '^0$' => "${safe}Idle${normal}",
303          '^1$' => "${warning}Watching LiveTV${normal}",
304          '^2$' => "${warning}Watching Pre-recorded${normal}",
305          '^3$' => "${warning}Watching DVD${normal}",
306          '^4$' => "${warning}Watching Blu-ray${normal}",
307          '^5$' => "${warning}Watching Video${normal}",
308          '^6$' => "${warning}Watching Recording${normal}",
309          '^7$' => "${warning}Recording${normal}",
310        },
311     },
312     'filter' => {
313       'state' => sub { return $c->{'encoder_skip_idle'} && $_[0] == 0 },
314     },
315     'subs' => {
316       'devtype' => sub {
317         if    ($_[0]->{'devlabel'} =~ /\[ (.+) :/)   { ", $1" }
318         else { '' }
319       },
320     },
321   },
322
323   # What programs (if any) are being recorded right now?
324   {
325     'name'  => 'Recording Now',
326     'type'  => 'xpath',
327     'xpath' => "//Status/Encoders/Encoder/Program",
328     'hide'  => 'after',
329     'attrs' => [ qw/title endTime channelName:.\/Channel[@channelName]
330                     encoderId:.\/Recording[@encoderId]
331                     chanNum:.\/Channel[@chanNum]/ ],
332     'template' => "__title__ (__channelName__) "
333       . ($c->{'encoder_details'} ? ", Enc: __encoderId__, Chan: __chanNum__ " : '')
334       . "Ends: __endTime__",
335     'rewrite' => {
336       '&endTime' => sub {
337         my ($value, $vars) = @_;
338
339         if ($value =~ /Z$/) {
340           $value = process_iso_date($value, { date => 0 });
341         } else {
342           $value =~ s/.*T//;
343         }
344
345         return $value;
346       },
347     },
348     'subs' => {
349       'find_next' => sub {
350         $warn_present ||= 1;
351         $next_time    = $c->{'date'} || 'now';
352       },
353     },
354   },
355
356   # The upcoming recordings.
357   {
358     'name'  => 'Scheduled Recordings',
359     'type'  => 'xpath',
360     'xpath' => '//Status/Scheduled/Program',
361     'defaults' => 'schedule',
362     'hide'  => 'after',
363     'subs' => {
364       'find_next' => sub {
365         my $vars = shift;
366         return
367           if defined $next_time && $next_time eq 'now';
368
369         my $date = ParseDate($vars->{'startTime'});
370         if ($next_time eq 'Never' || Date_Cmp($date, $next_time) < 0) {
371           $next_time = $date
372         };
373       },
374     },
375   },
376
377   # Conflicts
378   {
379     'name' => 'Schedule Conflicts',
380     'type' => 'sub',
381     'defaults' => 'schedule',
382     'sub' => \&process_conflicts
383   },
384
385   # Auto Expire
386   {
387     'name' => 'Shows due to Auto Expire',
388     'type' => 'sub',
389     'defaults' => 'schedule',
390     'sub' => \&process_auto_expire,
391     'filter' =>  {},   # Over ride the default filter from 'schedule'.
392   },
393
394   # Diskspace, before storage groups
395   {
396     'name' => 'Total Disk Space',
397     'type' => 'xpath',
398     'xpath' => '//Status/MachineInfo/Storage',
399     'protocol_version' => [ "<= 31" ],
400     'attrs' => [ qw/_total_total _total_used/ ],
401     'commify' => [ qw/si__total_total si__total_used/ ],
402     'human_readable_sizes' => [ qw/_total_total _total_used/ ],
403     'template' => "Total space is __si__total_total__ __si__total_total_unit__, with __si__total_used__ __si__total_used_unit__ used (__percent__)",
404     'format' => 'one line',
405     'optional' => 1,
406     'subs' => {
407       'percent' => sub {
408         calc_disk_space_percentage($_[0]->{'_total_used'}, $_[0]->{'_total_total'})
409       },
410     },
411   },
412
413   # Diskspace, with storage groups
414   {
415     'name' => 'Total Disk Space',
416     'type' => 'xpath',
417     'xpath' => '//Status/MachineInfo/Storage',
418     'protocol_version' => [ ">= 32" ],
419     'xml_version' => [ "== 0" ],
420     'attrs' => [ qw/drive_total_total drive_total_used/ ],
421     'commify' => [ qw/si_drive_total_total si_drive_total_used/ ],
422     'human_readable_sizes' => [ qw/drive_total_total drive_total_used/ ],
423     'template' => "Total space is __si_drive_total_total__ __si_drive_total_total_unit__, with __si_drive_total_used__ __si_drive_total_used_unit__ used (__percent__)",
424     'format' => 'one line',
425     'optional' => 1,
426     'subs' => {
427       'percent' => sub {
428         calc_disk_space_percentage($_[0]->{'drive_total_used'}, $_[0]->{'drive_total_total'})
429       },
430     },
431   },
432
433   # Diskspace, with storage groups and sensible XML layout.
434   {
435     'name' => 'Total Disk Space',
436     'type' => 'xpath',
437     'xpath' => '//Status/MachineInfo/Storage/Group[@id="total"]',
438     'protocol_version' => [ ">= 39" ],
439     'attrs' => [ qw/total used/ ],
440     'commify' => [ qw/si_total used/ ],
441     'human_readable_sizes' => [ qw/total used/ ],
442     'template' => "Total space is __si_total__ __si_total_unit__, with __si_used__ __si_used_unit__ used (__percent__)",
443     'format' => 'one line',
444     'optional' => 1,
445     'subs' => {
446       'percent' => sub {
447         calc_disk_space_percentage($_[0]->{'used'}, $_[0]->{'total'})
448       },
449     },
450   },
451
452   # Diskspace, with storage groups and sensible XML layout.
453   {
454     'name' => 'Disk Space',
455     'type' => 'xpath',
456     'xpath' => '//Status/MachineInfo/Storage/Group',
457     'protocol_version' => [ ">= 39" ],
458     'attrs' => [ qw/id total used/ ],
459     'commify' => [ qw/si_total used/ ],
460     'human_readable_sizes' => [ qw/total used/ ],
461     'template' => "Total space for group __id__ is __si_total__ __si_total_unit__, with __si_used__ __si_used_unit__ used (__percent__)",
462     'filter' =>  {
463       'id' => sub { return $_[0] eq 'total' },
464       'used' => sub {
465         return ! (
466           (defined $c->{'display'}{'Disk Space'} && $c->{'display'}{'Disk Space'})
467           || ($_[1]->{'used'} / $_[1]->{'total'}) * 100 > $c->{'disk_space_warn'})
468       },
469     },
470     'subs' => {
471       'percent' => sub {
472         calc_disk_space_percentage($_[0]->{'used'}, $_[0]->{'total'})
473       },
474     },
475   },
476
477   # How many hours till the next recording.
478   {
479     'name' => 'Next Recording In',
480     'type' => 'sub',
481     'format' => 'one line',
482     'template' => '__next_time__',
483     'rewrite' => {
484       '&next_time' => sub {
485         return $next_time
486           if $next_time eq 'Never' || $next_time eq 'now';
487
488         my $err;
489         my $delta   = DateCalc($c->{'date'} || 'now', $next_time, \$err, 1);
490         my $seconds = Delta_Format($delta, 'approx', 0, '%sh');
491
492         # If the next recording is more than 1 day in the future,
493         # print out the days and hours.
494         my $str;
495         if ($seconds > 24 * 3600) {
496           $str = Delta_Format($delta, 0, '%dh Days, %hv Hours');
497         } else {
498           $str = Delta_Format($delta, 0, '%hh Hours, %mv Minutes');
499         }
500
501         $str =~ s/\b1 (Day|Hour|Minute)s/1 $1/g;
502         $str =~ s/\b0 (Days|Hours)(, )?//;
503         $str =~ s/, 0 Minutes$//;
504
505         if ($seconds <= $c->{'recording_in_warn'}) {
506           $warn_present ||= 1;
507           $str = "$warning$str$normal";
508         }
509
510         return $str;
511       },
512     },
513     'filter' =>  {
514       'next_time' => sub { return $_[0] eq 'now' }
515     },
516     'sub' => sub {
517       return substitute_vars($_[0], { 'next_time' => $next_time });
518     },
519   },
520
521   # Check how much Guide data we have
522   {
523     'name'     => 'Guide Data',
524     'format'   => 'one line',
525     'type'     => 'xpath',
526     'xpath'    => '//Status/MachineInfo/Guide[@guideDays]',
527     'attrs'    => [qw/guideDays status guideThru/],
528     'template' => 'There is __guideDays__ days worth of data, through to __guideThru__',
529     'filter' => {
530       'guideDays' => sub {
531         if ($_[0] > $c->{'guide_days_warn'}) {
532           return
533             (defined $c->{'display'}{'Guide Data'} && ! $c->{'display'}{'Guide Data'}) || 1;
534         } else {
535           $warn_present ||= 1;
536           push @alerts, "GUIDE DATA";
537           return 0;
538         }
539       },
540     },
541     'rewrite'  => {
542       '&guideDays' => sub {
543         if ($_[0] <= $c->{'guide_days_warn'}) {
544           $warn_present ||= 1;
545           return "$warning$_[0]$normal";
546         } else {
547           return "$safe$_[0]$normal";
548         }
549       },
550       '/guideThru/' => { 'T\d+:\d+:\d+' => ' ' },
551       '&guideThru' => sub {
552         if ($_[1]->{'guideDays'} <= $c->{'guide_days_warn'}) {
553           $warn_present ||= 1;
554           return "$warning$_[0]$normal";
555         } else {
556           return "$safe$_[0]$normal";
557         }
558       },
559     },
560   },
561
562   {
563     'name'     => 'Guide Data',
564     'format'   => 'one line',
565     'type'     => 'xpath',
566     'xpath'    => '//Status/MachineInfo/Guide[@status=""]',
567     'template' => "${warning}No guide data!${normal}",
568   },
569 );
570
571 ###
572 ### Set some useful variables
573 ###
574 our $today    = substr(ParseDate('today'), 0, 8);
575 our $tomorrow = substr(ParseDate('tomorrow'), 0, 8);
576
577 if ($c->{'date'}) {
578   $today    = substr(ParseDate($c->{'date'}), 0, 8);
579   $tomorrow = substr(DateCalc($c->{'date'}, ParseDateDelta('1 day')), 0, 8);
580 }
581
582 if ($verbose) {
583   print "Today:               $today\n";
584   print "Tomorrow:            $tomorrow\n";
585   print "Config::Auto module: " . (defined $INC{'Config/Auto.pm'} ? 'Loaded' : 'Not Loaded') . "\n";
586   print "Config file loaded:  " . ($c->{'config_file_loaded'} ? 'Yes' : 'No') . "\n";
587 }
588
589 # If we're in return code only mode then we disable all blocks
590 # except for those explicitly enabled.
591 if ($return_code_only) {
592   warn "In return-code-only mode, disabling all blocks by default.\n"
593     if $verbose;
594
595   for my $block (@blocks) {
596     $c->{'display'}{ $block->{'name'} } ||= 0;
597   }
598 }
599
600 # A couple of global variables
601 my ($xml, $charset, $myth);
602 my %version;
603
604 my $exit_value = 0;
605 my $title =  "MythTV status for $c->{'host'}";
606 my $output = "$title\n";
607 $output .= '=' x length($title) . "\n";
608
609 for my $block (@blocks) {
610   $block->{'format'} ||= 'multi line';
611   $block->{'optional'} ||= 0;
612
613   warn "Considering: $block->{'name'}\n"
614     if $verbose;
615
616   my $hide = undef;
617   if (defined $c->{'display'}{ $block->{'name'} }
618     && $c->{'display'}{ $block->{'name'} } == 0) {
619     if (defined $block->{'hide'} && lc($block->{'hide'}) eq 'after') {
620       $hide = 1;
621     } else {
622       next;
623     }
624   }
625
626   warn "  Going to process: $block->{'name'}\n"
627     if $verbose;
628
629   # We might need to set some defaults.
630   if (defined $block->{'defaults'}) {
631     for my $field (keys %{ $defaults{ $block->{'defaults'} } }) {
632       $block->{$field} ||= $defaults{ $block->{'defaults'} }{$field};
633     }
634   }
635
636   my $result = undef;
637   $warn_present = 0;
638   if ($block->{'type'} eq 'xpath') {
639     ($xml, $charset) = load_xml()
640       unless defined $xml;
641
642     $result = process_xml($block, $xml);
643
644   } elsif ($block->{'type'} eq 'sub') {
645
646     $result = &{ $block->{'sub'} }($block)
647       if defined $block->{'sub'};
648   }
649
650   if (defined $result && $result ne '' && ! defined $hide) {
651     $exit_value ||= $warn_present;
652
653     if ($block->{'format'} eq 'one line') {
654       push @oneliners, [ $block->{'name'}, $result ];
655     } else {
656       $output .= "$block->{'name'}:\n";
657       $output .= $result . "\n\n";
658     }
659   }
660 }
661
662 # Deal with the one liners.
663 if (scalar(@oneliners) > 0) {
664
665   # Find the longest header
666   my $length = 0;
667   for $line (@oneliners) {
668     if (length($line->[0]) > $length) {
669       $length = length($line->[0]);
670     }
671   }
672
673   # Put the one liners together, with leading dots to the colon.
674   # We allow a hack for MS Outlook which normally recognises the
675   # oneliners block as a paragrap and wraps it.  If it sees "bullets"
676   # it believes it is a bulleted list and leaves it alone.
677   my $oneliners = "";
678   for $line (@oneliners) {
679     $oneliners .= ($c->{'oneliner_bullets'} ? '* ' : '' )
680       . "$line->[0]"
681       . ('.' x ($length - length($line->[0]))) . ": $line->[1]\n";
682   }
683
684   # What a hacky way of putting the one liners where I want them...
685   $output =~ s/^One Liners:\nPlace holder\n/$oneliners/m;
686 }
687
688 # Either print the status out, or email it.
689 if ($return_code_only) {
690   exit $exit_value;
691 } elsif (scalar(@{ $c->{'email'} }) == 0) {
692   if ($charset =~ /utf(-)?8/i) {
693     $output = encode('UTF-8', $output);
694   }
695   print "\n$output";
696 } else {
697   if ((! $c->{'email_only_on_alert'}) ||
698     ($c->{'email_only_on_alert'} && scalar(@alerts) > 0)) {
699     my $suffix = undef;
700     if (@alerts == 1) {
701       $suffix = $alerts[0];
702     } elsif (@alerts > 1) {
703       $suffix = "MULTIPLE WARNINGS";
704     }
705
706     my $mail = MIME::Entity->build(
707       To      => $c->{'email'},
708       Subject => encode('UTF-8', $title . (defined $suffix ? " - $suffix" : '')),
709       Charset => $charset,
710       Encoding=> "quoted-printable",
711       Data    => encode('UTF-8', $output),
712       );
713
714     $mail->send('sendmail');
715   }
716 }
717
718 exit $exit_value;
719
720 # Fetch the XML status from the backend.
721 sub load_xml {
722   my $status = '';
723   my $charset = '';
724
725   if (defined $c->{'xml_file'}) {
726     open (IN, "< $c->{'xml_file'}")
727       || die "Failed to open $c->{'xml_file'} for reading: $!\n";
728
729     $status = join("", <IN>);
730     $charset = 'UTF-8';
731
732     close IN;
733   } else {
734     my $content_type;
735     # In MythTV 0.25 the path changed from /xml to /Status/GetStatus
736     for my $path ('Status/GetStatus', 'xml') {
737       my $url = "http://$c->{'host'}:$c->{'port'}/$path";
738       ($content_type, $status) = xml_fetch($url);
739
740       last
741         if defined $status;
742     }
743
744     die "Nothing was received from the MythTV Backend.\n"
745       unless defined $status;
746     ($charset)  = ($content_type =~ /charset="(\S+?)"/);
747   }
748
749   if (defined $c->{'save_file'}) {
750     open(OUT, "> $c->{'save_file'}")
751       || die "Failed to open " . $c->{'save_file'} . " for writing: $!\n";
752     print OUT $status;
753     close OUT;
754   }
755
756   # Parse the XML
757   my $parser = XML::LibXML->new();
758
759   # Some XML data seems to have badness in it, including non-existant
760   # UTF-8 characters.  We'll try and recover.
761   $parser->recover(1);
762   $parser->recover_silently(1)
763     unless $verbose;
764
765   clean_xml(\$status);
766
767   # Try and hide any error messages that XML::LibXML insists on printing out.
768   open my $olderr, ">&STDERR";
769   open(STDERR, "> /dev/null") || die "Can't redirect stderr to /dev/null: $!";
770
771   my $xml = eval { $parser->parse_string( $status ) };
772
773   close (STDERR);
774   open (STDERR, ">&", $olderr);
775
776   if ($@) {
777     die "Failed to parse XML: $@\n";
778   }
779
780   # Pick out the XML version.
781   my $items = $xml->documentElement->find('//Status');
782   $version{'xml'}      = @{ $items }[0]->getAttribute('xmlVer') || 0;
783   $version{'protocol'} = @{ $items }[0]->getAttribute('protoVer');
784
785   warn "Loaded XML from " . ($c->{'xml_file'} || $c->{'host'}) . "\n"
786     if $verbose;
787
788   return ($xml, $charset);
789 }
790
791 # Prep the Perl MythTV API if available.
792 sub load_perl_api {
793   my $myth = undef;
794
795   eval { require MythTV };
796   if ($@) {
797     print $@
798       if $verbose;
799   } else {
800
801     # Surpress warnings from DBI.  I tried unsetting $^W but that is ignored.
802     local($SIG{__WARN__}) = sub { if ($verbose) { print shift } };
803     eval { $myth = new MythTV() };
804
805     if ($@) {
806       if ($verbose) {
807         warn "Failed to load Perl API\n";
808         print $@;
809         return undef;
810       }
811     } elsif ($verbose) {
812       warn "Loaded Perl API\n";
813     }
814   }
815
816   return $myth;
817 }
818
819 # We are sometimes passed dodgy XML from MythTV, make some attempts to clean
820 # it.
821 sub clean_xml {
822   my ($xml) = shift;
823
824   # Deal to invalid Unicode.
825   for my $bad ("&#xdbff;", "&#xdee9;") {
826     if ($$xml =~ s/$bad/?/g) {
827       warn "Found and replaced: $bad\n"
828         if $verbose;
829     }
830   }
831 }
832
833 sub process_xml {
834   my ($block, $xml) = @_;
835
836   # Only work on this block if we have received the appropriate version of
837   # the XML.
838   for my $vers (qw/protocol xml/) {
839     if (defined $block->{"${vers}_version"}) {
840       my $result = undef;
841
842       # All the version checks must pass.
843       for my $check (@{ $block->{"${vers}_version"} }) {
844         my $res = eval ( "$version{$vers} $check" );
845
846         if (! defined $result || $res != 1) {
847           $result = $res;
848         }
849       }
850
851       return
852         unless defined $result && $result ne '';
853
854       warn "We have the correct $vers version for $block->{'name'}\n"
855         if $verbose;
856     }
857   }
858
859   my $items = $xml->documentElement->find($block->{'xpath'});
860
861   # Don't do any work on this block if there is nothing for it.
862   return undef
863     if (scalar(@$items) == 0);
864
865   my @lines;
866   for my $item (@{ $items }) {
867     my %vars;
868     for my $key (@{ $block->{'attrs'} }) {
869       if ($key =~ /(.*?):(.*)/) {
870         my $subitem = $item->findnodes($2);
871         $vars{$1} = @{ $subitem }[0]->getAttribute($1)
872           if defined @{ $subitem }[0];
873       } else {
874         $vars{$key} = $key eq 'NODE_TEXT' ? $item->string_value : $item->getAttribute($key);
875       }
876     }
877
878     my $str = substitute_vars($block, \%vars);
879     push @lines, $str
880       if defined $str;
881   }
882
883   return join("\n", @lines);
884 }
885
886 sub process_conflicts {
887   my ($block) = @_;
888   $myth ||= load_perl_api();
889
890   return "Unable to access MythTV Perl API.  Try with --verbose to find out why."
891     unless defined $myth;
892
893   my @lines;
894
895   # This isn't defined in the 0.20 version of the API.  It is in 0.21svn.
896   my $recstatus_conflict = 7;
897
898   my %rows = $myth->backend_rows('QUERY_GETALLPENDING', 2);
899
900   foreach my $row (@{$rows{'rows'}}) {
901     my $show;
902     {
903       # MythTV::Program currently has a slightly broken line with a numeric
904       # comparision.
905       local($^W) = undef;
906       $show = new MythTV::Program(@$row);
907     }
908
909     if ($show->{'recstatus'} == $recstatus_conflict) {
910       my %vars = (
911         'title'     => $show->{'title'},
912         'startTime' => strftime("%FT%T", localtime($show->{'starttime'})),
913         'NODE_TEXT' => $show->{'description'},
914         'subTitle'  => $show->{'subtitle'},
915         'channelName' => $show->{'channame'},
916         'inputId'   => $show->{'inputid'},
917         'chanNum'   => $show->{'channum'},
918       );
919
920       my $str = substitute_vars($block, \%vars);
921       push @lines, decode('UTF-8', $str)
922         if defined $str;
923     }
924   }
925
926   if (scalar(@lines) == 1) {
927     push @alerts, "CONFLICT";
928   } elsif (scalar(@lines) > 1) {
929     push @alerts, "CONFLICTS";
930   }
931
932   return join("\n", @lines);
933 }
934
935 sub process_auto_expire {
936   my ($block) = @_;
937   $myth ||= load_perl_api();
938
939   return "Unable to access MythTV Perl API.  Try with --verbose to find out why."
940     unless defined $myth;
941
942   my @lines;
943
944   # This isn't defined in the 0.20 version of the API.  It is in 0.21svn.
945   my %rows = $myth->backend_rows('QUERY_RECORDINGS Delete', 2);
946
947   # Returned in date order, desc.  So reverse it to make the oldest
948   # ones come first.
949   foreach my $row (reverse @{$rows{'rows'}}) {
950     my $show;
951     {
952       # MythTV::Program currently has a slightly broken line with a numeric
953       # comparision.
954       local($^W) = undef;
955       $show = new MythTV::Program(@$row);
956     }
957
958     # Who cares about LiveTV recordings?
959     next if $show->{'progflags'} eq 'LiveTV';
960
961     my %vars = (
962       'title'     => $show->{'parentid'} || 'Unknown',
963       'startTime' => strftime("%FT%T", localtime($show->{'starttime'})),
964       'NODE_TEXT' => $show->{'description'},
965       'subTitle'  => $show->{'subtitle'},
966       'channelName' => $show->{'callsign'},
967       'inputId'   => $show->{'inputid'},
968       'chanNum'   => $show->{'chanid'},
969     );
970
971     my $str = substitute_vars($block, \%vars);
972     push @lines, decode('UTF-8', $str)
973       if defined $str;
974
975     # Don't do more work than is required.
976     last if --$c->{'auto_expire_count'} <= 0;
977   }
978
979   return join("\n", @lines);
980 }
981
982 # If either date or time are set to 0, then we don't display that bit of
983 # info.  For example:
984 #   process_iso_date($date, { date => 0 })
985 # Will only show the time.
986 sub process_iso_date {
987   my $date = shift;
988   my $options = shift;
989   $options->{'date'} //= 1;
990   $options->{'time'} //= 1;
991
992   # 2012-10-17T23:50:08Z
993   my $d = new Date::Manip::Date;
994   $d->parse($date);
995
996   # Work out our local timezone. The Date::Manip::Date
997   # docs say that convert will default to the local timezone,
998   # this appears to be lies.
999   my $dmb = $d->base();
1000   my ($tz) = $dmb->_now('tz',1);
1001   $d->convert($tz);
1002
1003   # Sample of what MythTV uses:
1004   # Thu 18 Oct 2012, 10:20
1005   my $format = '';
1006   $format .= '%Y-%m-%d' if $options->{'date'};
1007   $format .= ' '        if $options->{'date'} && $options->{'time'};
1008   $format .= '%X'       if $options->{'time'};
1009
1010   return $d->printf($format);
1011 }
1012
1013 sub substitute_vars {
1014   my $block = shift;
1015   my $vars  = shift;
1016
1017   my %commify = map { $_ => 1 } @{ $block->{'commify'} }
1018     if defined $block->{'commify'};
1019
1020   my $template = $block->{'template'};
1021   my $skip = undef;
1022   my ($key, $value);
1023
1024   # Convert disk spaces into more suitable units.
1025   if (defined $block->{'human_readable_sizes'}) {
1026     for my $key (@{ $block->{'human_readable_sizes'}}) {
1027       for my $unit (@size_thresholds) {
1028         if (defined $vars->{$key} && defined $unit->{'threshold'}) {
1029           if ($vars->{$key} > $unit->{'threshold'}) {
1030             $vars->{"si_$key"} = sprintf("%.1f", $vars->{$key} / $unit->{'conversion'});
1031             $vars->{"si_$key"} =~ s/\.0//;
1032             $vars->{"si_${key}_unit"} = $unit->{'unit'};
1033
1034             last;
1035           }
1036         } else {
1037           $vars->{"si_${key}"}      = $vars->{$key};
1038           $vars->{"si_${key}_unit"} = $unit->{'unit'};
1039         }
1040       }
1041     }
1042   }
1043
1044   while (($key, $value) = (each %{ $vars })) {
1045     if (! defined $value) {
1046       if ($block->{'optional'}) {
1047         warn "Unable to find any value for $key while at $block->{'name'}, marked as optional, skipping block.\n"
1048           if $verbose;
1049         return undef;
1050       } else {
1051         warn "Unable to find any value for $key while looking at $block->{'name'}\n";
1052         next;
1053       }
1054     }
1055
1056     $value = wrap('  ', '  ', $value)
1057       if $key eq 'NODE_TEXT';
1058
1059     $value =~ s/\s+$//;
1060     $value = 'Unknown'
1061       if $value eq '';
1062
1063     $skip = 1
1064       if defined $block->{'filter'}{$key} &&
1065       &{ $block->{'filter'}{$key} }($value, $vars);
1066
1067     if (defined $block->{'rewrite'}{"/$key/"}) {
1068       my ($search, $replace);
1069       while (($search, $replace) = each %{ $block->{'rewrite'}{"/$key/"} } ) {
1070         $value =~ s/$search/$replace/g;
1071       }
1072     }
1073
1074     if (defined $block->{'rewrite'}{"&$key"}) {
1075       $value = &{ $block->{'rewrite'}{"&$key"} }($value, $vars);
1076     }
1077
1078     $value = commify($value)
1079       if defined $commify{$key};
1080
1081     $template =~ s/__${key}__/$value/g;
1082   }
1083
1084   my ($name, $sub);
1085   while (($name, $sub) =  each %{ $block->{'subs'} }) {
1086     $value = &$sub($vars);
1087
1088     $template =~ s/__${name}__/$value/g
1089       if defined $value;
1090   }
1091
1092   return defined $skip ? undef : $template;
1093 }
1094
1095 # Work out the disk space percentage, possibly setting a flag that we should
1096 # raise an alert.
1097 sub calc_disk_space_percentage {
1098   my ($used, $total) = @_;
1099
1100   if (! (defined $used && defined $total) ){
1101     warn "Something is wrong calculating the disk space percentage.\n";
1102     return 'unknown';
1103   }
1104
1105   # Guard against zero disk space.
1106   $total = normalise_disk_space($total);
1107   if ($total == 0) {
1108     warn "Total disk space is 0 MB, I can't use that to calculate a percentage!\n";
1109     return 'unknown';
1110   }
1111
1112   my $percent = int((normalise_disk_space($used) / $total * 100) + 0.5);
1113
1114   if ($percent >= $c->{'disk_space_warn'}) {
1115     $exit_value ||= 1;
1116     push @alerts, "DISK SPACE";
1117     return "$warning$percent\%$normal";
1118   } else {
1119     return "$safe$percent\%$normal";
1120   }
1121 }
1122
1123 # Make sure that the disk space is in a common unit.
1124 # Currently that is MB.
1125 sub normalise_disk_space {
1126   if ($_[0] =~ /^[.0-9]+$/) {
1127     return $_[0];
1128   } elsif ($_[0] =~ /^([.0-9]+) (\w+)$/) {
1129     my $space = $1;
1130     my $unit = $2;
1131
1132     if ($unit eq 'B') {
1133       return $space / (1024 * 1024);
1134     } elsif ($unit eq 'KB') {
1135       return $space / 1024;
1136     } elsif ($unit eq 'MB') {
1137       return $space;
1138     } elsif ($unit eq 'GB') {
1139       return $space * 1024;
1140     } elsif ($unit eq 'TB') {
1141       return $space * 1024 * 1024;
1142     }
1143
1144     warn "Unknown unit for disk space: $unit.  Please let the author of mythtv-status know.\n";
1145     return $space;
1146   }
1147
1148   warn "Unrecognised format for disk space: $_[0].  Please let the author of mythtv-status know.\n";
1149   return $_[0];
1150 }
1151
1152 # Perform the fetch from the MythTV Backend in a child process.
1153 sub xml_fetch {
1154   my ($url) = @_;
1155
1156   $| = 1;
1157   my $pid = pipe_from_fork('CHILD');
1158   if ($pid) {
1159     # parent
1160     my $content_type;
1161     my $status;
1162
1163     eval {
1164       local $SIG{ALRM} = sub { die "alarm\n" };
1165       alarm(10);
1166       $content_type = <CHILD>;
1167       while (<CHILD>) {
1168         $status .= $_;
1169       }
1170       alarm(0);
1171     };
1172
1173     # The child didn't get back to us in time, kill them off
1174     # and forget what they sent us.
1175     if ($@) {
1176       $status = undef;
1177       my $result;
1178       warn "Our child has stopped talking to us, kill it off.\n";
1179       do {
1180         kill 9, $pid;
1181         $result = waitpid($pid, WNOHANG);
1182       } while $result > 0;
1183
1184       die "Unknown error during retrieval of status from the MythTV backend.\n";
1185     }
1186     $| = 0;
1187
1188     if (defined $content_type && $content_type =~ /utf(-)?8/i) {
1189       $status = decode('UTF-8', $status);
1190     }
1191     return ($content_type, $status);
1192   } else {
1193     # child
1194     $0 = "mythtv-status - child";
1195     my $ua = LWP::UserAgent->new;
1196     $ua->timeout(30);
1197     $ua->env_proxy;
1198
1199     my $response = ua_request_with_timeout($ua, $url);
1200     die "Sorry, failed to fetch $url: Connection to MythTV timed out.\n"
1201       unless defined $response;
1202
1203     # If we get a page doesn't exist, then just ignore it, we need to try
1204     # fetching the status page from a few different locations.
1205     if ($response->code == 404) {
1206       exit 1;
1207     }
1208
1209     die "Sorry, failed to fetch $url: " . $response->status_line . "\n"
1210       unless $response->is_success;
1211
1212     my $content = $response->decoded_content;
1213     if ($response->header('Content-Type') =~ /utf(-)?8/i) {
1214       $content = encode('UTF-8', $content);
1215     }
1216     print $response->header('Content-Type') . "\n";
1217     print $content . "\n";
1218
1219     exit 0;
1220   }
1221 }
1222
1223 # simulate open(FOO, "-|")
1224 sub pipe_from_fork ($) {
1225   my $parent = shift;
1226
1227   $SIG{CHLD} = 'IGNORE';
1228   pipe $parent, my $child or die;
1229   my $pid = fork();
1230   die "fork() failed: $!" unless defined $pid;
1231
1232   if ($pid) {
1233     close $child;
1234   } else {
1235     close $parent;
1236     open(STDOUT, ">&=" . fileno($child)) or die;
1237   }
1238   $pid;
1239 }
1240
1241 # Takes a LWP::UserAgent and an HTTP::Request.  Returns the result of the
1242 # HTTP::Request.  Handles hung servers as well as timeouts.  Based on:
1243 # http://stackoverflow.com/questions/73308/true-timeout-on-lwpuseragent-\
1244 #   request-method
1245 sub ua_request_with_timeout {
1246   my ($ua, $url) = @_;
1247   my ($req_timeout);
1248
1249   # Pick up LWP's request timeout setting.
1250   $req_timeout = $ua->timeout();
1251
1252   # If Sys::SigAction is available, we can use it to get whatever timeout
1253   # is set for LWP and use that to enforce a maximum timeout per request
1254   # in case of server deadlock.
1255   if (eval("{ local \$SIG{__DIE__}; require Sys::SigAction; }")) {
1256     our $resp = undef;
1257
1258     if (Sys::SigAction::timeout_call( $req_timeout,
1259         sub {$resp = $ua->get($url);})) {
1260       return undef;
1261     } else {
1262       return $resp;
1263     }
1264   }
1265
1266   # Otherwise, we roll a hard six with a SIGALRM for the timeout.
1267   else {
1268     my $resp = undef;
1269     our $req_has_timedout = 0;
1270     my ($newaction, $oldaction);
1271
1272     # Create a new SIGALRM handler to set the timed out flag if the
1273     # backend request is not answered before the interval has elapsed.
1274     # Note that die ends the request within the eval (below).  It is
1275     # caught by eval, allowing the code herein to continue and check
1276     # for a timeout.
1277     $newaction = POSIX::SigAction->new(
1278         sub { $req_has_timedout = 1; die "Backend request timeout"; },
1279         POSIX::SigSet->new(SIGALRM) );
1280
1281     # Replace the current SIGALRM handler with our new one, saving the
1282     # old one for restoration, later on.  If this fails, we just issue
1283     # the request directly and hope for the best.
1284     $oldaction = POSIX::SigAction->new();
1285
1286     if (!sigaction(SIGALRM, $newaction, $oldaction)) {
1287       warn "Error setting SIGALRM handler: ".$!."\n" if $verbose;
1288       return $ua->get($url);
1289     }
1290
1291     # Within an eval, set the timer and request a response from the
1292     # backend.  If the timer pops, the SIGALRM routine will set a flag
1293     # and kill the request.  The eval will catch it and we'll get on
1294     # with our lives.
1295     eval {
1296       alarm($req_timeout);
1297       $resp = $ua->get($url);
1298       alarm(0);
1299     };
1300
1301     # Cancel the SIGALRM, if the eval failed for any reason.  Reset the
1302     # SIGALRM handler back to its original state.
1303     alarm(0);
1304
1305     if (!sigaction(SIGALRM, $oldaction )) {
1306       warn "Error resetting SIGALRM handler: ".$!."\n" if $verbose;
1307     };
1308
1309     # If the request has timed out, return a HTTP 408 (timeout) response
1310     # or maybe just undef.  Otherwise, return the backend's response.
1311     if ($req_has_timedout) {
1312       warn "Backend request timed out (".$req_timeout." secs)\n" if $verbose;
1313 #      return HTTP::Response->new(408);
1314       return undef;
1315     } else {
1316       return $resp;
1317     }
1318   }
1319 }
1320
1321 # Beautify numbers by sticking commas in.
1322 sub commify {
1323   my ($num) = shift;
1324
1325   $num = reverse $num;
1326   $num =~ s<(\d\d\d)(?=\d)(?!\d*\.)><$1,>g;
1327   return reverse $num;
1328 }
1329
1330 sub print_version {
1331   print "mythtv-status, version $VERSION.\n";
1332   print "Written by Andrew Ruthven <andrew\@etc.gen.nz>\n";
1333   print "\n";
1334   exit;
1335 }
1336
1337 =head1 NAME
1338
1339 mythtv-status - Display the status of a MythTV backend
1340
1341 =head1 SYNOPSIS
1342
1343  mythtv-status [options]
1344
1345 =head1 DESCRIPTION
1346
1347 This script queries a MythTV backend and reports on the status of it,
1348 any upcoming recordings and any which are happening right now.
1349
1350 The intention is to warn you if there is a program being recorded or
1351 about to be recorded.
1352
1353 =head1 OPTIONS
1354
1355 =over
1356
1357 =item B<-c, --colour>
1358
1359 Use colour when showing the status of the encoder(s).
1360
1361 =item B<--date>
1362
1363 Set the date to run as, used for debugging purposes.
1364
1365 =item B<-d, --description>
1366
1367 Display the description for the scheduled recordings.
1368
1369 =item B<--disk-space-warn>
1370
1371 The threshold (in percent) of used disk space that we should show
1372 the disk space in red (if using colour) or send an email if we're
1373 in email mode with email only on warnings.
1374
1375 =item B<--encoder-details>
1376
1377 Display the input ID and channel name against the recording details.
1378
1379 =item B<--encoder-skip-idle>
1380
1381 Suppress displaying idle encoders in the Encoders block.
1382
1383 =item B<-e, --episode>
1384
1385 Display the episode (subtitle) for the scheduled recordings.
1386
1387 =item B<< --email <address>[ --email <address> ...] >>
1388
1389 Send the output to the listed email addresses.  By default the encoder status,
1390 currently recording shows and time till next recording is suppressed from
1391 the email.
1392
1393 To turn the additional blocks on you can use B<--encoders>, B<--recording-now>
1394 and/or B<--next-recording>.
1395
1396 =item B<--email-only-on-alert>
1397
1398 Only send an email out (if --email is present) if there is an alert
1399 (i.e., schedule conflict or low disk space).
1400
1401 =item B<-?, --help>
1402
1403 Display help.
1404
1405 =item B<< --file <file> >>
1406
1407 Load XML from the file specified instead of querying a MythTV backend.
1408 Handy for debugging things.
1409
1410 =item B<< --save-file <file> >>
1411
1412 Save the XML we received from the MythTV backend.
1413 Handy for debugging things.
1414
1415 =item B<< --guide-days-warn <days> >>
1416
1417 Warn if the number of days of guide data present is equal to or below
1418 this level.  Default is 2 days.
1419
1420 =item B<-h HOST, --host=HOST>
1421
1422 The host to check, defaults to localhost.
1423
1424 =item B<--nostatus>, B<--noencoders>, B<--norecording-now>, B<--noscheduled-recordings>, B<--noschedule-conflicts>, B<--nonext-recording>, B<--nototal-disk-space>, B<--nodisk-space>, B<--noguide-data>, B<--noauto-expire>
1425
1426 Suppress displaying blocks of the output if they would normally be displayed.
1427
1428 =item B<-p PORT, --port=PORT>
1429
1430 The port to use when connecting to MythTV, defaults to 6544.
1431
1432 =item B<--oneliner-bullets>
1433
1434 Insert asterisks (*) before each of the oneliners to stop some
1435 email clients from thinking the oneliner block is a paragraph and
1436 trying to word wrap them.
1437
1438 =item B<--auto-expire>
1439
1440 Display the shows due to auto expire (output is normally suppressed).
1441
1442 =item B<--auto-expire-count>
1443
1444 How many of the auto expire shows to display, defaults to 10.
1445
1446 =item B<--recording-in-warn>
1447
1448 If the "Next Recording In" time is less than this amount, display it
1449 in red.  This in seconds, and defaults to 3600 (1 hour).
1450
1451 =item B<--verbose>
1452
1453 Have slightly more verbose output.  This includes any warnings that might
1454 be generated while parsing the XML.
1455
1456 =item B<-v, --version>
1457
1458 Show the version of mythtv-status and then exit.
1459
1460 =back
1461
1462 =head1 OUTPUT
1463
1464 The output of this script is broken up into several chunks they are:
1465
1466 =over
1467
1468 =item Status
1469
1470 Some general info about the backend, currently just the timestamp of when
1471 this program was run.
1472
1473 =item Guide Data
1474
1475 The number of days of guide data is present.  By default it is only shown
1476 if the number of days is below the warning level.  To show it regardless
1477 of the warning level use --guide-data.
1478
1479 =item Encoders
1480
1481 Each encoder that the backend knows about are listed, with the hostname
1482 they are on, the encoder ID (in brackets) and the current status.
1483
1484 =item Recording Now
1485
1486 Any programs which are being recorded right now.
1487
1488 =item Scheduled Recordings
1489
1490 Up to 10 programs which are scheduled to be recorded today and tomorrow.
1491
1492 =item Schedule Conflicts
1493
1494 Any upcoming schedule conflicts (not just limited to today or tomorrow).
1495
1496 =item Shows due to Auto Expire
1497
1498 The shows which will be deleted and the order they'll be deleted if the
1499 auto expirer kicks in.
1500
1501 =item Total Disk Space
1502
1503 The amount of disk space in total, and used by MythTV.
1504
1505 =item Next Recording In
1506
1507 If there are no recordings currently happening, then the amount of time until
1508 the next recording is displayed.
1509
1510 =item Disk Space
1511
1512 Details about each storage group that MythTV knows about.  By default this
1513 only shows storage groups that are above the warning level.  Use
1514 B<--disk-space> to turn on display of all storage groups.
1515
1516 =back
1517
1518 =head1 RETURN CODES
1519
1520 mythtv-status provides some return codes.
1521
1522 =over
1523
1524 =item 0Z<>
1525
1526 Standard return code
1527
1528 =item 1Z<>
1529
1530 A warning is generated
1531
1532 =back
1533
1534 =head1 AUTHOR
1535
1536 Andrew Ruthven, andrew@etc.gen.nz
1537
1538 =head1 LICENSE
1539
1540 Copyright (c) 2007-2018 Andrew Ruthven <andrew@etc.gen.nz>
1541 This code is hereby licensed for public consumption under the GNU GPL v3.
1542
1543 =cut
1544