forked from SpaceOgre/SortTV
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sorttv.pl
executable file
·2258 lines (2069 loc) · 80.6 KB
/
sorttv.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
# SortTV
# Copyleft (ↄ) 2010-2013
# Z. Cliffe Schreuders
#
# Sorts tv shows into tvshow/series directories;
# If the dirs don't exist they are created;
# Updates xbmc via the web interface;
# Unsorted files are moved to a dir if specifed;
# Lots of other features.
#
# Other contributers:
# salithus - xbmc forum
# schmoko - xbmc forum
# CoinTos - xbmc forum
# gardz - xbmc forum
# Patrick Cole - [email protected]
# Martin Guillon - farfromrefug
# red_five - xbmc forum
# Fox - xbmc forum
# TechLife - xbmc forum
# frozenesper - xbmc forum
# deranjer - xbmc forum
# iamwudu - xbmc forum
# Nicolas Leclercq - https://sourceforge.net/u/exzz/
# Justin Metheny
#
# Please goto the xbmc forum to discuss SortTV:
# http://forum.xbmc.org/showthread.php?t=75949
#
# Get the latest version from here:
# http://sourceforge.net/projects/sorttv/files/
#
# Cliffe's website:
# http://z.cliffe.schreuders.org/
#
# Please consider a $5 donation if you find this program helpful.
# http://sourceforge.net/donate/index.php?group_id=330009
# If you prefer to donate via bitcoin, contact me for details
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
use warnings;
use strict;
use utf8;
use File::Copy::Recursive "dirmove", "dircopy";
use File::Copy;
use File::Glob ':glob';
use LWP::Simple qw($ua getstore get is_success);
use File::Spec::Functions "rel2abs";
use File::Basename;
use TVDB::API;
use WWW::TheMovieDB;
use JSON::Parse 'parse_json';
use XML::Simple;
use File::Find;
use File::Path qw(make_path);
use FileHandle;
use Fcntl ':flock';
use Getopt::Long;
use Getopt::Long qw(GetOptionsFromString);
use IO::Socket;
# Global config variables
my $man = my $help = 0;
my ($sortdir, $tvdir, $miscdir, $musicdir, $moviedir, $matchtype);
my ($xbmcoldwebserver, $xbmcaddress);
my $xbmcport = 9090;
my ($newshows, $new, $log);
my @musicext = ("aac","aif","iff","m3u","mid","midi","mp3","mpa","ra","ram","wave","wav","wma","ogg","oga","ogx","spx","flac","m4a", "pls");
my @videoext = ("avi","mpg","mpe","mpeg-1","mpeg-2","m4v","mkv","mov","mp4","mpeg","ogm","wmv","divx","dvr-ms","3gp","m1s","mpa","mp2","m2a","mp2v","m2v","m2s","qt","asf","asx","wmx","rm","ram","rmvb","3g2","flv","swf","aepx","ale","avp","avs","bdm","bik","bin","bsf","camproj","cpi","dat","dmsm","dream","dvdmedia","dzm","dzp","edl","f4v","fbr","fcproject","hdmov","imovieproj","ism","ismv","m2p","mod","moi","mts","mxf","ogv","pds","prproj","psh","r3d","rcproject","scm","smil","sqz","stx","swi","tix","trp","ts","veg","vf","vro","webm","wlmp","wtv","xvid","yuv","3gp2","3gpp","3p2","aaf","aep","aetx","ajp","amc","amv","amx","arcut","arf","avb","axm","bdmv","bdt3","bmk","camrec","cine","clpi","cmmp","cmmtpl","cmproj","cmrec","cst","d2v","d3v","dce","dck","dcr","dcr","dir","dmb","dmsd","dmsd3d","dmss","dpa","dpg","dv","dv-avi","dvr","dvx","dxr","dzt","evo","eye","f4p","fbz","fcp","flc","flh","fli","gfp","gts","hkm","ifo","imovieproject","ircp","ismc","ivf","ivr","izz","izzy","jts","jtv","m1pg","m21","m21","m2t",
"m2ts","m2v","mgv","mj2","mjp","mk3d","mnv","mp21","mp21","mpgindex","mpl","mpls","mpv","mqv","msdvd","mse","mswmm","mtv","mvd","mve","mvp","mvp","mvy","ncor","nsv","nuv","nvc","ogx","pgi","photoshow","piv","plproj","pmf","ppj","prel","pro","prtl","pxv","qtl","qtz","rcd","rdb","rec","rmd","rmp","rms","roq","rsx","rum","rv","rvl","sbk","scc","screenflow","seq","sfvidcap","siv","smi","smk","stl","svi","swt","tda3mt","tivo","tod","tp","tp0","tpd","tpr","tsp","tvs","usm","vc1","vcpf","vcv","vdo","vdr","vep","vfz","vgz","viewlet","vlab","vp6","vp7","vpj","vsp","wcp","wmd","wmmp","wmx","wp3","wpl","wvx","xej","xel","xesc","xfl","xlmv","zm1","zm2","zm3","zmv","iso","img");
my @subtitleext = ("ssa", "srt", "sub", "idx");
my @imageext = ("jpg", "jpeg", "tbn");
my (@whitelist, @blacklist, @deletelist, @sizerange, @filestosort, @nonmediaext);
my (%showrenames, %showtvdbids);
my $REDO_FILE = my $checkforupdates = my $moveseasons = my $windowsnames = my $tvdbrename = my $lookupseasonep = my $extractrar = my $useseasondirs = my $displaylicense = my $useextensions = "TRUE";
my $usedots = my $rename = my $seasondoubledigit = my $removesymlinks = my $needshowexist = my $flattennonepisodefiles = my $tvdbrequired = my $sort_movie_dir = my $use_movie_folder_name = "FALSE";
my $dryrun = "";
my $seasontitle = "Season ";
my $sortby = "MOVE";
my $duplicateimages = "SYMLINK";
my $sortolderthandays = my $poll = my $verbose = 0;
my $ifexists = "SKIP";
my $renameformat = "[SHOW_NAME] - [EP1][EP_NAME1]";
my $movierenameformat = "[MOVIE_TITLE] [YEAR2]/[MOVIE_TITLE] [YEAR1]";
my $fetchmovieimages = "TRUE";
my $treatdir = "RECURSIVELY_SORT_CONTENTS";
my $fetchimages = "NEW_SHOWS";
my $imagesformat = "POSTER";
my $scriptpath = dirname(rel2abs($0));
my $logfile = "$scriptpath/sorttv.log";
my $tvdblanguage = "en";
my $movielanguage = "en";
my $tvdb;
my $tmdb;
my $yeartoleranceforerror = 1;
my $forceeptitle = ""; # HACK for limitation in TVDB API module
# download timeout
$ua->timeout(20);
my $dir_perms;
my @optionlist = (
"misc-dir|non-episode-dir|misc=s" => sub { set_directory($_[1], \$miscdir); },
"xbmc-old-web-server|xbmc-web-server|xo=s" => \$xbmcoldwebserver,
"xbmc-remote-control|xr=s" => \$xbmcaddress,
"xbmc-remote-control-port|xrp=i" => \$xbmcport,
"match-type|ms=s" => \$matchtype,
"flatten-non-eps|fne=s" => \$flattennonepisodefiles,
"check-for-updates|up=s" => \$checkforupdates,
"treat-directories|td=s" => \$treatdir,
"consider-media-file-extensions|ext=s" => \$useextensions,
"if-file-exists|e=s" => \$ifexists,
"extract-compressed-before-sorting|rar=s" => \$extractrar,
"show-name-substitute=s" =>
sub {
if($_[1] =~ /(.*)-->(.*)/) {
my ($key, $val) = ($1, $2);
$key = fixtitle($key);
$showrenames{$key} = $val;
}
},
"whitelist|white=s" =>
sub {
# puts the shell pattern in as a regex
push @whitelist, glob2pat($_[1]);
},
"blacklist|black|ignore=s" =>
sub {
# puts the shell pattern in as a regex
push @blacklist, glob2pat($_[1]);
},
"deletelist|delete=s" =>
sub {
# puts the shell pattern in as a regex
push @deletelist, glob2pat($_[1]);
},
"tvdb-id-substitute|tis=s" =>
sub {
if($_[1] =~ /(.*)-->(.*)/) {
my ($key, $val) = ($1, $2);
$key = fixtitle($key);
$showtvdbids{$key} = $val;
}
},
"tvdb-episode-name-required|nreq=s" => \$tvdbrequired,
"display-license|dl=s" => \$displaylicense,
"log-file|o=s" => \$logfile,
"fetch-show-title|fst=s" => \$tvdbrename,
"rename-media|rename-episodes|rn=s" => \$rename,
"tv-lookup-language|lookup-language|lang=s" => \$tvdblanguage,
"movie-lookup-language|lang=s" => \$movielanguage,
"fetch-tv-images|fetch-images|fi=s" => \$fetchimages,
"fetch-movie-images|fmi=s" => \$fetchmovieimages,
"images-format|im=s" => \$imagesformat,
"duplicate-images|csi=s" => \$duplicateimages,
"require-show-directories-already-exist|rs=s" => \$needshowexist,
"force-windows-compatible-filenames|fw=s" => \$windowsnames,
"rename-tv-format|rename-format|rf=s" => \$renameformat,
"rename-movie-format|rf=s" => \$movierenameformat,
"remove-symlinks|rs=s" => \$removesymlinks,
"use-dots-instead-of-spaces|dots=s" => \$usedots,
"sort-by|by=s" => \$sortby,
"sort-only-older-than-days|age=i" => \$sortolderthandays,
"year-tolerance-for-error|yerr=i" => \$yeartoleranceforerror,
"poll-time|poll=s" =>
sub {
my $ptime = $_[1];
# convert times to seconds
if ($ptime =~ /^(.*)(?:secs?|s)$/) {
$ptime = $1;
} elsif ($ptime =~ /^(.*)(?:mins?|m)$/) {
$ptime = $1 * 60;
} elsif ($ptime =~ /^(.*)(?:hrs?|hours?|h)$/) {
$ptime = $1 * 3600;
} elsif ($ptime =~ /^(.*)(?:days?|d)$/) {
$ptime = $1 * 86400;
} elsif ($ptime =~ /(\d+)/) {
out("warn", "WARN: interpreting $ptime as $1 seconds\n");
$ptime = $1;
} else {
out("warn", "WARN: invalid poll time: $ptime. Must be secs, hrs, days etc.\n");
$ptime = 0;
}
$poll = $ptime;
},
"season-double-digits|sd=s" => \$seasondoubledigit,
"match-files-based-on-tvdb-lookups|tlookup=s" => \$lookupseasonep,
"use-season-directories|sd=s" => \$useseasondirs,
"season-title|st=s" => \$seasontitle,
"verbose|v" => \$verbose,
"dry-run|n" =>
sub {
$dryrun = "DRYRUN ";
out("std", "DRY RUN MODE: No file operations will occur on the to-sort directory, some directories may be created at the destination.\n");
},
"filesize-range|fsrange=s" =>
sub {
# Extract the min & max values, can mix and match postfixes
if ($_[1] !~ /(.*)-(.*)/) {
out ("warn", "WARN: invalid filesize range format. Must be 125MB-350MB, etc. Received: $_[1]");
return;
}
my $minfilesize = $1;
my $maxfilesize = $2;
$minfilesize =~ s/MB//;
$maxfilesize =~ s/MB//;
# Fix filesizes passed in to all MB
if ($minfilesize =~ /(.*)GB/) {
$minfilesize = $1 * 1024;
}
if ($maxfilesize =~ /(.*)GB/) {
$maxfilesize = $1 * 1024;
}
# Save as MB range
push @sizerange, "$minfilesize-$maxfilesize";
},
"no-network|nn" =>
sub {
out("verbose", "INFO: Disabling all network enabled features\n");
$xbmcoldwebserver = $xbmcaddress = $moviedir = "";
$tvdbrename = $fetchimages = $lookupseasonep = $checkforupdates = "FALSE";
$renameformat =~ s/\[EP_NAME\d\]//;
},
"read-config-file|conf=s" =>
sub {
get_config_from_file($_[1]);
},
"file-to-sort|file=s" =>
sub {
if(-r $_[1]) {
push @filestosort, $_[1];
} else {
out("warn", "WARN: file $_[1] does not exist\n");
}
out("verbose", "\@filestosort: @filestosort\n");
},
"directory-to-sort|sort=s" => sub { set_directory($_[1], \$sortdir); },
"tv-directory|directory-to-sort-into|tvdir=s" =>
sub {
if($_[1] eq "KEEP_IN_SAME_DIRECTORIES") {
out("std", "INFO: disabling everything except tv sorts within the same directory");
$miscdir = "";
$musicdir = "";
$moviedir = "";
$tvdir = "KEEP_IN_SAME_DIRECTORIES";
} else {
set_directory($_[1], \$tvdir);
}
},
"movie-directory|movie=s" => sub { set_directory($_[1], \$moviedir); },
"sort-movie-directories|mdsort=s" => \$sort_movie_dir,
"movie-language|mlang=s" => \$movielanguage,
"music-directory|music=s" => sub { set_directory($_[1], \$musicdir); },
"music-extension|me=s" => \@musicext,
"non-media-extension|nm=s" => \@nonmediaext,
"dir-permissions|dp=s" => \$dir_perms,
"movie-use-folder-name=s" => \$use_movie_folder_name,
"h|help|?" => \$help, man => \$man
);
# current episode being sorted
my ($showname, $year, $series, $episode, $pureshowname) = "";
{ # Main bare block
out("std", "SortTV\n", "~" x 6,"\n");
# ensure only one copy running at a time
if(open SELF, "< $0") {
flock SELF, LOCK_EX | LOCK_NB or die "SortTV is already running, exiting.\n";
}
check_for_updates() if $checkforupdates eq "TRUE";
get_config_from_file("$scriptpath/sorttv.conf");
# we declare all the possible options through command line
# each option can have multiple variables (|) and can be used like
# "--opt" or "--opt=value" or "opt=value" or "opt value" or "-opt value" etc
process_args();
display_license() if $displaylicense eq "TRUE";
# we stop the script and show the help if help or man option was used
showhelp() if $help or $man;
# ensure at least one input and one output
if((!defined($sortdir) && ! scalar @filestosort) || (!defined($tvdir) && !defined($moviedir) && !defined($musicdir) && !defined($miscdir))) {
out("warn", "Incorrect usage or configuration (missing sort or sort-to directories)\n");
out("warn", "run 'perl sorttv.pl --help' for more information about how to use SortTV\n");
exit;
}
# if uses thetvdb, set it up
if($renameformat =~ /\[EP_NAME\d]/i || $fetchimages ne "FALSE"
|| $lookupseasonep ne "FALSE" || $lookupseasonep ne "FALSE") {
my $TVDBAPIKEY = "FDDBDB916D936956";
$tvdb = TVDB::API::new($TVDBAPIKEY);
$tvdb->setLang($tvdblanguage);
my $hashref = $tvdb->getAvailableMirrors();
$tvdb->setMirrors($hashref);
$tvdb->chooseMirrors();
unless (-e "$scriptpath/.cache" || mkdir "$scriptpath/.cache") {
out("warn", "WARN: Could not create cache dir: $scriptpath/cache $!\n");
exit;
}
$tvdb->setCacheDB("$scriptpath/.cache/.tvdb.db");
$tvdb->setUserAgent("SortTV");
$tvdb->setBannerPath("$scriptpath/.cache/");
}
# if uses moviedb, set it up
if($moviedir) {
$tmdb = new WWW::TheMovieDB({
'key'=>'e3df0ef251745a7833d9e9114fc9b0c1',
'language'=>$movielanguage
});
}
$log = FileHandle->new("$logfile", "a") or out("warn", "WARN: Could not open log file $logfile: $!\n") if $logfile;
display_sortdirs();
out("std", "Polling every $poll seconds...\n") if $poll;
do {
display_time();
sort_directory($sortdir, @filestosort);
if($xbmcoldwebserver && $newshows) {
sleep(4);
# update xbmc video library
get "http://$xbmcoldwebserver/xbmcCmds/xbmcHttp?command=ExecBuiltIn(updatelibrary(video))";
# notification of update
get "http://$xbmcoldwebserver/xbmcCmds/xbmcHttp?command=ExecBuiltIn(Notification(,NEW EPISODES NOW AVAILABLE TO WATCH\n$newshows, 7000))";
}
if($xbmcaddress && $newshows) {
update_xbmc();
}
sleep($poll);
} while ($poll);
$log->close if(defined $log);
exit;
}
# notifies the user if a newer version is available
# gets the version of the latest release from sourceforge, and compares that to the local version
sub check_for_updates {
my ($version, $localmaj, $localmin, $currentversion, $currentmaj, $currentmin);
if(open (VER, "$scriptpath/.sorttv.version")) {
chomp($version = <VER>);
}
if($version =~ /(\d+).(\d+)/) {
($localmaj, $localmin) = ($1, $2);
}
$currentversion = get "http://sourceforge.net/p/sorttv/code/ci/master/tree/.sorttv.version?format=raw";
# exit if network problems
return unless $currentversion;
if($currentversion =~ /(\d+).(\d+)/) {
($currentmaj, $currentmin) = ($1, $2);
if($localmaj < $currentmaj || ($localmaj == $currentmaj && $localmin < $currentmin)) {
out("std", "UPDATE AVAILABLE: A new version of SortTV is available!\n\tVisit http://sourceforge.net/projects/sorttv/ to get the latest version.\n\tLocal version: $version, latest release: $currentversion\n");
}
} else {
# got something, but it does not conform to a valid version number
out("std", "Unable to determine latest release. A new version of SortTV may be available.\n\tVisit http://sourceforge.net/projects/sorttv/ to get the latest version.\n\tLocal version: $version\n");
}
}
sub update_xbmc {
my $sock = new IO::Socket::INET (
PeerAddr => $xbmcaddress,
PeerPort => $xbmcport,
Proto => 'tcp', 6 );
if($sock) {
print $sock '{"id":1,"method":"VideoLibrary.Scan","params":[],"jsonrpc":"2.0"}';
print $sock '{"jsonrpc": "2.0", "method": "VideoLibrary.ScanForContent", "id": 1}\n';
print $sock "{\"jsonrpc\":\"2.0\",\"method\":\"GUI.ShowNotification\",\"params\":{\"title\":\"New Shows Available to Watch\",\"message\":\"$new\",\"image\":\"\"},\"displaytime\":10000,\"id\":1}\n";
close($sock);
} else {
out("warn", "WARN: Could not connect to xbmc server: $!\n");
}
}
# checks for the old way we specified options, and converts
sub check_for_old_style_options {
my @list = @_;
foreach (@list) {
if($_ =~ /(^[^:= ]+):(.+)$/) {
$_ = "$1=$2";
}
}
@ARGV = @list;
}
# processes the arguments, checks for old style, calls GetOptions, then uses what is left
sub process_args {
check_for_old_style_options(@ARGV);
GetOptions(@optionlist);
set_directory($ARGV[0], \$sortdir) if defined $ARGV[0];
set_directory($ARGV[1], \$tvdir) if defined $ARGV[1];
}
# displays an overview of the sorting that is being done
sub display_sortdirs {
out("std", "Sorting:\n\tFrom $sortdir\n") if $sortdir;
out("std", "\tTV episodes into $tvdir\n") if $tvdir;
out("std", "\tMovies into $moviedir\n") if $moviedir;
out("std", "\tMusic into $musicdir\n") if $musicdir;
out("std", "\tEverything else into $miscdir\n") if $miscdir;
}
# displays the license and asks for a donation
sub display_license {
out("std", "SortTV is copyleft free open source software.\nYou are free to make modifications and share, as defined by version 3 of the GNU General Public License\n");
out("std", "If you find this software helpful, \$5 donations are welcomed:\nhttp://sourceforge.net/donate/index.php?group_id=330009\n");
out("std", "~" x 6,"\n");
}
# used to check that a dir exists, then set the corresponding variable
sub set_directory {
my ($dir, $dir_variable) = @_;
# use Unix slashes
$dir =~ s/\\/\//g;
if(-e $dir) {
$$dir_variable = $dir;
# append a trailing / if it's not there
$$dir_variable .= '/' if($$dir_variable !~ /\/$/);
} else {
out("warn", "WARN: directory does not exist ($dir)\n");
}
}
sub sort_directory {
# passed the directory to sort the contents of, and any additional files
my ($sortd, @files) = @_;
# escape special characters from bsd_glob
my $escapedsortd = $sortd;
$escapedsortd =~ s/(\[|]|\{|}|-|~)/\\$1/g;
if($extractrar eq "TRUE") {
extract_archives($escapedsortd, $sortd);
}
FILE: foreach my $file (bsd_glob($escapedsortd.'*'), @files) {
$showname = "";
my $nonep = "FALSE";
my $filename = filename($file);
out("verbose", "INFO: Currently checking file: $filename\n");
# check white and black lists
if(check_lists($file) eq "NEXT") {
next FILE;
}
# check size
if (check_filesize($file) eq "NEXT") {
next FILE;
}
# check age
if ($sortolderthandays && -M $file < $sortolderthandays) {
out("std", "SKIP: $file is newer than $sortolderthandays days old.\n");
next FILE;
}
# treat symlinks separately
if(-l $file) {
if($removesymlinks eq "TRUE") {
out("std", "DELETE: Removing symlink: $file\n");
unless ($dryrun) {
unlink($file) or out("warn", "WARN: Could not delete symlink $file: $!\n");
}
}
# otherwise file is a symlink, ignore
# ignore directories, if so configured
} elsif(-d $file && $treatdir eq "IGNORE") {
out("verbose", "INFO: Ignoring Dir: $file\n");
# ignore directories
# here we attempt to sort each type of media
# movie should come last since it will attempt to find a matching movie name on tmdb
} else {
# try to find a matching sort method
unless(is_music($file, $filename)
|| is_season_directory($file, $filename)
|| is_tv_episode($file, $filename)
|| is_movie($file, $filename)) {
# if it does not match the requirements for any sort method,
# either recursively sort each directory, or the file is an "other"
if(-d $file && $treatdir eq "RECURSIVELY_SORT_CONTENTS") {
out("verbose", "INFO: Entering into directory or compressed file $file\n");
sort_directory("$file/");
# removes any empty directories from the to-sort directory and sub-directories
finddepth(sub{rmdir},"$sortd");
} elsif($miscdir && $tvdir ne "KEEP_IN_SAME_DIRECTORIES") {
# move anything else
sort_other ("OTHER", "$miscdir", $file);
}
}
}
}
}
sub is_video_to_be_sorted {
my ($file, $filename) = @_;
return ($treatdir eq "AS_FILES_TO_SORT" and -d $file) || ($useextensions eq "FALSE" and -f $file and not is_other($file)) || (-f $file and not is_other($file) and matches_type($filename, @videoext, @imageext, @subtitleext));
}
sub is_music_to_be_sorted {
my ($file, $filename) = @_;
return (-f $file and not is_other($file) and matches_type($filename, @musicext));
}
# checks whether this is a movie to be sorted
# if it is, this kicks of the sorting process
sub is_movie {
my ($file, $filename) = @_;
# conditions for it to be checked
if($moviedir && (is_video_to_be_sorted($file, $filename) || (-d $file and $sort_movie_dir eq "TRUE"))) {
# check regex
if($filename =~ /(.*?)\s*-?\.?\s*\(?\[?((?:20|19)\d{2})\)?\]?(?:BDRip|\[Eng]|DVDRip|DVD|Bluray|XVID|DIVX|720|1080|HQ|x264|R5|RERip)*.*?(\.\w*$)/i
|| $filename =~ /(.*?)\.?(?:[[\]{}()]|\[Eng]|BDRip|DVDRip|DVD|Bluray|XVID|DIVX|720|1080|HQ|x264|R5|RERip)+.*?()(\.\w*$)/i
|| $filename =~ /(.*?)()(\.\w*$)/i || $filename =~ /(.*)()()/) {
my $title = $1;
my $year = $2;
my $ext = $3;
$title =~ s/(?:\[Eng]|BDRip|DVDRip|DVD|Bluray|XVID|DIVX|720|1080|HQ|x264|R5|RERip|[[\]{}()])//ig;
# at this point if it is not a known movie it is an "other"
if(match_and_sort_movie($title, $year, $ext, $file) eq "TRUE") {
return 1;
}
# try folder name instead, lots of unpacked files have really bad names
elsif($use_movie_folder_name eq "TRUE") {
out("verbose", "INFO: Using movie folder name to search tmdb\n");
my $tempPath = dirname($file);
$filename = basename($tempPath);
while($filename =~ /.*? \(extracted by SortTV\)/i){
$tempPath = dirname($tempPath);
$filename = basename($tempPath);
}
if($tempPath . "/" eq $sortdir){
out("verbose", "INFO: Movie folder is same as root folder, will not be used for tmdb search\n");
return 0;
}
#$filename = basename(dirname($file));
if($filename =~ /(.*?)\s*-?\.?\s*\(?\[?((?:20|19)\d{2})\)?\]?(?:BDRip|\[Eng]|DVDRip|DVD|Bluray|XVID|DIVX|720|1080|HQ|x264|R5|RERip)*.*?/i
|| $filename =~ /(.*?)\.?(?:[[\]{}()]|\[Eng]|BDRip|DVDRip|DVD|Bluray|XVID|DIVX|720|1080|HQ|x264|R5|RERip)+.*?()/i
|| $filename =~ /(.*)()/i) {
my $title = $1;
my $year = $2;
$title =~ s/(?:\[Eng]|BDRip|DVDRip|DVD|Bluray|XVID|DIVX|720|1080|HQ|x264|R5|RERip|[[\]{}()])//ig;
#at this point if it is not a known movie it is an "other"
if(match_and_sort_movie($title, $year, $ext, $file) eq "TRUE") {
return 1;
}
}
}
}
}
return 0;
}
# checks whether this is a TV episode to be sorted
# if it is, this kicks of the sorting process
sub is_tv_episode {
my ($file, $filename) = @_;
# conditions for it to be checked
if($tvdir && is_video_to_be_sorted($file, $filename)) {
# check regex
my $dirsandfile = $file;
$dirsandfile =~ s/\Q$sortdir\E//;
if($filename =~ /(.*?)(?:\.|\s|-|_|\[)+[Ss]0*(\d+)(?:\.|\s|-|_)*[Ee]0*(\d+).*/
# "Show/Season 1/S1E1.avi" or "Show/Season 1/1.avi" or "Show Season 1/101.avi" or "Show/Season 1/1x1.avi" or "Show Series 1 Episode 1.avi" etc
|| $dirsandfile =~ /(.*?)(?:\.|\s|\/|\\|-|\1)*(?:Season|Series|\Q$seasontitle\E)\D?0*(\d+)(?:\.|\s|\/|\\|-|\1)+[Ss]0*\2(?:\.|\s|-|_)*[Ee]0*(\d+).*/i
|| $dirsandfile =~ /(.*?)(?:\.|\s|\/|\\|-|\1)*(?:Season|Series|\Q$seasontitle\E)\D?0*(\d+)(?:\.|\s|\/|\\|-|\1)+\[?0*\2?\s*[xX-]?\s*0*(\d{1,2}).*/i
|| $dirsandfile =~ /(.*?)(?:\.|\s|\/|\\|-|\1)*(?:Season|Series|\Q$seasontitle\E)\D?0*(\d+)(?:\.|\s|\/|\\|-|\1)+\d??(?:[ .-]*Episode[ .-]*)?0*(\d{1,2}).*/i
# not a date, but is 1x1 or 1-1
|| ($filename !~ /(\d{4}[-.]\d{1,2}[-.]\d{1,2})/ && $filename =~ /(.*)(?:\.|\s|-|_)+\[?0*(\d{1,3})\s*[xX-]\s*0*(\d+).*/)
|| $filename =~ /(.*)(?:\.|\s|-|_)+(?:(?!19\d{2}|20\d{2})0*(\d{1,2})(\d{2}))(?:\.|\s).*/
|| ($matchtype eq "LIBERAL" && filename($file) =~ /(.*)(?:\.|\s|-|_)0*(\d+)\D*0*(\d+).*/)) {
$pureshowname = $1;
$pureshowname = fixpurename($pureshowname);
$showname = fixtitle($pureshowname);
if($seasondoubledigit eq "TRUE") {
$series = sprintf("%02d", $2);
} else {
$series = $2;
}
$episode = $3;
# extract year if present, for example "Show (2011)"
if($pureshowname =~ /(.*)(?:\.|\s|-|\(|\[)*\(?((?:20|19)\d{2})(?:\)|\])?/) {
$year = $2;
} else {
$year = "";
}
if($pureshowname ne "") {
if($tvdir !~ /^KEEP_IN_SAME_DIRECTORIES/) {
return move_episode($pureshowname, $showname, $series, $episode, $year, $file);
} else {
rename_episode($pureshowname, $series, $episode, $file);
return 1;
}
}
# match "Show - Episode title.avi" or "Show - [AirDate].avi"
} elsif($tvdir && $lookupseasonep eq "TRUE" && (-d $file && $treatdir eq "AS_FILES_TO_SORT" || -f $file && matches_type($filename, @videoext, @imageext, @subtitleext)) &&
(filename($file) =~ /(.*)(?:\.|\s)(\d{4}[-.]\d{1,2}[-.]\d{1,2}).*/ || filename($file) =~ /(.*)-(.*)(?:\..*)/)) {
$pureshowname = $1;
$showname = fixtitle($pureshowname);
my $episodetitle = fixdate($2);
$series = "";
$episode = "";
# calls fetchseasonep to try and find season and episode numbers: returns array [0] = Season [1] = Episode
my @foundseasonep = fetchseasonep(resolve_show_name($pureshowname), $episodetitle);
if(exists $foundseasonep[1]) {
$series = $foundseasonep[0];
$episode = $foundseasonep[1];
}
if($series ne "" && $episode ne "") {
if($seasondoubledigit eq "TRUE" && $series =~ /\d+/) {
$series = sprintf("%02d", $series);
}
if($tvdir !~ /^KEEP_IN_SAME_DIRECTORIES/) {
return move_episode($pureshowname, $showname, $series, $episode, $year, $file);
} else {
rename_episode($pureshowname, $series, $episode, $file);
return 1;
}
}
}
}
return 0;
}
# checks whether this is a tv season directory to be sorted as is
# if it is, this kicks of the sorting process
sub is_season_directory {
my ($file, $filename) = @_;
# conditions for it to be checked
if($tvdir && ($treatdir eq "AS_FILES_TO_SORT" && -d $file)) {
# check regex
if($file =~ /.*\/(.*)(?:Season|Series|$seasontitle)\D?0*(\d+).*/i && $1) {
out("verbose", "INFO: Treating $file as directory to sort\n");
$pureshowname = $1;
if($seasondoubledigit eq "TRUE") {
$series = sprintf("%02d", $2);
} else {
$series = $2;
}
$showname = fixtitle($pureshowname);
# extract year if present, for example "Show (2011)"
if($pureshowname =~ /(.*)(?:\.|\s|-|\(|\[)*\(?((?:20|19)\d{2})(?:\)|\])?/) {
$year = $2;
} else {
$year = "";
}
if(move_series($pureshowname, $showname, $series, $year, $file)) {
return 1;
}
}
}
return 0;
}
# checks whether this is music to be sorted
# if it is, this kicks of the sorting process
sub is_music {
my ($file, $filename) = @_;
# conditions for it to be checked
if($musicdir && is_music_to_be_sorted($file, $filename)) {
#move to music folder if music
sort_other ("MUSIC", "$musicdir", $file);
}
}
# used to sort files into another directory
sub sort_other {
my ($msg, $destdir, $file) = @_;
my $newname = $file;
$newname =~ s/\Q$sortdir\E//; #stripping the $sortdir from the filename
$newname = escape_myfilename($newname);
if($flattennonepisodefiles eq "FALSE") {
my $dirs = path($newname);
my $filename = filename($newname);
if(! -d $file && ! -e $destdir . $dirs) {
# recursively creates the dir structure
make_path($destdir . $dirs);
}
$newname = $dirs . $filename;
} else { # flatten
$newname =~ s/[\\\/]/-/g;
}
sort_file($file, $destdir . $newname, $msg);
}
sub get_config_from_file {
my ($filename) = @_;
my @arraytoconvert;
if(open (IN, $filename)) {
out("verbose", "INFO: Reading configuration settings from '$filename'\n");
while(my $in = <IN>) {
chomp($in);
$in =~ s/\\/\//g;
if($in =~ /^\s*#/ || $in =~ /^\s*$/) {
# ignores comments and whitespace
} else {
# convert from ':' to '=' assignment format
if($in =~ /(^[^:= ]+):(.+)$/) {
$in = "$1=$2";
}
GetOptionsFromString("'--$in'", @optionlist);
}
}
close (IN);
} else {
out("warn", "WARN: Couldn't open config file '$filename': $!\n");
out("warn", "INFO: An example config file is available and can make using SortTV easier\n");
}
}
sub showhelp {
my $heredoc = <<END;
Usage: sorttv.pl [OPTIONS] [directory-to-sort directory-to-sort-into]
By default SortTV tries to read the configuration from sorttv.conf
(an example config file is available online)
You can overwrite any config options with commandline arguments, which match the format of the config file (except that each argument starts with "--")
OPTIONS:
--directory-to-sort=dir
A directory containing files to sort
For example, set this to where completed downloads are stored
--file-to-sort=file
A file to be sorted
This argument can be repeated to add multiple individual files to sort
--tv-directory=dir
Where to sort episodes into (dir that will contain dirs for each show)
This directory will contain the structure (Show)/(Seasons)/(episodes)
Alternatively set this to "KEEP_IN_SAME_DIRECTORIES" for a recursive renaming of files in directory-to-sort
--movie-directory
Where to sort movies into
If not specified, movies are not moved
--music-directory=dir
Where to sort music into
If not specified, music is not moved
--misc-directory=dir
Where to put things that are not episodes etc
If this is supplied then files and directories that SortTV does not believe are episodes will be moved here
If not specified, non-episodes are not moved
--dry-run
Dry run mode. No file operations will occur on the to-sort directory.
Some directories may be created at the destination.
--whitelist=pattern
Only copy if the file matches one of these patterns
Uses shell-like simple pattern matches (eg *.avi)
This argument can be repeated to add more rules
--ignore=pattern
Don't copy if the file matches one of these blacklist patterns
Uses shell-like simple pattern matches (eg *.avi)
This argument can be repeated to add more rules
--delete=pattern
Delete the source file, if the file matches one of these patterns
Uses shell-like simple pattern matches (eg *.avi)
This argument can be repeated to add more rules
--consider-media-file-extensions=[TRUE|FALSE]
Consider the file extension before treating certain files as movies or TV episodes
Recommended: SortTV is aware of a large number of extensions, and this can avoid many false matches
if not specified, TRUE
--consider-media-file-extensions=[TRUE|FALSE]
Consider the file extension before treating certain files as movies or TV episodes
Recommended: SortTV is aware of a large number of extensions, and this can avoid many false matches
if not specified, TRUE
--non-media-extension=pattern
These extensions are NEVER movies or TV shows or music, treat them as "others" automatically
Note: Will not run these file types through tvdb, tmdb, etc.
Not typically required if consider-media-file-extensions=TRUE
This argument can be repeated to add multiple extensions
--filesize-range=pattern
Only copy files which fall within these filesize ranges.
Examples for the pattern include 345MB-355MB or 1.05GB-1.15GB
--sort-only-older-than-days=number
Sort only files or directories that are older than this number of days.
If not specified or zero, sort everything.
--xbmc-old-web-server=host:port
host:port for the old xbmc webserver, to automatically update library when new episodes arrive
Remember to enable the webserver within xbmc, and "set the content" of your TV directory in xbmc.
If not specified, xbmc is not updated
--xbmc-remote-control=host
host for the new xbmc communication, to automatically update library when new episodes arrive
You probably want to set this to "localhost"
--xbmc-remote-control-port=port
port number for the new xbmc communication
You probably want to set this to "9090", if that doesn't work, try "80"
--log-file=filepath
Log to this file
If not specified, output goes to sorttv.log in the script directory
--verbose
Output verbosity. Set to TRUE to show messages describing the decision making process.
If not specified, FALSE
--polling-time={X}
Tell the script to check for new files to sort every X seconds, minutes, hours, or days
You could set the script to start on system startup with polling, rather than using scheduling to start the script.
Valid values include "2secs", "2days", "1min", "3hrs", "30s" etc.
If not specified, polling is disabled and the script will only sort the directory once.
--read-config-file=filepath
Secondary config file, overwrites settings loaded so far
If not specified, only the default config file is loaded (sorttv.conf)
--fetch-show-title=[TRUE|FALSE]
Fetch show titles from thetvdb.com (for proper formatting)
If not specified, TRUE
--rename-episodes=[TRUE|FALSE]
Rename episodes to a new format when moving
If not specified, FALSE
--rename-tv-format={formatstring}
the format to use if renaming to a new format (as specified above)
Hint: including the Episode Title as part of the name slows the process down a bit since titles are retrieved from thetvdb.com
The formatstring can be made up of:
[SHOW_NAME]: "My Show"
[EP1]: "S01E01"
[EP2]: "1x1"
[EP3]: "1x01"
[EP4]: "01" (Episode number only)
[EP_NAME1]: " - Episode Title"
[EP_NAME2]: ".Episode Title"
[QUALITY]: " HD" (or " SD") - extracted from original file name
If not specified the format is "[SHOW_NAME] - [EP1][EP_NAME1]"
For example:
for "My Show S01E01 - Episode Title" (this is the default)
--rename-format=[SHOW_NAME] - [EP1][EP_NAME1]
for "My Show.S01E01.Episode Title"
--rename-format=[SHOW_NAME].[EP1][EP_NAME2]
--rename-movie-format={formatstring}
The format to use if renaming movies
The format can be made up of:
[MOVIE_TITLE]: "My Movie"
[YEAR1]: "(2011)"
[YEAR2]: "2011"
[QUALITY]: " HD" (or " SD") - extracted from original file name
[RATING]: "PG" - MPAA rating (US)
/: A sub-directory (folder) - movies can be in their own directories
If not specified the format is, "[MOVIE_TITLE] [YEAR2]/[MOVIE_TITLE] [YEAR1]"
--use-dots-instead-of-spaces=[TRUE|FALSE]
Renames episodes to replace spaces with dots
If not specified, FALSE
--movie-use-folder-name=[TRUE|FALSE]
Use folder name to search tmdb if no hit with file name.
If not specified, FALSE
--season-title=string
Season title
Note: if you want a space it needs to be included
(eg "Season " -> "Season 1", "Series "->"Series 1", "Season."->"Season.1")
If not specified, "Season "
--season-double-digits=[TRUE|FALSE]
Season format padded to double digits (eg "Season 01" rather than "Season 1")
If not specified, FALSE
--year-tolerance-for-error=number
The tolerated variance for year matches.
This applies to movies and to a lesser extent TV episodes (for sorting purposes).
For example, if a year is specified in the filename of a movie to sort, it can be off by this many years and still be considered the same movie as one in tmdb database.
Note that when sorting TV episodes, this is only considered when identifying local directories to sort into, and if a match is not found the year is subsequently ignored.
If not specified, 1
--match-type=[NORMAL|LIBERAL]
Match type.
LIBERAL assumes all files are episodes and tries to extract season and episode number any way possible.
If not specified, NORMAL
--match-files-based-on-tvdb-lookups=[TRUE|FALSE]
Attempt to sort files that are named after the episode title or air date.
For example, "My show - My episode title.avi" or "My show - 2010-12-12.avi"
could become "My Show - S01E01 - My episode title.avi"
Attempts to lookup the season and episode number based on the episodes in thetvdb.com database.
Since this involves downloading the list of episodes from the Internet, this will cause a slower sort.
If not specified, TRUE
--sort-by=[MOVE|COPY|MOVE-AND-LEAVE-SYMLINK-BEHIND|PLACE-SYMLINK|PLACE-HARDLINK]
Sort by moving or copying the file. If the file already exists because it was already copied it is silently skipped.
The MOVE-AND-LEAVE-SYMLINK-BEHIND option may be handy if you want to continue to seed after sorting, this leaves a symlink in place of the newly moved file.
PLACE-SYMLINK does not move the original file, but places a symlink in the sort-to directory (probably not what you want)
PLACE-HARDLINK does not move the original file, but places a hardlink in the sort-to directory. This might be helpful if you use Linux and you want a sorted and unsorted version on the same partition.
If not specified, MOVE
--treat-directories=[AS_FILES_TO_SORT|RECURSIVELY_SORT_CONTENTS|IGNORE]
How to treat directories.
AS_FILES_TO_SORT - sorts directories, moving entire directories that represents an episode, also detects and moves directories of entire seasons
RECURSIVELY_SORT_CONTENTS - doesn't move directories, just their contents, including subdirectories
IGNORE - ignores directories
If not specified, RECURSIVELY_SORT_CONTENTS
--require-show-directories-already-exist=[TRUE|FALSE]
Only sort into show directories that already exist
This may be helpful if you have multiple destination directories. Just set up all the other details in the conf file,
and specify the destination directory when invoking the script. Only episodes that match existing directories in the destination will be moved.
If this is false, then new directories are created for shows that dont have a directory.
If not specified, FALSE
--remove-symlinks=[TRUE|FALSE]
Deletes symlinks from the directory to sort while sorting.
This may be helpful if you want to remove all the symlinks you previously left behind using --sort-by=MOVE-AND-LEAVE-SYMLINK-BEHIND
You could schedule "perl sorttv.pl --remove-symlinks=TRUE" to remove these once a week/month
If this option is enabled and used at the same time as --sort-by=MOVE-AND-LEAVE-SYMLINK-BEHIND,
then only the previous links will be removed, and new ones may also be created
If not specified, FALSE
--show-name-substitute=NAME1-->NAME2
Substitutes names equal to NAME1 for NAME2
This argument can be repeated to add multiple rules for substitution
--tvdb-id-substitute=NAME1-->TVDB ID
Substitutes names equal to NAME1 for TVDB ID for lookups
This argument can be repeated to add multiple rules for substitution
--music-extension=extension
Define additional extensions for music files (SortTV knows a lot already)
This argument can be repeated to add multiple additional extensions
--sort-movie-directories=[TRUE|FALSE]
Attempt to sort entire directories that represent a movie
The directory (and all its contents AS IS) will be sorted
Note: Currently, this option WILL NOT rename or sort ANY of the contents of the directory,
including the movie. The directory will just be sorted into the movie-directory.
If not specified, FALSE
--force-windows-compatible-filenames=[TRUE|FALSE]
Forces MSWindows compatible file names, even when run on other platforms such as Linux
This may be helpful if you are writing to a Windows share from a Linux system
If not specified, TRUE
--tv-lookup-language=[en|...]
--movie-lookup-language=[en|...]
Set language for thetvdb / tmdb lookups, this effects episode and movie titles etc
Valid values include: it, zh, es, hu, nl, pl, sl, da, de, el, he, sv, eng, fi, no, fr, ru, cs, en, ja, hr, tr, ko, pt
If not specified, en (English)
--flatten-non-eps=[TRUE|FALSE]
Should non-episode files loose their directory structure?
This option only has an effect if a non-episode directory was specified.
If set to TRUE, they will be renamed after directory they were in.
Otherwise they keep their directory structure in the new non-episode-directory location.
If not specified, FALSE
--fetch-images=[NEW_SHOWS|FALSE]
Download images for shows, seasons, and episodes from thetvdb
Downloaded images are copied into the sort-to (destination) directory.
NEW_SHOWS - When new shows, seasons, or episodes are created the associated images are downloaded
FALSE - No images are downloaded
if not specified, NEW_SHOWS
--images-format=POSTER
Sets the image format to use, poster or banner.
POSTER/BANNER
if not specified, POSTER
--fetch-movie-images=[TRUE|FALSE]
Download images for movies
Downloaded images are copied into the sort-to (destination) directory.
If not specified, TRUE
--duplicate-images=[SYMLINK|COPY]
Duplicate images can be symlinked or copied. For example TV season images get placed in the main directory, and in season subdirectories.
The SYMLINK option is recommended. If symlinks are not available (for example, on Windows), then they will be copied.
If not specified, SYMLINK.
--if-file-exists=[SKIP|OVERWRITE]