-
Notifications
You must be signed in to change notification settings - Fork 278
/
__init__.py
1808 lines (1447 loc) · 58.1 KB
/
__init__.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
# coding=utf-8
"""Various helper methods."""
from __future__ import unicode_literals
import base64
import ctypes
import datetime
import errno
import hashlib
import imghdr
import io
import logging
import os
import platform
import random
import re
import shutil
import socket
import ssl
import stat
import struct
import time
import traceback
import uuid
import zipfile
from builtins import chr
from builtins import hex
from builtins import str
from builtins import zip
from itertools import cycle
from xml.etree import ElementTree
from cachecontrol import CacheControlAdapter
from cachecontrol.cache import DictCache
import certifi
from contextlib2 import suppress
import guessit
from medusa import app, db
from medusa.common import DOWNLOADED, USER_AGENT
from medusa.helper.common import (episode_num, http_code_description, media_extensions,
pretty_file_size, subtitle_extensions)
from medusa.helpers.utils import generate
from medusa.imdb import Imdb
from medusa.indexers.indexer_exceptions import IndexerException
from medusa.logger.adapters.style import BraceAdapter, BraceMessage
from medusa.session.core import MedusaSafeSession
from medusa.show.show import Show
import requests
from requests.compat import urlparse
from six import binary_type, ensure_binary, ensure_text, string_types, text_type, viewitems
from six.moves import http_client
log = BraceAdapter(logging.getLogger(__name__))
log.logger.addHandler(logging.NullHandler())
try:
import reflink
except ImportError:
reflink = None
try:
from psutil import Process
memory_usage_tool = 'psutil'
except ImportError:
try:
import resource # resource module is unix only
memory_usage_tool = 'resource'
except ImportError:
memory_usage_tool = None
def indent_xml(elem, level=0):
"""Do our pretty printing and make Matt very happy."""
i = '\n' + level * ' '
if elem:
if not elem.text or not elem.text.strip():
elem.text = i + ' '
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
indent_xml(elem, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def is_media_file(filename):
"""Check if named file may contain media.
:param filename: Filename to check
:type filename: str
:return: True if this is a known media file, False if not
:rtype: bool
"""
# ignore samples
try:
if re.search(r'(^|[\W_])(?<!shomin.)(sample\d*)[\W_]', filename, re.I):
return False
# ignore RARBG release intro
if re.search(r'^RARBG(\.(com|to))?\.(txt|avi|mp4)$', filename, re.I):
return False
# ignore MAC OS's retarded "resource fork" files
if filename.startswith('._'):
return False
sep_file = filename.rpartition('.')
if re.search('extras?$', sep_file[0], re.I):
return False
return sep_file[2].lower() in media_extensions
except TypeError as error: # Not a string
log.debug(u'Invalid filename. Filename must be a string. {error}',
{'error': error})
return False
def is_rar_file(filename):
"""Check if file is a RAR file, or part of a RAR set.
:param filename: Filename to check
:type filename: str
:return: True if this is RAR/Part file, False if not
:rtype: bool
"""
archive_regex = r'(?P<file>^(?P<base>(?:(?!\.part\d+\.rar$).)*)\.(?:(?:part0*1\.)?rar)$)'
return bool(re.search(archive_regex, filename))
def is_subtitle(file_path):
"""Return whether the file is a subtitle or not.
:param file_path: path to the file
:type file_path: text_type
:return: True if it is a subtitle, else False
:rtype: bool
"""
return get_extension(file_path) in subtitle_extensions
def get_extension(file_path):
"""Return the file extension without leading dot.
:param file_path: path to the file
:type file_path: text_type
:return: extension or empty string
:rtype: text_type
"""
return os.path.splitext(file_path)[1][1:]
def remove_file_failed(failed_file):
"""Remove file from filesystem.
:param failed_file: File to remove
:type failed_file: str
"""
try:
os.remove(failed_file)
except Exception:
pass
def make_dir(path):
"""Make a directory on the filesystem.
:param path: directory to make
:type path: str
:return: True if success, False if failure
:rtype: bool
"""
if not os.path.isdir(path):
try:
os.makedirs(path)
# do the library update for synoindex
from medusa import notifiers
notifiers.synoindex_notifier.addFolder(path)
except OSError:
return False
return True
def search_indexer_for_show_id(show_name, indexer=None, series_id=None, ui=None):
"""Contact indexer to check for information on shows by showid.
:param show_name: Name of show
:type show_name: str
:param indexer: Which indexer to use
:type indexer: int
:param indexer_id: Which indexer ID to look for
:type indexer_id: int
:param ui: Custom UI for indexer use
:return:
"""
from medusa.indexers.indexer_api import indexerApi
show_names = [re.sub('[. -]', ' ', show_name)]
# Query Indexers for each search term and build the list of results
for i in indexerApi().indexers if not indexer else int(indexer or []):
# Query Indexers for each search term and build the list of results
indexer_api = indexerApi(i)
indexer_api_params = indexer_api.api_params.copy()
if ui is not None:
indexer_api_params['custom_ui'] = ui
t = indexer_api.indexer(**indexer_api_params)
for name in show_names:
log.debug(u'Trying to find {name} on {indexer}',
{'name': name, 'indexer': indexer_api.name})
try:
search = t[series_id] if series_id else t[name]
except Exception:
continue
try:
searched_series_name = search[0]['seriesname']
except Exception:
searched_series_name = None
try:
searched_series_id = search[0]['id']
except Exception:
searched_series_id = None
if not (searched_series_name and searched_series_id):
continue
series = Show.find_by_id(app.showList, i, searched_series_id)
# Check if we can find the show in our list
# if not, it's not the right show
if (series_id is None) and (series is not None) and (series.indexerid == int(searched_series_id)):
return searched_series_name, i, int(searched_series_id)
elif (series_id is not None) and (int(series_id) == int(searched_series_id)):
return searched_series_name, i, int(series_id)
if indexer:
break
return None, None, None
def list_media_files(path):
"""Get a list of files possibly containing media in a path.
:param path: Path to check for files
:type path: str
:return: list of files
:rtype: list of str
"""
if not dir or not os.path.isdir(path):
return []
files = []
for cur_file in os.listdir(path):
full_cur_file = os.path.join(path, cur_file)
# if it's a folder do it recursively
if os.path.isdir(full_cur_file) and not cur_file.startswith('.') and not cur_file == 'Extras':
files += list_media_files(full_cur_file)
elif is_media_file(cur_file):
files.append(full_cur_file)
return files
def copy_file(src_file, dest_file):
"""Copy a file from source to destination.
:param src_file: Path of source file
:type src_file: str
:param dest_file: Path of destination file
:type dest_file: str
"""
try:
from shutil import SpecialFileError, Error
except ImportError:
from shutil import Error
SpecialFileError = Error
try:
shutil.copyfile(src_file, dest_file)
except (SpecialFileError, Error) as error:
log.warning('Error copying file: {error}', {'error': error})
except OSError as error:
msg = BraceMessage('OSError: {0!r}', error)
if error.errno == errno.ENOSPC:
# Only warn if device is out of space
log.warning(msg)
else:
# Error for any other OSError
log.error(msg)
else:
try:
shutil.copymode(src_file, dest_file)
except OSError:
pass
def move_file(src_file, dest_file):
"""Move a file from source to destination.
:param src_file: Path of source file
:type src_file: str
:param dest_file: Path of destination file
:type dest_file: str
"""
try:
shutil.move(src_file, dest_file)
fix_set_group_id(dest_file)
except OSError:
copy_file(src_file, dest_file)
os.unlink(src_file)
def link(src, dst):
"""Create a file link from source to destination.
TODO: Make this unicode proof
:param src: Source file
:type src: str
:param dst: Destination file
:type dst: str
"""
if os.name == 'nt':
if ctypes.windll.kernel32.CreateHardLinkW(text_type(dst), text_type(src), 0) == 0:
raise ctypes.WinError()
else:
os.link(src, dst)
def hardlink_file(src_file, dest_file):
"""Create a hard-link (inside filesystem link) between source and destination.
:param src_file: Source file
:type src_file: str
:param dest_file: Destination file
:type dest_file: str
"""
try:
link(src_file, dest_file)
fix_set_group_id(dest_file)
except OSError as msg:
if msg.errno == errno.EEXIST:
# File exists. Don't fallback to copy
log.warning(
u'Failed to create hardlink of {source} at {destination}.'
u' Error: {error!r}', {
'source': src_file,
'destination': dest_file,
'error': msg
}
)
else:
log.warning(
u'Failed to create hardlink of {source} at {destination}.'
u' Error: {error!r}. Copying instead', {
'source': src_file,
'destination': dest_file,
'error': msg,
}
)
copy_file(src_file, dest_file)
def symlink(src, dst):
"""Create a soft/symlink between source and destination.
:param src: Source file
:type src: str
:param dst: Destination file
:type dst: str
"""
if os.name == 'nt':
if ctypes.windll.kernel32.CreateSymbolicLinkW(text_type(dst), text_type(src),
1 if os.path.isdir(src) else 0) in [0, 1280]:
raise ctypes.WinError()
else:
os.symlink(src, dst)
def move_and_symlink_file(src_file, dest_file):
"""Move a file from source to destination, then create a symlink back from destination from source.
If this fails, copy the file from source to destination.
:param src_file: Source file
:type src_file: str
:param dest_file: Destination file
:type dest_file: str
"""
try:
shutil.move(src_file, dest_file)
fix_set_group_id(dest_file)
symlink(dest_file, src_file)
except OSError as msg:
if msg.errno == errno.EEXIST:
# File exists. Don't fallback to copy
log.warning(
u'Failed to create symlink of {source} at {destination}.'
u' Error: {error!r}', {
'source': src_file,
'destination': dest_file,
'error': msg,
}
)
else:
log.warning(
u'Failed to create symlink of {source} at {destination}.'
u' Error: {error!r}. Copying instead', {
'source': src_file,
'destination': dest_file,
'error': msg,
}
)
copy_file(src_file, dest_file)
def reflink_file(src_file, dest_file):
"""Copy a file from source to destination with a reference link.
:param src_file: Source file
:type src_file: str
:param dest_file: Destination file
:type dest_file: str
"""
try:
if reflink is None:
raise NotImplementedError()
reflink.reflink(src_file, dest_file)
except (reflink.ReflinkImpossibleError, IOError) as msg:
if msg.args and msg.args[0] == 'EOPNOTSUPP':
log.warning(
u'Failed to create reference link of {source} at {destination}.'
u' Error: Filesystem or OS has not implemented reflink. Copying instead', {
'source': src_file,
'destination': dest_file,
}
)
copy_file(src_file, dest_file)
elif msg.args and msg.args[0] == 'EXDEV':
log.warning(
u'Failed to create reference link of {source} at {destination}.'
u' Error: Can not reflink between two devices. Copying instead', {
'source': src_file,
'destination': dest_file,
}
)
copy_file(src_file, dest_file)
else:
log.warning(
u'Failed to create reflink of {source} at {destination}.'
u' Error: {error!r}. Copying instead', {
'source': src_file,
'destination': dest_file,
'error': msg,
}
)
copy_file(src_file, dest_file)
except NotImplementedError:
log.warning(
u'Failed to create reference link of {source} at {destination}.'
u' Error: Filesystem does not support reflink or reflink is not installed. Copying instead', {
'source': src_file,
'destination': dest_file,
}
)
copy_file(src_file, dest_file)
def make_dirs(path):
"""Create any folders that are missing and assigns them the permissions of their parents.
:param path:
:rtype path: str
"""
log.debug(u'Checking if the path {path} already exists', {'path': path})
if not os.path.isdir(path):
# Windows, create all missing folders
if os.name == 'nt' or os.name == 'ce':
try:
log.debug(u"Folder {path} didn't exist, creating it",
{'path': path})
os.makedirs(path)
except (OSError, IOError) as msg:
log.error(u'Failed creating {path} : {error!r}',
{'path': path, 'error': msg})
return False
# not Windows, create all missing folders and set permissions
else:
sofar = ''
folder_list = path.split(os.path.sep)
# look through each subfolder and make sure they all exist
for cur_folder in folder_list:
sofar += cur_folder + os.path.sep
# if it exists then just keep walking down the line
if os.path.isdir(sofar):
continue
try:
log.debug(u"Folder {path} didn't exist, creating it",
{'path': sofar})
os.mkdir(sofar)
# use normpath to remove end separator,
# otherwise checks permissions against itself
chmod_as_parent(os.path.normpath(sofar))
# do the library update for synoindex
from medusa import notifiers
notifiers.synoindex_notifier.addFolder(sofar)
except (OSError, IOError) as msg:
log.error(u'Failed creating {path} : {error!r}',
{'path': sofar, 'error': msg})
return False
return True
def rename_ep_file(cur_path, new_path, old_path_length=0):
"""Create all folders needed to move a file to its new location.
Rename it and then cleans up any folders left that are now empty.
:param cur_path: The absolute path to the file you want to move/rename
:type cur_path: str
:param new_path: The absolute path to the destination for the file WITHOUT THE EXTENSION
:type new_path: str
:param old_path_length: The length of media file path (old name) WITHOUT THE EXTENSION
:type old_path_length: int
"""
from medusa import subtitles
if old_path_length == 0 or old_path_length > len(cur_path):
# approach from the right
cur_file_name, cur_file_ext = os.path.splitext(cur_path)
else:
# approach from the left
cur_file_ext = cur_path[old_path_length:]
cur_file_name = cur_path[:old_path_length]
if cur_file_ext[1:] in subtitle_extensions:
# Extract subtitle language from filename
sublang = os.path.splitext(cur_file_name)[1][1:]
# Check if the language extracted from filename is a valid language
if sublang in subtitles.subtitle_code_filter():
cur_file_ext = '.' + sublang + cur_file_ext
# put the extension on the incoming file
new_path += cur_file_ext
# Only rename if something has changed in the new name
if cur_path == new_path:
return True
make_dirs(os.path.dirname(new_path))
# move the file
try:
log.info(u"Renaming file from '{old}' to '{new}'",
{'old': cur_path, 'new': new_path})
shutil.move(cur_path, new_path)
except (OSError, IOError) as msg:
log.error(u"Failed renaming '{old}' to '{new}' : {error!r}",
{'old': cur_path, 'new': new_path, 'error': msg})
return False
# clean up any old folders that are empty
delete_empty_folders(os.path.dirname(cur_path))
return True
def delete_empty_folders(top_dir, keep_dir=None):
"""Walk backwards up the path and deletes any empty folders found.
:param top_dir: Clean until this directory is reached (absolute path to a folder)
:type top_dir: str
:param keep_dir: Don't delete this directory, even if it's empty
:type keep_dir: str
"""
if not top_dir or not os.path.isdir(top_dir):
return
log.info(u'Trying to clean any empty folder under {path}',
{'path': top_dir})
for directory in os.walk(top_dir, topdown=False):
dirpath = directory[0]
if dirpath == top_dir:
return
if dirpath != keep_dir and not os.listdir(dirpath):
try:
log.info(u'Deleting empty folder: {folder}',
{'folder': dirpath})
os.rmdir(dirpath)
# Do the library update for synoindex
from medusa import notifiers
notifiers.synoindex_notifier.deleteFolder(dirpath)
except OSError as msg:
log.warning(u'Unable to delete {folder}. Error: {error!r}',
{'folder': dirpath, 'error': msg})
else:
log.debug(u'Not deleting {folder}. The folder is not empty'
u' or should be kept.', {'folder': dirpath})
def file_bit_filter(mode):
"""Strip special filesystem bits from file.
:param mode: mode to check and strip
:return: required mode for media file
"""
for bit in [stat.S_IXUSR, stat.S_IXGRP, stat.S_IXOTH, stat.S_ISUID, stat.S_ISGID]:
if mode & bit:
mode -= bit
return mode
def chmod_as_parent(child_path):
"""Retain permissions of parent for childs.
(Does not work for Windows hosts)
:param child_path: Child Path to change permissions to sync from parent
:type child_path: str
"""
if os.name == 'nt' or os.name == 'ce':
return
parent_path = os.path.dirname(child_path)
if not parent_path:
log.debug(u'No parent path provided in {path}, unable to get'
u' permissions from it', {'path': child_path})
return
child_path = os.path.join(parent_path, os.path.basename(child_path))
if not os.path.exists(child_path):
return
parent_path_stat = os.stat(parent_path)
parent_mode = stat.S_IMODE(parent_path_stat[stat.ST_MODE])
child_path_stat = os.stat(child_path.encode(app.SYS_ENCODING))
child_path_mode = stat.S_IMODE(child_path_stat[stat.ST_MODE])
if os.path.isfile(child_path):
child_mode = file_bit_filter(parent_mode)
else:
child_mode = parent_mode
if child_path_mode == child_mode:
return
child_path_owner = child_path_stat.st_uid
user_id = os.geteuid()
if user_id not in (0, child_path_owner):
log.debug(u'Not running as root or owner of {path}, not trying to set'
u' permissions', {'path': child_path})
return
try:
os.chmod(child_path, child_mode)
log.debug(
u'Setting permissions for {path} to {mode} as parent directory'
u' has {parent_mode}', {
'path': child_path,
'mode': child_mode,
'parent_mode': parent_mode
}
)
except OSError:
log.debug(u'Failed to set permission for {path} to {mode}',
{'path': child_path, 'mode': child_mode})
def fix_set_group_id(child_path):
"""Inherid SGID from parent.
(does not work on Windows hosts)
:param child_path: Path to inherit SGID permissions from parent
:type child_path: str
"""
if os.name == 'nt' or os.name == 'ce':
return
parent_path = os.path.dirname(child_path)
parent_stat = os.stat(parent_path)
parent_mode = stat.S_IMODE(parent_stat[stat.ST_MODE])
child_path = os.path.join(parent_path, os.path.basename(child_path))
if parent_mode & stat.S_ISGID:
parent_gid = parent_stat[stat.ST_GID]
child_stat = os.stat(child_path.encode(app.SYS_ENCODING))
child_gid = child_stat[stat.ST_GID]
if child_gid == parent_gid:
return
child_path_owner = child_stat.st_uid
user_id = os.geteuid()
if user_id not in (0, child_path_owner):
log.debug(u'Not running as root or owner of {path}, not trying to'
u' set the set-group-ID', {'path': child_path})
return
try:
os.chown(child_path, -1, parent_gid)
log.debug(u'Respecting the set-group-ID bit on the parent'
u' directory for {path}', {'path': child_path})
except OSError:
log.error(
u'Failed to respect the set-group-ID bit on the parent'
u' directory for {path} (setting group ID {gid})',
{'path': child_path, 'gid': parent_gid})
def is_anime_in_show_list():
"""Check if any shows in list contain anime.
:return: True if global showlist contains Anime, False if not
"""
for show in app.showList:
if show.is_anime:
return True
return False
def update_anime_support():
"""Check if we need to support anime, and if we do, enable the feature."""
app.ANIMESUPPORT = is_anime_in_show_list()
def get_absolute_number_from_season_and_episode(series_obj, season, episode):
"""Find the absolute number for a show episode.
:param show: Show object
:param season: Season number
:param episode: Episode number
:return: The absolute number
"""
absolute_number = None
if season and episode:
main_db_con = db.DBConnection()
sql = 'SELECT * FROM tv_episodes WHERE indexer = ? AND showid = ? AND season = ? AND episode = ?'
sql_results = main_db_con.select(sql, [series_obj.indexer, series_obj.series_id, season, episode])
if len(sql_results) == 1:
absolute_number = int(sql_results[0]['absolute_number'])
log.debug(
u'Found absolute number {absolute} for show {show} {ep}', {
'absolute': absolute_number,
'show': series_obj.name,
'ep': episode_num(season, episode),
}
)
else:
log.debug(u'No entries for absolute number for show {show} {ep}',
{'show': series_obj.name, 'ep': episode_num(season, episode)})
return absolute_number
def get_all_episodes_from_absolute_number(show, absolute_numbers, indexer_id=None, indexer=None):
episodes = []
season = None
if absolute_numbers:
if not show and (indexer_id and indexer):
show = Show.find_by_id(app.showList, indexer, indexer_id)
for absolute_number in absolute_numbers if show else []:
ep = show.get_episode(None, None, absolute_number=absolute_number)
if ep:
episodes.append(ep.episode)
# this will always take the last found season so eps that cross
# the season border are not handled well
season = ep.season
return season, episodes
def sanitize_scene_name(name, anime=False):
"""Take a show name and returns the "scenified" version of it.
:param name: Show name to be sanitized.
:param anime: Some show have a ' in their name(Kuroko's Basketball) and is needed for search.
:return: A string containing the scene version of the show name given.
"""
if not name:
return ''
bad_chars = u',:()!?\u2019'
if not anime:
bad_chars += u"'"
# strip out any bad chars
for x in bad_chars:
name = name.replace(x, '')
# tidy up stuff that doesn't belong in scene names
name = name.replace('- ', '.').replace(' ', '.').replace('&', 'and').replace('/', '.').replace(': ', ' ')
name = re.sub(r'\.\.*', '.', name)
if name.endswith('.'):
name = name[:-1]
return name
def create_https_certificates(ssl_cert, ssl_key):
"""Create self-signed HTTPS certificares and store in paths 'ssl_cert' and 'ssl_key'.
:param ssl_cert: Path of SSL certificate file to write
:param ssl_key: Path of SSL keyfile to write
:return: True on success, False on failure
:rtype: bool
"""
try:
from OpenSSL import crypto
from certgen import createKeyPair, createCertRequest, createCertificate, TYPE_RSA
except Exception:
log.warning(u'pyopenssl module missing, please install for'
u' https access')
return False
# Serial number for the certificate
serial = int(time.time())
# Create the CA Certificate
cakey = createKeyPair(TYPE_RSA, 1024)
careq = createCertRequest(cakey, CN='Certificate Authority')
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
cname = 'Medusa'
pkey = createKeyPair(TYPE_RSA, 1024)
req = createCertRequest(pkey, CN=cname)
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
# Save the key and certificate to disk
try:
io.open(ssl_key, 'wb').write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
io.open(ssl_cert, 'wb').write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
except Exception:
log.error(u'Error creating SSL key and certificate')
return False
return True
def backup_versioned_file(old_file, version):
"""Back up an old version of a file.
:param old_file: Original file, to take a backup from
:param version: Version of file to store in backup
:return: True if success, False if failure
"""
num_tries = 0
with suppress(TypeError):
version = u'.'.join([str(i) for i in version]) if not isinstance(version, str) else version
new_file = u'{old_file}.v{version}'.format(old_file=old_file, version=version)
while not os.path.isfile(new_file):
if not os.path.isfile(old_file):
log.debug(u"Not creating backup, {old_file} doesn't exist",
{'old_file': old_file})
break
try:
log.debug(u'Trying to back up {old} to new',
{'old': old_file, 'new': new_file})
shutil.copy(old_file, new_file)
log.debug(u'Backup done')
break
except OSError as error:
log.warning(u'Error while trying to back up {old} to {new}:'
u' {error!r}',
{'old': old_file, 'new': new_file, 'error': error})
num_tries += 1
time.sleep(1)
log.debug(u'Trying again.')
if num_tries >= 10:
log.error(u'Unable to back up {old} to {new}, please do it'
u' manually.', {'old': old_file, 'new': new_file})
return False
return True
def get_lan_ip():
"""Return IP of system."""
try:
return [ip for ip in socket.gethostbyname_ex(socket.gethostname())[2] if not ip.startswith('127.')][0]
except Exception:
return socket.gethostname()
def check_url(url):
"""Check if a URL exists without downloading the whole file.
We only check the URL header.
"""
# see also http://stackoverflow.com/questions/2924422
# http://stackoverflow.com/questions/1140661
good_codes = [http_client.OK, http_client.FOUND, http_client.MOVED_PERMANENTLY]
host, path = urlparse(url)[1:3] # elems [1] and [2]
try:
conn = http_client.HTTPConnection(host)
conn.request('HEAD', path)
return conn.getresponse().status in good_codes
except Exception:
return None
# Encryption
# ==========
# By Pedro Jose Pereira Vieito <[email protected]> (@pvieito)
#
# * If encryption_version==0 then return data without encryption
# * The keys should be unique for each device
#
# To add a new encryption_version:
# 1) Code your new encryption_version
# 2) Update the last encryption_version available in server/web/config/general.py
# 3) Remember to maintain old encryption versions and key generators for retro-compatibility
# Key Generators
unique_key1 = hex(uuid.getnode() ** 2) # Used in encryption v1
# Encryption Functions
def encrypt(data, encryption_version=0, _decrypt=False):
# Version 0: Plain text
if encryption_version == 0:
return data
else:
# Simple XOR encryption, Base64 encoded
# Version 1: unique_key1; Version 2: app.ENCRYPTION_SECRET
key = unique_key1 if encryption_version == 1 else app.ENCRYPTION_SECRET
if _decrypt:
data = ensure_text(base64.decodestring(ensure_binary(data)))
return ''.join(chr(ord(x) ^ ord(y)) for (x, y) in zip(data, cycle(key)))
else:
data = ''.join(chr(ord(x) ^ ord(y)) for (x, y) in zip(data, cycle(key)))
return ensure_text(base64.encodestring(ensure_binary(data))).strip()
def decrypt(data, encryption_version=0):
return encrypt(data, encryption_version, _decrypt=True)
def full_sanitize_scene_name(name):
return re.sub('[. -]', ' ', sanitize_scene_name(name)).lower().lstrip()
def get_show(name, try_indexers=False):
"""
Retrieve a series object using the series name.
:param name: A series name or a list of series names, when the parsed series result, returned multiple.
:param try_indexers: Toggle the lookup of the series using the series name and one or more indexers.
:return: The found series object or None.
"""
from medusa import classes, name_cache, scene_exceptions
if not app.showList:
return