state.pl 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. use IO::Select;
  5. use IO::Socket::UNIX;
  6. use File::Basename;
  7. use File::Temp qw( tempfile );
  8. # Local modules
  9. use Cwd qw(abs_path);
  10. use File::Basename qw(dirname);
  11. use lib dirname(abs_path($0));
  12. use DMX;
  13. # Prototypes
  14. sub mtime($);
  15. # User config
  16. my $MEDIA_PATH = `~/bin/video/mediaPath`;
  17. my %DISPLAY_DEVS = ('TV' => 1, 'PROJECTOR' => 1);
  18. my $DISPLAY = '<NONE>';
  19. my $STATE_TIMEOUT = 180;
  20. my %MON_FILES = ();
  21. # Available state files
  22. {
  23. # Plex
  24. $MON_FILES{'GUI'} = 'STATUS_GUI';
  25. $MON_FILES{'PLAYING'} = 'STATUS_PLAYING';
  26. # RiffTrax
  27. $MON_FILES{'RIFF'} = 'STATUS_VALUE';
  28. # Motion detection
  29. $MON_FILES{'MOTION'} = 'MTIME';
  30. $MON_FILES{'NO_MOTION'} = 'EXISTS';
  31. # Projector
  32. $MON_FILES{'PROJECTOR'} = 'STATUS';
  33. $MON_FILES{'PROJECTOR_COLOR'} = 'STATUS_VALUE';
  34. $MON_FILES{'PROJECTOR_INPUT'} = 'STATUS_VALUE';
  35. # Amplifier
  36. $MON_FILES{'AMPLIFIER'} = 'STATUS';
  37. $MON_FILES{'AMPLIFIER_VOL'} = 'STATUS_VALUE';
  38. $MON_FILES{'AMPLIFIER_MODE'} = 'STATUS_VALUE';
  39. $MON_FILES{'AMPLIFIER_INPUT'} = 'STATUS_VALUE';
  40. $MON_FILES{'AUDIO_AMP'} = 'EXISTS_ON';
  41. $MON_FILES{'STEREO_CMD'} = 'EXISTS_ON';
  42. # A/V Effects
  43. $MON_FILES{'LIGHTS'} = 'EXISTS_ON';
  44. $MON_FILES{'RAVE'} = 'EXISTS';
  45. $MON_FILES{'EFFECT'} = 'EXISTS';
  46. # Equipment
  47. $MON_FILES{'FAN_CMD'} = 'EXISTS_ON';
  48. $MON_FILES{ $MEDIA_PATH . '/DMX/cmd/GARAGE_CMD' } = 'EXISTS_CLEAR';
  49. # OS State
  50. $MON_FILES{'COLOR'} = 'STATUS_VALUE';
  51. $MON_FILES{'AUDIO'} = 'STATUS_VALUE';
  52. $MON_FILES{'AUDIO_STATE'} = 'STATUS_VALUE';
  53. # TV
  54. $MON_FILES{'TV'} = 'STATUS';
  55. $MON_FILES{'TV_VOL'} = 'STATUS_VALUE';
  56. }
  57. my %EXTRAS = (
  58. 'PLAYING' => {
  59. 'URL' => qr/^\<li\>Filename\:(.+)$/m,
  60. 'YEAR' => qr/^\<li\>Year\:(\d+)/m,
  61. 'SERIES' => qr/^\<li\>Show Title\:(.+)$/m,
  62. 'SEASON' => qr/^\<li\>Season\:(\d+)/m,
  63. 'EPISODE' => qr/^\<li\>Episode\:(\d+)/m,
  64. 'TITLE' => qr/^\<li\>Title\:(.+)$/m,
  65. 'LENGTH' => qr/^\<li\>Duration\:([\d\:]+)/m,
  66. 'POSITION' => qr/^\<li\>Time\:([\d\:]+)/m,
  67. 'SIZE' => qr/^\<li\>File size\:(\d+)/m,
  68. 'TYPE' => qr/^\<li\>Type\:(.+)$/m,
  69. }
  70. );
  71. # App config
  72. my $SOCK_TIMEOUT = 5;
  73. my $DATA_DIR = DMX::dataDir();
  74. my $CMD_FILE = $DATA_DIR . 'STATE.socket';
  75. my $MAX_CMD_LEN = 4096;
  76. my $RESET_CMD = $ENV{'HOME'} . '/bin/video/dmx/reset.sh';
  77. my $PUSH_TIMEOUT = 20;
  78. # Debug
  79. my $DEBUG = 0;
  80. if ($ENV{'DEBUG'}) {
  81. $DEBUG = 1;
  82. }
  83. # Command-line arguments
  84. my ($DELAY) = @ARGV;
  85. if (!$DELAY) {
  86. $DELAY = 0.5;
  87. }
  88. # Socket init
  89. my $select = DMX::stateSocket($CMD_FILE);
  90. # Add the extras to the main file list
  91. foreach my $file (keys(%EXTRAS)) {
  92. if (exists($MON_FILES{$file})) {
  93. foreach my $extra (keys(%{ $EXTRAS{$file} })) {
  94. $MON_FILES{ $file . '_' . $extra } = 'NONE';
  95. }
  96. }
  97. }
  98. # Init the file tracking structure
  99. my %files = ();
  100. foreach my $file (keys(%MON_FILES)) {
  101. my %tmp = (
  102. 'name' => basename($file),
  103. 'type' => $MON_FILES{$file},
  104. 'path' => $DATA_DIR . $file,
  105. 'update' => 0,
  106. 'status' => 0,
  107. 'last' => 0,
  108. 'available' => 0,
  109. );
  110. # Allow absolute paths to override the $DATA_DIR path
  111. if ($file =~ /^\//) {
  112. $tmp{'path'} = $file;
  113. }
  114. # Record the directory name for folder monitoring
  115. $tmp{'dir'} = dirname($tmp{'path'});
  116. # Push a hash ref
  117. $files{$file} = \%tmp;
  118. }
  119. # Delete any existing input files, to ensure our state is reset
  120. foreach my $file (values(%files)) {
  121. if (-e $file->{'path'}) {
  122. if ($DEBUG) {
  123. print STDERR 'Deleting ' . $file->{'name'} . ': ' . $file->{'path'} . "\n";
  124. }
  125. unlink($file->{'path'});
  126. }
  127. }
  128. # Reset all dependents at starts
  129. system($RESET_CMD);
  130. # Subscribers
  131. my @subscribers = ();
  132. # State
  133. my $state = 'INIT';
  134. my $stateLast = $state;
  135. my $status = '';
  136. my $statusLast = $status;
  137. my $updateLast = 0;
  138. my $pushLast = 0;
  139. # Loop forever
  140. while (1) {
  141. # Provide a method to force updates even when the state does not change
  142. my $update = 0;
  143. # Check for queued commands
  144. my @ready_clients = $select->can_read($DELAY);
  145. foreach my $fh (@ready_clients) {
  146. # Grab the inbound text
  147. my $path = undef();
  148. $fh->recv($path, $MAX_CMD_LEN);
  149. $path =~ s/^\s+//;
  150. $path =~ s/\s+$//;
  151. if ($DEBUG) {
  152. print STDERR 'Got path: ' . $path . "\n";
  153. }
  154. # Only accept valid paths
  155. if (!-r $path) {
  156. print STDERR 'Invalid socket path: ' . $path . "\n";
  157. next;
  158. }
  159. # Open the new socket
  160. my $sub = IO::Socket::UNIX->new(
  161. 'Peer' => $path,
  162. 'Type' => SOCK_DGRAM,
  163. 'Timeout' => $SOCK_TIMEOUT
  164. );
  165. if (!$sub) {
  166. print STDERR 'Unable to open socket: ' . $path . ": ${@}\n";
  167. next;
  168. }
  169. # Add the socket to our subscriber list
  170. my %tmp = (
  171. 'path' => $path,
  172. 'socket' => $sub,
  173. );
  174. push(@subscribers, \%tmp);
  175. # Force an update
  176. $update = 1;
  177. }
  178. # Use opendir/readdir in each folder to ensure fresh results
  179. my %folders = ();
  180. foreach my $file (values(%files)) {
  181. if (!$folders{ $file->{'dir'} }) {
  182. my $dh = undef();
  183. if (opendir($dh, $file->{'dir'})) {
  184. if ($DEBUG) {
  185. print STDERR 'Refreshed folder: ' . $file->{'dir'} . "\n";
  186. }
  187. my @files = readdir($dh);
  188. closedir($dh);
  189. }
  190. $folders{ $file->{'dir'} } = 1;
  191. }
  192. }
  193. # Monitor files of all types
  194. foreach my $file (values(%files)) {
  195. # Skip "NONE" files -- they are data stores handled in other actions
  196. if ($file->{'type'} eq 'NONE') {
  197. next;
  198. }
  199. # Always record the previous status and clear the new one
  200. $file->{'last'} = $file->{'status'};
  201. $file->{'status'} = 0;
  202. # Track available/unavailable files
  203. {
  204. my $wasAvailable = $file->{'available'};
  205. $file->{'available'} = -r $file->{'path'} ? 1 : 0;
  206. if ($wasAvailable != $file->{'available'}) {
  207. # Reset and extras for this file
  208. if (exists($EXTRAS{ $file->{'name'} })) {
  209. foreach my $extra (keys(%{ $EXTRAS{ $file->{'name'} } })) {
  210. my $name = $file->{'name'} . '_' . $extra;
  211. $files{$name}{'last'} = $files{$name}{'status'};
  212. $files{$name}{'status'} = 0;
  213. }
  214. }
  215. # Determine which display device we have (if any)
  216. if (exists($DISPLAY_DEVS{ $file->{'name'} })) {
  217. $DISPLAY = $file->{'name'};
  218. }
  219. if ($DEBUG) {
  220. if ($file->{'available'}) {
  221. print STDERR 'Added file ' . $file->{'name'} . ': ' . $file->{'path'} . "\n";
  222. } else {
  223. print STDERR 'Dropped file ' . $file->{'name'} . ': ' . $file->{'path'} . "\n";
  224. }
  225. }
  226. }
  227. }
  228. # Skip unavailable files
  229. if (!$file->{'available'}) {
  230. next;
  231. }
  232. # Record the last update for STATUS and MTIME files
  233. if ($file->{'type'} =~ /^STATUS/ || $file->{'type'} =~ /^MTIME/) {
  234. $file->{'update'} = mtime($file->{'path'});
  235. if ($DEBUG) {
  236. print STDERR 'Last change: ' . $file->{'name'} . ': ' . localtime($file->{'update'}) . "\n";
  237. }
  238. }
  239. # Grab the state from STATUS files
  240. if ($file->{'type'} =~ /^STATUS/) {
  241. my $text = '';
  242. {
  243. if (!open(my $fh, $file->{'path'})) {
  244. warn('Unable to open ' . $file->{'path'} . "\n");
  245. } else {
  246. local $/;
  247. $text = <$fh>;
  248. close($fh);
  249. }
  250. }
  251. # Record the main status for the file
  252. if ($file->{'type'} eq 'STATUS_PLAYING') {
  253. if ($text =~ /PlayStatus\:Playing/) {
  254. $file->{'status'} = 1;
  255. }
  256. } elsif ($file->{'type'} eq 'STATUS_GUI') {
  257. if (!($text =~ /ActiveWindowName\:Fullscreen video/)) {
  258. $file->{'status'} = 1;
  259. }
  260. } elsif ($file->{'type'} eq 'STATUS_VALUE') {
  261. $text =~ s/\n$//;
  262. $file->{'status'} = $text;
  263. } else {
  264. if ($text =~ /1/) {
  265. $file->{'status'} = 1;
  266. }
  267. }
  268. if ($DEBUG) {
  269. print STDERR 'Status: ' . $file->{'name'} . ': ' . $file->{'status'} . "\n";
  270. }
  271. # Handle extras, if any
  272. if (exists($EXTRAS{ $file->{'name'} })) {
  273. foreach my $extra (keys(%{ $EXTRAS{ $file->{'name'} } })) {
  274. my $name = $file->{'name'} . '_' . $extra;
  275. $files{$name}{'last'} = $files{$name}{'status'};
  276. $files{$name}{'status'} = 0;
  277. $files{$name}{'update'} = $file->{'update'};
  278. if ($text =~ $EXTRAS{ $file->{'name'} }{$extra}) {
  279. $files{$name}{'status'} = $1;
  280. }
  281. if ($DEBUG) {
  282. print STDERR 'Status (extra): ' . $name . ': ' . $files{$name}{'status'} . "\n";
  283. }
  284. }
  285. }
  286. }
  287. # Check for the presence of EXISTS files and record their last change
  288. if ($file->{'type'} =~ /^EXISTS/) {
  289. my $mtime = mtime($file->{'path'});
  290. if ($mtime > 0) {
  291. $file->{'status'} = 1;
  292. $file->{'update'} = $mtime;
  293. } elsif ($file->{'last'}) {
  294. $file->{'update'} = time();
  295. }
  296. if ($DEBUG) {
  297. print STDERR 'Exists: ' . $file->{'name'} . ': ' . $file->{'status'} . ':' . $file->{'update'} . "\n";
  298. }
  299. }
  300. }
  301. # Set the global update timestamp
  302. foreach my $file (values(%files)) {
  303. if ($file->{'update'} > $updateLast) {
  304. $updateLast = $file->{'update'};
  305. }
  306. }
  307. # Calculate the new state
  308. $stateLast = $state;
  309. if (exists($files{$DISPLAY}) && $files{$DISPLAY}->{'status'}) {
  310. # We are always either playing or paused if the display is on
  311. # When we're playing "Audio" assume we are paused
  312. if ( (exists($files{'PLAYING'}) && $files{'PLAYING'}->{'status'})
  313. && ((exists($files{'PLAYING_TYPE'}) && $files{'PLAYING_TYPE'}->{'status'} ne 'Audio') || !exists($files{'PLAYING_TYPE'})))
  314. {
  315. $state = 'PLAY';
  316. } else {
  317. $state = 'PAUSE';
  318. }
  319. } else {
  320. # If the display is off, check the timeouts
  321. my $timeSinceUpdate = time() - $updateLast;
  322. if ($timeSinceUpdate > $STATE_TIMEOUT) {
  323. $state = 'OFF';
  324. } elsif ($timeSinceUpdate < $STATE_TIMEOUT) {
  325. if (exists($files{'NO_MOTION'}) && $files{'NO_MOTION'}->{'status'}) {
  326. $state = 'OFF';
  327. } else {
  328. $state = 'MOTION';
  329. }
  330. }
  331. }
  332. if ($state eq 'INIT') {
  333. $state = 'OFF';
  334. }
  335. # Clear EXISTS_ON files when the main state is "OFF" (and before we append their status)
  336. foreach my $file (values(%files)) {
  337. if ($file->{'type'} eq 'EXISTS_ON' && $file->{'status'} && $state eq 'OFF') {
  338. unlink($file->{'path'});
  339. $file->{'stauts'} = 0;
  340. if ($DEBUG) {
  341. print STDERR 'Clearing exists flag for: ' . $file->{'name'} . "\n";
  342. }
  343. }
  344. }
  345. # Calculate the new status
  346. $statusLast = $status;
  347. {
  348. my @statTime = ();
  349. foreach my $file (values(%files)) {
  350. my $text = $file->{'status'};
  351. $text =~ s/\s/ /g;
  352. $text =~ s/\|/-/g;
  353. push(@statTime, $file->{'name'} . '|' . $text . '|' . $file->{'update'});
  354. }
  355. $status = "\n" . join("\n", @statTime);
  356. }
  357. # Clear EXISTS_CLEAR files immediately (but after we append their status)
  358. foreach my $file (values(%files)) {
  359. if ($file->{'type'} eq 'EXISTS_CLEAR' && $file->{'status'}) {
  360. unlink($file->{'path'});
  361. $file->{'stauts'} = 0;
  362. if ($DEBUG) {
  363. print STDERR 'Clearing exists flag for: ' . $file->{'name'} . "\n";
  364. }
  365. }
  366. }
  367. # Force updates on a periodic basis
  368. if (time() - $pushLast > $PUSH_TIMEOUT) {
  369. $update = 1;
  370. }
  371. # Update on any status change
  372. foreach my $file (values(%files)) {
  373. if ($file->{'status'} ne $file->{'last'}) {
  374. $update = 1;
  375. last;
  376. }
  377. }
  378. # Force updates on any state change
  379. if ($stateLast ne $state || $statusLast ne $status) {
  380. if ($DEBUG) {
  381. print STDERR 'State change: ' . $stateLast . $statusLast . ' => ' . $state . $status . "\n";
  382. }
  383. $update = 1;
  384. }
  385. # Push the update
  386. if ($update) {
  387. # Note the push
  388. $pushLast = time();
  389. # Send notifications to all subscribers
  390. foreach my $sub (@subscribers) {
  391. # Drop subscribers that are not available
  392. if (!eval { $sub->{'socket'}->send($state . $status) }) {
  393. print STDERR 'Dropping bad socket from subscriber list: ' . $sub->{'path'} . "\n";
  394. my @new_subscribers = ();
  395. foreach my $new (@subscribers) {
  396. if ($new->{'socket'} eq $sub->{'socket'}) {
  397. next;
  398. }
  399. push(@new_subscribers, $new);
  400. }
  401. @subscribers = @new_subscribers;
  402. }
  403. }
  404. # Save the state and value to disk
  405. my ($fh, $tmp) = tempfile($DATA_DIR . 'STATE.XXXXXXXX', 'UNLINK' => 0);
  406. print $fh $state . "\n";
  407. close($fh);
  408. rename($tmp, $DATA_DIR . 'STATE');
  409. }
  410. }
  411. sub mtime($) {
  412. my ($file) = @_;
  413. my $mtime = 0;
  414. if (-r $file) {
  415. (undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), undef(), $mtime, undef(), undef(), undef()) = stat($file);
  416. }
  417. return $mtime;
  418. }