-
Notifications
You must be signed in to change notification settings - Fork 59
/
plexWatch.pl
executable file
·4476 lines (3744 loc) · 177 KB
/
plexWatch.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/perl
my $version = '0.3.4';
my $author_info = <<EOF;
##########################################
# Author: Rob Reed
# Created: 2013-06-26
# Modified: 2016-01-30 12:05 PST
#
# Version: $version
# https://github.com/ljunkie/plexWatch
##########################################
EOF
use strict;
use LWP::UserAgent;
use XML::Simple;
use DBI;
use Time::Duration;
use Getopt::Long;
use Pod::Usage;
use Fcntl qw(:flock);
use POSIX qw(strftime);
use File::Basename;
use warnings;
use Time::Local;
use open qw/:std :utf8/; ## default encoding of these filehandles all at once (binmode could also be used)
use utf8;
use Encode;
use JSON;
use IO::Socket::SSL qw( SSL_VERIFY_NONE);
## windows
if ($^O eq 'MSWin32') {
}
## non windows
if ($^O ne 'MSWin32') {
require Time::ParseDate;
Time::ParseDate->import();
}
## end
## load config file
my $dirname = dirname(__FILE__);
if (!-e $dirname .'/config.pl') {
my $msg = "** missing file $dirname/config.pl. Did you move edit config.pl-dist and copy to config.pl?";
&DebugLog($msg,1) if $msg;
exit;
}
our ($data_dir, $server, $port, $appname, $user_display, $alert_format, $notify, $push_titles, $backup_opts, $myPlex_user, $myPlex_pass, $server_log, $log_client_ip, $debug_logging, $watched_show_completed, $watched_grouping_maxhr, $count_paused, $inc_non_library_content, @exclude_library_ids);
my @config_vars = ("data_dir", "server", "port", "appname", "user_display", "alert_format", "notify", "push_titles", "backup_opts", "myPlex_user", "myPlex_pass", "server_log", "log_client_ip", "debug_logging", "watched_show_completed", "watched_grouping_maxhr", "count_paused", "exclude_library_ids");
do $dirname.'/config.pl';
if (!$data_dir || !$server || !$port || !$appname || !$alert_format || !$notify) {
## TODO - make this information a little more useful!
my $msg = "config file missing data";
&DebugLog($msg,1) if $msg;
exit;
}
## end
############################################
## Advanced Options (override in config.pl)
## always display a 100% watched show. I.E. if user watches show1 100%, then restarts it and stops at < 90%, show two lines
$watched_show_completed = 1 if !defined($watched_show_completed);
## how many hours between starts of the same show do we allow grouping? 24 is max (3 hour default)
$watched_grouping_maxhr = 3 if !defined($watched_grouping_maxhr);
## DO not include non library content by default ( channels )
$inc_non_library_content = 0 if !defined($inc_non_library_content);
my $max_ra_backlog = 2; ## not added to config yet ( keep trying RA backlog for 2 days max )
## end
############################################
## for now, let's warn the user if they have enabled logging of clients IP's and the server log is not found
if ($server_log && !-f $server_log) {
my $msg = "warning: \$server_log is specified in config.pl and $server_log does not exist (required for logging of the clients IP address)\n" if $log_client_ip;
&DebugLog($msg,1) if $msg;
}
## ONLY Load modules if used
if (&ProviderEnabled('twitter')) {
require Net::Twitter::Lite;
require Net::Twitter::Lite::WithAPIv1_1;
require Net::OAuth;
require Scalar::Util;
Net::Twitter::Lite->import();
Net::Twitter::Lite::WithAPIv1_1->import();
Net::OAuth->import();
Scalar::Util->import('blessed');
}
if (&ProviderEnabled('GNTP')) {
require Growl::GNTP;
Growl::GNTP->import();
}
if (&ProviderEnabled('EMAIL')) {
require Net::SMTPS;
Net::SMTPS->import();
}
if ($log_client_ip) {
require File::ReadBackwards;
File::ReadBackwards->import();
}
## used for later..
my $format_options = {
'user' => 'user',
'orig_user' => 'orig_user',
'title' => 'title',
'start_start' => 'start_time',
'stop_time' => 'stop_time',
'rating' => 'rating of video - TV-MA, R, PG-13, etc',
'year' => 'year of video',
'platform' => 'client platform ',
'summary' => 'summary or video',
'duration' => 'duration watched',
'length' => 'length of video',
'progress' => 'progress of video [only available/correct on --watching and stop events]',
'time_left' => 'progress of video [only available/correct on --watching and stop events]',
'streamtype_extended' => '"Direct Play" or "Audio:copy Video:transcode"',
'streamtype' => 'T or D - for Transcoded or Direct',
'transcoded' => '1 or 0 - if transcoded',
'state' => 'playing, paused or buffering [ or stopped ] (useful on --watching)',
'percent_complete' => 'Percent of video watched -- user could have only watched 5 minutes, but skipped to end = 100%',
'ip_address' => 'Client IP Address',
};
if (!-d $data_dir) {
my $msg = "** Sorry. Please create your datadir $data_dir";
&DebugLog($msg,1) if $msg;
exit;
}
## place holder to back off notifications per provider
my $provider_452 = ();
# Grab our options.
my %options = ();
GetOptions(\%options,
'watched',
'nogrouping',
'stats',
'user:s',
'exclude_user:s@',
'exclude_library_id:s@',
'watching',
'notify',
'debug:s',
'start:s',
'stop:s',
'format_start:s',
'format_stop:s',
'format_watched:s',
'format_watching:s',
'format_options',
'test_notify:s',
'recently_added:s',
'id:s@',
'version',
'backup',
'clean_extras',
'show_xml',
'help|?'
) or pod2usage(2);
pod2usage(-verbose => 2) if (exists($options{'help'}));
if ($options{version}) {
print "\n\tVersion: $version\n\n";
print "$author_info\n";
exit;
}
my $debug_xml = $options{'show_xml'};
## ONLY load modules if used
if (defined($options{debug})) {
require Data::Dumper;
Data::Dumper->import();
if ($options{debug} =~ /\d/ && $options{debug} > 1) {
require diagnostics;
diagnostics->import();
} else {
$options{debug} = 1;
}
}
my $debug = $options{'debug'};
if ($options{'format_options'}) {
print "\nFormat Options for alerts\n";
print "\n\t --start='" . $alert_format->{'start'} ."'";
print "\n\t --stop='" . $alert_format->{'stop'} ."'";
print "\n\t --watched='" . $alert_format->{'watched'} ."'";
print "\n\t --watching='" . $alert_format->{'watching'} ."'";
print "\n\n";
foreach my $k (sort keys %{$format_options}) {
printf("%25s %s\n", "{$k}", $format_options->{$k});
}
print "\n";
exit;
}
## reset format if specified
$alert_format->{'start'} = $options{'format_start'} if $options{'format_start'};
$alert_format->{'stop'} = $options{'format_stop'} if $options{'format_stop'};
$alert_format->{'watched'} = $options{'format_watched'} if $options{'format_watched'};
$alert_format->{'watching'} = $options{'format_watching'} if $options{'format_watching'};
my %notify_func = &GetNotifyfuncs();
my $push_type_titles = &GetPushTitles();
## Check LOCK
# only allow one script run at a time.
# Before initDB
my $script_fh;
&CheckLock();
## END
my $dbh = &initDB(); ## Initialize sqlite db - last
my $DBversion = &checkVersion();
if ($DBversion ne $version) {
print "* Upgrading the plexWatch from $DBversion to $version -- (Forcing Backup)\n";
$options{'backup'} = 1;
}
&UpdateConfig(\@config_vars);
# clean any extras we may have logged. Make sure to run this before any
# backup or grouped table update as those come along with this.
if ($options{'clean_extras'}) {
&extrasCleaner();
exit;
}
if (&getLastGroupedTime() == 0) { &UpdateGroupedTable;} ## update DB table if first run.
&BackupSQlite; ## check if the SQLdb needs to be backed up
## update any missing rating keys
&updateRatingKeys();
my $PMS_token = &PMSToken(); # sets token if required
########################################## START MAIN #######################################################
## show what the notify alerts will look like
if (defined($options{test_notify})) {
&RunTestNotify();
exit;
}
####################################################################
## RECENTLY ADDED
if (defined($options{'recently_added'})) {
my $plex_sections = &GetSectionsIDs(); ## allow for multiple sections with the same type (movie, show, etc) -- or different types (2013-08-01)
my @want;
my @merged = ();
my $hkey = 'Video'; # for now, the only available option is Video
## backwards compatibility
if (!$options{'id'}) {
if ($options{'recently_added'} =~ /movie/i) {
push @want , 'movie';
}
## TO NOTE: plex used show and episode off an on. code for both
if ($options{'recently_added'} =~ /show|tv|episode/i) {
push @want , 'show';
}
foreach my $w (@want) {
foreach my $v (@{$plex_sections->{'types'}->{$w}}) { push (@merged, $v); }
}
} else {
# use the specific ID's specified
foreach my $id ( @{$options{'id'}} ) {
if (ref($plex_sections->{'raw'}->{$id})) {
push @want , $plex_sections->{'raw'}->{$id}->{'type'};
push @merged , $id;
} else {
print "\n\t**FAILURE - Section ID:$id does not exists!\n";
}
}
}
## show usage if the command didn't fit the bill
if (!@merged) {
print "\n\t* Available Sections:\n\n";
printf("\t%-5s %-20s %-10s %-20s\n", 'ID','Title','Type','Path');
print "\t-------------------------------------------------------------------\n";
foreach my $key (keys %{$plex_sections->{'raw'}}) {
next if $plex_sections->{'raw'}->{$key}->{'type'} !~ /movie|show|tv|episode/;
printf("\t%-5s %-20s %-10s %-20s\n", $key,$plex_sections->{'raw'}->{$key}->{'title'},$plex_sections->{'raw'}->{$key}->{'type'}, $plex_sections->{'raw'}->{$key}->{'Location'}->{'path'});
}
my $msg = "\n\t* Usage: \n\n";
$msg .= sprintf("\t%-22s: %s\n",'All Movie Sections',"$0 --recently_added=movie");
$msg .= sprintf("\t%-22s: %s\n",'All Movie/TV Sections',"$0 --recently_added=movie,show");
$msg .= sprintf("\t%-22s: %s\n",'Specific Section(s)',"$0 --recently_added --id=# --id=#");
print $msg . "\n";
exit;
}
my $info = &GetRecentlyAdded(\@merged,$hkey);
my $alerts = (); # containers to push alerts from oldest -> newest
my %seen;
foreach my $k (keys %{$info}) {
$seen{$k} = 1; ## alert seen
if (!ref($info->{$k})) {
my $msg = "Skipping KEY '$k' (expected key =~ '/library/metadata/###') -- it's not a hash ref?\n";
$msg .= "\$info->{'$k'} is not a hash ref?\n";
&DebugLog($msg,1) if $msg;
print Dumper($info->{$k}) if $options{debug};
next;
}
my $item = &ParseDataItem($info->{$k});
next if (!ref($item) or !$item->{'title'} or $item->{'title'} !~ /\w+/); ## verify we can parse the metadata ( sometimes the scanner is still filling in the info )
my $res = &RAdataAlert($k,$item);
next if (!ref($res) or !$res->{'alert'} or $res->{'alert'} !~ /\w+/ ); ## verify we can parse the metadata ( sometimes the scanner is still filling in the info )
$alerts->{$item->{addedAt}.$k} = $res;
}
## RA backlog - make sure we have all alerts -- some might has been added previously but notification failed and newer content has purged the results above
my $ra_done = &GetRecentlyAddedDB($max_ra_backlog);
my $push_type = 'push_recentlyadded';
foreach my $provider (keys %{$notify}) {
next if (!&ProviderEnabled($provider,$push_type));
#next if ( !$notify->{$provider}->{'enabled'} || !$notify->{$provider}->{$push_type}); ## skip provider if not enabled
foreach my $key (keys %{$ra_done}) {
next if $seen{$key}; ## already in alerts hash
next if ($ra_done->{$key}->{$provider}); ## provider already notified
## we passed checks -- let's process this old/failed notification
my $data = &GetItemMetadata($key,1);
## if result is not a href ( it's possible the video has been removed from the PMS )
if (!ref($data)) {
## maybe we got 404 -- I.E. old/removed video.. set at 404 -> not found
if ($data =~ /404/) {
&SetNotified_RA($provider,$key,404);
next;
}
## any other results we care about? maybe later
}
else {
my $item = &ParseDataItem($data);
## for back log -- verify we are only checking the type we have specified
my %wmap = map { $_ => 1 } @want;
next if $data->{'type'} =~ /episode/ && !exists($wmap{'show'}); ## next if episode and current task is not a show
next if $data->{'type'} =~ /movie/ && !exists($wmap{'movie'}); ## next if episode and current task is not a show
## check age of notification. -- allow two days ( we will keep trying to notify for 2 days.. if we keep failing.. we need to skip this)
my $age = time()-$ra_done->{$key}->{'time'};
my $ra_max_fail_days = $max_ra_backlog; ## TODO: advanced config options?
if ($age > 86400*$ra_max_fail_days) {
## notification is OLD .. set notify = 2 to exclude from processing
my $msg = "Could not notify $provider on [$key] $item->{'title'} for " . &durationrr($age) . " -- setting as old notification/done";
&ConsoleLog($msg,,1);
&SetNotified_RA($provider,$key,2);
}
if ($alerts->{$item->{addedAt}.$key}) {
## redundant code from above hash %seen
#print "$item->{'title'} is already in current releases... nothing missed\n";
} else {
my $msg = "$item->{'title'} is NOT in current releases -- we failed to notify previously, so trying again";
&DebugLog($msg,1) if $msg;
my $res = &RAdataAlert($key,$item);
$alerts->{$item->{addedAt}.$key} = $res;
}
}
}
}
&ProcessRAalerts($alerts) if ref($alerts);
}
sub RAdataAlert() {
my $item_id = shift;
my $item = shift;
my $result;
my $add_date = &twittime($item->{addedAt});
my $debug_done = '';
$debug_done .= $item->{'grandparentTitle'} . ' - ' if $item->{'grandparentTitle'};
$debug_done .= $item->{'title'} if $item->{'title'};
$debug_done .= " [$add_date]";
my $alert = 'unknown type';
my ($alert_url);
my $media;
$media .= $item->{'videoResolution'}.'p ' if $item->{'videoResolution'};
$media .= $item->{'audioChannels'}.'ch' if $item->{'audioChannels'};
##my $twitter; #twitter sucks... has to be short. --- might use this later.
if ($item->{'type'} eq 'show' || $item->{'type'} eq 'episode') {
$alert = $item->{'title'};
$alert .= " [$item->{'contentRating'}]" if $item->{'contentRating'};
$alert .= " [$item->{'year'}]" if $item->{'year'};
if ($item->{'duration'} && ($item->{'duration'} =~ /\d+/ && $item->{'duration'} > 1000)) {
$alert .= ' '. sprintf("%.0f",$item->{'duration'}/1000/60) . 'min';
}
$alert .= " [$media]" if $media;
$alert .= " [$add_date]";
#$twitter = $item->{'title'};
#$twitter .= " [$item->{'year'}]";
#$twitter .= ' '. sprintf("%.02d",$item->{'duration'}/1000/60) . 'min';
#$twitter .= " [$media]" if $media;
#$twitter .= " [$add_date]";
$alert_url .= ' http://www.imdb.com/find?s=tt&q=' . urlencode($item->{'imdb_title'});
}
if ($item->{'type'} eq 'movie') {
$alert = $item->{'title'};
$alert .= " [$item->{'contentRating'}]" if $item->{'contentRating'};
$alert .= " [$item->{'year'}]" if $item->{'year'};
if ($item->{'duration'} && ($item->{'duration'} =~ /\d+/ && $item->{'duration'} > 1000)) {
$alert .= ' '. sprintf("%.0f",$item->{'duration'}/1000/60) . 'min';
}
$alert .= " [$media]" if $media;
$alert .= " [$add_date]";
#$twitter = $alert; ## movies are normally short enough.
$alert_url .= ' http://www.imdb.com/find?s=tt&q=' . urlencode($item->{'imdb_title'});
}
$result->{'alert'} = $alert;
$result->{'item_id'} = $item_id;
$result->{'debug_done'} = $debug_done;
$result->{'alert_url'} = $alert_url;
$result->{'item_type'} = $item->{'type'};
return $result;
}
###############################################################################################################3
## --watched, --watching, --stats
####################################################################
## display the output is limited by user (display user)
if ( ($options{'watched'} || $options{'watching'} || $options{'stats'}) && $options{'user'}) {
my $extra = '';
$extra = $user_display->{$options{'user'}} if $user_display->{$options{'user'}};
foreach my $u (keys %{$user_display}) {
$extra = $u if $user_display->{$u} =~ /$options{'user'}/i;
}
$extra = '[' . $extra .']' if $extra;
printf("\n* Limiting results to %s %s\n", $options{'user'}, $extra);
}
## debug for now -- force updating the watched table
####################################################################
## print all watched content
##--watched
&ShowWatched;
## no options -- we can continue.. otherwise --stats, --watched, --watching or --notify MUST be specified
if (%options && !$options{'notify'} && !$options{'stats'} && !$options{'watched'} && !$options{'watching'} && !defined($options{'recently_added'}) ) {
my $msg = "* Skipping any Notifications -- command line options set, use '--notify' or supply no options to enable notifications";
print "\n$msg\n\n";
&DebugLog($msg) if $msg;
exit;
}
## set notify to 1 if we call --watching ( we need to either log start/update/stop current progress)
if ($options{'watching'} && !$options{'notify'}) {
$options{'notify'} = 2; #set notify to 2 -- meaning will will run through notify process to update current info, but we wiill not set as notified
}
elsif (!%options) {
$options{'notify'} = 1;
}
#################################################################
## Notify -notify || no options = notify on watch/stopped streams
##--notify
if ($options{'notify'}) {
my $live = &GetSessions(); ## query API for current streams
my $started= &GetStarted(); ## query streams already started/not stopped
my $playing = (); ## container of now playing id's - used for stopped status/notification
###########################################################################
## nothing being watched.. verify all notification went out
## this shouldn't happen ( only happened during development when I was testing -- but just in case )
#### to fix
## Quick hack to notify stopped content before start -- get a list of playing content
foreach my $k (keys %{$live}) {
my $userID = $live->{$k}->{User}->{id};
$userID = 'Local' if !$userID;
my $db_key = $k . '_' . $live->{$k}->{key} . '_' . $userID;
$playing->{$db_key} = 1;
}
## make sure we send out notifications -- this can happen when people call --watching and a new video started or stopped before --notify was called
my $did_unnotify = 0;
if ($options{'notify'} != 2) {
my $un = &GetUnNotified();
foreach my $k (keys %{$un}) {
my $start_epoch = $un->{$k}->{time} if $un->{$k}->{time};
my $stop_epoch = $un->{$k}->{stopped} if $un->{$k}->{stopped};
$stop_epoch = time() if !$stop_epoch; # we may not have a stop time yet.. lets set it.
my $ntype = 'stop';
$ntype = 'start' if ($playing->{$k});
my $paused = &getSecPaused($k);
my $info = &info_from_xml($un->{$k}->{'xml'},$ntype,$start_epoch,$stop_epoch,$paused);
$info->{'ip_address'} = $un->{$k}->{ip_address};
&DebugLog("sending unnotify for alert for ".$un->{$k}->{user}.':'.$un->{$k}->{title});
&Notify($info);
&SetNotified($un->{$k}->{id});
$did_unnotify = 1;
}
}
$started= &GetStarted() if $did_unnotify; ## refresh started if we notified
## Notify on any Stop
## Iterate through all non-stopped content and notify if not playing
if (ref($started)) {
foreach my $k (keys %{$started}) {
if (!$playing->{$k}) {
my $start_epoch = $started->{$k}->{time} if $started->{$k}->{time};
my $stop_epoch = time();
## process the update - need to supply the original XML (as an xml_ref) and session_id
my $xml_ref = XMLin($started->{$k}->{'xml'},KeyAttr => { Video => 'sessionKey' }, ForceArray => ['Video']);
$xml_ref->{Player}->{'state'} = 'stopped'; # force state as 'stopped' (since this XML is from the DB)
&ProcessUpdate($xml_ref, $started->{$k}->{'session_id'} ); ## go through normal update -- will set paused counter etc..
my $paused = &getSecPaused($k);
my $info = &info_from_xml($started->{$k}->{'xml'},'stop',$start_epoch,$stop_epoch,$paused);
$info->{'ip_address'} = $started->{$k}->{ip_address};
&SetStopped($started->{$k}->{id},$stop_epoch); # will mark as unnotified
$info->{'decoded'} = 1; ## XML - already decoded
&Notify($info) if $options{'notify'} != 2;
&SetNotified($started->{$k}->{id}) if $options{'notify'} != 2;
}
}
}
## Notify on start/now playing
foreach my $k (keys %{$live}) {
my $start_epoch = time();
my $stop_epoch = ''; ## not stopped yet
my $info = &info_from_xml(XMLout($live->{$k}),'start',$start_epoch,$stop_epoch,0);
$info->{'decoded'} = 1; ## live XML - already decoded
## for insert
my $userID = $info->{userID};
$userID = 'Local' if !$userID;
my $db_key = $k . '_' . $live->{$k}->{key} . '_' . $userID;
## these shouldn't be neede any more - to clean up as we now use XML data from DB
$info->{'orig_title'} = $live->{$k}->{title};
$info->{'orig_title_ep'} = '';
$info->{'episode'} = '';
$info->{'season'} = '';
$info->{'genre'} = '';
if ($live->{$k}->{grandparentTitle}) {
$info->{'orig_title'} = $live->{$k}->{grandparentTitle};
$info->{'orig_title_ep'} = $live->{$k}->{title};
$info->{'episode'} = $live->{$k}->{index};
$info->{'season'} = $live->{$k}->{parentIndex};
if ($info->{'episode'} < 10) { $info->{'episode'} = 0 . $info->{'episode'};}
if ($info->{'season'} < 10) { $info->{'season'} = 0 . $info->{'season'}; }
}
## end unused data to clean up
## ignore content that has already been notified
## However, UPDATE the XML in the DB
if ($started->{$db_key}) {
$info->{'ip_address'} = $started->{$db_key}->{ip_address};
## try and locate IP address on each run ( if empty )
if (!$info->{'ip_address'}) {
$info->{'ip_address'} = &LocateIP($info) if ref $info;
}
my $state_change = &ProcessUpdate($live->{$k},$db_key,$info->{'ip_address'}); ## update XML
## notify on pause/resume -- only providers with push_resume or push_pause will be notified
if ($state_change) {
&DebugLog($info->{'user'} . ':' . $info->{'title'} . ': state change [' . $info->{'state'} . '] notify called');
&Notify($info,'',1) if $options{'notify'} != 2;
}
if ($debug) {
&Notify($info) if $options{'notify'} != 2;
my $msg = "Already Notified -- Sent again due to --debug";
&DebugLog($msg,1) if $msg && $options{'notify'} != 2;
};
}
## unnotified - insert into DB and notify
else {
## quick and dirty hack for client IP address
$info->{'ip_address'} = &LocateIP($info) if ref $info;
## end the dirty feeling
my $insert_id = &ProcessStart($live->{$k},$db_key,$info->{'title'},$info->{'platform'},$info->{'orig_user'},$info->{'orig_title'},$info->{'orig_title_ep'},$info->{'genre'},$info->{'episode'},$info->{'season'},$info->{'summary'},$info->{'rating'},$info->{'year'},$info->{'ip_address'}, $info->{'ratingKey'}, $info->{'parentRatingKey'}, $info->{'grandparentRatingKey'} );
&Notify($info) if $options{'notify'} != 2;
## should probably have some checks to make sure we were notified.. TODO
&SetNotified($insert_id) if $options{'notify'} != 2;
}
}
}
#####################################################
## print content being watched
##--watching
if ($options{'watching'}) {
my $in_progress = &GetInProgress();
my $live = &GetSessions(); ## query API for current streams
my $found_live = 0;
printf ("\n======================================= %s ========================================",'Watching');
my %seen = ();
if (keys %{$in_progress}) {
print "\n";
foreach my $k (sort { $in_progress->{$a}->{user} cmp $in_progress->{$b}->{'user'} || $in_progress->{$a}->{time} cmp $in_progress->{$b}->{'time'} } (keys %{$in_progress}) ) {
my $live_key = (split("_",$k))[0];
if (!$live->{$live_key}) {
#print "must of been stopped-- but unnotified";
next;
}
$found_live = 1;
## use display name
my ($user,$orig_user) = &FriendlyName($in_progress->{$k}->{user},$in_progress->{$k}->{platform});
## skip/exclude users --user/--exclude_user
my $skip = 1;
## --exclude_user array ref
next if ( grep { $_ =~ /$in_progress->{$k}->{'user'}/i } @{$options{'exclude_user'}});
next if ( $user && grep { $_ =~ /^$user$/i } @{$options{'exclude_user'}});
if ($options{'user'}) {
$skip = 0 if $user =~ /^$options{'user'}$/i; ## user display (friendly) matches specified
$skip = 0 if $orig_user =~ /^$options{'user'}$/i; ## user (non friendly) matches specified
} else { $skip = 0; }
next if $skip;
if (!$seen{$user}) {
$seen{$user} = 1;
print "\nUser: " .decode('utf8', $user);
print ' ['. $orig_user .']' if $user ne $orig_user;
print "\n";
}
my $time = localtime ($in_progress->{$k}->{time} );
my $paused = &getSecPaused($k);
my $info = &info_from_xml(XMLout($live->{$live_key}),'watching',$in_progress->{$k}->{time},time(),$paused);
## encode the user for output ( only if different -- otherwise we will take what Plex has set)
if ($user ne $orig_user) {
$info->{'user'} = encode('utf8', $info->{'user'}) if eval { encode('UTF-8', $info->{'user'}); 1 };
} elsif ($info->{time} > 1392090762) {
# everything after 2013-02-11 is not encoded properly ( excluding above which is special )
$info->{'user'} = encode('utf8', $info->{'user'}) if eval { encode('UTF-8', $info->{'user'}); 1 };
}
$info->{'ip_address'} = $in_progress->{$k}->{ip_address};
my $alert = &Notify($info,1); ## only return formated alert
printf(" %s: %s\n",$time, $alert);
}
}
print "\n * nothing in progress\n" if !$found_live;
print " \n";
}
#################################################### SUB #########################################################################
sub formatAlert() {
my $info = shift;
return ($info) if !ref($info);
my $provider = shift;
my $provider_multi = shift;
## n_prov_format: alert_format override in the config.pl per provider
my $n_prov_format = {};
if ($provider) {
$n_prov_format = $notify->{$provider};
$n_prov_format = $notify->{$provider}->{$provider_multi} if $provider_multi;
}
$info->{'ip_address'} = '' if !$info->{'ip_address'};
my $type = $info->{'ntype'};
my @types = qw(start watched watching stop paused resumed);
my $format;
my $format_key = 'alert_format';
# external provider (scripts) uses script_format
if (defined($provider) && $provider eq 'external') {
$format_key = 'script_format';
return -1 if !$n_prov_format->{$format_key}->{$type};
$format = $n_prov_format->{$format_key}->{$type};
} else {
foreach my $tkey (@types) {
if ($type =~ /$tkey/i) {
$format = $alert_format->{$tkey}; # default alert formats per notify type
$format = $n_prov_format->{$format_key}->{$tkey} if $n_prov_format->{$format_key}->{$tkey}; # provider override
}
}
}
if ($debug) { print "\nformat: $format\n";}
## just to be sure we don't have any conflicts -- use double curly brackets {{variable}} ( users still only use 1 {variable} )
$format =~ s/{/{{/g;
$format =~ s/}/}}/g;
my $regex = join "|", keys %{$info};
$regex = qr/$regex/;
## decoding doesn't play nice if we have mixed content ( one variable umlauts, another cryillic) try and decode each string
if (!$info->{'decoded'}) {
foreach my $key (keys %{$info}) {
## save come cpu cycles.
if (!ref($info->{$key}) && $format =~ /{$key}/) {
$info->{$key} = decode('utf8',$info->{$key}) if eval { decode('UTF-8', $info->{$key}); 1 };
}
}
}
$format =~ s/{{($regex)}}/$info->{$1}/g; ## regex replace variables
$format =~ s/\[\]//g; ## trim any empty variable encapsulated in []
$format =~ s/\s+/ /g; ## remove double spaces
$format =~ s/\\n/\n/g; ## allow \n to be an actual new line
$format =~ s/{{newline}}/\n/g; ## allow \n to be an actual new line
## special for now.. might make this more useful -- just thrown together since email can include a ton of info
if ($format =~ /{{all_details}}/i) {
$format =~ s/\s*{{all_details}}\s*//i;
$format .= sprintf("\n\n%10s %s\n","","-----All Details-----");
my $f_extra;
foreach my $key (keys %{$info} ) {
if (!ref($info->{$key})) {
if ($info->{$key}) {
my $value = $info->{$key};
if (!$info->{'decoded'}) {
$value = decode('utf8',$value) if eval { decode('UTF-8', $value); 1 };
}
$format .= sprintf("%20s: %s\n",$key,$value);
}
} else {
$f_extra .= sprintf("\n\n%10s %s\n","","-----$key-----");
foreach my $k2 (keys %{$info->{$key}} ) {
if (!ref($info->{$key}->{$k2})) {
my $value = $info->{$key}->{$k2};
if (!$info->{'decoded'}) {
$value = decode('utf8',$value) if eval { decode('UTF-8', $value); 1 };
}
$f_extra .= sprintf("%20s: %s\n",$k2,$value);
}
}
}
}
$format .= $f_extra if $f_extra;
}
return ($format);
}
sub ConsoleLog() {
my $msg = shift;
my $alert_options = shift;
my $print = shift;
my $prefix = '';
if (ref($alert_options)) {
if ($alert_options->{'user'}) {
if ($msg !~ /\b$alert_options->{'user'}\b/i) {
$prefix .= $alert_options->{'user'} . ' ' if $alert_options->{'user'};
}
if ($msg !~ /\b$push_type_titles->{$alert_options->{'push_type'}}\b/i) {
$prefix .= $push_type_titles->{$alert_options->{'push_type'}} . ' ' if $alert_options->{'push_type'};
}
}
## append type (movie, episode) if supplied
$prefix .= ucfirst($alert_options->{'item_type'}) . ' ' if $alert_options->{'item_type'};
$prefix =~ s/\s+$//g;
}
$msg = $prefix . ': ' . $msg if $prefix;
my $console;
my $date = localtime;
if ($debug || $print) {
$console = &consoletxt("$date: DEBUG: $msg");
print $console ."\n";
} elsif ($options{test_notify}) {
$console = &consoletxt("$date: DEBUG test_notify: $msg");
print $console ."\n";
} else {
$console = &consoletxt("$date: $msg");
}
## file logging
if (&ProviderEnabled('file')) {
open FILE, ">>", $notify->{'file'}->{'filename'} or die $!;
print FILE "$console\n";
close(FILE);
print "FILE Notification successfully logged.\n" if $debug;
}
return 1;
}
sub NotifyFile() {
my $provider = 'file';
#my $msg = shift;
my $info = shift;
my ($alert) = &formatAlert($info,$provider);
my $msg = $alert;
my $alert_options = shift;
my $print = shift;
my $prefix = '';
if (ref($alert_options) && $alert_options->{'user'}) {
if ($msg !~ /\b$alert_options->{'user'}\b/i) {
$prefix .= $alert_options->{'user'} . ' ' if $alert_options->{'user'};
}
if ($msg !~ /\b$push_type_titles->{$alert_options->{'push_type'}}\b/i) {
$prefix .= $push_type_titles->{$alert_options->{'push_type'}} . ' ' if $alert_options->{'push_type'};
}
} else {
$prefix .= $push_type_titles->{$alert_options->{'push_type'}} . ' ' if $alert_options->{'push_type'};
}
## append type (movie, episode) if supplied
$prefix .= ucfirst($alert_options->{'item_type'}) . ' ' if $alert_options->{'item_type'};
$prefix =~ s/\s+$//g if $prefix;
$msg = $prefix . ': ' . $msg if $prefix;
my $console;
my $date = localtime;
if ($debug || $print) {
$console = &consoletxt("$date: DEBUG: $msg");
print $console ."\n";
} elsif ($options{test_notify}) {
$console = &consoletxt("$date: DEBUG test_notify: $msg");
print $console ."\n";
} else {
$console = &consoletxt("$date: $msg");
}
## file logging
if (&ProviderEnabled('file')) {
open FILE, ">>", $notify->{'file'}->{'filename'} or die $!;
print FILE "$console\n";
close(FILE);
print "FILE Notification successfully logged.\n" if $debug;
}
return 1;
}
sub NotifyExternal() {
my $provider = 'external';
# This is just a pretty basic routine to call external scripts.
my $info = shift;
my $alert_options = shift;
my ($success,$error);
foreach my $k (keys %{$notify->{$provider}}) {
my ($command) = &formatAlert($info,$provider,$k);
next if $command eq -1;
my $push_type = $alert_options->{'push_type'};
if (ref $notify->{$provider}->{$k} && $notify->{$provider}->{$k}->{'enabled'} && $notify->{$provider}->{$k}->{$push_type}) {
print "$provider key:$k enabled for this $alert_options->{'push_type'}\n" if $debug;
} else {
print "$provider key:$k NOT enabled for this $alert_options->{'push_type'} - skipping\n" if $debug;
next;
}
&DebugLog($provider . '-' . $k . ': ' . $push_type . ' enabled -> run cmd: ' . $command);
system($command)
}
return 1; # for now... success!
}
sub DebugLog() {
## still need to add this routine to many other places (TODO)
my $msg = shift;
my $print = shift;
my $date = localtime;
my $console = &consoletxt("$date: $msg");
print $console ."\n" if ($debug || $print);
if ($debug_logging) {
open FILE, ">>", $data_dir . '/' . 'debug.log' or die $!;
print FILE "$console\n";
close(FILE);
}
}
sub Notify() {
my $info = shift;
my $ret_alert = shift;
my $state_change = shift; ## we will check what the state is and notify accordingly
my $dinfo = $info->{'user'}.':'.$info->{'title'};
#&DebugLog($dinfo . ': '."ret_alert:$ret_alert, state_change:$state_change");
my $type = $info->{'ntype'};
## to fix
if ($state_change) {
$type = "resumed" if $info->{'state'} =~ /playing/i;
$type = "paused" if $info->{'state'} =~ /pause/i;
$info->{'ntype'} = $type;
&DebugLog($dinfo . ': '."state:$info->{'state'}, ntype:$type ");
}
my ($alert) = &formatAlert($info);
## --exclude_user array ref -- do not notify if user is excluded.. however continue processing -- logging to DB - logging to file still happens.
return 1 if ( grep { $_ =~ /$info->{'orig_user'}/i } @{$options{'exclude_user'}});
return 1 if ( grep { $_ =~ /$info->{'user'}/i } @{$options{'exclude_user'}});
## only return the alert - do not notify -- used for CLI to keep formatting the same
return &consoletxt($alert) if $ret_alert;
my $push_type;
if ($type =~ /start/) { $push_type = 'push_watching'; }
if ($type =~ /stop/) { $push_type = 'push_watched'; }
if ($type =~ /resume/) { $push_type = 'push_resumed'; }
if ($type =~ /pause/) { $push_type = 'push_paused'; }
&DebugLog($dinfo . ': '.'push_type:' . $push_type);
#my $alert_options = ();
my $alert_options = $info; ## include $info href
$alert_options->{'push_type'} = $push_type;
foreach my $provider (keys %{$notify}) {
if (&ProviderEnabled($provider,$push_type)) {
&DebugLog($dinfo . ': '.$provider . ' ' . $push_type . ' enabled -> sending notify');
$notify_func{$provider}->($info,$alert_options);
#$notify_func{$provider}->($alert,$alert_options);
}
}
}
sub ProviderEnabled() {
my ($provider,$push_type) = @_;
if (!$push_type) {
## provider is multi ( GNTP )
foreach my $k (keys %{$notify->{$provider}}) {
return 1 if (ref $notify->{$provider}->{$k} && $notify->{$provider}->{$k}->{'enabled'});
}
## provider is non-multi
return 1 if $notify->{$provider}->{'enabled'};