diff --git a/lib/pysrt/__init__.py b/lib/pysrt/__init__.py new file mode 100644 index 00000000..34e96717 --- /dev/null +++ b/lib/pysrt/__init__.py @@ -0,0 +1,18 @@ +from pysrt.srttime import SubRipTime +from pysrt.srtitem import SubRipItem +from pysrt.srtfile import SubRipFile +from pysrt.srtexc import Error, InvalidItem, InvalidTimeString +from pysrt.version import VERSION, VERSION_STRING + +__all__ = [ + 'SubRipFile', 'SubRipItem', 'SubRipFile', 'SUPPORT_UTF_32_LE', + 'SUPPORT_UTF_32_BE', 'InvalidItem', 'InvalidTimeString' +] + +ERROR_PASS = SubRipFile.ERROR_PASS +ERROR_LOG = SubRipFile.ERROR_LOG +ERROR_RAISE = SubRipFile.ERROR_RAISE + +open = SubRipFile.open +stream = SubRipFile.stream +from_string = SubRipFile.from_string diff --git a/lib/pysrt/commands.py b/lib/pysrt/commands.py new file mode 100644 index 00000000..557c663d --- /dev/null +++ b/lib/pysrt/commands.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable-all + +import os +import re +import sys +import codecs +import shutil +import argparse +from textwrap import dedent + +from chardet import detect +from pysrt import SubRipFile, SubRipTime, VERSION_STRING + +def underline(string): + return "\033[4m%s\033[0m" % string + + +class TimeAwareArgumentParser(argparse.ArgumentParser): + + RE_TIME_REPRESENTATION = re.compile(r'^\-?(\d+[hms]{0,2}){1,4}$') + + def parse_args(self, args=None, namespace=None): + time_index = -1 + for index, arg in enumerate(args): + match = self.RE_TIME_REPRESENTATION.match(arg) + if match: + time_index = index + break + + if time_index >= 0: + args.insert(time_index, '--') + + return super(TimeAwareArgumentParser, self).parse_args(args, namespace) + + +class SubRipShifter(object): + + BACKUP_EXTENSION = '.bak' + RE_TIME_STRING = re.compile(r'(\d+)([hms]{0,2})') + UNIT_RATIOS = { + 'ms': 1, + '': SubRipTime.SECONDS_RATIO, + 's': SubRipTime.SECONDS_RATIO, + 'm': SubRipTime.MINUTES_RATIO, + 'h': SubRipTime.HOURS_RATIO, + } + DESCRIPTION = dedent("""\ + Srt subtitle editor + + It can either shift, split or change the frame rate. + """) + TIMESTAMP_HELP = "A timestamp in the form: [-][Hh][Mm]S[s][MSms]" + SHIFT_EPILOG = dedent("""\ + + Examples: + 1 minute and 12 seconds foreward (in place): + $ srt -i shift 1m12s movie.srt + + half a second foreward: + $ srt shift 500ms movie.srt > othername.srt + + 1 second and half backward: + $ srt -i shift -1s500ms movie.srt + + 3 seconds backward: + $ srt -i shift -3 movie.srt + """) + RATE_EPILOG = dedent("""\ + + Examples: + Convert 23.9fps subtitles to 25fps: + $ srt -i rate 23.9 25 movie.srt + """) + LIMITS_HELP = "Each parts duration in the form: [Hh][Mm]S[s][MSms]" + SPLIT_EPILOG = dedent("""\ + + Examples: + For a movie in 2 parts with the first part 48 minutes and 18 seconds long: + $ srt split 48m18s movie.srt + => creates movie.1.srt and movie.2.srt + + For a movie in 3 parts of 20 minutes each: + $ srt split 20m 20m movie.srt + => creates movie.1.srt, movie.2.srt and movie.3.srt + """) + FRAME_RATE_HELP = "A frame rate in fps (commonly 23.9 or 25)" + ENCODING_HELP = dedent("""\ + Change file encoding. Useful for players accepting only latin1 subtitles. + List of supported encodings: http://docs.python.org/library/codecs.html#standard-encodings + """) + BREAK_EPILOG = dedent("""\ + Break lines longer than defined length + """) + LENGTH_HELP = "Maximum number of characters per line" + + def __init__(self): + self.output_file_path = None + + def build_parser(self): + parser = TimeAwareArgumentParser(description=self.DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('-i', '--in-place', action='store_true', dest='in_place', + help="Edit file in-place, saving a backup as file.bak (do not works for the split command)") + parser.add_argument('-e', '--output-encoding', metavar=underline('encoding'), action='store', dest='output_encoding', + type=self.parse_encoding, help=self.ENCODING_HELP) + parser.add_argument('-v', '--version', action='version', version='%%(prog)s %s' % VERSION_STRING) + subparsers = parser.add_subparsers(title='commands') + + shift_parser = subparsers.add_parser('shift', help="Shift subtitles by specified time offset", epilog=self.SHIFT_EPILOG, formatter_class=argparse.RawTextHelpFormatter) + shift_parser.add_argument('time_offset', action='store', metavar=underline('offset'), + type=self.parse_time, help=self.TIMESTAMP_HELP) + shift_parser.set_defaults(action=self.shift) + + rate_parser = subparsers.add_parser('rate', help="Convert subtitles from a frame rate to another", epilog=self.RATE_EPILOG, formatter_class=argparse.RawTextHelpFormatter) + rate_parser.add_argument('initial', action='store', type=float, help=self.FRAME_RATE_HELP) + rate_parser.add_argument('final', action='store', type=float, help=self.FRAME_RATE_HELP) + rate_parser.set_defaults(action=self.rate) + + split_parser = subparsers.add_parser('split', help="Split a file in multiple parts", epilog=self.SPLIT_EPILOG, formatter_class=argparse.RawTextHelpFormatter) + split_parser.add_argument('limits', action='store', nargs='+', type=self.parse_time, help=self.LIMITS_HELP) + split_parser.set_defaults(action=self.split) + + break_parser = subparsers.add_parser('break', help="Break long lines", epilog=self.BREAK_EPILOG, formatter_class=argparse.RawTextHelpFormatter) + break_parser.add_argument('length', action='store', type=int, help=self.LENGTH_HELP) + break_parser.set_defaults(action=self.break_lines) + + parser.add_argument('file', action='store') + + return parser + + def run(self, args): + self.arguments = self.build_parser().parse_args(args) + if self.arguments.in_place: + self.create_backup() + self.arguments.action() + + def parse_time(self, time_string): + negative = time_string.startswith('-') + if negative: + time_string = time_string[1:] + ordinal = sum(int(value) * self.UNIT_RATIOS[unit] for value, unit + in self.RE_TIME_STRING.findall(time_string)) + return -ordinal if negative else ordinal + + def parse_encoding(self, encoding_name): + try: + codecs.lookup(encoding_name) + except LookupError as error: + raise argparse.ArgumentTypeError(error.message) + return encoding_name + + def shift(self): + self.input_file.shift(milliseconds=self.arguments.time_offset) + self.input_file.write_into(self.output_file) + + def rate(self): + ratio = self.arguments.final / self.arguments.initial + self.input_file.shift(ratio=ratio) + self.input_file.write_into(self.output_file) + + def split(self): + limits = [0] + self.arguments.limits + [self.input_file[-1].end.ordinal + 1] + base_name, extension = os.path.splitext(self.arguments.file) + for index, (start, end) in enumerate(zip(limits[:-1], limits[1:])): + file_name = '%s.%s%s' % (base_name, index + 1, extension) + part_file = self.input_file.slice(ends_after=start, starts_before=end) + part_file.shift(milliseconds=-start) + part_file.clean_indexes() + part_file.save(path=file_name, encoding=self.output_encoding) + + def create_backup(self): + backup_file = self.arguments.file + self.BACKUP_EXTENSION + if not os.path.exists(backup_file): + shutil.copy2(self.arguments.file, backup_file) + self.output_file_path = self.arguments.file + self.arguments.file = backup_file + + def break_lines(self): + split_re = re.compile(r'(.{,%i})(?:\s+|$)' % self.arguments.length) + for item in self.input_file: + item.text = '\n'.join(split_re.split(item.text)[1::2]) + self.input_file.write_into(self.output_file) + + @property + def output_encoding(self): + return self.arguments.output_encoding or self.input_file.encoding + + @property + def input_file(self): + if not hasattr(self, '_source_file'): + with open(self.arguments.file, 'rb') as f: + content = f.read() + encoding = detect(content).get('encoding') + encoding = self.normalize_encoding(encoding) + + self._source_file = SubRipFile.open(self.arguments.file, + encoding=encoding, error_handling=SubRipFile.ERROR_LOG) + return self._source_file + + @property + def output_file(self): + if not hasattr(self, '_output_file'): + if self.output_file_path: + self._output_file = codecs.open(self.output_file_path, 'w+', encoding=self.output_encoding) + else: + self._output_file = sys.stdout + return self._output_file + + def normalize_encoding(self, encoding): + return encoding.lower().replace('-', '_') + + +def main(): + SubRipShifter().run(sys.argv[1:]) + +if __name__ == '__main__': + main() diff --git a/lib/pysrt/comparablemixin.py b/lib/pysrt/comparablemixin.py new file mode 100644 index 00000000..3ae70b07 --- /dev/null +++ b/lib/pysrt/comparablemixin.py @@ -0,0 +1,26 @@ +class ComparableMixin(object): + def _compare(self, other, method): + try: + return method(self._cmpkey(), other._cmpkey()) + except (AttributeError, TypeError): + # _cmpkey not implemented, or return different type, + # so I can't compare with "other". + return NotImplemented + + def __lt__(self, other): + return self._compare(other, lambda s, o: s < o) + + def __le__(self, other): + return self._compare(other, lambda s, o: s <= o) + + def __eq__(self, other): + return self._compare(other, lambda s, o: s == o) + + def __ge__(self, other): + return self._compare(other, lambda s, o: s >= o) + + def __gt__(self, other): + return self._compare(other, lambda s, o: s > o) + + def __ne__(self, other): + return self._compare(other, lambda s, o: s != o) diff --git a/lib/pysrt/compat.py b/lib/pysrt/compat.py new file mode 100644 index 00000000..653cf320 --- /dev/null +++ b/lib/pysrt/compat.py @@ -0,0 +1,24 @@ + +import sys + +# Syntax sugar. +_ver = sys.version_info + +#: Python 2.x? +is_py2 = (_ver[0] == 2) + +#: Python 3.x? +is_py3 = (_ver[0] == 3) + +from io import open as io_open + +if is_py2: + builtin_str = str + basestring = basestring + str = unicode + open = io_open +elif is_py3: + builtin_str = str + basestring = (str, bytes) + str = str + open = open diff --git a/lib/pysrt/srtexc.py b/lib/pysrt/srtexc.py new file mode 100644 index 00000000..971b4709 --- /dev/null +++ b/lib/pysrt/srtexc.py @@ -0,0 +1,31 @@ +""" +Exception classes +""" + + +class Error(Exception): + """ + Pysrt's base exception + """ + pass + + +class InvalidTimeString(Error): + """ + Raised when parser fail on bad formated time strings + """ + pass + + +class InvalidItem(Error): + """ + Raised when parser fail to parse a sub title item + """ + pass + + +class InvalidIndex(InvalidItem): + """ + Raised when parser fail to parse a sub title index + """ + pass diff --git a/lib/pysrt/srtfile.py b/lib/pysrt/srtfile.py new file mode 100644 index 00000000..350e4b4d --- /dev/null +++ b/lib/pysrt/srtfile.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +import os +import sys +import codecs + +try: + from collections import UserList +except ImportError: + from UserList import UserList + +from itertools import chain +from copy import copy + +from pysrt.srtexc import Error +from pysrt.srtitem import SubRipItem +from pysrt.compat import str + +BOMS = ((codecs.BOM_UTF32_LE, 'utf_32_le'), + (codecs.BOM_UTF32_BE, 'utf_32_be'), + (codecs.BOM_UTF16_LE, 'utf_16_le'), + (codecs.BOM_UTF16_BE, 'utf_16_be'), + (codecs.BOM_UTF8, 'utf_8')) +CODECS_BOMS = dict((codec, str(bom, codec)) for bom, codec in BOMS) +BIGGER_BOM = max(len(bom) for bom, encoding in BOMS) + + +class SubRipFile(UserList, object): + """ + SubRip file descriptor. + + Provide a pure Python mapping on all metadata. + + SubRipFile(items, eol, path, encoding) + + items -> list of SubRipItem. Default to []. + eol -> str: end of line character. Default to linesep used in opened file + if any else to os.linesep. + path -> str: path where file will be saved. To open an existant file see + SubRipFile.open. + encoding -> str: encoding used at file save. Default to utf-8. + """ + ERROR_PASS = 0 + ERROR_LOG = 1 + ERROR_RAISE = 2 + + DEFAULT_ENCODING = 'utf_8' + + def __init__(self, items=None, eol=None, path=None, encoding='utf-8'): + UserList.__init__(self, items or []) + self._eol = eol + self.path = path + self.encoding = encoding + + def _get_eol(self): + return self._eol or os.linesep + + def _set_eol(self, eol): + self._eol = self._eol or eol + + eol = property(_get_eol, _set_eol) + + def slice(self, starts_before=None, starts_after=None, ends_before=None, + ends_after=None): + """ + slice([starts_before][, starts_after][, ends_before][, ends_after]) \ +-> SubRipFile clone + + All arguments are optional, and should be coercible to SubRipTime + object. + + It reduce the set of subtitles to those that match match given time + constraints. + + The returned set is a clone, but still contains references to original + subtitles. So if you shift this returned set, subs contained in the + original SubRipFile instance will be altered too. + + Example: + >>> subs.slice(ends_after={'seconds': 20}).shift(seconds=2) + """ + clone = copy(self) + + if starts_before: + clone.data = (i for i in clone.data if i.start < starts_before) + if starts_after: + clone.data = (i for i in clone.data if i.start > starts_after) + if ends_before: + clone.data = (i for i in clone.data if i.end < ends_before) + if ends_after: + clone.data = (i for i in clone.data if i.end > ends_after) + + clone.data = list(clone.data) + return clone + + def at(self, timestamp=None, **kwargs): + """ + at(timestamp) -> SubRipFile clone + + timestamp argument should be coercible to SubRipFile object. + + A specialization of slice. Return all subtiles visible at the + timestamp mark. + + Example: + >>> subs.at((0, 0, 20, 0)).shift(seconds=2) + >>> subs.at(seconds=20).shift(seconds=2) + """ + time = timestamp or kwargs + return self.slice(starts_before=time, ends_after=time) + + def shift(self, *args, **kwargs): + """shift(hours, minutes, seconds, milliseconds, ratio) + + Shift `start` and `end` attributes of each items of file either by + applying a ratio or by adding an offset. + + `ratio` should be either an int or a float. + Example to convert subtitles from 23.9 fps to 25 fps: + >>> subs.shift(ratio=25/23.9) + + All "time" arguments are optional and have a default value of 0. + Example to delay all subs from 2 seconds and half + >>> subs.shift(seconds=2, milliseconds=500) + """ + for item in self: + item.shift(*args, **kwargs) + + def clean_indexes(self): + """ + clean_indexes() + + Sort subs and reset their index attribute. Should be called after + destructive operations like split or such. + """ + self.sort() + for index, item in enumerate(self): + item.index = index + 1 + + @property + def text(self): + return '\n'.join(i.text for i in self) + + @classmethod + def open(cls, path='', encoding=None, error_handling=ERROR_PASS): + """ + open([path, [encoding]]) + + If you do not provide any encoding, it can be detected if the file + contain a bit order mark, unless it is set to utf-8 as default. + """ + new_file = cls(path=path, encoding=encoding) + source_file = cls._open_unicode_file(path, claimed_encoding=encoding) + new_file.read(source_file, error_handling=error_handling) + source_file.close() + return new_file + + @classmethod + def from_string(cls, source, **kwargs): + """ + from_string(source, **kwargs) -> SubRipFile + + `source` -> a unicode instance or at least a str instance encoded with + `sys.getdefaultencoding()` + """ + error_handling = kwargs.pop('error_handling', None) + new_file = cls(**kwargs) + new_file.read(source.splitlines(True), error_handling=error_handling) + return new_file + + def read(self, source_file, error_handling=ERROR_PASS): + """ + read(source_file, [error_handling]) + + This method parse subtitles contained in `source_file` and append them + to the current instance. + + `source_file` -> Any iterable that yield unicode strings, like a file + opened with `codecs.open()` or an array of unicode. + """ + self.eol = self._guess_eol(source_file) + self.extend(self.stream(source_file, error_handling=error_handling)) + return self + + @classmethod + def stream(cls, source_file, error_handling=ERROR_PASS): + """ + stream(source_file, [error_handling]) + + This method yield SubRipItem instances a soon as they have been parsed + without storing them. It is a kind of SAX parser for .srt files. + + `source_file` -> Any iterable that yield unicode strings, like a file + opened with `codecs.open()` or an array of unicode. + + Example: + >>> import pysrt + >>> import codecs + >>> file = codecs.open('movie.srt', encoding='utf-8') + >>> for sub in pysrt.stream(file): + ... sub.text += "\nHello !" + ... print unicode(sub) + """ + string_buffer = [] + for index, line in enumerate(chain(source_file, '\n')): + if line.strip(): + string_buffer.append(line) + else: + source = string_buffer + string_buffer = [] + if source and all(source): + try: + yield SubRipItem.from_lines(source) + except Error as error: + error.args += (''.join(source), ) + cls._handle_error(error, error_handling, index) + + def save(self, path=None, encoding=None, eol=None): + """ + save([path][, encoding][, eol]) + + Use initial path if no other provided. + Use initial encoding if no other provided. + Use initial eol if no other provided. + """ + path = path or self.path + encoding = encoding or self.encoding + + save_file = codecs.open(path, 'w+', encoding=encoding) + self.write_into(save_file, eol=eol) + save_file.close() + + def write_into(self, output_file, eol=None): + """ + write_into(output_file [, eol]) + + Serialize current state into `output_file`. + + `output_file` -> Any instance that respond to `write()`, typically a + file object + """ + output_eol = eol or self.eol + + for item in self: + string_repr = str(item) + if output_eol != '\n': + string_repr = string_repr.replace('\n', output_eol) + output_file.write(string_repr) + # Only add trailing eol if it's not already present. + # It was kept in the SubRipItem's text before but it really + # belongs here. Existing applications might give us subtitles + # which already contain a trailing eol though. + if not string_repr.endswith(2 * output_eol): + output_file.write(output_eol) + + @classmethod + def _guess_eol(cls, string_iterable): + first_line = cls._get_first_line(string_iterable) + for eol in ('\r\n', '\r', '\n'): + if first_line.endswith(eol): + return eol + return os.linesep + + @classmethod + def _get_first_line(cls, string_iterable): + if hasattr(string_iterable, 'tell'): + previous_position = string_iterable.tell() + + try: + first_line = next(iter(string_iterable)) + except StopIteration: + return '' + if hasattr(string_iterable, 'seek'): + string_iterable.seek(previous_position) + + return first_line + + @classmethod + def _detect_encoding(cls, path): + file_descriptor = open(path, 'rb') + first_chars = file_descriptor.read(BIGGER_BOM) + file_descriptor.close() + + for bom, encoding in BOMS: + if first_chars.startswith(bom): + return encoding + + # TODO: maybe a chardet integration + return cls.DEFAULT_ENCODING + + @classmethod + def _open_unicode_file(cls, path, claimed_encoding=None): + encoding = claimed_encoding or cls._detect_encoding(path) + source_file = codecs.open(path, 'rU', encoding=encoding) + + # get rid of BOM if any + possible_bom = CODECS_BOMS.get(encoding, None) + if possible_bom: + file_bom = source_file.read(len(possible_bom)) + if not file_bom == possible_bom: + source_file.seek(0) # if not rewind + return source_file + + @classmethod + def _handle_error(cls, error, error_handling, index): + if error_handling == cls.ERROR_RAISE: + error.args = (index, ) + error.args + raise error + if error_handling == cls.ERROR_LOG: + name = type(error).__name__ + sys.stderr.write('PySRT-%s(line %s): \n' % (name, index)) + sys.stderr.write(error.args[0].encode('ascii', 'replace')) + sys.stderr.write('\n') diff --git a/lib/pysrt/srtitem.py b/lib/pysrt/srtitem.py new file mode 100644 index 00000000..4101716b --- /dev/null +++ b/lib/pysrt/srtitem.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +""" +SubRip's subtitle parser +""" +from pysrt.srtexc import InvalidItem, InvalidIndex +from pysrt.srttime import SubRipTime +from pysrt.comparablemixin import ComparableMixin +from pysrt.compat import str + +class SubRipItem(ComparableMixin): + """ + SubRipItem(index, start, end, text, position) + + index -> int: index of item in file. 0 by default. + start, end -> SubRipTime or coercible. + text -> unicode: text content for item. + position -> unicode: raw srt/vtt "display coordinates" string + """ + ITEM_PATTERN = '%s\n%s --> %s%s\n%s\n' + TIMESTAMP_SEPARATOR = '-->' + + def __init__(self, index=0, start=None, end=None, text='', position=''): + try: + self.index = int(index) + except (TypeError, ValueError): # try to cast as int, but it's not mandatory + self.index = index + + self.start = SubRipTime.coerce(start or 0) + self.end = SubRipTime.coerce(end or 0) + self.position = str(position) + self.text = str(text) + + def __str__(self): + position = ' %s' % self.position if self.position.strip() else '' + return self.ITEM_PATTERN % (self.index, self.start, self.end, + position, self.text) + + def _cmpkey(self): + return (self.start, self.end) + + def shift(self, *args, **kwargs): + """ + shift(hours, minutes, seconds, milliseconds, ratio) + + Add given values to start and end attributes. + All arguments are optional and have a default value of 0. + """ + self.start.shift(*args, **kwargs) + self.end.shift(*args, **kwargs) + + @classmethod + def from_string(cls, source): + return cls.from_lines(source.splitlines(True)) + + @classmethod + def from_lines(cls, lines): + if len(lines) < 2: + raise InvalidItem() + lines = [l.rstrip() for l in lines] + index = None + if cls.TIMESTAMP_SEPARATOR not in lines[0]: + index = lines.pop(0) + start, end, position = cls.split_timestamps(lines[0]) + body = '\n'.join(lines[1:]) + return cls(index, start, end, body, position) + + @classmethod + def split_timestamps(cls, line): + timestamps = line.split(cls.TIMESTAMP_SEPARATOR) + if len(timestamps) != 2: + raise InvalidItem() + start, end_and_position = timestamps + end_and_position = end_and_position.lstrip().split(' ', 1) + end = end_and_position[0] + position = end_and_position[1] if len(end_and_position) > 1 else '' + return (s.strip() for s in (start, end, position)) diff --git a/lib/pysrt/srttime.py b/lib/pysrt/srttime.py new file mode 100644 index 00000000..95c578f8 --- /dev/null +++ b/lib/pysrt/srttime.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +""" +SubRip's time format parser: HH:MM:SS,mmm +""" +import re +from datetime import time + +from pysrt.srtexc import InvalidTimeString +from pysrt.comparablemixin import ComparableMixin +from pysrt.compat import str, basestring + +class TimeItemDescriptor(object): + # pylint: disable-msg=R0903 + def __init__(self, ratio, super_ratio=0): + self.ratio = int(ratio) + self.super_ratio = int(super_ratio) + + def _get_ordinal(self, instance): + if self.super_ratio: + return instance.ordinal % self.super_ratio + return instance.ordinal + + def __get__(self, instance, klass): + if instance is None: + raise AttributeError + return self._get_ordinal(instance) // self.ratio + + def __set__(self, instance, value): + part = self._get_ordinal(instance) - instance.ordinal % self.ratio + instance.ordinal += value * self.ratio - part + + +class SubRipTime(ComparableMixin): + TIME_PATTERN = '%02d:%02d:%02d,%03d' + TIME_REPR = 'SubRipTime(%d, %d, %d, %d)' + RE_TIME_SEP = re.compile(r'\:|\.|\,') + RE_INTEGER = re.compile(r'^(\d+)') + SECONDS_RATIO = 1000 + MINUTES_RATIO = SECONDS_RATIO * 60 + HOURS_RATIO = MINUTES_RATIO * 60 + + hours = TimeItemDescriptor(HOURS_RATIO) + minutes = TimeItemDescriptor(MINUTES_RATIO, HOURS_RATIO) + seconds = TimeItemDescriptor(SECONDS_RATIO, MINUTES_RATIO) + milliseconds = TimeItemDescriptor(1, SECONDS_RATIO) + + def __init__(self, hours=0, minutes=0, seconds=0, milliseconds=0): + """ + SubRipTime(hours, minutes, seconds, milliseconds) + + All arguments are optional and have a default value of 0. + """ + super(SubRipTime, self).__init__() + self.ordinal = hours * self.HOURS_RATIO \ + + minutes * self.MINUTES_RATIO \ + + seconds * self.SECONDS_RATIO \ + + milliseconds + + def __repr__(self): + return self.TIME_REPR % tuple(self) + + def __str__(self): + if self.ordinal < 0: + # Represent negative times as zero + return str(SubRipTime.from_ordinal(0)) + return self.TIME_PATTERN % tuple(self) + + def _compare(self, other, method): + return super(SubRipTime, self)._compare(self.coerce(other), method) + + def _cmpkey(self): + return self.ordinal + + def __add__(self, other): + return self.from_ordinal(self.ordinal + self.coerce(other).ordinal) + + def __iadd__(self, other): + self.ordinal += self.coerce(other).ordinal + return self + + def __sub__(self, other): + return self.from_ordinal(self.ordinal - self.coerce(other).ordinal) + + def __isub__(self, other): + self.ordinal -= self.coerce(other).ordinal + return self + + def __mul__(self, ratio): + return self.from_ordinal(int(round(self.ordinal * ratio))) + + def __imul__(self, ratio): + self.ordinal = int(round(self.ordinal * ratio)) + return self + + @classmethod + def coerce(cls, other): + """ + Coerce many types to SubRipTime instance. + Supported types: + - str/unicode + - int/long + - datetime.time + - any iterable + - dict + """ + if isinstance(other, SubRipTime): + return other + if isinstance(other, basestring): + return cls.from_string(other) + if isinstance(other, int): + return cls.from_ordinal(other) + if isinstance(other, time): + return cls.from_time(other) + try: + return cls(**other) + except TypeError: + return cls(*other) + + def __iter__(self): + yield self.hours + yield self.minutes + yield self.seconds + yield self.milliseconds + + def shift(self, *args, **kwargs): + """ + shift(hours, minutes, seconds, milliseconds) + + All arguments are optional and have a default value of 0. + """ + if 'ratio' in kwargs: + self *= kwargs.pop('ratio') + self += self.__class__(*args, **kwargs) + + @classmethod + def from_ordinal(cls, ordinal): + """ + int -> SubRipTime corresponding to a total count of milliseconds + """ + return cls(milliseconds=int(ordinal)) + + @classmethod + def from_string(cls, source): + """ + str/unicode(HH:MM:SS,mmm) -> SubRipTime corresponding to serial + raise InvalidTimeString + """ + items = cls.RE_TIME_SEP.split(source) + if len(items) != 4: + raise InvalidTimeString + return cls(*(cls.parse_int(i) for i in items)) + + @classmethod + def parse_int(cls, digits): + try: + return int(digits) + except ValueError: + match = cls.RE_INTEGER.match(digits) + if match: + return int(match.group()) + return 0 + + @classmethod + def from_time(cls, source): + """ + datetime.time -> SubRipTime corresponding to time object + """ + return cls(hours=source.hour, minutes=source.minute, + seconds=source.second, milliseconds=source.microsecond // 1000) + + def to_time(self): + """ + Convert SubRipTime instance into a pure datetime.time object + """ + return time(self.hours, self.minutes, self.seconds, + self.milliseconds * 1000) diff --git a/lib/pysrt/version.py b/lib/pysrt/version.py new file mode 100644 index 00000000..f04e34e8 --- /dev/null +++ b/lib/pysrt/version.py @@ -0,0 +1,2 @@ +VERSION = (1, 0, 1) +VERSION_STRING = '.'.join(str(i) for i in VERSION) diff --git a/lib/subliminal/__init__.py b/lib/subliminal/__init__.py index 77297f5a..666440b6 100644 --- a/lib/subliminal/__init__.py +++ b/lib/subliminal/__init__.py @@ -31,4 +31,4 @@ except ImportError: __all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE', 'MATCHING_CONFIDENCE', 'list_subtitles', 'download_subtitles', 'Pool'] -logging.getLogger("subliminal").addHandler(NullHandler()) +logging.getLogger(__name__).addHandler(NullHandler()) diff --git a/lib/subliminal/api.py b/lib/subliminal/api.py index f95fda3b..51416f8c 100644 --- a/lib/subliminal/api.py +++ b/lib/subliminal/api.py @@ -23,7 +23,7 @@ import logging __all__ = ['list_subtitles', 'download_subtitles'] -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) def list_subtitles(paths, languages=None, services=None, force=True, multi=False, cache_dir=None, max_depth=3, scan_filter=None): @@ -94,10 +94,7 @@ def download_subtitles(paths, languages=None, services=None, force=True, multi=F order = order or [LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE] subtitles_by_video = list_subtitles(paths, languages, services, force, multi, cache_dir, max_depth, scan_filter) for video, subtitles in subtitles_by_video.iteritems(): - try: - subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True) - except StopIteration: - break + subtitles.sort(key=lambda s: key_subtitles(s, video, languages, services, order), reverse=True) results = [] service_instances = {} tasks = create_download_tasks(subtitles_by_video, languages, multi) diff --git a/lib/subliminal/async.py b/lib/subliminal/async.py index ff42764b..6a69b766 100644 --- a/lib/subliminal/async.py +++ b/lib/subliminal/async.py @@ -26,7 +26,7 @@ import threading __all__ = ['Worker', 'Pool'] -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class Worker(threading.Thread): diff --git a/lib/subliminal/cache.py b/lib/subliminal/cache.py index 31275e00..9add007a 100644 --- a/lib/subliminal/cache.py +++ b/lib/subliminal/cache.py @@ -27,7 +27,7 @@ except ImportError: __all__ = ['Cache', 'cachedmethod'] -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class Cache(object): diff --git a/lib/subliminal/core.py b/lib/subliminal/core.py index 9c3fa3dc..537fa655 100644 --- a/lib/subliminal/core.py +++ b/lib/subliminal/core.py @@ -31,8 +31,8 @@ import logging __all__ = ['SERVICES', 'LANGUAGE_INDEX', 'SERVICE_INDEX', 'SERVICE_CONFIDENCE', 'MATCHING_CONFIDENCE', 'create_list_tasks', 'create_download_tasks', 'consume_task', 'matching_confidence', 'key_subtitles', 'group_by_video'] -logger = logging.getLogger("subliminal") -SERVICES = ['opensubtitles', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles', 'itasa', 'usub'] +logger = logging.getLogger(__name__) +SERVICES = ['opensubtitles', 'bierdopje', 'subswiki', 'subtitulos', 'thesubdb', 'addic7ed', 'tvsubtitles'] LANGUAGE_INDEX, SERVICE_INDEX, SERVICE_CONFIDENCE, MATCHING_CONFIDENCE = range(4) diff --git a/lib/subliminal/infos.py b/lib/subliminal/infos.py index 5ab2084a..220ea859 100644 --- a/lib/subliminal/infos.py +++ b/lib/subliminal/infos.py @@ -15,4 +15,4 @@ # # You should have received a copy of the GNU Lesser General Public License # along with subliminal. If not, see . -__version__ = '0.6.3' +__version__ = '0.6.2' diff --git a/lib/subliminal/language.py b/lib/subliminal/language.py index 6403bcc0..efc75bb4 100644 --- a/lib/subliminal/language.py +++ b/lib/subliminal/language.py @@ -20,7 +20,7 @@ import re import logging -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) COUNTRIES = [('AF', 'AFG', '004', u'Afghanistan'), @@ -619,7 +619,6 @@ LANGUAGES = [('aar', '', 'aa', u'Afar', u'afar'), ('pli', '', 'pi', u'Pali', u'pali'), ('pol', '', 'pl', u'Polish', u'polonais'), ('pon', '', '', u'Pohnpeian', u'pohnpei'), - ('pob', '', 'pb', u'Brazilian Portuguese', u'brazilian portuguese'), ('por', '', 'pt', u'Portuguese', u'portugais'), ('pra', '', '', u'Prakrit languages', u'prâkrit, langues'), ('pro', '', '', u'Provençal, Old (to 1500)', u'provençal ancien (jusqu\'à 1500)'), diff --git a/lib/subliminal/services/__init__.py b/lib/subliminal/services/__init__.py index c52dd5dc..b82b309c 100644 --- a/lib/subliminal/services/__init__.py +++ b/lib/subliminal/services/__init__.py @@ -27,7 +27,7 @@ import zipfile __all__ = ['ServiceBase', 'ServiceConfig'] -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class ServiceBase(object): @@ -82,7 +82,7 @@ class ServiceBase(object): """Initialize connection""" logger.debug(u'Initializing %s' % self.__class__.__name__) self.session = requests.session() - self.session.headers.update({'User-Agent': self.user_agent}) + self.session.headers.update({'User-Agent': self.user_agent}) def init_cache(self): """Initialize cache, make sure it is loaded from disk""" @@ -220,16 +220,14 @@ class ServiceBase(object): # TODO: could check if maybe we already have a text file and # download it directly raise DownloadFailedError('Downloaded file is not a zip file') - zipsub = zipfile.ZipFile(zippath) - for subfile in zipsub.namelist(): - if os.path.splitext(subfile)[1] in EXTENSIONS: - with open(filepath, 'wb') as f: - f.write(zipsub.open(subfile).read()) - break - else: - zipsub.close() - raise DownloadFailedError('No subtitles found in zip file') - zipsub.close() + with zipfile.ZipFile(zippath) as zipsub: + for subfile in zipsub.namelist(): + if os.path.splitext(subfile)[1] in EXTENSIONS: + with open(filepath, 'w') as f: + f.write(zipsub.open(subfile).read()) + break + else: + raise DownloadFailedError('No subtitles found in zip file') os.remove(zippath) except Exception as e: logger.error(u'Download %s failed: %s' % (url, e)) diff --git a/lib/subliminal/services/addic7ed.py b/lib/subliminal/services/addic7ed.py index 665c5708..492fd744 100644 --- a/lib/subliminal/services/addic7ed.py +++ b/lib/subliminal/services/addic7ed.py @@ -29,17 +29,16 @@ import os import re -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class Addic7ed(ServiceBase): server_url = 'http://www.addic7ed.com' - site_url = 'http://www.addic7ed.com' api_based = False #TODO: Complete this languages = language_set(['ar', 'ca', 'de', 'el', 'en', 'es', 'eu', 'fr', 'ga', 'gl', 'he', 'hr', 'hu', - 'it', 'pl', 'pt', 'ro', 'ru', 'se', 'pb']) - language_map = {'Portuguese (Brazilian)': Language('pob'), 'Greek': Language('gre'), + 'it', 'pl', 'pt', 'ro', 'ru', 'se', 'pt-br']) + language_map = {'Portuguese (Brazilian)': Language('por-BR'), 'Greek': Language('gre'), 'Spanish (Latin America)': Language('spa'), 'Galego': Language('glg'), u'Català': Language('cat')} videos = [Episode] @@ -64,7 +63,6 @@ class Addic7ed(ServiceBase): return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode) def query(self, filepath, languages, keywords, series, season, episode): - logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages)) self.init_cache() try: @@ -92,7 +90,7 @@ class Addic7ed(ServiceBase): continue sub_keywords = split_keyword(cells[4].text.strip().lower()) #TODO: Maybe allow empty keywords here? (same in Subtitulos) - if keywords and not keywords & sub_keywords: + if not keywords & sub_keywords: logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords)) continue sub_link = '%s/%s' % (self.server_url, cells[9].a['href']) diff --git a/lib/subliminal/services/bierdopje.py b/lib/subliminal/services/bierdopje.py index 8642afb8..03577cd8 100644 --- a/lib/subliminal/services/bierdopje.py +++ b/lib/subliminal/services/bierdopje.py @@ -31,12 +31,11 @@ except ImportError: import pickle -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class BierDopje(ServiceBase): server_url = 'http://api.bierdopje.com/A2B638AC5D804C2E/' - site_url = 'http://www.bierdopje.com' user_agent = 'Subliminal/0.6' api_based = True languages = language_set(['eng', 'dut']) diff --git a/lib/subliminal/services/itasa.py b/lib/subliminal/services/itasa.py deleted file mode 100644 index f726a156..00000000 --- a/lib/subliminal/services/itasa.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2012 Mr_Orange -# -# This file is part of subliminal. -# -# subliminal is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# subliminal is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with subliminal. If not, see . -from . import ServiceBase -from ..exceptions import DownloadFailedError, ServiceError -from ..cache import cachedmethod -from ..language import language_set, Language -from ..subtitles import get_subtitle_path, ResultSubtitle, EXTENSIONS -from ..utils import get_keywords -from ..videos import Episode -from bs4 import BeautifulSoup -import logging -import re -import os -import requests -import zipfile -import StringIO -import guessit - -from sickbeard.common import Quality - -logger = logging.getLogger("subliminal") - - -class Itasa(ServiceBase): - server_url = 'http://www.italiansubs.net/' - site_url = 'http://www.italiansubs.net/' - api_based = False - languages = language_set(['it']) - videos = [Episode] - require_video = False - required_features = ['permissive'] - quality_dict = {Quality.SDTV : '', - Quality.SDDVD : 'dvdrip', - Quality.RAWHDTV : '1080i', - Quality.HDTV : '720p', - Quality.FULLHDTV : ('1080p','720p'), - Quality.HDWEBDL : 'web-dl', - Quality.FULLHDWEBDL : 'web-dl', - Quality.HDBLURAY : ('bdrip', 'bluray'), - Quality.FULLHDBLURAY : ('bdrip', 'bluray'), - Quality.UNKNOWN : 'unknown' #Any subtitle will be downloaded - } - - def init(self): - - super(Itasa, self).init() - login_pattern = '' - - response = requests.get(self.server_url + 'index.php') - if response.status_code != 200: - raise ServiceError('Initiate failed') - - match = re.search(login_pattern, response.content, re.IGNORECASE | re.DOTALL) - if not match: - raise ServiceError('Can not find unique id parameter on page') - - login_parameter = {'username': 'sickbeard', - 'passwd': 'subliminal', - 'remember': 'yes', - 'Submit': 'Login', - 'remember': 'yes', - 'option': 'com_user', - 'task': 'login', - 'silent': 'true', - 'return': match.group(1), - match.group(2): match.group(3) - } - - self.session = requests.session() - r = self.session.post(self.server_url + 'index.php', data=login_parameter) - if not re.search('logouticon.png', r.content, re.IGNORECASE | re.DOTALL): - raise ServiceError('Itasa Login Failed') - - @cachedmethod - def get_series_id(self, name): - """Get the show page and cache every show found in it""" - r = self.session.get(self.server_url + 'index.php?option=com_remository&Itemid=9') - soup = BeautifulSoup(r.content, self.required_features) - all_series = soup.find('div', attrs = {'id' : 'remositorycontainerlist'}) - for tv_series in all_series.find_all(href=re.compile('func=select')): - series_name = tv_series.text.lower().strip().replace(':','') - match = re.search('&id=([0-9]+)', tv_series['href']) - if match is None: - continue - series_id = int(match.group(1)) - self.cache_for(self.get_series_id, args=(series_name,), result=series_id) - return self.cached_value(self.get_series_id, args=(name,)) - - def get_episode_id(self, series, series_id, season, episode, quality): - """Get the id subtitle for episode with the given quality""" - - season_link = None - quality_link = None - episode_id = None - - r = self.session.get(self.server_url + 'index.php?option=com_remository&Itemid=6&func=select&id=' + str(series_id)) - soup = BeautifulSoup(r.content, self.required_features) - all_seasons = soup.find('div', attrs = {'id' : 'remositorycontainerlist'}) - for seasons in all_seasons.find_all(href=re.compile('func=select')): - if seasons.text.lower().strip() == 'stagione %s' % str(season): - season_link = seasons['href'] - break - - if not season_link: - logger.debug(u'Could not find season %s for series %s' % (series, str(season))) - return None - - r = self.session.get(season_link) - soup = BeautifulSoup(r.content, self.required_features) - - all_qualities = soup.find('div', attrs = {'id' : 'remositorycontainerlist'}) - for qualities in all_qualities.find_all(href=re.compile('func=select')): - if qualities.text.lower().strip() in self.quality_dict[quality]: - quality_link = qualities['href'] - r = self.session.get(qualities['href']) - soup = BeautifulSoup(r.content, self.required_features) - break - - #If we want SDTV we are just on the right page so quality link will be None - if not quality == Quality.SDTV and not quality_link: - logger.debug(u'Could not find a subtitle with required quality for series %s season %s' % (series, str(season))) - return None - - all_episodes = soup.find('div', attrs = {'id' : 'remositoryfilelisting'}) - for episodes in all_episodes.find_all(href=re.compile('func=fileinfo')): - ep_string = "%(seasonnumber)dx%(episodenumber)02d" % {'seasonnumber': season, 'episodenumber': episode} - if re.search(ep_string, episodes.text, re.I) or re.search('completa$', episodes.text, re.I): - match = re.search('&id=([0-9]+)', episodes['href']) - if match: - episode_id = match.group(1) - return episode_id - - return episode_id - - def list_checked(self, video, languages): - return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode) - - def query(self, filepath, languages, keywords, series, season, episode): - - logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages)) - self.init_cache() - try: - series = series.lower().replace('(','').replace(')','') - series_id = self.get_series_id(series) - except KeyError: - logger.debug(u'Could not find series id for %s' % series) - return [] - - episode_id = self.get_episode_id(series, series_id, season, episode, Quality.nameQuality(filepath)) - if not episode_id: - logger.debug(u'Could not find subtitle for series %s' % series) - return [] - - r = self.session.get(self.server_url + 'index.php?option=com_remository&Itemid=6&func=fileinfo&id=' + episode_id) - soup = BeautifulSoup(r.content) - - sub_link = soup.find('div', attrs = {'id' : 'remositoryfileinfo'}).find(href=re.compile('func=download'))['href'] - sub_language = self.get_language('it') - path = get_subtitle_path(filepath, sub_language, self.config.multi) - subtitle = ResultSubtitle(path, sub_language, self.__class__.__name__.lower(), sub_link) - - return [subtitle] - - def download(self, subtitle): - - logger.info(u'Downloading %s in %s' % (subtitle.link, subtitle.path)) - try: - r = self.session.get(subtitle.link, headers={'Referer': self.server_url, 'User-Agent': self.user_agent}) - zipcontent = StringIO.StringIO(r.content) - zipsub = zipfile.ZipFile(zipcontent) - -# if not zipsub.is_zipfile(zipcontent): -# raise DownloadFailedError('Downloaded file is not a zip file') - - subfile = '' - if len(zipsub.namelist()) == 1: - subfile = zipsub.namelist()[0] - else: - #Season Zip Retrive Season and episode Numbers from path - guess = guessit.guess_file_info(subtitle.path, 'episode') - ep_string = "s%(seasonnumber)02de%(episodenumber)02d" % {'seasonnumber': guess['season'], 'episodenumber': guess['episodeNumber']} - for file in zipsub.namelist(): - if re.search(ep_string, file, re.I): - subfile = file - break - if os.path.splitext(subfile)[1] in EXTENSIONS: - with open(subtitle.path, 'wb') as f: - f.write(zipsub.open(subfile).read()) - else: - zipsub.close() - raise DownloadFailedError('No subtitles found in zip file') - - zipsub.close() - except Exception as e: - if os.path.exists(subtitle.path): - os.remove(subtitle.path) - raise DownloadFailedError(str(e)) - - logger.debug(u'Download finished') - -Service = Itasa \ No newline at end of file diff --git a/lib/subliminal/services/opensubtitles.py b/lib/subliminal/services/opensubtitles.py index 65599d24..1cee25ba 100644 --- a/lib/subliminal/services/opensubtitles.py +++ b/lib/subliminal/services/opensubtitles.py @@ -27,12 +27,11 @@ import os.path import xmlrpclib -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class OpenSubtitles(ServiceBase): server_url = 'http://api.opensubtitles.org/xml-rpc' - site_url = 'http://www.opensubtitles.org' api_based = True # Source: http://www.opensubtitles.org/addons/export_languages.php languages = language_set(['aar', 'abk', 'ace', 'ach', 'ada', 'ady', 'afa', 'afh', 'afr', 'ain', 'aka', 'akk', @@ -74,9 +73,9 @@ class OpenSubtitles(ServiceBase): 'twi', 'tyv', 'udm', 'uga', 'uig', 'ukr', 'umb', 'urd', 'uzb', 'vai', 'ven', 'vie', 'vol', 'vot', 'wak', 'wal', 'war', 'was', 'wel', 'wen', 'wln', 'wol', 'xal', 'xho', 'yao', 'yap', 'yid', 'yor', 'ypk', 'zap', 'zen', 'zha', 'znd', 'zul', 'zun', - 'pob', 'rum-MD']) - language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), - Language('rum-MD'): 'mol', Language('srp'): 'scc'} + 'por-BR', 'rum-MD']) + language_map = {'mol': Language('rum-MD'), 'scc': Language('srp'), 'pob': Language('por-BR'), + Language('rum-MD'): 'mol', Language('srp'): 'scc', Language('por-BR'): 'pob'} language_code = 'alpha3' videos = [Episode, Movie] require_video = False diff --git a/lib/subliminal/services/podnapisi.py b/lib/subliminal/services/podnapisi.py index be02dd51..618c0e77 100644 --- a/lib/subliminal/services/podnapisi.py +++ b/lib/subliminal/services/podnapisi.py @@ -26,21 +26,20 @@ import logging import xmlrpclib -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class Podnapisi(ServiceBase): server_url = 'http://ssp.podnapisi.net:8000' - site_url = 'http://www.podnapisi.net' api_based = True languages = language_set(['ar', 'be', 'bg', 'bs', 'ca', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'et', 'fa', 'fi', 'fr', 'ga', 'he', 'hi', 'hr', 'hu', 'id', 'is', 'it', 'ja', 'ko', 'lt', 'lv', 'mk', 'ms', 'nl', 'nn', 'pl', 'pt', 'ro', 'ru', 'sk', 'sl', 'sq', 'sr', 'sv', 'th', 'tr', 'uk', - 'vi', 'zh', 'es-ar', 'pb']) + 'vi', 'zh', 'es-ar', 'pt-br']) language_map = {'jp': Language('jpn'), Language('jpn'): 'jp', 'gr': Language('gre'), Language('gre'): 'gr', -# 'pb': Language('por-BR'), Language('por-BR'): 'pb', + 'pb': Language('por-BR'), Language('por-BR'): 'pb', 'ag': Language('spa-AR'), Language('spa-AR'): 'ag', 'cyr': Language('srp')} videos = [Episode, Movie] diff --git a/lib/subliminal/services/podnapisiweb.py b/lib/subliminal/services/podnapisiweb.py deleted file mode 100644 index 57397b75..00000000 --- a/lib/subliminal/services/podnapisiweb.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2011-2012 Antoine Bertin -# -# This file is part of subliminal. -# -# subliminal is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# subliminal is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with subliminal. If not, see . -from . import ServiceBase -from ..exceptions import DownloadFailedError -from ..language import Language, language_set -from ..subtitles import ResultSubtitle -from ..utils import get_keywords -from ..videos import Episode, Movie -from bs4 import BeautifulSoup -import guessit -import logging -import re -from subliminal.subtitles import get_subtitle_path - - -logger = logging.getLogger("subliminal") - - -class PodnapisiWeb(ServiceBase): - server_url = 'http://simple.podnapisi.net' - site_url = 'http://www.podnapisi.net' - api_based = True - user_agent = 'Subliminal/0.6' - videos = [Episode, Movie] - require_video = False - required_features = ['xml'] - languages = language_set(['Albanian', 'Arabic', 'Spanish (Argentina)', 'Belarusian', 'Bosnian', 'Portuguese (Brazil)', 'Bulgarian', 'Catalan', - 'Chinese', 'Croatian', 'Czech', 'Danish', 'Dutch', 'English', 'Estonian', 'Persian', - 'Finnish', 'French', 'German', 'gre', 'Kalaallisut', 'Hebrew', 'Hindi', 'Hungarian', - 'Icelandic', 'Indonesian', 'Irish', 'Italian', 'Japanese', 'Kazakh', 'Korean', 'Latvian', - 'Lithuanian', 'Macedonian', 'Malay', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', - 'Russian', 'Serbian', 'Sinhala', 'Slovak', 'Slovenian', 'Spanish', 'Swedish', 'Thai', - 'Turkish', 'Ukrainian', 'Vietnamese']) - language_map = {Language('Albanian'): 29, Language('Arabic'): 12, Language('Spanish (Argentina)'): 14, Language('Belarusian'): 50, - Language('Bosnian'): 10, Language('Portuguese (Brazil)'): 48, Language('Bulgarian'): 33, Language('Catalan'): 53, - Language('Chinese'): 17, Language('Croatian'): 38, Language('Czech'): 7, Language('Danish'): 24, - Language('Dutch'): 23, Language('English'): 2, Language('Estonian'): 20, Language('Persian'): 52, - Language('Finnish'): 31, Language('French'): 8, Language('German'): 5, Language('gre'): 16, - Language('Kalaallisut'): 57, Language('Hebrew'): 22, Language('Hindi'): 42, Language('Hungarian'): 15, - Language('Icelandic'): 6, Language('Indonesian'): 54, Language('Irish'): 49, Language('Italian'): 9, - Language('Japanese'): 11, Language('Kazakh'): 58, Language('Korean'): 4, Language('Latvian'): 21, - Language('Lithuanian'): 19, Language('Macedonian'): 35, Language('Malay'): 55, - Language('Norwegian'): 3, Language('Polish'): 26, Language('Portuguese'): 32, Language('Romanian'): 13, - Language('Russian'): 27, Language('Serbian'): 36, Language('Sinhala'): 56, Language('Slovak'): 37, - Language('Slovenian'): 1, Language('Spanish'): 28, Language('Swedish'): 25, Language('Thai'): 44, - Language('Turkish'): 30, Language('Ukrainian'): 46, Language('Vietnamese'): 51, - 29: Language('Albanian'), 12: Language('Arabic'), 14: Language('Spanish (Argentina)'), 50: Language('Belarusian'), - 10: Language('Bosnian'), 48: Language('Portuguese (Brazil)'), 33: Language('Bulgarian'), 53: Language('Catalan'), - 17: Language('Chinese'), 38: Language('Croatian'), 7: Language('Czech'), 24: Language('Danish'), - 23: Language('Dutch'), 2: Language('English'), 20: Language('Estonian'), 52: Language('Persian'), - 31: Language('Finnish'), 8: Language('French'), 5: Language('German'), 16: Language('gre'), - 57: Language('Kalaallisut'), 22: Language('Hebrew'), 42: Language('Hindi'), 15: Language('Hungarian'), - 6: Language('Icelandic'), 54: Language('Indonesian'), 49: Language('Irish'), 9: Language('Italian'), - 11: Language('Japanese'), 58: Language('Kazakh'), 4: Language('Korean'), 21: Language('Latvian'), - 19: Language('Lithuanian'), 35: Language('Macedonian'), 55: Language('Malay'), 40: Language('Chinese'), - 3: Language('Norwegian'), 26: Language('Polish'), 32: Language('Portuguese'), 13: Language('Romanian'), - 27: Language('Russian'), 36: Language('Serbian'), 47: Language('Serbian'), 56: Language('Sinhala'), - 37: Language('Slovak'), 1: Language('Slovenian'), 28: Language('Spanish'), 25: Language('Swedish'), - 44: Language('Thai'), 30: Language('Turkish'), 46: Language('Ukrainian'), Language('Vietnamese'): 51} - - def list_checked(self, video, languages): - if isinstance(video, Movie): - return self.query(video.path or video.release, languages, video.title, year=video.year, - keywords=get_keywords(video.guess)) - if isinstance(video, Episode): - return self.query(video.path or video.release, languages, video.series, season=video.season, - episode=video.episode, keywords=get_keywords(video.guess)) - - def query(self, filepath, languages, title, season=None, episode=None, year=None, keywords=None): - params = {'sXML': 1, 'sK': title, 'sJ': ','.join([str(self.get_code(l)) for l in languages])} - if season is not None: - params['sTS'] = season - if episode is not None: - params['sTE'] = episode - if year is not None: - params['sY'] = year - if keywords is not None: - params['sR'] = keywords - r = self.session.get(self.server_url + '/ppodnapisi/search', params=params) - if r.status_code != 200: - logger.error(u'Request %s returned status code %d' % (r.url, r.status_code)) - return [] - subtitles = [] - soup = BeautifulSoup(r.content, self.required_features) - for sub in soup('subtitle'): - if 'n' in sub.flags: - logger.debug(u'Skipping hearing impaired') - continue - language = self.get_language(sub.languageId.text) - confidence = float(sub.rating.text) / 5.0 - sub_keywords = set() - for release in sub.release.text.split(): - sub_keywords |= get_keywords(guessit.guess_file_info(release + '.srt', 'autodetect')) - sub_path = get_subtitle_path(filepath, language, self.config.multi) - subtitle = ResultSubtitle(sub_path, language, self.__class__.__name__.lower(), - sub.url.text, confidence=confidence, keywords=sub_keywords) - subtitles.append(subtitle) - return subtitles - - def download(self, subtitle): - r = self.session.get(subtitle.link) - if r.status_code != 200: - raise DownloadFailedError() - soup = BeautifulSoup(r.content) - self.download_zip_file(self.server_url + soup.find('a', href=re.compile('download'))['href'], subtitle.path) - return subtitle - - -Service = PodnapisiWeb diff --git a/lib/subliminal/services/subswiki.py b/lib/subliminal/services/subswiki.py index 9f9a3414..add79a5f 100644 --- a/lib/subliminal/services/subswiki.py +++ b/lib/subliminal/services/subswiki.py @@ -26,16 +26,15 @@ import logging import urllib -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class SubsWiki(ServiceBase): server_url = 'http://www.subswiki.com' - site_url = 'http://www.subswiki.com' api_based = False - languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'pob', 'por', 'spa-ES', u'spa', u'ita', u'cat']) + languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat']) language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'), - u'Català': Language('cat'), u'Brazilian': Language('pob'), u'English (US)': Language('eng-US'), + u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'), u'English (UK)': Language('eng-GB')} language_code = 'name' videos = [Episode, Movie] @@ -78,7 +77,7 @@ class SubsWiki(ServiceBase): subtitles = [] for sub in soup('td', {'class': 'NewsTitle'}): sub_keywords = split_keyword(sub.b.string.lower()) - if keywords and not keywords & sub_keywords: + if not keywords & sub_keywords: logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords)) continue for html_language in sub.parent.parent.find_all('td', {'class': 'language'}): diff --git a/lib/subliminal/services/subtitulos.py b/lib/subliminal/services/subtitulos.py index 6dd085a3..ee225b8b 100644 --- a/lib/subliminal/services/subtitulos.py +++ b/lib/subliminal/services/subtitulos.py @@ -27,16 +27,15 @@ import unicodedata import urllib -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class Subtitulos(ServiceBase): server_url = 'http://www.subtitulos.es' - site_url = 'http://www.subtitulos.es' api_based = False - languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'pob', 'por', 'spa-ES', u'spa', u'ita', u'cat']) - language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), #u'Español (Latinoamérica)': Language('spa'), - u'Català': Language('cat'), u'Brazilian': Language('pob'), u'English (US)': Language('eng-US'), + languages = language_set(['eng-US', 'eng-GB', 'eng', 'fre', 'por-BR', 'por', 'spa-ES', u'spa', u'ita', u'cat']) + language_map = {u'Español': Language('spa'), u'Español (España)': Language('spa'), u'Español (Latinoamérica)': Language('spa'), + u'Català': Language('cat'), u'Brazilian': Language('por-BR'), u'English (US)': Language('eng-US'), u'English (UK)': Language('eng-GB'), 'Galego': Language('glg')} language_code = 'name' videos = [Episode] @@ -46,13 +45,12 @@ class Subtitulos(ServiceBase): # and the 'ó' char directly. This is because now BS4 converts the html # code chars into their equivalent unicode char release_pattern = re.compile('Versi.+n (.+) ([0-9]+).([0-9])+ megabytes') - extra_keywords_pattern = re.compile("(?:con|para)\s(?:720p)?(?:\-|\s)?([A-Za-z]+)(?:\-|\s)?(?:720p)?(?:\s|\.)(?:y\s)?(?:720p)?(?:\-\s)?([A-Za-z]+)?(?:\-\s)?(?:720p)?(?:\.)?"); - + def list_checked(self, video, languages): return self.query(video.path or video.release, languages, get_keywords(video.guess), video.series, video.season, video.episode) def query(self, filepath, languages, keywords, series, season, episode): - request_series = series.lower().replace(' ', '-').replace('&', '@').replace('(','').replace(')','') + request_series = series.lower().replace(' ', '_') if isinstance(request_series, unicode): request_series = unicodedata.normalize('NFKD', request_series).encode('ascii', 'ignore') logger.debug(u'Getting subtitles for %s season %d episode %d with languages %r' % (series, season, episode, languages)) @@ -67,7 +65,7 @@ class Subtitulos(ServiceBase): subtitles = [] for sub in soup('div', {'id': 'version'}): sub_keywords = split_keyword(self.release_pattern.search(sub.find('p', {'class': 'title-sub'}).contents[1]).group(1).lower()) - if keywords and not keywords & sub_keywords: + if not keywords & sub_keywords: logger.debug(u'None of subtitle keywords %r in %r' % (sub_keywords, keywords)) continue for html_language in sub.findAllNext('ul', {'class': 'sslist'}): diff --git a/lib/subliminal/services/thesubdb.py b/lib/subliminal/services/thesubdb.py index 93787ad6..274c775f 100644 --- a/lib/subliminal/services/thesubdb.py +++ b/lib/subliminal/services/thesubdb.py @@ -16,23 +16,22 @@ # You should have received a copy of the GNU Lesser General Public License # along with subliminal. If not, see . from . import ServiceBase -from ..language import language_set, Language +from ..language import language_set from ..subtitles import get_subtitle_path, ResultSubtitle from ..videos import Episode, Movie, UnknownVideo import logging -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) class TheSubDB(ServiceBase): server_url = 'http://api.thesubdb.com' - site_url = 'http://www.thesubdb.com/' user_agent = 'SubDB/1.0 (subliminal/0.6; https://github.com/Diaoul/subliminal)' api_based = True # Source: http://api.thesubdb.com/?action=languages languages = language_set(['af', 'cs', 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'id', 'it', - 'la', 'nl', 'no', 'oc', 'pl', 'pb', 'ro', 'ru', 'sl', 'sr', 'sv', + 'la', 'nl', 'no', 'oc', 'pl', 'pt', 'ro', 'ru', 'sl', 'sr', 'sv', 'tr']) videos = [Movie, Episode, UnknownVideo] require_video = True @@ -49,10 +48,6 @@ class TheSubDB(ServiceBase): logger.error(u'Request %s returned status code %d' % (r.url, r.status_code)) return [] available_languages = language_set(r.content.split(',')) - #this is needed becase for theSubDB pt languages is Portoguese Brazil and not Portoguese# - #So we are deleting pt language and adding pb language - if Language('pt') in available_languages: - available_languages = available_languages - language_set(['pt']) | language_set(['pb']) languages &= available_languages if not languages: logger.debug(u'Could not find subtitles for hash %s with languages %r (only %r available)' % (moviehash, languages, available_languages)) diff --git a/lib/subliminal/services/tvsubtitles.py b/lib/subliminal/services/tvsubtitles.py index f6b2fd52..a260f169 100644 --- a/lib/subliminal/services/tvsubtitles.py +++ b/lib/subliminal/services/tvsubtitles.py @@ -26,7 +26,7 @@ import logging import re -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) def match(pattern, string): @@ -39,14 +39,13 @@ def match(pattern, string): class TvSubtitles(ServiceBase): server_url = 'http://www.tvsubtitles.net' - site_url = 'http://www.tvsubtitles.net' api_based = False languages = language_set(['ar', 'bg', 'cs', 'da', 'de', 'el', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ro', 'ru', 'sv', 'tr', 'uk', - 'zh', 'pb']) + 'zh', 'pt-br']) #TODO: Find more exceptions language_map = {'gr': Language('gre'), 'cz': Language('cze'), 'ua': Language('ukr'), - 'cn': Language('chi'), 'br': Language('pob')} + 'cn': Language('chi')} videos = [Episode] require_video = False required_features = ['permissive'] diff --git a/lib/subliminal/services/usub.py b/lib/subliminal/services/usub.py deleted file mode 100644 index d7d07677..00000000 --- a/lib/subliminal/services/usub.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2013 Julien Goret -# -# This file is part of subliminal. -# -# subliminal is free software; you can redistribute it and/or modify it under -# the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation; either version 3 of the License, or -# (at your option) any later version. -# -# subliminal is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with subliminal. If not, see . -from . import ServiceBase -from ..exceptions import ServiceError -from ..language import language_set, Language -from ..subtitles import get_subtitle_path, ResultSubtitle -from ..utils import get_keywords, split_keyword -from ..videos import Episode -from bs4 import BeautifulSoup -import logging -import urllib - -logger = logging.getLogger("subliminal") - -class Usub(ServiceBase): - server_url = 'http://www.u-sub.net/sous-titres' - site_url = 'http://www.u-sub.net/' - api_based = False - languages = language_set(['fr']) - videos = [Episode] - require_video = False - #required_features = ['permissive'] - - def list_checked(self, video, languages): - return self.query(video.path or video.release, languages, get_keywords(video.guess), series=video.series, season=video.season, episode=video.episode) - - def query(self, filepath, languages, keywords=None, series=None, season=None, episode=None): - - ## Check if we really got informations about our episode - if series and season and episode: - request_series = series.lower().replace(' ', '-') - if isinstance(request_series, unicode): - request_series = request_series.encode('utf-8') - logger.debug(u'Getting subtitles for %s season %d episode %d with language %r' % (series, season, episode, languages)) - r = self.session.get('%s/%s/saison_%s' % (self.server_url, urllib.quote(request_series),season)) - if r.status_code == 404: - print "Error 404" - logger.debug(u'Could not find subtitles for %s' % (series)) - return [] - else: - print "One or more parameter missing" - raise ServiceError('One or more parameter missing') - - ## Check if we didn't got an big and nasty http error - if r.status_code != 200: - print u'Request %s returned status code %d' % (r.url, r.status_code) - logger.error(u'Request %s returned status code %d' % (r.url, r.status_code)) - return [] - - ## Editing episode informations to be able to use it with our search - if episode < 10 : - episode_num='0'+str(episode) - else : - episode_num=str(episode) - season_num = str(season) - series_name = series.lower().replace(' ', '.') - possible_episode_naming = [season_num+'x'+episode_num,season_num+episode_num] - - - ## Actually parsing the page for the good subtitles - soup = BeautifulSoup(r.content, self.required_features) - subtitles = [] - subtitles_list = soup.find('table', {'id' : 'subtitles_list'}) - link_list = subtitles_list.findAll('a', {'class' : 'dl_link'}) - - for link in link_list : - link_url = link.get('href') - splited_link = link_url.split('/') - filename = splited_link[len(splited_link)-1] - for episode_naming in possible_episode_naming : - if episode_naming in filename : - for language in languages: - path = get_subtitle_path(filepath, language, self.config.multi) - subtitle = ResultSubtitle(path, language, self.__class__.__name__.lower(), '%s' % (link_url)) - subtitles.append(subtitle) - return subtitles - - def download(self, subtitle): - ## All downloaded files are zip files - self.download_zip_file(subtitle.link, subtitle.path) - return subtitle - - -Service = Usub diff --git a/lib/subliminal/videos.py b/lib/subliminal/videos.py index 9d533b2b..8bbb0a39 100644 --- a/lib/subliminal/videos.py +++ b/lib/subliminal/videos.py @@ -26,13 +26,10 @@ import mimetypes import os import struct -from sickbeard import encodingKludge as ek -import sickbeard - __all__ = ['EXTENSIONS', 'MIMETYPES', 'Video', 'Episode', 'Movie', 'UnknownVideo', 'scan', 'hash_opensubtitles', 'hash_thesubdb'] -logger = logging.getLogger("subliminal") +logger = logging.getLogger(__name__) #: Video extensions EXTENSIONS = ['.avi', '.mkv', '.mpg', '.mp4', '.m4v', '.mov', '.ogm', '.ogv', '.wmv', @@ -58,10 +55,6 @@ class Video(object): self.imdbid = imdbid self._path = None self.hashes = {} - - if isinstance(path, unicode): - path = path.encode('utf-8') - if os.path.exists(path): self._path = path self.size = os.path.getsize(self._path) @@ -145,10 +138,6 @@ class Video(object): if folder == '': folder = '.' existing = [f for f in os.listdir(folder) if f.startswith(basename)] - if sickbeard.SUBTITLES_DIR: - subsDir = ek.ek(os.path.join, folder, sickbeard.SUBTITLES_DIR) - if ek.ek(os.path.isdir, subsDir): - existing.extend([f for f in os.listdir(subsDir) if f.startswith(basename)]) for path in existing: for ext in subtitles.EXTENSIONS: if path.endswith(ext): @@ -225,9 +214,6 @@ def scan(entry, max_depth=3, scan_filter=None, depth=0): :rtype: list of (:class:`Video`, [:class:`~subliminal.subtitles.Subtitle`]) """ - if isinstance(entry, unicode): - entry = entry.encode('utf-8') - if depth > max_depth and max_depth != 0: # we do not want to search the whole file system except if max_depth = 0 return [] if os.path.isdir(entry): # a dir? recurse diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index e9b61bfc..96013ff0 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -408,11 +408,10 @@ def symlink(src, dst): if os.name == 'nt': import ctypes - if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, - 1280]: raise ctypes.WinError() - else: - os.symlink(src, dst) - + if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0,1280]: + raise ctypes.WinError() + else: + os.symlink(src, dst) def moveAndSymlinkFile(srcFile, destFile): try: diff --git a/sickbeard/subtitles.py b/sickbeard/subtitles.py index e765268c..510f6897 100644 --- a/sickbeard/subtitles.py +++ b/sickbeard/subtitles.py @@ -31,7 +31,7 @@ SINGLE = 'und' def sortedServiceList(): - servicesMapping = dict([(x.lower(), x) for x in subliminal.core.SERVICES]) + servicesMapping = dict([(x.lower(), x) for x in subliminal.Subtitle.core.Providers]) newList = [] diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 3ea8b5aa..59a49984 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -1194,8 +1194,10 @@ class TVEpisode(object): previous_subtitles = self.subtitles try: + subliminal.cache_region.configure('dogpile.cache.dbm', arguments={'filename': os.path.join(sickbeard.CACHE_DIR, '/path/to/cachefile.dbm')}) + videos = subliminal.scan_videos([self.location], subtitles=True, embedded_subtitles=True) need_languages = set(sickbeard.SUBTITLES_LANGUAGES) - set(self.subtitles) - subtitles = subliminal.download_subtitles([self.location], languages=need_languages, + subtitles = subliminal.download_best_subtitles([self.location], languages=need_languages, services=sickbeard.subtitles.getEnabledServiceList(), force=force, multi=True, cache_dir=sickbeard.CACHE_DIR) diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py index 9d0636d3..e4203ae3 100644 --- a/sickbeard/webapi.py +++ b/sickbeard/webapi.py @@ -1545,8 +1545,8 @@ class CMD_SickBeardRestart(ApiCall): return _responds(RESULT_SUCCESS, msg="SickBeard is restarting...") -class CMD_SickBeardSearchTVDB(ApiCall): - _help = {"desc": "search for show at tvdb with a given string and language", +class CMD_SickBeardSearchIndexers(ApiCall): + _help = {"desc": "search for show on the indexers with a given string and language", "optionalParameters": {"name": {"desc": "name of the show you want to search for"}, "indexerid": {"desc": "thetvdb.com unique id of a show"}, "lang": {"desc": "the 2 letter abbreviation lang id"} @@ -1820,17 +1820,17 @@ class CMD_ShowAddExisting(ApiCall): if not ek.ek(os.path.isdir, self.location): return _responds(RESULT_FAILURE, msg='Not a valid location') - tvdbName = None - tvdbResult = CMD_SickBeardSearchTVDB([], {"indexerid": self.indexerid}).run() + indexerName = None + indexerResult = CMD_SickBeardSearchIndexers([], {"indexerid": self.indexerid}).run() - if tvdbResult['result'] == result_type_map[RESULT_SUCCESS]: - if not tvdbResult['data']['results']: + if indexerResult['result'] == result_type_map[RESULT_SUCCESS]: + if not indexerResult['data']['results']: return _responds(RESULT_FAILURE, msg="Empty results returned, check indexerid and try again") - if len(tvdbResult['data']['results']) == 1 and 'name' in tvdbResult['data']['results'][0]: - tvdbName = tvdbResult['data']['results'][0]['name'] + if len(indexerResult['data']['results']) == 1 and 'name' in indexerResult['data']['results'][0]: + indexerName = indexerResult['data']['results'][0]['name'] - if not tvdbName: - return _responds(RESULT_FAILURE, msg="Unable to retrieve information from tvdb") + if not indexerName: + return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") quality_map = {'sdtv': Quality.SDTV, 'sddvd': Quality.SDDVD, @@ -1860,12 +1860,12 @@ class CMD_ShowAddExisting(ApiCall): sickbeard.showQueueScheduler.action.addShow(int(self.indexerid), self.location, SKIPPED, newQuality, int(self.flatten_folders)) #@UndefinedVariable - return _responds(RESULT_SUCCESS, {"name": tvdbName}, tvdbName + " has been queued to be added") + return _responds(RESULT_SUCCESS, {"name": indexerName}, indexerName + " has been queued to be added") class CMD_ShowAddNew(ApiCall): _help = {"desc": "add a new show to sickbeard", - "requiredParameters": {"indexerid": {"desc": "thetvdb.com unique id of a show"} + "requiredParameters": {"indexerid": {"desc": "thetvdb.com or tvrage.com unique id of a show"} }, "optionalParameters": {"initial": {"desc": "initial quality for the show"}, "location": {"desc": "base path for where the show folder is to be created"}, @@ -1964,20 +1964,20 @@ class CMD_ShowAddNew(ApiCall): return _responds(RESULT_FAILURE, msg="Status prohibited") newStatus = self.status - tvdbName = None - tvdbResult = CMD_SickBeardSearchTVDB([], {"indexerid": self.indexerid}).run() + indexerName = None + indexerResult = CMD_SickBeardSearchTVDB([], {"indexerid": self.indexerid}).run() - if tvdbResult['result'] == result_type_map[RESULT_SUCCESS]: - if not tvdbResult['data']['results']: + if indexerResult['result'] == result_type_map[RESULT_SUCCESS]: + if not indexerResult['data']['results']: return _responds(RESULT_FAILURE, msg="Empty results returned, check indexerid and try again") - if len(tvdbResult['data']['results']) == 1 and 'name' in tvdbResult['data']['results'][0]: - tvdbName = tvdbResult['data']['results'][0]['name'] + if len(indexerResult['data']['results']) == 1 and 'name' in indexerResult['data']['results'][0]: + indexerName = indexerResult['data']['results'][0]['name'] - if not tvdbName: - return _responds(RESULT_FAILURE, msg="Unable to retrieve information from tvdb") + if not indexerName: + return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") # moved the logic check to the end in an attempt to eliminate empty directory being created from previous errors - showPath = ek.ek(os.path.join, self.location, helpers.sanitizeFileName(tvdbName)) + showPath = ek.ek(os.path.join, self.location, helpers.sanitizeFileName(indexerName)) # don't create show dir if config says not to if sickbeard.ADD_SHOWS_WO_DIR: @@ -1994,7 +1994,7 @@ class CMD_ShowAddNew(ApiCall): sickbeard.showQueueScheduler.action.addShow(int(self.indexerid), showPath, newStatus, newQuality, int(self.flatten_folders), self.subtitles, self.lang) #@UndefinedVariable - return _responds(RESULT_SUCCESS, {"name": tvdbName}, tvdbName + " has been queued to be added") + return _responds(RESULT_SUCCESS, {"name": indexerName}, indexerName + " has been queued to be added") class CMD_ShowCache(ApiCall): @@ -2578,7 +2578,7 @@ _functionMaper = {"help": CMD_Help, "sb.pausebacklog": CMD_SickBeardPauseBacklog, "sb.ping": CMD_SickBeardPing, "sb.restart": CMD_SickBeardRestart, - "sb.searchtvdb": CMD_SickBeardSearchTVDB, + "sb.searchtvdb": CMD_SickBeardSearchIndexers, "sb.setdefaults": CMD_SickBeardSetDefaults, "sb.shutdown": CMD_SickBeardShutdown, "show": CMD_Show, diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index e1d1a721..c1ba96f9 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2889,7 +2889,7 @@ class Home: air_by_date = config.checkbox_to_value(air_by_date) subtitles = config.checkbox_to_value(subtitles) - indexer_lang = showObj.lang + indexer_lang = indexerLang # if we changed the language then kick off an update if indexer_lang == showObj.lang: