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