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