forked from daraden/transcode-myth
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Transcode.py
executable file
·1592 lines (1482 loc) · 69.1 KB
/
Transcode.py
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/env python2
# -*- coding: UTF-8 -*-
from __future__ import print_function, division
import re
import subprocess
import json
import os
import sys
import tempfile
import time
from glob import glob
import logging
import logging.handlers
import shutil
from datetime import datetime
from io import open
import argparse
from MythTV import Recorded, Program, MythDB, VideoGrabber, Job, findfile
from MythTV.ttvdb import tvdb_api, tvdb_exceptions
class DictToNamespace(dict):
""" convert a dictionary and any nested dictionaries to namespace"""
def __init__(self, data, **kwargs):
super(DictToNamespace, self).__init__(**kwargs)
for k, v in data.items():
if isinstance(v, dict):
self.__setattr__(str(k), DictToNamespace(v))
self.__setitem__(str(k), DictToNamespace(v))
else:
try:
v = float(v)
if float(v).is_integer():
v = int(v)
except (TypeError, ValueError):
pass
self.__setitem__(str(k), v)
self.__setattr__(str(k), v)
config_dict = {'file': {'fileformat': 'mp4', 'logdir': '/',
'exportdir': '/', 'fallbackdir': '/', 'saveold': 1,
'usecommflag': 0, 'tvdirstruct': 'folders',
'mvdirstruct': 'none', 'commethod': 'remove',
'includesub': 0, 'export': 1, 'exporttype': 'kodi',
'episodetitle': 1, 'allowsearch': 0
},
'video': {'codechd': 'libx264', 'codecsd': 'libx264',
'presethd': 'medium', 'presetsd': 'medium',
'crfhd': 20, 'crfsd': 18, 'minratehd': 0,
'minratesd': 0, 'maxratehd': 0, 'maxratesd': 0,
'deinterlacehd': 'yadif', 'deinterlacesd': 'yadif'
},
'audio': {'codechd': 'aac', 'codecsd': 'aac', 'bpchd': 64,
'bpcsd': 64, 'language': 'eng'
}
}
conf_path = os.path.dirname(os.path.abspath(__file__))
config_file = '{}/conf.json'.format(conf_path)
class ConfigSetup:
"""
Load configuration file in json format. If no configuration file exists
a defaults dictionary is used to create one
"""
def __init__(self, configuration_file, defaults=None):
# Write default config file if none exists
self.file = None
self.video = None
self.audio = None
if not os.path.isfile(configuration_file):
with open(configuration_file, 'wb') as conf_write:
json.dump(defaults, conf_write)
config = {}
config_out = {}
# Set config dict to values in config file
with open(configuration_file, 'rb') as conf_read:
config.update(json.load(conf_read))
if config.keys() == defaults.keys():
for section, items in config.items():
config_out.update({str(section): {}})
if isinstance(items, dict):
for k, v in items.items():
if k in ['exportdir', 'fallbackdir']:
if not v.endswith('/'):
v = '{}/'.format(v)
k = str(k)
if not isinstance(v, (int, float)):
v = str(v)
items.update({str(k): str(v)})
config_out[str(section)].update({str(k): str(v)})
else:
config_out[str(section)].update({str(k): v})
if config_dict[section].keys() != items.keys():
invalid = ([item for item in items.keys()
if item not in config_dict[section].keys()])
missing = ([item for item in config_dict[section].keys()
if item not in items.keys()])
if invalid:
for item in invalid:
print('Invalid entry in configuration file: {}'
.format(item)
)
if missing:
for item in missing:
print('Missing entry in configuration file: {}'
.format(item)
)
for k, v in config_out.items():
setattr(self, k, DictToNamespace(v))
settings = ConfigSetup(config_file, defaults=config_dict)
def write_check(path):
"""Check if a directory is writeable if not return False."""
import errno
if not path.endswith('/'):
path = '{}/'.format(path)
test_file = '{}.test'.format(path)
try:
open(test_file, 'w')
except IOError as e:
print('Path is not writable:{}'.format(path))
if e.errno == errno.EACCES:
return False
else:
raise IOError(e.errno, e.strerror)
else:
os.remove(test_file)
return True
# *** logging setup ***
logdir = settings.file.logdir
if logdir is '/':
logdir = settings.file.logdir
logfile = '{}/transcode.log'.format(logdir)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
lf = logging.Formatter('%(asctime)s:%(levelname)s:%(message)s')
# setup console logging
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(lf)
logger.addHandler(ch)
# Setup file logging
try:
if not os.path.isdir(logdir):
os.makedirs(logdir)
if write_check(logdir):
fh = logging.handlers.TimedRotatingFileHandler(
filename=logfile, when='W0', interval=1, backupCount=10)
fh.setLevel(logging.DEBUG)
fh.setFormatter(lf)
logger.addHandler(fh)
if not write_check(logdir):
logging.error('logfile not accessible:{}'.format(logdir))
except Exception as e:
logging.error('logfile not accessible:{}'.format(logdir))
logging.error(e)
sys.exit(1)
# *** logging setup end ***
try:
db = MythDB()
except Exception as e:
logging.error(e)
sys.exit(1)
def program_check(program, alt_program=None):
"""
Check if program or optional alternate program is installed. returning
the path to the programs executable
"""
from distutils import spawn
if spawn.find_executable(program):
return spawn.find_executable(program)
if alt_program:
if spawn.find_executable(alt_program):
return spawn.find_executable(alt_program)
else:
if alt_program:
raise LookupError('Unable to find {} or {}'.format(program,
alt_program
)
)
if not alt_program:
raise LookupError('Unable to find {}'.format(program))
def get_free_space(path):
"""Get available space in path"""
st = os.statvfs(path)
fs = (st.f_bavail * st.f_frsize) / 1024
return fs
class AVInfo(dict):
"""
identify A/V configuration of input file and returns
self.video dict a list of self.audio.stream dicts,
and self.duration as float
"""
ffprobe = program_check('ffprobe', 'mythffprobe')
def __init__(self=None, input_file=None, **kwargs):
super(AVInfo, self).__init__(**kwargs)
self.audio = None
self.duration = None
self.video = None
command = [self.ffprobe, '-v', '-8', '-show_entries',
'stream=codec_type,index,codec_name,channels,width,'
'height,r_frame_rate:stream_tags=language:'
'format=duration', '-of', 'csv=nk=0:p=0', input_file
]
x = subprocess.check_output(command).split('\n')
vcd = {}
adict = {}
dur = 0
for vc in x:
if 'codec_type=video' in vc:
for item in vc.split(','):
k, v = item.split('=')
if k == 'r_frame_rate':
k = 'frame_rate'
v = int(v.split('/')[0]) / int(v.split('/')[1])
if k == 'index':
k = 'stream_index'
vcd.update({k: v})
for ac in x:
if 'codec_type=audio' in ac:
items = [item.split('=') for item in ac.split(',')]
streamdict = {k.strip(): v.strip() for (k, v) in items}
if 'tag:language' in streamdict.keys():
streamdict['language'] = (streamdict.pop
('tag:language')
)
adict.update({'stream{}'.format(streamdict['index'])
: streamdict})
if 'index' in streamdict.keys():
streamdict['stream_index'] = (streamdict.pop('index'))
adict.update({'stream{}'.format(streamdict['stream_index'])
: streamdict})
for d in x:
if d.startswith('duration='):
dur = float(d.split('=')[1])
self.__setattr__('video', DictToNamespace(vcd))
self.__setitem__('video', DictToNamespace(vcd))
self.__setattr__('duration', dur)
self.__setitem__('duration', dur)
self.__setitem__('audio', DictToNamespace(adict))
self.__setattr__('audio', DictToNamespace(adict))
def temp_check(tmpdir):
"""Create or clear temporary directory"""
try:
if os.path.isdir(tmpdir):
logging.info('chk:temp Folder found:{}'.format(tmpdir))
if os.listdir(tmpdir) != 0:
logging.warning('chk:Temp folder not empty!: '
'Removing Files:{}'.format(tmpdir)
)
shutil.rmtree(tmpdir)
os.makedirs(tmpdir)
if not os.path.isdir(tmpdir):
logging.info('chk:no temp folder found:{}'.format(tmpdir))
os.makedirs(tmpdir)
logging.info('chk:Temp folder created:{}'.format(tmpdir))
except Exception as e:
print(e)
logging.error(e)
def remove_temp(tmpdir):
"""Remove temporary directory"""
try:
if os.path.isdir(tmpdir):
logging.info('rem:temp Folder found:{}'.format(tmpdir))
shutil.rmtree(tmpdir)
if not os.path.isdir(tmpdir):
logging.info('rem:temp Folder Removed:{}'.format(tmpdir))
pass
except Exception as e:
print(e)
logging.error(e)
class FileSetup:
"""
Configure filename and directory structure to self.filename
and self.directory. using program-id to identify the output format.
"""
def __init__(self, settings, metadata):
# clean special characters from file/directory names
# using dict to make adding replacements simple
# may need to add several for windows compatibility
replace_dict = {'/': ' and '}
title = metadata.title
subtitle = metadata.subtitle
for replace_title in title:
if replace_title in replace_dict.keys():
title = title.replace(replace_title,
replace_dict[replace_title]
)
for replace_subtitle in subtitle:
if replace_subtitle in replace_dict.keys():
print(replace_dict.keys())
subtitle = subtitle.replace(replace_subtitle,
replace_dict[replace_subtitle])
mv_dir = settings.file.mvdirstruct
tv_dir = settings.file.tvdirstruct
program_id = metadata.programid
date = metadata.starttime.strftime('%Y.%m.%d')
sep = ''
if settings.file.exporttype == 'kodi':
sep = '_'
if settings.file.exporttype == 'plex':
sep = ' - '
if program_id.startswith('EP'):
logging.info('File setup: program identified as TV episode')
if tv_dir == 'none':
self.directory = ''
if tv_dir == 'folders':
self.directory = ('TV shows/{}/season {:02d}/'
.format(title, metadata.season)
)
if settings.file.episodetitle:
self.filename = ('{}{}S{:02d}E{:02d}{}{}'
.format(title, sep, metadata.season,
metadata.episode, sep,
subtitle
)
)
if not settings.file.episodetitle:
self.filename = ('{}{}S{:02d}E{:02d}'
.format(title, sep, metadata.season,
metadata.episode
)
)
# Movie setup
if program_id.startswith('MV'):
logging.info('File setup: program identified as Movie')
if metadata.year != metadata.starttime.year:
self.filename = '{}({})'.format(title, metadata.year)
if mv_dir == 'folders':
# may want/need to validate year?
self.directory = 'Movies/{}({})/'.format(title,
metadata.year
)
else:
self.filename = '{}'.format(title)
if mv_dir == 'folders':
# may need to validate year?
self.directory = 'Movies/{}/'.format(title)
if mv_dir == 'none':
self.directory = ''
# Unknown setup
if program_id is 'UNKNOWN':
logging.info('File setup: Program ID unknown')
self.directory = ''
if metadata.subtitle is not '':
self.filename = '{}{}{}{}{}'.format(title, sep,
subtitle, sep,
date
)
else:
self.filename = '{}{}{}'.format(title, sep, date)
def export_file(input_file, output_dir):
"""
Transfer file to output_dir, preforming hash verification to confirm
successful transfer. If verification fails a fallback.log file will be
created in the input_files directory. If the fallback.log file exists
any files listed within will be transferred
"""
input_name = input_file.split('/')[-1]
input_dir = '{}/'.format(os.path.dirname(input_file))
fallback_log = '{}fallback.log'.format(input_dir)
output_file = '{}{}'.format(output_dir, input_name)
def comp_hash(input_one, input_two, buffer_size=None):
"""compare the hash value of two files"""
def sha1_hash(file_path, buf_size=None):
"""generate sha1 hash from input"""
import hashlib
if not buf_size:
buf_size = 65536
else:
buf_size = buf_size
sha1 = hashlib.sha1()
with open(file_path, 'rb') as f:
while True:
data = f.read(buf_size)
if not data:
break
sha1.update(data)
return sha1.hexdigest()
hash_one = sha1_hash(input_one, buf_size=buffer_size)
hash_two = sha1_hash(input_two, buf_size=buffer_size)
if hash_one == hash_two:
return True
else:
return False
# Check for fallback log and process any entries
fallback_list = []
if os.path.isfile(fallback_log):
logging.info("Fallback log detected processing entry's")
with open(fallback_log, 'r') as input_files:
files = input_files.readlines()
for old_file in files:
old_file = old_file.rstrip('\n')
old_name = old_file.split('/')[-1]
old_dir = old_file.rstrip(old_name)
fallback_file = '{}{}'.format(input_dir, old_name)
if not os.path.isdir(old_dir):
os.makedirs(old_dir)
if os.path.isdir(old_dir):
if write_check(old_dir):
if not os.path.isfile(old_file):
if os.path.isfile(fallback_file):
shutil.copyfile(fallback_file, old_file)
successful_transfer = comp_hash(fallback_file,
old_file
)
if not successful_transfer:
# hash check failed add to fallback list
logging.error('Hash verification failed'
' for: {}'.format(old_file)
)
fallback_list.append(old_file)
# Should log reason?
if successful_transfer:
logging.info('Hash verification sucessful'
' for: {}'.format(old_file)
)
os.remove(fallback_file)
if not os.path.isdir(output_dir):
logging.info('Creating export directory')
os.makedirs(output_dir)
if os.path.isdir(output_dir):
logging.info('Copying file to export directory')
shutil.copyfile(input_file, output_file)
logging.info('Start hash verification')
successful_transfer = comp_hash(input_file, output_file)
if not successful_transfer:
logging.error('Hash verification failed')
fallback_list.append(output_file)
if successful_transfer:
logging.info('Hash verification sucessfull')
os.remove(input_file)
if fallback_list:
with open(fallback_log, 'w') as write_fallback:
for item in fallback_list:
write_fallback.write(u'{}\n'.format(item))
if not fallback_list and os.path.isfile(fallback_log):
os.remove(fallback_log)
def frames_to_time(cut_frames, frame_rate, frame_offset=0):
"""
Convert a list of frames to a list of times using The
frame-rate of the intended video file.
a frame offset can be used to shift the time for more
accurate times
"""
cut_times = []
for start, end in cut_frames:
if frame_offset != 0:
start = start + frame_offset
end = end + frame_offset
cut_times.append((start / frame_rate, end / frame_rate))
return cut_times
def generate_concat_filter(segment_list, avinfo):
"""
Generate FFMpeg filter complex using a list of tuples containing
start and end times of the desired segments. an AVInfo() instance
is used to identify the audio streams.
returns a tuple of filter complex, video map, audio map list
video map and audio map list are the outputs of the filter complex
"""
count = 0
filter_list = []
concat_list = []
video_map = ''
audio_list = []
for start, end in segment_list:
filter_string = ('[0:0]trim=start={}:end={},setpts=PTS-STARTPTS[v{}]'
.format(start, end, count)
)
audio_string = ''
for stream in avinfo.audio.keys():
stream_index = avinfo.audio[stream].stream_index
if audio_string is '':
audio_string = ('[0:{}]atrim=start={}:end={},'
'asetpts=PTS-STARTPTS[a{}s{}]'.format(
stream_index, start, end, count, stream_index))
else:
if (stream_index < re.search('\[0:\d\]',
audio_string).group(0)[4]):
audio_string = ('[0:{}]atrim=start={}:end={},'
'asetpts=PTS-STARTPTS[a{}s{}];{}'.format(
stream_index, start, end, count,
stream_index, audio_string)
)
else:
audio_string = ('{};{}'.format(
audio_string, '[0:{}]atrim=start={}:end={},'
'asetpts=PTS-STARTPTS[a{}s{}]'.format(
stream_index, start, end, count,
stream_index))
)
video_id = ''.join(re.findall('\[v\d{1,2}\]', filter_string))
audio_id = ''.join(re.findall('\[a\d{1,2}s\d\]', filter_string))
concat_list.append('{}{}'.format(video_id, audio_id))
filter_list.append('{};{}'.format(filter_string, audio_string))
count = count + 1
concat_string_list = []
last_concat = ''
count_two = 0
for item in filter_list:
audio_concat_string = ''
video_id = re.findall('\[v\d{1,2}\]', item)
audio_id = re.findall('\[a\d{1,2}s\d\]', item)
concat_id = '{}{}'.format(''.join(video_id), ''.join(audio_id))
if last_concat is '':
last_concat = concat_id
else:
for audio_stream in audio_id:
audio_concat_string = ('{}{}'.format(audio_concat_string,
'[ac{}s{}]'.format(
count_two,
audio_stream[-2]))
)
last_concat = ('{}{}concat=v=1:a={}[vc{}]{}'
.format(last_concat, concat_id, len(audio_id),
count_two, audio_concat_string
)
)
concat_string_list.append(last_concat)
last_concat = '[vc{}]{}'.format(count_two, audio_concat_string)
if item == filter_list[-1]:
audio_list.extend(re.findall('\[ac\d{1,2}s\d\]',
audio_concat_string
)
)
video_map = '[vc{}]'.format(count_two)
count_two = count_two + 1
concat_filter = ''
if concat_filter == '':
concat_filter = ('{};{};{}'.format(filter_list.pop(0),
filter_list.pop(0),
concat_string_list.pop(0))
)
while len(filter_list) >= 1:
concat_filter = ('{};{}'.format(concat_filter,
'{};{}'.format(filter_list.pop(0),
concat_string_list.pop(0)
)
)
)
return concat_filter, video_map, audio_list
def get_episode(series_title, episode_title=None, season_number=None,
episode_number=None, episode_separator=';',
part_separator='Part'):
"""
Retrieve and return a list of TV episode(s) metadata using ttvdb.tvdb_api.
episode_separator: character used for multiple episode subtitles
part_separator: character(s) used to separate episode subtitle from
episode part number. may need for non-english use?
"""
t = tvdb_api.Tvdb()
numerals = {'I': 1, 'II': 2, 'III': 3, 'IV': 4, 'V': 5, 'VI': 6, 'VII': 7,
'VIII': 8, 'IX': 7, 'X': 10
}
numbers_word = {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5,
'six': 6, 'seven': 7, 'eight': 8, 'nine': 9, 'ten': 10
}
episode_list = []
if episode_title:
if episode_separator and episode_separator in episode_title:
episode_split = episode_title.split(episode_separator)
for episode in episode_split:
try:
episode_result = (
get_episode(series_title, episode,
episode_separator=episode_separator,
part_separator=part_separator
)
)
except (tvdb_exceptions.tvdb_shownotfound,
tvdb_exceptions.tvdb_episodenotfound) as e:
print(e)
continue
if episode_result not in episode_list:
episode_list.extend(episode_result)
return episode_list
if part_separator and part_separator in episode_title:
part_split = episode_title.split('Part')
print(part_split)
part_title = part_split[0].rstrip()
part_number = part_split[-1].strip()
if part_number.isdigit():
part_number = int(part_number)
if part_number in numerals.keys():
part_number = numerals[part_number]
if part_number.lower() in numbers_word.keys():
part_number = numbers_word[part_number.lower()]
try:
result = t[series_title].search(part_title, key='episodename')
if len(result) >= 2:
for match in result:
if (match['episodename']
.endswith('({})'.format(part_number))):
episode_list.append(match)
if len(result) == 1:
episode_list.extend(result)
except (tvdb_exceptions.tvdb_shownotfound,
tvdb_exceptions.tvdb_seasonnotfound,
tvdb_exceptions.tvdb_episodenotfound) as e:
print(e)
try:
result = t[series_title].search(part_title,
key='episodename'
)
if len(result) == 1:
episode_list.extend(result)
except (tvdb_exceptions.tvdb_shownotfound,
tvdb_exceptions.tvdb_seasonnotfound,
tvdb_exceptions.tvdb_episodenotfound) as e:
print(e)
else:
episode_result = (t[series_title].search(episode_title,
key='episodename'
)
)
if len(episode_result) == 1 and episode_result not in episode_list:
episode_list.extend(episode_result)
if len(episode_result) == 0:
episode_result = (t[series_title].search(episode_title))
if len(episode_result) == 1:
episode_list.extend(episode_result)
if season_number and episode_number:
try:
episode_list.append(t[series_title][season_number][episode_number])
except (tvdb_exceptions.tvdb_shownotfound,
tvdb_exceptions.tvdb_seasonnotfound,
tvdb_exceptions.tvdb_episodenotfound) as e:
return e
for item in episode_list:
for k, v in item.items():
if k == 'episodenumber':
item.update({'episode': int(item.pop(k))})
if k == 'seasonnumber':
item.update({'season': int(item.pop(k))})
if k == 'firstaired':
item.update({'originalairdate': datetime.strptime(item.pop(k),
'%Y-%m-%d')
.date()
}
)
item.update({'airdate': item['originalairdate']})
item.update({'year': item['originalairdate'].year})
if k == 'overview':
item.update({'description': str(item.pop(k).encode('UTF-8'))})
if k == 'episodename':
item.update({'subtitle': str(item.pop(k).encode('UTF-8'))})
if k == 'rating':
item.update({'stars': float(item.pop(k))})
return episode_list
def get_movie(title, year=None):
"""Use MythTV video grabber to retrieve movie metadata."""
movie_grabber = VideoGrabber('movie')
movie_search = list(movie_grabber.search(title))
movie_result = []
result_len = len(movie_search)
for result in movie_search:
if result_len > 1:
if year is not None and year == result['year']:
movie_result.append(result)
elif result.title == title:
movie_result.append(result)
if result_len == 1:
movie_result = result
if result_len == 0:
print('Unable to locate Movie: {}'.format(title))
if len(movie_result) == 1:
return movie_result[0]
else:
return {}
class RecordingToMetadata:
"""
Retrieve required metadata from the MythTV database
allow_search[bool] option to allow metadata search for missing programid
"""
def __init__(self, recorded, allow_search=False):
self.title = u''
self.subtitle = u''
self.starttime = None
self.description = u''
self.season = 0
self.episode = 0
self.programid = u''
self.originalairdate = None
self.airdate = None
self.year = None
self.previouslyshown = None
self.cutlists = {}
cut_lists = {u'cut_list': None, u'uncut_list': None, u'skip_list': None,
u'unskip_list': None}
program = Program.fromRecorded(recorded)
for k, v in recorded.items():
if k in self.__dict__.keys():
if v not in ['', None]:
setattr(self, k, v)
for k, v in program.items():
if k in self.__dict__.keys():
if v not in ['', None]:
setattr(self, k, v)
self.title = u'{}'.format(self.title)
self.subtitle = u'{}'.format(self.subtitle)
self.description = u'{}'.format(self.description)
if recorded.cutlist:
cut_lists['cut_list'] = recorded.markup.getcutlist()
cut_lists['uncut_list'] = recorded.markup.getuncutlist()
if recorded.commflagged == 1:
cut_lists['skip_list'] = recorded.markup.getskiplist()
cut_lists['unskip_list'] = recorded.markup.getunskiplist()
self.cutlists = DictToNamespace(cut_lists)
# if no programid is found attempt internet search to identify
# TV episode or movie using available data.
if self.programid == u'':
if allow_search:
episode_data = []
if self.subtitle != u'':
episode_data = get_episode(self.title,
episode_title=self.subtitle
)
if self.subtitle == u'' and any([self.season, self.episode]):
episode_data = get_episode(self.title,
season_number=self.season,
episode_number=self.episode
)
if len(episode_data) == 1:
self.programid = u'EP'
for item in episode_data:
for k, v in item.items():
if k == 'airedSeason':
self.season = v
if k == 'airedEpisodeNumber':
self.episode = v
if k == 'firstAired':
self.originalairdate = (datetime
.strptime(v, '%Y-%m-%d')
)
self.year = self.originalairdate.year
if k is 'episodename' and self.subtitle is u'':
self.subtitle = u'{}'.format(v.decode('UTF-8'))
if k == 'description':
self.description = (u'{}'
.format(v.decode('UTF-8'))
)
# multi-episode recording setup would go here
if self.subtitle == u''\
and not any([self.season, self.episode]):
if self.year != self.starttime.year:
movie_data = get_movie(self.title, self.year)
else:
movie_data = get_movie(self.title)
if movie_data != {}:
self.programid = u'MV'
for item in movie_data:
for k, v in item.items():
if k == 'releasedate':
self.originalairdate = (datetime
.strptime(v,
'%Y-%m-%d'
)
)
self.year = self.originalairdate.year
if k == 'description':
self.description = u'{}'.format(v)
else:
if self.season and self.episode:
self.programid = u'EP'
if self.programid == u'':
self.programid = u'UNKNOWN'
def update_recorded(rec, input_file, output_file):
"""
Update MythTV database entry. clearing out old markup data and removing
thumbnail images.
"""
logging.info('Started: Database Recording update')
logging.debug('rec={} input_file={} output_file={}'.format(rec, input_file,
output_file
)
)
chanid = rec.chanid
starttime = (datetime.utcfromtimestamp(rec.starttime.timestamp())
.strftime('%Y%m%d%H%M%S')
)
logging.debug('chanid={} starttime={}'.format(chanid, starttime))
try:
subprocess.call(['mythutil', '--chanid', str(chanid), '--starttime',
str(starttime), '--clearcutlist'
]
)
except Exception as e:
logging.error('Mythutil exception clearing cut-list: {}'.format(e))
try:
subprocess.call(['mythutil', '--chanid', str(chanid), '--starttime',
str(starttime), '--clearskiplist'
]
)
except Exception as e:
logging.error('Mythutil exception clearing skip-list:{}'.format(e))
pass
for index, mark in reversed(list(enumerate(rec.markup))):
if mark.type in (rec.markup.MARK_COMM_START, rec.markup.MARK_COMM_END):
del rec.markup[index]
rec.bookmark = 0
rec.bookmarkupdate = datetime.now()
rec.cutlist = 0
rec.commflagged = 0
rec.markup.commit()
rec.basename = os.path.basename(output_file)
rec.filesize = os.path.getsize(output_file)
rec.transcoded = 1
rec.seek.clean()
rec.update()
try:
logging.info('Removing PNG files')
for png in glob('{}*.png'.format(input_file)):
os.remove(png)
except Exception as e:
logging.error('Error removing png files', e)
pass
try:
logging.info('Removing JPG files')
for jpg in glob('{}*.jpg'.format(input_file)):
os.remove(jpg)
except Exception as e:
logging.error('Error removing jpg files', e)
pass
try:
logging.info('Rebuilding seek-table')
subprocess.call(['mythcommflag', '--chanid', str(chanid), '--starttime',
str(starttime), '--rebuild'
]
)
except Exception as e:
logging.error('Mythcommflag ERROR clearing skip-list:{}'.format(e))
pass
class Encoder:
"""Configure and run FFmpeg encoding"""
ffmpeg = program_check('ffmpeg', 'mythffmpeg')
def __init__(self, input_file, output_file, settings=None, metadata=None):
self.input_file = input_file
self.output_file = output_file
self.settings = settings
self.temp_dir = ('{}{}/'
.format(self.settings.file.fallbackdir,
os.path.basename(input_file).rsplit('.')[0]
)
)
self.temp_file = ('{}{}'
.format(self.temp_dir,
os.path.basename(input_file).rsplit('.')[0]
)
)
self.av_info = AVInfo(input_file)
self.metadata = metadata
self.metadata_file = None
self.hd = False
self.map_count = 0
self.subtitle_input = None
self.subtitle_metadata = None
self.video_config = []
self.audio_config = []
if (self.av_info.video.height >= 720
and self.av_info.video.width >= 1280):
self.hd = True
else:
self.hd = False
# Check directory access
if not write_check(self.settings.file.fallbackdir):
logging.error('Fallback directory is not writable')
sys.exit(1)
if not write_check(self.settings.file.exportdir):
if self.settings.file.export:
logging.error('Export directory is not writable')
sys.exit(1)
else:
logging.warning('Export directory is not writable')
temp_check(self.temp_dir)
def video_setup():
"""Create self.video_config list for use by ffmpeg"""
self.video_config = ['-map', '0:0', '-filter:v', 'yadif=0:-1:1',
'-movflags', 'faststart', '-forced-idr', '1',
'-c:v'
]
if self.hd:
self.video_config.extend((self.settings.video.codechd,
'-preset:v',
self.settings.video.presethd,
'-crf:v',
str(self.settings.video.crfhd)
)
)
# if max_HD != 0:
# max_HD = max_HD * 1000
# Vparam.extend(('-maxrate:v', str(max_HD), '-bufsize:v',
# str(max_HD * vbuff))
# )
# if min_HD != 0:
# min_HD = min_HD * 1000
# Vparam.extend(('-minrate:v', str(min_HD)))
elif not self.hd:
self.video_config.extend((self.settings.video.codecsd,
'-preset:v',
self.settings.video.presethd,
'-crf:v',
str(self.settings.video.crfsd)
)
)
# if max_SD != 0:
# max_SD = max_SD * 1000
# Vparam.extend(('-maxrate:v', str(max_SD), '-bufsize:v',
# str(max_SD * vbuff))
# )
# if min_SD != 0:
# min_SD = min_SD * 1000
# Vparam.extend(('-minrate:v', str(min_SD)))
def audio_setup():
"""Create self.audio_config list for use by ffmpeg"""
self.audio_config = []
# if self.settings.audio.language == 'all':
audio_map_list = []
audio_map = []
for select in self.av_info.audio:
audio_map.extend(['-map',
'0:{}'.format(self.av_info.audio[select]
.stream_index
)
]
)
audio_map.append('-c:a')
if self.hd: