diff --git a/CHANGELOG.md b/CHANGELOG.md index b44b3d8f45..cdf25b0f09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.3.7 (2019-11-18) + +#### Fixes +- Fixed broken TheTVDB caused by API v3 changes ([#7355](https://github.com/pymedusa/Medusa/pull/7355)) +- DisplayShow: Fixed Xem and Medusa season exceptions not shown anymore ([#7360](https://github.com/pymedusa/Medusa/pull/7360)) + +----- + ## 0.3.6 (2019-11-11) #### New Features diff --git a/ext/readme.md b/ext/readme.md index 4133ccfc73..587dab6621 100644 --- a/ext/readme.md +++ b/ext/readme.md @@ -5,9 +5,9 @@ ext | `adba` | pymedusa/[6efeff3](https://github.com/pymedusa/adba/tree/6efeff3a ext | appdirs.py | [1.4.3](https://pypi.org/project/appdirs/1.4.3/) | `simpleanidb`, `subliminal` (cli only) | - ext | `attrs` | [18.2.0](https://pypi.org/project/attrs/18.2.0/) | `imdbpie` | Module: `attr` ext | `babelfish` | [f403000](https://github.com/Diaoul/babelfish/tree/f403000dd63092cfaaae80be9f309fd85c7f20c9) | **`medusa`**, `guessit`, `knowit`, `subliminal` | - -**ext2** | backports.functools_lru_cache.py | [1.5](https://pypi.org/project/backports.functools_lru_cache/1.5/) | `soupsieve` | - +**ext2** | backports.functools_lru_cache.py | [1.6.1](https://pypi.org/project/backports.functools_lru_cache/1.6.1/) | `soupsieve` | - **ext2** | backports_abc.py | [0.5](https://pypi.org/project/backports_abc/0.5/) | `tornado` | - -**ext2 ext3** | `beautifulsoup4` | [4.7.1](https://pypi.org/project/beautifulsoup4/4.7.1/) | **`medusa`**, `subliminal` | Module: `bs4` +**ext2 ext3** | `beautifulsoup4` | [4.8.1](https://pypi.org/project/beautifulsoup4/4.8.1/) | **`medusa`**, `subliminal` | Module: `bs4` ext | `bencode.py` | [2.1.0](https://pypi.org/project/bencode.py/2.1.0/) | **`medusa`** | Module: `bencode` ext | `boto` | [2.48.0](https://pypi.org/project/boto/2.48.0/) | `imdbpie` | - ext | `CacheControl` | [0.12.5](https://pypi.org/project/CacheControl/0.12.5/) | **`medusa`** | Module: `cachecontrol` @@ -60,7 +60,7 @@ ext | `requests-oauthlib` | [1.2.0](https://pypi.org/project/requests-oauthlib/1 **ext3** | `sgmllib3k` | [1.0.0](https://pypi.org/project/sgmllib3k/1.0.0/) | `feedparser` | File: `sgmllib.py` **ext2** | singledispatch.py
`singledispatch_helpers.py` | [3.4.0.3](https://pypi.org/project/singledispatch/3.4.0.3/) | `tornado` | - ext | six.py | [1.12.0](https://pypi.org/project/six/1.12.0/) | **`medusa`**, `adba`, `configobj`, `guessit`, `html5lib`, `imdbpie`, `Js2Py`, `knowit`, `python-dateutil`, `rebulk`, `subliminal`, `tvdbapiv2`, `validators` | - -ext | `soupsieve` | [1.7](https://pypi.org/project/soupsieve/1.7/) | `beautifulsoup4` | - +ext | `soupsieve` | [1.9.5](https://pypi.org/project/soupsieve/1.9.5/) | `beautifulsoup4` | - ext | `stevedore` | [1.30.1](https://pypi.org/project/stevedore/1.30.1/) | `subliminal` | - ext | `subliminal` | [develop@6ac2fa2](https://github.com/Diaoul/subliminal/tree/6ac2fa23ee5baa7d8452552edaa7c4a8a00d237a) | **`medusa`** | - ext | `tornado` | [5.1.1](https://pypi.org/project/tornado/5.1.1/) | **`medusa`**, `tornroutes` | - diff --git a/ext/soupsieve/__init__.py b/ext/soupsieve/__init__.py index 6464671472..ebd3a4a432 100644 --- a/ext/soupsieve/__init__.py +++ b/ext/soupsieve/__init__.py @@ -30,33 +30,37 @@ from . import css_parser as cp from . import css_match as cm from . import css_types as ct -from .util import DEBUG +from .util import DEBUG, deprecated, SelectorSyntaxError # noqa: F401 __all__ = ( - 'SoupSieve', 'compile', 'purge', 'DEBUG', - 'comments', 'icomments', 'closest', 'select', 'select_one', - 'iselect', 'match', 'filter' + 'DEBUG', 'SelectorSyntaxError', 'SoupSieve', + 'closest', 'comments', 'compile', 'filter', 'icomments', + 'iselect', 'match', 'select', 'select_one' ) SoupSieve = cm.SoupSieve -def compile(pattern, namespaces=None, flags=0): # noqa: A001 +def compile(pattern, namespaces=None, flags=0, **kwargs): # noqa: A001 """Compile CSS pattern.""" - if namespaces is None: - namespaces = ct.Namespaces() - if not isinstance(namespaces, ct.Namespaces): - namespaces = ct.Namespaces(**(namespaces)) + if namespaces is not None: + namespaces = ct.Namespaces(**namespaces) + + custom = kwargs.get('custom') + if custom is not None: + custom = ct.CustomSelectors(**custom) if isinstance(pattern, SoupSieve): - if flags != pattern.flags: - raise ValueError("Cannot change flags of a pattern") - elif namespaces != pattern.namespaces: - raise ValueError("Cannot change namespaces of a pattern") + if flags: + raise ValueError("Cannot process 'flags' argument on a compiled selector list") + elif namespaces is not None: + raise ValueError("Cannot process 'namespaces' argument on a compiled selector list") + elif custom is not None: + raise ValueError("Cannot process 'custom' argument on a compiled selector list") return pattern - return cp._cached_css_compile(pattern, namespaces, flags) + return cp._cached_css_compile(pattern, namespaces, custom, flags) def purge(): @@ -65,51 +69,59 @@ def purge(): cp._purge_cache() -def closest(select, tag, namespaces=None, flags=0): +def closest(select, tag, namespaces=None, flags=0, **kwargs): """Match closest ancestor.""" - return compile(select, namespaces, flags).closest(tag) + return compile(select, namespaces, flags, **kwargs).closest(tag) -def match(select, tag, namespaces=None, flags=0): +def match(select, tag, namespaces=None, flags=0, **kwargs): """Match node.""" - return compile(select, namespaces, flags).match(tag) + return compile(select, namespaces, flags, **kwargs).match(tag) -def filter(select, iterable, namespaces=None, flags=0): # noqa: A001 +def filter(select, iterable, namespaces=None, flags=0, **kwargs): # noqa: A001 """Filter list of nodes.""" - return compile(select, namespaces, flags).filter(iterable) + return compile(select, namespaces, flags, **kwargs).filter(iterable) -def comments(tag, limit=0, flags=0): +@deprecated("'comments' is not related to CSS selectors and will be removed in the future.") +def comments(tag, limit=0, flags=0, **kwargs): """Get comments only.""" - return list(icomments(tag, limit, flags)) + return [comment for comment in cm.CommentsMatch(tag).get_comments(limit)] -def icomments(tag, limit=0, flags=0): +@deprecated("'icomments' is not related to CSS selectors and will be removed in the future.") +def icomments(tag, limit=0, flags=0, **kwargs): """Iterate comments only.""" - for comment in cm.get_comments(tag, limit): + for comment in cm.CommentsMatch(tag).get_comments(limit): yield comment -def select_one(select, tag, namespaces=None, flags=0): +def select_one(select, tag, namespaces=None, flags=0, **kwargs): """Select a single tag.""" - return compile(select, namespaces, flags).select_one(tag) + return compile(select, namespaces, flags, **kwargs).select_one(tag) -def select(select, tag, namespaces=None, limit=0, flags=0): +def select(select, tag, namespaces=None, limit=0, flags=0, **kwargs): """Select the specified tags.""" - return compile(select, namespaces, flags).select(tag, limit) + return compile(select, namespaces, flags, **kwargs).select(tag, limit) -def iselect(select, tag, namespaces=None, limit=0, flags=0): +def iselect(select, tag, namespaces=None, limit=0, flags=0, **kwargs): """Iterate the specified tags.""" - for el in compile(select, namespaces, flags).iselect(tag, limit): + for el in compile(select, namespaces, flags, **kwargs).iselect(tag, limit): yield el + + +def escape(ident): + """Escape identifier.""" + + return cp.escape(ident) diff --git a/ext/soupsieve/__meta__.py b/ext/soupsieve/__meta__.py index 10adb064a3..109e4733bd 100644 --- a/ext/soupsieve/__meta__.py +++ b/ext/soupsieve/__meta__.py @@ -186,5 +186,5 @@ def parse_version(ver, pre=False): return Version(major, minor, micro, release, pre, post, dev) -__version_info__ = Version(1, 7, 0, "final") +__version_info__ = Version(1, 9, 5, "final") __version__ = __version_info__._get_canonical() diff --git a/ext/soupsieve/css_match.py b/ext/soupsieve/css_match.py index dae2b4032e..9ff2e88f1f 100644 --- a/ext/soupsieve/css_match.py +++ b/ext/soupsieve/css_match.py @@ -24,6 +24,7 @@ REL_HAS_CLOSE_SIBLING = ':+' NS_XHTML = 'http://www.w3.org/1999/xhtml' +NS_XML = 'http://www.w3.org/XML/1998/namespace' DIR_FLAGS = ct.SEL_DIR_LTR | ct.SEL_DIR_RTL RANGES = ct.SEL_IN_RANGE | ct.SEL_OUT_OF_RANGE @@ -42,6 +43,7 @@ RE_DATETIME = re.compile( r'^(?P[0-9]{4,})-(?P[0-9]{2})-(?P[0-9]{2})T(?P[0-9]{2}):(?P[0-9]{2})$' ) +RE_WILD_STRIP = re.compile(r'(?:(?:-\*-)(?:\*(?:-|$))*|-\*$)') MONTHS_30 = (4, 6, 9, 11) # April, June, September, and November FEB = 2 @@ -52,34 +54,9 @@ DAYS_IN_WEEK = 7 -def assert_valid_input(tag): - """Check if valid input tag.""" - - # Fail on unexpected types. - if not util.is_tag(tag): - raise TypeError("Expected a BeautifulSoup 'Tag', but instead recieved type {}".format(type(tag))) - - -def get_comments(el, limit=0): - """Get comments.""" - - assert_valid_input(el) - - if limit < 1: - limit = None - - for child in el.descendants: - if util.is_comment(child): - yield child - if limit is not None: - limit -= 1 - if limit < 1: - break - - -class FakeNthParent(object): +class _FakeParent(object): """ - Fake parent for `nth` selector. + Fake parent class. When we have a fragment with no `BeautifulSoup` document object, we can't evaluate `nth` selectors properly. Create a temporary @@ -91,164 +68,280 @@ def __init__(self, element): self.contents = [element] + def __len__(self): + """Length.""" -class CSSMatch(object): - """Perform CSS matching.""" + return len(self.contents) - def __init__(self, selectors, scope, namespaces, flags): - """Initialize.""" - assert_valid_input(scope) - self.tag = scope - self.cached_meta_lang = None - self.cached_default_forms = [] - self.cached_indeterminate_forms = [] - self.selectors = selectors - self.namespaces = namespaces - self.flags = flags - doc = scope - while doc.parent: - doc = doc.parent - root = None - if not util.is_doc(doc): - root = doc - else: - for child in doc.children: - if util.is_tag(child): - root = child - break - self.root = root - self.scope = scope if scope is not doc else root - self.html_namespace = self.is_html_ns(root) - self.is_xml = doc._is_xml and not self.html_namespace +class _DocumentNav(object): + """Navigate a Beautiful Soup document.""" - def is_html_ns(self, el): - """Check if in HTML namespace.""" + @classmethod + def assert_valid_input(cls, tag): + """Check if valid input tag or document.""" - ns = getattr(el, 'namespace') if el else None - return ns and ns == NS_XHTML + # Fail on unexpected types. + if not cls.is_tag(tag): + raise TypeError("Expected a BeautifulSoup 'Tag', but instead recieved type {}".format(type(tag))) - def get_namespace(self, el): - """Get the namespace for the element.""" + @staticmethod + def is_doc(obj): + """Is `BeautifulSoup` object.""" - namespace = '' - ns = el.namespace - if ns: - namespace = ns - return namespace + import bs4 + return isinstance(obj, bs4.BeautifulSoup) - def supports_namespaces(self): - """Check if namespaces are supported in the HTML type.""" + @staticmethod + def is_tag(obj): + """Is tag.""" - return self.is_xml or self.html_namespace + import bs4 + return isinstance(obj, bs4.Tag) - def get_bidi(self, el): - """Get directionality from element text.""" + @staticmethod + def is_comment(obj): + """Is comment.""" - for node in el.children: + import bs4 + return isinstance(obj, bs4.Comment) - # Analyze child text nodes - if util.is_tag(node): + @staticmethod + def is_declaration(obj): # pragma: no cover + """Is declaration.""" - # Avoid analyzing certain elements specified in the specification. - direction = DIR_MAP.get(util.lower(node.attrs.get('dir', '')), None) - if ( - util.lower(node.name) in ('bdi', 'script', 'style', 'textarea') or - direction is not None - ): - continue # pragma: no cover + import bs4 + return isinstance(obj, bs4.Declaration) - # Check directionality of this node's text - value = self.get_bidi(node) - if value is not None: - return value + @staticmethod + def is_cdata(obj): + """Is CDATA.""" - # Direction could not be determined - continue # pragma: no cover + import bs4 + return isinstance(obj, bs4.CData) - # Skip `doctype` comments, etc. - if util.is_special_string(node): - continue + @staticmethod + def is_processing_instruction(obj): # pragma: no cover + """Is processing instruction.""" - # Analyze text nodes for directionality. - for c in node: - bidi = unicodedata.bidirectional(c) - if bidi in ('AL', 'R', 'L'): - return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL - return None + import bs4 + return isinstance(obj, bs4.ProcessingInstruction) - def get_attribute(self, el, attr, prefix): - """Get attribute from element if it exists.""" + @staticmethod + def is_navigable_string(obj): + """Is navigable string.""" - value = None - if self.supports_namespaces(): - value = None - # If we have not defined namespaces, we can't very well find them, so don't bother trying. - if prefix: - ns = self.namespaces.get(prefix) - if ns is None and prefix != '*': - return None - else: - ns = None + import bs4 + return isinstance(obj, bs4.NavigableString) - for k, v in el.attrs.items(): + @staticmethod + def is_special_string(obj): + """Is special string.""" - # Get attribute parts - namespace = getattr(k, 'namespace', None) - name = getattr(k, 'name', None) + import bs4 + return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction, bs4.Doctype)) - # Can't match a prefix attribute as we haven't specified one to match - # Try to match it normally as a whole `p:a` as selector may be trying `p\:a`. - if ns is None: - if (self.is_xml and attr == k) or (not self.is_xml and util.lower(attr) == util.lower(k)): - value = v + @classmethod + def is_content_string(cls, obj): + """Check if node is content string.""" + + return cls.is_navigable_string(obj) and not cls.is_special_string(obj) + + @staticmethod + def create_fake_parent(el): + """Create fake parent for a given element.""" + + return _FakeParent(el) + + @staticmethod + def is_xml_tree(el): + """Check if element (or document) is from a XML tree.""" + + return el._is_xml + + def is_iframe(self, el): + """Check if element is an `iframe`.""" + + return ((el.name if self.is_xml_tree(el) else util.lower(el.name)) == 'iframe') and self.is_html_tag(el) + + def is_root(self, el): + """ + Return whether element is a root element. + + We check that the element is the root of the tree (which we have already pre-calculated), + and we check if it is the root element under an `iframe`. + """ + + root = self.root and self.root is el + if not root: + parent = self.get_parent(el) + root = parent is not None and self.is_html and self.is_iframe(parent) + return root + + def get_contents(self, el, no_iframe=False): + """Get contents or contents in reverse.""" + if not no_iframe or not self.is_iframe(el): + for content in el.contents: + yield content + + def get_children(self, el, start=None, reverse=False, tags=True, no_iframe=False): + """Get children.""" + + if not no_iframe or not self.is_iframe(el): + last = len(el.contents) - 1 + if start is None: + index = last if reverse else 0 + else: + index = start + end = -1 if reverse else last + 1 + incr = -1 if reverse else 1 + + if 0 <= index <= last: + while index != end: + node = el.contents[index] + index += incr + if not tags or self.is_tag(node): + yield node + + def get_descendants(self, el, tags=True, no_iframe=False): + """Get descendants.""" + + if not no_iframe or not self.is_iframe(el): + next_good = None + for child in el.descendants: + + if next_good is not None: + if child is not next_good: + continue + next_good = None + + is_tag = self.is_tag(child) + + if no_iframe and is_tag and self.is_iframe(child): + if child.next_sibling is not None: + next_good = child.next_sibling + else: + last_child = child + while self.is_tag(last_child) and last_child.contents: + last_child = last_child.contents[-1] + next_good = last_child.next_element + yield child + if next_good is None: break - # Coverage is not finding this even though it is executed. - # Adding a print statement before this (and erasing coverage) causes coverage to find the line. - # Ignore the false positive message. + # Coverage isn't seeing this even though it's executed continue # pragma: no cover - # We can't match our desired prefix attribute as the attribute doesn't have a prefix - if namespace is None or ns != namespace and prefix != '*': - continue + if not tags or is_tag: + yield child - if self.is_xml: - # The attribute doesn't match. - if attr != name: - continue - else: - # The attribute doesn't match. - if util.lower(attr) != util.lower(name): - continue - value = v - break + def get_parent(self, el, no_iframe=False): + """Get parent.""" + + parent = el.parent + if no_iframe and parent is not None and self.is_iframe(parent): + parent = None + return parent + + @staticmethod + def get_tag_name(el): + """Get tag.""" + + return el.name + + @staticmethod + def get_prefix_name(el): + """Get prefix.""" + + return el.prefix + + @staticmethod + def get_uri(el): + """Get namespace `URI`.""" + + return el.namespace + + @classmethod + def get_next(cls, el, tags=True): + """Get next sibling tag.""" + + sibling = el.next_sibling + while tags and not cls.is_tag(sibling) and sibling is not None: + sibling = sibling.next_sibling + return sibling + + @classmethod + def get_previous(cls, el, tags=True): + """Get previous sibling tag.""" + + sibling = el.previous_sibling + while tags and not cls.is_tag(sibling) and sibling is not None: + sibling = sibling.previous_sibling + return sibling + + @staticmethod + def has_html_ns(el): + """ + Check if element has an HTML namespace. + + This is a bit different than whether a element is treated as having an HTML namespace, + like we do in the case of `is_html_tag`. + """ + + ns = getattr(el, 'namespace') if el else None + return ns and ns == NS_XHTML + + @staticmethod + def split_namespace(el, attr_name): + """Return namespace and attribute name without the prefix.""" + + return getattr(attr_name, 'namespace', None), getattr(attr_name, 'name', None) + + @staticmethod + def get_attribute_by_name(el, name, default=None): + """Get attribute by name.""" + + value = default + if el._is_xml: + try: + value = el.attrs[name] + except KeyError: + pass else: for k, v in el.attrs.items(): - if util.lower(attr) != util.lower(k): - continue - value = v - break + if util.lower(k) == name: + value = v + break return value - def get_classes(self, el): + @staticmethod + def iter_attributes(el): + """Iterate attributes.""" + + for k, v in el.attrs.items(): + yield k, v + + @classmethod + def get_classes(cls, el): """Get classes.""" - classes = el.attrs.get('class', []) + classes = cls.get_attribute_by_name(el, 'class', []) if isinstance(classes, util.ustr): classes = RE_NOT_WS.findall(classes) return classes - def get_attribute_by_name(self, el, name, default=None): - """Get attribute by name.""" + def get_text(self, el, no_iframe=False): + """Get text.""" - value = default - for k, v in el.attrs.items(): - if (k if self.is_xml else util.lower(k)) == name: - value = v - break - return value + return ''.join( + [node for node in self.get_descendants(el, tags=False, no_iframe=no_iframe) if self.is_content_string(node)] + ) - def validate_day(self, year, month, day): + +class Inputs(object): + """Class for parsing and validating input items.""" + + @staticmethod + def validate_day(year, month, day): """Validate day.""" max_days = LONG_MONTH @@ -258,7 +351,8 @@ def validate_day(self, year, month, day): max_days = SHORT_MONTH return 1 <= day <= max_days - def validate_week(self, year, week): + @staticmethod + def validate_week(year, week): """Validate week.""" max_week = datetime.strptime("{}-{}-{}".format(12, 31, year), "%m-%d-%Y").isocalendar()[1] @@ -266,27 +360,32 @@ def validate_week(self, year, week): max_week = 53 return 1 <= week <= max_week - def validate_month(self, month): + @staticmethod + def validate_month(month): """Validate month.""" return 1 <= month <= 12 - def validate_year(self, year): + @staticmethod + def validate_year(year): """Validate year.""" return 1 <= year - def validate_hour(self, hour): + @staticmethod + def validate_hour(hour): """Validate hour.""" return 0 <= hour <= 23 - def validate_minutes(self, minutes): + @staticmethod + def validate_minutes(minutes): """Validate minutes.""" return 0 <= minutes <= 59 - def parse_input_value(self, itype, value): + @classmethod + def parse_value(cls, itype, value): """Parse the input value.""" parsed = None @@ -296,28 +395,28 @@ def parse_input_value(self, itype, value): year = int(m.group('year'), 10) month = int(m.group('month'), 10) day = int(m.group('day'), 10) - if self.validate_year(year) and self.validate_month(month) and self.validate_day(year, month, day): + if cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day): parsed = (year, month, day) elif itype == "month": m = RE_MONTH.match(value) if m: year = int(m.group('year'), 10) month = int(m.group('month'), 10) - if self.validate_year(year) and self.validate_month(month): + if cls.validate_year(year) and cls.validate_month(month): parsed = (year, month) elif itype == "week": m = RE_WEEK.match(value) if m: year = int(m.group('year'), 10) week = int(m.group('week'), 10) - if self.validate_year(year) and self.validate_week(year, week): + if cls.validate_year(year) and cls.validate_week(year, week): parsed = (year, week) elif itype == "time": m = RE_TIME.match(value) if m: hour = int(m.group('hour'), 10) minutes = int(m.group('minutes'), 10) - if self.validate_hour(hour) and self.validate_minutes(minutes): + if cls.validate_hour(hour) and cls.validate_minutes(minutes): parsed = (hour, minutes) elif itype == "datetime-local": m = RE_DATETIME.match(value) @@ -328,8 +427,8 @@ def parse_input_value(self, itype, value): hour = int(m.group('hour'), 10) minutes = int(m.group('minutes'), 10) if ( - self.validate_year(year) and self.validate_month(month) and self.validate_day(year, month, day) and - self.validate_hour(hour) and self.validate_minutes(minutes) + cls.validate_year(year) and cls.validate_month(month) and cls.validate_day(year, month, day) and + cls.validate_hour(hour) and cls.validate_minutes(minutes) ): parsed = (year, month, day, hour, minutes) elif itype in ("number", "range"): @@ -338,11 +437,219 @@ def parse_input_value(self, itype, value): parsed = float(m.group('value')) return parsed + +class _Match(object): + """Perform CSS matching.""" + + def __init__(self, selectors, scope, namespaces, flags): + """Initialize.""" + + self.assert_valid_input(scope) + self.tag = scope + self.cached_meta_lang = [] + self.cached_default_forms = [] + self.cached_indeterminate_forms = [] + self.selectors = selectors + self.namespaces = {} if namespaces is None else namespaces + self.flags = flags + self.iframe_restrict = False + + # Find the root element for the whole tree + doc = scope + parent = self.get_parent(doc) + while parent: + doc = parent + parent = self.get_parent(doc) + root = None + if not self.is_doc(doc): + root = doc + else: + for child in self.get_children(doc): + root = child + break + + self.root = root + self.scope = scope if scope is not doc else root + self.has_html_namespace = self.has_html_ns(root) + + # A document can be both XML and HTML (XHTML) + self.is_xml = self.is_xml_tree(doc) + self.is_html = not self.is_xml or self.has_html_namespace + + def supports_namespaces(self): + """Check if namespaces are supported in the HTML type.""" + + return self.is_xml or self.has_html_namespace + + def get_tag_ns(self, el): + """Get tag namespace.""" + + if self.supports_namespaces(): + namespace = '' + ns = self.get_uri(el) + if ns: + namespace = ns + else: + namespace = NS_XHTML + return namespace + + def is_html_tag(self, el): + """Check if tag is in HTML namespace.""" + + return self.get_tag_ns(el) == NS_XHTML + + def get_tag(self, el): + """Get tag.""" + + name = self.get_tag_name(el) + return util.lower(name) if name is not None and not self.is_xml else name + + def get_prefix(self, el): + """Get prefix.""" + + prefix = self.get_prefix_name(el) + return util.lower(prefix) if prefix is not None and not self.is_xml else prefix + + def find_bidi(self, el): + """Get directionality from element text.""" + + for node in self.get_children(el, tags=False): + + # Analyze child text nodes + if self.is_tag(node): + + # Avoid analyzing certain elements specified in the specification. + direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(node, 'dir', '')), None) + if ( + self.get_tag(node) in ('bdi', 'script', 'style', 'textarea', 'iframe') or + not self.is_html_tag(node) or + direction is not None + ): + continue # pragma: no cover + + # Check directionality of this node's text + value = self.find_bidi(node) + if value is not None: + return value + + # Direction could not be determined + continue # pragma: no cover + + # Skip `doctype` comments, etc. + if self.is_special_string(node): + continue + + # Analyze text nodes for directionality. + for c in node: + bidi = unicodedata.bidirectional(c) + if bidi in ('AL', 'R', 'L'): + return ct.SEL_DIR_LTR if bidi == 'L' else ct.SEL_DIR_RTL + return None + + def extended_language_filter(self, lang_range, lang_tag): + """Filter the language tags.""" + + match = True + lang_range = RE_WILD_STRIP.sub('-', lang_range).lower() + ranges = lang_range.split('-') + subtags = lang_tag.lower().split('-') + length = len(ranges) + rindex = 0 + sindex = 0 + r = ranges[rindex] + s = subtags[sindex] + + # Primary tag needs to match + if r != '*' and r != s: + match = False + + rindex += 1 + sindex += 1 + + # Match until we run out of ranges + while match and rindex < length: + r = ranges[rindex] + try: + s = subtags[sindex] + except IndexError: + # Ran out of subtags, + # but we still have ranges + match = False + continue + + # Empty range + if not r: + match = False + continue + + # Matched range + elif s == r: + rindex += 1 + + # Implicit wildcard cannot match + # singletons + elif len(s) == 1: + match = False + continue + + # Implicitly matched, so grab next subtag + sindex += 1 + + return match + + def match_attribute_name(self, el, attr, prefix): + """Match attribute name and return value if it exists.""" + + value = None + if self.supports_namespaces(): + value = None + # If we have not defined namespaces, we can't very well find them, so don't bother trying. + if prefix: + ns = self.namespaces.get(prefix) + if ns is None and prefix != '*': + return None + else: + ns = None + + for k, v in self.iter_attributes(el): + + # Get attribute parts + namespace, name = self.split_namespace(el, k) + + # Can't match a prefix attribute as we haven't specified one to match + # Try to match it normally as a whole `p:a` as selector may be trying `p\:a`. + if ns is None: + if (self.is_xml and attr == k) or (not self.is_xml and util.lower(attr) == util.lower(k)): + value = v + break + # Coverage is not finding this even though it is executed. + # Adding a print statement before this (and erasing coverage) causes coverage to find the line. + # Ignore the false positive message. + continue # pragma: no cover + + # We can't match our desired prefix attribute as the attribute doesn't have a prefix + if namespace is None or ns != namespace and prefix != '*': + continue + + # The attribute doesn't match. + if (util.lower(attr) != util.lower(name)) if not self.is_xml else (attr != name): + continue + + value = v + break + else: + for k, v in self.iter_attributes(el): + if util.lower(attr) != util.lower(k): + continue + value = v + break + return value + def match_namespace(self, el, tag): """Match the namespace of the element.""" match = True - namespace = self.get_namespace(el) + namespace = self.get_tag_ns(el) default_namespace = self.namespaces.get('') tag_ns = '' if tag.prefix is None else self.namespaces.get(tag.prefix, None) # We must match the default namespace if one is not provided @@ -365,8 +672,8 @@ def match_attributes(self, el, attributes): match = True if attributes: for a in attributes: - value = self.get_attribute(el, a.attribute, a.prefix) - pattern = a.xml_type_pattern if not self.html_namespace and a.xml_type_pattern else a.pattern + value = self.match_attribute_name(el, a.attribute, a.prefix) + pattern = a.xml_type_pattern if self.is_xml and a.xml_type_pattern else a.pattern if isinstance(value, list): value = ' '.join(value) if value is None: @@ -382,19 +689,19 @@ def match_attributes(self, el, attributes): def match_tagname(self, el, tag): """Match tag name.""" + name = (util.lower(tag.name) if not self.is_xml and tag.name is not None else tag.name) return not ( - tag.name and - tag.name not in ((util.lower(el.name) if not self.is_xml else el.name), '*') + name is not None and + name not in (self.get_tag(el), '*') ) def match_tag(self, el, tag): """Match the tag.""" - has_ns = self.supports_namespaces() match = True if tag is not None: # Verify namespace - if has_ns and not self.match_namespace(el, tag): + if not self.match_namespace(el, tag): match = False if not self.match_tagname(el, tag): match = False @@ -405,27 +712,22 @@ def match_past_relations(self, el, relation): found = False if relation[0].rel_type == REL_PARENT: - parent = el.parent + parent = self.get_parent(el, no_iframe=self.iframe_restrict) while not found and parent: found = self.match_selectors(parent, relation) - parent = parent.parent + parent = self.get_parent(parent, no_iframe=self.iframe_restrict) elif relation[0].rel_type == REL_CLOSE_PARENT: - parent = el.parent + parent = self.get_parent(el, no_iframe=self.iframe_restrict) if parent: found = self.match_selectors(parent, relation) elif relation[0].rel_type == REL_SIBLING: - sibling = el.previous_sibling + sibling = self.get_previous(el) while not found and sibling: - if not util.is_tag(sibling): - sibling = sibling.previous_sibling - continue found = self.match_selectors(sibling, relation) - sibling = sibling.previous_sibling + sibling = self.get_previous(sibling) elif relation[0].rel_type == REL_CLOSE_SIBLING: - sibling = el.previous_sibling - while sibling and not util.is_tag(sibling): - sibling = sibling.previous_sibling - if sibling and util.is_tag(sibling): + sibling = self.get_previous(el) + if sibling and self.is_tag(sibling): found = self.match_selectors(sibling, relation) return found @@ -433,9 +735,8 @@ def match_future_child(self, parent, relation, recursive=False): """Match future child.""" match = False - for child in (parent.descendants if recursive else parent.children): - if not util.is_tag(child): - continue + children = self.get_descendants if recursive else self.get_children + for child in children(parent, no_iframe=self.iframe_restrict): match = self.match_selectors(child, relation) if match: break @@ -450,18 +751,13 @@ def match_future_relations(self, el, relation): elif relation[0].rel_type == REL_HAS_CLOSE_PARENT: found = self.match_future_child(el, relation) elif relation[0].rel_type == REL_HAS_SIBLING: - sibling = el.next_sibling + sibling = self.get_next(el) while not found and sibling: - if not util.is_tag(sibling): - sibling = sibling.next_sibling - continue found = self.match_selectors(sibling, relation) - sibling = sibling.next_sibling + sibling = self.get_next(sibling) elif relation[0].rel_type == REL_HAS_CLOSE_SIBLING: - sibling = el.next_sibling - while sibling and not util.is_tag(sibling): - sibling = sibling.next_sibling - if sibling and util.is_tag(sibling): + sibling = self.get_next(el) + if sibling and self.is_tag(sibling): found = self.match_selectors(sibling, relation) return found @@ -482,7 +778,7 @@ def match_id(self, el, ids): found = True for i in ids: - if i != el.attrs.get('id', ''): + if i != self.get_attribute_by_name(el, 'id', ''): found = False break return found @@ -501,7 +797,28 @@ def match_classes(self, el, classes): def match_root(self, el): """Match element as root.""" - return self.root and self.root is el + is_root = self.is_root(el) + if is_root: + sibling = self.get_previous(el, tags=False) + while is_root and sibling is not None: + if ( + self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or + self.is_cdata(sibling) + ): + is_root = False + else: + sibling = self.get_previous(sibling, tags=False) + if is_root: + sibling = self.get_next(el, tags=False) + while is_root and sibling is not None: + if ( + self.is_tag(sibling) or (self.is_content_string(sibling) and sibling.strip()) or + self.is_cdata(sibling) + ): + is_root = False + else: + sibling = self.get_next(sibling, tags=False) + return is_root def match_scope(self, el): """Match element as scope.""" @@ -512,8 +829,8 @@ def match_nth_tag_type(self, el, child): """Match tag type for `nth` matches.""" return( - (child.name == (util.lower(el.name) if not self.is_xml else el.name)) and - (not self.supports_namespaces() or self.get_namespace(child) == self.get_namespace(el)) + (self.get_tag(child) == self.get_tag(el)) and + (self.get_tag_ns(child) == self.get_tag_ns(el)) ) def match_nth(self, el, nth): @@ -525,11 +842,12 @@ def match_nth(self, el, nth): matched = False if n.selectors and not self.match_selectors(el, n.selectors): break - parent = el.parent + parent = self.get_parent(el) if parent is None: - parent = FakeNthParent(el) + parent = self.create_fake_parent(el) last = n.last - last_index = len(parent.contents) - 1 + last_index = len(parent) - 1 + index = last_index if last else 0 relative_index = 0 a = n.a b = n.b @@ -537,7 +855,6 @@ def match_nth(self, el, nth): count = 0 count_incr = 1 factor = -1 if last else 1 - index = len(parent.contents) - 1 if last else 0 idx = last_idx = a * count + b if var else a # We can only adjust bounds within a variable index @@ -585,10 +902,9 @@ def match_nth(self, el, nth): while 1 <= idx <= last_index + 1: child = None # Evaluate while our child index is still in range. - while 0 <= index <= last_index: - child = parent.contents[index] + for child in self.get_children(parent, start=index, reverse=factor < 0, tags=False): index += factor - if not util.is_tag(child): + if not self.is_tag(child): continue # Handle `of S` in `nth-child` if n.selectors and not self.match_selectors(child, n.selectors): @@ -622,14 +938,11 @@ def match_empty(self, el): """Check if element is empty (if requested).""" is_empty = True - for child in el.children: - if util.is_tag(child): + for child in self.get_children(el, tags=False): + if self.is_tag(child): is_empty = False break - elif ( - (util.is_navigable_string(child) and not util.is_special_string(child)) and - RE_NOT_EMPTY.search(child) - ): + elif self.is_content_string(child) and RE_NOT_EMPTY.search(child): is_empty = False break return is_empty @@ -646,12 +959,18 @@ def match_subselectors(self, el, selectors): def match_contains(self, el, contains): """Match element if it contains text.""" - types = (util.get_navigable_string_type(el),) match = True - for c in contains: - if c not in el.get_text(types=types): + content = None + for contain_list in contains: + if content is None: + content = self.get_text(el, no_iframe=self.is_html) + found = False + for text in contain_list.text: + if text in content: + found = True + break + if not found: match = False - break return match def match_default(self, el): @@ -661,12 +980,12 @@ def match_default(self, el): # Find this input's form form = None - parent = el.parent + parent = self.get_parent(el, no_iframe=True) while parent and form is None: - if util.lower(parent.name) == 'form': + if self.get_tag(parent) == 'form' and self.is_html_tag(parent): form = parent else: - parent = parent.parent + parent = self.get_parent(parent, no_iframe=True) # Look in form cache to see if we've already located its default button found_form = False @@ -679,44 +998,38 @@ def match_default(self, el): # We didn't have the form cached, so look for its default button if not found_form: - child_found = False - for child in form.descendants: - if not util.is_tag(child): - continue - name = util.lower(child.name) + for child in self.get_descendants(form, no_iframe=True): + name = self.get_tag(child) # Can't do nested forms (haven't figured out why we never hit this) if name == 'form': # pragma: no cover break if name in ('input', 'button'): - for k, v in child.attrs.items(): - if util.lower(k) == 'type' and util.lower(v) == 'submit': - child_found = True - self.cached_default_forms.append([form, child]) - if el is child: - match = True - break - if child_found: - break + v = self.get_attribute_by_name(child, 'type', '') + if v and util.lower(v) == 'submit': + self.cached_default_forms.append([form, child]) + if el is child: + match = True + break return match def match_indeterminate(self, el): """Match default.""" match = False - name = el.attrs.get('name') + name = self.get_attribute_by_name(el, 'name') def get_parent_form(el): """Find this input's form.""" form = None - parent = el.parent + parent = self.get_parent(el, no_iframe=True) while form is None: - if util.lower(parent.name) == 'form': + if self.get_tag(parent) == 'form' and self.is_html_tag(parent): form = parent break - elif parent.parent: - parent = parent.parent - else: - form = parent + last_parent = parent + parent = self.get_parent(parent, no_iframe=True) + if parent is None: + form = last_parent break return form @@ -734,15 +1047,15 @@ def get_parent_form(el): # We didn't have the form cached, so validate that the radio button is indeterminate if not found_form: checked = False - for child in form.descendants: - if not util.is_tag(child) or child is el: + for child in self.get_descendants(form, no_iframe=True): + if child is el: continue - tag_name = util.lower(child.name) + tag_name = self.get_tag(child) if tag_name == 'input': is_radio = False check = False has_name = False - for k, v in child.attrs.items(): + for k, v in self.iter_attributes(child): if util.lower(k) == 'type' and util.lower(v) == 'radio': is_radio = True elif util.lower(k) == 'name' and v == name: @@ -765,38 +1078,49 @@ def match_lang(self, el, langs): match = False has_ns = self.supports_namespaces() + root = self.root + has_html_namespace = self.has_html_namespace # Walk parents looking for `lang` (HTML) or `xml:lang` XML property. parent = el found_lang = None - while parent and parent.parent and not found_lang: - ns = self.is_html_ns(parent) - for k, v in parent.attrs.items(): + last = None + while not found_lang: + has_html_ns = self.has_html_ns(parent) + for k, v in self.iter_attributes(parent): + attr_ns, attr = self.split_namespace(parent, k) if ( - (self.is_xml and k == 'xml:lang') or + ((not has_ns or has_html_ns) and (util.lower(k) if not self.is_xml else k) == 'lang') or ( - not self.is_xml and ( - ((not has_ns or ns) and util.lower(k) == 'lang') or - (has_ns and not ns and util.lower(k) == 'xml:lang') - ) + has_ns and not has_html_ns and attr_ns == NS_XML and + (util.lower(attr) if not self.is_xml and attr is not None else attr) == 'lang' ) ): found_lang = v break - parent = parent.parent + last = parent + parent = self.get_parent(parent, no_iframe=self.is_html) + + if parent is None: + root = last + has_html_namespace = self.has_html_ns(root) + parent = last + break # Use cached meta language. - if not found_lang and self.cached_meta_lang is not None: - found_lang = self.cached_meta_lang + if not found_lang and self.cached_meta_lang: + for cache in self.cached_meta_lang: + if root is cache[0]: + found_lang = cache[1] # If we couldn't find a language, and the document is HTML, look to meta to determine language. - if found_lang is None and not self.is_xml: + if found_lang is None and (not self.is_xml or (has_html_namespace and root.name == 'html')): # Find head found = False for tag in ('html', 'head'): found = False - for child in parent.children: - if util.is_tag(child) and util.lower(child.name) == tag: + for child in self.get_children(parent, no_iframe=self.is_html): + if self.get_tag(child) == tag and self.is_html_tag(child): found = True parent = child break @@ -806,29 +1130,29 @@ def match_lang(self, el, langs): # Search meta tags if found: for child in parent: - if util.is_tag(child) and util.lower(child.name) == 'meta': + if self.is_tag(child) and self.get_tag(child) == 'meta' and self.is_html_tag(parent): c_lang = False content = None - for k, v in child.attrs.items(): + for k, v in self.iter_attributes(child): if util.lower(k) == 'http-equiv' and util.lower(v) == 'content-language': c_lang = True if util.lower(k) == 'content': content = v if c_lang and content: found_lang = content - self.cached_meta_lang = found_lang + self.cached_meta_lang.append((root, found_lang)) break if found_lang: break if not found_lang: - self.cached_meta_lang = False + self.cached_meta_lang.append((root, False)) # If we determined a language, compare. if found_lang: for patterns in langs: match = False for pattern in patterns: - if pattern.match(found_lang): + if self.extended_language_filter(pattern, found_lang): match = True if not match: break @@ -842,20 +1166,24 @@ def match_dir(self, el, directionality): if directionality & ct.SEL_DIR_LTR and directionality & ct.SEL_DIR_RTL: return False + if el is None or not self.is_html_tag(el): + return False + # Element has defined direction of left to right or right to left - direction = DIR_MAP.get(util.lower(el.attrs.get('dir', '')), None) + direction = DIR_MAP.get(util.lower(self.get_attribute_by_name(el, 'dir', '')), None) if direction not in (None, 0): return direction == directionality # Element is the document element (the root) and no direction assigned, assume left to right. - is_root = self.match_root(el) + is_root = self.is_root(el) if is_root and direction is None: return ct.SEL_DIR_LTR == directionality # If `input[type=telephone]` and no direction is assigned, assume left to right. - is_input = util.lower(el.name) == 'input' - is_textarea = util.lower(el.name) == 'textarea' - is_bdi = util.lower(el.name) == 'bdi' + name = self.get_tag(el) + is_input = name == 'input' + is_textarea = name == 'textarea' + is_bdi = name == 'bdi' itype = util.lower(self.get_attribute_by_name(el, 'type', '')) if is_input else '' if is_input and itype == 'tel' and direction is None: return ct.SEL_DIR_LTR == directionality @@ -864,8 +1192,8 @@ def match_dir(self, el, directionality): if ((is_input and itype in ('text', 'search', 'tel', 'url', 'email')) or is_textarea) and direction == 0: if is_textarea: value = [] - for node in el.contents: - if util.is_navigable_string(node) and not util.is_special_string(node): + for node in self.get_contents(el, no_iframe=True): + if self.is_content_string(node): value.append(node) value = ''.join(value) else: @@ -880,19 +1208,19 @@ def match_dir(self, el, directionality): return ct.SEL_DIR_LTR == directionality elif is_root: return ct.SEL_DIR_LTR == directionality - return self.match_dir(el.parent, directionality) + return self.match_dir(self.get_parent(el, no_iframe=True), directionality) # Auto handling for `bdi` and other non text inputs. if (is_bdi and direction is None) or direction == 0: - direction = self.get_bidi(el) + direction = self.find_bidi(el) if direction is not None: return direction == directionality elif is_root: return ct.SEL_DIR_LTR == directionality - return self.match_dir(el.parent, directionality) + return self.match_dir(self.get_parent(el, no_iframe=True), directionality) # Match parents direction - return self.match_dir(el.parent, directionality) + return self.match_dir(self.get_parent(el, no_iframe=True), directionality) def match_range(self, el, condition): """ @@ -906,13 +1234,13 @@ def match_range(self, el, condition): out_of_range = False - itype = self.get_attribute_by_name(el, 'type').lower() + itype = util.lower(self.get_attribute_by_name(el, 'type')) mn = self.get_attribute_by_name(el, 'min', None) if mn is not None: - mn = self.parse_input_value(itype, mn) + mn = Inputs.parse_value(itype, mn) mx = self.get_attribute_by_name(el, 'max', None) if mx is not None: - mx = self.parse_input_value(itype, mx) + mx = Inputs.parse_value(itype, mx) # There is no valid min or max, so we cannot evaluate a range if mn is None and mx is None: @@ -920,7 +1248,7 @@ def match_range(self, el, condition): value = self.get_attribute_by_name(el, 'value', None) if value is not None: - value = self.parse_input_value(itype, value) + value = Inputs.parse_value(itype, value) if value is not None: if itype in ("date", "datetime-local", "month", "week", "number", "range"): if mn is not None and value < mn: @@ -954,24 +1282,47 @@ def match_defined(self, el): if it doesn't, there is nothing we can do. """ + name = self.get_tag(el) return ( - self.is_xml or - el.name.find('-') == -1 or - el.name.find(':') != -1 or - el.prefix is not None + name.find('-') == -1 or + name.find(':') != -1 or + self.get_prefix(el) is not None ) + def match_placeholder_shown(self, el): + """ + Match placeholder shown according to HTML spec. + + - text area should be checked if they have content. A single newline does not count as content. + + """ + + match = False + content = self.get_text(el) + if content in ('', '\n'): + match = True + + return match + def match_selectors(self, el, selectors): """Check if element matches one of the selectors.""" match = False is_not = selectors.is_not is_html = selectors.is_html - if not (is_html and self.is_xml): + + # Internal selector lists that use the HTML flag, will automatically get the `html` namespace. + if is_html: + namespaces = self.namespaces + iframe_restrict = self.iframe_restrict + self.namespaces = {'html': NS_XHTML} + self.iframe_restrict = True + + if not is_html or self.is_html: for selector in selectors: match = is_not # We have a un-matchable situation (like `:focus` as you can focus an element in this environment) - if isinstance(selector, ct.NullSelector): + if isinstance(selector, ct.SelectorNull): continue # Verify tag matches if not self.match_tag(el, selector.tag): @@ -985,6 +1336,9 @@ def match_selectors(self, el, selectors): # Verify element is scope if selector.flags & ct.SEL_SCOPE and not self.match_scope(el): continue + # Verify element has placeholder shown + if selector.flags & ct.SEL_PLACEHOLDER_SHOWN and not self.match_placeholder_shown(el): + continue # Verify `nth` matches if not self.match_nth(el, selector.nth): continue @@ -1027,6 +1381,11 @@ def match_selectors(self, el, selectors): match = not is_not break + # Restore actual namespaces being used for external selector lists + if is_html: + self.namespaces = namespaces + self.iframe_restrict = iframe_restrict + return match def select(self, limit=0): @@ -1035,8 +1394,8 @@ def select(self, limit=0): if limit < 1: limit = None - for child in self.tag.descendants: - if util.is_tag(child) and self.match(child): + for child in self.get_descendants(self.tag): + if self.match(child): yield child if limit is not None: limit -= 1 @@ -1052,32 +1411,61 @@ def closest(self): if self.match(current): closest = current else: - current = current.parent + current = self.get_parent(current) return closest def filter(self): # noqa A001 """Filter tag's children.""" - return [node for node in self.tag if not util.is_navigable_string(node) and self.match(node)] + return [tag for tag in self.get_contents(self.tag) if not self.is_navigable_string(tag) and self.match(tag)] def match(self, el): """Match.""" - return not util.is_doc(el) and util.is_tag(el) and self.match_selectors(el, self.selectors) + return not self.is_doc(el) and self.is_tag(el) and self.match_selectors(el, self.selectors) + + +class CSSMatch(_DocumentNav, _Match): + """The Beautiful Soup CSS match class.""" + + +class CommentsMatch(_DocumentNav): + """Comments matcher.""" + + def __init__(self, el): + """Initialize.""" + + self.assert_valid_input(el) + self.tag = el + + def get_comments(self, limit=0): + """Get comments.""" + + if limit < 1: + limit = None + + for child in self.get_descendants(self.tag, tags=False): + if self.is_comment(child): + yield child + if limit is not None: + limit -= 1 + if limit < 1: + break class SoupSieve(ct.Immutable): """Compiled Soup Sieve selector matching object.""" - __slots__ = ("pattern", "selectors", "namespaces", "flags", "_hash") + __slots__ = ("pattern", "selectors", "namespaces", "custom", "flags", "_hash") - def __init__(self, pattern, selectors, namespaces, flags): + def __init__(self, pattern, selectors, namespaces, custom, flags): """Initialize.""" super(SoupSieve, self).__init__( pattern=pattern, selectors=selectors, namespaces=namespaces, + custom=custom, flags=flags ) @@ -1103,20 +1491,22 @@ def filter(self, iterable): # noqa A001 so for those, we use a new `CSSMatch` for each item in the iterable. """ - if util.is_tag(iterable): + if CSSMatch.is_tag(iterable): return CSSMatch(self.selectors, iterable, self.namespaces, self.flags).filter() else: - return [node for node in iterable if not util.is_navigable_string(node) and self.match(node)] + return [node for node in iterable if not CSSMatch.is_navigable_string(node) and self.match(node)] + @util.deprecated("'comments' is not related to CSS selectors and will be removed in the future.") def comments(self, tag, limit=0): """Get comments only.""" - return list(self.icomments(tag, limit)) + return [comment for comment in CommentsMatch(tag).get_comments(limit)] + @util.deprecated("'icomments' is not related to CSS selectors and will be removed in the future.") def icomments(self, tag, limit=0): """Iterate comments only.""" - for comment in get_comments(tag, limit): + for comment in CommentsMatch(tag).get_comments(limit): yield comment def select_one(self, tag): @@ -1139,7 +1529,12 @@ def iselect(self, tag, limit=0): def __repr__(self): # pragma: no cover """Representation.""" - return "SoupSieve(pattern={!r}, namespaces={!r}, flags={!r})".format(self.pattern, self.namespaces, self.flags) + return "SoupSieve(pattern={!r}, namespaces={!r}, custom={!r}, flags={!r})".format( + self.pattern, + self.namespaces, + self.custom, + self.flags + ) __str__ = __repr__ diff --git a/ext/soupsieve/css_parser.py b/ext/soupsieve/css_parser.py index ac1a2cdd02..e7c8833b0e 100644 --- a/ext/soupsieve/css_parser.py +++ b/ext/soupsieve/css_parser.py @@ -4,7 +4,9 @@ from . import util from . import css_match as cm from . import css_types as ct -from collections import OrderedDict +from .util import SelectorSyntaxError + +UNICODE_REPLACEMENT_CHAR = 0xFFFD # Simple pseudo classes that take no parameters PSEUDO_SIMPLE = { @@ -84,19 +86,26 @@ # Sub-patterns parts # Whitespace -WS = r'[ \t\r\n\f]' +NEWLINE = r'(?:\r\n|(?!\r\n)[\n\f\r])' +WS = r'(?:[ \t]|{})'.format(NEWLINE) # Comments COMMENTS = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)' # Whitespace with comments included WSC = r'(?:{ws}|{comments})'.format(ws=WS, comments=COMMENTS) # CSS escapes -CSS_ESCAPES = r'(?:\\[a-f0-9]{{1,6}}{ws}?|\\[^\r\n\f])'.format(ws=WS) +CSS_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$))'.format(ws=WS) +CSS_STRING_ESCAPES = r'(?:\\(?:[a-f0-9]{{1,6}}{ws}?|[^\r\n\f]|$|{nl}))'.format(ws=WS, nl=NEWLINE) # CSS Identifier -IDENTIFIER = r'(?:(?!-?[0-9]|--)(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})+)'.format(esc=CSS_ESCAPES) +IDENTIFIER = r''' +(?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})+|--) +(?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{esc})*) +'''.format(esc=CSS_ESCAPES) # `nth` content NTH = r'(?:[-+])?(?:[0-9]+n?|n)(?:(?<=n){ws}*(?:[-+]){ws}*(?:[0-9]+))?'.format(ws=WSC) # Value: quoted string or identifier -VALUE = r'''(?:"(?:\\.|[^\\"]+)*?"|'(?:\\.|[^\\']+)*?'|{ident}+)'''.format(ident=IDENTIFIER) +VALUE = r''' +(?:"(?:\\(?:.|{nl})|[^\\"\r\n\f]+)*?"|'(?:\\(?:.|{nl})|[^\\'\r\n\f]+)*?'|{ident}+) +'''.format(nl=NEWLINE, ident=IDENTIFIER) # Attribute value comparison. `!=` is handled special as it is non-standard. ATTR = r''' (?:{ws}*(?P[!~^|*$]?=){ws}*(?P{value})(?:{ws}+(?P[is]))?)?{ws}*\] @@ -113,6 +122,10 @@ PAT_ATTR = r'\[{ws}*(?P(?:(?:{ident}|\*)?\|)?{ident}){attr}'.format(ws=WSC, ident=IDENTIFIER, attr=ATTR) # Pseudo class (`:pseudo-class`, `:pseudo-class(`) PAT_PSEUDO_CLASS = r'(?P:{ident})(?P\({ws}*)?'.format(ws=WSC, ident=IDENTIFIER) +# Pseudo class special patterns. Matches `:pseudo-class(` for special case pseudo classes. +PAT_PSEUDO_CLASS_SPECIAL = r'(?P:{ident})(?P\({ws}*)'.format(ws=WSC, ident=IDENTIFIER) +# Custom pseudo class (`:--custom-pseudo`) +PAT_PSEUDO_CLASS_CUSTOM = r'(?P:(?=--){ident})'.format(ident=IDENTIFIER) # Closing pseudo group (`)`) PAT_PSEUDO_CLOSE = r'{ws}*\)'.format(ws=WSC) # Pseudo element (`::pseudo-element`) @@ -121,37 +134,45 @@ PAT_AT_RULE = r'@P{ident}'.format(ident=IDENTIFIER) # Pseudo class `nth-child` (`:nth-child(an+b [of S]?)`, `:first-child`, etc.) PAT_PSEUDO_NTH_CHILD = r''' -(?P:nth-(?:last-)?child -\({ws}*(?P{nth}|even|odd))(?:{wsc}*\)|(?P{comments}*{ws}{wsc}*of{comments}*{ws}{wsc}*)) -'''.format(wsc=WSC, comments=COMMENTS, ws=WS, nth=NTH) +(?P{name} +(?P{nth}|even|odd))(?:{wsc}*\)|(?P{comments}*{ws}{wsc}*of{comments}*{ws}{wsc}*)) +'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, wsc=WSC, comments=COMMENTS, ws=WS, nth=NTH) # Pseudo class `nth-of-type` (`:nth-of-type(an+b)`, `:first-of-type`, etc.) PAT_PSEUDO_NTH_TYPE = r''' -(?P:nth-(?:last-)?of-type -\({ws}*(?P{nth}|even|odd)){ws}*\) -'''.format(ws=WSC, nth=NTH) +(?P{name} +(?P{nth}|even|odd)){ws}*\) +'''.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, nth=NTH) # Pseudo class language (`:lang("*-de", en)`) -PAT_PSEUDO_LANG = r':lang\({ws}*(?P{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format(ws=WSC, value=VALUE) +PAT_PSEUDO_LANG = r'{name}(?P{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format( + name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE +) # Pseudo class direction (`:dir(ltr)`) -PAT_PSEUDO_DIR = r':dir\({ws}*(?Pltr|rtl){ws}*\)'.format(ws=WSC) +PAT_PSEUDO_DIR = r'{name}(?Pltr|rtl){ws}*\)'.format(name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC) # Combining characters (`>`, `~`, ` `, `+`, `,`) -PAT_COMBINE = r'{ws}*?(?P[,+>~]|[ \t\r\n\f](?![,+>~])){ws}*'.format(ws=WSC) +PAT_COMBINE = r'{wsc}*?(?P[,+>~]|{ws}(?![,+>~])){wsc}*'.format(ws=WS, wsc=WSC) # Extra: Contains (`:contains(text)`) -PAT_PSEUDO_CONTAINS = r':contains\({ws}*(?P{value}){ws}*\)'.format(ws=WSC, value=VALUE) +PAT_PSEUDO_CONTAINS = r'{name}(?P{value}(?:{ws}*,{ws}*{value})*){ws}*\)'.format( + name=PAT_PSEUDO_CLASS_SPECIAL, ws=WSC, value=VALUE +) # Regular expressions # CSS escape pattern -RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f]))'.format(ws=WSC), re.I) +RE_CSS_ESC = re.compile(r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$))'.format(ws=WSC), re.I) +RE_CSS_STR_ESC = re.compile( + r'(?:(\\[a-f0-9]{{1,6}}{ws}?)|(\\[^\r\n\f])|(\\$)|(\\{nl}))'.format(ws=WS, nl=NEWLINE), re.I +) # Pattern to break up `nth` specifiers RE_NTH = re.compile( r'(?P[-+])?(?P[0-9]+n?|n)(?:(?<=n){ws}*(?P[-+]){ws}*(?P[0-9]+))?'.format(ws=WSC), re.I ) -# Pattern to iterate multiple languages. -RE_LANG = re.compile(r'(?:(?P{value})|(?P{ws}*,{ws}*))'.format(ws=WSC, value=VALUE), re.X) +# Pattern to iterate multiple values. +RE_VALUES = re.compile(r'(?:(?P{value})|(?P{ws}*,{ws}*))'.format(ws=WSC, value=VALUE), re.X) # Whitespace checks RE_WS = re.compile(WS) RE_WS_BEGIN = re.compile('^{}*'.format(WSC)) RE_WS_END = re.compile('{}*$'.format(WSC)) +RE_CUSTOM = re.compile(r'^{}$'.format(PAT_PSEUDO_CLASS_CUSTOM), re.X) # Constants # List split token @@ -169,19 +190,22 @@ FLG_OPEN = 0x40 FLG_IN_RANGE = 0x80 FLG_OUT_OF_RANGE = 0x100 +FLG_PLACEHOLDER_SHOWN = 0x200 # Maximum cached patterns to store _MAXCACHE = 500 @util.lru_cache(maxsize=_MAXCACHE) -def _cached_css_compile(pattern, namespaces, flags): +def _cached_css_compile(pattern, namespaces, custom, flags): """Cached CSS compile.""" + custom_selectors = process_custom(custom) return cm.SoupSieve( pattern, - CSSParser(pattern, flags).process_selectors(), + CSSParser(pattern, custom=custom_selectors, flags=flags).process_selectors(), namespaces, + custom, flags ) @@ -192,30 +216,142 @@ def _purge_cache(): _cached_css_compile.cache_clear() -def css_unescape(string): - """Unescape CSS value.""" +def process_custom(custom): + """Process custom.""" + + custom_selectors = {} + if custom is not None: + for key, value in custom.items(): + name = util.lower(key) + if RE_CUSTOM.match(name) is None: + raise SelectorSyntaxError("The name '{}' is not a valid custom pseudo-class name".format(name)) + if name in custom_selectors: + raise KeyError("The custom selector '{}' has already been registered".format(name)) + custom_selectors[css_unescape(name)] = value + return custom_selectors + + +def css_unescape(content, string=False): + """ + Unescape CSS value. + + Strings allow for spanning the value on multiple strings by escaping a new line. + """ def replace(m): """Replace with the appropriate substitute.""" - return util.uchr(int(m.group(1)[1:], 16)) if m.group(1) else m.group(2)[1:] - - return RE_CSS_ESC.sub(replace, string) + if m.group(1): + codepoint = int(m.group(1)[1:], 16) + if codepoint == 0: + codepoint = UNICODE_REPLACEMENT_CHAR + value = util.uchr(codepoint) + elif m.group(2): + value = m.group(2)[1:] + elif m.group(3): + value = '\ufffd' + else: + value = '' + + return value + + return (RE_CSS_ESC if not string else RE_CSS_STR_ESC).sub(replace, content) + + +def escape(ident): + """Escape identifier.""" + + string = [] + length = len(ident) + start_dash = length > 0 and ident[0] == '-' + if length == 1 and start_dash: + # Need to escape identifier that is a single `-` with no other characters + string.append('\\{}'.format(ident)) + else: + for index, c in enumerate(ident): + codepoint = util.uord(c) + if codepoint == 0x00: + string.append('\ufffd') + elif (0x01 <= codepoint <= 0x1F) or codepoint == 0x7F: + string.append('\\{:x} '.format(codepoint)) + elif (index == 0 or (start_dash and index == 1)) and (0x30 <= codepoint <= 0x39): + string.append('\\{:x} '.format(codepoint)) + elif ( + codepoint in (0x2D, 0x5F) or codepoint >= 0x80 or (0x30 <= codepoint <= 0x39) or + (0x30 <= codepoint <= 0x39) or (0x41 <= codepoint <= 0x5A) or (0x61 <= codepoint <= 0x7A) + ): + string.append(c) + else: + string.append('\\{}'.format(c)) + return ''.join(string) class SelectorPattern(object): """Selector pattern.""" - def __init__(self, pattern): + def __init__(self, name, pattern): + """Initialize.""" + + self.name = name + self.re_pattern = re.compile(pattern, re.I | re.X | re.U) + + def get_name(self): + """Get name.""" + + return self.name + + def enabled(self, flags): + """Enabled.""" + + return True + + def match(self, selector, index): + """Match the selector.""" + + return self.re_pattern.match(selector, index) + + +class SpecialPseudoPattern(SelectorPattern): + """Selector pattern.""" + + def __init__(self, patterns): """Initialize.""" - self.pattern = re.compile(pattern, re.I | re.X | re.U) + self.patterns = {} + for p in patterns: + name = p[0] + pattern = SelectorPattern(name, p[2]) + for pseudo in p[1]: + self.patterns[pseudo] = pattern + + self.matched_name = None + self.re_pseudo_name = re.compile(PAT_PSEUDO_CLASS_SPECIAL, re.I | re.X | re.U) + + def get_name(self): + """Get name.""" + + return self.matched_name.get_name() def enabled(self, flags): """Enabled.""" return True + def match(self, selector, index): + """Match the selector.""" + + pseudo = None + m = self.re_pseudo_name.match(selector, index) + if m: + name = util.lower(css_unescape(m.group('name'))) + pattern = self.patterns.get(name) + if pattern: + pseudo = pattern.match(selector, index) + if pseudo: + self.matched_name = pattern + + return pseudo + class _Selector(object): """ @@ -256,7 +392,7 @@ def freeze(self): """Freeze self.""" if self.no_match: - return ct.NullSelector() + return ct.SelectorNull() else: return ct.Selector( self.tag, @@ -289,105 +425,108 @@ def __str__(self): # pragma: no cover class CSSParser(object): """Parse CSS selectors.""" - css_tokens = OrderedDict( - [ - ("pseudo_close", SelectorPattern(PAT_PSEUDO_CLOSE)), - ("pseudo_contains", SelectorPattern(PAT_PSEUDO_CONTAINS)), - ("pseudo_nth_child", SelectorPattern(PAT_PSEUDO_NTH_CHILD)), - ("pseudo_nth_type", SelectorPattern(PAT_PSEUDO_NTH_TYPE)), - ("pseudo_lang", SelectorPattern(PAT_PSEUDO_LANG)), - ("pseudo_dir", SelectorPattern(PAT_PSEUDO_DIR)), - ("pseudo_class", SelectorPattern(PAT_PSEUDO_CLASS)), - ("pseudo_element", SelectorPattern(PAT_PSEUDO_ELEMENT)), - ("at_rule", SelectorPattern(PAT_AT_RULE)), - ("id", SelectorPattern(PAT_ID)), - ("class", SelectorPattern(PAT_CLASS)), - ("tag", SelectorPattern(PAT_TAG)), - ("attribute", SelectorPattern(PAT_ATTR)), - ("combine", SelectorPattern(PAT_COMBINE)) - ] + css_tokens = ( + SelectorPattern("pseudo_close", PAT_PSEUDO_CLOSE), + SpecialPseudoPattern( + ( + ("pseudo_contains", (':contains',), PAT_PSEUDO_CONTAINS), + ("pseudo_nth_child", (':nth-child', ':nth-last-child'), PAT_PSEUDO_NTH_CHILD), + ("pseudo_nth_type", (':nth-of-type', ':nth-last-of-type'), PAT_PSEUDO_NTH_TYPE), + ("pseudo_lang", (':lang',), PAT_PSEUDO_LANG), + ("pseudo_dir", (':dir',), PAT_PSEUDO_DIR) + ) + ), + SelectorPattern("pseudo_class_custom", PAT_PSEUDO_CLASS_CUSTOM), + SelectorPattern("pseudo_class", PAT_PSEUDO_CLASS), + SelectorPattern("pseudo_element", PAT_PSEUDO_ELEMENT), + SelectorPattern("at_rule", PAT_AT_RULE), + SelectorPattern("id", PAT_ID), + SelectorPattern("class", PAT_CLASS), + SelectorPattern("tag", PAT_TAG), + SelectorPattern("attribute", PAT_ATTR), + SelectorPattern("combine", PAT_COMBINE) ) - def __init__(self, selector, flags=0): + def __init__(self, selector, custom=None, flags=0): """Initialize.""" - self.pattern = selector + self.pattern = selector.replace('\x00', '\ufffd') self.flags = flags self.debug = self.flags & util.DEBUG + self.custom = {} if custom is None else custom def parse_attribute_selector(self, sel, m, has_selector): """Create attribute selector from the returned regex match.""" + inverse = False op = m.group('cmp') - if op and op.startswith('!'): - # Equivalent to `:not([attr=value])` - attr = m.group('ns_attr') - value = m.group('value') - case = m.group('case') - if not case: - case = '' - sel.selectors.append( - self.parse_selectors( - # Simulate the content of `:not`, but make the attribute as `=` instead of `!=`. - self.selector_iter('[{}={} {}]'.format(attr, value, case)), - m.end(0), - FLG_PSEUDO | FLG_NOT - ) - ) - has_selector = True + case = util.lower(m.group('case')) if m.group('case') else None + parts = [css_unescape(a) for a in m.group('ns_attr').split('|')] + ns = '' + is_type = False + pattern2 = None + if len(parts) > 1: + ns = parts[0] + attr = parts[1] else: - case = util.lower(m.group('case')) if m.group('case') else None - parts = [css_unescape(a) for a in m.group('ns_attr').split('|')] - ns = '' - is_type = False - pattern2 = None - if len(parts) > 1: - ns = parts[0] - attr = parts[1] - else: - attr = parts[0] - if case: - flags = re.I if case == 'i' else 0 - elif util.lower(attr) == 'type': - flags = re.I - is_type = True - else: - flags = 0 + attr = parts[0] + if case: + flags = re.I if case == 'i' else 0 + elif util.lower(attr) == 'type': + flags = re.I + is_type = True + else: + flags = 0 - if op: - value = css_unescape( - m.group('value')[1:-1] if m.group('value').startswith(('"', "'")) else m.group('value') - ) + if op: + if m.group('value').startswith(('"', "'")): + value = css_unescape(m.group('value')[1:-1], True) else: - value = None - if not op: - # Attribute name - pattern = None - elif op.startswith('^'): - # Value start with - pattern = re.compile(r'^%s.*' % re.escape(value), flags) - elif op.startswith('$'): - # Value ends with - pattern = re.compile(r'.*?%s$' % re.escape(value), flags) - elif op.startswith('*'): - # Value contains - pattern = re.compile(r'.*?%s.*' % re.escape(value), flags) - elif op.startswith('~'): - # Value contains word within space separated list - # `~=` should match nothing if it is empty or contains whitespace, - # so if either of these cases is present, use `[^\s\S]` which cannot be matched. - value = r'[^\s\S]' if not value or RE_WS.search(value) else re.escape(value) - pattern = re.compile(r'.*?(?:(?<=^)|(?<=[ \t\r\n\f]))%s(?=(?:[ \t\r\n\f]|$)).*' % value, flags) - elif op.startswith('|'): - # Value starts with word in dash separated list - pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags) - else: - # Value matches - pattern = re.compile(r'^%s$' % re.escape(value), flags) - if is_type and pattern: - pattern2 = re.compile(pattern.pattern) - has_selector = True - sel.attributes.append(ct.SelectorAttribute(attr, ns, pattern, pattern2)) + value = css_unescape(m.group('value')) + else: + value = None + if not op: + # Attribute name + pattern = None + elif op.startswith('^'): + # Value start with + pattern = re.compile(r'^%s.*' % re.escape(value), flags) + elif op.startswith('$'): + # Value ends with + pattern = re.compile(r'.*?%s$' % re.escape(value), flags) + elif op.startswith('*'): + # Value contains + pattern = re.compile(r'.*?%s.*' % re.escape(value), flags) + elif op.startswith('~'): + # Value contains word within space separated list + # `~=` should match nothing if it is empty or contains whitespace, + # so if either of these cases is present, use `[^\s\S]` which cannot be matched. + value = r'[^\s\S]' if not value or RE_WS.search(value) else re.escape(value) + pattern = re.compile(r'.*?(?:(?<=^)|(?<=[ \t\r\n\f]))%s(?=(?:[ \t\r\n\f]|$)).*' % value, flags) + elif op.startswith('|'): + # Value starts with word in dash separated list + pattern = re.compile(r'^%s(?:-.*)?$' % re.escape(value), flags) + else: + # Value matches + pattern = re.compile(r'^%s$' % re.escape(value), flags) + if op.startswith('!'): + # Equivalent to `:not([attr=value])` + inverse = True + if is_type and pattern: + pattern2 = re.compile(pattern.pattern) + + # Append the attribute selector + sel_attr = ct.SelectorAttribute(attr, ns, pattern, pattern2) + if inverse: + # If we are using `!=`, we need to nest the pattern under a `:not()`. + sub_sel = _Selector() + sub_sel.attributes.append(sel_attr) + not_list = ct.SelectorList([sub_sel.freeze()], True, False) + sel.selectors.append(not_list) + else: + sel.attributes.append(sel_attr) + + has_selector = True return has_selector def parse_tag_pattern(self, sel, m, has_selector): @@ -404,11 +543,39 @@ def parse_tag_pattern(self, sel, m, has_selector): has_selector = True return has_selector + def parse_pseudo_class_custom(self, sel, m, has_selector): + """ + Parse custom pseudo class alias. + + Compile custom selectors as we need them. When compiling a custom selector, + set it to `None` in the dictionary so we can avoid an infinite loop. + """ + + pseudo = util.lower(css_unescape(m.group('name'))) + selector = self.custom.get(pseudo) + if selector is None: + raise SelectorSyntaxError( + "Undefined custom selector '{}' found at postion {}".format(pseudo, m.end(0)), + self.pattern, + m.end(0) + ) + + if not isinstance(selector, ct.SelectorList): + self.custom[pseudo] = None + selector = CSSParser( + selector, custom=self.custom, flags=self.flags + ).process_selectors(flags=FLG_PSEUDO) + self.custom[pseudo] = selector + + sel.selectors.append(selector) + has_selector = True + return has_selector + def parse_pseudo_class(self, sel, m, has_selector, iselector, is_html): """Parse pseudo class.""" complex_pseudo = False - pseudo = util.lower(m.group('name')) + pseudo = util.lower(css_unescape(m.group('name'))) if m.group('open'): complex_pseudo = True if complex_pseudo and pseudo in PSEUDO_COMPLEX: @@ -480,7 +647,11 @@ def parse_pseudo_class(self, sel, m, has_selector, iselector, is_html): sel.no_match = True has_selector = True elif pseudo in PSEUDO_SUPPORTED: - raise SyntaxError("Invalid syntax for pseudo class '{}'".format(pseudo)) + raise SelectorSyntaxError( + "Invalid syntax for pseudo class '{}'".format(pseudo), + self.pattern, + m.start(0) + ) else: raise NotImplementedError( "'{}' pseudo-class is not implemented at this time".format(pseudo) @@ -492,13 +663,19 @@ def parse_pseudo_nth(self, sel, m, has_selector, iselector): """Parse `nth` pseudo.""" mdict = m.groupdict() - postfix = '_child' if mdict.get('pseudo_nth_child') else '_type' - content = mdict.get('nth' + postfix) + if mdict.get('pseudo_nth_child'): + postfix = '_child' + else: + postfix = '_type' + mdict['name'] = util.lower(css_unescape(mdict['name'])) + content = util.lower(mdict.get('nth' + postfix)) if content == 'even': + # 2n s1 = 2 - s2 = 2 + s2 = 0 var = True elif content == 'odd': + # 2n+1 s1 = 2 s2 = 1 var = True @@ -521,7 +698,7 @@ def parse_pseudo_nth(self, sel, m, has_selector, iselector): s1 = int(s1, 10) s2 = int(s2, 10) - pseudo_sel = util.lower(m.group('pseudo_nth' + postfix)) + pseudo_sel = mdict['name'] if postfix == '_child': if m.group('of'): # Parse the rest of `of S`. @@ -529,14 +706,14 @@ def parse_pseudo_nth(self, sel, m, has_selector, iselector): else: # Use default `*|*` for `of S`. nth_sel = CSS_NTH_OF_S_DEFAULT - if pseudo_sel.startswith(':nth-child'): + if pseudo_sel == ':nth-child': sel.nth.append(ct.SelectorNth(s1, var, s2, False, False, nth_sel)) - elif pseudo_sel.startswith(':nth-last-child'): + elif pseudo_sel == ':nth-last-child': sel.nth.append(ct.SelectorNth(s1, var, s2, False, True, nth_sel)) else: - if pseudo_sel.startswith(':nth-of-type'): + if pseudo_sel == ':nth-of-type': sel.nth.append(ct.SelectorNth(s1, var, s2, True, False, ct.SelectorList())) - elif pseudo_sel.startswith(':nth-last-of-type'): + elif pseudo_sel == ':nth-last-of-type': sel.nth.append(ct.SelectorNth(s1, var, s2, True, True, ct.SelectorList())) has_selector = True return has_selector @@ -562,8 +739,12 @@ def parse_has_combinator(self, sel, m, has_selector, selectors, rel_type, index) combinator = WS_COMBINATOR if combinator == COMMA_COMBINATOR: if not has_selector: - raise SyntaxError( - "The combinator '{}' at postion {}, must have a selector before it".format(combinator, index) + # If we've not captured any selector parts, the comma is either at the beginning of the pattern + # or following another comma, both of which are unexpected. Commas must split selectors. + raise SelectorSyntaxError( + "The combinator '{}' at postion {}, must have a selector before it".format(combinator, index), + self.pattern, + index ) sel.rel_type = rel_type selectors[-1].relations.append(sel) @@ -571,8 +752,21 @@ def parse_has_combinator(self, sel, m, has_selector, selectors, rel_type, index) selectors.append(_Selector()) else: if has_selector: + # End the current selector and associate the leading combinator with this selector. sel.rel_type = rel_type selectors[-1].relations.append(sel) + elif rel_type[1:] != WS_COMBINATOR: + # It's impossible to have two whitespace combinators after each other as the patterns + # will gobble up trailing whitespace. It is also impossible to have a whitespace + # combinator after any other kind for the same reason. But we could have + # multiple non-whitespace combinators. So if the current combinator is not a whitespace, + # then we've hit the multiple combinator case, so we should fail. + raise SelectorSyntaxError( + 'The multiple combinators at position {}'.format(index), + self.pattern, + index + ) + # Set the leading combinator for the next selector. rel_type = ':' + combinator sel = _Selector() @@ -586,9 +780,12 @@ def parse_combinator(self, sel, m, has_selector, selectors, relations, is_pseudo if not combinator: combinator = WS_COMBINATOR if not has_selector: - raise SyntaxError( - "The combinator '{}' at postion {}, must have a selector before it".format(combinator, index) + raise SelectorSyntaxError( + "The combinator '{}' at postion {}, must have a selector before it".format(combinator, index), + self.pattern, + index ) + if combinator == COMMA_COMBINATOR: if not sel.tag and not is_pseudo: # Implied `*` @@ -612,46 +809,45 @@ def parse_class_id(self, sel, m, has_selector): selector = m.group(0) if selector.startswith('.'): sel.classes.append(css_unescape(selector[1:])) - has_selector = True else: sel.ids.append(css_unescape(selector[1:])) - has_selector = True + has_selector = True return has_selector def parse_pseudo_contains(self, sel, m, has_selector): """Parse contains.""" - content = m.group('value') - if content.startswith(("'", '"')): - content = content[1:-1] - content = css_unescape(content) - sel.contains.append(content) + values = m.group('values') + patterns = [] + for token in RE_VALUES.finditer(values): + if token.group('split'): + continue + value = token.group('value') + if value.startswith(("'", '"')): + value = css_unescape(value[1:-1], True) + else: + value = css_unescape(value) + patterns.append(value) + sel.contains.append(ct.SelectorContains(tuple(patterns))) has_selector = True return has_selector def parse_pseudo_lang(self, sel, m, has_selector): """Parse pseudo language.""" - lang = m.group('lang') + values = m.group('values') patterns = [] - for token in RE_LANG.finditer(lang): + for token in RE_VALUES.finditer(values): if token.group('split'): continue value = token.group('value') if value.startswith(('"', "'")): - value = value[1:-1] - parts = css_unescape(value).split('-') - - new_parts = [] - first = True - for part in parts: - if part == '*' and first: - new_parts.append('(?!x\b)[a-z0-9]+?') - elif part != '*': - new_parts.append(('' if first else '(-(?!x\b)[a-z0-9]+)*?\\-') + re.escape(part)) - if first: - first = False - patterns.append(re.compile(r'^{}(?:-.*)?$'.format(''.join(new_parts)), re.I)) + value = css_unescape(value[1:-1], True) + else: + value = css_unescape(value) + + patterns.append(value) + sel.lang.append(ct.SelectorLang(patterns)) has_selector = True @@ -674,16 +870,16 @@ def parse_selectors(self, iselector, index=0, flags=0): closed = False relations = [] rel_type = ":" + WS_COMBINATOR - split_last = False - is_open = flags & FLG_OPEN - is_pseudo = flags & FLG_PSEUDO - is_relative = flags & FLG_RELATIVE - is_not = flags & FLG_NOT - is_html = flags & FLG_HTML - is_default = flags & FLG_DEFAULT - is_indeterminate = flags & FLG_INDETERMINATE - is_in_range = flags & FLG_IN_RANGE - is_out_of_range = flags & FLG_OUT_OF_RANGE + is_open = bool(flags & FLG_OPEN) + is_pseudo = bool(flags & FLG_PSEUDO) + is_relative = bool(flags & FLG_RELATIVE) + is_not = bool(flags & FLG_NOT) + is_html = bool(flags & FLG_HTML) + is_default = bool(flags & FLG_DEFAULT) + is_indeterminate = bool(flags & FLG_INDETERMINATE) + is_in_range = bool(flags & FLG_IN_RANGE) + is_out_of_range = bool(flags & FLG_OUT_OF_RANGE) + is_placeholder_shown = bool(flags & FLG_PLACEHOLDER_SHOWN) if self.debug: # pragma: no cover if is_pseudo: @@ -704,6 +900,8 @@ def parse_selectors(self, iselector, index=0, flags=0): print(' is_in_range: True') if is_out_of_range: print(' is_out_of_range: True') + if is_placeholder_shown: + print(' is_placeholder_shown: True') if is_relative: selectors.append(_Selector()) @@ -715,6 +913,8 @@ def parse_selectors(self, iselector, index=0, flags=0): # Handle parts if key == "at_rule": raise NotImplementedError("At-rules found at position {}".format(m.start(0))) + elif key == 'pseudo_class_custom': + has_selector = self.parse_pseudo_class_custom(sel, m, has_selector) elif key == 'pseudo_class': has_selector, is_html = self.parse_pseudo_class(sel, m, has_selector, iselector, is_html) elif key == 'pseudo_element': @@ -730,16 +930,22 @@ def parse_selectors(self, iselector, index=0, flags=0): # Currently only supports HTML is_html = True elif key == 'pseudo_close': - if split_last: - raise SyntaxError("Expected a selector at postion {}".format(m.start(0))) + if not has_selector: + raise SelectorSyntaxError( + "Expected a selector at postion {}".format(m.start(0)), + self.pattern, + m.start(0) + ) if is_open: closed = True break else: - raise SyntaxError("Unmatched pseudo-class close at postion {}".format(m.start(0))) + raise SelectorSyntaxError( + "Unmatched pseudo-class close at postion {}".format(m.start(0)), + self.pattern, + m.start(0) + ) elif key == 'combine': - if split_last: - raise SyntaxError("Unexpected combinator at position {}".format(m.start(0))) if is_relative: has_selector, sel, rel_type = self.parse_has_combinator( sel, m, has_selector, selectors, rel_type, index @@ -748,28 +954,29 @@ def parse_selectors(self, iselector, index=0, flags=0): has_selector, sel = self.parse_combinator( sel, m, has_selector, selectors, relations, is_pseudo, index ) - split_last = True - index = m.end(0) - continue elif key == 'attribute': has_selector = self.parse_attribute_selector(sel, m, has_selector) elif key == 'tag': if has_selector: - raise SyntaxError("Tag name found at position {} instead of at the start".format(m.start(0))) + raise SelectorSyntaxError( + "Tag name found at position {} instead of at the start".format(m.start(0)), + self.pattern, + m.start(0) + ) has_selector = self.parse_tag_pattern(sel, m, has_selector) elif key in ('class', 'id'): has_selector = self.parse_class_id(sel, m, has_selector) - split_last = False index = m.end(0) except StopIteration: pass if is_open and not closed: - raise SyntaxError("Unclosed pseudo-class at position {}".format(index)) - - if split_last: - raise SyntaxError("Expected a selector at position {}".format(index)) + raise SelectorSyntaxError( + "Unclosed pseudo-class at position {}".format(index), + self.pattern, + index + ) if has_selector: if not sel.tag and not is_pseudo: @@ -782,9 +989,13 @@ def parse_selectors(self, iselector, index=0, flags=0): sel.relations.extend(relations) del relations[:] selectors.append(sel) - elif is_relative: + else: # We will always need to finish a selector when `:has()` is used as it leads with combining. - raise SyntaxError('Expected a selector at position {}'.format(index)) + raise SelectorSyntaxError( + 'Expected a selector at position {}'.format(index), + self.pattern, + index + ) # Some patterns require additional logic, such as default. We try to make these the # last pattern, and append the appropriate flag to that selector which communicates @@ -797,6 +1008,8 @@ def parse_selectors(self, iselector, index=0, flags=0): selectors[-1].flags = ct.SEL_IN_RANGE if is_out_of_range: selectors[-1].flags = ct.SEL_OUT_OF_RANGE + if is_placeholder_shown: + selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html) @@ -813,20 +1026,21 @@ def selector_iter(self, pattern): print('## PARSING: {!r}'.format(pattern)) while index <= end: m = None - for k, v in self.css_tokens.items(): + for v in self.css_tokens: if not v.enabled(self.flags): # pragma: no cover continue - m = v.pattern.match(pattern, index) + m = v.match(pattern, index) if m: + name = v.get_name() if self.debug: # pragma: no cover - print("TOKEN: '{}' --> {!r} at position {}".format(k, m.group(0), m.start(0))) + print("TOKEN: '{}' --> {!r} at position {}".format(name, m.group(0), m.start(0))) index = m.end(0) - yield k, m + yield name, m break if m is None: c = pattern[index] # If the character represents the start of one of the known selector types, - # throw an exception mentions that the known selector type in error; + # throw an exception mentioning that the known selector type is in error; # otherwise, report the invalid character. if c == '[': msg = "Malformed attribute selector at position {}".format(index) @@ -838,18 +1052,12 @@ def selector_iter(self, pattern): msg = "Malformed pseudo-class selector at position {}".format(index) else: msg = "Invalid character {!r} position {}".format(c, index) - raise SyntaxError(msg) + raise SelectorSyntaxError(msg, self.pattern, index) if self.debug: # pragma: no cover print('## END PARSING') def process_selectors(self, index=0, flags=0): - """ - Process selectors. - - We do our own selectors as BeautifulSoup4 has some annoying quirks, - and we don't really need to do nth selectors or siblings or - descendants etc. - """ + """Process selectors.""" return self.parse_selectors(self.selector_iter(self.pattern), index, flags) @@ -859,13 +1067,12 @@ def process_selectors(self, index=0, flags=0): # CSS pattern for `:link` and `:any-link` CSS_LINK = CSSParser( - ':is(a, area, link)[href]' + 'html|*:is(a, area, link)[href]' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:checked` CSS_CHECKED = CSSParser( ''' - :is(input[type=checkbox], input[type=radio])[checked], - select > option[selected] + html|*:is(input[type=checkbox], input[type=radio])[checked], html|option[selected] ''' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:default` (must compile CSS_CHECKED first) @@ -877,71 +1084,64 @@ def process_selectors(self, index=0, flags=0): This pattern must be at the end. Special logic is applied to the last selector. */ - form :is(button, input)[type="submit"] + html|form html|*:is(button, input)[type="submit"] ''' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_DEFAULT) # CSS pattern for `:indeterminate` CSS_INDETERMINATE = CSSParser( ''' - input[type="checkbox"][indeterminate], - input[type="radio"]:is(:not([name]), [name=""]):not([checked]), - progress:not([value]), + html|input[type="checkbox"][indeterminate], + html|input[type="radio"]:is(:not([name]), [name=""]):not([checked]), + html|progress:not([value]), /* This pattern must be at the end. Special logic is applied to the last selector. */ - input[type="radio"][name][name!='']:not([checked]) + html|input[type="radio"][name][name!='']:not([checked]) ''' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_INDETERMINATE) # CSS pattern for `:disabled` CSS_DISABLED = CSSParser( ''' - :is(input[type!=hidden], button, select, textarea, fieldset, optgroup, option)[disabled], - optgroup[disabled] > option, - fieldset[disabled] > :not(legend) :is(input[type!=hidden], button, select, textarea), - fieldset[disabled] > :is(input[type!=hidden], button, select, textarea) + html|*:is(input[type!=hidden], button, select, textarea, fieldset, optgroup, option, fieldset)[disabled], + html|optgroup[disabled] > html|option, + html|fieldset[disabled] > html|*:is(input[type!=hidden], button, select, textarea, fieldset), + html|fieldset[disabled] > + html|*:not(legend:nth-of-type(1)) html|*:is(input[type!=hidden], button, select, textarea, fieldset) ''' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:enabled` CSS_ENABLED = CSSParser( ''' - :is(a, area, link)[href], - :is(fieldset, optgroup):not([disabled]), - option:not(optgroup[disabled] *):not([disabled]), - :is(input[type!=hidden], button, select, textarea):not( - fieldset[disabled] > :not(legend) *, - fieldset[disabled] > * - ):not([disabled]) + html|*:is(input[type!=hidden], button, select, textarea, fieldset, optgroup, option, fieldset):not(:disabled) ''' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:required` CSS_REQUIRED = CSSParser( - ':is(input, textarea, select)[required]' + 'html|*:is(input, textarea, select)[required]' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:optional` CSS_OPTIONAL = CSSParser( - ':is(input, textarea, select):not([required])' + 'html|*:is(input, textarea, select):not([required])' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:placeholder-shown` CSS_PLACEHOLDER_SHOWN = CSSParser( ''' - :is( - input:is( - :not([type]), - [type=""], - [type=text], - [type=search], - [type=url], - [type=tel], - [type=email], - [type=password], - [type=number] - ), - textarea - )[placeholder][placeholder!=''] + html|input:is( + :not([type]), + [type=""], + [type=text], + [type=search], + [type=url], + [type=tel], + [type=email], + [type=password], + [type=number] + )[placeholder][placeholder!='']:is(:not([value]), [value=""]), + html|textarea[placeholder][placeholder!=''] ''' -).process_selectors(flags=FLG_PSEUDO | FLG_HTML) +).process_selectors(flags=FLG_PSEUDO | FLG_HTML | FLG_PLACEHOLDER_SHOWN) # CSS pattern default for `:nth-child` "of S" feature CSS_NTH_OF_S_DEFAULT = CSSParser( '*|*' @@ -949,7 +1149,7 @@ def process_selectors(self, index=0, flags=0): # CSS pattern for `:read-write` (CSS_DISABLED must be compiled first) CSS_READ_WRITE = CSSParser( ''' - :is( + html|*:is( textarea, input:is( :not([type]), @@ -968,19 +1168,19 @@ def process_selectors(self, index=0, flags=0): [type=week] ) ):not([readonly], :disabled), - :is([contenteditable=""], [contenteditable="true" i]) + html|*:is([contenteditable=""], [contenteditable="true" i]) ''' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:read-only` CSS_READ_ONLY = CSSParser( ''' - :not(:read-write) + html|*:not(:read-write) ''' ).process_selectors(flags=FLG_PSEUDO | FLG_HTML) # CSS pattern for `:in-range` CSS_IN_RANGE = CSSParser( ''' - input:is( + html|input:is( [type="date"], [type="month"], [type="week"], @@ -997,7 +1197,7 @@ def process_selectors(self, index=0, flags=0): # CSS pattern for `:out-of-range` CSS_OUT_OF_RANGE = CSSParser( ''' - input:is( + html|input:is( [type="date"], [type="month"], [type="week"], diff --git a/ext/soupsieve/css_types.py b/ext/soupsieve/css_types.py index 457bea5354..e4baef37a6 100644 --- a/ext/soupsieve/css_types.py +++ b/ext/soupsieve/css_types.py @@ -4,13 +4,15 @@ __all__ = ( 'Selector', - 'NullSelector', + 'SelectorNull', 'SelectorTag', 'SelectorAttribute', + 'SelectorContains', 'SelectorNth', 'SelectorLang', 'SelectorList', - 'Namespaces' + 'Namespaces', + 'CustomSelectors' ) @@ -24,6 +26,7 @@ SEL_IN_RANGE = 0x80 SEL_OUT_OF_RANGE = 0x100 SEL_DEFINED = 0x200 +SEL_PLACEHOLDER_SHOWN = 0x400 class Immutable(object): @@ -146,6 +149,25 @@ def __init__(self, *args, **kwargs): super(Namespaces, self).__init__(*args, **kwargs) +class CustomSelectors(ImmutableDict): + """Custom selectors.""" + + def __init__(self, *args, **kwargs): + """Initialize.""" + + # If there are arguments, check the first index. + # `super` should fail if the user gave multiple arguments, + # so don't bother checking that. + arg = args[0] if args else kwargs + is_dict = isinstance(arg, dict) + if is_dict and not all([isinstance(k, util.string) and isinstance(v, util.string) for k, v in arg.items()]): + raise TypeError('CustomSelectors keys and values must be Unicode strings') + elif not is_dict and not all([isinstance(k, util.string) and isinstance(v, util.string) for k, v in arg]): + raise TypeError('CustomSelectors keys and values must be Unicode strings') + + super(CustomSelectors, self).__init__(*args, **kwargs) + + class Selector(Immutable): """Selector.""" @@ -175,13 +197,13 @@ def __init__( ) -class NullSelector(Immutable): +class SelectorNull(Immutable): """Null Selector.""" def __init__(self): """Initialize.""" - super(NullSelector, self).__init__() + super(SelectorNull, self).__init__() class SelectorTag(Immutable): @@ -214,6 +236,19 @@ def __init__(self, attribute, prefix, pattern, xml_type_pattern): ) +class SelectorContains(Immutable): + """Selector contains rule.""" + + __slots__ = ("text", "_hash") + + def __init__(self, text): + """Initialize.""" + + super(SelectorContains, self).__init__( + text=text + ) + + class SelectorNth(Immutable): """Selector nth type.""" @@ -301,9 +336,10 @@ def pickle_register(obj): pickle_register(Selector) -pickle_register(NullSelector) +pickle_register(SelectorNull) pickle_register(SelectorTag) pickle_register(SelectorAttribute) +pickle_register(SelectorContains) pickle_register(SelectorNth) pickle_register(SelectorLang) pickle_register(SelectorList) diff --git a/ext/soupsieve/util.py b/ext/soupsieve/util.py index 260965c07a..6158367a59 100644 --- a/ext/soupsieve/util.py +++ b/ext/soupsieve/util.py @@ -4,29 +4,36 @@ import warnings import sys import struct +import os +import re +MODULE = os.path.dirname(__file__) PY3 = sys.version_info >= (3, 0) +PY35 = sys.version_info >= (3, 5) +PY37 = sys.version_info >= (3, 7) if PY3: from functools import lru_cache # noqa F401 import copyreg # noqa F401 from collections.abc import Hashable, Mapping # noqa F401 - ustr = str # noqa - bstr = bytes # noqa - unichar = chr # noqa - string = str # noqa + ustr = str + bstr = bytes + unichar = chr + string = str else: from backports.functools_lru_cache import lru_cache # noqa F401 import copy_reg as copyreg # noqa F401 from collections import Hashable, Mapping # noqa F401 - ustr = unicode # noqa - bstr = str # noqa - unichar = unichr # noqa - string = basestring # noqa + ustr = unicode # noqa: F821 + bstr = str + unichar = unichr # noqa: F821 + string = basestring # noqa: F821 -DEBUG = 0x10000 +DEBUG = 0x00001 + +RE_PATTERN_LINE_SPLIT = re.compile(r'(?:\r\n|(?!\r\n)[\n\r])|$') LC_A = ord('a') LC_Z = ord('z') @@ -34,69 +41,6 @@ UC_Z = ord('Z') -def is_doc(obj): - """Is `BeautifulSoup` object.""" - - import bs4 - return isinstance(obj, bs4.BeautifulSoup) - - -def is_tag(obj): - """Is tag.""" - - import bs4 - return isinstance(obj, bs4.Tag) - - -def is_comment(obj): - """Is comment.""" - - import bs4 - return isinstance(obj, bs4.Comment) - - -def is_declaration(obj): # pragma: no cover - """Is declaration.""" - - import bs4 - return isinstance(obj, bs4.Declaration) - - -def is_cdata(obj): # pragma: no cover - """Is CDATA.""" - - import bs4 - return isinstance(obj, bs4.Declaration) - - -def is_processing_instruction(obj): # pragma: no cover - """Is processing instruction.""" - - import bs4 - return isinstance(obj, bs4.ProcessingInstruction) - - -def is_navigable_string(obj): - """Is navigable string.""" - - import bs4 - return isinstance(obj, bs4.NavigableString) - - -def is_special_string(obj): - """Is special string.""" - - import bs4 - return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction)) - - -def get_navigable_string_type(obj): - """Get navigable string type.""" - - import bs4 - return bs4.NavigableString - - def lower(string): """Lower.""" @@ -126,6 +70,36 @@ def uchr(i): return struct.pack('i', i).decode('utf-32') +def uord(c): + """Get Unicode ordinal.""" + + if len(c) == 2: # pragma: no cover + high, low = [ord(p) for p in c] + ordinal = (high - 0xD800) * 0x400 + low - 0xDC00 + 0x10000 + else: + ordinal = ord(c) + + return ordinal + + +class SelectorSyntaxError(SyntaxError): + """Syntax error in a CSS selector.""" + + def __init__(self, msg, pattern=None, index=None): + """Initialize.""" + + self.line = None + self.col = None + self.context = None + + if pattern is not None and index is not None: + # Format pattern to show line and column position + self.context, self.line, self.col = get_pattern_context(pattern, index) + msg = '{}\n line {}:\n{}'.format(msg, self.line, self.context) + + super(SelectorSyntaxError, self).__init__(msg) + + def deprecated(message, stacklevel=2): # pragma: no cover """ Raise a `DeprecationWarning` when wrapped function/method is called. @@ -154,3 +128,43 @@ def warn_deprecated(message, stacklevel=2): # pragma: no cover category=DeprecationWarning, stacklevel=stacklevel ) + + +def get_pattern_context(pattern, index): + """Get the pattern context.""" + + last = 0 + current_line = 1 + col = 1 + text = [] + line = 1 + + # Split pattern by newline and handle the text before the newline + for m in RE_PATTERN_LINE_SPLIT.finditer(pattern): + linetext = pattern[last:m.start(0)] + if not len(m.group(0)) and not len(text): + indent = '' + offset = -1 + col = index - last + 1 + elif last <= index < m.end(0): + indent = '--> ' + offset = (-1 if index > m.start(0) else 0) + 3 + col = index - last + 1 + else: + indent = ' ' + offset = None + if len(text): + # Regardless of whether we are presented with `\r\n`, `\r`, or `\n`, + # we will render the output with just `\n`. We will still log the column + # correctly though. + text.append('\n') + text.append('{}{}'.format(indent, linetext)) + if offset is not None: + text.append('\n') + text.append(' ' * (col + offset) + '^') + line = current_line + + current_line += 1 + last = m.end(0) + + return ''.join(text), line, col diff --git a/ext2/backports/__init__.py b/ext2/backports/__init__.py index febdb2f820..69e3be50da 100644 --- a/ext2/backports/__init__.py +++ b/ext2/backports/__init__.py @@ -1,5 +1 @@ -# A Python "namespace package" http://www.python.org/dev/peps/pep-0382/ -# This always goes inside of a namespace package's __init__.py - -from pkgutil import extend_path -__path__ = extend_path(__path__, __name__) +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/ext2/backports/functools_lru_cache.py b/ext2/backports/functools_lru_cache.py index 707c6c766d..e0b19d951a 100644 --- a/ext2/backports/functools_lru_cache.py +++ b/ext2/backports/functools_lru_cache.py @@ -8,10 +8,12 @@ @functools.wraps(functools.update_wrapper) -def update_wrapper(wrapper, - wrapped, - assigned = functools.WRAPPER_ASSIGNMENTS, - updated = functools.WRAPPER_UPDATES): +def update_wrapper( + wrapper, + wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES, +): """ Patch two bugs in functools.update_wrapper. """ @@ -34,10 +36,17 @@ def __hash__(self): return self.hashvalue -def _make_key(args, kwds, typed, - kwd_mark=(object(),), - fasttypes=set([int, str, frozenset, type(None)]), - sorted=sorted, tuple=tuple, type=type, len=len): +def _make_key( + args, + kwds, + typed, + kwd_mark=(object(),), + fasttypes=set([int, str, frozenset, type(None)]), + sorted=sorted, + tuple=tuple, + type=type, + len=len, +): 'Make a cache key from optionally typed positional and keyword arguments' key = args if kwds: @@ -82,16 +91,16 @@ def lru_cache(maxsize=100, typed=False): def decorating_function(user_function): cache = dict() - stats = [0, 0] # make statistics updateable non-locally - HITS, MISSES = 0, 1 # names for the stats fields + stats = [0, 0] # make statistics updateable non-locally + HITS, MISSES = 0, 1 # names for the stats fields make_key = _make_key - cache_get = cache.get # bound method to lookup key or return None - _len = len # localize the global len() function - lock = RLock() # because linkedlist updates aren't threadsafe - root = [] # root of the circular doubly linked list - root[:] = [root, root, None, None] # initialize by pointing to self - nonlocal_root = [root] # make updateable non-locally - PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields + cache_get = cache.get # bound method to lookup key or return None + _len = len # localize the global len() function + lock = RLock() # because linkedlist updates aren't threadsafe + root = [] # root of the circular doubly linked list + root[:] = [root, root, None, None] # initialize by pointing to self + nonlocal_root = [root] # make updateable non-locally + PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields if maxsize == 0: @@ -106,7 +115,9 @@ def wrapper(*args, **kwds): def wrapper(*args, **kwds): # simple caching without ordering or size limit key = make_key(args, kwds, typed) - result = cache_get(key, root) # root used here as a unique not-found sentinel + result = cache_get( + key, root + ) # root used here as a unique not-found sentinel if result is not root: stats[HITS] += 1 return result @@ -123,7 +134,8 @@ def wrapper(*args, **kwds): with lock: link = cache_get(key) if link is not None: - # record recent use of the key by moving it to the front of the list + # record recent use of the key by moving it + # to the front of the list root, = nonlocal_root link_prev, link_next, key, result = link link_prev[NEXT] = link_next diff --git a/ext2/bs4/__init__.py b/ext2/bs4/__init__.py index d3a10869d6..1ea7b97aec 100644 --- a/ext2/bs4/__init__.py +++ b/ext2/bs4/__init__.py @@ -18,7 +18,7 @@ """ __author__ = "Leonard Richardson (leonardr@segfault.org)" -__version__ = "4.7.1" +__version__ = "4.8.1" __copyright__ = "Copyright (c) 2004-2019 Leonard Richardson" # Use of this source code is governed by the MIT license. __license__ = "MIT" @@ -63,7 +63,7 @@ class BeautifulSoup(Tag): handle_starttag(name, attrs) # See note about return value handle_endtag(name) handle_data(data) # Appends to the current data node - endData(containerClass=NavigableString) # Ends the current data node + endData(containerClass) # Ends the current data node No matter how complicated the underlying parser is, you should be able to build a tree using 'start tag' events, 'end tag' events, @@ -78,14 +78,14 @@ class BeautifulSoup(Tag): # If the end-user gives no indication which tree builder they # want, look for one with these features. DEFAULT_BUILDER_FEATURES = ['html', 'fast'] - + ASCII_SPACES = '\x20\x0a\x09\x0c\x0d' NO_PARSER_SPECIFIED_WARNING = "No parser was explicitly specified, so I'm using the best available %(markup_type)s parser for this system (\"%(parser)s\"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.\n\nThe code that caused this warning is on line %(line_number)s of the file %(filename)s. To get rid of this warning, pass the additional argument 'features=\"%(parser)s\"' to the BeautifulSoup constructor.\n" def __init__(self, markup="", features=None, builder=None, parse_only=None, from_encoding=None, exclude_encodings=None, - **kwargs): + element_classes=None, **kwargs): """Constructor. :param markup: A string or a file-like object representing @@ -98,8 +98,10 @@ def __init__(self, markup="", features=None, builder=None, name a specific parser, so that Beautiful Soup gives you the same results across platforms and virtual environments. - :param builder: A specific TreeBuilder to use instead of looking one - up based on `features`. You shouldn't need to use this. + :param builder: A TreeBuilder subclass to instantiate (or + instance to use) instead of looking one up based on + `features`. You only need to use this if you've implemented a + custom TreeBuilder. :param parse_only: A SoupStrainer. Only parts of the document matching the SoupStrainer will be considered. This is useful @@ -115,14 +117,26 @@ def __init__(self, markup="", features=None, builder=None, the document's encoding but you know Beautiful Soup's guess is wrong. + :param element_classes: A dictionary mapping BeautifulSoup + classes like Tag and NavigableString to other classes you'd + like to be instantiated instead as the parse tree is + built. This is useful for using subclasses to modify the + default behavior of Tag or NavigableString. + :param kwargs: For backwards compatibility purposes, the constructor accepts certain keyword arguments used in Beautiful Soup 3. None of these arguments do anything in - Beautiful Soup 4 and there's no need to actually pass keyword - arguments into the constructor. + Beautiful Soup 4; they will result in a warning and then be ignored. + + Apart from this, any keyword arguments passed into the BeautifulSoup + constructor are propagated to the TreeBuilder constructor. This + makes it possible to configure a TreeBuilder beyond saying + which one to use. + """ if 'convertEntities' in kwargs: + del kwargs['convertEntities'] warnings.warn( "BS4 does not respect the convertEntities argument to the " "BeautifulSoup constructor. Entities are always converted " @@ -177,13 +191,19 @@ def deprecated_argument(old_name, new_name): warnings.warn("You provided Unicode markup but also provided a value for from_encoding. Your from_encoding will be ignored.") from_encoding = None - if len(kwargs) > 0: - arg = kwargs.keys().pop() - raise TypeError( - "__init__() got an unexpected keyword argument '%s'" % arg) - - if builder is None: - original_features = features + self.element_classes = element_classes or dict() + + # We need this information to track whether or not the builder + # was specified well enough that we can omit the 'you need to + # specify a parser' warning. + original_builder = builder + original_features = features + + if isinstance(builder, type): + # A builder class was passed in; it needs to be instantiated. + builder_class = builder + builder = None + elif builder is None: if isinstance(features, basestring): features = [features] if features is None or len(features) == 0: @@ -194,9 +214,16 @@ def deprecated_argument(old_name, new_name): "Couldn't find a tree builder with the features you " "requested: %s. Do you need to install a parser library?" % ",".join(features)) - builder = builder_class() - if not (original_features == builder.NAME or - original_features in builder.ALTERNATE_NAMES): + + # At this point either we have a TreeBuilder instance in + # builder, or we have a builder_class that we can instantiate + # with the remaining **kwargs. + if builder is None: + builder = builder_class(**kwargs) + if not original_builder and not ( + original_features == builder.NAME or + original_features in builder.ALTERNATE_NAMES + ): if builder.is_xml: markup_type = "XML" else: @@ -231,7 +258,10 @@ def deprecated_argument(old_name, new_name): markup_type=markup_type ) warnings.warn(self.NO_PARSER_SPECIFIED_WARNING % values, stacklevel=2) - + else: + if kwargs: + warnings.warn("Keyword arguments to the BeautifulSoup constructor will be ignored. These would normally be passed into the TreeBuilder constructor, but a TreeBuilder instance was passed in as `builder`.") + self.builder = builder self.is_xml = builder.is_xml self.known_xml = self.is_xml @@ -272,6 +302,8 @@ def deprecated_argument(old_name, new_name): ' Beautiful Soup.' % markup) self._check_markup_is_url(markup) + rejections = [] + success = False for (self.markup, self.original_encoding, self.declared_html_encoding, self.contains_replacement_characters) in ( self.builder.prepare_markup( @@ -279,10 +311,18 @@ def deprecated_argument(old_name, new_name): self.reset() try: self._feed() + success = True break - except ParserRejectedMarkup: + except ParserRejectedMarkup as e: + rejections.append(e) pass + if not success: + other_exceptions = [unicode(e) for e in rejections] + raise ParserRejectedMarkup( + u"The markup you provided was rejected by the parser. Trying a different parser or a different encoding may help.\n\nOriginal exception(s) from parser:\n " + "\n ".join(other_exceptions) + ) + # Clear out the markup and remove the builder's circular # reference to this object. self.markup = None @@ -355,13 +395,20 @@ def reset(self): self.preserve_whitespace_tag_stack = [] self.pushTag(self) - def new_tag(self, name, namespace=None, nsprefix=None, attrs={}, **kwattrs): + def new_tag(self, name, namespace=None, nsprefix=None, attrs={}, + sourceline=None, sourcepos=None, **kwattrs): """Create a new tag associated with this soup.""" kwattrs.update(attrs) - return Tag(None, self.builder, name, namespace, nsprefix, kwattrs) + return self.element_classes.get(Tag, Tag)( + None, self.builder, name, namespace, nsprefix, kwattrs, + sourceline=sourceline, sourcepos=sourcepos + ) - def new_string(self, s, subclass=NavigableString): + def new_string(self, s, subclass=None): """Create a new NavigableString associated with this soup.""" + subclass = subclass or self.element_classes.get( + NavigableString, NavigableString + ) return subclass(s) def insert_before(self, successor): @@ -388,7 +435,17 @@ def pushTag(self, tag): if tag.name in self.builder.preserve_whitespace_tags: self.preserve_whitespace_tag_stack.append(tag) - def endData(self, containerClass=NavigableString): + def endData(self, containerClass=None): + + # Default container is NavigableString. + containerClass = containerClass or NavigableString + + # The user may want us to instantiate some alias for the + # container class. + containerClass = self.element_classes.get( + containerClass, containerClass + ) + if self.current_data: current_data = u''.join(self.current_data) # If whitespace is not preserved, and this string contains @@ -509,7 +566,8 @@ def _popToTag(self, name, nsprefix=None, inclusivePop=True): return most_recently_popped - def handle_starttag(self, name, namespace, nsprefix, attrs): + def handle_starttag(self, name, namespace, nsprefix, attrs, sourceline=None, + sourcepos=None): """Push a start tag on to the stack. If this method returns None, the tag was rejected by the @@ -526,8 +584,11 @@ def handle_starttag(self, name, namespace, nsprefix, attrs): or not self.parse_only.search_tag(name, attrs))): return None - tag = Tag(self, self.builder, name, namespace, nsprefix, attrs, - self.currentTag, self._most_recent_element) + tag = self.element_classes.get(Tag, Tag)( + self, self.builder, name, namespace, nsprefix, attrs, + self.currentTag, self._most_recent_element, + sourceline=sourceline, sourcepos=sourcepos + ) if tag is None: return tag if self._most_recent_element is not None: diff --git a/ext2/bs4/builder/__init__.py b/ext2/bs4/builder/__init__.py index 42077503f7..7efbf89a49 100644 --- a/ext2/bs4/builder/__init__.py +++ b/ext2/bs4/builder/__init__.py @@ -7,7 +7,6 @@ from bs4.element import ( CharsetMetaAttributeValue, ContentMetaAttributeValue, - HTMLAwareEntitySubstitution, nonwhitespace_re ) @@ -90,18 +89,58 @@ class TreeBuilder(object): is_xml = False picklable = False - preserve_whitespace_tags = set() empty_element_tags = None # A tag will be considered an empty-element # tag when and only when it has no contents. # A value for these tag/attribute combinations is a space- or # comma-separated list of CDATA, rather than a single CDATA. - cdata_list_attributes = {} + DEFAULT_CDATA_LIST_ATTRIBUTES = {} + DEFAULT_PRESERVE_WHITESPACE_TAGS = set() + + USE_DEFAULT = object() - def __init__(self): + # Most parsers don't keep track of line numbers. + TRACKS_LINE_NUMBERS = False + + def __init__(self, multi_valued_attributes=USE_DEFAULT, + preserve_whitespace_tags=USE_DEFAULT, + store_line_numbers=USE_DEFAULT): + """Constructor. + + :param multi_valued_attributes: If this is set to None, the + TreeBuilder will not turn any values for attributes like + 'class' into lists. Setting this do a dictionary will + customize this behavior; look at DEFAULT_CDATA_LIST_ATTRIBUTES + for an example. + + Internally, these are called "CDATA list attributes", but that + probably doesn't make sense to an end-user, so the argument name + is `multi_valued_attributes`. + + :param preserve_whitespace_tags: A list of tags to treat + the way
 tags are treated in HTML. Tags in this list
+        will have 
+
+        :param store_line_numbers: If the parser keeps track of the
+        line numbers and positions of the original markup, that
+        information will, by default, be stored in each corresponding
+        `Tag` object. You can turn this off by passing
+        store_line_numbers=False. If the parser you're using doesn't 
+        keep track of this information, then setting store_line_numbers=True
+        will do nothing.
+        """
         self.soup = None
-
+        if multi_valued_attributes is self.USE_DEFAULT:
+            multi_valued_attributes = self.DEFAULT_CDATA_LIST_ATTRIBUTES
+        self.cdata_list_attributes = multi_valued_attributes
+        if preserve_whitespace_tags is self.USE_DEFAULT:
+            preserve_whitespace_tags = self.DEFAULT_PRESERVE_WHITESPACE_TAGS
+        self.preserve_whitespace_tags = preserve_whitespace_tags
+        if store_line_numbers == self.USE_DEFAULT:
+            store_line_numbers = self.TRACKS_LINE_NUMBERS
+        self.store_line_numbers = store_line_numbers
+        
     def initialize_soup(self, soup):
         """The BeautifulSoup object has been initialized and is now
         being associated with the TreeBuilder.
@@ -131,13 +170,13 @@ def can_be_empty_element(self, tag_name):
         if self.empty_element_tags is None:
             return True
         return tag_name in self.empty_element_tags
-        
+    
     def feed(self, markup):
         raise NotImplementedError()
 
     def prepare_markup(self, markup, user_specified_encoding=None,
-                       document_declared_encoding=None):
-        return markup, None, None, False
+                       document_declared_encoding=None, exclude_encodings=None):
+        yield markup, None, None, False
 
     def test_fragment_to_document(self, fragment):
         """Wrap an HTML fragment to make it look like a document.
@@ -237,7 +276,6 @@ class HTMLTreeBuilder(TreeBuilder):
     Such as which tags are empty-element tags.
     """
 
-    preserve_whitespace_tags = HTMLAwareEntitySubstitution.preserve_whitespace_tags
     empty_element_tags = set([
         # These are from HTML5.
         'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr',
@@ -259,7 +297,7 @@ class HTMLTreeBuilder(TreeBuilder):
     # encounter one of these attributes, we will parse its value into
     # a list of values if possible. Upon output, the list will be
     # converted back into a string.
-    cdata_list_attributes = {
+    DEFAULT_CDATA_LIST_ATTRIBUTES = {
         "*" : ['class', 'accesskey', 'dropzone'],
         "a" : ['rel', 'rev'],
         "link" :  ['rel', 'rev'],
@@ -276,6 +314,8 @@ class HTMLTreeBuilder(TreeBuilder):
         "output" : ["for"],
         }
 
+    DEFAULT_PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea'])
+    
     def set_up_substitutions(self, tag):
         # We are only interested in  tags
         if tag.name != 'meta':
@@ -323,8 +363,15 @@ def register_treebuilders_from(module):
             this_module.builder_registry.register(obj)
 
 class ParserRejectedMarkup(Exception):
-    pass
-
+    def __init__(self, message_or_exception):
+        """Explain why the parser rejected the given markup, either
+        with a textual explanation or another exception.
+        """
+        if isinstance(message_or_exception, Exception):
+            e = message_or_exception
+            message_or_exception = "%s: %s" % (e.__class__.__name__, unicode(e))
+        super(ParserRejectedMarkup, self).__init__(message_or_exception)
+            
 # Builders are registered in reverse order of priority, so that custom
 # builder registrations will take precedence. In general, we want lxml
 # to take precedence over html5lib, because it's faster. And we only
diff --git a/ext2/bs4/builder/_html5lib.py b/ext2/bs4/builder/_html5lib.py
index 6fa8593118..13f697c602 100644
--- a/ext2/bs4/builder/_html5lib.py
+++ b/ext2/bs4/builder/_html5lib.py
@@ -45,6 +45,10 @@ class HTML5TreeBuilder(HTMLTreeBuilder):
 
     features = [NAME, PERMISSIVE, HTML_5, HTML]
 
+    # html5lib can tell us which line number and position in the
+    # original file is the source of an element.
+    TRACKS_LINE_NUMBERS = True
+    
     def prepare_markup(self, markup, user_specified_encoding,
                        document_declared_encoding=None, exclude_encodings=None):
         # Store the user-specified encoding for use later on.
@@ -62,7 +66,7 @@ def feed(self, markup):
         if self.soup.parse_only is not None:
             warnings.warn("You provided a value for parse_only, but the html5lib tree builder doesn't support parse_only. The entire document will be parsed.")
         parser = html5lib.HTMLParser(tree=self.create_treebuilder)
-
+        self.underlying_builder.parser = parser
         extra_kwargs = dict()
         if not isinstance(markup, unicode):
             if new_html5lib:
@@ -70,7 +74,7 @@ def feed(self, markup):
             else:
                 extra_kwargs['encoding'] = self.user_specified_encoding
         doc = parser.parse(markup, **extra_kwargs)
-
+        
         # Set the character encoding detected by the tokenizer.
         if isinstance(markup, unicode):
             # We need to special-case this because html5lib sets
@@ -84,10 +88,13 @@ def feed(self, markup):
                 # with other tree builders.
                 original_encoding = original_encoding.name
             doc.original_encoding = original_encoding
-
+        self.underlying_builder.parser = None
+            
     def create_treebuilder(self, namespaceHTMLElements):
         self.underlying_builder = TreeBuilderForHtml5lib(
-            namespaceHTMLElements, self.soup)
+            namespaceHTMLElements, self.soup,
+            store_line_numbers=self.store_line_numbers
+        )
         return self.underlying_builder
 
     def test_fragment_to_document(self, fragment):
@@ -96,15 +103,26 @@ def test_fragment_to_document(self, fragment):
 
 
 class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder):
-
-    def __init__(self, namespaceHTMLElements, soup=None):
+    
+    def __init__(self, namespaceHTMLElements, soup=None,
+                 store_line_numbers=True, **kwargs):
         if soup:
             self.soup = soup
         else:
             from bs4 import BeautifulSoup
-            self.soup = BeautifulSoup("", "html.parser")
+            # TODO: Why is the parser 'html.parser' here? To avoid an
+            # infinite loop?
+            self.soup = BeautifulSoup(
+                "", "html.parser", store_line_numbers=store_line_numbers,
+                **kwargs
+            )
         super(TreeBuilderForHtml5lib, self).__init__(namespaceHTMLElements)
 
+        # This will be set later to an html5lib.html5parser.HTMLParser
+        # object, which we can use to track the current line number.
+        self.parser = None
+        self.store_line_numbers = store_line_numbers
+        
     def documentClass(self):
         self.soup.reset()
         return Element(self.soup, self.soup, None)
@@ -118,7 +136,16 @@ def insertDoctype(self, token):
         self.soup.object_was_parsed(doctype)
 
     def elementClass(self, name, namespace):
-        tag = self.soup.new_tag(name, namespace)
+        kwargs = {}
+        if self.parser and self.store_line_numbers:
+            # This represents the point immediately after the end of the
+            # tag. We don't know when the tag started, but we do know
+            # where it ended -- the character just before this one.
+            sourceline, sourcepos = self.parser.tokenizer.stream.position()
+            kwargs['sourceline'] = sourceline
+            kwargs['sourcepos'] = sourcepos-1
+        tag = self.soup.new_tag(name, namespace, **kwargs)
+
         return Element(tag, self.soup, namespace)
 
     def commentClass(self, data):
@@ -126,6 +153,8 @@ def commentClass(self, data):
 
     def fragmentClass(self):
         from bs4 import BeautifulSoup
+        # TODO: Why is the parser 'html.parser' here? To avoid an
+        # infinite loop?
         self.soup = BeautifulSoup("", "html.parser")
         self.soup.name = "[document_fragment]"
         return Element(self.soup, self.soup, None)
@@ -199,7 +228,7 @@ def __iter__(self):
     def __setitem__(self, name, value):
         # If this attribute is a multi-valued attribute for this element,
         # turn its value into a list.
-        list_attr = HTML5TreeBuilder.cdata_list_attributes
+        list_attr = self.element.cdata_list_attributes
         if (name in list_attr['*']
             or (self.element.name in list_attr
                 and name in list_attr[self.element.name])):
diff --git a/ext2/bs4/builder/_htmlparser.py b/ext2/bs4/builder/_htmlparser.py
index ff09ca3109..cd50eb0aed 100644
--- a/ext2/bs4/builder/_htmlparser.py
+++ b/ext2/bs4/builder/_htmlparser.py
@@ -99,7 +99,11 @@ def handle_starttag(self, name, attrs, handle_empty_element=True):
             attr_dict[key] = value
             attrvalue = '""'
         #print "START", name
-        tag = self.soup.handle_starttag(name, None, None, attr_dict)
+        sourceline, sourcepos = self.getpos()
+        tag = self.soup.handle_starttag(
+            name, None, None, attr_dict, sourceline=sourceline,
+            sourcepos=sourcepos
+        )
         if tag and tag.is_empty_element and handle_empty_element:
             # Unlike other parsers, html.parser doesn't send separate end tag
             # events for empty-element tags. (It's handled in
@@ -214,12 +218,19 @@ class HTMLParserTreeBuilder(HTMLTreeBuilder):
     NAME = HTMLPARSER
     features = [NAME, HTML, STRICT]
 
-    def __init__(self, *args, **kwargs):
+    # The html.parser knows which line number and position in the
+    # original file is the source of an element.
+    TRACKS_LINE_NUMBERS = True
+    
+    def __init__(self, parser_args=None, parser_kwargs=None, **kwargs):
+        super(HTMLParserTreeBuilder, self).__init__(**kwargs)
+        parser_args = parser_args or []
+        parser_kwargs = parser_kwargs or {}
         if CONSTRUCTOR_TAKES_STRICT and not CONSTRUCTOR_STRICT_IS_DEPRECATED:
-            kwargs['strict'] = False
+            parser_kwargs['strict'] = False
         if CONSTRUCTOR_TAKES_CONVERT_CHARREFS:
-            kwargs['convert_charrefs'] = False
-        self.parser_args = (args, kwargs)
+            parser_kwargs['convert_charrefs'] = False
+        self.parser_args = (parser_args, parser_kwargs)
 
     def prepare_markup(self, markup, user_specified_encoding=None,
                        document_declared_encoding=None, exclude_encodings=None):
diff --git a/ext2/bs4/builder/_lxml.py b/ext2/bs4/builder/_lxml.py
index b7e172cc9a..ea66d8bd5b 100644
--- a/ext2/bs4/builder/_lxml.py
+++ b/ext2/bs4/builder/_lxml.py
@@ -57,6 +57,12 @@ class LXMLTreeBuilderForXML(TreeBuilder):
 
     DEFAULT_NSMAPS_INVERTED = _invert(DEFAULT_NSMAPS)
 
+    # NOTE: If we parsed Element objects and looked at .sourceline,
+    # we'd be able to see the line numbers from the original document.
+    # But instead we build an XMLParser or HTMLParser object to serve
+    # as the target of parse messages, and those messages don't include
+    # line numbers.
+    
     def initialize_soup(self, soup):
         """Let the BeautifulSoup object know about the standard namespace
         mapping.
@@ -94,7 +100,7 @@ def parser_for(self, encoding):
             parser = parser(target=self, strip_cdata=False, encoding=encoding)
         return parser
 
-    def __init__(self, parser=None, empty_element_tags=None):
+    def __init__(self, parser=None, empty_element_tags=None, **kwargs):
         # TODO: Issue a warning if parser is present but not a
         # callable, since that means there's no way to create new
         # parsers for different encodings.
@@ -103,6 +109,7 @@ def __init__(self, parser=None, empty_element_tags=None):
             self.empty_element_tags = set(empty_element_tags)
         self.soup = None
         self.nsmaps = [self.DEFAULT_NSMAPS_INVERTED]
+        super(LXMLTreeBuilderForXML, self).__init__(**kwargs)
         
     def _getNsTag(self, tag):
         # Split the namespace URL out of a fully-qualified lxml tag
@@ -168,7 +175,7 @@ def feed(self, markup):
                     self.parser.feed(data)
             self.parser.close()
         except (UnicodeDecodeError, LookupError, etree.ParserError), e:
-            raise ParserRejectedMarkup(str(e))
+            raise ParserRejectedMarkup(e)
 
     def close(self):
         self.nsmaps = [self.DEFAULT_NSMAPS_INVERTED]
@@ -287,7 +294,7 @@ def feed(self, markup):
             self.parser.feed(markup)
             self.parser.close()
         except (UnicodeDecodeError, LookupError, etree.ParserError), e:
-            raise ParserRejectedMarkup(str(e))
+            raise ParserRejectedMarkup(e)
 
 
     def test_fragment_to_document(self, fragment):
diff --git a/ext2/bs4/check_block.py b/ext2/bs4/check_block.py
new file mode 100644
index 0000000000..a60a7b7454
--- /dev/null
+++ b/ext2/bs4/check_block.py
@@ -0,0 +1,4 @@
+import requests
+data = requests.get("https://www.crummy.com/").content
+from bs4 import _s
+data = [x for x in _s(data).block_text()]
diff --git a/ext2/bs4/dammit.py b/ext2/bs4/dammit.py
index fb2f8b8ed6..74fa7f0245 100644
--- a/ext2/bs4/dammit.py
+++ b/ext2/bs4/dammit.py
@@ -22,6 +22,8 @@
     #  PyPI package: cchardet
     import cchardet
     def chardet_dammit(s):
+        if isinstance(s, unicode):
+            return None
         return cchardet.detect(s)['encoding']
 except ImportError:
     try:
@@ -30,6 +32,8 @@ def chardet_dammit(s):
         #  PyPI package: chardet
         import chardet
         def chardet_dammit(s):
+            if isinstance(s, unicode):
+                return None
             return chardet.detect(s)['encoding']
         #import chardet.constants
         #chardet.constants._debug = 1
@@ -44,10 +48,19 @@ def chardet_dammit(s):
 except ImportError:
     pass
 
-xml_encoding_re = re.compile(
-    '^<\\?.*encoding=[\'"](.*?)[\'"].*\\?>'.encode(), re.I)
-html_meta_re = re.compile(
-    '<\\s*meta[^>]+charset\\s*=\\s*["\']?([^>]*?)[ /;\'">]'.encode(), re.I)
+# Build bytestring and Unicode versions of regular expressions for finding
+# a declared encoding inside an XML or HTML document.
+xml_encoding = u'^\s*<\\?.*encoding=[\'"](.*?)[\'"].*\\?>'
+html_meta = u'<\\s*meta[^>]+charset\\s*=\\s*["\']?([^>]*?)[ /;\'">]'
+encoding_res = dict()
+encoding_res[bytes] = {
+    'html' : re.compile(html_meta.encode("ascii"), re.I),
+    'xml' : re.compile(xml_encoding.encode("ascii"), re.I),
+}
+encoding_res[unicode] = {
+    'html' : re.compile(html_meta, re.I),
+    'xml' : re.compile(xml_encoding, re.I)
+}
 
 class EntitySubstitution(object):
 
@@ -57,15 +70,24 @@ def _populate_class_variables():
         lookup = {}
         reverse_lookup = {}
         characters_for_re = []
-        for codepoint, name in list(codepoint2name.items()):
+
+        # &apos is an XHTML entity and an HTML 5, but not an HTML 4
+        # entity. We don't want to use it, but we want to recognize it on the way in.
+        #
+        # TODO: Ideally we would be able to recognize all HTML 5 named
+        # entities, but that's a little tricky.
+        extra = [(39, 'apos')]
+        for codepoint, name in list(codepoint2name.items()) + extra:
             character = unichr(codepoint)
-            if codepoint != 34:
+            if codepoint not in (34, 39):
                 # There's no point in turning the quotation mark into
-                # ", unless it happens within an attribute value, which
-                # is handled elsewhere.
+                # " or the single quote into ', unless it
+                # happens within an attribute value, which is handled
+                # elsewhere.
                 characters_for_re.append(character)
                 lookup[character] = name
-            # But we do want to turn " into the quotation mark.
+            # But we do want to recognize those entities on the way in and
+            # convert them to Unicode characters.
             reverse_lookup[name] = character
         re_definition = "[%s]" % "".join(characters_for_re)
         return lookup, reverse_lookup, re.compile(re_definition)
@@ -310,14 +332,22 @@ def find_declared_encoding(cls, markup, is_html=False, search_entire_document=Fa
             xml_endpos = 1024
             html_endpos = max(2048, int(len(markup) * 0.05))
 
+        if isinstance(markup, bytes):
+            res = encoding_res[bytes]
+        else:
+            res = encoding_res[unicode]
+
+        xml_re = res['xml']
+        html_re = res['html']
         declared_encoding = None
-        declared_encoding_match = xml_encoding_re.search(markup, endpos=xml_endpos)
+        declared_encoding_match = xml_re.search(markup, endpos=xml_endpos)
         if not declared_encoding_match and is_html:
-            declared_encoding_match = html_meta_re.search(markup, endpos=html_endpos)
+            declared_encoding_match = html_re.search(markup, endpos=html_endpos)
         if declared_encoding_match is not None:
-            declared_encoding = declared_encoding_match.groups()[0].decode(
-                'ascii', 'replace')
+            declared_encoding = declared_encoding_match.groups()[0]
         if declared_encoding:
+            if isinstance(declared_encoding, bytes):
+                declared_encoding = declared_encoding.decode('ascii', 'replace')
             return declared_encoding.lower()
         return None
 
diff --git a/ext2/bs4/element.py b/ext2/bs4/element.py
index 547b8bae8d..2001ad5854 100644
--- a/ext2/bs4/element.py
+++ b/ext2/bs4/element.py
@@ -16,7 +16,11 @@
         'The soupsieve package is not installed. CSS selectors cannot be used.'
     )
 
-from bs4.dammit import EntitySubstitution
+from bs4.formatter import (
+    Formatter,
+    HTMLFormatter,
+    XMLFormatter,
+)
 
 DEFAULT_OUTPUT_ENCODING = "utf-8"
 PY3K = (sys.version_info[0] > 2)
@@ -41,7 +45,12 @@ def alias(self):
 
 class NamespacedAttribute(unicode):
 
-    def __new__(cls, prefix, name, namespace=None):
+    def __new__(cls, prefix, name=None, namespace=None):
+        if not name:
+            # This is the default namespace. Its name "has no value"
+            # per https://www.w3.org/TR/xml-names/#defaulting
+            name = None
+
         if name is None:
             obj = unicode.__new__(cls, prefix)
         elif prefix is None:
@@ -99,138 +108,71 @@ def rewrite(match):
             return match.group(1) + encoding
         return self.CHARSET_RE.sub(rewrite, self.original_value)
 
-class HTMLAwareEntitySubstitution(EntitySubstitution):
-
-    """Entity substitution rules that are aware of some HTML quirks.
-
-    Specifically, the contents of \n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app-link.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app-link.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./app-link.vue?vue&type=template&id=13023830&\"\nimport script from \"./app-link.vue?vue&type=script&lang=js&\"\nexport * from \"./app-link.vue?vue&type=script&lang=js&\"\nimport style0 from \"./app-link.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./asset.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./asset.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./asset.vue?vue&type=template&id=8ae62598&\"\nimport script from \"./asset.vue?vue&type=script&lang=js&\"\nexport * from \"./asset.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.link)?_c('img',{class:_vm.cls,attrs:{\"src\":_vm.src},on:{\"error\":function($event){_vm.error = true}}}):_c('app-link',{attrs:{\"href\":_vm.href}},[_c('img',{class:_vm.cls,attrs:{\"src\":_vm.src},on:{\"error\":function($event){_vm.error = true}}})])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-template.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-template.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","import { render, staticRenderFns } from \"./config-template.vue?vue&type=template&id=58f1e02e&\"\nimport script from \"./config-template.vue?vue&type=script&lang=js&\"\nexport * from \"./config-template.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"config-template-content\"}},[_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":_vm.labelFor}},[_c('span',[_vm._v(_vm._s(_vm.label))])]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_vm._t(\"default\")],2)])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-textbox-number.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-textbox-number.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","import { render, staticRenderFns } from \"./config-textbox-number.vue?vue&type=template&id=3f3851e7&\"\nimport script from \"./config-textbox-number.vue?vue&type=script&lang=js&\"\nexport * from \"./config-textbox-number.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-textbox-number.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"config-textbox-number-content\"}},[_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":_vm.id}},[_c('span',[_vm._v(_vm._s(_vm.label))])]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('input',_vm._b({directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.localValue),expression:\"localValue\"}],attrs:{\"type\":\"number\"},domProps:{\"value\":(_vm.localValue)},on:{\"input\":[function($event){if($event.target.composing){ return; }_vm.localValue=$event.target.value},function($event){return _vm.updateValue()}]}},'input',{min: _vm.min, max: _vm.max, step: _vm.step, id: _vm.id, name: _vm.id, class: _vm.inputClass, placeholder: _vm.placeholder, disabled: _vm.disabled},false)),_vm._v(\" \"),_vm._l((_vm.explanations),function(explanation,index){return _c('p',{key:index},[_vm._v(_vm._s(explanation))])}),_vm._v(\" \"),_vm._t(\"default\")],2)])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-textbox.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-textbox.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","import { render, staticRenderFns } from \"./config-textbox.vue?vue&type=template&id=012c9055&\"\nimport script from \"./config-textbox.vue?vue&type=script&lang=js&\"\nexport * from \"./config-textbox.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-textbox.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"config-textbox\"}},[_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":_vm.id}},[_c('span',[_vm._v(_vm._s(_vm.label))])]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[((({id: _vm.id, type: _vm.type, name: _vm.id, class: _vm.inputClass, placeholder: _vm.placeholder, disabled: _vm.disabled}).type)==='checkbox')?_c('input',_vm._b({directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.localValue),expression:\"localValue\"}],attrs:{\"type\":\"checkbox\"},domProps:{\"checked\":Array.isArray(_vm.localValue)?_vm._i(_vm.localValue,null)>-1:(_vm.localValue)},on:{\"input\":function($event){return _vm.updateValue()},\"change\":function($event){var $$a=_vm.localValue,$$el=$event.target,$$c=$$el.checked?(true):(false);if(Array.isArray($$a)){var $$v=null,$$i=_vm._i($$a,$$v);if($$el.checked){$$i<0&&(_vm.localValue=$$a.concat([$$v]))}else{$$i>-1&&(_vm.localValue=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{_vm.localValue=$$c}}}},'input',{id: _vm.id, type: _vm.type, name: _vm.id, class: _vm.inputClass, placeholder: _vm.placeholder, disabled: _vm.disabled},false)):((({id: _vm.id, type: _vm.type, name: _vm.id, class: _vm.inputClass, placeholder: _vm.placeholder, disabled: _vm.disabled}).type)==='radio')?_c('input',_vm._b({directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.localValue),expression:\"localValue\"}],attrs:{\"type\":\"radio\"},domProps:{\"checked\":_vm._q(_vm.localValue,null)},on:{\"input\":function($event){return _vm.updateValue()},\"change\":function($event){_vm.localValue=null}}},'input',{id: _vm.id, type: _vm.type, name: _vm.id, class: _vm.inputClass, placeholder: _vm.placeholder, disabled: _vm.disabled},false)):_c('input',_vm._b({directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.localValue),expression:\"localValue\"}],attrs:{\"type\":({id: _vm.id, type: _vm.type, name: _vm.id, class: _vm.inputClass, placeholder: _vm.placeholder, disabled: _vm.disabled}).type},domProps:{\"value\":(_vm.localValue)},on:{\"input\":[function($event){if($event.target.composing){ return; }_vm.localValue=$event.target.value},function($event){return _vm.updateValue()}]}},'input',{id: _vm.id, type: _vm.type, name: _vm.id, class: _vm.inputClass, placeholder: _vm.placeholder, disabled: _vm.disabled},false)),_vm._v(\" \"),_vm._l((_vm.explanations),function(explanation,index){return _c('p',{key:index},[_vm._v(_vm._s(explanation))])}),_vm._v(\" \"),_vm._t(\"default\")],2)])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-toggle-slider.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-toggle-slider.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","import { render, staticRenderFns } from \"./config-toggle-slider.vue?vue&type=template&id=448de07a&\"\nimport script from \"./config-toggle-slider.vue?vue&type=script&lang=js&\"\nexport * from \"./config-toggle-slider.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-toggle-slider.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"config-toggle-slider-content\"}},[_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":_vm.id}},[_c('span',[_vm._v(_vm._s(_vm.label))])]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('toggle-button',_vm._b({attrs:{\"width\":45,\"height\":22,\"sync\":\"\"},on:{\"input\":function($event){return _vm.updateValue()}},model:{value:(_vm.localChecked),callback:function ($$v) {_vm.localChecked=$$v},expression:\"localChecked\"}},'toggle-button',{id: _vm.id, name: _vm.id, disabled: _vm.disabled},false)),_vm._v(\" \"),_vm._l((_vm.explanations),function(explanation,index){return _c('p',{key:index},[_vm._v(_vm._s(explanation))])}),_vm._v(\" \"),_vm._t(\"default\")],2)])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./file-browser.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./file-browser.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./file-browser.vue?vue&type=template&id=eff76864&scoped=true&\"\nimport script from \"./file-browser.vue?vue&type=script&lang=js&\"\nexport * from \"./file-browser.vue?vue&type=script&lang=js&\"\nimport style0 from \"./file-browser.vue?vue&type=style&index=0&id=eff76864&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  \"eff76864\",\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"file-browser max-width\"},[_c('div',{class:(_vm.showBrowseButton ? 'input-group' : 'input-group-no-btn')},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.currentPath),expression:\"currentPath\"}],ref:\"locationInput\",staticClass:\"form-control input-sm fileBrowserField\",attrs:{\"name\":_vm.name,\"type\":\"text\"},domProps:{\"value\":(_vm.currentPath)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.currentPath=$event.target.value}}}),_vm._v(\" \"),(_vm.showBrowseButton)?_c('div',{staticClass:\"input-group-btn\",attrs:{\"title\":_vm.title,\"alt\":_vm.title},on:{\"click\":function($event){$event.preventDefault();return _vm.openDialog($event)}}},[_vm._m(0)]):_vm._e()]),_vm._v(\" \"),_c('div',{ref:\"fileBrowserDialog\",staticClass:\"fileBrowserDialog\",staticStyle:{\"display\":\"none\"}}),_vm._v(\" \"),_c('input',{ref:\"fileBrowserSearchBox\",staticClass:\"form-control\",staticStyle:{\"display\":\"none\"},attrs:{\"type\":\"text\"},domProps:{\"value\":_vm.currentPath},on:{\"keyup\":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }return _vm.browse($event.target.value)}}}),_vm._v(\" \"),_c('ul',{ref:\"fileBrowserFileList\",staticStyle:{\"display\":\"none\"}},_vm._l((_vm.files),function(file){return _c('li',{key:file.name,staticClass:\"ui-state-default ui-corner-all\"},[_c('a',{on:{\"mouseover\":function($event){return _vm.toggleFolder(file, $event)},\"mouseout\":function($event){return _vm.toggleFolder(file, $event)},\"click\":function($event){return _vm.fileClicked(file)}}},[_c('span',{class:'ui-icon ' + (file.isFile ? 'ui-icon-blank' : 'ui-icon-folder-collapsed')}),_vm._v(\" \"+_vm._s(file.name)+\"\\n            \")])])}),0)])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"btn btn-default input-sm\",staticStyle:{\"font-size\":\"14px\"}},[_c('i',{staticClass:\"glyphicon glyphicon-open\"})])}]\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./language-select.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./language-select.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./language-select.vue?vue&type=template&id=6d9e3033&\"\nimport script from \"./language-select.vue?vue&type=script&lang=js&\"\nexport * from \"./language-select.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('select')}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./name-pattern.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./name-pattern.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./name-pattern.vue?vue&type=template&id=4cc642ae&\"\nimport script from \"./name-pattern.vue?vue&type=script&lang=js&\"\nexport * from \"./name-pattern.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"name-pattern-wrapper\"}},[(_vm.type)?_c('div',{staticClass:\"form-group\"},[_c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"enable_naming_custom\"}},[_c('span',[_vm._v(\"Custom \"+_vm._s(_vm.type))])]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('toggle-button',{attrs:{\"width\":45,\"height\":22,\"id\":\"enable_naming_custom\",\"name\":\"enable_naming_custom\",\"sync\":\"\"},on:{\"input\":function($event){return _vm.update()}},model:{value:(_vm.isEnabled),callback:function ($$v) {_vm.isEnabled=$$v},expression:\"isEnabled\"}}),_vm._v(\" \"),_c('span',[_vm._v(\"Name \"+_vm._s(_vm.type)+\" shows differently than regular shows?\")])],1)]):_vm._e(),_vm._v(\" \"),(!_vm.type || _vm.isEnabled)?_c('div',{staticClass:\"episode-naming\"},[_c('div',{staticClass:\"form-group\"},[_vm._m(0),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedNamingPattern),expression:\"selectedNamingPattern\"}],staticClass:\"form-control input-sm\",attrs:{\"id\":\"name_presets\"},on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedNamingPattern=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},_vm.updatePatternSamples],\"input\":function($event){return _vm.update()}}},_vm._l((_vm.presets),function(preset){return _c('option',{key:preset.pattern,attrs:{\"id\":preset.pattern}},[_vm._v(_vm._s(preset.example))])}),0)])]),_vm._v(\" \"),_c('div',{attrs:{\"id\":\"naming_custom\"}},[(_vm.isCustom)?_c('div',{staticClass:\"form-group\",staticStyle:{\"padding-top\":\"0\"}},[_vm._m(1),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.customName),expression:\"customName\"}],staticClass:\"form-control-inline-max input-sm max-input350\",attrs:{\"type\":\"text\",\"name\":\"naming_pattern\",\"id\":\"naming_pattern\"},domProps:{\"value\":(_vm.customName)},on:{\"change\":_vm.updatePatternSamples,\"input\":[function($event){if($event.target.composing){ return; }_vm.customName=$event.target.value},function($event){return _vm.update()}]}}),_vm._v(\" \"),_c('img',{staticClass:\"legend\",attrs:{\"src\":\"images/legend16.png\",\"width\":\"16\",\"height\":\"16\",\"alt\":\"[Toggle Key]\",\"id\":\"show_naming_key\",\"title\":\"Toggle Naming Legend\"},on:{\"click\":function($event){_vm.showLegend = !_vm.showLegend}}})])]):_vm._e(),_vm._v(\" \"),(_vm.showLegend && _vm.isCustom)?_c('div',{staticClass:\"nocheck\",attrs:{\"id\":\"naming_key\"}},[_c('table',{staticClass:\"Key\"},[_vm._m(2),_vm._v(\" \"),_vm._m(3),_vm._v(\" \"),_c('tbody',[_vm._m(4),_vm._v(\" \"),_vm._m(5),_vm._v(\" \"),_vm._m(6),_vm._v(\" \"),_vm._m(7),_vm._v(\" \"),_vm._m(8),_vm._v(\" \"),_vm._m(9),_vm._v(\" \"),_vm._m(10),_vm._v(\" \"),_vm._m(11),_vm._v(\" \"),_vm._m(12),_vm._v(\" \"),_vm._m(13),_vm._v(\" \"),_vm._m(14),_vm._v(\" \"),_vm._m(15),_vm._v(\" \"),_vm._m(16),_vm._v(\" \"),_vm._m(17),_vm._v(\" \"),_vm._m(18),_vm._v(\" \"),_vm._m(19),_vm._v(\" \"),_c('tr',[_vm._m(20),_vm._v(\" \"),_c('td',[_vm._v(\"%M\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.getDateFormat('M')))])]),_vm._v(\" \"),_c('tr',{staticClass:\"even\"},[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%D\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.getDateFormat('d')))])]),_vm._v(\" \"),_c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%Y\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.getDateFormat('yyyy')))])]),_vm._v(\" \"),_c('tr',[_vm._m(21),_vm._v(\" \"),_c('td',[_vm._v(\"%CM\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.getDateFormat('M')))])]),_vm._v(\" \"),_c('tr',{staticClass:\"even\"},[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%CD\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.getDateFormat('d')))])]),_vm._v(\" \"),_c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%CY\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.getDateFormat('yyyy')))])]),_vm._v(\" \"),_vm._m(22),_vm._v(\" \"),_vm._m(23),_vm._v(\" \"),_vm._m(24),_vm._v(\" \"),_vm._m(25),_vm._v(\" \"),_vm._m(26),_vm._v(\" \"),_vm._m(27),_vm._v(\" \"),_vm._m(28),_vm._v(\" \"),_vm._m(29),_vm._v(\" \"),_vm._m(30)])])]):_vm._e()]),_vm._v(\" \"),(_vm.selectedMultiEpStyle)?_c('div',{staticClass:\"form-group\"},[_vm._m(31),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedMultiEpStyle),expression:\"selectedMultiEpStyle\"}],staticClass:\"form-control input-sm\",attrs:{\"id\":\"naming_multi_ep\",\"name\":\"naming_multi_ep\"},on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedMultiEpStyle=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},_vm.updatePatternSamples],\"input\":function($event){return _vm.update($event)}}},_vm._l((_vm.availableMultiEpStyles),function(multiEpStyle){return _c('option',{key:multiEpStyle.value,attrs:{\"id\":\"multiEpStyle\"},domProps:{\"value\":multiEpStyle.value}},[_vm._v(_vm._s(multiEpStyle.text))])}),0)])]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"form-group row\"},[_c('h3',{staticClass:\"col-sm-12\"},[_vm._v(\"Single-EP Sample:\")]),_vm._v(\" \"),_c('div',{staticClass:\"example col-sm-12\"},[_c('span',{staticClass:\"jumbo\",attrs:{\"id\":\"naming_example\"}},[_vm._v(_vm._s(_vm.namingExample))])])]),_vm._v(\" \"),(_vm.isMulti)?_c('div',{staticClass:\"form-group row\"},[_c('h3',{staticClass:\"col-sm-12\"},[_vm._v(\"Multi-EP sample:\")]),_vm._v(\" \"),_c('div',{staticClass:\"example col-sm-12\"},[_c('span',{staticClass:\"jumbo\",attrs:{\"id\":\"naming_example_multi\"}},[_vm._v(_vm._s(_vm.namingExampleMulti))])])]):_vm._e(),_vm._v(\" \"),(_vm.animeType > 0)?_c('div',{staticClass:\"form-group\"},[_vm._m(32),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.animeType),expression:\"animeType\"}],attrs:{\"type\":\"radio\",\"name\":\"naming_anime\",\"id\":\"naming_anime\",\"value\":\"1\"},domProps:{\"checked\":_vm._q(_vm.animeType,\"1\")},on:{\"change\":[function($event){_vm.animeType=\"1\"},_vm.updatePatternSamples],\"input\":function($event){return _vm.update()}}}),_vm._v(\" \"),_c('span',[_vm._v(\"Add the absolute number to the season/episode format?\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Only applies to animes. (e.g. S15E45 - 310 vs S15E45)\")])])]):_vm._e(),_vm._v(\" \"),(_vm.animeType > 0)?_c('div',{staticClass:\"form-group\"},[_vm._m(33),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.animeType),expression:\"animeType\"}],attrs:{\"type\":\"radio\",\"name\":\"naming_anime\",\"id\":\"naming_anime_only\",\"value\":\"2\"},domProps:{\"checked\":_vm._q(_vm.animeType,\"2\")},on:{\"change\":[function($event){_vm.animeType=\"2\"},_vm.updatePatternSamples],\"input\":function($event){return _vm.update()}}}),_vm._v(\" \"),_c('span',[_vm._v(\"Replace season/episode format with absolute number\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Only applies to animes.\")])])]):_vm._e(),_vm._v(\" \"),(_vm.animeType > 0)?_c('div',{staticClass:\"form-group\"},[_vm._m(34),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.animeType),expression:\"animeType\"}],attrs:{\"type\":\"radio\",\"name\":\"naming_anime\",\"id\":\"naming_anime_none\",\"value\":\"3\"},domProps:{\"checked\":_vm._q(_vm.animeType,\"3\")},on:{\"change\":[function($event){_vm.animeType=\"3\"},_vm.updatePatternSamples],\"input\":function($event){return _vm.update()}}}),_vm._v(\" \"),_c('span',[_vm._v(\"Don't include the absolute number\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Only applies to animes.\")])])]):_vm._e()]):_vm._e()])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"name_presets\"}},[_c('span',[_vm._v(\"Name Pattern:\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\"},[_c('span',[_vm._v(\" \")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('thead',[_c('tr',[_c('th',{staticClass:\"align-right\"},[_vm._v(\"Meaning\")]),_vm._v(\" \"),_c('th',[_vm._v(\"Pattern\")]),_vm._v(\" \"),_c('th',{attrs:{\"width\":\"60%\"}},[_vm._v(\"Result\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tfoot',[_c('tr',[_c('th',{attrs:{\"colspan\":\"3\"}},[_vm._v(\"Use lower case if you want lower case names (eg. %sn, %e.n, %q_n etc)\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Show Name:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%SN\")]),_vm._v(\" \"),_c('td',[_vm._v(\"Show Name\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%S.N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"Show.Name\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%S_N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"Show_Name\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Season Number:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%S\")]),_vm._v(\" \"),_c('td',[_vm._v(\"2\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%0S\")]),_vm._v(\" \"),_c('td',[_vm._v(\"02\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"XEM Season Number:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%XS\")]),_vm._v(\" \"),_c('td',[_vm._v(\"2\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%0XS\")]),_vm._v(\" \"),_c('td',[_vm._v(\"02\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Episode Number:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%E\")]),_vm._v(\" \"),_c('td',[_vm._v(\"3\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%0E\")]),_vm._v(\" \"),_c('td',[_vm._v(\"03\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"XEM Episode Number:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%XE\")]),_vm._v(\" \"),_c('td',[_vm._v(\"3\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%0XE\")]),_vm._v(\" \"),_c('td',[_vm._v(\"03\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Absolute Episode Number:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%AB\")]),_vm._v(\" \"),_c('td',[_vm._v(\"003\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Xem Absolute Episode Number:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%XAB\")]),_vm._v(\" \"),_c('td',[_vm._v(\"003\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Episode Name:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%EN\")]),_vm._v(\" \"),_c('td',[_vm._v(\"Episode Name\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%E.N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"Episode.Name\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%E_N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"Episode_Name\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Air Date:\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Post-Processing Date:\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Quality:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%QN\")]),_vm._v(\" \"),_c('td',[_vm._v(\"720p BluRay\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%Q.N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"720p.BluRay\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%Q_N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"720p_BluRay\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Scene Quality:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%SQN\")]),_vm._v(\" \"),_c('td',[_vm._v(\"720p HDTV x264\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%SQ.N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"720p.HDTV.x264\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_vm._v(\" \"),_c('td',[_vm._v(\"%SQ_N\")]),_vm._v(\" \"),_c('td',[_vm._v(\"720p_HDTV_x264\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('i',{staticClass:\"glyphicon glyphicon-info-sign\",attrs:{\"title\":\"Multi-EP style is ignored\"}}),_vm._v(\" \"),_c('b',[_vm._v(\"Release Name:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%RN\")]),_vm._v(\" \"),_c('td',[_vm._v(\"Show.Name.S02E03.HDTV.x264-RLSGROUP\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',{staticClass:\"align-right\"},[_c('i',{staticClass:\"glyphicon glyphicon-info-sign\",attrs:{\"title\":\"UNKNOWN_RELEASE_GROUP is used in place of RLSGROUP if it could not be properly detected\"}}),_vm._v(\" \"),_c('b',[_vm._v(\"Release Group:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%RG\")]),_vm._v(\" \"),_c('td',[_vm._v(\"RLSGROUP\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"even\"},[_c('td',{staticClass:\"align-right\"},[_c('i',{staticClass:\"glyphicon glyphicon-info-sign\",attrs:{\"title\":\"If episode is proper/repack add 'proper' to name.\"}}),_vm._v(\" \"),_c('b',[_vm._v(\"Release Type:\")])]),_vm._v(\" \"),_c('td',[_vm._v(\"%RT\")]),_vm._v(\" \"),_c('td',[_vm._v(\"PROPER\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"naming_multi_ep\"}},[_c('span',[_vm._v(\"Multi-Episode Style:\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"naming_anime\"}},[_c('span',[_vm._v(\"Add Absolute Number\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"naming_anime_only\"}},[_c('span',[_vm._v(\"Only Absolute Number\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"naming_anime_none\"}},[_c('span',[_vm._v(\"No Absolute Number\")])])}]\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./plot-info.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./plot-info.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./plot-info.vue?vue&type=template&id=b6b71cd8&\"\nimport script from \"./plot-info.vue?vue&type=script&lang=js&\"\nexport * from \"./plot-info.vue?vue&type=script&lang=js&\"\nimport style0 from \"./plot-info.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.description !== '')?_c('img',{directives:[{name:\"tooltip\",rawName:\"v-tooltip.right\",value:({content: _vm.description}),expression:\"{content: description}\",modifiers:{\"right\":true}}],class:_vm.plotInfoClass,attrs:{\"src\":\"images/info32.png\",\"width\":\"16\",\"height\":\"16\",\"alt\":\"\"}}):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./quality-chooser.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./quality-chooser.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./quality-chooser.vue?vue&type=template&id=751f4e5c&scoped=true&\"\nimport script from \"./quality-chooser.vue?vue&type=script&lang=js&\"\nexport * from \"./quality-chooser.vue?vue&type=script&lang=js&\"\nimport style0 from \"./quality-chooser.vue?vue&type=style&index=0&id=751f4e5c&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  \"751f4e5c\",\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('select',{directives:[{name:\"model\",rawName:\"v-model.number\",value:(_vm.selectedQualityPreset),expression:\"selectedQualityPreset\",modifiers:{\"number\":true}}],staticClass:\"form-control form-control-inline input-sm\",attrs:{\"name\":\"quality_preset\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return _vm._n(val)}); _vm.selectedQualityPreset=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[(_vm.keep)?_c('option',{attrs:{\"value\":\"keep\"}},[_vm._v(\"< Keep >\")]):_vm._e(),_vm._v(\" \"),_c('option',{domProps:{\"value\":0}},[_vm._v(\"Custom\")]),_vm._v(\" \"),_vm._l((_vm.qualityPresets),function(preset){return _c('option',{key:(\"quality-preset-\" + (preset.key)),domProps:{\"value\":preset.value}},[_vm._v(\"\\n            \"+_vm._s(preset.name)+\"\\n        \")])})],2),_vm._v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.selectedQualityPreset === 0),expression:\"selectedQualityPreset === 0\"}],attrs:{\"id\":\"customQualityWrapper\"}},[_vm._m(0),_vm._v(\" \"),_c('div',[_c('h5',[_vm._v(\"Allowed\")]),_vm._v(\" \"),_c('select',{directives:[{name:\"model\",rawName:\"v-model.number\",value:(_vm.allowedQualities),expression:\"allowedQualities\",modifiers:{\"number\":true}}],staticClass:\"form-control form-control-inline input-sm\",attrs:{\"name\":\"allowed_qualities\",\"multiple\":\"multiple\",\"size\":_vm.validQualities.length},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return _vm._n(val)}); _vm.allowedQualities=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},_vm._l((_vm.validQualities),function(quality){return _c('option',{key:(\"quality-list-\" + (quality.key)),domProps:{\"value\":quality.value}},[_vm._v(\"\\n                    \"+_vm._s(quality.name)+\"\\n                \")])}),0)]),_vm._v(\" \"),_c('div',[_c('h5',[_vm._v(\"Preferred\")]),_vm._v(\" \"),_c('select',{directives:[{name:\"model\",rawName:\"v-model.number\",value:(_vm.preferredQualities),expression:\"preferredQualities\",modifiers:{\"number\":true}}],staticClass:\"form-control form-control-inline input-sm\",attrs:{\"name\":\"preferred_qualities\",\"multiple\":\"multiple\",\"size\":_vm.validQualities.length,\"disabled\":_vm.allowedQualities.length === 0},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return _vm._n(val)}); _vm.preferredQualities=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},_vm._l((_vm.validQualities),function(quality){return _c('option',{key:(\"quality-list-\" + (quality.key)),domProps:{\"value\":quality.value}},[_vm._v(\"\\n                    \"+_vm._s(quality.name)+\"\\n                \")])}),0)])]),_vm._v(\" \"),(_vm.selectedQualityPreset !== 'keep')?_c('div',[((_vm.allowedQualities.length + _vm.preferredQualities.length) >= 1)?_c('div',{attrs:{\"id\":\"qualityExplanation\"}},[_vm._m(1),_vm._v(\" \"),(_vm.preferredQualities.length === 0)?_c('h5',[_vm._v(\"\\n                This will download \"),_c('b',[_vm._v(\"any\")]),_vm._v(\" of these qualities and then stops searching:\\n                \"),_c('label',{attrs:{\"id\":\"allowedExplanation\"}},[_vm._v(_vm._s(_vm.explanation.allowed.join(', ')))])]):[_c('h5',[_vm._v(\"\\n                    Downloads \"),_c('b',[_vm._v(\"any\")]),_vm._v(\" of these qualities:\\n                    \"),_c('label',{attrs:{\"id\":\"allowedPreferredExplanation\"}},[_vm._v(_vm._s(_vm.explanation.allowed.join(', ')))])]),_vm._v(\" \"),_c('h5',[_vm._v(\"\\n                    But it will stop searching when one of these is downloaded:\\n                    \"),_c('label',{attrs:{\"id\":\"preferredExplanation\"}},[_vm._v(_vm._s(_vm.explanation.preferred.join(', ')))])])]],2):_c('div',[_vm._v(\"Please select at least one allowed quality.\")])]):_vm._e(),_vm._v(\" \"),(_vm.backloggedEpisodes)?_c('div',[_c('h5',{staticClass:\"{ 'red-text': !backloggedEpisodes.status }\",domProps:{\"innerHTML\":_vm._s(_vm.backloggedEpisodes.html)}})]):_vm._e(),_vm._v(\" \"),(_vm.archive)?_c('div',{attrs:{\"id\":\"archive\"}},[_c('h5',[_c('b',[_vm._v(\"Archive downloaded episodes that are not currently in\\n                \"),_c('app-link',{staticClass:\"backlog-link\",attrs:{\"href\":\"manage/backlogOverview/\",\"target\":\"_blank\"}},[_vm._v(\"backlog\")]),_vm._v(\".\")],1),_vm._v(\" \"),_c('br'),_vm._v(\"Avoids unnecessarily increasing your backlog\\n            \"),_c('br')]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa btn-inline\",attrs:{\"disabled\":_vm.archiveButton.disabled},on:{\"click\":function($event){$event.preventDefault();return _vm.archiveEpisodes($event)}}},[_vm._v(\"\\n            \"+_vm._s(_vm.archiveButton.text)+\"\\n        \")]),_vm._v(\" \"),_c('h5',[_vm._v(_vm._s(_vm.archivedStatus))])]):_vm._e()])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('p',[_c('b',[_c('strong',[_vm._v(\"Preferred\")])]),_vm._v(\" qualities will replace those in \"),_c('b',[_c('strong',[_vm._v(\"allowed\")])]),_vm._v(\", even if they are lower.\\n        \")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('h5',[_c('b',[_vm._v(\"Quality setting explanation:\")])])}]\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./scroll-buttons.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./scroll-buttons.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./scroll-buttons.vue?vue&type=template&id=03c5223c&\"\nimport script from \"./scroll-buttons.vue?vue&type=script&lang=js&\"\nexport * from \"./scroll-buttons.vue?vue&type=script&lang=js&\"\nimport style0 from \"./scroll-buttons.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"scroll-buttons-wrapper\"}},[_c('div',{staticClass:\"scroll-wrapper top\",class:{ show: _vm.showToTop },on:{\"click\":function($event){$event.preventDefault();return _vm.scrollTop($event)}}},[_vm._m(0)]),_vm._v(\" \"),_c('div',{staticClass:\"scroll-wrapper left\",class:{ show: _vm.showLeftRight }},[_c('span',{staticClass:\"scroll-left-inner\"},[_c('i',{staticClass:\"glyphicon glyphicon-circle-arrow-left\",on:{\"click\":function($event){$event.preventDefault();return _vm.scrollLeft($event)}}})])]),_vm._v(\" \"),_c('div',{staticClass:\"scroll-wrapper right\",class:{ show: _vm.showLeftRight }},[_c('span',{staticClass:\"scroll-right-inner\"},[_c('i',{staticClass:\"glyphicon glyphicon-circle-arrow-right\",on:{\"click\":function($event){$event.preventDefault();return _vm.scrollRight($event)}}})])])])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('span',{staticClass:\"scroll-top-inner\"},[_c('i',{staticClass:\"glyphicon glyphicon-circle-arrow-up\"})])}]\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./select-list.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./select-list.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./select-list.vue?vue&type=template&id=e3747674&scoped=true&\"\nimport script from \"./select-list.vue?vue&type=script&lang=js&\"\nexport * from \"./select-list.vue?vue&type=script&lang=js&\"\nimport style0 from \"./select-list.vue?vue&type=style&index=0&id=e3747674&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  \"e3747674\",\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',_vm._b({staticClass:\"select-list max-width\"},'div',{disabled: _vm.disabled},false),[_c('i',{staticClass:\"switch-input glyphicon glyphicon-refresh\",attrs:{\"title\":\"Switch between a list and comma separated values\"},on:{\"click\":function($event){return _vm.switchFields()}}}),_vm._v(\" \"),(!_vm.csvMode)?_c('ul',[_vm._l((_vm.editItems),function(item){return _c('li',{key:item.id},[_c('div',{staticClass:\"input-group\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(item.value),expression:\"item.value\"}],staticClass:\"form-control input-sm\",attrs:{\"type\":\"text\"},domProps:{\"value\":(item.value)},on:{\"input\":[function($event){if($event.target.composing){ return; }_vm.$set(item, \"value\", $event.target.value)},function($event){return _vm.removeEmpty(item)}]}}),_vm._v(\" \"),_c('div',{staticClass:\"input-group-btn\",on:{\"click\":function($event){return _vm.deleteItem(item)}}},[_vm._m(0,true)])])])}),_vm._v(\" \"),_c('div',{staticClass:\"new-item\"},[_c('div',{staticClass:\"input-group\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.newItem),expression:\"newItem\"}],ref:\"newItemInput\",staticClass:\"form-control input-sm\",attrs:{\"type\":\"text\",\"placeholder\":\"add new values per line\"},domProps:{\"value\":(_vm.newItem)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.newItem=$event.target.value}}}),_vm._v(\" \"),_c('div',{staticClass:\"input-group-btn\",on:{\"click\":function($event){return _vm.addNewItem()}}},[_vm._m(1)])])]),_vm._v(\" \"),(_vm.newItem.length > 0)?_c('div',{staticClass:\"new-item-help\"},[_vm._v(\"\\n            Click \"),_c('i',{staticClass:\"glyphicon glyphicon-plus\"}),_vm._v(\" to finish adding the value.\\n        \")]):_vm._e()],2):_c('div',{staticClass:\"csv\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.csv),expression:\"csv\"}],staticClass:\"form-control input-sm\",attrs:{\"type\":\"text\",\"placeholder\":\"add values comma separated\"},domProps:{\"value\":(_vm.csv)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.csv=$event.target.value}}})])])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"btn btn-default input-sm\",staticStyle:{\"font-size\":\"14px\"}},[_c('i',{staticClass:\"glyphicon glyphicon-remove\",attrs:{\"title\":\"Remove\"}})])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"btn btn-default input-sm\",staticStyle:{\"font-size\":\"14px\"}},[_c('i',{staticClass:\"glyphicon glyphicon-plus\",attrs:{\"title\":\"Add\"}})])}]\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./show-selector.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./show-selector.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./show-selector.vue?vue&type=template&id=7c6db9fa&\"\nimport script from \"./show-selector.vue?vue&type=script&lang=js&\"\nexport * from \"./show-selector.vue?vue&type=script&lang=js&\"\nimport style0 from \"./show-selector.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"show-selector form-inline hidden-print\"},[_c('div',{staticClass:\"select-show-group pull-left top-5 bottom-5\"},[(_vm.shows.length === 0)?_c('select',{class:_vm.selectClass,attrs:{\"disabled\":\"\"}},[_c('option',[_vm._v(\"Loading...\")])]):_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedShowSlug),expression:\"selectedShowSlug\"}],class:_vm.selectClass,on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedShowSlug=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},function($event){return _vm.$emit('change', _vm.selectedShowSlug)}]}},[(_vm.placeholder)?_c('option',{attrs:{\"disabled\":\"\",\"hidden\":\"\"},domProps:{\"value\":_vm.placeholder,\"selected\":!_vm.selectedShowSlug}},[_vm._v(_vm._s(_vm.placeholder))]):_vm._e(),_vm._v(\" \"),(_vm.whichList === -1)?_vm._l((_vm.showLists),function(curShowList){return _c('optgroup',{key:curShowList.type,attrs:{\"label\":curShowList.type}},_vm._l((curShowList.shows),function(show){return _c('option',{key:show.id.slug,domProps:{\"value\":show.id.slug}},[_vm._v(_vm._s(show.title))])}),0)}):_vm._l((_vm.showLists[_vm.whichList].shows),function(show){return _c('option',{key:show.id.slug,domProps:{\"value\":show.id.slug}},[_vm._v(_vm._s(show.title))])})],2)])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./state-switch.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./state-switch.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./state-switch.vue?vue&type=template&id=3464a40e&\"\nimport script from \"./state-switch.vue?vue&type=script&lang=js&\"\nexport * from \"./state-switch.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('img',_vm._b({attrs:{\"height\":\"16\",\"width\":\"16\"},on:{\"click\":function($event){return _vm.$emit('click')}}},'img',{ src: _vm.src, alt: _vm.alt },false))}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","export { default as AppLink } from './app-link.vue';\nexport { default as Asset } from './asset.vue';\nexport { default as ConfigTemplate } from './config-template.vue';\nexport { default as ConfigTextboxNumber } from './config-textbox-number.vue';\nexport { default as ConfigTextbox } from './config-textbox.vue';\nexport { default as ConfigToggleSlider } from './config-toggle-slider.vue';\nexport { default as FileBrowser } from './file-browser.vue';\nexport { default as LanguageSelect } from './language-select.vue';\nexport { default as NamePattern } from './name-pattern.vue';\nexport { default as PlotInfo } from './plot-info.vue';\nexport { default as QualityChooser } from './quality-chooser.vue';\nexport { default as QualityPill } from './quality-pill.vue';\nexport { default as ScrollButtons } from './scroll-buttons.vue';\nexport { default as SelectList } from './select-list.vue';\nexport { default as ShowSelector } from './show-selector.vue';\nexport { default as StateSwitch } from './state-switch.vue';\n","import axios from 'axios';\n\nexport const webRoot = document.body.getAttribute('web-root');\nexport const apiKey = document.body.getAttribute('api-key');\n\n/**\n * Api client based on the axios client, to communicate with medusa's web routes, which return json data.\n */\nexport const apiRoute = axios.create({\n    baseURL: webRoot + '/',\n    timeout: 60000,\n    headers: {\n        Accept: 'application/json',\n        'Content-Type': 'application/json'\n    }\n});\n\n/**\n * Api client based on the axios client, to communicate with medusa's api v1.\n */\nexport const apiv1 = axios.create({\n    baseURL: webRoot + '/api/v1/' + apiKey + '/',\n    timeout: 30000,\n    headers: {\n        Accept: 'application/json',\n        'Content-Type': 'application/json'\n    }\n});\n\n/**\n * Api client based on the axios client, to communicate with medusa's api v2.\n */\nexport const api = axios.create({\n    baseURL: webRoot + '/api/v2/',\n    timeout: 30000,\n    headers: {\n        Accept: 'application/json',\n        'Content-Type': 'application/json',\n        'X-Api-Key': apiKey\n    }\n});\n","export const isDevelopment = process.env.NODE_ENV === 'development';\n\n/**\n * Calculate the combined value of the selected qualities.\n * @param {number[]} allowedQualities - Array of allowed qualities.\n * @param {number[]} [preferredQualities=[]] - Array of preferred qualities.\n * @returns {number} An unsigned integer.\n */\nexport const combineQualities = (allowedQualities, preferredQualities = []) => {\n    const reducer = (accumulator, currentValue) => accumulator | currentValue;\n    const allowed = allowedQualities.reduce(reducer, 0);\n    const preferred = preferredQualities.reduce(reducer, 0);\n\n    return (allowed | (preferred << 16)) >>> 0; // Unsigned int\n};\n\n/**\n * Return a human readable representation of the provided size.\n * @param {number} bytes - The size in bytes to convert\n * @param {boolean} [useDecimal=false] - Use decimal instead of binary prefixes (e.g. kilo = 1000 instead of 1024)\n * @returns {string} The converted size.\n */\nexport const humanFileSize = (bytes, useDecimal = false) => {\n    if (!bytes) {\n        bytes = 0;\n    }\n\n    bytes = Math.max(bytes, 0);\n\n    const thresh = useDecimal ? 1000 : 1024;\n    if (Math.abs(bytes) < thresh) {\n        return bytes.toFixed(2) + ' B';\n    }\n    const units = ['KB', 'MB', 'GB', 'TB', 'PB'];\n    let u = -1;\n    do {\n        bytes /= thresh;\n        ++u;\n    } while (Math.abs(bytes) >= thresh && u < units.length - 1);\n\n    return `${bytes.toFixed(2)} ${units[u]}`;\n};\n\n// Maps Python date/time tokens to date-fns tokens\n// Python: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior\n// date-fns: https://date-fns.org/v2.0.0-alpha.27/docs/format\nconst datePresetMap = {\n    '%a': 'ccc', // Weekday name, short\n    '%A': 'cccc', // Weekday name, full\n    '%w': 'c', // Weekday number\n    '%d': 'dd', // Day of the month, zero-padded\n    '%b': 'LLL', // Month name, short\n    '%B': 'LLLL', // Month name, full\n    '%m': 'MM', // Month number, zero-padded\n    '%y': 'yy', // Year without century, zero-padded\n    '%Y': 'yyyy', // Year with century\n    '%H': 'HH', // Hour (24-hour clock), zero-padded\n    '%I': 'hh', // Hour (12-hour clock), zero-padded\n    '%p': 'a', // AM / PM\n    '%M': 'mm', // Minute, zero-padded\n    '%S': 'ss', // Second, zero-padded\n    '%f': 'SSSSSS', // Microsecond, zero-padded\n    '%z': 'xx', // UTC offset in the form +HHMM or -HHMM\n    // '%Z': '', // [UNSUPPORTED] Time zone name\n    '%j': 'DDD', // Day of the year, zero-padded\n    '%U': 'II', // Week number of the year (Sunday as the first day of the week), zero padded\n    '%W': 'ww', // Week number of the year (Monday as the first day of the week)\n    '%c': 'Pp', // Locale's appropriate date and time representation\n    '%x': 'P', // Locale's appropriate date representation\n    '%X': 'p', // Locale's appropriate time representation\n    '%%': '%' // Literal '%' character\n};\n\n/**\n * Convert a Python date format to a DateFns compatible date format.\n * Automatically escapes non-token characters.\n * @param {string} format - The Python date format.\n * @returns {string} The new format.\n */\nexport const convertDateFormat = format => {\n    let newFormat = '';\n    let index = 0;\n    let escaping = false;\n    while (index < format.length) {\n        const chr = format.charAt(index);\n        // Escape single quotes\n        if (chr === \"'\") {\n            newFormat += chr + chr;\n        } else if (chr === '%') {\n            if (escaping) {\n                escaping = false;\n                newFormat += \"'\";\n            }\n\n            ++index;\n            if (index === format.length) {\n                throw new Error(`Single % at end of format string: ${format}`);\n            }\n            const chr2 = format.charAt(index);\n            const tokenKey = chr + chr2;\n            const token = datePresetMap[tokenKey];\n            if (token === undefined) {\n                throw new Error(`Unrecognized token \"${tokenKey}\" in format string: ${format}`);\n            }\n            newFormat += token;\n        // Only letters need to escaped\n        } else if (/[^a-z]/i.test(chr)) {\n            if (escaping) {\n                escaping = false;\n                newFormat += \"'\";\n            }\n            newFormat += chr;\n        // Escape anything else\n        } else {\n            if (!escaping) {\n                escaping = true;\n                newFormat += \"'\";\n            }\n            newFormat += chr;\n        }\n\n        ++index;\n\n        if (index === format.length && escaping) {\n            newFormat += \"'\";\n        }\n    }\n    return newFormat;\n};\n\n/**\n * Create an array with unique strings\n * @param {string[]} array - array with strings\n * @returns {string[]} array with unique strings\n */\nexport const arrayUnique = array => {\n    return array.reduce((result, item) => {\n        return result.includes(item) ? result : result.concat(item);\n    }, []);\n};\n\n/**\n * Exclude strings out of the array `exclude` compared to the strings in the array baseArray.\n * @param {string[]} baseArray - array of strings\n * @param {string[]} exclude - array of strings which we want to exclude in baseArray\n * @returns {string[]} reduced array\n */\nexport const arrayExclude = (baseArray, exclude) => {\n    return baseArray.filter(item => !exclude.includes(item));\n};\n\n/**\n * A simple wait function.\n * @param {number} ms - Time to wait.\n * @returns {Promise} Resolves when done waiting.\n */\nexport const wait = /* istanbul ignore next */ ms => new Promise(resolve => setTimeout(resolve, ms));\n\n/**\n * Returns when `check` evaluates as truthy.\n * @param {function} check - Function to evaluate every poll interval.\n * @param {number} [poll=100] - Interval to check, in milliseconds.\n * @param {number} [timeout=3000] - Timeout to stop waiting after, in milliseconds.\n * @returns {Promise} The approximate amount of time waited, in milliseconds.\n * @throws Will throw an error when the timeout has been exceeded.\n */\nexport const waitFor = /* istanbul ignore next */ async (check, poll = 100, timeout = 3000) => {\n    let ms = 0;\n    while (!check()) {\n        await wait(poll); // eslint-disable-line no-await-in-loop\n        ms += poll;\n        if (ms > timeout) {\n            throw new Error(`waitFor timed out (${timeout}ms)`);\n        }\n    }\n    return ms;\n};\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./backstretch.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./backstretch.vue?vue&type=script&lang=js&\"","var render, staticRenderFns\nimport script from \"./backstretch.vue?vue&type=script&lang=js&\"\nexport * from \"./backstretch.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n  script,\n  render,\n  staticRenderFns,\n  false,\n  null,\n  null,\n  null\n  \n)\n\nexport default component.exports","const LOGIN_PENDING = '🔒 Logging in';\nconst LOGIN_SUCCESS = '🔒 ✅ Login Successful';\nconst LOGIN_FAILED = '🔒 ❌ Login Failed';\nconst LOGOUT = '🔒 Logout';\nconst REFRESH_TOKEN = '🔒 Refresh Token';\nconst REMOVE_AUTH_ERROR = '🔒 Remove Auth Error';\nconst SOCKET_ONOPEN = '🔗 ✅ WebSocket connected';\nconst SOCKET_ONCLOSE = '🔗 ❌ WebSocket disconnected';\nconst SOCKET_ONERROR = '🔗 ❌ WebSocket error';\nconst SOCKET_ONMESSAGE = '🔗 ✉️ 📥 WebSocket message received';\nconst SOCKET_RECONNECT = '🔗 🔃 WebSocket reconnecting';\nconst SOCKET_RECONNECT_ERROR = '🔗 🔃 ❌ WebSocket reconnection attempt failed';\nconst NOTIFICATIONS_ENABLED = '🔔 Notifications Enabled';\nconst NOTIFICATIONS_DISABLED = '🔔 Notifications Disabled';\nconst ADD_CONFIG = '⚙️ Config added to store';\nconst ADD_SHOW = '📺 Show added to store';\nconst ADD_SHOW_EPISODE = '📺 Shows season with episodes added to store';\nconst ADD_STATS = 'ℹ️ Statistics added to store';\n\nexport {\n    LOGIN_PENDING,\n    LOGIN_SUCCESS,\n    LOGIN_FAILED,\n    LOGOUT,\n    REFRESH_TOKEN,\n    REMOVE_AUTH_ERROR,\n    SOCKET_ONOPEN,\n    SOCKET_ONCLOSE,\n    SOCKET_ONERROR,\n    SOCKET_ONMESSAGE,\n    SOCKET_RECONNECT,\n    SOCKET_RECONNECT_ERROR,\n    NOTIFICATIONS_ENABLED,\n    NOTIFICATIONS_DISABLED,\n    ADD_CONFIG,\n    ADD_SHOW,\n    ADD_SHOW_EPISODE,\n    ADD_STATS\n};\n","import {\n    LOGIN_PENDING,\n    LOGIN_SUCCESS,\n    LOGIN_FAILED,\n    LOGOUT,\n    REFRESH_TOKEN,\n    REMOVE_AUTH_ERROR\n} from '../mutation-types';\n\nconst state = {\n    isAuthenticated: false,\n    user: {},\n    tokens: {\n        access: null,\n        refresh: null\n    },\n    error: null\n};\n\nconst mutations = {\n    [LOGIN_PENDING]() { },\n    [LOGIN_SUCCESS](state, user) {\n        state.user = user;\n        state.isAuthenticated = true;\n        state.error = null;\n    },\n    [LOGIN_FAILED](state, { error }) {\n        state.user = {};\n        state.isAuthenticated = false;\n        state.error = error;\n    },\n    [LOGOUT](state) {\n        state.user = {};\n        state.isAuthenticated = false;\n        state.error = null;\n    },\n    [REFRESH_TOKEN]() {},\n    [REMOVE_AUTH_ERROR]() {}\n};\n\nconst getters = {};\n\nconst actions = {\n    login(context, credentials) {\n        const { commit } = context;\n        commit(LOGIN_PENDING);\n\n        // @TODO: Add real JWT login\n        const apiLogin = credentials => Promise.resolve(credentials);\n\n        return apiLogin(credentials).then(user => {\n            commit(LOGIN_SUCCESS, user);\n            return { success: true };\n        }).catch(error => {\n            commit(LOGIN_FAILED, { error, credentials });\n            return { success: false, error };\n        });\n    },\n    logout(context) {\n        const { commit } = context;\n        commit(LOGOUT);\n    }\n};\n\nexport default {\n    state,\n    mutations,\n    getters,\n    actions\n};\n","import { ADD_CONFIG } from '../mutation-types';\n\nconst state = {\n    torrents: {\n        authType: null,\n        dir: null,\n        enabled: null,\n        highBandwidth: null,\n        host: null,\n        label: null,\n        labelAnime: null,\n        method: null,\n        path: null,\n        paused: null,\n        rpcUrl: null,\n        seedLocation: null,\n        seedTime: null,\n        username: null,\n        password: null,\n        verifySSL: null,\n        testStatus: 'Click below to test'\n    },\n    nzb: {\n        enabled: null,\n        method: null,\n        nzbget: {\n            category: null,\n            categoryAnime: null,\n            categoryAnimeBacklog: null,\n            categoryBacklog: null,\n            host: null,\n            priority: null,\n            useHttps: null,\n            username: null,\n            password: null\n        },\n        sabnzbd: {\n            category: null,\n            forced: null,\n            categoryAnime: null,\n            categoryBacklog: null,\n            categoryAnimeBacklog: null,\n            host: null,\n            username: null,\n            password: null,\n            apiKey: null\n        }\n    }\n};\n\nconst mutations = {\n    [ADD_CONFIG](state, { section, config }) {\n        if (section === 'clients') {\n            state = Object.assign(state, config);\n        }\n    }\n};\n\nconst getters = {};\n\nconst actions = {};\n\nexport default {\n    state,\n    mutations,\n    getters,\n    actions\n};\n","import { api } from '../../api';\nimport { ADD_CONFIG } from '../mutation-types';\nimport { arrayUnique, arrayExclude } from '../../utils/core';\n\nconst state = {\n    wikiUrl: null,\n    donationsUrl: null,\n    localUser: null,\n    posterSortdir: null,\n    locale: null,\n    themeName: null,\n    selectedRootIndex: null,\n    webRoot: null,\n    namingForceFolders: null,\n    cacheDir: null,\n    databaseVersion: {\n        major: null,\n        minor: null\n    },\n    programDir: null,\n    dataDir: null,\n    animeSplitHomeInTabs: null,\n    torrents: {\n        authType: null,\n        dir: null,\n        enabled: null,\n        highBandwidth: null,\n        host: null,\n        label: null,\n        labelAnime: null,\n        method: null,\n        path: null,\n        paused: null,\n        rpcurl: null,\n        seedLocation: null,\n        seedTime: null,\n        username: null,\n        verifySSL: null\n    },\n    layout: {\n        show: {\n            specials: null,\n            showListOrder: []\n        },\n        home: null,\n        history: null,\n        schedule: null\n    },\n    dbPath: null,\n    nzb: {\n        enabled: null,\n        method: null,\n        nzbget: {\n            category: null,\n            categoryAnime: null,\n            categoryAnimeBacklog: null,\n            categoryBacklog: null,\n            host: null,\n            priority: null,\n            useHttps: null,\n            username: null\n        },\n        sabnzbd: {\n            category: null,\n            forced: null,\n            categoryAnime: null,\n            categoryBacklog: null,\n            categoryAnimeBacklog: null,\n            host: null,\n            username: null,\n            password: null,\n            apiKey: null\n        }\n    },\n    configFile: null,\n    fanartBackground: null,\n    trimZero: null,\n    animeSplitHome: null,\n    gitUsername: null,\n    branch: null,\n    commitHash: null,\n    indexers: {\n        config: {\n            main: {\n                externalMappings: {},\n                statusMap: {},\n                traktIndexers: {},\n                validLanguages: [],\n                langabbvToId: {}\n            },\n            indexers: {\n                tvdb: {\n                    apiParams: {\n                        useZip: null,\n                        language: null\n                    },\n                    baseUrl: null,\n                    enabled: null,\n                    icon: null,\n                    id: null,\n                    identifier: null,\n                    mappedTo: null,\n                    name: null,\n                    scene_loc: null, // eslint-disable-line camelcase\n                    showUrl: null,\n                    xemOrigin: null\n                },\n                tmdb: {\n                    apiParams: {\n                        useZip: null,\n                        language: null\n                    },\n                    baseUrl: null,\n                    enabled: null,\n                    icon: null,\n                    id: null,\n                    identifier: null,\n                    mappedTo: null,\n                    name: null,\n                    scene_loc: null, // eslint-disable-line camelcase\n                    showUrl: null,\n                    xemOrigin: null\n                },\n                tvmaze: {\n                    apiParams: {\n                        useZip: null,\n                        language: null\n                    },\n                    baseUrl: null,\n                    enabled: null,\n                    icon: null,\n                    id: null,\n                    identifier: null,\n                    mappedTo: null,\n                    name: null,\n                    scene_loc: null, // eslint-disable-line camelcase\n                    showUrl: null,\n                    xemOrigin: null\n                }\n            }\n        }\n    },\n    sourceUrl: null,\n    rootDirs: [],\n    fanartBackgroundOpacity: null,\n    appArgs: [],\n    comingEpsDisplayPaused: null,\n    sortArticle: null,\n    timePreset: null,\n    subtitles: {\n        enabled: null\n    },\n    fuzzyDating: null,\n    backlogOverview: {\n        status: null,\n        period: null\n    },\n    posterSortby: null,\n    news: {\n        lastRead: null,\n        latest: null,\n        unread: null\n    },\n    logs: {\n        debug: null,\n        dbDebug: null,\n        loggingLevels: {},\n        numErrors: null,\n        numWarnings: null\n    },\n    failedDownloads: {\n        enabled: null,\n        deleteFailed: null\n    },\n    postProcessing: {\n        naming: {\n            pattern: null,\n            multiEp: null,\n            enableCustomNamingSports: null,\n            enableCustomNamingAirByDate: null,\n            patternSports: null,\n            patternAirByDate: null,\n            enableCustomNamingAnime: null,\n            patternAnime: null,\n            animeMultiEp: null,\n            animeNamingType: null,\n            stripYear: null\n        },\n        showDownloadDir: null,\n        processAutomatically: null,\n        processMethod: null,\n        deleteRarContent: null,\n        unpack: null,\n        noDelete: null,\n        reflinkAvailable: null,\n        postponeIfSyncFiles: null,\n        autoPostprocessorFrequency: 10,\n        airdateEpisodes: null,\n        moveAssociatedFiles: null,\n        allowedExtensions: [],\n        addShowsWithoutDir: null,\n        createMissingShowDirs: null,\n        renameEpisodes: null,\n        postponeIfNoSubs: null,\n        nfoRename: null,\n        syncFiles: [],\n        fileTimestampTimezone: 'local',\n        extraScripts: [],\n        extraScriptsUrl: null,\n        multiEpStrings: {}\n    },\n    sslVersion: null,\n    pythonVersion: null,\n    comingEpsSort: null,\n    githubUrl: null,\n    datePreset: null,\n    subtitlesMulti: null,\n    pid: null,\n    os: null,\n    anonRedirect: null,\n    logDir: null,\n    recentShows: [],\n    randomShowSlug: null, // @TODO: Recreate this in Vue when the webapp has a reliable list of shows to choose from.\n    showDefaults: {\n        status: null,\n        statusAfter: null,\n        quality: null,\n        subtitles: null,\n        seasonFolders: null,\n        anime: null,\n        scene: null\n    }\n};\n\nconst mutations = {\n    [ADD_CONFIG](state, { section, config }) {\n        if (section === 'main') {\n            state = Object.assign(state, config);\n        }\n    }\n};\n\nconst getters = {\n    layout: state => layout => state.layout[layout],\n    effectiveIgnored: (state, _, rootState) => series => {\n        const seriesIgnored = series.config.release.ignoredWords.map(x => x.toLowerCase());\n        const globalIgnored = rootState.search.filters.ignored.map(x => x.toLowerCase());\n        if (!series.config.release.ignoredWordsExclude) {\n            return arrayUnique(globalIgnored.concat(seriesIgnored));\n        }\n        return arrayExclude(globalIgnored, seriesIgnored);\n    },\n    effectiveRequired: (state, _, rootState) => series => {\n        const globalRequired = rootState.search.filters.required.map(x => x.toLowerCase());\n        const seriesRequired = series.config.release.requiredWords.map(x => x.toLowerCase());\n        if (!series.config.release.requiredWordsExclude) {\n            return arrayUnique(globalRequired.concat(seriesRequired));\n        }\n        return arrayExclude(globalRequired, seriesRequired);\n    },\n    // Get an indexer's name using its ID.\n    indexerIdToName: state => indexerId => {\n        if (!indexerId) {\n            return undefined;\n        }\n        const { indexers } = state.indexers.config;\n        return Object.keys(indexers).find(name => indexers[name].id === parseInt(indexerId, 10));\n    },\n    // Get an indexer's ID using its name.\n    indexerNameToId: state => indexerName => {\n        if (!indexerName) {\n            return undefined;\n        }\n        const { indexers } = state.indexers.config;\n        return indexers[name].id;\n    }\n};\n\nconst actions = {\n    getConfig(context, section) {\n        const { commit } = context;\n        return api.get('/config/' + (section || '')).then(res => {\n            if (section) {\n                const config = res.data;\n                commit(ADD_CONFIG, { section, config });\n                return config;\n            }\n\n            const sections = res.data;\n            Object.keys(sections).forEach(section => {\n                const config = sections[section];\n                commit(ADD_CONFIG, { section, config });\n            });\n            return sections;\n        });\n    },\n    setConfig(context, { section, config }) {\n        if (section !== 'main') {\n            return;\n        }\n\n        // If an empty config object was passed, use the current state config\n        config = Object.keys(config).length === 0 ? context.state : config;\n\n        return api.patch('config/' + section, config);\n    },\n    updateConfig(context, { section, config }) {\n        const { commit } = context;\n        return commit(ADD_CONFIG, { section, config });\n    },\n    setLayout(context, { page, layout }) {\n        return api.patch('config/main', {\n            layout: {\n                [page]: layout\n            }\n        }).then(() => {\n            setTimeout(() => {\n                // For now we reload the page since the layouts use python still\n                location.reload();\n            }, 500);\n        });\n    }\n};\n\nexport default {\n    state,\n    mutations,\n    getters,\n    actions\n};\n","import { ADD_CONFIG } from '../mutation-types';\n\n/**\n * An object representing a split quality.\n *\n * @typedef {Object} Quality\n * @property {number[]} allowed - Allowed qualities\n * @property {number[]} preferred - Preferred qualities\n */\n\nconst state = {\n    qualities: {\n        values: [],\n        anySets: [],\n        presets: []\n    },\n    statuses: []\n};\n\nconst mutations = {\n    [ADD_CONFIG](state, { section, config }) {\n        if (section === 'consts') {\n            state = Object.assign(state, config);\n        }\n    }\n};\n\nconst getters = {\n    // Get a quality object using a key or a value\n    getQuality: state => ({ key, value }) => {\n        if ([key, value].every(x => x === undefined) || [key, value].every(x => x !== undefined)) {\n            throw new Error('Conflict in `getQuality`: Please provide either `key` or `value`.');\n        }\n        return state.qualities.values.find(quality => key === quality.key || value === quality.value);\n    },\n    // Get a quality any-set object using a key or a value\n    getQualityAnySet: state => ({ key, value }) => {\n        if ([key, value].every(x => x === undefined) || [key, value].every(x => x !== undefined)) {\n            throw new Error('Conflict in `getQualityAnySet`: Please provide either `key` or `value`.');\n        }\n        return state.qualities.anySets.find(preset => key === preset.key || value === preset.value);\n    },\n    // Get a quality preset object using a key or a value\n    getQualityPreset: state => ({ key, value }) => {\n        if ([key, value].every(x => x === undefined) || [key, value].every(x => x !== undefined)) {\n            throw new Error('Conflict in `getQualityPreset`: Please provide either `key` or `value`.');\n        }\n        return state.qualities.presets.find(preset => key === preset.key || value === preset.value);\n    },\n    // Get a status object using a key or a value\n    getStatus: state => ({ key, value }) => {\n        if ([key, value].every(x => x === undefined) || [key, value].every(x => x !== undefined)) {\n            throw new Error('Conflict in `getStatus`: Please provide either `key` or `value`.');\n        }\n        return state.statuses.find(status => key === status.key || value === status.value);\n    },\n    // Get an episode overview status using the episode status and quality\n    // eslint-disable-next-line no-unused-vars\n    getOverviewStatus: _state => (status, quality, showQualities) => {\n        if (['Unset', 'Unaired'].includes(status)) {\n            return 'Unaired';\n        }\n\n        if (['Skipped', 'Ignored'].includes(status)) {\n            return 'Skipped';\n        }\n\n        if (['Wanted', 'Failed'].includes(status)) {\n            return 'Wanted';\n        }\n\n        if (['Snatched', 'Snatched (Proper)', 'Snatched (Best)'].includes(status)) {\n            return 'Snatched';\n        }\n\n        if (['Downloaded'].includes(status)) {\n            if (showQualities.preferred.includes(quality)) {\n                return 'Preferred';\n            }\n\n            if (showQualities.allowed.includes(quality)) {\n                return 'Allowed';\n            }\n        }\n\n        return status;\n    },\n    splitQuality: state => {\n        /**\n         * Split a combined quality to allowed and preferred qualities.\n         * Converted Python method from `medusa.common.Quality.split_quality`.\n         *\n         * @param {number} quality - The combined quality to split\n         * @returns {Quality} The split quality\n         */\n        const _splitQuality = quality => {\n            return state.qualities.values.reduce((result, { value }) => {\n                quality >>>= 0; // Unsigned int\n                if (value & quality) {\n                    result.allowed.push(value);\n                }\n                if ((value << 16) & quality) {\n                    result.preferred.push(value);\n                }\n                return result;\n            }, { allowed: [], preferred: [] });\n        };\n        return _splitQuality;\n    }\n};\n\nconst actions = {};\n\nexport default {\n    state,\n    mutations,\n    getters,\n    actions\n};\n","const state = {\n    show: {\n        airs: null,\n        airsFormatValid: null,\n        akas: null,\n        cache: null,\n        classification: null,\n        seasonCount: [],\n        config: {\n            airByDate: null,\n            aliases: [],\n            anime: null,\n            defaultEpisodeStatus: null,\n            dvdOrder: null,\n            location: null,\n            locationValid: null,\n            paused: null,\n            qualities: {\n                allowed: [],\n                preferred: []\n            },\n            release: {\n                requiredWords: [],\n                ignoredWords: [],\n                blacklist: [],\n                whitelist: [],\n                requiredWordsExclude: null,\n                ignoredWordsExclude: null\n            },\n            scene: null,\n            seasonFolders: null,\n            sports: null,\n            subtitlesEnabled: null,\n            airdateOffset: null\n        },\n        countries: null,\n        genres: [],\n        id: {\n            tvdb: null,\n            slug: null\n        },\n        indexer: null,\n        imdbInfo: {\n            akas: null,\n            certificates: null,\n            countries: null,\n            countryCodes: null,\n            genres: null,\n            imdbId: null,\n            imdbInfoId: null,\n            indexer: null,\n            indexerId: null,\n            lastUpdate: null,\n            plot: null,\n            rating: null,\n            runtimes: null,\n            title: null,\n            votes: null\n        },\n        language: null,\n        network: null,\n        nextAirDate: null,\n        plot: null,\n        rating: {\n            imdb: {\n                rating: null,\n                votes: null\n            }\n        },\n        runtime: null,\n        showType: null,\n        status: null,\n        title: null,\n        type: null,\n        year: {},\n        size: null,\n\n        // ===========================\n        // Detailed (`?detailed=true`)\n        // ===========================\n\n        showQueueStatus: [],\n        xemNumbering: [],\n        sceneAbsoluteNumbering: [],\n        allSceneExceptions: [],\n        xemAbsoluteNumbering: [],\n        sceneNumbering: [],\n\n        // ===========================\n        // Episodes (`?episodes=true`)\n        // ===========================\n\n        // Seasons array is added to the show object under this query,\n        // but we currently check to see if this property is defined before fetching the show with `?episodes=true`.\n        // seasons: [],\n        episodeCount: null\n    }\n};\n\nconst mutations = {};\n\nconst getters = {};\n\nconst actions = {};\n\nexport default {\n    state,\n    mutations,\n    getters,\n    actions\n};\n","import { ADD_CONFIG } from '../mutation-types';\n\nconst state = {\n    metadataProviders: {}\n};\n\nconst mutations = {\n    [ADD_CONFIG](state, { section, config }) {\n        if (section === 'metadata') {\n            state = Object.assign(state, config);\n        }\n    }\n};\n\nconst getters = {};\n\nconst actions = {};\n\nexport default {\n    state,\n    mutations,\n    getters,\n    actions\n};\n","import { NOTIFICATIONS_ENABLED, NOTIFICATIONS_DISABLED } from '../mutation-types';\n\nconst state = {\n    enabled: true\n};\n\nconst mutations = {\n    [NOTIFICATIONS_ENABLED](state) {\n        state.enabled = true;\n    },\n    [NOTIFICATIONS_DISABLED](state) {\n        state.enabled = false;\n    }\n};\n\nconst getters = {};\n\nconst actions = {\n    enable(context) {\n        const { commit } = context;\n        commit(NOTIFICATIONS_ENABLED);\n    },\n    disable(context) {\n        const { commit } = context;\n        commit(NOTIFICATIONS_DISABLED);\n    },\n    test() {\n        return window.displayNotification('error', 'test', 'test
hello world
  • item 1
  • item 2
', 'notification-test');\n }\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import { ADD_CONFIG } from '../../mutation-types';\nimport boxcar2 from './boxcar2';\nimport discord from './discord';\nimport email from './email';\nimport emby from './emby';\nimport freemobile from './freemobile';\nimport growl from './growl';\nimport kodi from './kodi';\nimport libnotify from './libnotify';\nimport nmj from './nmj';\nimport nmjv2 from './nmjv2';\nimport plex from './plex';\nimport prowl from './prowl';\nimport pushalot from './pushalot';\nimport pushbullet from './pushbullet';\nimport join from './join';\nimport pushover from './pushover';\nimport pyTivo from './py-tivo';\nimport slack from './slack';\nimport synology from './synology';\nimport synologyIndex from './synology-index';\nimport telegram from './telegram';\nimport trakt from './trakt';\nimport twitter from './twitter';\n\nconst state = {};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'notifiers') {\n state = Object.assign(state, config);\n }\n }\n};\n\nconst getters = {};\n\nconst actions = {};\n\nconst modules = {\n boxcar2,\n discord,\n email,\n emby,\n freemobile,\n growl,\n kodi,\n libnotify,\n nmj,\n nmjv2,\n plex,\n prowl,\n pushalot,\n pushbullet,\n join,\n pushover,\n pyTivo,\n slack,\n synology,\n synologyIndex,\n telegram,\n trakt,\n twitter\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions,\n modules\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n accessToken: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n webhook: null,\n tts: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n host: null,\n port: null,\n from: null,\n tls: null,\n username: null,\n password: null,\n addressList: [],\n subject: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n host: null,\n apiKey: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n api: null,\n id: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n host: null,\n password: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n alwaysOn: null,\n libraryCleanPending: null,\n cleanLibrary: null,\n host: [],\n username: null,\n password: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n update: {\n library: null,\n full: null,\n onlyFirst: null\n }\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n host: null,\n database: null,\n mount: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n host: null,\n dbloc: null,\n database: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n client: {\n host: [],\n username: null,\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null\n },\n server: {\n updateLibrary: null,\n host: [],\n enabled: null,\n https: null,\n username: null,\n password: null,\n token: null\n }\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n api: [],\n messageTitle: null,\n priority: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n authToken: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n authToken: null,\n device: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n api: null,\n device: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n apiKey: null,\n userKey: null,\n device: [],\n sound: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n host: null,\n name: null,\n shareName: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n webhook: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n api: null,\n id: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n pinUrl: null,\n username: null,\n accessToken: null,\n timeout: null,\n defaultIndexer: null,\n sync: null,\n syncRemove: null,\n syncWatchlist: null,\n methodAdd: null,\n removeWatchlist: null,\n removeSerieslist: null,\n removeShowFromApplication: null,\n startPaused: null,\n blacklistName: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","export const state = {\n enabled: null,\n notifyOnSnatch: null,\n notifyOnDownload: null,\n notifyOnSubtitleDownload: null,\n dmto: null,\n prefix: null,\n directMessage: null\n};\n\nexport const mutations = {};\n\nexport const getters = {};\n\nexport const actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import { ADD_CONFIG } from '../mutation-types';\n\nconst state = {\n filters: {\n ignoreUnknownSubs: false,\n ignored: [\n 'german',\n 'french',\n 'core2hd',\n 'dutch',\n 'swedish',\n 'reenc',\n 'MrLss',\n 'dubbed'\n ],\n undesired: [\n 'internal',\n 'xvid'\n ],\n ignoredSubsList: [\n 'dk',\n 'fin',\n 'heb',\n 'kor',\n 'nor',\n 'nordic',\n 'pl',\n 'swe'\n ],\n required: [],\n preferred: []\n },\n general: {\n minDailySearchFrequency: 10,\n minBacklogFrequency: 720,\n dailySearchFrequency: 40,\n checkPropersInterval: '4h',\n usenetRetention: 500,\n maxCacheAge: 30,\n backlogDays: 7,\n torrentCheckerFrequency: 60,\n backlogFrequency: 720,\n cacheTrimming: false,\n deleteFailed: false,\n downloadPropers: true,\n useFailedDownloads: false,\n minTorrentCheckerFrequency: 30,\n removeFromClient: false,\n randomizeProviders: false,\n propersSearchDays: 2,\n allowHighPriority: true,\n trackersList: [\n 'udp://tracker.coppersurfer.tk:6969/announce',\n 'udp://tracker.leechers-paradise.org:6969/announce',\n 'udp://tracker.zer0day.to:1337/announce',\n 'udp://tracker.opentrackr.org:1337/announce',\n 'http://tracker.opentrackr.org:1337/announce',\n 'udp://p4p.arenabg.com:1337/announce',\n 'http://p4p.arenabg.com:1337/announce',\n 'udp://explodie.org:6969/announce',\n 'udp://9.rarbg.com:2710/announce',\n 'http://explodie.org:6969/announce',\n 'http://tracker.dler.org:6969/announce',\n 'udp://public.popcorn-tracker.org:6969/announce',\n 'udp://tracker.internetwarriors.net:1337/announce',\n 'udp://ipv4.tracker.harry.lu:80/announce',\n 'http://ipv4.tracker.harry.lu:80/announce',\n 'udp://mgtracker.org:2710/announce',\n 'http://mgtracker.org:6969/announce',\n 'udp://tracker.mg64.net:6969/announce',\n 'http://tracker.mg64.net:6881/announce',\n 'http://torrentsmd.com:8080/announce'\n ]\n }\n};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'search') {\n state = Object.assign(state, config);\n }\n }\n};\n\nconst getters = {};\n\nconst actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import Vue from 'vue';\n\nimport { api } from '../../api';\nimport { ADD_SHOW, ADD_SHOW_EPISODE } from '../mutation-types';\n\n/**\n * @typedef {object} ShowIdentifier\n * @property {string} indexer The indexer name (e.g. `tvdb`)\n * @property {number} id The show ID on the indexer (e.g. `12345`)\n */\n\nconst state = {\n shows: [],\n currentShow: {\n indexer: null,\n id: null\n }\n};\n\nconst mutations = {\n [ADD_SHOW](state, show) {\n const existingShow = state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer]));\n\n if (!existingShow) {\n console.debug(`Adding ${show.title || show.indexer + String(show.id)} as it wasn't found in the shows array`, show);\n state.shows.push(show);\n return;\n }\n\n // Merge new show object over old one\n // this allows detailed queries to update the record\n // without the non-detailed removing the extra data\n console.debug(`Found ${show.title || show.indexer + String(show.id)} in shows array attempting merge`);\n const newShow = {\n ...existingShow,\n ...show\n };\n\n // Update state\n Vue.set(state.shows, state.shows.indexOf(existingShow), newShow);\n console.debug(`Merged ${newShow.title || newShow.indexer + String(newShow.id)}`, newShow);\n },\n currentShow(state, { indexer, id }) {\n state.currentShow.indexer = indexer;\n state.currentShow.id = id;\n },\n [ADD_SHOW_EPISODE](state, { show, episodes }) {\n // Creating a new show object (from the state one) as we want to trigger a store update\n const newShow = Object.assign({}, state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer])));\n\n if (!newShow.seasons) {\n newShow.seasons = [];\n }\n\n // Recreate an Array with season objects, with each season having an episodes array.\n // This format is used by vue-good-table (displayShow).\n episodes.forEach(episode => {\n const existingSeason = newShow.seasons.find(season => season.season === episode.season);\n\n if (existingSeason) {\n const foundIndex = existingSeason.episodes.findIndex(element => element.slug === episode.slug);\n if (foundIndex === -1) {\n existingSeason.episodes.push(episode);\n } else {\n existingSeason.episodes.splice(foundIndex, 1, episode);\n }\n } else {\n const newSeason = {\n season: episode.season,\n episodes: [],\n html: false,\n mode: 'span',\n label: 1\n };\n newShow.seasons.push(newSeason);\n newSeason.episodes.push(episode);\n }\n });\n\n // Update state\n const existingShow = state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer]));\n Vue.set(state.shows, state.shows.indexOf(existingShow), newShow);\n console.log(`Storing episodes for show ${newShow.title} seasons: ${[...new Set(episodes.map(episode => episode.season))].join(', ')}`);\n }\n\n};\n\nconst getters = {\n getShowById: state => {\n /**\n * Get a show from the loaded shows state, identified by show ID and indexer name.\n *\n * @param {ShowIdentifier} show Show identifiers.\n * @returns {object|undefined} Show object or undefined if not found.\n */\n const getShowById = ({ id, indexer }) => state.shows.find(show => Number(show.id[indexer]) === Number(id));\n return getShowById;\n },\n getShowByTitle: state => title => state.shows.find(show => show.title === title),\n getSeason: state => ({ id, indexer, season }) => {\n const show = state.shows.find(show => Number(show.id[indexer]) === Number(id));\n return show && show.seasons ? show.seasons[season] : undefined;\n },\n getEpisode: state => ({ id, indexer, season, episode }) => {\n const show = state.shows.find(show => Number(show.id[indexer]) === Number(id));\n return show && show.seasons && show.seasons[season] ? show.seasons[season][episode] : undefined;\n },\n getCurrentShow: (state, getters, rootState) => {\n return state.shows.find(show => Number(show.id[state.currentShow.indexer]) === Number(state.currentShow.id)) || rootState.defaults.show;\n }\n};\n\n/**\n * An object representing request parameters for getting a show from the API.\n *\n * @typedef {object} ShowGetParameters\n * @property {boolean} detailed Fetch detailed information? (e.g. scene/xem numbering)\n * @property {boolean} episodes Fetch seasons & episodes?\n */\n\nconst actions = {\n /**\n * Get show from API and commit it to the store.\n *\n * @param {*} context The store context.\n * @param {ShowIdentifier&ShowGetParameters} parameters Request parameters.\n * @returns {Promise} The API response.\n */\n getShow(context, { indexer, id, detailed, episodes }) {\n return new Promise((resolve, reject) => {\n const { commit } = context;\n const params = {};\n let timeout = 30000;\n\n if (detailed !== undefined) {\n params.detailed = detailed;\n timeout = 60000;\n timeout = 60000;\n }\n\n if (episodes !== undefined) {\n params.episodes = episodes;\n timeout = 60000;\n }\n\n api.get(`/series/${indexer}${id}`, { params, timeout })\n .then(res => {\n commit(ADD_SHOW, res.data);\n resolve(res.data);\n })\n .catch(error => {\n reject(error);\n });\n });\n },\n /**\n * Get episdoes for a specified show from API and commit it to the store.\n *\n * @param {*} context - The store context.\n * @param {ShowParameteres} parameters - Request parameters.\n * @returns {Promise} The API response.\n */\n getEpisodes({ commit, getters }, { indexer, id, season }) {\n return new Promise((resolve, reject) => {\n const { getShowById } = getters;\n const show = getShowById({ id, indexer });\n\n const limit = 1000;\n const params = {\n limit\n };\n\n if (season) {\n params.season = season;\n }\n\n // Get episodes\n api.get(`/series/${indexer}${id}/episodes`, { params })\n .then(response => {\n commit(ADD_SHOW_EPISODE, { show, episodes: response.data });\n resolve();\n })\n .catch(error => {\n console.log(`Could not retrieve a episodes for show ${indexer}${id}, error: ${error}`);\n reject(error);\n });\n });\n },\n /**\n * Get shows from API and commit them to the store.\n *\n * @param {*} context - The store context.\n * @param {(ShowIdentifier&ShowGetParameters)[]} shows Shows to get. If not provided, gets the first 1k shows.\n * @returns {undefined|Promise} undefined if `shows` was provided or the API response if not.\n */\n getShows(context, shows) {\n const { commit, dispatch } = context;\n\n // If no shows are provided get the first 1000\n if (!shows) {\n return (() => {\n const limit = 1000;\n const page = 1;\n const params = {\n limit,\n page\n };\n\n // Get first page\n api.get('/series', { params })\n .then(response => {\n const totalPages = Number(response.headers['x-pagination-total']);\n response.data.forEach(show => {\n commit(ADD_SHOW, show);\n });\n\n // Optionally get additional pages\n const pageRequests = [];\n for (let page = 2; page <= totalPages; page++) {\n const newPage = { page };\n newPage.limit = params.limit;\n pageRequests.push(api.get('/series', { params: newPage }).then(response => {\n response.data.forEach(show => {\n commit(ADD_SHOW, show);\n });\n }));\n }\n\n return Promise.all(pageRequests);\n })\n .catch(() => {\n console.log('Could not retrieve a list of shows');\n });\n })();\n }\n\n return shows.forEach(show => dispatch('getShow', show));\n },\n setShow(context, { indexer, id, data }) {\n // Update show, updated show will arrive over a WebSocket message\n return api.patch(`series/${indexer}${id}`, data);\n },\n updateShow(context, show) {\n // Update local store\n const { commit } = context;\n return commit(ADD_SHOW, show);\n }\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import {\n SOCKET_ONOPEN,\n SOCKET_ONCLOSE,\n SOCKET_ONERROR,\n SOCKET_ONMESSAGE,\n SOCKET_RECONNECT,\n SOCKET_RECONNECT_ERROR\n} from '../mutation-types';\n\nconst state = {\n isConnected: false,\n // Current message\n message: {},\n // Delivered messages for this session\n messages: [],\n reconnectError: false\n};\n\nconst mutations = {\n [SOCKET_ONOPEN](state) {\n state.isConnected = true;\n },\n [SOCKET_ONCLOSE](state) {\n state.isConnected = false;\n },\n [SOCKET_ONERROR](state, event) {\n console.error(state, event);\n },\n // Default handler called for all websocket methods\n [SOCKET_ONMESSAGE](state, message) {\n const { data, event } = message;\n\n // Set the current message\n state.message = message;\n\n if (event === 'notification') {\n // Save it so we can look it up later\n const existingMessage = state.messages.filter(message => message.hash === data.hash);\n if (existingMessage.length === 1) {\n state.messages[state.messages.indexOf(existingMessage)] = message;\n } else {\n state.messages.push(message);\n }\n }\n },\n // Mutations for websocket reconnect methods\n [SOCKET_RECONNECT](state, count) {\n console.info(state, count);\n },\n [SOCKET_RECONNECT_ERROR](state) {\n state.reconnectError = true;\n\n const title = 'Error connecting to websocket';\n let error = '';\n error += 'Please check your network connection. ';\n error += 'If you are using a reverse proxy, please take a look at our wiki for config examples.';\n\n window.displayNotification('notice', title, error);\n }\n};\n\nconst getters = {};\n\nconst actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import { api } from '../../api';\nimport { ADD_STATS } from '../mutation-types';\n\nconst state = {\n overall: {\n episodes: {\n downloaded: null,\n snatched: null,\n total: null\n },\n shows: {\n active: null,\n total: null\n }\n }\n};\n\nconst mutations = {\n [ADD_STATS](state, payload) {\n const { type, stats } = payload;\n state[type] = Object.assign(state[type], stats);\n }\n};\n\nconst getters = {};\n\nconst actions = {\n getStats(context, type) {\n const { commit } = context;\n return api.get('/stats/' + (type || '')).then(res => {\n commit(ADD_STATS, {\n type: (type || 'overall'),\n stats: res.data\n });\n });\n }\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import { ADD_CONFIG } from '../mutation-types';\n\n/**\n * An object representing a scheduler.\n *\n * If a scheduler isn't initialized on the backend,\n * this object will only have the `key` and `name` properties.\n * @typedef {object} Scheduler\n * @property {string} key\n * A camelCase key representing this scheduler.\n * @property {string} name\n * The scheduler's name.\n * @property {boolean} [isAlive]\n * Is the scheduler alive?\n * @property {boolean|string} [isEnabled]\n * Is the scheduler enabled? For the `backlog` scheduler, the value might be `Paused`.\n * @property {boolean} [isActive]\n * Is the scheduler's action currently running?\n * @property {string|null} [startTime]\n * The time of day in which this scheduler runs (format: ISO-8601 time), or `null` if not applicable.\n * @property {number} [cycleTime]\n * The duration in milliseconds between each run, or `null` if not applicable.\n * @property {number} [nextRun]\n * The duration in milliseconds until the next run.\n * @property {string} [lastRun]\n * The date and time of the previous run (format: ISO-8601 date-time).\n * @property {boolean} [isSilent]\n * Is the scheduler silent?\n */\n\nconst state = {\n memoryUsage: null,\n schedulers: [],\n showQueue: []\n};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'system') {\n state = Object.assign(state, config);\n }\n }\n};\n\nconst getters = {\n getScheduler: state => {\n /**\n * Get a scheduler object using a key.\n *\n * @param {string} key The combined quality to split.\n * @returns {Scheduler|object} The scheduler object or an empty object if not found.\n */\n const _getScheduler = key => state.schedulers.find(scheduler => key === scheduler.key) || {};\n return _getScheduler;\n }\n};\n\nconst actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import Vue from 'vue';\nimport Vuex, { Store } from 'vuex';\nimport VueNativeSock from 'vue-native-websocket';\nimport {\n auth,\n clients,\n config,\n consts,\n defaults,\n metadata,\n notifications,\n notifiers,\n search,\n shows,\n socket,\n stats,\n system\n} from './modules';\nimport {\n SOCKET_ONOPEN,\n SOCKET_ONCLOSE,\n SOCKET_ONERROR,\n SOCKET_ONMESSAGE,\n SOCKET_RECONNECT,\n SOCKET_RECONNECT_ERROR\n} from './mutation-types';\n\nVue.use(Vuex);\n\nconst store = new Store({\n modules: {\n auth,\n clients,\n config,\n consts,\n defaults,\n metadata,\n notifications,\n notifiers,\n search,\n shows,\n socket,\n stats,\n system\n },\n state: {},\n mutations: {},\n getters: {},\n actions: {}\n});\n\n// Keep as a non-arrow function for `this` context.\nconst passToStoreHandler = function(eventName, event, next) {\n const target = eventName.toUpperCase();\n const eventData = event.data;\n\n if (target === 'SOCKET_ONMESSAGE') {\n const message = JSON.parse(eventData);\n const { data, event } = message;\n\n // Show the notification to the user\n if (event === 'notification') {\n const { body, hash, type, title } = data;\n window.displayNotification(type, title, body, hash);\n } else if (event === 'configUpdated') {\n const { section, config } = data;\n this.store.dispatch('updateConfig', { section, config });\n } else if (event === 'showUpdated') {\n this.store.dispatch('updateShow', data);\n } else {\n window.displayNotification('info', event, data);\n }\n }\n\n // Resume normal 'passToStore' handling\n next(eventName, event);\n};\n\nconst websocketUrl = (() => {\n const { protocol, host } = window.location;\n const proto = protocol === 'https:' ? 'wss:' : 'ws:';\n const WSMessageUrl = '/ui';\n const webRoot = document.body.getAttribute('web-root');\n return `${proto}//${host}${webRoot}/ws${WSMessageUrl}`;\n})();\n\nVue.use(VueNativeSock, websocketUrl, {\n store,\n format: 'json',\n reconnection: true, // (Boolean) whether to reconnect automatically (false)\n reconnectionAttempts: 2, // (Number) number of reconnection attempts before giving up (Infinity),\n reconnectionDelay: 1000, // (Number) how long to initially wait before attempting a new (1000)\n passToStoreHandler, // (Function|) Handler for events triggered by the WebSocket (false)\n mutations: {\n SOCKET_ONOPEN,\n SOCKET_ONCLOSE,\n SOCKET_ONERROR,\n SOCKET_ONMESSAGE,\n SOCKET_RECONNECT,\n SOCKET_RECONNECT_ERROR\n }\n});\n\nexport default store;\n","/** @type {import('.').SubMenu} */\nexport const configSubMenu = [\n { title: 'General', path: 'config/general/', icon: 'menu-icon-config' },\n { title: 'Backup/Restore', path: 'config/backuprestore/', icon: 'menu-icon-backup' },\n { title: 'Search Settings', path: 'config/search/', icon: 'menu-icon-manage-searches' },\n { title: 'Search Providers', path: 'config/providers/', icon: 'menu-icon-provider' },\n { title: 'Subtitles Settings', path: 'config/subtitles/', icon: 'menu-icon-backlog' },\n { title: 'Post Processing', path: 'config/postProcessing/', icon: 'menu-icon-postprocess' },\n { title: 'Notifications', path: 'config/notifications/', icon: 'menu-icon-notification' },\n { title: 'Anime', path: 'config/anime/', icon: 'menu-icon-anime' }\n];\n\n// eslint-disable-next-line valid-jsdoc\n/** @type {import('.').SubMenuFunction} */\nexport const errorlogsSubMenu = vm => {\n const { $route, $store } = vm;\n const level = $route.params.level || $route.query.level;\n const { config } = $store.state;\n const { loggingLevels, numErrors, numWarnings } = config.logs;\n if (Object.keys(loggingLevels).length === 0) {\n return [];\n }\n\n const isLevelError = (level === undefined || Number(level) === loggingLevels.error);\n\n return [\n {\n title: 'Clear Errors',\n path: 'errorlogs/clearerrors/',\n requires: numErrors >= 1 && isLevelError,\n icon: 'ui-icon ui-icon-trash'\n },\n {\n title: 'Clear Warnings',\n path: `errorlogs/clearerrors/?level=${loggingLevels.warning}`,\n requires: numWarnings >= 1 && Number(level) === loggingLevels.warning,\n icon: 'ui-icon ui-icon-trash'\n },\n {\n title: 'Submit Errors',\n path: 'errorlogs/submit_errors/',\n requires: numErrors >= 1 && isLevelError,\n confirm: 'submiterrors',\n icon: 'ui-icon ui-icon-arrowreturnthick-1-n'\n }\n ];\n};\n\n/** @type {import('.').SubMenu} */\nexport const historySubMenu = [\n { title: 'Clear History', path: 'history/clearHistory', icon: 'ui-icon ui-icon-trash', confirm: 'clearhistory' },\n { title: 'Trim History', path: 'history/trimHistory', icon: 'menu-icon-cut', confirm: 'trimhistory' }\n];\n\n// eslint-disable-next-line valid-jsdoc\n/** @type {import('.').SubMenuFunction} */\nexport const showSubMenu = vm => {\n const { $route, $store } = vm;\n const { config, notifiers } = $store.state;\n\n const indexerName = $route.params.indexer || $route.query.indexername;\n const showId = $route.params.id || $route.query.seriesid;\n\n const show = $store.getters.getCurrentShow;\n const { showQueueStatus } = show;\n\n const queuedActionStatus = action => {\n if (!showQueueStatus) {\n return false;\n }\n return Boolean(showQueueStatus.find(status => status.action === action && status.active === true));\n };\n\n const isBeingAdded = queuedActionStatus('isBeingAdded');\n const isBeingUpdated = queuedActionStatus('isBeingUpdated');\n const isBeingSubtitled = queuedActionStatus('isBeingSubtitled');\n\n /** @type {import('.').SubMenu} */\n let menu = [{\n title: 'Edit',\n path: `home/editShow?indexername=${indexerName}&seriesid=${showId}`,\n icon: 'ui-icon ui-icon-pencil'\n }];\n if (!isBeingAdded && !isBeingUpdated) {\n menu = menu.concat([\n {\n title: show.config.paused ? 'Resume' : 'Pause',\n path: `home/togglePause?indexername=${indexerName}&seriesid=${showId}`,\n icon: `ui-icon ui-icon-${show.config.paused ? 'play' : 'pause'}`\n },\n {\n title: 'Remove',\n path: `home/deleteShow?indexername=${indexerName}&seriesid=${showId}`,\n confirm: 'removeshow',\n icon: 'ui-icon ui-icon-trash'\n },\n {\n title: 'Re-scan files',\n path: `home/refreshShow?indexername=${indexerName}&seriesid=${showId}`,\n icon: 'ui-icon ui-icon-refresh'\n },\n {\n title: 'Force Full Update',\n path: `home/updateShow?indexername=${indexerName}&seriesid=${showId}`,\n icon: 'ui-icon ui-icon-transfer-e-w'\n },\n {\n title: 'Update show in KODI',\n path: `home/updateKODI?indexername=${indexerName}&seriesid=${showId}`,\n requires: notifiers.kodi.enabled && notifiers.kodi.update.library,\n icon: 'menu-icon-kodi'\n },\n {\n title: 'Update show in Emby',\n path: `home/updateEMBY?indexername=${indexerName}&seriesid=${showId}`,\n requires: notifiers.emby.enabled,\n icon: 'menu-icon-emby'\n },\n {\n title: 'Preview Rename',\n path: `home/testRename?indexername=${indexerName}&seriesid=${showId}`,\n icon: 'ui-icon ui-icon-tag'\n },\n {\n title: 'Download Subtitles',\n path: `home/subtitleShow?indexername=${indexerName}&seriesid=${showId}`,\n requires: config.subtitles.enabled && !isBeingSubtitled && show.config.subtitlesEnabled,\n icon: 'menu-icon-backlog'\n }\n ]);\n }\n return menu;\n};\n","import {\n configSubMenu,\n errorlogsSubMenu,\n historySubMenu,\n showSubMenu\n} from './sub-menus';\n\n/** @type {import('.').Route[]} */\nconst homeRoutes = [\n {\n path: '/home',\n name: 'home',\n meta: {\n title: 'Home',\n header: 'Show List',\n topMenu: 'home'\n }\n },\n {\n path: '/home/editShow',\n name: 'editShow',\n meta: {\n topMenu: 'home',\n subMenu: showSubMenu\n },\n component: () => import('../components/edit-show.vue')\n },\n {\n path: '/home/displayShow',\n name: 'show',\n meta: {\n topMenu: 'home',\n subMenu: showSubMenu\n },\n component: () => import('../components/display-show.vue')\n },\n {\n path: '/home/snatchSelection',\n name: 'snatchSelection',\n meta: {\n topMenu: 'home',\n subMenu: showSubMenu\n }\n },\n {\n path: '/home/testRename',\n name: 'testRename',\n meta: {\n title: 'Preview Rename',\n header: 'Preview Rename',\n topMenu: 'home'\n }\n },\n {\n path: '/home/postprocess',\n name: 'postprocess',\n meta: {\n title: 'Manual Post-Processing',\n header: 'Manual Post-Processing',\n topMenu: 'home'\n }\n },\n {\n path: '/home/status',\n name: 'status',\n meta: {\n title: 'Status',\n topMenu: 'system'\n }\n },\n {\n path: '/home/restart',\n name: 'restart',\n meta: {\n title: 'Restarting...',\n header: 'Performing Restart',\n topMenu: 'system'\n }\n },\n {\n path: '/home/shutdown',\n name: 'shutdown',\n meta: {\n header: 'Shutting down',\n topMenu: 'system'\n }\n },\n {\n path: '/home/update',\n name: 'update',\n meta: {\n topMenu: 'system'\n }\n }\n];\n\n/** @type {import('.').Route[]} */\nconst configRoutes = [\n {\n path: '/config',\n name: 'config',\n meta: {\n title: 'Help & Info',\n header: 'Medusa Configuration',\n topMenu: 'config',\n subMenu: configSubMenu,\n converted: true\n },\n component: () => import('../components/config.vue')\n },\n {\n path: '/config/anime',\n name: 'configAnime',\n meta: {\n title: 'Config - Anime',\n header: 'Anime',\n topMenu: 'config',\n subMenu: configSubMenu\n }\n },\n {\n path: '/config/backuprestore',\n name: 'configBackupRestore',\n meta: {\n title: 'Config - Backup/Restore',\n header: 'Backup/Restore',\n topMenu: 'config',\n subMenu: configSubMenu\n }\n },\n {\n path: '/config/general',\n name: 'configGeneral',\n meta: {\n title: 'Config - General',\n header: 'General Configuration',\n topMenu: 'config',\n subMenu: configSubMenu\n }\n },\n {\n path: '/config/notifications',\n name: 'configNotifications',\n meta: {\n title: 'Config - Notifications',\n header: 'Notifications',\n topMenu: 'config',\n subMenu: configSubMenu,\n converted: true\n },\n component: () => import('../components/config-notifications.vue')\n },\n {\n path: '/config/postProcessing',\n name: 'configPostProcessing',\n meta: {\n title: 'Config - Post Processing',\n header: 'Post Processing',\n topMenu: 'config',\n subMenu: configSubMenu,\n converted: true\n },\n component: () => import('../components/config-post-processing.vue')\n },\n {\n path: '/config/providers',\n name: 'configSearchProviders',\n meta: {\n title: 'Config - Providers',\n header: 'Search Providers',\n topMenu: 'config',\n subMenu: configSubMenu\n }\n },\n {\n path: '/config/search',\n name: 'configSearchSettings',\n meta: {\n title: 'Config - Episode Search',\n header: 'Search Settings',\n topMenu: 'config',\n subMenu: configSubMenu,\n converted: true\n },\n component: () => import('../components/config-search.vue')\n },\n {\n path: '/config/subtitles',\n name: 'configSubtitles',\n meta: {\n title: 'Config - Subtitles',\n header: 'Subtitles',\n topMenu: 'config',\n subMenu: configSubMenu\n }\n }\n];\n\n/** @type {import('.').Route[]} */\nconst addShowRoutes = [\n {\n path: '/addShows',\n name: 'addShows',\n meta: {\n title: 'Add Shows',\n header: 'Add Shows',\n topMenu: 'home',\n converted: true\n },\n component: () => import('../components/add-shows.vue')\n },\n {\n path: '/addShows/addExistingShows',\n name: 'addExistingShows',\n meta: {\n title: 'Add Existing Shows',\n header: 'Add Existing Shows',\n topMenu: 'home'\n }\n },\n {\n path: '/addShows/newShow',\n name: 'addNewShow',\n meta: {\n title: 'Add New Show',\n header: 'Add New Show',\n topMenu: 'home'\n }\n },\n {\n path: '/addShows/trendingShows',\n name: 'addTrendingShows',\n meta: {\n topMenu: 'home'\n }\n },\n {\n path: '/addShows/popularShows',\n name: 'addPopularShows',\n meta: {\n title: 'Popular Shows',\n header: 'Popular Shows',\n topMenu: 'home'\n }\n },\n {\n path: '/addShows/popularAnime',\n name: 'addPopularAnime',\n meta: {\n title: 'Popular Anime Shows',\n header: 'Popular Anime Shows',\n topMenu: 'home'\n }\n }\n];\n\n/** @type {import('.').Route} */\nconst loginRoute = {\n path: '/login',\n name: 'login',\n meta: {\n title: 'Login'\n },\n component: () => import('../components/login.vue')\n};\n\n/** @type {import('.').Route} */\nconst addRecommendedRoute = {\n path: '/addRecommended',\n name: 'addRecommended',\n meta: {\n title: 'Add Recommended Shows',\n header: 'Add Recommended Shows',\n topMenu: 'home',\n converted: true\n },\n component: () => import('../components/add-recommended.vue')\n};\n\n/** @type {import('.').Route} */\nconst scheduleRoute = {\n path: '/schedule',\n name: 'schedule',\n meta: {\n title: 'Schedule',\n header: 'Schedule',\n topMenu: 'schedule'\n }\n};\n\n/** @type {import('.').Route} */\nconst historyRoute = {\n path: '/history',\n name: 'history',\n meta: {\n title: 'History',\n header: 'History',\n topMenu: 'history',\n subMenu: historySubMenu\n }\n};\n\n/** @type {import('.').Route[]} */\nconst manageRoutes = [\n {\n path: '/manage',\n name: 'manage',\n meta: {\n title: 'Mass Update',\n header: 'Mass Update',\n topMenu: 'manage'\n }\n },\n {\n path: '/manage/backlogOverview',\n name: 'manageBacklogOverview',\n meta: {\n title: 'Backlog Overview',\n header: 'Backlog Overview',\n topMenu: 'manage'\n }\n },\n {\n path: '/manage/episodeStatuses',\n name: 'manageEpisodeOverview',\n meta: {\n title: 'Episode Overview',\n header: 'Episode Overview',\n topMenu: 'manage'\n }\n },\n {\n path: '/manage/failedDownloads',\n name: 'manageFailedDownloads',\n meta: {\n title: 'Failed Downloads',\n header: 'Failed Downloads',\n topMenu: 'manage'\n }\n },\n {\n path: '/manage/manageSearches',\n name: 'manageManageSearches',\n meta: {\n title: 'Manage Searches',\n header: 'Manage Searches',\n topMenu: 'manage'\n }\n },\n {\n path: '/manage/massEdit',\n name: 'manageMassEdit',\n meta: {\n title: 'Mass Edit',\n topMenu: 'manage'\n }\n },\n {\n path: '/manage/subtitleMissed',\n name: 'manageSubtitleMissed',\n meta: {\n title: 'Missing Subtitles',\n header: 'Missing Subtitles',\n topMenu: 'manage'\n }\n },\n {\n path: '/manage/subtitleMissedPP',\n name: 'manageSubtitleMissedPP',\n meta: {\n title: 'Missing Subtitles in Post-Process folder',\n header: 'Missing Subtitles in Post-Process folder',\n topMenu: 'manage'\n }\n }\n];\n\n/** @type {import('.').Route[]} */\nconst errorLogsRoutes = [\n {\n path: '/errorlogs',\n name: 'errorlogs',\n meta: {\n title: 'Logs & Errors',\n topMenu: 'system',\n subMenu: errorlogsSubMenu\n }\n },\n {\n path: '/errorlogs/viewlog',\n name: 'viewlog',\n meta: {\n title: 'Logs',\n header: 'Log File',\n topMenu: 'system',\n converted: true\n },\n component: () => import('../components/logs.vue')\n }\n];\n\n/** @type {import('.').Route} */\nconst newsRoute = {\n path: '/news',\n name: 'news',\n meta: {\n title: 'News',\n header: 'News',\n topMenu: 'system'\n }\n};\n\n/** @type {import('.').Route} */\nconst changesRoute = {\n path: '/changes',\n name: 'changes',\n meta: {\n title: 'Changelog',\n header: 'Changelog',\n topMenu: 'system'\n }\n};\n\n/** @type {import('.').Route} */\nconst ircRoute = {\n path: '/IRC',\n name: 'IRC',\n meta: {\n title: 'IRC',\n topMenu: 'system',\n converted: true\n },\n component: () => import('../components/irc.vue')\n};\n\n/** @type {import('.').Route} */\nconst notFoundRoute = {\n path: '/not-found',\n name: 'not-found',\n meta: {\n title: '404',\n header: '404 - page not found'\n },\n component: () => import('../components/http/404.vue')\n};\n\n// @NOTE: Redirect can only be added once all routes are vue\n/*\n/** @type {import('.').Route} *-/\nconst notFoundRedirect = {\n path: '*',\n redirect: '/not-found'\n};\n*/\n\n/** @type {import('.').Route[]} */\nexport default [\n ...homeRoutes,\n ...configRoutes,\n ...addShowRoutes,\n loginRoute,\n addRecommendedRoute,\n scheduleRoute,\n historyRoute,\n ...manageRoutes,\n ...errorLogsRoutes,\n newsRoute,\n changesRoute,\n ircRoute,\n notFoundRoute\n];\n","import Vue from 'vue';\nimport VueRouter from 'vue-router';\n\nimport routes from './routes';\n\nVue.use(VueRouter);\n\nexport const base = document.body.getAttribute('web-root') + '/';\n\nconst router = new VueRouter({\n base,\n mode: 'history',\n routes\n});\n\nrouter.beforeEach((to, from, next) => {\n const { meta } = to;\n const { title } = meta;\n\n // If there's no title then it's not a .vue route\n // or it's handling its own title\n if (title) {\n document.title = `${title} | Medusa`;\n }\n\n // Always call next otherwise the will be empty\n next();\n});\n\nexport default router;\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"anidb-release-group-ui-wrapper top-10 max-width\"},[(_vm.fetchingGroups)?[_c('state-switch',{attrs:{\"state\":\"loading\",\"theme\":_vm.config.themeName}}),_vm._v(\" \"),_c('span',[_vm._v(\"Fetching release groups...\")])]:_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-sm-4 left-whitelist\"},[_c('span',[_vm._v(\"Whitelist\")]),(_vm.showDeleteFromWhitelist)?_c('img',{staticClass:\"deleteFromWhitelist\",attrs:{\"src\":\"images/no16.png\"},on:{\"click\":function($event){return _vm.deleteFromList('whitelist')}}}):_vm._e(),_vm._v(\" \"),_c('ul',[_vm._l((_vm.itemsWhitelist),function(release){return _c('li',{key:release.id,class:{active: release.toggled},on:{\"click\":function($event){release.toggled = !release.toggled}}},[_vm._v(_vm._s(release.name))])}),_vm._v(\" \"),_c('div',{staticClass:\"arrow\",on:{\"click\":function($event){return _vm.moveToList('whitelist')}}},[_c('img',{attrs:{\"src\":\"images/curved-arrow-left.png\"}})])],2)]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-4 center-available\"},[_c('span',[_vm._v(\"Release groups\")]),_vm._v(\" \"),_c('ul',[_vm._l((_vm.itemsReleaseGroups),function(release){return _c('li',{key:release.id,staticClass:\"initial\",class:{active: release.toggled},on:{\"click\":function($event){release.toggled = !release.toggled}}},[_vm._v(_vm._s(release.name))])}),_vm._v(\" \"),_c('div',{staticClass:\"arrow\",on:{\"click\":function($event){return _vm.moveToList('releasegroups')}}},[_c('img',{attrs:{\"src\":\"images/curved-arrow-left.png\"}})])],2)]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-4 right-blacklist\"},[_c('span',[_vm._v(\"Blacklist\")]),(_vm.showDeleteFromBlacklist)?_c('img',{staticClass:\"deleteFromBlacklist\",attrs:{\"src\":\"images/no16.png\"},on:{\"click\":function($event){return _vm.deleteFromList('blacklist')}}}):_vm._e(),_vm._v(\" \"),_c('ul',[_vm._l((_vm.itemsBlacklist),function(release){return _c('li',{key:release.id,class:{active: release.toggled},on:{\"click\":function($event){release.toggled = !release.toggled}}},[_vm._v(_vm._s(release.name))])}),_vm._v(\" \"),_c('div',{staticClass:\"arrow\",on:{\"click\":function($event){return _vm.moveToList('blacklist')}}},[_c('img',{attrs:{\"src\":\"images/curved-arrow-left.png\"}})])],2)])]),_vm._v(\" \"),_c('div',{staticClass:\"row\",attrs:{\"id\":\"add-new-release-group\"}},[_c('div',{staticClass:\"col-md-4\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.newGroup),expression:\"newGroup\"}],staticClass:\"form-control input-sm\",attrs:{\"type\":\"text\",\"placeholder\":\"add custom group\"},domProps:{\"value\":(_vm.newGroup)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.newGroup=$event.target.value}}})]),_vm._v(\" \"),_vm._m(0)])],2)}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-md-8\"},[_c('p',[_vm._v(\"Use the input to add custom whitelist / blacklist release groups. Click on the \"),_c('img',{attrs:{\"src\":\"images/curved-arrow-left.png\"}}),_vm._v(\" to add it to the correct list.\")])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./anidb-release-group-ui.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./anidb-release-group-ui.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./anidb-release-group-ui.vue?vue&type=template&id=290c5884&scoped=true&\"\nimport script from \"./anidb-release-group-ui.vue?vue&type=script&lang=js&\"\nexport * from \"./anidb-release-group-ui.vue?vue&type=script&lang=js&\"\nimport style0 from \"./anidb-release-group-ui.vue?vue&type=style&index=0&id=290c5884&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"290c5884\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"show-header-container\"},[_c('div',{staticClass:\"row\"},[(_vm.show)?_c('div',{staticClass:\"col-lg-12\",attrs:{\"id\":\"showtitle\",\"data-showname\":_vm.show.title}},[_c('div',[_c('h1',{staticClass:\"title\",attrs:{\"data-indexer-name\":_vm.show.indexer,\"data-series-id\":_vm.show.id[_vm.show.indexer],\"id\":'scene_exception_' + _vm.show.id[_vm.show.indexer]}},[_c('app-link',{staticClass:\"snatchTitle\",attrs:{\"href\":'home/displayShow?indexername=' + _vm.show.indexer + '&seriesid=' + _vm.show.id[_vm.show.indexer]}},[_vm._v(_vm._s(_vm.show.title))])],1)]),_vm._v(\" \"),(_vm.type === 'snatch-selection')?_c('div',{staticClass:\"pull-right\",attrs:{\"id\":\"show-specials-and-seasons\"}},[_c('span',{staticClass:\"h2footer display-specials\"},[_vm._v(\"\\n Manual search for:\"),_c('br'),_vm._v(\" \"),_c('app-link',{staticClass:\"snatchTitle\",attrs:{\"href\":'home/displayShow?indexername=' + _vm.show.indexer + '&seriesid=' + _vm.show.id[_vm.show.indexer]}},[_vm._v(_vm._s(_vm.show.title))]),_vm._v(\" / Season \"+_vm._s(_vm.season)),(_vm.episode !== undefined && _vm.manualSearchType !== 'season')?[_vm._v(\" Episode \"+_vm._s(_vm.episode))]:_vm._e()],2)]):_vm._e(),_vm._v(\" \"),(_vm.type !== 'snatch-selection' && _vm.seasons.length >= 1)?_c('div',{staticClass:\"pull-right\",attrs:{\"id\":\"show-specials-and-seasons\"}},[(_vm.seasons.includes(0))?_c('span',{staticClass:\"h2footer display-specials\"},[_vm._v(\"\\n Display Specials: \"),_c('a',{staticClass:\"inner\",staticStyle:{\"cursor\":\"pointer\"},on:{\"click\":function($event){return _vm.toggleSpecials()}}},[_vm._v(_vm._s(_vm.displaySpecials ? 'Show' : 'Hide'))])]):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"h2footer display-seasons clear\"},[_c('span',[(_vm.seasons.length >= 15)?_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.jumpToSeason),expression:\"jumpToSeason\"}],staticClass:\"form-control input-sm\",staticStyle:{\"position\":\"relative\"},attrs:{\"id\":\"seasonJump\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.jumpToSeason=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[_c('option',{attrs:{\"value\":\"jump\"}},[_vm._v(\"Jump to Season\")]),_vm._v(\" \"),_vm._l((_vm.seasons),function(seasonNumber){return _c('option',{key:(\"jumpToSeason-\" + seasonNumber),domProps:{\"value\":seasonNumber}},[_vm._v(\"\\n \"+_vm._s(seasonNumber === 0 ? 'Specials' : (\"Season \" + seasonNumber))+\"\\n \")])})],2):(_vm.seasons.length >= 1)?[_vm._v(\"\\n Season:\\n \"),_vm._l((_vm.reverse(_vm.seasons)),function(seasonNumber,index){return [_c('app-link',{key:(\"jumpToSeason-\" + seasonNumber),attrs:{\"href\":(\"#season-\" + seasonNumber)},nativeOn:{\"click\":function($event){$event.preventDefault();_vm.jumpToSeason = seasonNumber}}},[_vm._v(\"\\n \"+_vm._s(seasonNumber === 0 ? 'Specials' : seasonNumber)+\"\\n \")]),_vm._v(\" \"),(index !== (_vm.seasons.length - 1))?_c('span',{key:(\"separator-\" + index),staticClass:\"separator\"},[_vm._v(\"| \")]):_vm._e()]})]:_vm._e()],2)])]):_vm._e()]):_vm._e()]),_vm._v(\" \"),_vm._l((_vm.activeShowQueueStatuses),function(queueItem){return _c('div',{key:queueItem.action,staticClass:\"row\"},[_c('div',{staticClass:\"alert alert-info\"},[_vm._v(\"\\n \"+_vm._s(queueItem.message)+\"\\n \")])])}),_vm._v(\" \"),_c('div',{staticClass:\"row\",attrs:{\"id\":\"row-show-summary\"}},[_c('div',{staticClass:\"col-md-12\",attrs:{\"id\":\"col-show-summary\"}},[_c('div',{staticClass:\"show-poster-container\"},[_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"image-flex-container col-md-12\"},[_c('asset',{attrs:{\"default\":\"images/poster.png\",\"show-slug\":_vm.show.id.slug,\"type\":\"posterThumb\",\"cls\":\"show-image shadow\",\"link\":true}})],1)])]),_vm._v(\" \"),_c('div',{staticClass:\"ver-spacer\"}),_vm._v(\" \"),_c('div',{staticClass:\"show-info-container\"},[_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"pull-right col-lg-3 col-md-3 hidden-sm hidden-xs\"},[_c('asset',{attrs:{\"default\":\"images/banner.png\",\"show-slug\":_vm.show.id.slug,\"type\":\"banner\",\"cls\":\"show-banner pull-right shadow\",\"link\":true}})],1),_vm._v(\" \"),_c('div',{staticClass:\"pull-left col-lg-9 col-md-9 col-sm-12 col-xs-12\",attrs:{\"id\":\"show-rating\"}},[(_vm.show.rating.imdb && _vm.show.rating.imdb.rating)?_c('span',{staticClass:\"imdbstars\",attrs:{\"qtip-content\":((_vm.show.rating.imdb.rating) + \" / 10 Stars
\" + (_vm.show.rating.imdb.votes) + \" Votes\")}},[_c('span',{style:({ width: (Number(_vm.show.rating.imdb.rating) * 12) + '%' })})]):_vm._e(),_vm._v(\" \"),(!_vm.show.id.imdb)?[(_vm.show.year.start)?_c('span',[_vm._v(\"(\"+_vm._s(_vm.show.year.start)+\") - \"+_vm._s(_vm.show.runtime)+\" minutes - \")]):_vm._e()]:[_vm._l((_vm.show.countryCodes),function(country){return _c('img',{key:'flag-' + country,class:['country-flag', 'flag-' + country],staticStyle:{\"margin-left\":\"3px\",\"vertical-align\":\"middle\"},attrs:{\"src\":\"images/blank.png\",\"width\":\"16\",\"height\":\"11\"}})}),_vm._v(\" \"),(_vm.show.imdbInfo.year)?_c('span',[_vm._v(\"\\n (\"+_vm._s(_vm.show.imdbInfo.year)+\") -\\n \")]):_vm._e(),_vm._v(\" \"),_c('span',[_vm._v(\"\\n \"+_vm._s(_vm.show.imdbInfo.runtimes || _vm.show.runtime)+\" minutes\\n \")]),_vm._v(\" \"),_c('app-link',{attrs:{\"href\":'https://www.imdb.com/title/' + _vm.show.id.imdb,\"title\":'https://www.imdb.com/title/' + _vm.show.id.imdb}},[_c('img',{staticStyle:{\"margin-top\":\"-1px\",\"vertical-align\":\"middle\"},attrs:{\"alt\":\"[imdb]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/imdb.png\"}})])],_vm._v(\" \"),(_vm.show.id.trakt)?_c('app-link',{attrs:{\"href\":'https://trakt.tv/shows/' + _vm.show.id.trakt,\"title\":'https://trakt.tv/shows/' + _vm.show.id.trakt}},[_c('img',{attrs:{\"alt\":\"[trakt]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/trakt.png\"}})]):_vm._e(),_vm._v(\" \"),(_vm.showIndexerUrl && _vm.indexerConfig[_vm.show.indexer].icon)?_c('app-link',{attrs:{\"href\":_vm.showIndexerUrl,\"title\":_vm.showIndexerUrl}},[_c('img',{staticStyle:{\"margin-top\":\"-1px\",\"vertical-align\":\"middle\"},attrs:{\"alt\":_vm.indexerConfig[_vm.show.indexer].name,\"height\":\"16\",\"width\":\"16\",\"src\":'images/' + _vm.indexerConfig[_vm.show.indexer].icon}})]):_vm._e(),_vm._v(\" \"),(_vm.show.xemNumbering && _vm.show.xemNumbering.length > 0)?_c('app-link',{attrs:{\"href\":'http://thexem.de/search?q=' + _vm.show.title,\"title\":'http://thexem.de/search?q=' + _vm.show.title}},[_c('img',{staticStyle:{\"margin-top\":\"-1px\",\"vertical-align\":\"middle\"},attrs:{\"alt\":\"[xem]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/xem.png\"}})]):_vm._e(),_vm._v(\" \"),(_vm.show.id.tvdb)?_c('app-link',{attrs:{\"href\":'https://fanart.tv/series/' + _vm.show.id.tvdb,\"title\":'https://fanart.tv/series/' + _vm.show.id[_vm.show.indexer]}},[_c('img',{staticClass:\"fanart\",attrs:{\"alt\":\"[fanart.tv]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/fanart.tv.png\"}})]):_vm._e()],2),_vm._v(\" \"),_c('div',{staticClass:\"pull-left col-lg-9 col-md-9 col-sm-12 col-xs-12\",attrs:{\"id\":\"tags\"}},[(_vm.show.genres)?_c('ul',{staticClass:\"tags\"},_vm._l((_vm.dedupeGenres(_vm.show.genres)),function(genre){return _c('app-link',{key:genre.toString(),attrs:{\"href\":'https://trakt.tv/shows/popular/?genres=' + genre.toLowerCase().replace(' ', '-'),\"title\":'View other popular ' + genre + ' shows on trakt.tv'}},[_c('li',[_vm._v(_vm._s(genre))])])}),1):_c('ul',{staticClass:\"tags\"},_vm._l((_vm.showGenres),function(genre){return _c('app-link',{key:genre.toString(),attrs:{\"href\":'https://www.imdb.com/search/title?count=100&title_type=tv_series&genres=' + genre.toLowerCase().replace(' ', '-'),\"title\":'View other popular ' + genre + ' shows on IMDB'}},[_c('li',[_vm._v(_vm._s(genre))])])}),1)])]),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[(_vm.configLoaded)?_c('div',{staticClass:\"col-md-12\",attrs:{\"id\":\"summary\"}},[_c('div',{class:[{ summaryFanArt: _vm.config.fanartBackground }, 'col-lg-9', 'col-md-8', 'col-sm-8', 'col-xs-12'],attrs:{\"id\":\"show-summary\"}},[_c('table',{staticClass:\"summaryTable pull-left\"},[(_vm.show.plot)?_c('tr',[_c('td',{staticStyle:{\"padding-bottom\":\"15px\"},attrs:{\"colspan\":\"2\"}},[_c('truncate',{attrs:{\"length\":250,\"clamp\":\"show more...\",\"less\":\"show less...\",\"text\":_vm.show.plot},on:{\"toggle\":function($event){return _vm.$emit('reflow')}}})],1)]):_vm._e(),_vm._v(\" \"),(_vm.getQualityPreset({ value: _vm.combinedQualities }) !== undefined)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Quality:\")]),_vm._v(\" \"),_c('td',[_c('quality-pill',{attrs:{\"quality\":_vm.combinedQualities}})],1)]):[(_vm.combineQualities(_vm.show.config.qualities.allowed) > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Allowed Qualities:\")]),_vm._v(\" \"),_c('td',[_vm._l((_vm.show.config.qualities.allowed),function(curQuality,index){return [_vm._v(_vm._s(index > 0 ? ', ' : '')),_c('quality-pill',{key:(\"allowed-\" + curQuality),attrs:{\"quality\":curQuality}})]})],2)]):_vm._e(),_vm._v(\" \"),(_vm.combineQualities(_vm.show.config.qualities.preferred) > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Preferred Qualities:\")]),_vm._v(\" \"),_c('td',[_vm._l((_vm.show.config.qualities.preferred),function(curQuality,index){return [_vm._v(_vm._s(index > 0 ? ', ' : '')),_c('quality-pill',{key:(\"preferred-\" + curQuality),attrs:{\"quality\":curQuality}})]})],2)]):_vm._e()],_vm._v(\" \"),(_vm.show.network && _vm.show.airs)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Originally Airs: \")]),_c('td',[_vm._v(_vm._s(_vm.show.airs)),(!_vm.show.airsFormatValid)?_c('b',{staticClass:\"invalid-value\"},[_vm._v(\" (invalid time format)\")]):_vm._e(),_vm._v(\" on \"+_vm._s(_vm.show.network))])]):(_vm.show.network)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Originally Airs: \")]),_c('td',[_vm._v(_vm._s(_vm.show.network))])]):(_vm.show.airs)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Originally Airs: \")]),_c('td',[_vm._v(_vm._s(_vm.show.airs)),(!_vm.show.airsFormatValid)?_c('b',{staticClass:\"invalid-value\"},[_vm._v(\" (invalid time format)\")]):_vm._e()])]):_vm._e(),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Show Status: \")]),_c('td',[_vm._v(_vm._s(_vm.show.status))])]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Default EP Status: \")]),_c('td',[_vm._v(_vm._s(_vm.show.config.defaultEpisodeStatus))])]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_c('span',{class:{'invalid-value': !_vm.show.config.locationValid}},[_vm._v(\"Location: \")])]),_c('td',[_c('span',{class:{'invalid-value': !_vm.show.config.locationValid}},[_vm._v(_vm._s(_vm.show.config.location))]),_vm._v(_vm._s(_vm.show.config.locationValid ? '' : ' (Missing)'))])]),_vm._v(\" \"),(_vm.show.config.aliases.length > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\",staticStyle:{\"vertical-align\":\"top\"}},[_vm._v(\"Scene Name:\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.show.config.aliases.join(', ')))])]):_vm._e(),_vm._v(\" \"),(_vm.show.config.release.requiredWords.length + _vm.search.filters.required.length > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\",staticStyle:{\"vertical-align\":\"top\"}},[_c('span',{class:{required: _vm.type === 'snatch-selection'}},[_vm._v(\"Required Words: \")])]),_vm._v(\" \"),_c('td',[(_vm.show.config.release.requiredWords.length)?_c('span',{staticClass:\"break-word\"},[_vm._v(\"\\n \"+_vm._s(_vm.show.config.release.requiredWords.join(', '))+\"\\n \")]):_vm._e(),_vm._v(\" \"),(_vm.search.filters.required.length > 0)?_c('span',{staticClass:\"break-word global-filter\"},[_c('app-link',{attrs:{\"href\":\"config/search/#searchfilters\"}},[(_vm.show.config.release.requiredWords.length > 0)?[(_vm.show.config.release.requiredWordsExclude)?_c('span',[_vm._v(\" excluded from: \")]):_c('span',[_vm._v(\"+ \")])]:_vm._e(),_vm._v(\"\\n \"+_vm._s(_vm.search.filters.required.join(', '))+\"\\n \")],2)],1):_vm._e()])]):_vm._e(),_vm._v(\" \"),(_vm.show.config.release.ignoredWords.length + _vm.search.filters.ignored.length > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\",staticStyle:{\"vertical-align\":\"top\"}},[_c('span',{class:{ignored: _vm.type === 'snatch-selection'}},[_vm._v(\"Ignored Words: \")])]),_vm._v(\" \"),_c('td',[(_vm.show.config.release.ignoredWords.length)?_c('span',{staticClass:\"break-word\"},[_vm._v(\"\\n \"+_vm._s(_vm.show.config.release.ignoredWords.join(', '))+\"\\n \")]):_vm._e(),_vm._v(\" \"),(_vm.search.filters.ignored.length > 0)?_c('span',{staticClass:\"break-word global-filter\"},[_c('app-link',{attrs:{\"href\":\"config/search/#searchfilters\"}},[(_vm.show.config.release.ignoredWords.length > 0)?[(_vm.show.config.release.ignoredWordsExclude)?_c('span',[_vm._v(\" excluded from: \")]):_c('span',[_vm._v(\"+ \")])]:_vm._e(),_vm._v(\"\\n \"+_vm._s(_vm.search.filters.ignored.join(', '))+\"\\n \")],2)],1):_vm._e()])]):_vm._e(),_vm._v(\" \"),(_vm.search.filters.preferred.length > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\",staticStyle:{\"vertical-align\":\"top\"}},[_c('span',{class:{preferred: _vm.type === 'snatch-selection'}},[_vm._v(\"Preferred Words: \")])]),_vm._v(\" \"),_c('td',[_c('app-link',{attrs:{\"href\":\"config/search/#searchfilters\"}},[_c('span',{staticClass:\"break-word\"},[_vm._v(_vm._s(_vm.search.filters.preferred.join(', ')))])])],1)]):_vm._e(),_vm._v(\" \"),(_vm.search.filters.undesired.length > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\",staticStyle:{\"vertical-align\":\"top\"}},[_c('span',{class:{undesired: _vm.type === 'snatch-selection'}},[_vm._v(\"Undesired Words: \")])]),_vm._v(\" \"),_c('td',[_c('app-link',{attrs:{\"href\":\"config/search/#searchfilters\"}},[_c('span',{staticClass:\"break-word\"},[_vm._v(_vm._s(_vm.search.filters.undesired.join(', ')))])])],1)]):_vm._e(),_vm._v(\" \"),(_vm.show.config.release.whitelist && _vm.show.config.release.whitelist.length > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Wanted Groups:\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.show.config.release.whitelist.join(', ')))])]):_vm._e(),_vm._v(\" \"),(_vm.show.config.release.blacklist && _vm.show.config.release.blacklist.length > 0)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Unwanted Groups:\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.show.config.release.blacklist.join(', ')))])]):_vm._e(),_vm._v(\" \"),(_vm.show.config.airdateOffset !== 0)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Daily search offset:\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.show.config.airdateOffset)+\" hours\")])]):_vm._e(),_vm._v(\" \"),(_vm.show.config.locationValid && _vm.show.size > -1)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Size:\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.humanFileSize(_vm.show.size)))])]):_vm._e()],2)]),_vm._v(\" \"),_c('div',{staticClass:\"col-lg-3 col-md-4 col-sm-4 col-xs-12 pull-xs-left\",attrs:{\"id\":\"show-status\"}},[_c('table',{staticClass:\"pull-xs-left pull-md-right pull-sm-right pull-lg-right\"},[(_vm.show.language)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Info Language:\")]),_c('td',[_c('img',{attrs:{\"src\":'images/subtitles/flags/' + _vm.getCountryISO2ToISO3(_vm.show.language) + '.png',\"width\":\"16\",\"height\":\"11\",\"alt\":_vm.show.language,\"title\":_vm.show.language,\"onError\":\"this.onerror=null;this.src='images/flags/unknown.png';\"}})])]):_vm._e(),_vm._v(\" \"),(_vm.config.subtitles.enabled)?_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Subtitles: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.subtitlesEnabled},on:{\"click\":function($event){return _vm.toggleConfigOption('subtitlesEnabled');}}})],1)]):_vm._e(),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Season Folders: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.seasonFolders || _vm.config.namingForceFolders}})],1)]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Paused: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.paused},on:{\"click\":function($event){return _vm.toggleConfigOption('paused')}}})],1)]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Air-by-Date: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.airByDate},on:{\"click\":function($event){return _vm.toggleConfigOption('airByDate')}}})],1)]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Sports: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.sports},on:{\"click\":function($event){return _vm.toggleConfigOption('sports')}}})],1)]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Anime: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.anime},on:{\"click\":function($event){return _vm.toggleConfigOption('anime')}}})],1)]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"DVD Order: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.dvdOrder},on:{\"click\":function($event){return _vm.toggleConfigOption('dvdOrder')}}})],1)]),_vm._v(\" \"),_c('tr',[_c('td',{staticClass:\"showLegend\"},[_vm._v(\"Scene Numbering: \")]),_c('td',[_c('state-switch',{attrs:{\"theme\":_vm.config.themeName,\"state\":_vm.show.config.scene},on:{\"click\":function($event){return _vm.toggleConfigOption('scene')}}})],1)])])])]):_vm._e()])])])]),_vm._v(\" \"),(_vm.show)?_c('div',{staticClass:\"row\",attrs:{\"id\":\"row-show-episodes-controls\"}},[_c('div',{staticClass:\"col-md-12\",attrs:{\"id\":\"col-show-episodes-controls\"}},[(_vm.type === 'show')?_c('div',{staticClass:\"row key\"},[_c('div',{staticClass:\"col-lg-12\",attrs:{\"id\":\"checkboxControls\"}},[(_vm.show.seasons)?_c('div',{staticClass:\"pull-left top-5\",attrs:{\"id\":\"key-padding\"}},_vm._l((_vm.overviewStatus),function(status){return _c('label',{key:status.id,attrs:{\"for\":status.id}},[_c('span',{class:status.id},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(status.checked),expression:\"status.checked\"}],attrs:{\"type\":\"checkbox\",\"id\":status.id},domProps:{\"checked\":Array.isArray(status.checked)?_vm._i(status.checked,null)>-1:(status.checked)},on:{\"change\":[function($event){var $$a=status.checked,$$el=$event.target,$$c=$$el.checked?(true):(false);if(Array.isArray($$a)){var $$v=null,$$i=_vm._i($$a,$$v);if($$el.checked){$$i<0&&(_vm.$set(status, \"checked\", $$a.concat([$$v])))}else{$$i>-1&&(_vm.$set(status, \"checked\", $$a.slice(0,$$i).concat($$a.slice($$i+1))))}}else{_vm.$set(status, \"checked\", $$c)}},function($event){return _vm.$emit('update-overview-status', _vm.overviewStatus)}]}}),_vm._v(\"\\n \"+_vm._s(status.name)+\": \"),_c('b',[_vm._v(_vm._s(_vm.episodeSummary[status.name]))])])])}),0):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"pull-lg-right top-5\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedStatus),expression:\"selectedStatus\"}],staticClass:\"form-control form-control-inline input-sm-custom input-sm-smallfont\",attrs:{\"id\":\"statusSelect\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedStatus=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[_c('option',{domProps:{\"value\":'Change status to:'}},[_vm._v(\"Change status to:\")]),_vm._v(\" \"),_vm._l((_vm.changeStatusOptions),function(status){return _c('option',{key:status.key,domProps:{\"value\":status.value}},[_vm._v(\"\\n \"+_vm._s(status.name)+\"\\n \")])})],2),_vm._v(\" \"),_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedQuality),expression:\"selectedQuality\"}],staticClass:\"form-control form-control-inline input-sm-custom input-sm-smallfont\",attrs:{\"id\":\"qualitySelect\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedQuality=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[_c('option',{domProps:{\"value\":'Change quality to:'}},[_vm._v(\"Change quality to:\")]),_vm._v(\" \"),_vm._l((_vm.qualities),function(quality){return _c('option',{key:quality.key,domProps:{\"value\":quality.value}},[_vm._v(\"\\n \"+_vm._s(quality.name)+\"\\n \")])})],2),_vm._v(\" \"),_c('input',{attrs:{\"type\":\"hidden\",\"id\":\"series-slug\"},domProps:{\"value\":_vm.show.id.slug}}),_vm._v(\" \"),_c('input',{attrs:{\"type\":\"hidden\",\"id\":\"series-id\"},domProps:{\"value\":_vm.show.id[_vm.show.indexer]}}),_vm._v(\" \"),_c('input',{attrs:{\"type\":\"hidden\",\"id\":\"indexer\"},domProps:{\"value\":_vm.show.indexer}}),_vm._v(\" \"),_c('input',{staticClass:\"btn-medusa\",attrs:{\"type\":\"button\",\"id\":\"changeStatus\",\"value\":\"Go\"},on:{\"click\":_vm.changeStatusClicked}})])])]):_c('div')])]):_vm._e()],2)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./show-header.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./show-header.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./show-header.vue?vue&type=template&id=411f7edb&scoped=true&\"\nimport script from \"./show-header.vue?vue&type=script&lang=js&\"\nexport * from \"./show-header.vue?vue&type=script&lang=js&\"\nimport style0 from \"./show-header.vue?vue&type=style&index=0&id=411f7edb&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"411f7edb\",\n null\n \n)\n\nexport default component.exports","import { api } from '../api';\n\n/**\n * Attach a jquery qtip to elements with the .imdbstars class.\n */\nexport const attachImdbTooltip = () => {\n $('.imdbstars').qtip({\n content: {\n text() {\n // Retrieve content from custom attribute of the $('.selector') elements.\n return $(this).attr('qtip-content');\n }\n },\n show: {\n solo: true\n },\n position: {\n my: 'right center',\n at: 'center left',\n adjust: {\n y: 0,\n x: -6\n }\n },\n style: {\n tip: {\n corner: true,\n method: 'polygon'\n },\n classes: 'qtip-rounded qtip-shadow ui-tooltip-sb'\n }\n });\n};\n\n/**\n * Attach a default qtip to elements with the addQTip class.\n */\nexport const addQTip = () => {\n $('.addQTip').each((_, element) => {\n $(element).css({\n cursor: 'help',\n 'text-shadow': '0px 0px 0.5px #666'\n });\n\n const my = $(element).data('qtip-my') || 'left center';\n const at = $(element).data('qtip-at') || 'middle right';\n\n $(element).qtip({\n show: {\n solo: true\n },\n position: {\n my,\n at\n },\n style: {\n tip: {\n corner: true,\n method: 'polygon'\n },\n classes: 'qtip-rounded qtip-shadow ui-tooltip-sb'\n }\n });\n });\n};\n\n/**\n * Start checking for running searches.\n * @param {String} showSlug - Show slug\n * @param {Object} vm - vue instance\n */\nexport const updateSearchIcons = (showSlug, vm) => {\n if ($.fn.updateSearchIconsStarted || !showSlug) {\n return;\n }\n\n $.fn.updateSearchIconsStarted = true;\n $.fn.forcedSearches = [];\n\n const enableLink = el => {\n el.disabled = false;\n };\n\n const disableLink = el => {\n el.disabled = true;\n };\n\n /**\n * Update search icons based on it's current search status (queued, error, searched)\n * @param {*} results - Search queue results\n * @param {*} vm - Vue instance\n */\n const updateImages = results => {\n $.each(results, (_, ep) => {\n // Get td element for current ep\n const loadingImage = 'loading16.gif';\n const queuedImage = 'queued.png';\n const searchImage = 'search16.png';\n\n if (ep.show.slug !== vm.show.id.slug) {\n return true;\n }\n\n // Try to get the
Element\n const img = vm.$refs[`search-${ep.episode.slug}`];\n if (img) {\n if (ep.search.status.toLowerCase() === 'searching') {\n // El=$('td#' + ep.season + 'x' + ep.episode + '.search img');\n img.title = 'Searching';\n img.alt = 'Searching';\n img.src = 'images/' + loadingImage;\n disableLink(img);\n } else if (ep.search.status.toLowerCase() === 'queued') {\n // El=$('td#' + ep.season + 'x' + ep.episode + '.search img');\n img.title = 'Queued';\n img.alt = 'queued';\n img.src = 'images/' + queuedImage;\n disableLink(img);\n } else if (ep.search.status.toLowerCase() === 'finished') {\n // El=$('td#' + ep.season + 'x' + ep.episode + '.search img');\n img.title = 'Searching';\n img.alt = 'searching';\n img.src = 'images/' + searchImage;\n enableLink(img);\n }\n }\n });\n };\n\n /**\n * Check the search queues / history for current or past searches and update the icons.\n */\n const checkManualSearches = () => {\n let pollInterval = 5000;\n\n api.get(`search/${showSlug}`)\n .then(response => {\n if (response.data.results && response.data.results.length > 0) {\n pollInterval = 5000;\n } else {\n pollInterval = 15000;\n }\n\n updateImages(response.data.results);\n }).catch(error => {\n console.error(String(error));\n pollInterval = 30000;\n }).finally(() => {\n setTimeout(checkManualSearches, pollInterval);\n });\n };\n\n checkManualSearches();\n};\n","\n\n\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./add-show-options.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./add-show-options.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./add-show-options.vue?vue&type=template&id=63a4b08d&\"\nimport script from \"./add-show-options.vue?vue&type=script&lang=js&\"\nexport * from \"./add-show-options.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"add-show-options-content\"}},[_c('fieldset',{staticClass:\"component-group-list\"},[_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(0),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('quality-chooser',{attrs:{\"overall-quality\":_vm.defaultConfig.quality},on:{\"update:quality:allowed\":function($event){_vm.quality.allowed = $event},\"update:quality:preferred\":function($event){_vm.quality.preferred = $event}}})],1)])]),_vm._v(\" \"),(_vm.subtitlesEnabled)?_c('div',{attrs:{\"id\":\"use-subtitles\"}},[_c('config-toggle-slider',{attrs:{\"label\":\"Subtitles\",\"id\":\"subtitles\",\"value\":_vm.selectedSubtitleEnabled,\"explanations\":['Download subtitles for this show?']},on:{\"input\":function($event){_vm.selectedSubtitleEnabled = $event}}})],1):_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(1),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedStatus),expression:\"selectedStatus\"}],staticClass:\"form-control form-control-inline input-sm\",attrs:{\"id\":\"defaultStatus\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedStatus=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},_vm._l((_vm.defaultEpisodeStatusOptions),function(option){return _c('option',{key:option.value,domProps:{\"value\":option.value}},[_vm._v(_vm._s(option.name))])}),0)])])]),_vm._v(\" \"),_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(2),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedStatusAfter),expression:\"selectedStatusAfter\"}],staticClass:\"form-control form-control-inline input-sm\",attrs:{\"id\":\"defaultStatusAfter\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedStatusAfter=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},_vm._l((_vm.defaultEpisodeStatusOptions),function(option){return _c('option',{key:option.value,domProps:{\"value\":option.value}},[_vm._v(_vm._s(option.name))])}),0)])])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Season Folders\",\"id\":\"season_folders\",\"value\":_vm.selectedSeasonFoldersEnabled,\"disabled\":_vm.namingForceFolders,\"explanations\":['Group episodes by season folders?']},on:{\"input\":function($event){_vm.selectedSeasonFoldersEnabled = $event}}}),_vm._v(\" \"),(_vm.enableAnimeOptions)?_c('config-toggle-slider',{attrs:{\"label\":\"Anime\",\"id\":\"anime\",\"value\":_vm.selectedAnimeEnabled,\"explanations\":['Is this show an Anime?']},on:{\"input\":function($event){_vm.selectedAnimeEnabled = $event}}}):_vm._e(),_vm._v(\" \"),(_vm.enableAnimeOptions && _vm.selectedAnimeEnabled)?_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(3),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('anidb-release-group-ui',{staticClass:\"max-width\",attrs:{\"show-name\":_vm.showName},on:{\"change\":_vm.onChangeReleaseGroupsAnime}})],1)])]):_vm._e(),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Scene Numbering\",\"id\":\"scene\",\"value\":_vm.selectedSceneEnabled,\"explanations\":['Is this show scene numbered?']},on:{\"input\":function($event){_vm.selectedSceneEnabled = $event}}}),_vm._v(\" \"),_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(4),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('button',{staticClass:\"btn-medusa btn-inline\",attrs:{\"type\":\"button\",\"disabled\":_vm.saving || _vm.saveDefaultsDisabled},on:{\"click\":function($event){$event.preventDefault();return _vm.saveDefaults($event)}}},[_vm._v(\"Save Defaults\")])])])])],1)])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"customQuality\"}},[_c('span',[_vm._v(\"Quality\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"defaultStatus\"}},[_c('span',[_vm._v(\"Status for previously aired episodes\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"defaultStatusAfter\"}},[_c('span',[_vm._v(\"Status for all future episodes\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"anidbReleaseGroup\"}},[_c('span',[_vm._v(\"Release Groups\")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('label',{staticClass:\"col-sm-2 control-label\",attrs:{\"for\":\"saveDefaultsButton\"}},[_c('span',[_vm._v(\"Use current values as the defaults\")])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app-footer.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app-footer.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./app-footer.vue?vue&type=template&id=b234372e&scoped=true&\"\nimport script from \"./app-footer.vue?vue&type=script&lang=js&\"\nexport * from \"./app-footer.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"b234372e\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('footer',[_c('div',{staticClass:\"footer clearfix\"},[_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.stats.overall.shows.total))]),_vm._v(\" Shows (\"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.stats.overall.shows.active))]),_vm._v(\" Active)\\n | \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.stats.overall.episodes.downloaded))]),_vm._v(\" \"),(_vm.stats.overall.episodes.snatched)?[_c('span',{staticClass:\"footerhighlight\"},[_c('app-link',{attrs:{\"href\":(\"manage/episodeStatuses?whichStatus=\" + _vm.snatchedStatus),\"title\":\"View overview of snatched episodes\"}},[_vm._v(\"+\"+_vm._s(_vm.stats.overall.episodes.snatched))])],1),_vm._v(\"\\n Snatched\\n \")]:_vm._e(),_vm._v(\"\\n / \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.stats.overall.episodes.total))]),_vm._v(\" Episodes Downloaded \"),(_vm.episodePercentage)?_c('span',{staticClass:\"footerhighlight\"},[_vm._v(\"(\"+_vm._s(_vm.episodePercentage)+\")\")]):_vm._e(),_vm._v(\"\\n | Daily Search: \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.schedulerNextRun('dailySearch')))]),_vm._v(\"\\n | Backlog Search: \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.schedulerNextRun('backlog')))]),_vm._v(\" \"),_c('div',[(_vm.system.memoryUsage)?[_vm._v(\"\\n Memory used: \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.system.memoryUsage))]),_vm._v(\" |\\n \")]:_vm._e(),_vm._v(\" \"),_vm._v(\"\\n Branch: \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.config.branch || 'Unknown'))]),_vm._v(\" |\\n Now: \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.nowInUserPreset))])],2)],2)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app-header.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app-header.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./app-header.vue?vue&type=template&id=76fdd3a5&\"\nimport script from \"./app-header.vue?vue&type=script&lang=js&\"\nexport * from \"./app-header.vue?vue&type=script&lang=js&\"\nimport style0 from \"./app-header.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('nav',{staticClass:\"navbar navbar-default navbar-fixed-top hidden-print\",attrs:{\"role\":\"navigation\"}},[_c('div',{staticClass:\"container-fluid\"},[_c('div',{staticClass:\"navbar-header\"},[_c('button',{staticClass:\"navbar-toggle collapsed\",attrs:{\"type\":\"button\",\"data-toggle\":\"collapse\",\"data-target\":\"#main_nav\"}},[(_vm.toolsBadgeCount > 0)?_c('span',{class:(\"floating-badge\" + _vm.toolsBadgeClass)},[_vm._v(_vm._s(_vm.toolsBadgeCount))]):_vm._e(),_vm._v(\" \"),_c('span',{staticClass:\"sr-only\"},[_vm._v(\"Toggle navigation\")]),_vm._v(\" \"),_c('span',{staticClass:\"icon-bar\"}),_vm._v(\" \"),_c('span',{staticClass:\"icon-bar\"}),_vm._v(\" \"),_c('span',{staticClass:\"icon-bar\"})]),_vm._v(\" \"),_c('app-link',{staticClass:\"navbar-brand\",attrs:{\"href\":\"home/\",\"title\":\"Medusa\"}},[_c('img',{staticClass:\"img-responsive pull-left\",staticStyle:{\"height\":\"50px\"},attrs:{\"alt\":\"Medusa\",\"src\":\"images/medusa.png\"}})])],1),_vm._v(\" \"),(_vm.isAuthenticated)?_c('div',{staticClass:\"collapse navbar-collapse\",attrs:{\"id\":\"main_nav\"}},[_c('ul',{staticClass:\"nav navbar-nav navbar-right\"},[_c('li',{staticClass:\"navbar-split dropdown\",class:{ active: _vm.topMenu === 'home' },attrs:{\"id\":\"NAVhome\"}},[_c('app-link',{staticClass:\"dropdown-toggle\",attrs:{\"href\":\"home/\",\"aria-haspopup\":\"true\",\"data-toggle\":\"dropdown\",\"data-hover\":\"dropdown\"}},[_c('span',[_vm._v(\"Shows\")]),_vm._v(\" \"),_c('b',{staticClass:\"caret\"})]),_vm._v(\" \"),_c('ul',{staticClass:\"dropdown-menu\"},[_c('li',[_c('app-link',{attrs:{\"href\":\"home/\"}},[_c('i',{staticClass:\"menu-icon-home\"}),_vm._v(\" Show List\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"addShows/\"}},[_c('i',{staticClass:\"menu-icon-addshow\"}),_vm._v(\" Add Shows\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"addRecommended/\"}},[_c('i',{staticClass:\"menu-icon-addshow\"}),_vm._v(\" Add Recommended Shows\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"home/postprocess/\"}},[_c('i',{staticClass:\"menu-icon-postprocess\"}),_vm._v(\" Manual Post-Processing\")])],1),_vm._v(\" \"),(_vm.recentShows.length > 0)?_c('li',{staticClass:\"divider\",attrs:{\"role\":\"separator\"}}):_vm._e(),_vm._v(\" \"),_vm._l((_vm.recentShows),function(recentShow){return _c('li',{key:recentShow.link},[_c('app-link',{attrs:{\"href\":recentShow.link}},[_c('i',{staticClass:\"menu-icon-addshow\"}),_vm._v(\" \"+_vm._s(recentShow.name)+\"\\n \")])],1)})],2),_vm._v(\" \"),_c('div',{staticStyle:{\"clear\":\"both\"}})],1),_vm._v(\" \"),_c('li',{class:{ active: _vm.topMenu === 'schedule' },attrs:{\"id\":\"NAVschedule\"}},[_c('app-link',{attrs:{\"href\":\"schedule/\"}},[_vm._v(\"Schedule\")])],1),_vm._v(\" \"),_c('li',{class:{ active: _vm.topMenu === 'history' },attrs:{\"id\":\"NAVhistory\"}},[_c('app-link',{attrs:{\"href\":\"history/\"}},[_vm._v(\"History\")])],1),_vm._v(\" \"),_c('li',{staticClass:\"navbar-split dropdown\",class:{ active: _vm.topMenu === 'manage' },attrs:{\"id\":\"NAVmanage\"}},[_c('app-link',{staticClass:\"dropdown-toggle\",attrs:{\"href\":\"manage/episodeStatuses/\",\"aria-haspopup\":\"true\",\"data-toggle\":\"dropdown\",\"data-hover\":\"dropdown\"}},[_c('span',[_vm._v(\"Manage\")]),_vm._v(\" \"),_c('b',{staticClass:\"caret\"})]),_vm._v(\" \"),_c('ul',{staticClass:\"dropdown-menu\"},[_c('li',[_c('app-link',{attrs:{\"href\":\"manage/\"}},[_c('i',{staticClass:\"menu-icon-manage\"}),_vm._v(\" Mass Update\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"manage/backlogOverview/\"}},[_c('i',{staticClass:\"menu-icon-backlog-view\"}),_vm._v(\" Backlog Overview\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"manage/manageSearches/\"}},[_c('i',{staticClass:\"menu-icon-manage-searches\"}),_vm._v(\" Manage Searches\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"manage/episodeStatuses/\"}},[_c('i',{staticClass:\"menu-icon-manage2\"}),_vm._v(\" Episode Status Management\")])],1),_vm._v(\" \"),(_vm.linkVisible.plex)?_c('li',[_c('app-link',{attrs:{\"href\":\"home/updatePLEX/\"}},[_c('i',{staticClass:\"menu-icon-plex\"}),_vm._v(\" Update PLEX\")])],1):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.kodi)?_c('li',[_c('app-link',{attrs:{\"href\":\"home/updateKODI/\"}},[_c('i',{staticClass:\"menu-icon-kodi\"}),_vm._v(\" Update KODI\")])],1):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.emby)?_c('li',[_c('app-link',{attrs:{\"href\":\"home/updateEMBY/\"}},[_c('i',{staticClass:\"menu-icon-emby\"}),_vm._v(\" Update Emby\")])],1):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.manageTorrents)?_c('li',[_c('app-link',{attrs:{\"href\":\"manage/manageTorrents/\",\"target\":\"_blank\"}},[_c('i',{staticClass:\"menu-icon-bittorrent\"}),_vm._v(\" Manage Torrents\")])],1):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.failedDownloads)?_c('li',[_c('app-link',{attrs:{\"href\":\"manage/failedDownloads/\"}},[_c('i',{staticClass:\"menu-icon-failed-download\"}),_vm._v(\" Failed Downloads\")])],1):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.subtitleMissed)?_c('li',[_c('app-link',{attrs:{\"href\":\"manage/subtitleMissed/\"}},[_c('i',{staticClass:\"menu-icon-backlog\"}),_vm._v(\" Missed Subtitle Management\")])],1):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.subtitleMissedPP)?_c('li',[_c('app-link',{attrs:{\"href\":\"manage/subtitleMissedPP/\"}},[_c('i',{staticClass:\"menu-icon-backlog\"}),_vm._v(\" Missed Subtitle in Post-Process folder\")])],1):_vm._e()]),_vm._v(\" \"),_c('div',{staticStyle:{\"clear\":\"both\"}})],1),_vm._v(\" \"),_c('li',{staticClass:\"navbar-split dropdown\",class:{ active: _vm.topMenu === 'config' },attrs:{\"id\":\"NAVconfig\"}},[_c('app-link',{staticClass:\"dropdown-toggle\",attrs:{\"href\":\"config/\",\"aria-haspopup\":\"true\",\"data-toggle\":\"dropdown\",\"data-hover\":\"dropdown\"}},[_c('span',{staticClass:\"visible-xs-inline\"},[_vm._v(\"Config\")]),_c('img',{staticClass:\"navbaricon hidden-xs\",attrs:{\"src\":\"images/menu/system18.png\"}}),_vm._v(\" \"),_c('b',{staticClass:\"caret\"})]),_vm._v(\" \"),_c('ul',{staticClass:\"dropdown-menu\"},[_c('li',[_c('app-link',{attrs:{\"href\":\"config/\"}},[_c('i',{staticClass:\"menu-icon-help\"}),_vm._v(\" Help & Info\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/general/\"}},[_c('i',{staticClass:\"menu-icon-config\"}),_vm._v(\" General\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/backuprestore/\"}},[_c('i',{staticClass:\"menu-icon-backup\"}),_vm._v(\" Backup & Restore\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/search/\"}},[_c('i',{staticClass:\"menu-icon-manage-searches\"}),_vm._v(\" Search Settings\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/providers/\"}},[_c('i',{staticClass:\"menu-icon-provider\"}),_vm._v(\" Search Providers\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/subtitles/\"}},[_c('i',{staticClass:\"menu-icon-backlog\"}),_vm._v(\" Subtitles Settings\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/postProcessing/\"}},[_c('i',{staticClass:\"menu-icon-postprocess\"}),_vm._v(\" Post Processing\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/notifications/\"}},[_c('i',{staticClass:\"menu-icon-notification\"}),_vm._v(\" Notifications\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"config/anime/\"}},[_c('i',{staticClass:\"menu-icon-anime\"}),_vm._v(\" Anime\")])],1)]),_vm._v(\" \"),_c('div',{staticStyle:{\"clear\":\"both\"}})],1),_vm._v(\" \"),_c('li',{staticClass:\"navbar-split dropdown\",class:{ active: _vm.topMenu === 'system' },attrs:{\"id\":\"NAVsystem\"}},[_c('app-link',{staticClass:\"padding-right-15 dropdown-toggle\",attrs:{\"href\":\"home/status/\",\"aria-haspopup\":\"true\",\"data-toggle\":\"dropdown\",\"data-hover\":\"dropdown\"}},[_c('span',{staticClass:\"visible-xs-inline\"},[_vm._v(\"Tools\")]),_c('img',{staticClass:\"navbaricon hidden-xs\",attrs:{\"src\":\"images/menu/system18-2.png\"}}),_vm._v(\" \"),(_vm.toolsBadgeCount > 0)?_c('span',{class:(\"badge\" + _vm.toolsBadgeClass)},[_vm._v(_vm._s(_vm.toolsBadgeCount))]):_vm._e(),_vm._v(\" \"),_c('b',{staticClass:\"caret\"})]),_vm._v(\" \"),_c('ul',{staticClass:\"dropdown-menu\"},[_c('li',[_c('app-link',{attrs:{\"href\":\"news/\"}},[_c('i',{staticClass:\"menu-icon-news\"}),_vm._v(\" News \"),(_vm.config.news.unread > 0)?_c('span',{staticClass:\"badge\"},[_vm._v(_vm._s(_vm.config.news.unread))]):_vm._e()])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"IRC/\"}},[_c('i',{staticClass:\"menu-icon-irc\"}),_vm._v(\" IRC\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"changes/\"}},[_c('i',{staticClass:\"menu-icon-changelog\"}),_vm._v(\" Changelog\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":_vm.config.donationsUrl}},[_c('i',{staticClass:\"menu-icon-support\"}),_vm._v(\" Support Medusa\")])],1),_vm._v(\" \"),_c('li',{staticClass:\"divider\",attrs:{\"role\":\"separator\"}}),_vm._v(\" \"),(_vm.config.logs.numErrors > 0)?_c('li',[_c('app-link',{attrs:{\"href\":\"errorlogs/\"}},[_c('i',{staticClass:\"menu-icon-error\"}),_vm._v(\" View Errors \"),_c('span',{staticClass:\"badge btn-danger\"},[_vm._v(_vm._s(_vm.config.logs.numErrors))])])],1):_vm._e(),_vm._v(\" \"),(_vm.config.logs.numWarnings > 0)?_c('li',[_c('app-link',{attrs:{\"href\":(\"errorlogs/?level=\" + _vm.warningLevel)}},[_c('i',{staticClass:\"menu-icon-viewlog-errors\"}),_vm._v(\" View Warnings \"),_c('span',{staticClass:\"badge btn-warning\"},[_vm._v(_vm._s(_vm.config.logs.numWarnings))])])],1):_vm._e(),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"errorlogs/viewlog/\"}},[_c('i',{staticClass:\"menu-icon-viewlog\"}),_vm._v(\" View Log\")])],1),_vm._v(\" \"),_c('li',{staticClass:\"divider\",attrs:{\"role\":\"separator\"}}),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":(\"home/updateCheck?pid=\" + (_vm.config.pid))}},[_c('i',{staticClass:\"menu-icon-update\"}),_vm._v(\" Check For Updates\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":(\"home/restart/?pid=\" + (_vm.config.pid))},nativeOn:{\"click\":function($event){$event.preventDefault();return _vm.confirmDialog($event, 'restart')}}},[_c('i',{staticClass:\"menu-icon-restart\"}),_vm._v(\" Restart\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":(\"home/shutdown/?pid=\" + (_vm.config.pid))},nativeOn:{\"click\":function($event){$event.preventDefault();return _vm.confirmDialog($event, 'shutdown')}}},[_c('i',{staticClass:\"menu-icon-shutdown\"}),_vm._v(\" Shutdown\")])],1),_vm._v(\" \"),(_vm.username)?_c('li',[_c('app-link',{attrs:{\"href\":\"logout\"},nativeOn:{\"click\":function($event){$event.preventDefault();return _vm.confirmDialog($event, 'logout')}}},[_c('i',{staticClass:\"menu-icon-shutdown\"}),_vm._v(\" Logout\")])],1):_vm._e(),_vm._v(\" \"),_c('li',{staticClass:\"divider\",attrs:{\"role\":\"separator\"}}),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":\"home/status/\"}},[_c('i',{staticClass:\"menu-icon-info\"}),_vm._v(\" Server Status\")])],1)]),_vm._v(\" \"),_c('div',{staticStyle:{\"clear\":\"both\"}})],1)])]):_vm._e()])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./home.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./home.vue?vue&type=script&lang=js&\"","var render, staticRenderFns\nimport script from \"./home.vue?vue&type=script&lang=js&\"\nexport * from \"./home.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./manual-post-process.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./manual-post-process.vue?vue&type=script&lang=js&\"","var render, staticRenderFns\nimport script from \"./manual-post-process.vue?vue&type=script&lang=js&\"\nexport * from \"./manual-post-process.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./root-dirs.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./root-dirs.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./root-dirs.vue?vue&type=template&id=a78942dc&\"\nimport script from \"./root-dirs.vue?vue&type=script&lang=js&\"\nexport * from \"./root-dirs.vue?vue&type=script&lang=js&\"\nimport style0 from \"./root-dirs.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"root-dirs-wrapper\"}},[_c('div',{staticClass:\"root-dirs-selectbox\"},[_c('select',_vm._g(_vm._b({directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedRootDir),expression:\"selectedRootDir\"}],ref:\"rootDirs\",attrs:{\"name\":\"rootDir\",\"id\":\"rootDirs\",\"size\":\"6\"},on:{\"change\":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.selectedRootDir=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},'select',_vm.$attrs,false),_vm.$listeners),_vm._l((_vm.rootDirs),function(curDir){return _c('option',{key:curDir.path,domProps:{\"value\":curDir.path}},[_vm._v(\"\\n \"+_vm._s(_vm._f(\"markDefault\")(curDir))+\"\\n \")])}),0)]),_vm._v(\" \"),_c('div',{staticClass:\"root-dirs-controls\"},[_c('button',{staticClass:\"btn-medusa\",attrs:{\"type\":\"button\"},on:{\"click\":function($event){$event.preventDefault();return _vm.add($event)}}},[_vm._v(\"New\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa\",attrs:{\"type\":\"button\",\"disabled\":!_vm.selectedRootDir},on:{\"click\":function($event){$event.preventDefault();return _vm.edit($event)}}},[_vm._v(\"Edit\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa\",attrs:{\"type\":\"button\",\"disabled\":!_vm.selectedRootDir},on:{\"click\":function($event){$event.preventDefault();return _vm.remove($event)}}},[_vm._v(\"Delete\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa\",attrs:{\"type\":\"button\",\"disabled\":!_vm.selectedRootDir},on:{\"click\":function($event){$event.preventDefault();return _vm.setDefault($event)}}},[_vm._v(\"Set as Default *\")])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./snatch-selection.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./snatch-selection.vue?vue&type=script&lang=js&\"","var render, staticRenderFns\nimport script from \"./snatch-selection.vue?vue&type=script&lang=js&\"\nexport * from \"./snatch-selection.vue?vue&type=script&lang=js&\"\nimport style0 from \"./snatch-selection.vue?vue&type=style&index=0&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./status.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./status.vue?vue&type=script&lang=js&\"","var render, staticRenderFns\nimport script from \"./status.vue?vue&type=script&lang=js&\"\nexport * from \"./status.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./sub-menu.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./sub-menu.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./sub-menu.vue?vue&type=template&id=0918603e&scoped=true&\"\nimport script from \"./sub-menu.vue?vue&type=script&lang=js&\"\nexport * from \"./sub-menu.vue?vue&type=script&lang=js&\"\nimport style0 from \"./sub-menu.vue?vue&type=style&index=0&id=0918603e&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"0918603e\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (_vm.subMenu.length > 0)?_c('div',{attrs:{\"id\":\"sub-menu-wrapper\"}},[_c('div',{staticClass:\"row shadow\",attrs:{\"id\":\"sub-menu-container\"}},[_c('div',{staticClass:\"submenu-default hidden-print col-md-12\",attrs:{\"id\":\"sub-menu\"}},[_vm._l((_vm.subMenu),function(menuItem){return _c('app-link',{key:(\"sub-menu-\" + (menuItem.title)),staticClass:\"btn-medusa top-5 bottom-5\",attrs:{\"href\":menuItem.path},nativeOn:_vm._d({},[_vm.clickEventCond(menuItem),function($event){$event.preventDefault();return _vm.confirmDialog($event, menuItem.confirm)}])},[_c('span',{class:['pull-left', menuItem.icon]}),_vm._v(\" \"+_vm._s(menuItem.title)+\"\\n \")])}),_vm._v(\" \"),(_vm.showSelectorVisible)?_c('show-selector',{attrs:{\"show-slug\":_vm.curShowSlug,\"follow-selection\":\"\"}}):_vm._e()],2)]),_vm._v(\" \"),_c('div',{staticClass:\"btn-group\"})]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","// @TODO: Remove this file before v1.0.0\nimport Vue from 'vue';\nimport AsyncComputed from 'vue-async-computed';\nimport VueMeta from 'vue-meta';\nimport Snotify from 'vue-snotify';\nimport VueCookies from 'vue-cookies';\nimport VModal from 'vue-js-modal';\nimport { VTooltip } from 'v-tooltip';\n\nimport {\n AddShowOptions,\n AnidbReleaseGroupUi,\n AppFooter,\n AppHeader,\n AppLink,\n Asset,\n Backstretch,\n ConfigTemplate,\n ConfigTextbox,\n ConfigTextboxNumber,\n ConfigToggleSlider,\n FileBrowser,\n Home,\n LanguageSelect,\n ManualPostProcess,\n PlotInfo,\n QualityChooser,\n QualityPill,\n RootDirs,\n ScrollButtons,\n SelectList,\n ShowSelector,\n SnatchSelection,\n StateSwitch,\n Status,\n SubMenu\n} from './components';\nimport store from './store';\nimport { isDevelopment } from './utils/core';\n\n/**\n * Register global components and x-template components.\n */\nexport const registerGlobalComponents = () => {\n // Start with the x-template components\n let { components = [] } = window;\n\n // Add global components (in use by `main.mako`)\n // @TODO: These should be registered in an `App.vue` component when possible,\n // along with some of the `main.mako` template\n components = components.concat([\n AppFooter,\n AppHeader,\n ScrollButtons,\n SubMenu\n ]);\n\n // Add global components (in use by pages/components that are not SFCs yet)\n // Use this when it's not possible to use `components: { ... }` in a component's definition.\n // If a component that uses any of these is a SFC, please use the `components` key when defining it.\n // @TODO: Instead of globally registering these,\n // they should be registered in each component that uses them\n components = components.concat([\n AddShowOptions,\n AnidbReleaseGroupUi,\n AppLink,\n Asset,\n Backstretch,\n ConfigTemplate,\n ConfigTextbox,\n ConfigTextboxNumber,\n ConfigToggleSlider,\n FileBrowser,\n LanguageSelect,\n PlotInfo,\n QualityChooser,\n QualityPill, // @FIXME: (sharkykh) Used in a hack/workaround in `static/js/ajax-episode-search.js`\n RootDirs,\n SelectList,\n ShowSelector,\n StateSwitch\n ]);\n\n // Add components for pages that use `pageComponent`\n // @TODO: These need to be converted to Vue SFCs\n components = components.concat([\n Home,\n ManualPostProcess,\n SnatchSelection,\n Status\n ]);\n\n // Register the components globally\n components.forEach(component => {\n if (isDevelopment) {\n console.debug(`Registering ${component.name}`);\n }\n Vue.component(component.name, component);\n });\n};\n\n/**\n * Register plugins.\n */\nexport const registerPlugins = () => {\n Vue.use(AsyncComputed);\n Vue.use(VueMeta);\n Vue.use(Snotify);\n Vue.use(VueCookies);\n Vue.use(VModal);\n Vue.use(VTooltip);\n\n // Set default cookie expire time\n VueCookies.config('10y');\n};\n\n/**\n * Apply the global Vue shim.\n */\nexport default () => {\n const warningTemplate = (name, state) =>\n `${name} is using the global Vuex '${state}' state, ` +\n `please replace that with a local one using: mapState(['${state}'])`;\n\n Vue.mixin({\n data() {\n // These are only needed for the root Vue\n if (this.$root === this) {\n return {\n globalLoading: true,\n pageComponent: false\n };\n }\n return {};\n },\n mounted() {\n if (this.$root === this && !window.location.pathname.includes('/login')) {\n const { username } = window;\n Promise.all([\n /* This is used by the `app-header` component\n to only show the logout button if a username is set */\n store.dispatch('login', { username }),\n store.dispatch('getConfig'),\n store.dispatch('getStats')\n ]).then(([_, config]) => {\n this.$emit('loaded');\n // Legacy - send config.main to jQuery (received by index.js)\n const event = new CustomEvent('medusa-config-loaded', { detail: config.main });\n window.dispatchEvent(event);\n }).catch(error => {\n console.debug(error);\n alert('Unable to connect to Medusa!'); // eslint-disable-line no-alert\n });\n }\n\n this.$once('loaded', () => {\n this.$root.globalLoading = false;\n });\n },\n // Make auth and config accessible to all components\n // @TODO: Remove this completely\n computed: {\n // Deprecate the global `Vuex.mapState(['auth', 'config'])`\n auth() {\n if (isDevelopment && !this.__VUE_DEVTOOLS_UID__) {\n console.warn(warningTemplate(this._name, 'auth'));\n }\n return this.$store.state.auth;\n },\n config() {\n if (isDevelopment && !this.__VUE_DEVTOOLS_UID__) {\n console.warn(warningTemplate(this._name, 'config'));\n }\n return this.$store.state.config;\n }\n }\n });\n\n if (isDevelopment) {\n console.debug('Loading local Vue');\n }\n\n registerPlugins();\n\n registerGlobalComponents();\n};\n","// style-loader: Adds some css to the DOM by adding a \n","// style-loader: Adds some css to the DOM by adding a \n","\n\n\n\n","// style-loader: Adds some css to the DOM by adding a \n","// style-loader: Adds some css to the DOM by adding a \n","// style-loader: Adds some css to the DOM by adding a \n","\n\n\n\n","\n\n\n\n","\n\n\n\n\n","\n\n\n\n\n","\n \n \n
= 1\" id=\"show-specials-and-seasons\" class=\"pull-right\">\n \n Display Specials: {{ displaySpecials ? 'Show' : 'Hide' }}\n \n\n
\n \n \n \n \n
\n
\n \n \n\n
\n
\n {{ queueItem.message }}\n
\n
\n\n
\n
\n
\n
\n
\n \n
\n
\n
\n\n
\n\n
\n
\n
\n \n
\n
\n ${show.rating.imdb.votes} Votes`\"\n >\n \n \n \n \n \n \"[trakt]\"\n \n \n \n \n\n 0\" :href=\"'http://thexem.de/search?q=' + show.title\" :title=\"'http://thexem.de/search?q=' + show.title\">\n \"[xem]\"\n \n\n \n \"[fanart.tv]\"\n \n
\n
\n
    \n
  • {{ genre }}
  • \n
\n
    \n
  • {{ genre }}
  • \n
\n
\n
\n\n
\n \n
\n
\n \n \n \n \n\n \n \n \n \n \n\n \n \n\n \n \n \n \n \n \n\n 0\">\n \n \n \n\n 0\">\n \n \n \n 0\">\n \n \n \n\n 0\">\n \n \n \n 0\">\n \n \n \n\n 0\">\n \n \n \n\n 0\">\n \n \n \n\n \n \n \n \n -1\">\n \n \n \n
\n \n
Quality:
Originally Airs: {{ show.airs }} (invalid time format) on {{ show.network }}
Originally Airs: {{ show.network }}
Originally Airs: {{ show.airs }} (invalid time format)
Show Status: {{ show.status }}
Default EP Status: {{ show.config.defaultEpisodeStatus }}
Location: {{show.config.location}}{{show.config.locationValid ? '' : ' (Missing)'}}
Scene Name:{{show.config.aliases.join(', ')}}
\n Required Words: \n \n \n {{show.config.release.requiredWords.join(', ')}}\n \n 0\" class=\"break-word global-filter\">\n \n \n {{search.filters.required.join(', ')}}\n \n \n
\n Ignored Words: \n \n \n {{show.config.release.ignoredWords.join(', ')}}\n \n 0\" class=\"break-word global-filter\">\n \n \n {{search.filters.ignored.join(', ')}}\n \n \n
\n Preferred Words: \n \n \n {{search.filters.preferred.join(', ')}}\n \n
\n Undesired Words: \n \n \n {{search.filters.undesired.join(', ')}}\n \n
Wanted Groups:{{show.config.release.whitelist.join(', ')}}
Unwanted Groups:{{show.config.release.blacklist.join(', ')}}
Daily search offset:{{show.config.airdateOffset}} hours
Size:{{humanFileSize(show.size)}}
\n
\n\n \n
\n \n \n \n \n \n \n \n \n \n \n
Info Language:
Subtitles:
Season Folders:
Paused:
Air-by-Date:
Sports:
Anime:
DVD Order:
Scene Numbering:
\n
\n
\n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n \n
\n
\n\n \n\n \n \n \n \n \n
\n
\n
\n
\n
\n
\n
\n\n\n\n\n\n","// style-loader: Adds some css to the DOM by adding a \n","// style-loader: Adds some css to the DOM by adding a \n","\n\n\n","// style-loader: Adds some css to the DOM by adding a \n","// style-loader: Adds some css to the DOM by adding a \n","\n\n\n","// style-loader: Adds some css to the DOM by adding a \n","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./subtitle-search.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./subtitle-search.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./subtitle-search.vue?vue&type=template&id=0c54ccdc&scoped=true&\"\nimport script from \"./subtitle-search.vue?vue&type=script&lang=js&\"\nexport * from \"./subtitle-search.vue?vue&type=script&lang=js&\"\nimport style0 from \"./subtitle-search.vue?vue&type=style&index=0&id=0c54ccdc&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"0c54ccdc\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('span',{class:_vm.override.class || ['quality', _vm.pill.key],attrs:{\"title\":_vm.title}},[_vm._v(_vm._s(_vm.override.text || _vm.pill.name))])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./quality-pill.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./quality-pill.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./quality-pill.vue?vue&type=template&id=9f56cf6c&scoped=true&\"\nimport script from \"./quality-pill.vue?vue&type=script&lang=js&\"\nexport * from \"./quality-pill.vue?vue&type=script&lang=js&\"\nimport style0 from \"./quality-pill.vue?vue&type=style&index=0&id=9f56cf6c&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"9f56cf6c\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"addShowPortal\"}},[_c('app-link',{staticClass:\"btn-medusa btn-large\",attrs:{\"href\":\"addShows/trendingShows/?traktList=anticipated\",\"id\":\"btnNewShow\"}},[_c('div',{staticClass:\"button\"},[_c('div',{staticClass:\"add-list-icon-addtrakt\"})]),_vm._v(\" \"),_c('div',{staticClass:\"buttontext\"},[_c('h3',[_vm._v(\"Add From Trakt Lists\")]),_vm._v(\" \"),_c('p',[_vm._v(\"For shows that you haven't downloaded yet, this option lets you choose from a show from one of the Trakt lists to add to Medusa .\")])])]),_vm._v(\" \"),_c('app-link',{staticClass:\"btn-medusa btn-large\",attrs:{\"href\":\"addShows/popularShows/\",\"id\":\"btnNewShow\"}},[_c('div',{staticClass:\"button\"},[_c('div',{staticClass:\"add-list-icon-addimdb\"})]),_vm._v(\" \"),_c('div',{staticClass:\"buttontext\"},[_c('h3',[_vm._v(\"Add From IMDB's Popular Shows\")]),_vm._v(\" \"),_c('p',[_vm._v(\"View IMDB's list of the most popular shows. This feature uses IMDB's MOVIEMeter algorithm to identify popular TV Shows.\")])])]),_vm._v(\" \"),_c('app-link',{staticClass:\"btn-medusa btn-large\",attrs:{\"href\":\"addShows/popularAnime/\",\"id\":\"btnNewShow\"}},[_c('div',{staticClass:\"button\"},[_c('div',{staticClass:\"add-list-icon-addanime\"})]),_vm._v(\" \"),_c('div',{staticClass:\"buttontext\"},[_c('h3',[_vm._v(\"Add From Anidb's Hot Anime list\")]),_vm._v(\" \"),_c('p',[_vm._v(\"View Anidb's list of the most popular anime shows. Anidb provides lists for Popular Anime, using the \\\"Hot Anime\\\" list.\")])])])],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./add-recommended.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./add-recommended.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./add-recommended.vue?vue&type=template&id=56f7e8ee&\"\nimport script from \"./add-recommended.vue?vue&type=script&lang=js&\"\nexport * from \"./add-recommended.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _vm._m(0)}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"login\"},[_c('form',{attrs:{\"action\":\"\",\"method\":\"post\"}},[_c('h1',[_vm._v(\"Medusa\")]),_vm._v(\" \"),_c('div',{staticClass:\"ctrlHolder\"},[_c('input',{staticClass:\"inlay\",attrs:{\"name\":\"username\",\"type\":\"text\",\"placeholder\":\"Username\",\"autocomplete\":\"off\"}})]),_vm._v(\" \"),_c('div',{staticClass:\"ctrlHolder\"},[_c('input',{staticClass:\"inlay\",attrs:{\"name\":\"password\",\"type\":\"password\",\"placeholder\":\"Password\",\"autocomplete\":\"off\"}})]),_vm._v(\" \"),_c('div',{staticClass:\"ctrlHolder\"},[_c('label',{staticClass:\"remember_me\",attrs:{\"title\":\"for 30 days\"}},[_c('input',{staticClass:\"inlay\",attrs:{\"id\":\"remember_me\",\"name\":\"remember_me\",\"type\":\"checkbox\",\"value\":\"1\",\"checked\":\"checked\"}}),_vm._v(\" Remember me\")]),_vm._v(\" \"),_c('input',{staticClass:\"button\",attrs:{\"name\":\"submit\",\"type\":\"submit\",\"value\":\"Login\"}})])])])}]\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./login.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./login.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","import { render, staticRenderFns } from \"./login.vue?vue&type=template&id=75c0637c&\"\nimport script from \"./login.vue?vue&type=script&lang=js&\"\nexport * from \"./login.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('div',{staticClass:\"col-md-12 pull-right\"},[_c('div',{staticClass:\"logging-filter-control pull-right\"},[_c('div',{staticClass:\"show-option\"},[_c('button',{staticClass:\"btn-medusa btn-inline\",attrs:{\"type\":\"button\"},on:{\"click\":function($event){_vm.autoUpdate = !_vm.autoUpdate}}},[_c('i',{class:(\"glyphicon glyphicon-\" + (_vm.autoUpdate ? 'pause' : 'play'))}),_vm._v(\"\\n \"+_vm._s(_vm.autoUpdate ? 'Pause' : 'Resume')+\"\\n \")])]),_vm._v(\" \"),_c('div',{staticClass:\"show-option\"},[_c('span',[_vm._v(\"Logging level:\\n \"),_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.minLevel),expression:\"minLevel\"}],staticClass:\"form-control form-control-inline input-sm\",on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.minLevel=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},function($event){return _vm.fetchLogsDebounced()}]}},_vm._l((_vm.levels),function(level){return _c('option',{key:level,domProps:{\"value\":level.toUpperCase()}},[_vm._v(_vm._s(level))])}),0)])]),_vm._v(\" \"),_c('div',{staticClass:\"show-option\"},[_c('span',[_vm._v(\"Filter log by:\\n \"),_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.threadFilter),expression:\"threadFilter\"}],staticClass:\"form-control form-control-inline input-sm\",on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.threadFilter=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},function($event){return _vm.fetchLogsDebounced()}]}},[_vm._m(0),_vm._v(\" \"),_vm._l((_vm.filters),function(filter){return _c('option',{key:filter.value,domProps:{\"value\":filter.value}},[_vm._v(_vm._s(filter.title))])})],2)])]),_vm._v(\" \"),_c('div',{staticClass:\"show-option\"},[_c('span',[_vm._v(\"Period:\\n \"),_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.periodFilter),expression:\"periodFilter\"}],staticClass:\"form-control form-control-inline input-sm\",on:{\"change\":[function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = \"_value\" in o ? o._value : o.value;return val}); _vm.periodFilter=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},function($event){return _vm.fetchLogsDebounced()}]}},[_c('option',{attrs:{\"value\":\"all\"}},[_vm._v(\"All\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"one_day\"}},[_vm._v(\"Last 24h\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"three_days\"}},[_vm._v(\"Last 3 days\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"one_week\"}},[_vm._v(\"Last 7 days\")])])])]),_vm._v(\" \"),_c('div',{staticClass:\"show-option\"},[_c('span',[_vm._v(\"Search log by:\\n \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.searchQuery),expression:\"searchQuery\"}],staticClass:\"form-control form-control-inline input-sm\",attrs:{\"type\":\"text\",\"placeholder\":\"clear to reset\"},domProps:{\"value\":(_vm.searchQuery)},on:{\"keyup\":function($event){return _vm.fetchLogsDebounced()},\"keypress\":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,\"enter\",13,$event.key,\"Enter\")){ return null; }return _vm.fetchLogsDebounced.flush()},\"input\":function($event){if($event.target.composing){ return; }_vm.searchQuery=$event.target.value}}})])])])]),_vm._v(\" \"),_c('pre',{staticClass:\"col-md-12\",class:{ fanartOpacity: _vm.config.fanartBackground }},[_c('div',{staticClass:\"notepad\"},[_c('app-link',{attrs:{\"href\":_vm.rawViewLink}},[_c('img',{attrs:{\"src\":\"images/notepad.png\"}})])],1),_vm._l((_vm.logLines),function(line,index){return _c('div',{key:(\"line-\" + index)},[_vm._v(_vm._s(_vm._f(\"formatLine\")(line)))])})],2),_vm._v(\" \"),_c('backstretch',{attrs:{\"slug\":_vm.config.randomShowSlug}})],1)}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('option',{attrs:{\"value\":\"\"}},[_vm._v(\"\")])}]\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./logs.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./logs.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./logs.vue?vue&type=template&id=2eac3843&scoped=true&\"\nimport script from \"./logs.vue?vue&type=script&lang=js&\"\nexport * from \"./logs.vue?vue&type=script&lang=js&\"\nimport style0 from \"./logs.vue?vue&type=style&index=0&id=2eac3843&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"2eac3843\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"addShowPortal\"}},[_c('app-link',{staticClass:\"btn-medusa btn-large\",attrs:{\"href\":\"addShows/newShow/\",\"id\":\"btnNewShow\"}},[_c('div',{staticClass:\"button\"},[_c('div',{staticClass:\"add-list-icon-addnewshow\"})]),_vm._v(\" \"),_c('div',{staticClass:\"buttontext\"},[_c('h3',[_vm._v(\"Add New Show\")]),_vm._v(\" \"),_c('p',[_vm._v(\"For shows that you haven't downloaded yet, this option finds a show on your preferred indexer, creates a directory for it's episodes, and adds it to Medusa.\")])])]),_vm._v(\" \"),_c('app-link',{staticClass:\"btn-medusa btn-large\",attrs:{\"href\":\"addShows/existingShows/\",\"id\":\"btnExistingShow\"}},[_c('div',{staticClass:\"button\"},[_c('div',{staticClass:\"add-list-icon-addexistingshow\"})]),_vm._v(\" \"),_c('div',{staticClass:\"buttontext\"},[_c('h3',[_vm._v(\"Add Existing Shows\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Use this option to add shows that already have a folder created on your hard drive. Medusa will scan your existing metadata/episodes and add the show accordingly.\")])])])],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./add-shows.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./add-shows.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./add-shows.vue?vue&type=template&id=2fd1eaaf&\"\nimport script from \"./add-shows.vue?vue&type=script&lang=js&\"\nexport * from \"./add-shows.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"config-content\"}},[_c('table',{staticClass:\"infoTable\",attrs:{\"cellspacing\":\"1\",\"border\":\"0\",\"cellpadding\":\"0\",\"width\":\"100%\"}},[_c('tr',[_vm._m(0),_vm._v(\" \"),_c('td',[_vm._v(\"\\n Branch:\\n \"),(_vm.config.branch)?_c('span',[_c('app-link',{attrs:{\"href\":((_vm.config.sourceUrl) + \"/tree/\" + (_vm.config.branch))}},[_vm._v(_vm._s(_vm.config.branch))])],1):_c('span',[_vm._v(\"Unknown\")]),_vm._v(\" \"),_c('br'),_vm._v(\"\\n Commit:\\n \"),(_vm.config.commitHash)?_c('span',[_c('app-link',{attrs:{\"href\":((_vm.config.sourceUrl) + \"/commit/\" + (_vm.config.commitHash))}},[_vm._v(_vm._s(_vm.config.commitHash))])],1):_c('span',[_vm._v(\"Unknown\")]),_vm._v(\" \"),_c('br'),_vm._v(\"\\n Version:\\n \"),(_vm.config.release)?_c('span',[_c('app-link',{attrs:{\"href\":((_vm.config.sourceUrl) + \"/releases/tag/v\" + (_vm.config.release))}},[_vm._v(_vm._s(_vm.config.release))])],1):_c('span',[_vm._v(\"Unknown\")]),_vm._v(\" \"),_c('br'),_vm._v(\"\\n Database:\\n \"),(_vm.config.databaseVersion)?_c('span',[_vm._v(_vm._s(_vm.config.databaseVersion.major)+\".\"+_vm._s(_vm.config.databaseVersion.minor))]):_c('span',[_vm._v(\"Unknown\")])])]),_vm._v(\" \"),_c('tr',[_vm._m(1),_c('td',[_vm._v(_vm._s(_vm.config.pythonVersion))])]),_vm._v(\" \"),_c('tr',[_vm._m(2),_c('td',[_vm._v(_vm._s(_vm.config.sslVersion))])]),_vm._v(\" \"),_c('tr',[_vm._m(3),_c('td',[_vm._v(_vm._s(_vm.config.os))])]),_vm._v(\" \"),_c('tr',[_vm._m(4),_c('td',[_vm._v(_vm._s(_vm.config.locale))])]),_vm._v(\" \"),_vm._m(5),_vm._v(\" \"),_vm._m(6),_vm._v(\" \"),_c('tr',[_vm._m(7),_c('td',[_vm._v(_vm._s(_vm.config.localUser))])]),_vm._v(\" \"),_c('tr',[_vm._m(8),_c('td',[_vm._v(_vm._s(_vm.config.programDir))])]),_vm._v(\" \"),_c('tr',[_vm._m(9),_c('td',[_vm._v(_vm._s(_vm.config.configFile))])]),_vm._v(\" \"),_c('tr',[_vm._m(10),_c('td',[_vm._v(_vm._s(_vm.config.dbPath))])]),_vm._v(\" \"),_c('tr',[_vm._m(11),_c('td',[_vm._v(_vm._s(_vm.config.cacheDir))])]),_vm._v(\" \"),_c('tr',[_vm._m(12),_c('td',[_vm._v(_vm._s(_vm.config.logDir))])]),_vm._v(\" \"),(_vm.config.appArgs)?_c('tr',[_vm._m(13),_c('td',[_c('pre',[_vm._v(_vm._s(_vm.config.appArgs.join(' ')))])])]):_vm._e(),_vm._v(\" \"),(_vm.config.webRoot)?_c('tr',[_vm._m(14),_c('td',[_vm._v(_vm._s(_vm.config.webRoot))])]):_vm._e(),_vm._v(\" \"),(_vm.config.runsInDocker)?_c('tr',[_vm._m(15),_c('td',[_vm._v(\"Yes\")])]):_vm._e(),_vm._v(\" \"),_vm._m(16),_vm._v(\" \"),_vm._m(17),_vm._v(\" \"),_c('tr',[_vm._m(18),_c('td',[_c('app-link',{attrs:{\"href\":_vm.config.githubUrl}},[_vm._v(_vm._s(_vm.config.githubUrl))])],1)]),_vm._v(\" \"),_c('tr',[_vm._m(19),_c('td',[_c('app-link',{attrs:{\"href\":_vm.config.wikiUrl}},[_vm._v(_vm._s(_vm.config.wikiUrl))])],1)]),_vm._v(\" \"),_c('tr',[_vm._m(20),_c('td',[_c('app-link',{attrs:{\"href\":_vm.config.sourceUrl}},[_vm._v(_vm._s(_vm.config.sourceUrl))])],1)]),_vm._v(\" \"),_c('tr',[_vm._m(21),_c('td',[_c('app-link',{attrs:{\"href\":\"irc://irc.freenode.net/#pymedusa\"}},[_c('i',[_vm._v(\"#pymedusa\")]),_vm._v(\" on \"),_c('i',[_vm._v(\"irc.freenode.net\")])])],1)])])])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-application\"}),_vm._v(\" Medusa Info:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-python\"}),_vm._v(\" Python Version:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-ssl\"}),_vm._v(\" SSL Version:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-os\"}),_vm._v(\" OS:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-locale\"}),_vm._v(\" Locale:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_c('td',[_vm._v(\" \")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"infoTableSeperator\"},[_c('td',[_vm._v(\" \")]),_c('td',[_vm._v(\" \")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-user\"}),_vm._v(\" User:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-dir\"}),_vm._v(\" Program Folder:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-config\"}),_vm._v(\" Config File:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-db\"}),_vm._v(\" Database File:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-cache\"}),_vm._v(\" Cache Folder:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-log\"}),_vm._v(\" Log Folder:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-arguments\"}),_vm._v(\" Arguments:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-dir\"}),_vm._v(\" Web Root:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-docker\"}),_vm._v(\" Runs in Docker:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',[_c('td',[_vm._v(\" \")]),_c('td',[_vm._v(\" \")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{staticClass:\"infoTableSeperator\"},[_c('td',[_vm._v(\" \")]),_c('td',[_vm._v(\" \")])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-web\"}),_vm._v(\" Website:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-wiki\"}),_vm._v(\" Wiki:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-github\"}),_vm._v(\" Source:\")])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('td',[_c('i',{staticClass:\"icon16-config-mirc\"}),_vm._v(\" IRC Chat:\")])}]\n\nexport { render, staticRenderFns }","import mod from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./config.vue?vue&type=template&id=c1a78232&scoped=true&\"\nimport script from \"./config.vue?vue&type=script&lang=js&\"\nexport * from \"./config.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config.vue?vue&type=style&index=0&id=c1a78232&scoped=true&lang=css&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"c1a78232\",\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"align-center\"},[_vm._v(\"You have reached this page by accident, please check the url.\")])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./404.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./404.vue?vue&type=script&lang=js&\"","\n\n\n","import { render, staticRenderFns } from \"./404.vue?vue&type=template&id=3cfbf450&\"\nimport script from \"./404.vue?vue&type=script&lang=js&\"\nexport * from \"./404.vue?vue&type=script&lang=js&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('iframe',{staticClass:\"irc-frame loading-spinner\",attrs:{\"src\":_vm.frameSrc}})}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","