diff --git a/.dockerignore b/.dockerignore index aeebce8a68..fc967549c7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,7 +16,6 @@ cache/ Dockerfile dredd/ hooks/ -runscripts/ tests/ themes-default/ venv/ diff --git a/.github/workflows/docker-image-develop.yml b/.github/workflows/docker-image-develop.yml deleted file mode 100644 index defeba4c7b..0000000000 --- a/.github/workflows/docker-image-develop.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Docker Image CI Develop - -on: - push: - branches: [ develop ] - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: linux/amd64,linux/arm/v7,linux/arm64 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - with: - install: true - - name: docker login - env: - DOCKER_USER: ${{secrets.DOCKER_USER}} - DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} - run: | - docker login -u $DOCKER_USER -p $DOCKER_PASSWORD - - name: Build the Docker image - run: docker buildx build . --push --platform linux/amd64,linux/arm/v7,linux/arm64 --file Dockerfile --tag pymedusa/medusa:develop diff --git a/.github/workflows/docker-image-master.yml b/.github/workflows/docker-image-master.yml deleted file mode 100644 index 8d927e9645..0000000000 --- a/.github/workflows/docker-image-master.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Docker Image CI Master - -on: - push: - branches: [ master ] - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: linux/amd64,linux/arm/v7,linux/arm64 - # https://github.com/docker/setup-buildx-action - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - with: - install: true - - name: docker login - env: - DOCKER_USER: ${{secrets.DOCKER_USER}} - DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} - run: | - docker login -u $DOCKER_USER -p $DOCKER_PASSWORD - - name: Build the Docker image - run: docker buildx build . --push --platform linux/amd64,linux/arm/v7,linux/arm64 --file Dockerfile --tag pymedusa/medusa:master --tag pymedusa/medusa:latest diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000000..16ed6ae5cd --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,57 @@ +name: Docker Image CI + +on: + push: + branches: [ master, develop ] + +env: + PLATFORMS: linux/amd64,linux/arm/v7,linux/arm64 + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + # https://github.com/docker/setup-qemu-action + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: ${{ env.PLATFORMS }} + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ github.repository_owner }}/medusa + # tag with master or develop + tags: | + type=ref,event=branch + # also tag master as latest + flavor: latest=${{ github.ref_name == 'master' }} + - name: Log into Dockerhub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Build Docker image + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile + platforms: ${{ env.PLATFORMS }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true + pull: true + build-args: | + GIT_COMMIT=${{ github.sha }} + GIT_BRANCH=${{ github.ref_name }} + BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9c654955..9076bf02b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 1.0.0 (31-05-2022) + +#### New Features +- Add option to mass-update the info language ([10516](https://github.com/pymedusa/Medusa/pull/10516)) + +#### Improvements +- Multiple UI fixes / enhancements ([10566](https://github.com/pymedusa/Medusa/pull/10566)) +- Add config setting to allow overriding xem url ([10541](https://github.com/pymedusa/Medusa/pull/10541)) +- Increase addic7ed http request timeout ([10565](https://github.com/pymedusa/Medusa/pull/10565)) +- Improve anime title parsing for `Title Season 2 - 01` ([10534](https://github.com/pymedusa/Medusa/pull/10534)) +- Improve detection of commit / branch when run in docker ([10531](https://github.com/pymedusa/Medusa/pull/10531)) +- Improve guessit parsing for shows with numbers in them like `9-1-1` ([10493](https://github.com/pymedusa/Medusa/pull/10493)) +- Bump Knowit + pymediainfo to version 0.4.0 and 5.1.0 ([10564](https://github.com/pymedusa/Medusa/pull/10564)) + +#### Fixes +- Fix malformed imdb id's when imdb id not available ([10669](https://github.com/pymedusa/Medusa/pull/10669)) +- Fix shows being searched 2 days early for tvmaze shows ([10668](https://github.com/pymedusa/Medusa/pull/10668)) +- Disable guessit cache for postprocessing ([10532](https://github.com/pymedusa/Medusa/pull/10532)) +- Fix .plexmatch file misread as xml causing warnings ([10510](https://github.com/pymedusa/Medusa/pull/10510)) + ## 0.5.29 (11-04-2022) #### New Features diff --git a/Dockerfile b/Dockerfile index 0b3c549886..0d749d0da9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,4 +32,4 @@ EXPOSE 8081 VOLUME /config /downloads /tv /anime WORKDIR /app/medusa -CMD [ "python", "start.py", "--nolaunch", "--datadir", "/config" ] +CMD [ "runscripts/init.docker" ] diff --git a/ext/knowit/__init__.py b/ext/knowit/__init__.py index b753f1ded5..eda7067795 100644 --- a/ext/knowit/__init__.py +++ b/ext/knowit/__init__.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- """Know your media files better.""" -from __future__ import unicode_literals - __title__ = 'knowit' -__version__ = '0.3.0-dev' +__version__ = '0.4.0' __short_version__ = '.'.join(__version__.split('.')[:2]) __author__ = 'Rato AQ2' __license__ = 'MIT' -__copyright__ = 'Copyright 2016-2017, Rato AQ2' +__copyright__ = 'Copyright 2016-2021, Rato AQ2' __url__ = 'https://github.com/ratoaq2/knowit' #: Video extensions @@ -19,9 +16,4 @@ '.omf', '.ps', '.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo', '.vob', '.vro', '.webm', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid') -try: - from collections import OrderedDict -except ImportError: # pragma: no cover - from ordereddict import OrderedDict - -from .api import KnowitException, know +from knowit.api import KnowitException, know diff --git a/ext/knowit/__main__.py b/ext/knowit/__main__.py index 3b55af8729..c301484211 100644 --- a/ext/knowit/__main__.py +++ b/ext/knowit/__main__.py @@ -1,25 +1,24 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - +import argparse import json import logging +import os import sys +import typing from argparse import ArgumentParser -from six import PY2 import yaml -from . import ( +from knowit import ( __url__, __version__, api, ) -from .provider import ProviderError -from .serializer import ( +from knowit.provider import ProviderError +from knowit.serializer import ( get_json_encoder, get_yaml_dumper, ) -from .utils import recurse_paths +from knowit.utils import recurse_paths logging.basicConfig(stream=sys.stdout, format='%(message)s') logging.getLogger('CONSOLE').setLevel(logging.INFO) @@ -29,45 +28,96 @@ logger = logging.getLogger('knowit') -def build_argument_parser(): - """Build the argument parser. - - :return: the argument parser - :rtype: ArgumentParser - """ +def build_argument_parser() -> ArgumentParser: + """Build the argument parser.""" opts = ArgumentParser() - opts.add_argument(dest='videopath', help='Path to the video to introspect', nargs='*') + opts.add_argument( + dest='videopath', + help='Path to the video to introspect', + nargs='*', + type=str, + ) provider_opts = opts.add_argument_group('Providers') - provider_opts.add_argument('-p', '--provider', dest='provider', - help='The provider to be used: mediainfo, ffmpeg or enzyme.') + provider_opts.add_argument( + '-p', + '--provider', + dest='provider', + help='The provider to be used: mediainfo, ffmpeg, mkvmerge or enzyme.', + type=str, + ) output_opts = opts.add_argument_group('Output') - output_opts.add_argument('--debug', action='store_true', dest='debug', - help='Print useful information for debugging knowit and for reporting bugs.') - output_opts.add_argument('--report', action='store_true', dest='report', - help='Parse media and report all non-detected values') - output_opts.add_argument('-y', '--yaml', action='store_true', dest='yaml', - help='Display output in yaml format') - output_opts.add_argument('-N', '--no-units', action='store_true', dest='no_units', - help='Display output without units') - output_opts.add_argument('-P', '--profile', dest='profile', - help='Display values according to specified profile: code, default, human, technical') + output_opts.add_argument( + '--debug', + action='store_true', + dest='debug', + help='Print information for debugging knowit and for reporting bugs.' + ) + output_opts.add_argument( + '--report', + action='store_true', + dest='report', + help='Parse media and report all non-detected values' + ) + output_opts.add_argument( + '-y', + '--yaml', + action='store_true', + dest='yaml', + help='Display output in yaml format' + ) + output_opts.add_argument( + '-N', + '--no-units', + action='store_true', + dest='no_units', + help='Display output without units' + ) + output_opts.add_argument( + '-P', + '--profile', + dest='profile', + help='Display values according to specified profile: code, default, human, technical', + type=str, + ) conf_opts = opts.add_argument_group('Configuration') - conf_opts.add_argument('--mediainfo', dest='mediainfo', - help='The location to search for MediaInfo binaries') - conf_opts.add_argument('--ffmpeg', dest='ffmpeg', - help='The location to search for FFmpeg (ffprobe) binaries') + conf_opts.add_argument( + '--mediainfo', + dest='mediainfo', + help='The location to search for MediaInfo binaries', + type=str, + ) + conf_opts.add_argument( + '--ffmpeg', + dest='ffmpeg', + help='The location to search for ffprobe (FFmpeg) binaries', + type=str, + ) + conf_opts.add_argument( + '--mkvmerge', + dest='mkvmerge', + help='The location to search for mkvmerge (MKVToolNix) binaries', + type=str, + ) information_opts = opts.add_argument_group('Information') - information_opts.add_argument('--version', dest='version', action='store_true', - help='Display knowit version.') + information_opts.add_argument( + '--version', + dest='version', + action='store_true', + help='Display knowit version.' + ) return opts -def knowit(video_path, options, context): +def knowit( + video_path: typing.Union[str, os.PathLike], + options: argparse.Namespace, + context: typing.MutableMapping, +) -> typing.Mapping: """Extract video metadata.""" context['path'] = video_path if not options.report: @@ -77,27 +127,49 @@ def knowit(video_path, options, context): info = api.know(video_path, context) if not options.report: console.info('Knowit %s found: ', __version__) - console.info(dump(info, options, context)) - + console.info(dumps(info, options, context)) return info -def dump(info, options, context): +def _as_yaml( + info: typing.Mapping[str, typing.Any], + context: typing.Mapping, +) -> str: + """Convert info to string using YAML format.""" + data = {info['path']: info} if 'path' in info else info + return yaml.dump( + data, + Dumper=get_yaml_dumper(context), + default_flow_style=False, + allow_unicode=True, + sort_keys=False, + ) + + +def _as_json( + info: typing.Mapping[str, typing.Any], + context: typing.Mapping, +) -> str: + """Convert info to string using JSON format.""" + return json.dumps( + info, + cls=get_json_encoder(context), + indent=4, + ensure_ascii=False, + ) + + +def dumps( + info: typing.Mapping[str, typing.Any], + options: argparse.Namespace, + context: typing.Mapping, +) -> str: """Convert info to string using json or yaml format.""" - if options.yaml: - data = {info['path']: info} if 'path' in info else info - result = yaml.dump(data, Dumper=get_yaml_dumper(context), - default_flow_style=False, allow_unicode=True) - if PY2: - result = result.decode('utf-8') - - else: - result = json.dumps(info, cls=get_json_encoder(context), indent=4, ensure_ascii=False) - - return result + convert = _as_yaml if options.yaml else _as_json + return convert(info, context) -def main(args=None): +def main(args: typing.List[str] = None) -> None: """Execute main function for entry point.""" argument_parser = build_argument_parser() args = args or sys.argv[1:] @@ -111,40 +183,42 @@ def main(args=None): paths = recurse_paths(options.videopath) - if paths: - report = {} - for i, videopath in enumerate(paths): - try: - context = dict(vars(options)) - if options.report: - context['report'] = report - else: - del context['report'] - knowit(videopath, options, context) - except ProviderError: - logger.exception('Error when processing video') - except OSError: - logger.exception('OS error when processing video') - except UnicodeError: - logger.exception('Character encoding error when processing video') - except api.KnowitException as e: - logger.error(e) - if options.report and i % 20 == 19 and report: - console.info('Unknown values so far:') - console.info(dump(report, options, vars(options))) - - if options.report: - if report: - console.info('Knowit %s found unknown values:', __version__) - console.info(dump(report, options, vars(options))) - console.info('Please report them at %s', __url__) + if not paths: + if options.version: + console.info(api.debug_info()) + else: + argument_parser.print_help() + return + + report: typing.MutableMapping[str, str] = {} + for i, video_path in enumerate(paths): + try: + context = {k: v for k, v in vars(options).items() if v is not None} + if options.report: + context['report'] = report else: - console.info('Knowit %s knows everything. :-)', __version__) - - elif options.version: - console.info(api.debug_info()) - else: - argument_parser.print_help() + del context['report'] + knowit(video_path, options, context) + except ProviderError: + logger.exception('Error when processing video') + except OSError: + logger.exception('OS error when processing video') + except UnicodeError: + logger.exception('Character encoding error when processing video') + except api.KnowitException as e: + logger.error(e) + + if options.report and i % 20 == 19 and report: + console.info('Unknown values so far:') + console.info(dumps(report, options, vars(options))) + + if options.report: + if report: + console.info('Knowit %s found unknown values:', __version__) + console.info(dumps(report, options, vars(options))) + console.info('Please report them at %s', __url__) + else: + console.info('Knowit %s knows everything. :-)', __version__) if __name__ == '__main__': diff --git a/ext/knowit/api.py b/ext/knowit/api.py index 769ec1601f..4df7806054 100644 --- a/ext/knowit/api.py +++ b/ext/knowit/api.py @@ -1,55 +1,50 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - +import os import traceback +import typing -from . import OrderedDict, __version__ -from .config import Config +from knowit import __version__ +from knowit.config import Config +from knowit.provider import Provider from .providers import ( EnzymeProvider, FFmpegProvider, MediaInfoProvider, + MkvMergeProvider, ) -_provider_map = OrderedDict([ - ('mediainfo', MediaInfoProvider), - ('ffmpeg', FFmpegProvider), - ('enzyme', EnzymeProvider) -]) +_provider_map = { + 'mediainfo': MediaInfoProvider, + 'ffmpeg': FFmpegProvider, + 'mkvmerge': MkvMergeProvider, + 'enzyme': EnzymeProvider, +} provider_names = _provider_map.keys() -available_providers = OrderedDict([]) +available_providers: typing.Dict[str, Provider] = {} class KnowitException(Exception): - """Exception raised when knowit fails to perform media info extraction because of an internal error.""" + """Exception raised when knowit encounters an internal error.""" -def initialize(context=None): +def initialize(context: typing.Optional[typing.Mapping] = None) -> None: """Initialize knowit.""" if not available_providers: context = context or {} config = Config.build(context.get('config')) for name, provider_cls in _provider_map.items(): - available_providers[name] = provider_cls(config, context.get(name) or config.general.get(name)) - + general_config = getattr(config, 'general', {}) + mapping = context.get(name) or general_config.get(name) + available_providers[name] = provider_cls(config, mapping) -def know(video_path, context=None): - """Return a dict containing the video metadata. - :param video_path: - :type video_path: string - :param context: - :type context: dict - :return: - :rtype: dict - """ - try: - # handle path-like objects - video_path = video_path.__fspath__() - except AttributeError: - pass +def know( + video_path: typing.Union[str, os.PathLike], + context: typing.Optional[typing.MutableMapping] = None +) -> typing.Mapping: + """Return a mapping of video metadata.""" + video_path = os.fspath(video_path) try: context = context or {} @@ -70,9 +65,9 @@ def know(video_path, context=None): raise KnowitException(debug_info(context=context, exc_info=True)) -def dependencies(context=None): +def dependencies(context: typing.Mapping = None) -> typing.Mapping: """Return all dependencies detected by knowit.""" - deps = OrderedDict([]) + deps = {} try: initialize(context) for name, provider_cls in _provider_map.items(): @@ -86,15 +81,18 @@ def dependencies(context=None): return deps -def _centered(value): +def _centered(value: str) -> str: value = value[-52:] - return '| {msg:^53} |'.format(msg=value) + return f'| {value:^53} |' -def debug_info(context=None, exc_info=False): +def debug_info( + context: typing.Optional[typing.MutableMapping] = None, + exc_info: bool = False, +) -> str: lines = [ '+-------------------------------------------------------+', - _centered('KnowIt {0}'.format(__version__)), + _centered(f'KnowIt {__version__}'), '+-------------------------------------------------------+' ] @@ -114,7 +112,7 @@ def debug_info(context=None, exc_info=False): lines.append('+-------------------------------------------------------+') for k, v in context.items(): if v: - lines.append(_centered('{}: {}'.format(k, v))) + lines.append(_centered(f'{k}: {v}')) if debug_data: lines.append('+-------------------------------------------------------+') diff --git a/ext/knowit/config.py b/ext/knowit/config.py index 04e8713e23..8331399233 100644 --- a/ext/knowit/config.py +++ b/ext/knowit/config.py @@ -1,34 +1,38 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from collections import namedtuple +import os +import typing from logging import NullHandler, getLogger from pkg_resources import resource_stream -from six import text_type import yaml -from .serializer import get_yaml_loader +from knowit.serializer import get_yaml_loader logger = getLogger(__name__) logger.addHandler(NullHandler()) -_valid_aliases = ('code', 'default', 'human', 'technical') -_Value = namedtuple('_Value', _valid_aliases) + +class _Value(typing.NamedTuple): + code: str + default: str + human: str + technical: str + + +_valid_aliases = _Value._fields -class Config(object): +class Config: """Application config class.""" @classmethod - def build(cls, path=None): + def build(cls, path: typing.Optional[typing.Union[str, os.PathLike]] = None) -> 'Config': """Build config instance.""" loader = get_yaml_loader() with resource_stream('knowit', 'defaults.yml') as stream: cfgs = [yaml.load(stream, Loader=loader)] if path: - with open(path, 'r') as stream: + with open(path, 'rb') as stream: cfgs.append(yaml.load(stream, Loader=loader)) profiles_data = {} @@ -41,7 +45,7 @@ def build(cls, path=None): if 'knowledge' in cfg: knowledge_data.update(cfg['knowledge']) - data = {'general': {}} + data: typing.Dict[str, typing.MutableMapping] = {'general': {}} for class_name, data_map in knowledge_data.items(): data.setdefault(class_name, {}) for code, detection_values in data_map.items(): @@ -52,7 +56,7 @@ def build(cls, path=None): alias_map.setdefault('technical', alias_map['human']) value = _Value(**{k: v for k, v in alias_map.items() if k in _valid_aliases}) for detection_value in detection_values: - data[class_name][text_type(detection_value)] = value + data[class_name][str(detection_value)] = value config = Config() config.__dict__ = data diff --git a/ext/knowit/core.py b/ext/knowit/core.py index c567d2ccf4..9736d7ba26 100644 --- a/ext/knowit/core.py +++ b/ext/knowit/core.py @@ -1,36 +1,226 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - +import typing from logging import NullHandler, getLogger -from six import text_type - logger = getLogger(__name__) logger.addHandler(NullHandler()) +T = typing.TypeVar('T') + +_visible_chars_table = dict.fromkeys(range(32)) + + +def _is_unknown(value: typing.Any) -> bool: + return isinstance(value, str) and (not value or value.lower() == 'unknown') + -class Reportable(object): +class Reportable(typing.Generic[T]): """Reportable abstract class.""" - def __init__(self, name, description=None, reportable=True): - """Constructor.""" - self.name = name + def __init__( + self, + *args: str, + description: typing.Optional[str] = None, + reportable: bool = True, + ): + """Initialize the object.""" + self.names = args self._description = description self.reportable = reportable @property - def description(self): + def description(self) -> str: """Rule description.""" - return self._description or self.name + return self._description or '|'.join(self.names) - def report(self, value, context): + def report(self, value: typing.Union[str, T], context: typing.MutableMapping) -> None: """Report unknown value.""" if not value or not self.reportable: return - value = text_type(value) if 'report' in context: report_map = context['report'].setdefault(self.description, {}) if value not in report_map: report_map[value] = context['path'] logger.info('Invalid %s: %r', self.description, value) + + +class Property(Reportable[T]): + """Property class.""" + + def __init__( + self, + *args: str, + default: typing.Optional[T] = None, + private: bool = False, + description: typing.Optional[str] = None, + delimiter: str = ' / ', + **kwargs, + ): + """Init method.""" + super().__init__(*args, description=description, **kwargs) + self.default = default + self.private = private + # Used to detect duplicated values. e.g.: en / en or High@L4.0 / High@L4.0 or Progressive / Progressive + self.delimiter = delimiter + + def extract_value( + self, + track: typing.Mapping, + context: typing.MutableMapping, + ) -> typing.Optional[T]: + """Extract the property value from a given track.""" + for name in self.names: + names = name.split('.') + value = track.get(names[0], {}).get(names[1]) if len(names) == 2 else track.get(name) + if value is None: + if self.default is None: + continue + + value = self.default + + if isinstance(value, bytes): + value = value.decode() + + if isinstance(value, str): + value = value.translate(_visible_chars_table).strip() + if _is_unknown(value): + continue + value = self._deduplicate(value) + + result = self.handle(value, context) + if result is not None and not _is_unknown(result): + return result + + return None + + @classmethod + def _deduplicate(cls, value: str) -> str: + values = value.split(' / ') + if len(values) == 2 and values[0] == values[1]: + return values[0] + return value + + def handle(self, value: T, context: typing.MutableMapping) -> typing.Optional[T]: + """Return the value without any modification.""" + return value + + +class Configurable(Property[T]): + """Configurable property where values are in a config mapping.""" + + def __init__(self, config: typing.Mapping[str, typing.Mapping], *args: str, + config_key: typing.Optional[str] = None, **kwargs): + """Init method.""" + super().__init__(*args, **kwargs) + self.mapping = getattr(config, config_key or self.__class__.__name__) if config else {} + + @classmethod + def _extract_key(cls, value: str) -> typing.Union[str, bool]: + return value.upper() + + @classmethod + def _extract_fallback_key(cls, value: str, key: str) -> typing.Optional[T]: + return None + + def _lookup( + self, + key: str, + context: typing.MutableMapping, + ) -> typing.Union[T, None, bool]: + result = self.mapping.get(key) + if result is not None: + result = getattr(result, context.get('profile') or 'default') + return result if result != '__ignored__' else False + return None + + def handle(self, value, context): + """Return Variable or Constant.""" + key = self._extract_key(value) + if key is False: + return + + result = self._lookup(key, context) + if result is False: + return + + while not result and key: + key = self._extract_fallback_key(value, key) + result = self._lookup(key, context) + if result is False: + return + + if not result: + self.report(value, context) + + return result + + +class MultiValue(Property): + """Property with multiple values.""" + + def __init__(self, prop: typing.Optional[Property] = None, delimiter='/', single=False, + handler=None, name=None, **kwargs): + """Init method.""" + super().__init__(*(prop.names if prop else (name,)), **kwargs) + self.prop = prop + self.delimiter = delimiter + self.single = single + self.handler = handler + + def handle( + self, + value: str, + context: typing.MutableMapping, + ) -> typing.Union[T, typing.List[T]]: + """Handle properties with multiple values.""" + if self.handler: + call = self.handler + elif self.prop: + call = self.prop.handle + else: + raise NotImplementedError('No handler available') + + result = call(value, context) + if result is not None: + return result + + if isinstance(value, list): + if len(value) == 1: + values = self._split(value[0], self.delimiter) + else: + values = value + else: + values = self._split(value, self.delimiter) + + if values is None: + return call(values, context) + if len(values) > 1 and not self.single: + results = [call(item, context) if not _is_unknown(item) else None for item in values] + results = [r for r in results if r is not None] + if results: + return results + return call(values[0], context) + + @classmethod + def _split( + cls, + value: typing.Optional[T], + delimiter: str = '/', + ) -> typing.Optional[typing.List[str]]: + if value is None: + return None + + return [x.strip() for x in str(value).split(delimiter)] + + +class Rule(Reportable[T]): + """Rule abstract class.""" + + def __init__(self, name: str, override=False, **kwargs): + """Initialize the object.""" + super().__init__(name, **kwargs) + self.override = override + + def execute(self, props, pv_props, context: typing.Mapping): + """How to execute a rule.""" + raise NotImplementedError diff --git a/ext/knowit/defaults.yml b/ext/knowit/defaults.yml index 234f934268..af6b79c321 100644 --- a/ext/knowit/defaults.yml +++ b/ext/knowit/defaults.yml @@ -200,6 +200,12 @@ knowledge: HIGH: - HIGH + VideoHdrFormat: + DV: + - DOLBY VISION + HDR10: + - SMPTE ST 2086 + ScanType: PROGRESSIVE: - PROGRESSIVE @@ -232,6 +238,7 @@ knowledge: MA: - MA - DTS-HD MA + - XLL MAIN: - MAIN LC: @@ -282,6 +289,7 @@ knowledge: - DTS-HD AAC: - AAC + - AAC-2 FLAC: - FLAC PCM: @@ -503,6 +511,13 @@ profiles: HIGH: default: High + VideoHdrFormat: + DV: + default: Dolby Vision + HDR10: + default: HDR10 + technical: HDR10 - SMPTE ST 2086 + ScanType: PROGRESSIVE: default: Progressive diff --git a/ext/knowit/properties/__init__.py b/ext/knowit/properties/__init__.py index f871bc47f8..da0cfd9e72 100644 --- a/ext/knowit/properties/__init__.py +++ b/ext/knowit/properties/__init__.py @@ -1,27 +1,28 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .audio import ( +from knowit.properties.audio import ( AudioChannels, AudioCodec, AudioCompression, AudioProfile, BitRateMode, ) -from .basic import Basic -from .duration import Duration -from .language import Language -from .quantity import Quantity -from .subtitle import ( +from knowit.properties.general import ( + Basic, + Duration, + Language, + Quantity, + YesNo, +) +from knowit.properties.subtitle import ( SubtitleFormat, ) -from .video import ( +from knowit.properties.video import ( Ratio, ScanType, VideoCodec, + VideoDimensions, VideoEncoder, + VideoHdrFormat, VideoProfile, VideoProfileLevel, VideoProfileTier, ) -from .yesno import YesNo diff --git a/ext/knowit/properties/audio.py b/ext/knowit/properties/audio.py new file mode 100644 index 0000000000..8347420287 --- /dev/null +++ b/ext/knowit/properties/audio.py @@ -0,0 +1,55 @@ +import typing + +from knowit.core import Configurable, Property + + +class BitRateMode(Configurable[str]): + """Bit Rate mode property.""" + + +class AudioCompression(Configurable[str]): + """Audio Compression property.""" + + +class AudioProfile(Configurable[str]): + """Audio profile property.""" + + +class AudioChannels(Property[int]): + """Audio Channels property.""" + + ignored = { + 'object based', # Dolby Atmos + } + + def handle(self, value: typing.Union[int, str], context: typing.MutableMapping) -> typing.Optional[int]: + """Handle audio channels.""" + if isinstance(value, int): + return value + + if value.lower() not in self.ignored: + try: + return int(value) + except ValueError: + self.report(value, context) + return None + + +class AudioCodec(Configurable[str]): + """Audio codec property.""" + + @classmethod + def _extract_key(cls, value) -> str: + key = str(value).upper() + if key.startswith('A_'): + key = key[2:] + + # only the first part of the word. E.g.: 'AAC LC' => 'AAC' + return key.split(' ')[0] + + @classmethod + def _extract_fallback_key(cls, value, key) -> typing.Optional[str]: + if '/' in key: + return key.split('/')[0] + else: + return None diff --git a/ext/knowit/properties/audio/__init__.py b/ext/knowit/properties/audio/__init__.py deleted file mode 100644 index c7a1198f2c..0000000000 --- a/ext/knowit/properties/audio/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .bitratemode import BitRateMode -from .channels import AudioChannels -from .codec import AudioCodec -from .compression import AudioCompression -from .profile import AudioProfile diff --git a/ext/knowit/properties/audio/bitratemode.py b/ext/knowit/properties/audio/bitratemode.py deleted file mode 100644 index 82fb9e68fd..0000000000 --- a/ext/knowit/properties/audio/bitratemode.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...property import Configurable - - -class BitRateMode(Configurable): - """Bit Rate mode property.""" - - pass diff --git a/ext/knowit/properties/audio/channels.py b/ext/knowit/properties/audio/channels.py deleted file mode 100644 index 597a46bc55..0000000000 --- a/ext/knowit/properties/audio/channels.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from six import text_type - -from ...property import Property - - -class AudioChannels(Property): - """Audio Channels property.""" - - ignored = { - 'object based', # Dolby Atmos - } - - def handle(self, value, context): - """Handle audio channels.""" - if isinstance(value, int): - return value - - v = text_type(value).lower() - if v not in self.ignored: - try: - return int(v) - except ValueError: - self.report(value, context) diff --git a/ext/knowit/properties/audio/codec.py b/ext/knowit/properties/audio/codec.py deleted file mode 100644 index 9107de4e73..0000000000 --- a/ext/knowit/properties/audio/codec.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from six import text_type - -from ...property import Configurable - - -class AudioCodec(Configurable): - """Audio codec property.""" - - @classmethod - def _extract_key(cls, value): - key = text_type(value).upper() - if key.startswith('A_'): - key = key[2:] - - # only the first part of the word. E.g.: 'AAC LC' => 'AAC' - return key.split(' ')[0] - - @classmethod - def _extract_fallback_key(cls, value, key): - if '/' in key: - return key.split('/')[0] diff --git a/ext/knowit/properties/audio/compression.py b/ext/knowit/properties/audio/compression.py deleted file mode 100644 index 4842b80e91..0000000000 --- a/ext/knowit/properties/audio/compression.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...property import Configurable - - -class AudioCompression(Configurable): - """Audio Compression property.""" - - pass diff --git a/ext/knowit/properties/audio/profile.py b/ext/knowit/properties/audio/profile.py deleted file mode 100644 index 05a39c98ef..0000000000 --- a/ext/knowit/properties/audio/profile.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...property import Configurable - - -class AudioProfile(Configurable): - """Audio profile property.""" - - pass diff --git a/ext/knowit/properties/basic.py b/ext/knowit/properties/basic.py deleted file mode 100644 index 46176cdd47..0000000000 --- a/ext/knowit/properties/basic.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from six import text_type - -from ..property import Property - - -class Basic(Property): - """Basic property to handle int, float and other basic types.""" - - def __init__(self, name, data_type, allow_fallback=False, **kwargs): - """Init method.""" - super(Basic, self).__init__(name, **kwargs) - self.data_type = data_type - self.allow_fallback = allow_fallback - - def handle(self, value, context): - """Handle value.""" - if isinstance(value, self.data_type): - return value - - try: - return self.data_type(text_type(value)) - except ValueError: - if not self.allow_fallback: - self.report(value, context) diff --git a/ext/knowit/properties/duration.py b/ext/knowit/properties/duration.py deleted file mode 100644 index f902356c25..0000000000 --- a/ext/knowit/properties/duration.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import re -from datetime import timedelta - -from six import text_type - -from ..property import Property - - -class Duration(Property): - """Duration property.""" - - duration_re = re.compile(r'(?P\d{1,2}):' - r'(?P\d{1,2}):' - r'(?P\d{1,2})(?:\.' - r'(?P\d{3})' - r'(?P\d{3})?\d*)?') - - def handle(self, value, context): - """Return duration as timedelta.""" - if isinstance(value, timedelta): - return value - elif isinstance(value, int): - return timedelta(milliseconds=value) - try: - return timedelta(milliseconds=int(float(value))) - except ValueError: - pass - - try: - h, m, s, ms, mc = self.duration_re.match(text_type(value)).groups('0') - return timedelta(hours=int(h), minutes=int(m), seconds=int(s), milliseconds=int(ms), microseconds=int(mc)) - except ValueError: - pass - - self.report(value, context) diff --git a/ext/knowit/properties/general.py b/ext/knowit/properties/general.py new file mode 100644 index 0000000000..c522f87fc8 --- /dev/null +++ b/ext/knowit/properties/general.py @@ -0,0 +1,144 @@ +import re +import typing +from datetime import timedelta +from decimal import Decimal, InvalidOperation + +import babelfish + +from knowit.core import Configurable, Property +from knowit.utils import round_decimal + +T = typing.TypeVar('T') + + +class Basic(Property[T]): + """Basic property to handle int, Decimal and other basic types.""" + + def __init__(self, *args: str, data_type: typing.Type, + processor: typing.Optional[typing.Callable[[T], T]] = None, + allow_fallback: bool = False, **kwargs): + """Init method.""" + super().__init__(*args, **kwargs) + self.data_type = data_type + self.processor = processor or (lambda x: x) + self.allow_fallback = allow_fallback + + def handle(self, value, context: typing.MutableMapping): + """Handle value.""" + if isinstance(value, self.data_type): + return self.processor(value) + + try: + return self.processor(self.data_type(value)) + except ValueError: + if not self.allow_fallback: + self.report(value, context) + + +class Duration(Property[timedelta]): + """Duration property.""" + + duration_re = re.compile(r'(?P\d{1,2}):' + r'(?P\d{1,2}):' + r'(?P\d{1,2})(?:\.' + r'(?P\d{3})' + r'(?P\d{3})?\d*)?') + + def __init__(self, *args: str, resolution: typing.Union[int, Decimal] = 1, **kwargs): + """Initialize a Duration.""" + super().__init__(*args, **kwargs) + self.resolution = resolution + + def handle(self, value, context: typing.MutableMapping): + """Return duration as timedelta.""" + if isinstance(value, timedelta): + return value + elif isinstance(value, int): + return timedelta(milliseconds=int(value * self.resolution)) + try: + return timedelta( + milliseconds=int(Decimal(value) * self.resolution)) + except (ValueError, InvalidOperation): + pass + + match = self.duration_re.match(value) + if not match: + self.report(value, context) + return None + + params = { + key: int(value) + for key, value in match.groupdict().items() + if value + } + return timedelta(**params) + + +class Language(Property[babelfish.Language]): + """Language property.""" + + def handle(self, value, context: typing.MutableMapping): + """Handle languages.""" + try: + if len(value) == 3: + return babelfish.Language.fromalpha3b(value) + + return babelfish.Language.fromietf(value) + except (babelfish.Error, ValueError): + pass + + try: + return babelfish.Language.fromname(value) + except babelfish.Error: + pass + + self.report(value, context) + return babelfish.Language('und') + + +class Quantity(Property): + """Quantity is a property with unit.""" + + def __init__(self, *args: str, unit, data_type=int, **kwargs): + """Init method.""" + super().__init__(*args, **kwargs) + self.unit = unit + self.data_type = data_type + + def handle(self, value, context): + """Handle value with unit.""" + if not isinstance(value, self.data_type): + try: + value = self.data_type(value) + except ValueError: + self.report(value, context) + return + if isinstance(value, Decimal): + value = round_decimal(value, min_digits=1, max_digits=3) + + return value if context.get('no_units') else value * self.unit + + +class YesNo(Configurable[str]): + """Yes or No handler.""" + + yes_values = ('yes', 'true', '1') + + def __init__(self, *args: str, yes=True, no=False, hide_value=None, + config: typing.Optional[ + typing.Mapping[str, typing.Mapping]] = None, + config_key: typing.Optional[str] = None, + **kwargs): + """Init method.""" + super().__init__(config or {}, config_key=config_key, *args, **kwargs) + self.yes = yes + self.no = no + self.hide_value = hide_value + + def handle(self, value, context): + """Handle boolean values.""" + result = self.yes if str(value).lower() in self.yes_values else self.no + if result == self.hide_value: + return None + + return super().handle(result, context) if self.mapping else result diff --git a/ext/knowit/properties/language.py b/ext/knowit/properties/language.py deleted file mode 100644 index b203c816c9..0000000000 --- a/ext/knowit/properties/language.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import babelfish - -from ..property import Property - - -class Language(Property): - """Language property.""" - - def handle(self, value, context): - """Handle languages.""" - try: - if len(value) == 3: - return babelfish.Language.fromalpha3b(value) - - return babelfish.Language.fromietf(value) - except (babelfish.Error, ValueError): - pass - - try: - return babelfish.Language.fromname(value) - except babelfish.Error: - pass - - self.report(value, context) - return babelfish.Language('und') diff --git a/ext/knowit/properties/quantity.py b/ext/knowit/properties/quantity.py deleted file mode 100644 index 487dc275d2..0000000000 --- a/ext/knowit/properties/quantity.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from six import text_type - -from ..property import Property - - -class Quantity(Property): - """Quantity is a property with unit.""" - - def __init__(self, name, unit, data_type=int, **kwargs): - """Init method.""" - super(Quantity, self).__init__(name, **kwargs) - self.unit = unit - self.data_type = data_type - - def handle(self, value, context): - """Handle value with unit.""" - if not isinstance(value, self.data_type): - try: - value = self.data_type(text_type(value)) - except ValueError: - self.report(value, context) - return - - return value if context.get('no_units') else value * self.unit diff --git a/ext/knowit/properties/subtitle.py b/ext/knowit/properties/subtitle.py new file mode 100644 index 0000000000..67f2733f71 --- /dev/null +++ b/ext/knowit/properties/subtitle.py @@ -0,0 +1,14 @@ + +from knowit.core import Configurable + + +class SubtitleFormat(Configurable[str]): + """Subtitle Format property.""" + + @classmethod + def _extract_key(cls, value) -> str: + key = str(value).upper() + if key.startswith('S_'): + key = key[2:] + + return key.split('/')[-1] diff --git a/ext/knowit/properties/subtitle/__init__.py b/ext/knowit/properties/subtitle/__init__.py deleted file mode 100644 index b791152fb2..0000000000 --- a/ext/knowit/properties/subtitle/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .format import SubtitleFormat diff --git a/ext/knowit/properties/subtitle/format.py b/ext/knowit/properties/subtitle/format.py deleted file mode 100644 index 7d57348ca1..0000000000 --- a/ext/knowit/properties/subtitle/format.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from six import text_type - -from ...property import Configurable - - -class SubtitleFormat(Configurable): - """Subtitle Format property.""" - - @classmethod - def _extract_key(cls, value): - key = text_type(value) .upper() - if key.startswith('S_'): - key = key[2:] - - return key.split('/')[-1] diff --git a/ext/knowit/properties/video.py b/ext/knowit/properties/video.py new file mode 100644 index 0000000000..e1b293d01b --- /dev/null +++ b/ext/knowit/properties/video.py @@ -0,0 +1,120 @@ +import re +import typing +from decimal import Decimal + +from knowit.core import Configurable +from knowit.core import Property +from knowit.utils import round_decimal + + +class VideoCodec(Configurable[str]): + """Video Codec handler.""" + + @classmethod + def _extract_key(cls, value) -> str: + key = value.upper().split('/')[-1] + if key.startswith('V_'): + key = key[2:] + + return key.split(' ')[-1] + + +class VideoDimensions(Property[int]): + """Dimensions property.""" + + def __init__(self, *args: str, dimension='width' or 'height', **kwargs): + """Initialize the object.""" + super().__init__(*args, **kwargs) + self.dimension = dimension + + dimensions_re = re.compile(r'(?P\d+)x(?P\d+)') + + def handle(self, value, context) -> typing.Optional[int]: + """Handle ratio.""" + match = self.dimensions_re.match(value) + if match: + match_dict = match.groupdict() + try: + value = match_dict[self.dimension] + except KeyError: + pass + else: + return int(value) + + self.report(value, context) + return None + + +class VideoEncoder(Configurable): + """Video Encoder property.""" + + +class VideoHdrFormat(Configurable): + """Video HDR Format property.""" + + +class VideoProfile(Configurable[str]): + """Video Profile property.""" + + @classmethod + def _extract_key(cls, value) -> str: + return value.upper().split('@')[0] + + +class VideoProfileLevel(Configurable[str]): + """Video Profile Level property.""" + + @classmethod + def _extract_key(cls, value) -> typing.Union[str, bool]: + values = str(value).upper().split('@') + if len(values) > 1: + value = values[1] + return value + + # There's no level, so don't warn or report it + return False + + +class VideoProfileTier(Configurable[str]): + """Video Profile Tier property.""" + + @classmethod + def _extract_key(cls, value) -> typing.Union[str, bool]: + values = str(value).upper().split('@') + if len(values) > 2: + return values[2] + + # There's no tier, so don't warn or report it + return False + + +class Ratio(Property[Decimal]): + """Ratio property.""" + + def __init__(self, *args: str, unit=None, **kwargs): + """Initialize the object.""" + super().__init__(*args, **kwargs) + self.unit = unit + + ratio_re = re.compile(r'(?P\d+)[:/](?P\d+)') + + def handle(self, value, context) -> typing.Optional[Decimal]: + """Handle ratio.""" + match = self.ratio_re.match(value) + if match: + width, height = match.groups() + if (width, height) == ('0', '1'): # identity + return Decimal('1.0') + + result = round_decimal(Decimal(width) / Decimal(height), min_digits=1, max_digits=3) + if self.unit: + result *= self.unit + + return result + + self.report(value, context) + return None + + +class ScanType(Configurable[str]): + """Scan Type property.""" diff --git a/ext/knowit/properties/video/__init__.py b/ext/knowit/properties/video/__init__.py deleted file mode 100644 index e823b39d6b..0000000000 --- a/ext/knowit/properties/video/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .codec import VideoCodec -from .encoder import VideoEncoder -from .profile import VideoProfile -from .profile import VideoProfileLevel -from .profile import VideoProfileTier -from .ratio import Ratio -from .scantype import ScanType diff --git a/ext/knowit/properties/video/codec.py b/ext/knowit/properties/video/codec.py deleted file mode 100644 index d1a873cd53..0000000000 --- a/ext/knowit/properties/video/codec.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...property import Configurable - - -class VideoCodec(Configurable): - """Video Codec handler.""" - - @classmethod - def _extract_key(cls, value): - key = value.upper().split('/')[-1] - if key.startswith('V_'): - key = key[2:] - - return key.split(' ')[-1] diff --git a/ext/knowit/properties/video/encoder.py b/ext/knowit/properties/video/encoder.py deleted file mode 100644 index b2c925b69c..0000000000 --- a/ext/knowit/properties/video/encoder.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...property import Configurable - - -class VideoEncoder(Configurable): - """Video Encoder property.""" - - pass diff --git a/ext/knowit/properties/video/profile.py b/ext/knowit/properties/video/profile.py deleted file mode 100644 index 2459d40d00..0000000000 --- a/ext/knowit/properties/video/profile.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from six import text_type - -from ...property import Configurable - - -class VideoProfile(Configurable): - """Video Profile property.""" - - @classmethod - def _extract_key(cls, value): - return value.upper().split('@')[0] - - -class VideoProfileLevel(Configurable): - """Video Profile Level property.""" - - @classmethod - def _extract_key(cls, value): - values = text_type(value).upper().split('@') - if len(values) > 1: - value = values[1] - return value - - # There's no level, so don't warn or report it - return False - - -class VideoProfileTier(Configurable): - """Video Profile Tier property.""" - - @classmethod - def _extract_key(cls, value): - values = value.upper().split('@') - if len(values) > 2: - return values[2] - - # There's no tier, so don't warn or report it - return False diff --git a/ext/knowit/properties/video/ratio.py b/ext/knowit/properties/video/ratio.py deleted file mode 100644 index 149183bd2e..0000000000 --- a/ext/knowit/properties/video/ratio.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import re - -from six import text_type - -from ...property import Property - - -class Ratio(Property): - """Ratio property.""" - - def __init__(self, name, unit=None, **kwargs): - """Constructor.""" - super(Ratio, self).__init__(name, **kwargs) - self.unit = unit - - ratio_re = re.compile(r'(?P\d+)[:/](?P\d+)') - - def handle(self, value, context): - """Handle ratio.""" - match = self.ratio_re.match(text_type(value)) - if match: - width, height = match.groups() - if (width, height) == ('0', '1'): # identity - return 1. - - result = round(float(width) / float(height), 3) - if self.unit: - result *= self.unit - - return result - - self.report(value, context) diff --git a/ext/knowit/properties/video/scantype.py b/ext/knowit/properties/video/scantype.py deleted file mode 100644 index e744ff7ad2..0000000000 --- a/ext/knowit/properties/video/scantype.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...property import Configurable - - -class ScanType(Configurable): - """Scan Type property.""" - - pass diff --git a/ext/knowit/properties/yesno.py b/ext/knowit/properties/yesno.py deleted file mode 100644 index 28edce59b4..0000000000 --- a/ext/knowit/properties/yesno.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from six import text_type - -from ..property import Property - - -class YesNo(Property): - """Yes or No handler.""" - - mapping = ('yes', 'true', '1') - - def __init__(self, name, yes=True, no=False, hide_value=None, **kwargs): - """Init method.""" - super(YesNo, self).__init__(name, **kwargs) - self.yes = yes - self.no = no - self.hide_value = hide_value - - def handle(self, value, context): - """Handle boolean values.""" - v = text_type(value).lower() - result = self.yes if v in self.mapping else self.no - return result if result != self.hide_value else None diff --git a/ext/knowit/property.py b/ext/knowit/property.py deleted file mode 100644 index 475ea403b7..0000000000 --- a/ext/knowit/property.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from logging import NullHandler, getLogger -from six import PY3, binary_type, string_types, text_type - -from .core import Reportable - -logger = getLogger(__name__) -logger.addHandler(NullHandler()) - -_visible_chars_table = dict.fromkeys(range(32)) - - -def _is_unknown(value): - return isinstance(value, text_type) and (not value or value.lower() == 'unknown') - - -class Property(Reportable): - """Property class.""" - - def __init__(self, name, default=None, private=False, description=None, delimiter=' / ', **kwargs): - """Init method.""" - super(Property, self).__init__(name, description, **kwargs) - self.default = default - self.private = private - # Used to detect duplicated values. e.g.: en / en or High@L4.0 / High@L4.0 or Progressive / Progressive - self.delimiter = delimiter - - def extract_value(self, track, context): - """Extract the property value from a given track.""" - names = self.name.split('.') - value = track.get(names[0], {}).get(names[1]) if len(names) == 2 else track.get(self.name) - if value is None: - if self.default is None: - return - - value = self.default - - if isinstance(value, string_types): - if isinstance(value, binary_type): - value = text_type(value) - else: - value = value.translate(_visible_chars_table).strip() - if _is_unknown(value): - return - value = self._deduplicate(value) - - result = self.handle(value, context) - if result is not None and not _is_unknown(result): - return result - - @classmethod - def _deduplicate(cls, value): - values = value.split(' / ') - if len(values) == 2 and values[0] == values[1]: - return values[0] - return value - - def handle(self, value, context): - """Return the value without any modification.""" - return value - - -class Configurable(Property): - """Configurable property where values are in a config mapping.""" - - def __init__(self, config, *args, **kwargs): - """Init method.""" - super(Configurable, self).__init__(*args, **kwargs) - self.mapping = getattr(config, self.__class__.__name__) - - @classmethod - def _extract_key(cls, value): - return text_type(value).upper() - - @classmethod - def _extract_fallback_key(cls, value, key): - pass - - def _lookup(self, key, context): - result = self.mapping.get(key) - if result is not None: - result = getattr(result, context.get('profile') or 'default') - return result if result != '__ignored__' else False - - def handle(self, value, context): - """Return Variable or Constant.""" - key = self._extract_key(value) - if key is False: - return - - result = self._lookup(key, context) - if result is False: - return - - while not result and key: - key = self._extract_fallback_key(value, key) - result = self._lookup(key, context) - if result is False: - return - - if not result: - self.report(value, context) - - return result - - -class MultiValue(Property): - """Property with multiple values.""" - - def __init__(self, prop=None, delimiter='/', single=False, handler=None, name=None, **kwargs): - """Init method.""" - super(MultiValue, self).__init__(prop.name if prop else name, **kwargs) - self.prop = prop - self.delimiter = delimiter - self.single = single - self.handler = handler - - def handle(self, value, context): - """Handle properties with multiple values.""" - values = (self._split(value[0], self.delimiter) - if len(value) == 1 else value) if isinstance(value, list) else self._split(value, self.delimiter) - call = self.handler or self.prop.handle - if len(values) > 1 and not self.single: - return [call(item, context) if not _is_unknown(item) else None for item in values] - - return call(values[0], context) - - @classmethod - def _split(cls, value, delimiter='/'): - if value is None: - return - - v = text_type(value) - result = map(text_type.strip, v.split(delimiter)) - return list(result) if PY3 else result diff --git a/ext/knowit/provider.py b/ext/knowit/provider.py old mode 100644 new mode 100755 index cb58c01808..f8c29f5f3b --- a/ext/knowit/provider.py +++ b/ext/knowit/provider.py @@ -1,27 +1,38 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import os +import typing from logging import NullHandler, getLogger -from . import OrderedDict -from .properties import Quantity -from .units import units +import knowit.config +from knowit.core import Property, Rule +from knowit.properties import Quantity +from knowit.units import units logger = getLogger(__name__) logger.addHandler(NullHandler()) -size_property = Quantity('size', units.byte, description='media size') +size_property = Quantity('size', unit=units.byte, description='media size') +PropertyMap = typing.Mapping[str, Property] +PropertyConfig = typing.Mapping[str, PropertyMap] -class Provider(object): +RuleMap = typing.Mapping[str, Rule] +RuleConfig = typing.Mapping[str, RuleMap] + + +class Provider: """Base class for all providers.""" min_fps = 10 max_fps = 200 - def __init__(self, config, mapping, rules=None): + def __init__( + self, + config: knowit.config.Config, + mapping: PropertyConfig, + rules: typing.Optional[RuleConfig] = None, + ): """Init method.""" self.config = config self.mapping = mapping @@ -82,7 +93,7 @@ def _describe_track(self, track, track_type, context): :param track_type: :rtype: dict """ - props = OrderedDict() + props = {} pv_props = {} for name, prop in self.mapping[track_type].items(): if not prop: diff --git a/ext/knowit/providers/__init__.py b/ext/knowit/providers/__init__.py index 66a0075c55..34ea048bef 100644 --- a/ext/knowit/providers/__init__.py +++ b/ext/knowit/providers/__init__.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- """Provider package.""" -from __future__ import unicode_literals -from .enzyme import EnzymeProvider -from .ffmpeg import FFmpegProvider -from .mediainfo import MediaInfoProvider +from knowit.providers.enzyme import EnzymeProvider +from knowit.providers.ffmpeg import FFmpegProvider +from knowit.providers.mediainfo import MediaInfoProvider +from knowit.providers.mkvmerge import MkvMergeProvider diff --git a/ext/knowit/providers/enzyme.py b/ext/knowit/providers/enzyme.py index dd9c29417f..5dd3d8cef4 100644 --- a/ext/knowit/providers/enzyme.py +++ b/ext/knowit/providers/enzyme.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, unicode_literals import json import logging @@ -7,8 +5,8 @@ from logging import NullHandler, getLogger import enzyme -from .. import OrderedDict -from ..properties import ( +from knowit.core import Property +from knowit.properties import ( AudioCodec, Basic, Duration, @@ -17,21 +15,20 @@ VideoCodec, YesNo, ) -from ..property import Property -from ..provider import ( +from knowit.provider import ( MalformedFileError, Provider, ) -from ..rules import ( +from knowit.rules import ( AudioChannelsRule, ClosedCaptionRule, HearingImpairedRule, LanguageRule, ResolutionRule, ) -from ..serializer import get_json_encoder -from ..units import units -from ..utils import todict +from knowit.serializer import get_json_encoder +from knowit.units import units +from knowit.utils import to_dict logger = getLogger(__name__) logger.addHandler(NullHandler()) @@ -42,61 +39,62 @@ class EnzymeProvider(Provider): def __init__(self, config, *args, **kwargs): """Init method.""" - super(EnzymeProvider, self).__init__(config, { - 'general': OrderedDict([ - ('title', Property('title', description='media title')), - ('duration', Duration('duration', description='media duration')), - ]), - 'video': OrderedDict([ - ('id', Basic('number', int, description='video track number')), - ('name', Property('name', description='video track name')), - ('language', Language('language', description='video language')), - ('width', Quantity('width', units.pixel)), - ('height', Quantity('height', units.pixel)), - ('scan_type', YesNo('interlaced', yes='Interlaced', no='Progressive', default='Progressive', - description='video scan type')), - ('resolution', None), # populated with ResolutionRule - # ('bit_depth', Property('bit_depth', Integer('video bit depth'))), - ('codec', VideoCodec(config, 'codec_id', description='video codec')), - ('forced', YesNo('forced', hide_value=False, description='video track forced')), - ('default', YesNo('default', hide_value=False, description='video track default')), - ('enabled', YesNo('enabled', hide_value=True, description='video track enabled')), - ]), - 'audio': OrderedDict([ - ('id', Basic('number', int, description='audio track number')), - ('name', Property('name', description='audio track name')), - ('language', Language('language', description='audio language')), - ('codec', AudioCodec(config, 'codec_id', description='audio codec')), - ('channels_count', Basic('channels', int, description='audio channels count')), - ('channels', None), # populated with AudioChannelsRule - ('forced', YesNo('forced', hide_value=False, description='audio track forced')), - ('default', YesNo('default', hide_value=False, description='audio track default')), - ('enabled', YesNo('enabled', hide_value=True, description='audio track enabled')), - ]), - 'subtitle': OrderedDict([ - ('id', Basic('number', int, description='subtitle track number')), - ('name', Property('name', description='subtitle track name')), - ('language', Language('language', description='subtitle language')), - ('hearing_impaired', None), # populated with HearingImpairedRule - ('closed_caption', None), # populated with ClosedCaptionRule - ('forced', YesNo('forced', hide_value=False, description='subtitle track forced')), - ('default', YesNo('default', hide_value=False, description='subtitle track default')), - ('enabled', YesNo('enabled', hide_value=True, description='subtitle track enabled')), - ]), + super().__init__(config, { + 'general': { + 'title': Property('title', description='media title'), + 'duration': Duration('duration', description='media duration'), + }, + 'video': { + 'id': Basic('number', data_type=int, description='video track number'), + 'name': Property('name', description='video track name'), + 'language': Language('language', description='video language'), + 'width': Quantity('width', unit=units.pixel), + 'height': Quantity('height', unit=units.pixel), + 'scan_type': YesNo('interlaced', yes='Interlaced', no='Progressive', default='Progressive', + config=config, config_key='ScanType', + description='video scan type'), + 'resolution': None, # populated with ResolutionRule + # 'bit_depth', Property('bit_depth', Integer('video bit depth')), + 'codec': VideoCodec(config, 'codec_id', description='video codec'), + 'forced': YesNo('forced', hide_value=False, description='video track forced'), + 'default': YesNo('default', hide_value=False, description='video track default'), + 'enabled': YesNo('enabled', hide_value=True, description='video track enabled'), + }, + 'audio': { + 'id': Basic('number', data_type=int, description='audio track number'), + 'name': Property('name', description='audio track name'), + 'language': Language('language', description='audio language'), + 'codec': AudioCodec(config, 'codec_id', description='audio codec'), + 'channels_count': Basic('channels', data_type=int, description='audio channels count'), + 'channels': None, # populated with AudioChannelsRule + 'forced': YesNo('forced', hide_value=False, description='audio track forced'), + 'default': YesNo('default', hide_value=False, description='audio track default'), + 'enabled': YesNo('enabled', hide_value=True, description='audio track enabled'), + }, + 'subtitle': { + 'id': Basic('number', data_type=int, description='subtitle track number'), + 'name': Property('name', description='subtitle track name'), + 'language': Language('language', description='subtitle language'), + 'hearing_impaired': None, # populated with HearingImpairedRule + 'closed_caption': None, # populated with ClosedCaptionRule + 'forced': YesNo('forced', hide_value=False, description='subtitle track forced'), + 'default': YesNo('default', hide_value=False, description='subtitle track default'), + 'enabled': YesNo('enabled', hide_value=True, description='subtitle track enabled'), + }, }, { - 'video': OrderedDict([ - ('language', LanguageRule('video language')), - ('resolution', ResolutionRule('video resolution')), - ]), - 'audio': OrderedDict([ - ('language', LanguageRule('audio language')), - ('channels', AudioChannelsRule('audio channels')), - ]), - 'subtitle': OrderedDict([ - ('language', LanguageRule('subtitle language')), - ('hearing_impaired', HearingImpairedRule('subtitle hearing impaired')), - ('closed_caption', ClosedCaptionRule('closed caption')), - ]) + 'video': { + 'language': LanguageRule('video language'), + 'resolution': ResolutionRule('video resolution'), + }, + 'audio': { + 'language': LanguageRule('audio language'), + 'channels': AudioChannelsRule('audio channels'), + }, + 'subtitle': { + 'language': LanguageRule('subtitle language'), + 'hearing_impaired': HearingImpairedRule('subtitle hearing impaired'), + 'closed_caption': ClosedCaptionRule('closed caption'), + } }) def accepts(self, video_path): @@ -107,7 +105,7 @@ def accepts(self, video_path): def extract_info(cls, video_path): """Extract info from the video.""" with open(video_path, 'rb') as f: - return todict(enzyme.MKV(f)) + return to_dict(enzyme.MKV(f)) def describe(self, video_path, context): """Return video metadata.""" diff --git a/ext/knowit/providers/ffmpeg.py b/ext/knowit/providers/ffmpeg.py index c849bc43d3..2474408ccb 100644 --- a/ext/knowit/providers/ffmpeg.py +++ b/ext/knowit/providers/ffmpeg.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import json import logging @@ -7,13 +5,9 @@ from logging import NullHandler, getLogger from subprocess import check_output -from six import ensure_text - -from .. import ( - OrderedDict, - VIDEO_EXTENSIONS, -) -from ..properties import ( +from knowit import VIDEO_EXTENSIONS +from knowit.core import Property +from knowit.properties import ( AudioChannels, AudioCodec, AudioProfile, @@ -29,24 +23,20 @@ VideoProfileLevel, YesNo, ) -from ..property import ( - Property, -) -from ..provider import ( +from knowit.provider import ( MalformedFileError, Provider, ) -from ..rules import ( +from knowit.rules import ( AudioChannelsRule, - AudioCodecRule, ClosedCaptionRule, HearingImpairedRule, LanguageRule, ResolutionRule, ) -from ..serializer import get_json_encoder -from ..units import units -from ..utils import ( +from knowit.serializer import get_json_encoder +from knowit.units import units +from knowit.utils import ( define_candidate, detect_os, ) @@ -69,10 +59,10 @@ ''' -class FFmpegExecutor(object): +class FFmpegExecutor: """Executor that knows how to execute media info: using ctypes or cli.""" - version_re = re.compile(r'\bversion\s+(?P\d+(?:\.\d+)+)\b') + version_re = re.compile(r'\bversion\s+(?P[^\b\s]+)') locations = { 'unix': ('/usr/local/ffmpeg/lib', '/usr/local/ffmpeg/bin', '__PATH__'), 'windows': ('__PATH__', ), @@ -80,7 +70,7 @@ class FFmpegExecutor(object): } def __init__(self, location, version): - """Constructor.""" + """Initialize the object.""" self.location = location self.version = version @@ -96,7 +86,7 @@ def _execute(self, filename): def _get_version(cls, output): match = cls.version_re.search(output) if match: - version = tuple([int(v) for v in match.groupdict()['version'].split('.')]) + version = match.groupdict()['version'] return version @classmethod @@ -120,19 +110,19 @@ class FFmpegCliExecutor(FFmpegExecutor): } def _execute(self, filename): - return ensure_text(check_output([self.location, '-v', 'quiet', '-print_format', 'json', - '-show_format', '-show_streams', '-sexagesimal', filename])) + return check_output([self.location, '-v', 'quiet', '-print_format', 'json', + '-show_format', '-show_streams', '-sexagesimal', filename]).decode() @classmethod def create(cls, os_family=None, suggested_path=None): """Create the executor instance.""" for candidate in define_candidate(cls.locations, cls.names, os_family, suggested_path): try: - output = ensure_text(check_output([candidate, '-version'])) + output = check_output([candidate, '-version']).decode() version = cls._get_version(output) if version: - logger.debug('FFmpeg cli detected: %s v%s', candidate, '.'.join(map(str, version))) - return FFmpegCliExecutor(candidate, version) + logger.debug('FFmpeg cli detected: %s v%s', candidate, version) + return FFmpegCliExecutor(candidate, version.split('.')) except OSError: pass @@ -142,78 +132,76 @@ class FFmpegProvider(Provider): def __init__(self, config, suggested_path=None): """Init method.""" - super(FFmpegProvider, self).__init__(config, { - 'general': OrderedDict([ - ('title', Property('tags.title', description='media title')), - ('path', Property('filename', description='media path')), - ('duration', Duration('duration', description='media duration')), - ('size', Quantity('size', units.byte, description='media size')), - ('bit_rate', Quantity('bit_rate', units.bps, description='media bit rate')), - ]), - 'video': OrderedDict([ - ('id', Basic('index', int, allow_fallback=True, description='video track number')), - ('name', Property('tags.title', description='video track name')), - ('language', Language('tags.language', description='video language')), - ('duration', Duration('duration', description='video duration')), - ('width', Quantity('width', units.pixel)), - ('height', Quantity('height', units.pixel)), - ('scan_type', ScanType(config, 'field_order', default='Progressive', description='video scan type')), - ('aspect_ratio', Ratio('display_aspect_ratio', description='display aspect ratio')), - ('pixel_aspect_ratio', Ratio('sample_aspect_ratio', description='pixel aspect ratio')), - ('resolution', None), # populated with ResolutionRule - ('frame_rate', Ratio('r_frame_rate', unit=units.FPS, description='video frame rate')), + super().__init__(config, { + 'general': { + 'title': Property('tags.title', description='media title'), + 'path': Property('filename', description='media path'), + 'duration': Duration('duration', description='media duration'), + 'size': Quantity('size', unit=units.byte, description='media size'), + 'bit_rate': Quantity('bit_rate', unit=units.bps, description='media bit rate'), + }, + 'video': { + 'id': Basic('index', data_type=int, allow_fallback=True, description='video track number'), + 'name': Property('tags.title', description='video track name'), + 'language': Language('tags.language', description='video language'), + 'duration': Duration('duration', description='video duration'), + 'width': Quantity('width', unit=units.pixel), + 'height': Quantity('height', unit=units.pixel), + 'scan_type': ScanType(config, 'field_order', default='Progressive', description='video scan type'), + 'aspect_ratio': Ratio('display_aspect_ratio', description='display aspect ratio'), + 'pixel_aspect_ratio': Ratio('sample_aspect_ratio', description='pixel aspect ratio'), + 'resolution': None, # populated with ResolutionRule + 'frame_rate': Ratio('r_frame_rate', unit=units.FPS, description='video frame rate'), # frame_rate_mode - ('bit_rate', Quantity('bit_rate', units.bps, description='video bit rate')), - ('bit_depth', Quantity('bits_per_raw_sample', units.bit, description='video bit depth')), - ('codec', VideoCodec(config, 'codec_name', description='video codec')), - ('profile', VideoProfile(config, 'profile', description='video codec profile')), - ('profile_level', VideoProfileLevel(config, 'level', description='video codec profile level')), - # ('profile_tier', VideoProfileTier(config, 'codec_profile', description='video codec profile tier')), - ('forced', YesNo('disposition.forced', hide_value=False, description='video track forced')), - ('default', YesNo('disposition.default', hide_value=False, description='video track default')), - ]), - 'audio': OrderedDict([ - ('id', Basic('index', int, allow_fallback=True, description='audio track number')), - ('name', Property('tags.title', description='audio track name')), - ('language', Language('tags.language', description='audio language')), - ('duration', Duration('duration', description='audio duration')), - ('codec', AudioCodec(config, 'codec_name', description='audio codec')), - ('_codec', AudioCodec(config, 'profile', description='audio codec', private=True, reportable=False)), - ('profile', AudioProfile(config, 'profile', description='audio codec profile')), - ('channels_count', AudioChannels('channels', description='audio channels count')), - ('channels', None), # populated with AudioChannelsRule - ('bit_depth', Quantity('bits_per_raw_sample', units.bit, description='audio bit depth')), - ('bit_rate', Quantity('bit_rate', units.bps, description='audio bit rate')), - ('sampling_rate', Quantity('sample_rate', units.Hz, description='audio sampling rate')), - ('forced', YesNo('disposition.forced', hide_value=False, description='audio track forced')), - ('default', YesNo('disposition.default', hide_value=False, description='audio track default')), - ]), - 'subtitle': OrderedDict([ - ('id', Basic('index', int, allow_fallback=True, description='subtitle track number')), - ('name', Property('tags.title', description='subtitle track name')), - ('language', Language('tags.language', description='subtitle language')), - ('hearing_impaired', YesNo('disposition.hearing_impaired', - hide_value=False, description='subtitle hearing impaired')), - ('closed_caption', None), # populated with ClosedCaptionRule - ('format', SubtitleFormat(config, 'codec_name', description='subtitle format')), - ('forced', YesNo('disposition.forced', hide_value=False, description='subtitle track forced')), - ('default', YesNo('disposition.default', hide_value=False, description='subtitle track default')), - ]), + 'bit_rate': Quantity('bit_rate', unit=units.bps, description='video bit rate'), + 'bit_depth': Quantity('bits_per_raw_sample', unit=units.bit, description='video bit depth'), + 'codec': VideoCodec(config, 'codec_name', description='video codec'), + 'profile': VideoProfile(config, 'profile', description='video codec profile'), + 'profile_level': VideoProfileLevel(config, 'level', description='video codec profile level'), + # 'profile_tier': VideoProfileTier(config, 'codec_profile', description='video codec profile tier'), + 'forced': YesNo('disposition.forced', hide_value=False, description='video track forced'), + 'default': YesNo('disposition.default', hide_value=False, description='video track default'), + }, + 'audio': { + 'id': Basic('index', data_type=int, allow_fallback=True, description='audio track number'), + 'name': Property('tags.title', description='audio track name'), + 'language': Language('tags.language', description='audio language'), + 'duration': Duration('duration', description='audio duration'), + 'codec': AudioCodec(config, 'profile', 'codec_name', description='audio codec'), + 'profile': AudioProfile(config, 'profile', description='audio codec profile'), + 'channels_count': AudioChannels('channels', description='audio channels count'), + 'channels': None, # populated with AudioChannelsRule + 'bit_depth': Quantity('bits_per_raw_sample', unit=units.bit, description='audio bit depth'), + 'bit_rate': Quantity('bit_rate', unit=units.bps, description='audio bit rate'), + 'sampling_rate': Quantity('sample_rate', unit=units.Hz, description='audio sampling rate'), + 'forced': YesNo('disposition.forced', hide_value=False, description='audio track forced'), + 'default': YesNo('disposition.default', hide_value=False, description='audio track default'), + }, + 'subtitle': { + 'id': Basic('index', data_type=int, allow_fallback=True, description='subtitle track number'), + 'name': Property('tags.title', description='subtitle track name'), + 'language': Language('tags.language', description='subtitle language'), + 'hearing_impaired': YesNo('disposition.hearing_impaired', + hide_value=False, description='subtitle hearing impaired'), + 'closed_caption': None, # populated with ClosedCaptionRule + 'format': SubtitleFormat(config, 'codec_name', description='subtitle format'), + 'forced': YesNo('disposition.forced', hide_value=False, description='subtitle track forced'), + 'default': YesNo('disposition.default', hide_value=False, description='subtitle track default'), + }, }, { - 'video': OrderedDict([ - ('language', LanguageRule('video language')), - ('resolution', ResolutionRule('video resolution')), - ]), - 'audio': OrderedDict([ - ('language', LanguageRule('audio language')), - ('channels', AudioChannelsRule('audio channels')), - ('codec', AudioCodecRule('audio codec', override=True)), - ]), - 'subtitle': OrderedDict([ - ('language', LanguageRule('subtitle language')), - ('hearing_impaired', HearingImpairedRule('subtitle hearing impaired')), - ('closed_caption', ClosedCaptionRule('closed caption')) - ]) + 'video': { + 'language': LanguageRule('video language'), + 'resolution': ResolutionRule('video resolution'), + }, + 'audio': { + 'language': LanguageRule('audio language'), + 'channels': AudioChannelsRule('audio channels'), + }, + 'subtitle': { + 'language': LanguageRule('subtitle language'), + 'hearing_impaired': HearingImpairedRule('subtitle hearing impaired'), + 'closed_caption': ClosedCaptionRule('closed caption'), + }, }) self.executor = FFmpegExecutor.get_executor_instance(suggested_path) @@ -272,5 +260,6 @@ def version(self): """Return ffmpeg version information.""" if not self.executor: return {} + version = '.'.join(map(str, self.executor.version)) - return {self.executor.location: 'v{}'.format('.'.join(map(str, self.executor.version)))} + return {self.executor.location: f'v{version}'} diff --git a/ext/knowit/providers/mediainfo.py b/ext/knowit/providers/mediainfo.py index 519fe862a1..39fd403edd 100644 --- a/ext/knowit/providers/mediainfo.py +++ b/ext/knowit/providers/mediainfo.py @@ -1,22 +1,17 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +import json import re from ctypes import c_void_p, c_wchar_p +from decimal import Decimal from logging import DEBUG, NullHandler, getLogger from subprocess import CalledProcessError, check_output -from xml.dom import minidom -from xml.etree import ElementTree from pymediainfo import MediaInfo from pymediainfo import __version__ as pymediainfo_version -from six import ensure_text -from .. import ( - OrderedDict, - VIDEO_EXTENSIONS, -) -from ..properties import ( +from knowit import VIDEO_EXTENSIONS +from knowit.core import MultiValue, Property +from knowit.properties import ( AudioChannels, AudioCodec, AudioCompression, @@ -30,20 +25,16 @@ SubtitleFormat, VideoCodec, VideoEncoder, + VideoHdrFormat, VideoProfile, - VideoProfileLevel, VideoProfileTier, YesNo, ) -from ..property import ( - MultiValue, - Property, -) -from ..provider import ( +from knowit.provider import ( MalformedFileError, Provider, ) -from ..rules import ( +from knowit.rules import ( AtmosRule, AudioChannelsRule, ClosedCaptionRule, @@ -52,10 +43,10 @@ LanguageRule, ResolutionRule, ) -from ..units import units -from ..utils import ( +from knowit.units import units +from knowit.utils import ( define_candidate, - detect_os, + detect_os, round_decimal, ) logger = getLogger(__name__) @@ -79,7 +70,7 @@ ''' -class MediaInfoExecutor(object): +class MediaInfoExecutor: """Media info executable knows how to execute media info: using ctypes or cli.""" version_re = re.compile(r'\bv(?P\d+(?:\.\d+)+)\b') @@ -91,7 +82,7 @@ class MediaInfoExecutor(object): } def __init__(self, location, version): - """Constructor.""" + """Initialize the object.""" self.location = location self.version = version @@ -130,22 +121,21 @@ class MediaInfoCliExecutor(MediaInfoExecutor): } def _execute(self, filename): - output_type = 'OLDXML' if self.version >= (17, 10) else 'XML' - return MediaInfo(ensure_text(check_output([self.location, '--Output=' + output_type, '--Full', filename]))) + return json.loads(check_output([self.location, '--Output=JSON', '--Full', filename]).decode()) @classmethod def create(cls, os_family=None, suggested_path=None): """Create the executor instance.""" for candidate in define_candidate(cls.locations, cls.names, os_family, suggested_path): try: - output = ensure_text(check_output([candidate, '--version'])) + output = check_output([candidate, '--version']).decode() version = cls._get_version(output) if version: logger.debug('MediaInfo cli detected: %s', candidate) return MediaInfoCliExecutor(candidate, version) except CalledProcessError as e: # old mediainfo returns non-zero exit code for mediainfo --version - version = cls._get_version(ensure_text(e.output)) + version = cls._get_version(e.output.decode()) if version: logger.debug('MediaInfo cli detected: %s', candidate) return MediaInfoCliExecutor(candidate, version) @@ -164,14 +154,14 @@ class MediaInfoCTypesExecutor(MediaInfoExecutor): def _execute(self, filename): # Create a MediaInfo handle - return MediaInfo.parse(filename, library_file=self.location) + return json.loads(MediaInfo.parse(filename, library_file=self.location, output='JSON')) @classmethod def create(cls, os_family=None, suggested_path=None): """Create the executor instance.""" for candidate in define_candidate(cls.locations, cls.names, os_family, suggested_path): if MediaInfo.can_parse(candidate): - lib = MediaInfo._get_library(candidate) + lib, handle, lib_version_str, lib_version = MediaInfo._get_library(candidate) lib.MediaInfo_Option.argtypes = [c_void_p, c_wchar_p, c_wchar_p] lib.MediaInfo_Option.restype = c_wchar_p version = MediaInfoExecutor._get_version(lib.MediaInfo_Option(None, "Info_Version", "")) @@ -187,88 +177,97 @@ class MediaInfoProvider(Provider): def __init__(self, config, suggested_path): """Init method.""" - super(MediaInfoProvider, self).__init__(config, { - 'general': OrderedDict([ - ('title', Property('title', description='media title')), - ('path', Property('complete_name', description='media path')), - ('duration', Duration('duration', description='media duration')), - ('size', Quantity('file_size', units.byte, description='media size')), - ('bit_rate', Quantity('overall_bit_rate', units.bps, description='media bit rate')), - ]), - 'video': OrderedDict([ - ('id', Basic('track_id', int, allow_fallback=True, description='video track number')), - ('name', Property('name', description='video track name')), - ('language', Language('language', description='video language')), - ('duration', Duration('duration', description='video duration')), - ('size', Quantity('stream_size', units.byte, description='video stream size')), - ('width', Quantity('width', units.pixel)), - ('height', Quantity('height', units.pixel)), - ('scan_type', ScanType(config, 'scan_type', default='Progressive', description='video scan type')), - ('aspect_ratio', Basic('display_aspect_ratio', float, description='display aspect ratio')), - ('pixel_aspect_ratio', Basic('pixel_aspect_ratio', float, description='pixel aspect ratio')), - ('resolution', None), # populated with ResolutionRule - ('frame_rate', Quantity('frame_rate', units.FPS, float, description='video frame rate')), + super().__init__(config, { + 'general': { + 'title': Property('Title', description='media title'), + 'path': Property('CompleteName', description='media path'), + 'duration': Duration('Duration', resolution=1000, description='media duration'), + 'size': Quantity('FileSize', unit=units.byte, description='media size'), + 'bit_rate': Quantity('OverallBitRate', unit=units.bps, description='media bit rate'), + }, + 'video': { + 'id': Basic('ID', data_type=int, allow_fallback=True, description='video track number'), + 'name': Property('Title', description='video track name'), + 'language': Language('Language', description='video language'), + 'duration': Duration('Duration', resolution=1000, description='video duration'), + 'size': Quantity('StreamSize', unit=units.byte, description='video stream size'), + 'width': Quantity('Width', unit=units.pixel), + 'height': Quantity('Height', unit=units.pixel), + 'scan_type': ScanType(config, 'ScanType', default='Progressive', description='video scan type'), + 'aspect_ratio': Basic('DisplayAspectRatio', data_type=Decimal, + processor=lambda x: round_decimal(x, min_digits=1, max_digits=3), + description='display aspect ratio'), + 'pixel_aspect_ratio': Basic('PixelAspectRatio', data_type=Decimal, + processor=lambda x: round_decimal(x, min_digits=1, max_digits=3), + description='pixel aspect ratio'), + 'resolution': None, # populated with ResolutionRule + 'frame_rate': Quantity('FrameRate', unit=units.FPS, data_type=Decimal, description='video frame rate'), # frame_rate_mode - ('bit_rate', Quantity('bit_rate', units.bps, description='video bit rate')), - ('bit_depth', Quantity('bit_depth', units.bit, description='video bit depth')), - ('codec', VideoCodec(config, 'codec', description='video codec')), - ('profile', VideoProfile(config, 'codec_profile', description='video codec profile')), - ('profile_level', VideoProfileLevel(config, 'codec_profile', description='video codec profile level')), - ('profile_tier', VideoProfileTier(config, 'codec_profile', description='video codec profile tier')), - ('encoder', VideoEncoder(config, 'encoded_library_name', description='video encoder')), - ('media_type', Property('internet_media_type', description='video media type')), - ('forced', YesNo('forced', hide_value=False, description='video track forced')), - ('default', YesNo('default', hide_value=False, description='video track default')), - ]), - 'audio': OrderedDict([ - ('id', Basic('track_id', int, allow_fallback=True, description='audio track number')), - ('name', Property('title', description='audio track name')), - ('language', Language('language', description='audio language')), - ('duration', Duration('duration', description='audio duration')), - ('size', Quantity('stream_size', units.byte, description='audio stream size')), - ('codec', MultiValue(AudioCodec(config, 'codec', description='audio codec'))), - ('profile', MultiValue(AudioProfile(config, 'format_profile', description='audio codec profile'), - delimiter=' / ')), - ('channels_count', MultiValue(AudioChannels('channel_s', description='audio channels count'))), - ('channel_positions', MultiValue(name='other_channel_positions', handler=(lambda x, *args: x), - delimiter=' / ', private=True, description='audio channels position')), - ('channels', None), # populated with AudioChannelsRule - ('bit_depth', Quantity('bit_depth', units.bit, description='audio bit depth')), - ('bit_rate', MultiValue(Quantity('bit_rate', units.bps, description='audio bit rate'))), - ('bit_rate_mode', MultiValue(BitRateMode(config, 'bit_rate_mode', description='audio bit rate mode'))), - ('sampling_rate', MultiValue(Quantity('sampling_rate', units.Hz, description='audio sampling rate'))), - ('compression', MultiValue(AudioCompression(config, 'compression_mode', - description='audio compression'))), - ('forced', YesNo('forced', hide_value=False, description='audio track forced')), - ('default', YesNo('default', hide_value=False, description='audio track default')), - ]), - 'subtitle': OrderedDict([ - ('id', Basic('track_id', int, allow_fallback=True, description='subtitle track number')), - ('name', Property('title', description='subtitle track name')), - ('language', Language('language', description='subtitle language')), - ('hearing_impaired', None), # populated with HearingImpairedRule - ('_closed_caption', Property('captionservicename', private=True)), - ('closed_caption', None), # populated with ClosedCaptionRule - ('format', SubtitleFormat(config, 'codec_id', description='subtitle format')), - ('forced', YesNo('forced', hide_value=False, description='subtitle track forced')), - ('default', YesNo('default', hide_value=False, description='subtitle track default')), - ]), + 'bit_rate': Quantity('BitRate', unit=units.bps, description='video bit rate'), + 'bit_depth': Quantity('BitDepth', unit=units.bit, description='video bit depth'), + 'codec': VideoCodec(config, 'CodecID', description='video codec'), + 'profile': VideoProfile(config, 'Format_Profile', description='video codec profile'), + 'profile_level': Property('Format_Level', description='video codec profile level'), + 'profile_tier': VideoProfileTier(config, 'Format_Tier', description='video codec profile tier'), + 'encoder': VideoEncoder(config, 'Encoded_Library_Name', description='video encoder'), + 'hdr_format': MultiValue(VideoHdrFormat(config, 'HDR_Format', description='video hdr format'), + delimiter=' / '), + 'media_type': Property('InternetMediaType', description='video media type'), + 'forced': YesNo('Forced', hide_value=False, description='video track forced'), + 'default': YesNo('Default', hide_value=False, description='video track default'), + }, + 'audio': { + 'id': Basic('ID', data_type=int, allow_fallback=True, description='audio track number'), + 'name': Property('Title', description='audio track name'), + 'language': Language('Language', description='audio language'), + 'duration': Duration('Duration', resolution=1000, description='audio duration'), + 'size': Quantity('StreamSize', unit=units.byte, description='audio stream size'), + 'codec': MultiValue(AudioCodec(config, 'CodecID', description='audio codec')), + 'format_commercial': Property('Format_Commercial', private=True), + 'profile': MultiValue(AudioProfile(config, 'Format_Profile', 'Format_AdditionalFeatures', + description='audio codec profile'), + delimiter=' / '), + 'channels_count': MultiValue(AudioChannels('Channels_Original', 'Channels', + description='audio channels count')), + 'channel_positions': MultiValue(name='ChannelPositions_String2', handler=(lambda x, *args: x), + delimiter=' / ', private=True, description='audio channels position'), + 'channels': None, # populated with AudioChannelsRule + 'bit_depth': Quantity('BitDepth', unit=units.bit, description='audio bit depth'), + 'bit_rate': MultiValue(Quantity('BitRate', unit=units.bps, description='audio bit rate')), + 'bit_rate_mode': MultiValue(BitRateMode(config, 'BitRate_Mode', description='audio bit rate mode')), + 'sampling_rate': MultiValue(Quantity('SamplingRate', unit=units.Hz, description='audio sampling rate')), + 'compression': MultiValue(AudioCompression(config, 'Compression_Mode', + description='audio compression')), + 'forced': YesNo('Forced', hide_value=False, description='audio track forced'), + 'default': YesNo('Default', hide_value=False, description='audio track default'), + }, + 'subtitle': { + 'id': Basic('ID', data_type=int, allow_fallback=True, description='subtitle track number'), + 'name': Property('Title', description='subtitle track name'), + 'language': Language('Language', description='subtitle language'), + 'hearing_impaired': None, # populated with HearingImpairedRule + '_closed_caption': Property('ClosedCaptionsPresent', private=True), + 'closed_caption': None, # populated with ClosedCaptionRule + 'format': SubtitleFormat(config, 'CodecID', description='subtitle format'), + 'forced': YesNo('Forced', hide_value=False, description='subtitle track forced'), + 'default': YesNo('Default', hide_value=False, description='subtitle track default'), + }, }, { - 'video': OrderedDict([ - ('language', LanguageRule('video language')), - ('resolution', ResolutionRule('video resolution')), - ]), - 'audio': OrderedDict([ - ('language', LanguageRule('audio language')), - ('channels', AudioChannelsRule('audio channels')), - ('_atmosrule', AtmosRule('atmos rule')), - ('_dtshdrule', DtsHdRule('dts-hd rule')), - ]), - 'subtitle': OrderedDict([ - ('language', LanguageRule('subtitle language')), - ('hearing_impaired', HearingImpairedRule('subtitle hearing impaired')), - ('closed_caption', ClosedCaptionRule('closed caption')), - ]) + 'video': { + 'language': LanguageRule('video language'), + 'resolution': ResolutionRule('video resolution'), + }, + 'audio': { + 'language': LanguageRule('audio language'), + 'channels': AudioChannelsRule('audio channels'), + '_atmosrule': AtmosRule(config, 'atmos rule'), + '_dtshdrule': DtsHdRule(config, 'dts-hd rule'), + }, + 'subtitle': { + 'language': LanguageRule('subtitle language'), + 'hearing_impaired': HearingImpairedRule('subtitle hearing impaired'), + 'closed_caption': ClosedCaptionRule('closed caption'), + } }) self.executor = MediaInfoExecutor.get_executor_instance(suggested_path) @@ -282,12 +281,11 @@ def accepts(self, video_path): def describe(self, video_path, context): """Return video metadata.""" - media_info = self.executor.extract_info(video_path) + data = self.executor.extract_info(video_path) def debug_data(): """Debug data.""" - xml = ensure_text(ElementTree.tostring(media_info.xml_dom)).replace('\r', '').replace('\n', '') - return ensure_text(minidom.parseString(xml).toprettyxml(indent=' ', newl='\n', encoding='utf-8')) + return json.dumps(data, indent=4) context['debug_data'] = debug_data @@ -295,15 +293,15 @@ def debug_data(): logger.debug('Video %r scanned using mediainfo %r has raw data:\n%s', video_path, self.executor.location, debug_data()) - data = media_info.to_data() result = {} - if data.get('tracks'): + tracks = data.get('media', {}).get('track', []) + if tracks: general_tracks = [] video_tracks = [] audio_tracks = [] subtitle_tracks = [] - for track in data.get('tracks'): - track_type = track.get('track_type') + for track in tracks: + track_type = track.get('@type') if track_type == 'General': general_tracks.append(track) elif track_type == 'Video': @@ -328,8 +326,8 @@ def debug_data(): @property def version(self): """Return mediainfo version information.""" - versions = [('pymediainfo', pymediainfo_version)] + versions = {'pymediainfo': pymediainfo_version} if self.executor: - versions.append((self.executor.location, 'v{}'.format('.'.join(map(str, self.executor.version))))) - - return OrderedDict(versions) + executor_version = '.'.join(map(str, self.executor.version)) + versions[self.executor.location] = f'v{executor_version}' + return versions diff --git a/ext/knowit/providers/mkvmerge.py b/ext/knowit/providers/mkvmerge.py new file mode 100644 index 0000000000..e5aca15506 --- /dev/null +++ b/ext/knowit/providers/mkvmerge.py @@ -0,0 +1,248 @@ + +import json +import logging +import re +from decimal import Decimal +from logging import NullHandler, getLogger +from subprocess import check_output + +from knowit.core import Property +from knowit.properties import ( + AudioCodec, + Basic, + Duration, + Language, + Quantity, + VideoCodec, + VideoDimensions, + YesNo, +) +from knowit.provider import ( + MalformedFileError, + Provider, +) +from knowit.rules import ( + AudioChannelsRule, + ClosedCaptionRule, + HearingImpairedRule, + LanguageRule, + ResolutionRule, +) +from knowit.serializer import get_json_encoder +from knowit.units import units +from knowit.utils import define_candidate, detect_os + +logger = getLogger(__name__) +logger.addHandler(NullHandler()) + +WARN_MSG = r''' +========================================================================================= +mkvmerge not found on your system or could not be loaded. +Visit https://mkvtoolnix.download to download it. +If you still have problems, please check if the downloaded version matches your system. +To load mkvmerge from a specific location, please define the location as follow: + knowit --mkvmerge /usr/local/mkvmerge/bin + knowit --mkvmerge /usr/local/mkvmerge/bin/ffprobe + knowit --mkvmerge "C:\Program Files\mkvmerge" + knowit --mkvmerge C:\Software\mkvmerge.exe +========================================================================================= +''' + + +class MkvMergeExecutor: + """Executor that knows how to execute mkvmerge.""" + + version_re = re.compile(r'\bv(?P[^\b\s]+)') + locations = { + 'unix': ('/usr/local/mkvmerge/lib', '/usr/local/mkvmerge/bin', '__PATH__'), + 'windows': ('__PATH__', ), + 'macos': ('__PATH__', ), + } + + def __init__(self, location, version): + """Initialize the object.""" + self.location = location + self.version = version + + def extract_info(self, filename): + """Extract media info.""" + json_dump = self._execute(filename) + return json.loads(json_dump) + + def _execute(self, filename): + raise NotImplementedError + + @classmethod + def _get_version(cls, output): + match = cls.version_re.search(output) + if match: + version = match.groupdict()['version'] + return version + + @classmethod + def get_executor_instance(cls, suggested_path=None): + """Return executor instance.""" + os_family = detect_os() + logger.debug('Detected os: %s', os_family) + for exec_cls in (MkvMergeCliExecutor, ): + executor = exec_cls.create(os_family, suggested_path) + if executor: + return executor + + +class MkvMergeCliExecutor(MkvMergeExecutor): + """Executor that uses mkvmerge cli.""" + + names = { + 'unix': ('mkvmerge', ), + 'windows': ('mkvmerge.exe', ), + 'macos': ('mkvmerge', ), + } + + def _execute(self, filename): + return check_output([self.location, '-i', '-F', 'json', filename]).decode() + + @classmethod + def create(cls, os_family=None, suggested_path=None): + """Create the executor instance.""" + for candidate in define_candidate(cls.locations, cls.names, os_family, suggested_path): + try: + output = check_output([candidate, '--version']).decode() + version = cls._get_version(output) + if version: + logger.debug('MkvMerge cli detected: %s v%s', candidate, version) + return MkvMergeCliExecutor(candidate, version.split('.')) + except OSError: + pass + + +class MkvMergeProvider(Provider): + """MkvMerge Provider.""" + + def __init__(self, config, suggested_path=None, *args, **kwargs): + """Init method.""" + super().__init__(config, { + 'general': { + 'title': Property('title', description='media title'), + 'duration': Duration('duration', resolution=Decimal('0.000001'), description='media duration'), + }, + 'video': { + 'id': Basic('number', data_type=int, description='video track number'), + 'name': Property('name', description='video track name'), + 'language': Language('language_ietf', 'language', description='video language'), + 'width': VideoDimensions('display_dimensions', dimension='width'), + 'height': VideoDimensions('display_dimensions', dimension='height'), + 'scan_type': YesNo('interlaced', yes='Interlaced', no='Progressive', default='Progressive', + config=config, config_key='ScanType', + description='video scan type'), + 'resolution': None, # populated with ResolutionRule + # 'bit_depth', Property('bit_depth', Integer('video bit depth')), + 'codec': VideoCodec(config, 'codec_id', description='video codec'), + 'forced': YesNo('forced_track', hide_value=False, description='video track forced'), + 'default': YesNo('default_track', hide_value=False, description='video track default'), + 'enabled': YesNo('enabled_track', hide_value=True, description='video track enabled'), + }, + 'audio': { + 'id': Basic('number', data_type=int, description='audio track number'), + 'name': Property('name', description='audio track name'), + 'language': Language('language_ietf', 'language', description='audio language'), + 'codec': AudioCodec(config, 'codec_id', description='audio codec'), + 'channels_count': Basic('audio_channels', data_type=int, description='audio channels count'), + 'channels': None, # populated with AudioChannelsRule + 'sampling_rate': Quantity('audio_sampling_frequency', unit=units.Hz, description='audio sampling rate'), + 'forced': YesNo('forced_track', hide_value=False, description='audio track forced'), + 'default': YesNo('default_track', hide_value=False, description='audio track default'), + 'enabled': YesNo('enabled_track', hide_value=True, description='audio track enabled'), + }, + 'subtitle': { + 'id': Basic('number', data_type=int, description='subtitle track number'), + 'name': Property('name', description='subtitle track name'), + 'language': Language('language_ietf', 'language', description='subtitle language'), + 'hearing_impaired': None, # populated with HearingImpairedRule + 'closed_caption': None, # populated with ClosedCaptionRule + 'forced': YesNo('forced_track', hide_value=False, description='subtitle track forced'), + 'default': YesNo('default_track', hide_value=False, description='subtitle track default'), + 'enabled': YesNo('enabled_track', hide_value=True, description='subtitle track enabled'), + }, + }, { + 'video': { + 'language': LanguageRule('video language', override=True), + 'resolution': ResolutionRule('video resolution'), + }, + 'audio': { + 'language': LanguageRule('audio language', override=True), + 'channels': AudioChannelsRule('audio channels'), + }, + 'subtitle': { + 'language': LanguageRule('subtitle language', override=True), + 'hearing_impaired': HearingImpairedRule('subtitle hearing impaired'), + 'closed_caption': ClosedCaptionRule('closed caption'), + } + }) + self.executor = MkvMergeExecutor.get_executor_instance(suggested_path) + + def accepts(self, video_path): + """Accept Matroska videos when mkvmerge is available.""" + if self.executor is None: + logger.warning(WARN_MSG) + self.executor = False + + return self.executor and video_path.lower().endswith(('.mkv', '.mka', '.mks')) + + @classmethod + def extract_info(cls, video_path): + """Extract info from the video.""" + return json.loads(check_output(['mkvmerge', '-i', '-F', video_path]).decode()) + + def describe(self, video_path, context): + """Return video metadata.""" + data = self.executor.extract_info(video_path) + + def debug_data(): + """Debug data.""" + return json.dumps(data, cls=get_json_encoder(context), indent=4, ensure_ascii=False) + + context['debug_data'] = debug_data + + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Video %r scanned using mkvmerge %r has raw data:\n%s', + video_path, self.executor.location, debug_data()) + + def merge_properties(target: dict): + """Merge properties sub properties into the target container.""" + return {**{k: v for k, v in target.items() if k != 'properties'}, **target.get('properties', {})} + + general_track = merge_properties(data.get('container', {})) + video_tracks = [] + audio_tracks = [] + subtitle_tracks = [] + for track in data.get('tracks'): + track_type = track.get('type') + merged = merge_properties(track) + if track_type == 'video': + video_tracks.append(merged) + elif track_type == 'audio': + audio_tracks.append(merged) + elif track_type == 'subtitles': + subtitle_tracks.append(merged) + + result = self._describe_tracks(video_path, general_track, video_tracks, audio_tracks, subtitle_tracks, context) + + if not result: + raise MalformedFileError + + result['provider'] = { + 'name': 'mkvmerge', + 'version': self.version + } + + return result + + @property + def version(self): + """Return mkvmerge version information.""" + if not self.executor: + return {} + version = '.'.join(map(str, self.executor.version)) + + return {self.executor.location: f'v{version}'} diff --git a/ext/knowit/rule.py b/ext/knowit/rule.py deleted file mode 100644 index 6d0764955b..0000000000 --- a/ext/knowit/rule.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .core import Reportable - - -class Rule(Reportable): - """Rule abstract class.""" - - def __init__(self, name, override=False, **kwargs): - """Constructor.""" - super(Rule, self).__init__(name, **kwargs) - self.override = override - - def execute(self, props, pv_props, context): - """How to execute a rule.""" - raise NotImplementedError diff --git a/ext/knowit/rules/__init__.py b/ext/knowit/rules/__init__.py index 533706258e..90a943a8f3 100644 --- a/ext/knowit/rules/__init__.py +++ b/ext/knowit/rules/__init__.py @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from .audio import AtmosRule -from .audio import AudioChannelsRule -from .audio import AudioCodecRule -from .audio import DtsHdRule -from .language import LanguageRule -from .subtitle import ClosedCaptionRule -from .subtitle import HearingImpairedRule -from .video import ResolutionRule +from knowit.rules.audio import AtmosRule +from knowit.rules.audio import AudioChannelsRule +from knowit.rules.audio import DtsHdRule +from knowit.rules.general import LanguageRule +from knowit.rules.subtitle import ClosedCaptionRule +from knowit.rules.subtitle import HearingImpairedRule +from knowit.rules.video import ResolutionRule diff --git a/ext/knowit/rules/audio.py b/ext/knowit/rules/audio.py new file mode 100644 index 0000000000..b35d364370 --- /dev/null +++ b/ext/knowit/rules/audio.py @@ -0,0 +1,104 @@ +import typing +from decimal import Decimal +from logging import NullHandler, getLogger + +from knowit.core import Rule + +logger = getLogger(__name__) +logger.addHandler(NullHandler()) + + +class AtmosRule(Rule): + """Atmos rule.""" + + def __init__(self, config: typing.Mapping[str, typing.Mapping], name: str, + **kwargs): + """Initialize an Atmos rule.""" + super().__init__(name, **kwargs) + self.audio_codecs = getattr(config, 'AudioCodec') + + def execute(self, props, pv_props, context): + """Execute the rule against properties.""" + profile = context.get('profile') or 'default' + format_commercial = pv_props.get('format_commercial') + if 'codec' in props and format_commercial and 'atmos' in format_commercial.lower(): + props['codec'] = [props['codec'], + getattr(self.audio_codecs['ATMOS'], profile)] + + +class AudioChannelsRule(Rule): + """Audio Channel rule.""" + + mapping = { + 1: '1.0', + 2: '2.0', + 6: '5.1', + 8: '7.1', + } + + def execute(self, props, pv_props, context): + """Execute the rule against properties.""" + count = props.get('channels_count') + if count is None: + return + + channels = self.mapping.get(count) if isinstance(count, int) else None + positions = pv_props.get('channel_positions') or [] + positions = positions if isinstance(positions, list) else [positions] + candidate = 0 + for position in positions: + if not position: + continue + + c = Decimal('0.0') + for i in position.split('/'): + try: + c += Decimal(i) + except ValueError: + logger.debug('Invalid %s: %s', self.description, i) + pass + + c_count = int(c) + int(round((c - int(c)) * 10)) + if c_count == count: + return str(c) + + candidate = max(candidate, c) + + if channels: + return channels + + if candidate: + return candidate + + self.report(positions, context) + + +class DtsHdRule(Rule): + """DTS-HD rule.""" + + def __init__(self, config: typing.Mapping[str, typing.Mapping], name: str, + **kwargs): + """Initialize a DTS-HD Rule.""" + super().__init__(name, **kwargs) + self.audio_codecs = getattr(config, 'AudioCodec') + self.audio_profiles = getattr(config, 'AudioProfile') + + @classmethod + def _redefine(cls, props, name, index): + actual = props.get(name) + if isinstance(actual, list): + value = actual[index] + if value is None: + del props[name] + else: + props[name] = value + + def execute(self, props, pv_props, context): + """Execute the rule against properties.""" + profile = context.get('profile') or 'default' + + if props.get('codec') == getattr(self.audio_codecs['DTS'], + profile) and props.get('profile') in ( + getattr(self.audio_profiles['MA'], profile), + getattr(self.audio_profiles['HRA'], profile)): + props['codec'] = getattr(self.audio_codecs['DTS-HD'], profile) diff --git a/ext/knowit/rules/audio/__init__.py b/ext/knowit/rules/audio/__init__.py deleted file mode 100644 index d8a9470473..0000000000 --- a/ext/knowit/rules/audio/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .atmos import AtmosRule -from .channels import AudioChannelsRule -from .codec import AudioCodecRule -from .dtshd import DtsHdRule diff --git a/ext/knowit/rules/audio/atmos.py b/ext/knowit/rules/audio/atmos.py deleted file mode 100644 index 3e429d866c..0000000000 --- a/ext/knowit/rules/audio/atmos.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...rule import Rule - - -class AtmosRule(Rule): - """Atmos rule.""" - - @classmethod - def _redefine(cls, props, name, index): - actual = props.get(name) - if isinstance(actual, list): - value = actual[index] - if value is None: - del props[name] - else: - props[name] = value - - def execute(self, props, pv_props, context): - """Execute the rule against properties.""" - codecs = props.get('codec') or [] - # TODO: handle this properly - if 'atmos' in {codec.lower() for codec in codecs if codec}: - index = None - for i, codec in enumerate(codecs): - if codec and 'atmos' in codec.lower(): - index = i - break - - if index is not None: - for name in ('channels_count', 'sampling_rate'): - self._redefine(props, name, index) diff --git a/ext/knowit/rules/audio/channels.py b/ext/knowit/rules/audio/channels.py deleted file mode 100644 index 50975d5b2f..0000000000 --- a/ext/knowit/rules/audio/channels.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from logging import NullHandler, getLogger -from six import text_type - -from ...rule import Rule - -logger = getLogger(__name__) -logger.addHandler(NullHandler()) - - -class AudioChannelsRule(Rule): - """Audio Channel rule.""" - - mapping = { - 1: '1.0', - 2: '2.0', - 6: '5.1', - 8: '7.1', - } - - def execute(self, props, pv_props, context): - """Execute the rule against properties.""" - count = props.get('channels_count') - if count is None: - return - - channels = self.mapping.get(count) if isinstance(count, int) else None - positions = pv_props.get('channel_positions') or [] - positions = positions if isinstance(positions, list) else [positions] - candidate = 0 - for position in positions: - if not position: - continue - - c = 0 - for i in position.split('/'): - try: - c += float(i) - except ValueError: - logger.debug('Invalid %s: %s', self.description, i) - pass - - c_count = int(c) + int(round((c - int(c)) * 10)) - if c_count == count: - return text_type(c) - - candidate = max(candidate, c) - - if channels: - return channels - - if candidate: - return text_type(candidate) - - self.report(positions, context) diff --git a/ext/knowit/rules/audio/codec.py b/ext/knowit/rules/audio/codec.py deleted file mode 100644 index 5690e220b3..0000000000 --- a/ext/knowit/rules/audio/codec.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...rule import Rule - - -class AudioCodecRule(Rule): - """Audio Codec rule.""" - - def execute(self, props, pv_props, context): - """Execute the rule against properties.""" - if '_codec' in pv_props: - return pv_props.get('_codec') diff --git a/ext/knowit/rules/audio/dtshd.py b/ext/knowit/rules/audio/dtshd.py deleted file mode 100644 index d44cdf138d..0000000000 --- a/ext/knowit/rules/audio/dtshd.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from ...rule import Rule - - -class DtsHdRule(Rule): - """DTS-HD rule.""" - - @classmethod - def _redefine(cls, props, name, index): - actual = props.get(name) - if isinstance(actual, list): - value = actual[index] - if value is None: - del props[name] - else: - props[name] = value - - def execute(self, props, pv_props, context): - """Execute the rule against properties.""" - if props.get('codec') == 'DTS-HD': - index = None - for i, profile in enumerate(props.get('profile', [])): - if profile and profile.upper() != 'CORE': - index = i - break - - if index is not None: - for name in ('profile', 'channels_count', 'bit_rate', - 'bit_rate_mode', 'sampling_rate', 'compression'): - self._redefine(props, name, index) diff --git a/ext/knowit/rules/language.py b/ext/knowit/rules/general.py similarity index 89% rename from ext/knowit/rules/language.py rename to ext/knowit/rules/general.py index 8a51ccf059..b492c03a52 100644 --- a/ext/knowit/rules/language.py +++ b/ext/knowit/rules/general.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import re from logging import NullHandler, getLogger import babelfish -from ..rule import Rule +from knowit.core import Rule logger = getLogger(__name__) logger.addHandler(NullHandler()) diff --git a/ext/knowit/rules/subtitle/closedcaption.py b/ext/knowit/rules/subtitle.py similarity index 52% rename from ext/knowit/rules/subtitle/closedcaption.py rename to ext/knowit/rules/subtitle.py index 14be06fdd2..fa16fdbc12 100644 --- a/ext/knowit/rules/subtitle/closedcaption.py +++ b/ext/knowit/rules/subtitle.py @@ -1,9 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import re -from ...rule import Rule +from knowit.core import Rule class ClosedCaptionRule(Rule): @@ -16,3 +13,15 @@ def execute(self, props, pv_props, context): for name in (pv_props.get('_closed_caption'), props.get('name')): if name and self.cc_re.search(name): return True + + +class HearingImpairedRule(Rule): + """Hearing Impaired rule.""" + + hi_re = re.compile(r'(\bsdh\b)', re.IGNORECASE) + + def execute(self, props, pv_props, context): + """Hearing Impaired.""" + name = props.get('name') + if name and self.hi_re.search(name): + return True diff --git a/ext/knowit/rules/subtitle/__init__.py b/ext/knowit/rules/subtitle/__init__.py deleted file mode 100644 index eff71d670e..0000000000 --- a/ext/knowit/rules/subtitle/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .closedcaption import ClosedCaptionRule -from .hearingimpaired import HearingImpairedRule diff --git a/ext/knowit/rules/subtitle/hearingimpaired.py b/ext/knowit/rules/subtitle/hearingimpaired.py deleted file mode 100644 index 54c4d56794..0000000000 --- a/ext/knowit/rules/subtitle/hearingimpaired.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -import re - -from ...rule import Rule - - -class HearingImpairedRule(Rule): - """Hearing Impaired rule.""" - - hi_re = re.compile(r'(\bsdh\b)', re.IGNORECASE) - - def execute(self, props, pv_props, context): - """Hearing Impaired.""" - name = props.get('name') - if name and self.hi_re.search(name): - return True diff --git a/ext/knowit/rules/video/resolution.py b/ext/knowit/rules/video.py similarity index 83% rename from ext/knowit/rules/video/resolution.py rename to ext/knowit/rules/video.py index bcdd594edc..dabde0a55d 100644 --- a/ext/knowit/rules/video/resolution.py +++ b/ext/knowit/rules/video.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +from decimal import Decimal -from ...rule import Rule +from knowit.core import Rule class ResolutionRule(Rule): @@ -47,7 +46,7 @@ def execute(self, props, pv_props, context): except AttributeError: pass - dar = props.get('aspect_ratio', float(width) / height) + dar = props.get('aspect_ratio', Decimal(width) / height) par = props.get('pixel_aspect_ratio', 1) scan_type = props.get('scan_type', 'p')[0].lower() @@ -68,8 +67,7 @@ def execute(self, props, pv_props, context): selected_resolution = r if selected_resolution: - return '{0}{1}'.format(selected_resolution, scan_type) + return f'{selected_resolution}{scan_type}' - msg = '{width}x{height} - scan_type: {scan_type}, aspect_ratio: {dar}, pixel_aspect_ratio: {par}'.format( - width=width, height=height, scan_type=scan_type, dar=dar, par=par) + msg = f'{width}x{height} - scan_type: {scan_type}, aspect_ratio: {dar}, pixel_aspect_ratio: {par}' self.report(msg, context) diff --git a/ext/knowit/rules/video/__init__.py b/ext/knowit/rules/video/__init__.py deleted file mode 100644 index 77c0b406f9..0000000000 --- a/ext/knowit/rules/video/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from .resolution import ResolutionRule diff --git a/ext/knowit/serializer.py b/ext/knowit/serializer.py index a799df7680..4922dc7f2a 100644 --- a/ext/knowit/serializer.py +++ b/ext/knowit/serializer.py @@ -1,29 +1,35 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - +import datetime import json -from collections import OrderedDict +import re +import typing from datetime import timedelta +from decimal import Decimal import babelfish -from six import text_type import yaml +from yaml.composer import Composer +from yaml.constructor import SafeConstructor +from yaml.parser import Parser +from yaml.reader import Reader +from yaml.resolver import Resolver as DefaultResolver +from yaml.scanner import Scanner -from .units import units +from knowit.units import units +from knowit.utils import round_decimal -def format_property(context, o): +def format_property(profile: str, o): """Convert properties to string.""" if isinstance(o, timedelta): - return format_duration(o, context['profile']) + return format_duration(o, profile) if isinstance(o, babelfish.language.Language): - return format_language(o, context['profile']) + return format_language(o, profile) if hasattr(o, 'units'): - return format_quantity(o, context['profile']) + return format_quantity(o, profile) - return text_type(o) + return str(o) def get_json_encoder(context): @@ -32,7 +38,7 @@ class StringEncoder(json.JSONEncoder): """String json encoder.""" def default(self, o): - return format_property(context, o) + return format_property(context['profile'], o) return StringEncoder @@ -46,14 +52,8 @@ def default_representer(self, data): """Convert data to string.""" if isinstance(data, int): return self.represent_int(data) - if isinstance(data, float): - return self.represent_float(data) return self.represent_str(str(data)) - def ordered_dict_representer(self, data): - """Representer for OrderedDict.""" - return self.represent_mapping('tag:yaml.org,2002:map', data.items()) - def default_language_representer(self, data): """Convert language to string.""" return self.represent_str(format_language(data, context['profile'])) @@ -66,10 +66,10 @@ def default_duration_representer(self, data): """Convert quantity to string.""" return self.default_representer(format_duration(data, context['profile'])) - CustomDumper.add_representer(OrderedDict, CustomDumper.ordered_dict_representer) CustomDumper.add_representer(babelfish.Language, CustomDumper.default_language_representer) CustomDumper.add_representer(timedelta, CustomDumper.default_duration_representer) CustomDumper.add_representer(units.Quantity, CustomDumper.default_quantity_representer) + CustomDumper.add_representer(Decimal, CustomDumper.default_representer) return CustomDumper @@ -77,26 +77,65 @@ def default_duration_representer(self, data): def get_yaml_loader(constructors=None): """Return a yaml loader that handles sequences as python lists.""" constructors = constructors or {} - - class CustomLoader(yaml.Loader): + yaml_implicit_resolvers = dict(DefaultResolver.yaml_implicit_resolvers) + + class Resolver(DefaultResolver): + """Custom YAML Resolver.""" + + Resolver.yaml_implicit_resolvers.clear() + for ch, vs in yaml_implicit_resolvers.items(): + Resolver.yaml_implicit_resolvers.setdefault(ch, []).extend( + (tag, regexp) for tag, regexp in vs + if not tag.endswith('float') + ) + Resolver.add_implicit_resolver( # regex copied from yaml source + '!decimal', + re.compile(r'''^(?: + [-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? + |\.[0-9_]+(?:[eE][-+][0-9]+)? + |[-+]?[0-9][0-9_]*(?::[0-9]?[0-9])+\.[0-9_]* + |[-+]?\.(?:inf|Inf|INF) + |\.(?:nan|NaN|NAN) + )$''', re.VERBOSE), + list('-+0123456789.') + ) + + class CustomLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): """Custom YAML Loader.""" - pass + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + SafeConstructor.__init__(self) + Resolver.__init__(self) - CustomLoader.add_constructor('tag:yaml.org,2002:seq', CustomLoader.construct_python_tuple) + CustomLoader.add_constructor('tag:yaml.org,2002:seq', yaml.Loader.construct_python_tuple) for tag, constructor in constructors.items(): CustomLoader.add_constructor(tag, constructor) + def decimal_constructor(loader, node): + value = loader.construct_scalar(node) + return Decimal(value) + + CustomLoader.add_constructor('!decimal', decimal_constructor) + return CustomLoader -def format_duration(duration, profile='default'): +def format_duration( + duration: datetime.timedelta, + profile='default', +) -> typing.Union[str, Decimal]: if profile == 'technical': return str(duration) seconds = duration.total_seconds() if profile == 'code': - return duration.total_seconds() + return round_decimal( + Decimal((duration.days * 86400 + duration.seconds) * 10 ** 6 + duration.microseconds) / 10**6, min_digits=1 + ) hours = int(seconds // 3600) seconds = seconds - (hours * 3600) @@ -104,23 +143,28 @@ def format_duration(duration, profile='default'): seconds = int(seconds - (minutes * 60)) if profile == 'human': if hours > 0: - return '{0} hours {1:02d} minutes {2:02d} seconds'.format(hours, minutes, seconds) + return f'{hours} hours {minutes:02d} minutes { seconds:02d} seconds' if minutes > 0: - return '{0} minutes {1:02d} seconds'.format(minutes, seconds) - - return '{0} seconds'.format(seconds) + return f'{minutes} minutes {seconds:02d} seconds' + return f'{seconds} seconds' - return '{0}:{1:02d}:{2:02d}'.format(hours, minutes, seconds) + return f'{hours}:{minutes:02d}:{seconds:02d}' -def format_language(language, profile='default'): +def format_language( + language: babelfish.language.Language, + profile: str = 'default', +) -> str: if profile in ('default', 'human'): return str(language.name) return str(language) -def format_quantity(quantity, profile='default'): +def format_quantity( + quantity, + profile='default', +) -> str: """Human friendly format.""" if profile == 'code': return quantity.magnitude @@ -140,16 +184,26 @@ def format_quantity(quantity, profile='default'): return str(quantity) -def _format_quantity(num, unit='B', binary=False, precision=2): - fmt_pattern = '{value:3.%sf} {prefix}{affix}{unit}' % precision - factor = 1024. if binary else 1000. - binary_affix = 'i' if binary else '' +def _format_quantity( + num, + unit: str = 'B', + binary: bool = False, + precision: int = 2, +) -> str: + if binary: + factor = 1024 + affix = 'i' + else: + factor = 1000 + affix = '' for prefix in ('', 'K', 'M', 'G', 'T', 'P', 'E', 'Z'): if abs(num) < factor: - return fmt_pattern.format(value=num, prefix=prefix, affix=binary_affix, unit=unit) + break num /= factor + else: + prefix = 'Y' - return fmt_pattern.format(value=num, prefix='Y', affix=binary_affix, unit=unit) + return f'{num:3.{precision}f} {prefix}{affix}{unit}' YAMLLoader = get_yaml_loader() diff --git a/ext/knowit/units.py b/ext/knowit/units.py index 2397a60bc3..73ec16a5ae 100644 --- a/ext/knowit/units.py +++ b/ext/knowit/units.py @@ -1,23 +1,32 @@ -# -*- coding: utf-8 -*- +import typing +try: + import pint +except ImportError: + pint = False -def _build_unit_registry(): - try: - from pint import UnitRegistry - registry = UnitRegistry() - registry.define('FPS = 1 * hertz') - except ImportError: - class NoUnitRegistry: +class NullRegistry: + """A NullRegistry that masquerades as a pint.UnitRegistry.""" + + def __init__(self): + """Initialize a null registry.""" - def __init__(self): - pass + def __getattr__(self, item: typing.Any) -> int: + """Return a Scalar 1 to simulate a unit.""" + return 1 - def __getattr__(self, item): - return 1 + def __bool__(self): + """Return False since a NullRegistry is not a pint.UnitRegistry.""" + return False - registry = NoUnitRegistry() + def define(self, *args, **kwargs): + """Pretend to add unit to the registry.""" + +def _build_unit_registry(): + registry = pint.UnitRegistry() if pint else NullRegistry() + registry.define('FPS = 1 * hertz') return registry diff --git a/ext/knowit/utils.py b/ext/knowit/utils.py index c65d54943d..2bb0ed74b5 100644 --- a/ext/knowit/utils.py +++ b/ext/knowit/utils.py @@ -1,95 +1,134 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os import sys -from collections import OrderedDict +import typing +from decimal import Decimal -from six import PY2, string_types, text_type +from knowit import VIDEO_EXTENSIONS -from . import VIDEO_EXTENSIONS +if sys.version_info < (3, 8): + OS_FAMILY = str +else: + OS_FAMILY = typing.Literal['windows', 'macos', 'unix'] +OPTION_MAP = typing.Dict[str, typing.Tuple[str]] -def recurse_paths(paths): - """Return a file system encoded list of videofiles. - :param paths: - :type paths: string or list - :return: - :rtype: list - """ +def recurse_paths( + paths: typing.Union[str, typing.Iterable[str]] +) -> typing.List[str]: + """Return a list of video files.""" enc_paths = [] - if isinstance(paths, (string_types, text_type)): + if isinstance(paths, str): paths = [p.strip() for p in paths.split(',')] if ',' in paths else paths.split() - encoding = sys.getfilesystemencoding() for path in paths: if os.path.isfile(path): - enc_paths.append(path.decode(encoding) if PY2 else path) + enc_paths.append(path) if os.path.isdir(path): for root, directories, filenames in os.walk(path): for filename in filenames: if os.path.splitext(filename)[1] in VIDEO_EXTENSIONS: - if PY2 and os.name == 'nt': - fullpath = os.path.join(root, filename.decode(encoding)) - else: - fullpath = os.path.join(root, filename).decode(encoding) - enc_paths.append(fullpath) + full_path = os.path.join(root, filename) + enc_paths.append(full_path) # Lets remove any dupes since mediainfo is rather slow. - seen = set() - seen_add = seen.add - return [f for f in enc_paths if not (f in seen or seen_add(f))] + unique_paths = dict.fromkeys(enc_paths) + return list(unique_paths) -def todict(obj, classkey=None): +def to_dict( + obj: typing.Any, + classkey: typing.Optional[typing.Type] = None +) -> typing.Union[str, dict, list]: """Transform an object to dict.""" - if isinstance(obj, string_types): + if isinstance(obj, str): return obj elif isinstance(obj, dict): data = {} for (k, v) in obj.items(): - data[k] = todict(v, classkey) + data[k] = to_dict(v, classkey) return data elif hasattr(obj, '_ast'): - return todict(obj._ast()) + return to_dict(obj._ast()) elif hasattr(obj, '__iter__'): - return [todict(v, classkey) for v in obj] + return [to_dict(v, classkey) for v in obj] elif hasattr(obj, '__dict__'): - values = [(key, todict(value, classkey)) + values = [(key, to_dict(value, classkey)) for key, value in obj.__dict__.items() if not callable(value) and not key.startswith('_')] - data = OrderedDict([(k, v) for k, v in values if v is not None]) + data = {k: v for k, v in values if v is not None} if classkey is not None and hasattr(obj, '__class__'): data[classkey] = obj.__class__.__name__ return data return obj -def detect_os(): +def detect_os() -> OS_FAMILY: """Detect os family: windows, macos or unix.""" if os.name in ('nt', 'dos', 'os2', 'ce'): return 'windows' if sys.platform == 'darwin': return 'macos' - return 'unix' -def define_candidate(locations, names, os_family=None, suggested_path=None): - """Generate candidate list for the given parameters.""" +def define_candidate( + locations: OPTION_MAP, + names: OPTION_MAP, + os_family: typing.Optional[OS_FAMILY] = None, + suggested_path: typing.Optional[str] = None, +) -> typing.Generator[str, None, None]: + """Select family-specific options and generate possible candidates.""" os_family = os_family or detect_os() - for location in (suggested_path, ) + locations[os_family]: + family_names = names[os_family] + all_locations = (suggested_path, ) + locations[os_family] + yield from build_candidates(all_locations, family_names) + + +def build_candidates( + locations: typing.Iterable[typing.Optional[str]], + names: typing.Iterable[str], +) -> typing.Generator[str, None, None]: + """Build candidate names.""" + for location in locations: if not location: continue - if location == '__PATH__': - for name in names[os_family]: - yield name + yield from build_path_candidates(names) elif os.path.isfile(location): yield location elif os.path.isdir(location): - for name in names[os_family]: + for name in names: cmd = os.path.join(location, name) if os.path.isfile(cmd): yield cmd + + +def build_path_candidates( + names: typing.Iterable[str], + os_family: typing.Optional[OS_FAMILY] = None, +) -> typing.Generator[str, None, None]: + """Build candidate names on environment PATH.""" + os_family = os_family or detect_os() + if os_family != 'windows': + yield from names + else: + paths = os.environ['PATH'].split(';') + yield from ( + os.path.join(path, name) + for path in paths + for name in names + ) + + +def round_decimal(value: Decimal, min_digits=0, max_digits: typing.Optional[int] = None): + exponent = value.normalize().as_tuple().exponent + if exponent >= 0: + return round(value, min_digits) + + decimal_places = abs(exponent) + if decimal_places <= min_digits: + return round(value, min_digits) + if max_digits: + return round(value, min(max_digits, decimal_places)) + return value diff --git a/ext/pymediainfo/__init__.py b/ext/pymediainfo/__init__.py new file mode 100644 index 0000000000..9c186798b3 --- /dev/null +++ b/ext/pymediainfo/__init__.py @@ -0,0 +1,528 @@ +# vim: set fileencoding=utf-8 : +""" +This module is a wrapper around the MediaInfo library. +""" +import ctypes +import json +import os +import pathlib +import re +import sys +import warnings +import xml.etree.ElementTree as ET +from typing import Any, Dict, List, Optional, Tuple, Union + +try: + from importlib import metadata +except ImportError: + import importlib_metadata as metadata # type: ignore + +try: + __version__ = metadata.version("pymediainfo") +except metadata.PackageNotFoundError: + __version__ = "" + + +class Track: + """ + An object associated with a media file track. + + Each :class:`Track` attribute corresponds to attributes parsed from MediaInfo's output. + All attributes are lower case. Attributes that are present several times such as `Duration` + yield a second attribute starting with `other_` which is a list of all alternative + attribute values. + + When a non-existing attribute is accessed, `None` is returned. + + Example: + + >>> t = mi.tracks[0] + >>> t + + >>> t.duration + 3000 + >>> t.other_duration + ['3 s 0 ms', '3 s 0 ms', '3 s 0 ms', + '00:00:03.000', '00:00:03.000'] + >>> type(t.non_existing) + NoneType + + All available attributes can be obtained by calling :func:`to_data`. + """ + + def __eq__(self, other): # type: ignore + return self.__dict__ == other.__dict__ + + def __getattribute__(self, name): # type: ignore + try: + return object.__getattribute__(self, name) + except AttributeError: + pass + return None + + def __getstate__(self): # type: ignore + return self.__dict__ + + def __setstate__(self, state): # type: ignore + self.__dict__ = state + + def __init__(self, xml_dom_fragment: ET.Element): + self.track_type = xml_dom_fragment.attrib["type"] + repeated_attributes = [] + for elem in xml_dom_fragment: + node_name = elem.tag.lower().strip().strip("_") + if node_name == "id": + node_name = "track_id" + node_value = elem.text + if getattr(self, node_name) is None: + setattr(self, node_name, node_value) + else: + other_node_name = f"other_{node_name}" + repeated_attributes.append((node_name, other_node_name)) + if getattr(self, other_node_name) is None: + setattr(self, other_node_name, [node_value]) + else: + getattr(self, other_node_name).append(node_value) + + for primary_key, other_key in repeated_attributes: + try: + # Attempt to convert the main value to int + # Usually, if an attribute is repeated, one of its value + # is an int and others are human-readable formats + setattr(self, primary_key, int(getattr(self, primary_key))) + except ValueError: + # If it fails, try to find a secondary value + # that is an int and swap it with the main value + for other_value in getattr(self, other_key): + try: + current = getattr(self, primary_key) + # Set the main value to an int + setattr(self, primary_key, int(other_value)) + # Append its previous value to other values + getattr(self, other_key).append(current) + break + except ValueError: + pass + + def __repr__(self): # type: ignore + return "".format(self.track_id, self.track_type) + + def to_data(self) -> Dict[str, Any]: + """ + Returns a dict representation of the track attributes. + + Example: + + >>> sorted(track.to_data().keys())[:3] + ['codec', 'codec_extensions_usually_used', 'codec_url'] + >>> t.to_data()["file_size"] + 5988 + + + :rtype: dict + """ + return self.__dict__ + + +class MediaInfo: + """ + An object containing information about a media file. + + + :class:`MediaInfo` objects can be created by directly calling code from + libmediainfo (in this case, the library must be present on the system): + + >>> pymediainfo.MediaInfo.parse("/path/to/file.mp4") + + Alternatively, objects may be created from MediaInfo's XML output. + Such output can be obtained using the ``XML`` output format on versions older than v17.10 + and the ``OLDXML`` format on newer versions. + + Using such an XML file, we can create a :class:`MediaInfo` object: + + >>> with open("output.xml") as f: + ... mi = pymediainfo.MediaInfo(f.read()) + + :param str xml: XML output obtained from MediaInfo. + :param str encoding_errors: option to pass to :func:`str.encode`'s `errors` + parameter before parsing `xml`. + :raises xml.etree.ElementTree.ParseError: if passed invalid XML. + :var tracks: A list of :py:class:`Track` objects which the media file contains. + For instance: + + >>> mi = pymediainfo.MediaInfo.parse("/path/to/file.mp4") + >>> for t in mi.tracks: + ... print(t) + + + """ + + def __eq__(self, other): # type: ignore + return self.tracks == other.tracks + + def __init__(self, xml: str, encoding_errors: str = "strict"): + xml_dom = ET.fromstring(xml.encode("utf-8", encoding_errors)) + self.tracks = [] + # This is the case for libmediainfo < 18.03 + # https://github.com/sbraz/pymediainfo/issues/57 + # https://github.com/MediaArea/MediaInfoLib/commit/575a9a32e6960ea34adb3bc982c64edfa06e95eb + if xml_dom.tag == "File": + xpath = "track" + else: + xpath = "File/track" + for xml_track in xml_dom.iterfind(xpath): + self.tracks.append(Track(xml_track)) + + def _tracks(self, track_type: str) -> List[Track]: + return [track for track in self.tracks if track.track_type == track_type] + + @property + def general_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``General``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("General") + + @property + def video_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Video``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Video") + + @property + def audio_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Audio``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Audio") + + @property + def text_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Text``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Text") + + @property + def other_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Other``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Other") + + @property + def image_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Image``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Image") + + @property + def menu_tracks(self) -> List[Track]: + """ + :return: All :class:`Track`\\s of type ``Menu``. + :rtype: list of :class:`Track`\\s + """ + return self._tracks("Menu") + + @staticmethod + def _normalize_filename(filename: Any) -> Any: + if hasattr(os, "PathLike") and isinstance(filename, os.PathLike): + return os.fspath(filename) + if pathlib is not None and isinstance(filename, pathlib.PurePath): + return str(filename) + return filename + + @classmethod + def _define_library_prototypes(cls, lib: Any) -> Any: + lib.MediaInfo_Inform.restype = ctypes.c_wchar_p + lib.MediaInfo_New.argtypes = [] + lib.MediaInfo_New.restype = ctypes.c_void_p + lib.MediaInfo_Option.argtypes = [ + ctypes.c_void_p, + ctypes.c_wchar_p, + ctypes.c_wchar_p, + ] + lib.MediaInfo_Option.restype = ctypes.c_wchar_p + lib.MediaInfo_Inform.argtypes = [ctypes.c_void_p, ctypes.c_size_t] + lib.MediaInfo_Inform.restype = ctypes.c_wchar_p + lib.MediaInfo_Open.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p] + lib.MediaInfo_Open.restype = ctypes.c_size_t + lib.MediaInfo_Open_Buffer_Init.argtypes = [ + ctypes.c_void_p, + ctypes.c_uint64, + ctypes.c_uint64, + ] + lib.MediaInfo_Open_Buffer_Init.restype = ctypes.c_size_t + lib.MediaInfo_Open_Buffer_Continue.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_size_t, + ] + lib.MediaInfo_Open_Buffer_Continue.restype = ctypes.c_size_t + lib.MediaInfo_Open_Buffer_Continue_GoTo_Get.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Open_Buffer_Continue_GoTo_Get.restype = ctypes.c_uint64 + lib.MediaInfo_Open_Buffer_Finalize.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Open_Buffer_Finalize.restype = ctypes.c_size_t + lib.MediaInfo_Delete.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Delete.restype = None + lib.MediaInfo_Close.argtypes = [ctypes.c_void_p] + lib.MediaInfo_Close.restype = None + + @staticmethod + def _get_library_paths(os_is_nt: bool) -> Tuple[str]: + if os_is_nt: + library_paths = ("MediaInfo.dll",) + elif sys.platform == "darwin": + library_paths = ("libmediainfo.0.dylib", "libmediainfo.dylib") + else: + library_paths = ("libmediainfo.so.0",) + script_dir = os.path.dirname(__file__) + # Look for the library file in the script folder + for library in library_paths: + absolute_library_path = os.path.join(script_dir, library) + if os.path.isfile(absolute_library_path): + # If we find it, don't try any other filename + library_paths = (absolute_library_path,) + break + return library_paths + + @classmethod + def _get_library( + cls, + library_file: Optional[str] = None, + ) -> Tuple[Any, Any, str, Tuple[int, ...]]: + os_is_nt = os.name in ("nt", "dos", "os2", "ce") + if os_is_nt: + lib_type = ctypes.WinDLL # type: ignore + else: + lib_type = ctypes.CDLL + if library_file is None: + library_paths = cls._get_library_paths(os_is_nt) + else: + library_paths = (library_file,) + exceptions = [] + for library_path in library_paths: + try: + lib = lib_type(library_path) + cls._define_library_prototypes(lib) + # Without a handle, there might be problems when using concurrent threads + # https://github.com/sbraz/pymediainfo/issues/76#issuecomment-574759621 + handle = lib.MediaInfo_New() + version = lib.MediaInfo_Option(handle, "Info_Version", "") + match = re.search(r"^MediaInfoLib - v(\S+)", version) + if match: + lib_version_str = match.group(1) + lib_version = tuple(int(_) for _ in lib_version_str.split(".")) + else: + raise RuntimeError("Could not determine library version") + return (lib, handle, lib_version_str, lib_version) + except OSError as exc: + exceptions.append(str(exc)) + raise OSError( + "Failed to load library from {} - {}".format( + ", ".join(library_paths), ", ".join(exceptions) + ) + ) + + @classmethod + def can_parse(cls, library_file: Optional[str] = None) -> bool: + """ + Checks whether media files can be analyzed using libmediainfo. + + :param str library_file: path to the libmediainfo library, this should only be used if + the library cannot be auto-detected. + :rtype: bool + """ + try: + lib, handle = cls._get_library(library_file)[:2] + lib.MediaInfo_Close(handle) + lib.MediaInfo_Delete(handle) + return True + except Exception: # pylint: disable=broad-except + return False + + @classmethod + def parse( + # pylint: disable=too-many-statements + # pylint: disable=too-many-branches, too-many-locals, too-many-arguments + cls, + filename: Any, + library_file: Optional[str] = None, + cover_data: bool = False, + encoding_errors: str = "strict", + parse_speed: float = 0.5, + full: bool = True, + legacy_stream_display: bool = False, + mediainfo_options: Optional[Dict[str, str]] = None, + output: Optional[str] = None, + ) -> Union[str, "MediaInfo"]: + """ + Analyze a media file using libmediainfo. + + .. note:: + Because of the way the underlying library works, this method should not + be called simultaneously from multiple threads *with different arguments*. + Doing so will cause inconsistencies or failures by changing + library options that are shared across threads. + + :param filename: path to the media file or file-like object which will be analyzed. + A URL can also be used if libmediainfo was compiled + with CURL support. + :param str library_file: path to the libmediainfo library, this should only be used if + the library cannot be auto-detected. + :param bool cover_data: whether to retrieve cover data as base64. + :param str encoding_errors: option to pass to :func:`str.encode`'s `errors` + parameter before parsing MediaInfo's XML output. + :param float parse_speed: passed to the library as `ParseSpeed`, + this option takes values between 0 and 1. + A higher value will yield more precise results in some cases + but will also increase parsing time. + :param bool full: display additional tags, including computer-readable values + for sizes and durations. + :param bool legacy_stream_display: display additional information about streams. + :param dict mediainfo_options: additional options that will be passed to the + `MediaInfo_Option` function, for example: ``{"Language": "raw"}``. + Do not use this parameter when running the method simultaneously from multiple threads, + it will trigger a reset of all options which will cause inconsistencies or failures. + :param str output: custom output format for MediaInfo, corresponds to the CLI's + ``--Output`` parameter. Setting this causes the method to + return a `str` instead of a :class:`MediaInfo` object. + + Useful values include: + * the empty `str` ``""`` (corresponds to the default + text output, obtained when running ``mediainfo`` with no + additional parameters) + + * ``"XML"`` + + * ``"JSON"`` + + * ``%``-delimited templates (see ``mediainfo --Info-Parameters``) + :type filename: str or pathlib.Path or os.PathLike or file-like object. + :rtype: str if `output` is set. + :rtype: :class:`MediaInfo` otherwise. + :raises FileNotFoundError: if passed a non-existent file. + :raises ValueError: if passed a file-like object opened in text mode. + :raises OSError: if the library file could not be loaded. + :raises RuntimeError: if parsing fails, this should not + happen unless libmediainfo itself fails. + + Examples: + >>> pymediainfo.MediaInfo.parse("tests/data/sample.mkv") + + + >>> import json + >>> mi = pymediainfo.MediaInfo.parse("tests/data/sample.mkv", + ... output="JSON") + >>> json.loads(mi)["media"]["track"][0] + {'@type': 'General', 'TextCount': '1', 'FileExtension': 'mkv', + 'FileSize': '5904', … } + + + """ + lib, handle, lib_version_str, lib_version = cls._get_library(library_file) + # The XML option was renamed starting with version 17.10 + if lib_version >= (17, 10): + xml_option = "OLDXML" + else: + xml_option = "XML" + # Cover_Data is not extracted by default since version 18.03 + # See https://github.com/MediaArea/MediaInfoLib/commit/d8fd88a1 + if lib_version >= (18, 3): + lib.MediaInfo_Option(handle, "Cover_Data", "base64" if cover_data else "") + lib.MediaInfo_Option(handle, "CharSet", "UTF-8") + lib.MediaInfo_Option(handle, "Inform", xml_option if output is None else output) + lib.MediaInfo_Option(handle, "Complete", "1" if full else "") + lib.MediaInfo_Option(handle, "ParseSpeed", str(parse_speed)) + lib.MediaInfo_Option(handle, "LegacyStreamDisplay", "1" if legacy_stream_display else "") + if mediainfo_options is not None: + if lib_version < (19, 9): + warnings.warn( + "This version of MediaInfo (v{}) does not support resetting all " + "options to their default values, passing it custom options is not recommended " + "and may result in unpredictable behavior, see " + "https://github.com/MediaArea/MediaInfoLib/issues/1128".format(lib_version_str), + RuntimeWarning, + ) + for option_name, option_value in mediainfo_options.items(): + lib.MediaInfo_Option(handle, option_name, option_value) + try: + filename.seek(0, 2) + file_size = filename.tell() + filename.seek(0) + except AttributeError: # filename is not a file-like object + file_size = None + + if file_size is not None: # We have a file-like object, use the buffer protocol: + # Some file-like objects do not have a mode + if "b" not in getattr(filename, "mode", "b"): + raise ValueError("File should be opened in binary mode") + lib.MediaInfo_Open_Buffer_Init(handle, file_size, 0) + while True: + buffer = filename.read(64 * 1024) + if buffer: + # https://github.com/MediaArea/MediaInfoLib/blob/v20.09/Source/MediaInfo/File__Analyze.h#L1429 + # 4th bit = finished + if lib.MediaInfo_Open_Buffer_Continue(handle, buffer, len(buffer)) & 0x08: + break + # Ask MediaInfo if we need to seek + seek = lib.MediaInfo_Open_Buffer_Continue_GoTo_Get(handle) + # https://github.com/MediaArea/MediaInfoLib/blob/v20.09/Source/MediaInfoDLL/MediaInfoJNI.cpp#L127 + if seek != ctypes.c_uint64(-1).value: + filename.seek(seek) + # Inform MediaInfo we have sought + lib.MediaInfo_Open_Buffer_Init(handle, file_size, filename.tell()) + else: + break + lib.MediaInfo_Open_Buffer_Finalize(handle) + else: # We have a filename, simply pass it: + filename = cls._normalize_filename(filename) + # If an error occured + if lib.MediaInfo_Open(handle, filename) == 0: + lib.MediaInfo_Close(handle) + lib.MediaInfo_Delete(handle) + # If filename doesn't look like a URL and doesn't exist + if "://" not in filename and not os.path.exists(filename): + raise FileNotFoundError(filename) + # We ran into another kind of error + raise RuntimeError( + "An error occured while opening {}" " with libmediainfo".format(filename) + ) + info: str = lib.MediaInfo_Inform(handle, 0) + # Reset all options to their defaults so that they aren't + # retained when the parse method is called several times + # https://github.com/MediaArea/MediaInfoLib/issues/1128 + # Do not call it when it is not required because it breaks threads + # https://github.com/sbraz/pymediainfo/issues/76#issuecomment-575245093 + if mediainfo_options is not None and lib_version >= (19, 9): + lib.MediaInfo_Option(handle, "Reset", "") + # Delete the handle + lib.MediaInfo_Close(handle) + lib.MediaInfo_Delete(handle) + if output is None: + return cls(info, encoding_errors) + return info + + def to_data(self) -> Dict[str, Any]: + """ + Returns a dict representation of the object's :py:class:`Tracks `. + + :rtype: dict + """ + return {"tracks": [_.to_data() for _ in self.tracks]} + + def to_json(self) -> str: + """ + Returns a JSON representation of the object's :py:class:`Tracks `. + + :rtype: str + """ + return json.dumps(self.to_data()) diff --git a/ext/readme.md b/ext/readme.md index eae18b1caf..a0a3d44fc3 100644 --- a/ext/readme.md +++ b/ext/readme.md @@ -28,7 +28,8 @@ ext | **`idna`** | [2.8](https://pypi.org/project/idna/2.8/) | `requests` | - ext | **`imdbpie`** | [5.6.4](https://pypi.org/project/imdbpie/5.6.4/) | **`medusa`** | - ext | `importlib-resources` | [5.4.0](https://pypi.org/project/importlib-resources/5.4.0/) | `guessit` | Module: `importlib_resources` ext | `jsonrpclib-pelix` | [0.4.2](https://pypi.org/project/jsonrpclib-pelix/0.4.2/) | **`medusa`** | Module: `jsonrpclib` / Used by guessit on python version < 3.9` -ext | **`knowit`** | [eea9ac1](https://github.com/ratoaq2/knowit/tree/eea9ac18e38c930230cf81b5dca4a9af9fb10d4e) | **`medusa`** | - +ext | **`knowit`** | [eea9ac1](https://github.com/ratoaq2/knowit/tree/0.4.0) | **`medusa`** | - +ext | **`pymediainfo`** | [5.1.0](https://pypi.org/project/pymediainfo/5.1.0/) | `knowit` | - ext | `Mako` | [1.1.4](https://pypi.org/project/Mako/1.1.4/) | **`medusa`** | Module: `mako` ext | `markdown2` | [2.4.2](https://pypi.org/project/markdown2/2.4.2/) | **`medusa`** | File: `markdown2.py` ext | `MarkupSafe` | [1.1.1](https://pypi.org/project/MarkupSafe/1.1.1/) | `Mako` | Module: `markupsafe` diff --git a/lib/pymediainfo/__init__.py b/lib/pymediainfo/__init__.py deleted file mode 100644 index 3299096ece..0000000000 --- a/lib/pymediainfo/__init__.py +++ /dev/null @@ -1,316 +0,0 @@ -# vim: set fileencoding=utf-8 : -import os -import re -import locale -import json -import sys -from pkg_resources import get_distribution, DistributionNotFound -import xml.etree.ElementTree as ET -from ctypes import * - -try: - import pathlib -except ImportError: - pathlib = None - -if sys.version_info < (3,): - import urlparse -else: - import urllib.parse as urlparse - -try: - __version__ = get_distribution("pymediainfo").version -except DistributionNotFound: - __version__ = '3.2.1' - pass - -class Track(object): - """ - An object associated with a media file track. - - Each :class:`Track` attribute corresponds to attributes parsed from MediaInfo's output. - All attributes are lower case. Attributes that are present several times such as Duration - yield a second attribute starting with `other_` which is a list of all alternative attribute values. - - When a non-existing attribute is accessed, `None` is returned. - - Example: - - >>> t = mi.tracks[0] - >>> t - - >>> t.duration - 3000 - >>> t.to_data()["other_duration"] - ['3 s 0 ms', '3 s 0 ms', '3 s 0 ms', - '00:00:03.000', '00:00:03.000'] - >>> type(t.non_existing) - NoneType - - All available attributes can be obtained by calling :func:`to_data`. - """ - def __getattribute__(self, name): - try: - return object.__getattribute__(self, name) - except: - pass - return None - def __init__(self, xml_dom_fragment): - self.xml_dom_fragment = xml_dom_fragment - self.track_type = xml_dom_fragment.attrib['type'] - for el in self.xml_dom_fragment: - node_name = el.tag.lower().strip().strip('_') - if node_name == 'id': - node_name = 'track_id' - node_value = el.text - other_node_name = "other_%s" % node_name - if getattr(self, node_name) is None: - setattr(self, node_name, node_value) - else: - if getattr(self, other_node_name) is None: - setattr(self, other_node_name, [node_value, ]) - else: - getattr(self, other_node_name).append(node_value) - - for o in [d for d in self.__dict__.keys() if d.startswith('other_')]: - try: - primary = o.replace('other_', '') - setattr(self, primary, int(getattr(self, primary))) - except: - for v in getattr(self, o): - try: - current = getattr(self, primary) - setattr(self, primary, int(v)) - getattr(self, o).append(current) - break - except: - pass - def __repr__(self): - return("".format(self.track_id, self.track_type)) - def to_data(self): - """ - Returns a dict representation of the track attributes. - - Example: - - >>> sorted(track.to_data().keys())[:3] - ['codec', 'codec_extensions_usually_used', 'codec_url'] - >>> t.to_data()["file_size"] - 5988 - - - :rtype: dict - """ - data = {} - for k, v in self.__dict__.items(): - if k != 'xml_dom_fragment': - data[k] = v - return data - - -class MediaInfo(object): - """ - An object containing information about a media file. - - - :class:`MediaInfo` objects can be created by directly calling code from - libmediainfo (in this case, the library must be present on the system): - - >>> pymediainfo.MediaInfo.parse("/path/to/file.mp4") - - Alternatively, objects may be created from MediaInfo's XML output. - Such output can be obtained using the ``XML`` output format on versions older than v17.10 - and the ``OLDXML`` format on newer versions. - - Using such an XML file, we can create a :class:`MediaInfo` object: - - >>> with open("output.xml") as f: - ... mi = pymediainfo.MediaInfo(f.read()) - - :param str xml: XML output obtained from MediaInfo. - :param str encoding_errors: option to pass to :func:`str.encode`'s `errors` - parameter before parsing `xml`. - :raises xml.etree.ElementTree.ParseError: if passed invalid XML (Python ≥ 2.7). - :raises xml.parsers.expat.ExpatError: if passed invalid XML (Python 2.6). - """ - def __init__(self, xml, encoding_errors="strict"): - self.xml_dom = MediaInfo._parse_xml_data_into_dom(xml, encoding_errors) - @staticmethod - def _parse_xml_data_into_dom(xml_data, encoding_errors="strict"): - return ET.fromstring(xml_data.encode("utf-8", encoding_errors)) - @staticmethod - def _get_library(library_file=None): - os_is_nt = os.name in ("nt", "dos", "os2", "ce") - if os_is_nt: - lib_type = WinDLL - else: - lib_type = CDLL - if library_file is None: - if os_is_nt: - library_names = ("MediaInfo.dll",) - elif sys.platform == "darwin": - library_names = ("libmediainfo.0.dylib", "libmediainfo.dylib") - else: - library_names = ("libmediainfo.so.0",) - script_dir = os.path.dirname(__file__) - # Look for the library file in the script folder - for library in library_names: - lib_path = os.path.join(script_dir, library) - if os.path.isfile(lib_path): - # If we find it, don't try any other filename - library_names = (lib_path,) - break - else: - library_names = (library_file,) - for i, library in enumerate(library_names, start=1): - try: - return lib_type(library) - except OSError: - # If we've tried all possible filenames - if i == len(library_names): - raise - @classmethod - def can_parse(cls, library_file=None): - """ - Checks whether media files can be analyzed using libmediainfo. - - :rtype: bool - """ - try: - cls._get_library(library_file) - return True - except: - return False - @classmethod - def parse(cls, filename, library_file=None, cover_data=False, - encoding_errors="strict", parse_speed=0.5): - """ - Analyze a media file using libmediainfo. - If libmediainfo is located in a non-standard location, the `library_file` parameter can be used: - - >>> pymediainfo.MediaInfo.parse("tests/data/sample.mkv", - ... library_file="/path/to/libmediainfo.dylib") - - :param filename: path to the media file which will be analyzed. - A URL can also be used if libmediainfo was compiled - with CURL support. - :param str library_file: path to the libmediainfo library, this should only be used if the library cannot be auto-detected. - :param bool cover_data: whether to retrieve cover data as base64. - :param str encoding_errors: option to pass to :func:`str.encode`'s `errors` - parameter before parsing MediaInfo's XML output. - :param float parse_speed: passed to the library as `ParseSpeed`, - this option takes values between 0 and 1. - A higher value will yield more precise results in some cases - but will also increase parsing time. - :type filename: str or pathlib.Path - :rtype: MediaInfo - :raises FileNotFoundError: if passed a non-existent file - (Python ≥ 3.3), does not work on Windows. - :raises IOError: if passed a non-existent file (Python < 3.3), - does not work on Windows. - :raises RuntimeError: if parsing fails, this should not - happen unless libmediainfo itself fails. - """ - lib = cls._get_library(library_file) - if pathlib is not None and isinstance(filename, pathlib.PurePath): - filename = str(filename) - url = False - else: - url = urlparse.urlparse(filename) - # Try to open the file (if it's not a URL) - # Doesn't work on Windows because paths are URLs - if not (url and url.scheme): - # Test whether the file is readable - with open(filename, "rb"): - pass - # Define arguments and return types - lib.MediaInfo_Inform.restype = c_wchar_p - lib.MediaInfo_New.argtypes = [] - lib.MediaInfo_New.restype = c_void_p - lib.MediaInfo_Option.argtypes = [c_void_p, c_wchar_p, c_wchar_p] - lib.MediaInfo_Option.restype = c_wchar_p - lib.MediaInfo_Inform.argtypes = [c_void_p, c_size_t] - lib.MediaInfo_Inform.restype = c_wchar_p - lib.MediaInfo_Open.argtypes = [c_void_p, c_wchar_p] - lib.MediaInfo_Open.restype = c_size_t - lib.MediaInfo_Delete.argtypes = [c_void_p] - lib.MediaInfo_Delete.restype = None - lib.MediaInfo_Close.argtypes = [c_void_p] - lib.MediaInfo_Close.restype = None - # Obtain the library version - lib_version = lib.MediaInfo_Option(None, "Info_Version", "") - lib_version = tuple(int(_) for _ in re.search("^MediaInfoLib - v(\\S+)", lib_version).group(1).split(".")) - # The XML option was renamed starting with version 17.10 - if lib_version >= (17, 10): - xml_option = "OLDXML" - else: - xml_option = "XML" - # Cover_Data is not extracted by default since version 18.03 - # See https://github.com/MediaArea/MediaInfoLib/commit/d8fd88a1c282d1c09388c55ee0b46029e7330690 - if cover_data and lib_version >= (18, 3): - lib.MediaInfo_Option(None, "Cover_Data", "base64") - # Create a MediaInfo handle - handle = lib.MediaInfo_New() - lib.MediaInfo_Option(handle, "CharSet", "UTF-8") - # Fix for https://github.com/sbraz/pymediainfo/issues/22 - # Python 2 does not change LC_CTYPE - # at startup: https://bugs.python.org/issue6203 - if (sys.version_info < (3,) and os.name == "posix" - and locale.getlocale() == (None, None)): - locale.setlocale(locale.LC_CTYPE, locale.getdefaultlocale()) - lib.MediaInfo_Option(None, "Inform", xml_option) - lib.MediaInfo_Option(None, "Complete", "1") - lib.MediaInfo_Option(None, "ParseSpeed", str(parse_speed)) - if lib.MediaInfo_Open(handle, filename) == 0: - raise RuntimeError("An eror occured while opening {0}" - " with libmediainfo".format(filename)) - xml = lib.MediaInfo_Inform(handle, 0) - # Delete the handle - lib.MediaInfo_Close(handle) - lib.MediaInfo_Delete(handle) - return cls(xml, encoding_errors) - def _populate_tracks(self): - self._tracks = [] - iterator = "findall" if sys.version_info < (2, 7) else "iterfind" - # This is the case for libmediainfo < 18.03 - # https://github.com/sbraz/pymediainfo/issues/57 - # https://github.com/MediaArea/MediaInfoLib/commit/575a9a32e6960ea34adb3bc982c64edfa06e95eb - if self.xml_dom.tag == "File": - xpath = "track" - else: - xpath = "File/track" - for xml_track in getattr(self.xml_dom, iterator)(xpath): - self._tracks.append(Track(xml_track)) - @property - def tracks(self): - """ - A list of :py:class:`Track` objects which the media file contains. - - For instance: - - >>> mi = pymediainfo.MediaInfo.parse("/path/to/file.mp4") - >>> for t in mi.tracks: - ... print(t) - - - """ - if not hasattr(self, "_tracks"): - self._populate_tracks() - return self._tracks - def to_data(self): - """ - Returns a dict representation of the object's :py:class:`Tracks `. - - :rtype: dict - """ - data = {'tracks': []} - for track in self.tracks: - data['tracks'].append(track.to_data()) - return data - def to_json(self): - """ - Returns a JSON representation of the object's :py:class:`Tracks `. - - :rtype: str - """ - return json.dumps(self.to_data()) diff --git a/medusa/__main__.py b/medusa/__main__.py index 2087550a62..48eb27dddc 100755 --- a/medusa/__main__.py +++ b/medusa/__main__.py @@ -520,8 +520,14 @@ def initialize(self, console_logging=True): # set current commit branch from environment variable, if needed # use this to inject the branch information on immutable installations (e.g. Docker containers) commit_branch_env = os.environ.get('MEDUSA_COMMIT_BRANCH') + if commit_branch_env and app.CUR_COMMIT_BRANCH != commit_branch_env: app.CUR_COMMIT_BRANCH = commit_branch_env + # Overwrite the current branch for non-git installations like docker. + app.BRANCH = commit_branch_env + + # Asume we only have these environ variables when building a docker container. + app.RUNS_IN_DOCKER = CheckVersion.runs_in_docker() app.ACTUAL_CACHE_DIR = check_setting_str(app.CFG, 'General', 'cache_dir', 'cache') @@ -582,6 +588,7 @@ def initialize(self, console_logging=True): app.PROXY_INDEXERS = bool(check_setting_int(app.CFG, 'General', 'proxy_indexers', 1)) app.PROXY_CLIENTS = bool(check_setting_int(app.CFG, 'General', 'proxy_clients', 1)) app.PROXY_OTHERS = bool(check_setting_int(app.CFG, 'General', 'proxy_others', 1)) + app.XEM_URL = check_setting_str(app.CFG, 'General', 'xem_url', 'https://thexem.info') # attempt to help prevent users from breaking links by using a bad url if not app.ANON_REDIRECT.endswith('?'): @@ -1683,6 +1690,7 @@ def save_config(): new_config['General']['proxy_indexers'] = int(app.PROXY_INDEXERS) new_config['General']['proxy_clients'] = int(app.PROXY_CLIENTS) new_config['General']['proxy_others'] = int(app.PROXY_OTHERS) + new_config['General']['xem_url'] = app.XEM_URL new_config['General']['use_listview'] = int(app.USE_LISTVIEW) new_config['General']['metadata_kodi'] = app.METADATA_KODI diff --git a/medusa/app.py b/medusa/app.py index 82ba545c60..7a311b2522 100644 --- a/medusa/app.py +++ b/medusa/app.py @@ -63,6 +63,7 @@ def __init__(self): self.SUBTITLES_URL = '{0}/wiki/Subtitle%20Scripts'.format(self.APPLICATION_URL) self.RARBG_APPID = 'medusa' self.SECURE_TOKEN = 'medusa_user' + self.XEM_URL = 'https://thexem.info' # static configuration self.LOCALE = None, None diff --git a/medusa/common.py b/medusa/common.py index 1cff15164d..2a66c635ca 100644 --- a/medusa/common.py +++ b/medusa/common.py @@ -39,7 +39,7 @@ log.logger.addHandler(logging.NullHandler()) INSTANCE_ID = text_type(uuid.uuid1()) -VERSION = '0.5.29' +VERSION = '1.0.0' USER_AGENT = 'Medusa/{version} ({system}; {release}; {instance})'.format( version=VERSION, system=platform.system(), release=platform.release(), diff --git a/medusa/failed_processor.py b/medusa/failed_processor.py index e44e9a0f4b..419063bef2 100644 --- a/medusa/failed_processor.py +++ b/medusa/failed_processor.py @@ -42,7 +42,7 @@ def _process_release_name(self): raise FailedPostProcessingFailedException() try: - parse_result = NameParser().parse(release_name) + parse_result = NameParser().parse(release_name, use_cache=False) except (InvalidNameException, InvalidShowException): self.log(logger.WARNING, 'Not enough information to parse release name into a valid show. ' 'Consider adding scene exceptions or improve naming for: {release}'.format diff --git a/medusa/helper/exceptions.py b/medusa/helper/exceptions.py index 213e223ef7..2397b08a56 100644 --- a/medusa/helper/exceptions.py +++ b/medusa/helper/exceptions.py @@ -86,6 +86,14 @@ class EpisodePostProcessingFailedException(ApplicationException): """ +class EpisodePostProcessingAbortException(ApplicationException): + """ + The episode post-processing aborted. + + Meaning we should not fail the postprocess and atempt to snatch a new one. + """ + + class FailedPostProcessingFailedException(ApplicationException): """ The failed post-processing failed diff --git a/medusa/helpers/externals.py b/medusa/helpers/externals.py index a3c01ec117..4570efaa8a 100644 --- a/medusa/helpers/externals.py +++ b/medusa/helpers/externals.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import logging +from json.decoder import JSONDecodeError from medusa import app, db from medusa.indexers.api import indexerApi @@ -45,7 +46,7 @@ def get_trakt_externals(externals): try: result = sync.search_by_id(externals[external_key], id_type=trakt_mapping[external_key], media_type='show') - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Error getting external key {external}, error: {error!r}', { 'external': trakt_mapping[external_key], 'error': error }) diff --git a/medusa/helpers/trakt.py b/medusa/helpers/trakt.py index 64734192b3..d66950e123 100644 --- a/medusa/helpers/trakt.py +++ b/medusa/helpers/trakt.py @@ -1,6 +1,7 @@ """Module with Trakt helper methods.""" import logging +from json.decoder import JSONDecodeError from medusa.helpers import get_title_without_year from medusa.indexers.imdb.api import ImdbIdentifier @@ -22,7 +23,7 @@ def get_trakt_user(): user = users.get_user_settings() username = user['user']['username'] return users.User(username) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Unable to get trakt user, error: {error}', {'error': error}) raise @@ -59,7 +60,7 @@ def get_trakt_show_collection(trakt_list, limit=None): return [tv_episode.show_data for tv_episode in calendar_items] return tv.anticipated_shows(limit=limit, extended='full,images') - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Unable to get trakt list {trakt_list}: {error!r}', {'trakt_list': trakt_list, 'error': error}) diff --git a/medusa/indexers/imdb/api.py b/medusa/indexers/imdb/api.py index 815d662c7b..7bf7e57d20 100644 --- a/medusa/indexers/imdb/api.py +++ b/medusa/indexers/imdb/api.py @@ -64,6 +64,10 @@ def imdb_id(self): @imdb_id.setter def imdb_id(self, value): """Set imdb id.""" + if value is None or value == '': + self._imdb_id = self.series_id = None + return + if isinstance(value, string_types) and 'tt' in value: self._imdb_id = self._clean(value) self.series_id = int(self._imdb_id.split('tt')[-1]) @@ -270,13 +274,16 @@ def _get_show_by_id(self, imdb_id): # pylint: disable=unused-argument if companies.get('distribution'): origins = self.imdb_api.get_title_versions(ImdbIdentifier(imdb_id).imdb_id)['origins'][0] released_in_regions = [ - dist for dist in companies['distribution'] if dist.get('regions') and origins in dist['regions'] + dist for dist in companies['distribution'] + if dist.get('regions') and origins in dist['regions'] and dist['isOriginalAiring'] and dist['startYear'] ] - # Used item.get('startYear') because a startYear is not always available. - first_release = sorted(released_in_regions, key=lambda x: x.get('startYear')) - if first_release: - mapped_results['network'] = first_release[0]['company']['name'] + if released_in_regions: + # Used item.get('startYear') because a startYear is not always available. + first_release = sorted(released_in_regions, key=lambda x: x.get('startYear')) + + if first_release: + mapped_results['network'] = first_release[0]['company']['name'] except (AttributeError, LookupError, RequestException): log.info('No company data available for {0}, cant get a network', imdb_id) diff --git a/medusa/indexers/tvmaze/api.py b/medusa/indexers/tvmaze/api.py index 7277cff5af..ae925c5088 100644 --- a/medusa/indexers/tvmaze/api.py +++ b/medusa/indexers/tvmaze/api.py @@ -219,6 +219,11 @@ def _get_show_by_id(self, tvmaze_id, request_language='en'): # pylint: disable= return mapped_results = self._map_results(results, self.series_map) + + # Sanitize the airs_time. As for some shows this might be missing. + if not mapped_results.get('airs_time'): + mapped_results['airs_time'] = '0:00AM' + return OrderedDict({'series': mapped_results}) def _get_episodes(self, tvmaze_id, specials=False, *args, **kwargs): # pylint: disable=unused-argument diff --git a/medusa/metadata/generic.py b/medusa/metadata/generic.py index 08991ee594..ccf3acfeb5 100644 --- a/medusa/metadata/generic.py +++ b/medusa/metadata/generic.py @@ -795,9 +795,14 @@ def _retrieve_show_image(self, image_type, show_obj, episode=None, which=None): else: image_url = re.sub('posters', '_cache/posters', indexer_show_obj['poster']) - if not image_url: + if not image_url and show_obj.indexer != INDEXER_TMDB and show_obj.externals.get('tmdb_id'): # Try and get images from TMDB image_url = self._retrieve_show_images_from_tmdb(show_obj, image_type) + if not image_url: + log.info( + 'Could not find any {type} images on TMDB for {series}', + {'type': image_type, 'series': show_obj.name} + ) elif image_type == 'banner_thumb': if getattr(indexer_show_obj, 'banner', None): @@ -809,9 +814,14 @@ def _retrieve_show_image(self, image_type, show_obj, episode=None, which=None): if getattr(indexer_show_obj, image_type, None): image_url = indexer_show_obj[image_type] - if not image_url and show_obj.indexer != INDEXER_TMDB: + if not image_url and show_obj.indexer != INDEXER_TMDB and show_obj.externals.get('tmdb_id'): # Try and get images from TMDB image_url = self._retrieve_show_images_from_tmdb(show_obj, image_type) + if not image_url: + log.info( + 'Could not find any {type} images on TMDB for {series}', + {'type': image_type, 'series': show_obj.name} + ) if image_url: image_data = get_image(image_url, which) @@ -1039,6 +1049,9 @@ def _retrieve_show_images_from_tmdb(show, img_type): 'poster_thumb': 'poster_path', 'banner_thumb': None} + if not types[img_type]: + return + # get TMDB configuration info tmdb.API_KEY = app.TMDB_API_KEY config = tmdb.Configuration() @@ -1058,20 +1071,19 @@ def size_str_to_int(x): max_size = max(sizes, key=size_str_to_int) try: - search = tmdb.Search() - for show_name in show.get_all_possible_names(): - for result in search.collection(query=show_name)['results'] + search.tv(query=show_name)['results']: - if types[img_type] and result.get(types[img_type]): - return '{0}{1}{2}'.format(base_url, max_size, result[types[img_type]]) + result = tmdb.TV(show.externals.get('tmdb_id')).info() + if not result[types[img_type]]: + return + if img_type == 'poster_thumb': + return '{0}{1}{2}'.format( + base_url, 'w780' if 'w780' in response['images']['poster_sizes'] else max_size, result[types[img_type]] + ) + + return '{0}{1}{2}'.format(base_url, max_size, result[types[img_type]]) except Exception: pass - log.info( - 'Could not find any {type} images on TMDB for {series}', - {'type': img_type, 'series': show.name} - ) - def to_json(self): """Return JSON representation.""" data = {} diff --git a/medusa/metadata/plex.py b/medusa/metadata/plex.py index 270742ad4f..2dd0f14ab2 100644 --- a/medusa/metadata/plex.py +++ b/medusa/metadata/plex.py @@ -91,7 +91,7 @@ def _show_data(self, show_obj): continue if (indexer_slug == 'imdb'): - external_id = ImdbIdentifier(show_id).imdb_id + external_id = ImdbIdentifier(external_id).imdb_id externals[f'{indexer_slug}id'] = str(external_id) @@ -114,31 +114,31 @@ def write_show_file(self, show_obj): if not data: return False - flexmatch_file_path = self.get_show_file_path(show_obj) - flexmatch_file_dir = os.path.dirname(flexmatch_file_path) + plexmatch_file_path = self.get_show_file_path(show_obj) + plexmatch_file_dir = os.path.dirname(plexmatch_file_path) try: - if not os.path.isdir(flexmatch_file_dir): + if not os.path.isdir(plexmatch_file_dir): log.debug( 'Metadata directory did not exist, creating it at {location}', - {'location': flexmatch_file_dir} + {'location': plexmatch_file_dir} ) - os.makedirs(flexmatch_file_dir) - helpers.chmod_as_parent(flexmatch_file_dir) + os.makedirs(plexmatch_file_dir) + helpers.chmod_as_parent(plexmatch_file_dir) log.debug( - 'Writing show flexmatch file to {location}', - {'location': flexmatch_file_dir} + 'Writing show plexmatch file to {location}', + {'location': plexmatch_file_dir} ) - flexmatch_file = io.open(flexmatch_file_path, 'wb') - flexmatch_file.write(data.encode('utf-8')) - flexmatch_file.close() - helpers.chmod_as_parent(flexmatch_file_path) + plexmatch_file = io.open(plexmatch_file_path, 'wb') + plexmatch_file.write(data.encode('utf-8')) + plexmatch_file.close() + helpers.chmod_as_parent(plexmatch_file_path) except IOError as error: log.error( 'Unable to write file to {location} - are you sure the folder is writable? {error}', - {'location': flexmatch_file_path, 'error': error} + {'location': plexmatch_file_path, 'error': error} ) return False @@ -190,11 +190,11 @@ def write_ep_file(self, ep_obj): :param ep_obj: Episode object for which to create the metadata """ - # Parse existing .flexmatch data - flexmatch_file_path = self.get_show_file_path(ep_obj.series) - flexmatch_file_dir = os.path.dirname(flexmatch_file_path) + # Parse existing .plexmatch data + plexmatch_file_path = self.get_show_file_path(ep_obj.series) + plexmatch_file_dir = os.path.dirname(plexmatch_file_path) - with open(flexmatch_file_path) as f: + with open(plexmatch_file_path) as f: current_content = f.readlines() data = self._ep_data(current_content, ep_obj) @@ -202,26 +202,26 @@ def write_ep_file(self, ep_obj): if not data: return False - if not (flexmatch_file_path and flexmatch_file_dir): - log.debug('Unable to write episode flexmatch file because episode location is missing.') + if not (plexmatch_file_path and plexmatch_file_dir): + log.debug('Unable to write episode plexmatch file because episode location is missing.') return False try: - if not os.path.isdir(flexmatch_file_dir): + if not os.path.isdir(plexmatch_file_dir): log.debug('Metadata directory missing, creating it at {location}', - {'location': flexmatch_file_dir}) - os.makedirs(flexmatch_file_dir) - helpers.chmod_as_parent(flexmatch_file_dir) + {'location': plexmatch_file_dir}) + os.makedirs(plexmatch_file_dir) + helpers.chmod_as_parent(plexmatch_file_dir) - log.debug('Writing episode flexmatch file to {location}', - {'location': flexmatch_file_path}) + log.debug('Writing episode plexmatch file to {location}', + {'location': plexmatch_file_path}) - with open(flexmatch_file_path, 'w') as outfile: + with open(plexmatch_file_path, 'w') as outfile: outfile.write('\n'.join(data)) - helpers.chmod_as_parent(flexmatch_file_path) + helpers.chmod_as_parent(plexmatch_file_path) except IOError: - log.error('Unable to write file to {location}', {'location': flexmatch_file_path}) + log.error('Unable to write file to {location}', {'location': plexmatch_file_path}) return False return True @@ -253,6 +253,10 @@ def retrieveShowMetadata(self, folder): """Disable retrieve show by metadata.""" return None, None, None + def update_show_indexer_metadata(self, show_obj): + """Disable update show metadata.""" + return + # present a standard "interface" from the module metadata_class = PlexMetadata diff --git a/medusa/name_parser/guessit_parser.py b/medusa/name_parser/guessit_parser.py index 7e1e20afc9..074a2c273a 100644 --- a/medusa/name_parser/guessit_parser.py +++ b/medusa/name_parser/guessit_parser.py @@ -59,16 +59,18 @@ 'gb', ] -series_re = re.compile(r'^(?P.*?)(?: \(?(?:(?P\d{4})|(?P[A-Z]{2}))\)?)?$') +series_re = re.compile(r'^(?P.*?)(?: ?(?:(?P\(\d{4}\))|(?P[A-Z]{2}))?)?$') -def guessit(name, options=None): +def guessit(name, options=None, cached=True): """Guess the episode information from a given release name. :param name: the release name :type name: str :param options: :type options: dict + :param cached: + :type cached: Boolean :return: the guessed properties :rtype: dict """ @@ -81,10 +83,14 @@ def guessit(name, options=None): allowed_languages=allowed_languages, allowed_countries=allowed_countries)) - result = guessit_cache.get_or_invalidate(name, final_options) + result = None + if cached: + result = guessit_cache.get_or_invalidate(name, final_options) if not result: + log.debug('New guessit parse for item {name}', {'name': name}) result = default_api.guessit(name, options=final_options) - guessit_cache.add(name, result) + # We don't want to cache at this point. As this is a bare guessit result. + # Meaning we haven't been able to calculate any season scene exception at this point. result['parsing_time'] = time() - start_time return result @@ -113,9 +119,11 @@ def get_expected_titles(show_list): if not match: continue - if not any(char.isdigit() or char == '-' for char in exception): + if not any(char.isdigit() or char == '-' for char in match.group(1)): continue + # At this point only add titles (without the year ex: 9-1-1 (2018), + # only use `9-1-1`. And only when it has a number or '-' in its title. expected_titles.append(exception) return expected_titles @@ -135,7 +143,7 @@ def get_or_invalidate(self, name, obj): self.invalidation_object = obj if self.invalidation_object == obj: - log.debug('Using guessit cache item for {name}', {'name': name}) + log.debug('Trying guessit cache for item {name}', {'name': name}) return self.get(name) log.debug('GuessIt cache was cleared due to invalidation object change: previous={previous} new={new}', diff --git a/medusa/name_parser/parser.py b/medusa/name_parser/parser.py index b296c0cdfe..22707f3faa 100644 --- a/medusa/name_parser/parser.py +++ b/medusa/name_parser/parser.py @@ -425,13 +425,15 @@ def erase_cached_parse(indexer, indexer_id): """Remove all names from given indexer and indexer_id.""" name_parser_cache.remove_by_indexer(indexer, indexer_id) - def parse(self, name, cache_result=True): + def parse(self, name, cache_result=True, use_cache=True): """Parse the name into a ParseResult. :param name: :type name: str :param cache_result: :type cache_result: bool + :param use_cache: + :type use_cache: bool :return: :rtype: ParseResult """ @@ -440,9 +442,10 @@ def parse(self, name, cache_result=True): if self.naming_pattern: cache_result = False - cached = name_parser_cache.get(name) - if cached: - return cached + if use_cache: + cached = name_parser_cache.get(name) + if cached: + return cached start_time = time.time() result = self._parse_string(name) @@ -453,8 +456,10 @@ def parse(self, name, cache_result=True): if cache_result: name_parser_cache.add(name, result) + log.debug('Parsed {name} into {result} and added to cache', {'name': name, 'result': result}) + else: + log.debug('Parsed {name} into {result}', {'name': name, 'result': result}) - log.debug('Parsed {name} into {result}', {'name': name, 'result': result}) return result @staticmethod @@ -469,8 +474,8 @@ def assert_supported(result): 'Parser result: {result}'.format(result=result)) log.debug( - 'Matched release {release} to a series in your database: {name}', - {'release': result.original_name, 'name': result.series.name} + 'Matched release {release} to a series in your database: {name} using guessit title: {title}', + {'release': result.original_name, 'name': result.series.name, 'title': result.guess.get('title')} ) if result.season_number is None and not result.episode_numbers and \ @@ -596,6 +601,14 @@ def __str__(self): total_time=self.total_time)) return helpers.canonical_name(obj, fmt='{key}: {value}', separator=', ') + def to_dict(self): + """Return an dict representation.""" + return OrderedDict(self.guess, **dict(season=self.season_number, + episode=self.episode_numbers, + absolute_episode=self.ab_episode_numbers, + quality=common.Quality.qualityStrings[self.quality], + total_time=self.total_time)) + # Python 2 compatibility __unicode__ = __str__ diff --git a/medusa/name_parser/rules/rules.py b/medusa/name_parser/rules/rules.py index ab38c7aa9f..bc0e37320e 100644 --- a/medusa/name_parser/rules/rules.py +++ b/medusa/name_parser/rules/rules.py @@ -771,6 +771,108 @@ def when(self, matches, context): return to_remove, to_append +class AnimeWithMultipleSeasons(Rule): + """Add season to title and remove episode for specific anime patterns. + + There are animes where the title contains the format of `Title Season 2 - 01` + If the title can be found in expected titles, we can asume that the value after the dash (-) + is an episode. + + Medusa rule: + - The title is found in expected titles. + - The value after the dash, is the episode. + + e.g.: [Tsundere-Raws] Tate no Yuusha no Nariagari Season 2 - 03 VOSTFR (CR) [WEB 720p x264 AAC].mkv + + guessit -t episode "[Tsundere-Raws] Tate no Yuusha no Nariagari Season 2 - 03 VOSTFR (CR) [WEB 720p x264 AAC].mkv" + + without this rule: + For: [Tsundere-Raws] Tate no Yuusha no Nariagari Season 2 - 03.mkv + GuessIt found: { + "release_group": "Tsundere-Raws", + "title": "Tate no Yuusha no Nariagari", + "season": [ + 2, + 3 + ], + "container": "mkv", + "mimetype": "video/x-matroska", + "type": "episode" + } + + with this rule: + For: [Tsundere-Raws] Tate no Yuusha no Nariagari Season 2 - 03.mkv + GuessIt found: { + "release_group": "Tsundere-Raws", + "title": "Tate no Yuusha no Nariagari Season 2", + "episode": 3, + "container": "mkv", + "mimetype": "video/x-matroska", + "type": "episode" + } + """ + + priority = POST_PROCESS + consequence = [RemoveMatch, AppendMatch] + ends_with_digit = re.compile(r'(_|\W)\d+$') + + def when(self, matches, context): + """Evaluate the rule. + + :param matches: + :type matches: rebulk.match.Matches + :param context: + :type context: dict + :return: + """ + is_anime = context.get('show_type') == 'anime' or matches.tagged('anime') + if not is_anime: + return + + titles = matches.named('title') + if not titles: + return + + episodes = matches.named('episode') + if episodes: + return + + seasons = matches.named('season') + if not seasons or len(seasons) > 1: + return + + initiator_value = seasons[0].initiator.value + if '-' not in initiator_value: + return + + if initiator_value.split('-')[0].strip() not in titles[0].value: + return + + to_remove = [] + to_append = [] + + fileparts = matches.markers.named('path') + for filepart in marker_sorted(fileparts, matches): + seasons = sorted(matches.range(filepart.start, filepart.end, + predicate=lambda match: match.name == 'season')) + + title = matches.previous(seasons[0], index=-1, + predicate=lambda match: match.name == 'title' and match.end <= filepart.end) + + if not title or not self.ends_with_digit.search(str(title.value)): + continue + + season = seasons[0] + new_episode = copy.copy(season) + new_episode.name = 'episode' + new_episode.value = season.value + + to_remove.append(season) + to_append.append(new_episode) + + return to_remove, to_append + + class OnePreGroupAsMultiEpisode(Rule): """Remove last episode (one) and add the first episode as absolute. @@ -1797,6 +1899,7 @@ def rules(): FixInvalidAbsoluteReleaseGroups, AnimeWithSeasonAbsoluteEpisodeNumbers, AnimeWithSeasonMultipleEpisodeNumbers, + AnimeWithMultipleSeasons, AnimeAbsoluteEpisodeNumbers, AbsoluteEpisodeNumbers, AbsoluteEpisodeWithX26Y, diff --git a/medusa/notifiers/trakt.py b/medusa/notifiers/trakt.py index 7b6b0eda0a..f1dbe1ea82 100644 --- a/medusa/notifiers/trakt.py +++ b/medusa/notifiers/trakt.py @@ -5,6 +5,7 @@ import logging from builtins import object +from json.decoder import JSONDecodeError from medusa import app from medusa.helpers import get_title_without_year @@ -83,7 +84,7 @@ def update_library(ep_obj): # update library sync.add_to_collection(data) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Unable to update Trakt: {error!r}', {'error': error}) @staticmethod @@ -96,7 +97,7 @@ def update_watchlist_show(show_obj, remove=False): result = sync.remove_from_watchlist(trakt_media_object) else: result = sync.add_to_watchlist(trakt_media_object) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Unable to update Trakt: {error!r}', {'error': error}) return False @@ -120,7 +121,7 @@ def update_watchlist_episode(show_obj, episodes, remove=False): result = sync.remove_from_watchlist({'shows': [create_episode_structure(show_obj, episodes)]}) else: result = sync.add_to_watchlist({'shows': [create_episode_structure(show_obj, episodes)]}) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Unable to update Trakt watchlist: {error!r}', {'error': error}) return False @@ -142,7 +143,7 @@ def add_episode_to_watchlist(episode): try: tv_episode = tv.TVEpisode(show_id, episode.season, episode.episode) tv_episode.add_to_watchlist() - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Unable to add episode to watchlist: {error!r}', {'error': error}) @staticmethod @@ -167,6 +168,6 @@ def test_notify(blacklist_name=None): return "Trakt blacklist doesn't exists" else: return 'Test notice sent successfully to Trakt' - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning('Unable to test TRAKT: {error!r}', {'error': error}) return 'Test notice failed to Trakt: {0!r}'.format(error) diff --git a/medusa/post_processor.py b/medusa/post_processor.py index 9451f95574..206d74b5d1 100644 --- a/medusa/post_processor.py +++ b/medusa/post_processor.py @@ -40,6 +40,7 @@ notifiers, ) from medusa.common import ( + ARCHIVED, DOWNLOADED, Quality, SNATCHED, @@ -53,6 +54,7 @@ ) from medusa.helper.exceptions import ( EpisodeNotFoundException, + EpisodePostProcessingAbortException, EpisodePostProcessingFailedException, ShowDirectoryNotFoundException, ) @@ -120,7 +122,7 @@ def __init__(self, file_path, nzb_name=None, process_method=None, is_priority=No self.is_priority = is_priority self._output = [] self.version = None - self.anidbEpisode = None + self.anidb_episode = None self.manually_searched = False self.info_hash = None self.item_resources = OrderedDict([('file name', self.file_name), @@ -562,12 +564,12 @@ def _add_to_anidb_mylist(self, file_path): :param file_path: file to add to mylist """ if set_up_anidb_connection(): - if not self.anidbEpisode: # seems like we could parse the name before, now lets build the anidb object - self.anidbEpisode = self._build_anidb_episode(app.ADBA_CONNECTION, file_path) + if not self.anidb_episode: # seems like we could parse the name before, now lets build the anidb object + self.anidb_episode = self._build_anidb_episode(app.ADBA_CONNECTION, file_path) self.log(u'Adding the file to the anidb mylist', logger.DEBUG) try: - self.anidbEpisode.add_to_mylist(state=1) # state = 1 sets the state of the file to "internal HDD" + self.anidb_episode.add_to_mylist(state=1) # state = 1 sets the state of the file to "internal HDD" except Exception as e: self.log(u'Exception message: {0!r}'.format(e)) @@ -694,7 +696,7 @@ def _analyze_name(self, name): # parse the name to break it into show, season, episodes, quality and version try: - parse_result = NameParser().parse(name) + parse_result = NameParser().parse(name, use_cache=False) except (InvalidNameException, InvalidShowException) as error: self.log(u'{0}'.format(error), logger.DEBUG) return to_return @@ -705,7 +707,7 @@ def _analyze_name(self, name): not parse_result.is_anime]): try: parse_result.series.erase_cached_parse() - parse_result = NameParser(parse_method='anime').parse(name) + parse_result = NameParser(parse_method='anime').parse(name, use_cache=False) except (InvalidNameException, InvalidShowException) as error: self.log(u'{0}'.format(error), logger.DEBUG) return to_return @@ -735,9 +737,10 @@ def _finalize(self, parse_result): self.is_proper = bool(parse_result.proper_tags) # if the result is complete set release name - if parse_result.series_name and ((parse_result.season_number is not None and parse_result.episode_numbers) or - parse_result.air_date) and parse_result.release_group: - + if parse_result.series_name and ( + (parse_result.season_number is not None and parse_result.episode_numbers) + or parse_result.air_date + ) and parse_result.release_group: if not self.release_name: self.release_name = remove_extension(os.path.basename(parse_result.original_name)) @@ -1058,7 +1061,7 @@ def process(self): self.in_history = False # reset the anidb episode object - self.anidbEpisode = None + self.anidb_episode = None # try to find the file info (series_obj, season, episodes, quality, version) = self._find_info() @@ -1101,15 +1104,21 @@ def process(self): self.log(u'Episode has a version in it, using that: v{0}'.format(version), logger.DEBUG) new_ep_version = version + # Exempted from manual snatched downloads. If the destination episode is archived abort postprocessing. + if not self.manually_searched and ep_obj.status == ARCHIVED: + raise EpisodePostProcessingAbortException( + 'Destination episode has a status of Archived. Abort postprocessing.' + ) + # check for an existing file - existing_file_status = self._compare_file_size(ep_obj.location) + existing_file_size_comparison = self._compare_file_size(ep_obj.location) if not priority_download: - if existing_file_status == PostProcessor.EXISTS_SAME: + if existing_file_size_comparison == PostProcessor.EXISTS_SAME: self.log(u'File exists and the new file has the same size, aborting post-processing') return True - if existing_file_status != PostProcessor.DOESNT_EXIST: + if existing_file_size_comparison != PostProcessor.DOESNT_EXIST: if self.is_proper and new_ep_quality == old_ep_quality: self.log(u'New file is a PROPER, marking it safe to replace') self.flag_kodi_clean_library() @@ -1147,7 +1156,7 @@ def process(self): # if the file is priority then we're going to replace it even if it exists else: # Set to clean Kodi if file exists and it is priority_download - if existing_file_status != PostProcessor.DOESNT_EXIST: + if existing_file_size_comparison != PostProcessor.DOESNT_EXIST: self.flag_kodi_clean_library() self.log(u"This download is marked a priority download so I'm going to replace " u'an existing file if I find one') diff --git a/medusa/process_tv.py b/medusa/process_tv.py index 9e80ea8f77..33489e9a13 100644 --- a/medusa/process_tv.py +++ b/medusa/process_tv.py @@ -16,7 +16,12 @@ from medusa.clients import torrent from medusa.common import DOWNLOADED, SNATCHED, SNATCHED_BEST, SNATCHED_PROPER from medusa.helper.common import is_sync_file -from medusa.helper.exceptions import EpisodePostProcessingFailedException, FailedPostProcessingFailedException, ex +from medusa.helper.exceptions import ( + EpisodePostProcessingAbortException, + EpisodePostProcessingFailedException, + FailedPostProcessingFailedException, + ex +) from medusa.logger.adapters.style import CustomBraceAdapter from medusa.name_parser.parser import InvalidNameException, InvalidShowException, NameParser from medusa.queues import generic_queue @@ -263,6 +268,7 @@ def __init__(self, path, process_method=None, failed=False, episodes=[], process self.failed = failed self.resource_name = None self.result = True + self.aborted = False self.succeeded = True self.missed_files = [] self.unwanted_files = [] @@ -877,12 +883,21 @@ def process_media(self, path, video_files, force=False, is_priority=None, ignore processor = None self.result = False process_fail_message = ex(error) + except EpisodePostProcessingAbortException as error: + processor = None + self.result = True + self.aborted = True + process_fail_message = ex(error) if processor: self._output += processor._output if self.result: - self.log_and_output('Processing succeeded for {file_path}', **{'file_path': file_path}) + if not self.aborted: + self.log_and_output('Processing succeeded for {file_path}', **{'file_path': file_path}) + else: + self.log_and_output('Processing aborted for {file_path}: {process_fail_message}', + level=logging.WARNING, **{'file_path': file_path, 'process_fail_message': process_fail_message}) else: self.log_and_output('Processing failed for {file_path}: {process_fail_message}', level=logging.WARNING, **{'file_path': file_path, 'process_fail_message': process_fail_message}) @@ -969,7 +984,7 @@ def subtitles_enabled(*args): continue try: - parse_result = NameParser().parse(name) + parse_result = NameParser().parse(name, use_cache=False) if parse_result.series.indexerid: main_db_con = db.DBConnection() sql_results = main_db_con.select('SELECT subtitles FROM tv_shows WHERE indexer = ? AND indexer_id = ? LIMIT 1', diff --git a/medusa/providers/generic_provider.py b/medusa/providers/generic_provider.py index 2f07dc5639..87ebda169d 100644 --- a/medusa/providers/generic_provider.py +++ b/medusa/providers/generic_provider.py @@ -957,7 +957,8 @@ def to_json(self): 'cookies': { 'enabled': self.enable_cookies, 'values': self.cookies - } + }, + 'url': self.custom_url or self.url if hasattr(self, 'custom_url') else self.url }, 'animeOnly': self.anime_only, 'type': self.provider_type, diff --git a/medusa/providers/torrent/json/eztv.py b/medusa/providers/torrent/json/eztv.py index cc218e19df..ffbe3be33c 100644 --- a/medusa/providers/torrent/json/eztv.py +++ b/medusa/providers/torrent/json/eztv.py @@ -65,7 +65,7 @@ def search(self, search_strings, age=0, ep_obj=None, **kwargs): imdb_id = self.series.externals.get(mappings[10]) if imdb_id: imdb_id = ImdbIdentifier(imdb_id).imdb_id - search_params['imdb_id'] = imdb_id + search_params['imdb_id'] = imdb_id[2:] log.debug('Search string (IMDb ID): {imdb_id}', {'imdb_id': imdb_id}) else: log.debug('IMDb ID not found') diff --git a/medusa/queues/show_queue.py b/medusa/queues/show_queue.py index 3fb89dd4e1..c14ad51eb3 100644 --- a/medusa/queues/show_queue.py +++ b/medusa/queues/show_queue.py @@ -20,6 +20,7 @@ import logging import traceback from builtins import object +from json.decoder import JSONDecodeError from imdbpie.exceptions import ImdbAPIError @@ -1200,7 +1201,7 @@ def run(self): if app.USE_TRAKT: try: app.trakt_checker_scheduler.action.remove_show_trakt_library(self.show) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.warning( '{id}: Unable to delete show {show} from Trakt.' ' Please remove manually otherwise it will be added again.' diff --git a/medusa/scene_exceptions.py b/medusa/scene_exceptions.py index e25bfbf83a..b70f2eb784 100644 --- a/medusa/scene_exceptions.py +++ b/medusa/scene_exceptions.py @@ -18,6 +18,8 @@ from medusa.logger.adapters.style import BraceAdapter from medusa.session.core import MedusaSafeSession +from requests.compat import urljoin + from six import iteritems logger = BraceAdapter(logging.getLogger(__name__)) @@ -346,7 +348,7 @@ def _get_custom_exceptions(force): def _get_xem_exceptions(force): xem_exceptions = defaultdict(dict) - url = 'https://thexem.info/map/allNames' + url = urljoin(app.XEM_URL, '/map/allNames') params = { 'origin': None, 'seasonNumbers': 1, diff --git a/medusa/scene_numbering.py b/medusa/scene_numbering.py index 741bea8b00..c560006e52 100644 --- a/medusa/scene_numbering.py +++ b/medusa/scene_numbering.py @@ -29,13 +29,15 @@ import traceback from builtins import str -from medusa import db, logger +from medusa import app, db, logger from medusa.helper.common import episode_num from medusa.helper.exceptions import ex from medusa.indexers.api import indexerApi from medusa.logger.adapters.style import BraceAdapter from medusa.scene_exceptions import safe_session +from requests.compat import urljoin + from six import viewitems @@ -607,14 +609,22 @@ def xem_refresh(series_obj, force=False): logger.log(u'{0} is an unsupported indexer in XEM'.format(indexerApi(indexer_id).name), logger.DEBUG) return # XEM MAP URL - url = 'https://thexem.info/map/havemap?origin={0}'.format(indexerApi(indexer_id).config['xem_origin']) + url = urljoin(app.XEM_URL, '/map/havemap?origin={0}') + url = url.format(indexerApi(indexer_id).config['xem_origin']) parsed_json = safe_session.get_json(url) - if not parsed_json or 'result' not in parsed_json or 'success' not in parsed_json['result'] or 'data' not in parsed_json or str(series_id) not in parsed_json['data']: + if ( + not parsed_json + or 'result' not in parsed_json + or 'success' not in parsed_json['result'] + or 'data' not in parsed_json + or str(series_id) not in parsed_json['data'] + ): logger.log(u'No XEM data for show ID {0} on {1}'.format(series_id, series_obj.indexer_name), logger.DEBUG) return # XEM API URL - url = 'https://thexem.info/map/all?id={0}&origin={1}&destination=scene'.format(series_id, indexerApi(indexer_id).config['xem_origin']) + url = urljoin(app.XEM_URL, '/map/all?id={0}&origin={1}&destination=scene') + url = url.format(series_id, indexerApi(indexer_id).config['xem_origin']) parsed_json = safe_session.get_json(url) if not parsed_json or 'result' not in parsed_json or 'success' not in parsed_json['result']: logger.log(u'No XEM data for show ID {0} on {1}'.format(indexer_id, series_obj.indexer_name), logger.DEBUG) diff --git a/medusa/schedulers/trakt_checker.py b/medusa/schedulers/trakt_checker.py index fee5d0325b..a5d9ea2c87 100644 --- a/medusa/schedulers/trakt_checker.py +++ b/medusa/schedulers/trakt_checker.py @@ -8,6 +8,7 @@ import time from builtins import object from builtins import str +from json.decoder import JSONDecodeError from medusa import app, db, ui from medusa.common import ARCHIVED, DOWNLOADED, Quality, SKIPPED, SNATCHED, SNATCHED_BEST, SNATCHED_PROPER, WANTED @@ -90,7 +91,7 @@ def run(self, force=False): try: self.sync_watchlist() self.sync_library() - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.exception('Trakt exception while running trakt_checker.\nError: {error}', {'error': error}) self.amActive = False @@ -100,7 +101,7 @@ def find_show(self, indexerid, indexer): trakt_library = [] try: trakt_library = sync.get_collection('shows') - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info('Unable to retrieve shows from Trakt collection. Error: {error!r}', {'error': error}) if not trakt_library: @@ -126,7 +127,7 @@ def remove_show_trakt_library(self, show_obj): # Remove all episodes from the Trakt collection for this show try: self.remove_episode_trakt_collection(filter_show=show_obj) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info("Unable to remove all episodes from show '{show}' from Trakt library. Error: {error!r}", { 'show': show_obj.name, 'error': error @@ -134,7 +135,7 @@ def remove_show_trakt_library(self, show_obj): try: sync.remove_from_collection(create_show_structure(show_obj)) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info("Unable to remove show '{show}' from Trakt library. Error: {error!r}", { 'show': show_obj.name, 'error': error @@ -153,7 +154,7 @@ def add_show_trakt_library(self, show_obj): try: result = sync.add_to_collection(create_show_structure(show_obj)) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info("Unable to add show '{show}' to Trakt library. Error: {error!r}", { 'show': show_obj.name, 'error': error @@ -239,7 +240,7 @@ def remove_episode_trakt_collection(self, filter_show=None): try: sync.remove_from_collection({'shows': media_object_shows}) self._get_show_collection() - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info('Unable to remove episodes from Trakt collection. Error: {error!r}', { 'error': error }) @@ -300,7 +301,7 @@ def add_episode_trakt_collection(self): try: sync.add_to_collection({'shows': media_object_shows}) self._get_show_collection() - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info('Unable to add episodes to Trakt collection. Error: {error!r}', {'error': error}) def sync_watchlist(self): @@ -382,7 +383,7 @@ def remove_episode_watchlist(self): try: sync.remove_from_collection({'shows': media_object_shows}) self._get_episode_watchlist() - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info('Unable to remove episodes from Trakt watchlist. Error: {error!r}', { 'error': error }) @@ -441,7 +442,7 @@ def add_episode_watchlist(self): try: sync.add_to_watchlist({'shows': media_object_shows}) self._get_episode_watchlist() - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info('Unable to add episode to Trakt watchlist. Error: {error!r}', { 'error': error }) @@ -466,7 +467,7 @@ def add_show_watchlist(self): if trakt_show_objects: try: sync.add_to_watchlist({'shows': trakt_show_objects}) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info('Unable to add shows to Trakt watchlist. Error: {error!r}', {'error': error}) self._get_show_watchlist() @@ -491,7 +492,7 @@ def remove_from_library(self): try: trakt_show = tv.TVShow(str(trakt_id or ImdbIdentifier(show.imdb_id).imdb_id)) progress = trakt_show.progress - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info("Unable to check if show '{show}' is ended/completed. Error: {error!r}", { 'show': show.name, 'error': error @@ -726,7 +727,7 @@ def _get_episode_watchlist(self): """Get episodes watchlist.""" try: self.episode_watchlist = sync.get_watchlist('episodes') - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info(u'Unable to retrieve episodes from Trakt watchlist. Error: {error!r}', {'error': error}) return False return True @@ -735,7 +736,7 @@ def _get_show_collection(self): """Get show collection.""" try: self.collection_list = sync.get_collection('shows') - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: log.info('Unable to retrieve shows from Trakt collection. Error: {error!r}', {'error': error}) return False return True diff --git a/medusa/server/api/v2/guessit.py b/medusa/server/api/v2/guessit.py index 1b5d4eb868..f5388e215b 100644 --- a/medusa/server/api/v2/guessit.py +++ b/medusa/server/api/v2/guessit.py @@ -4,10 +4,10 @@ import logging -import guessit - from medusa.logger.adapters.style import CustomBraceAdapter +from medusa.name_parser.guessit_parser import guessit from medusa.name_parser.parser import InvalidNameException, InvalidShowException, NameParser +from medusa.name_parser.rules import default_api from medusa.server.api.v2.base import BaseRequestHandler @@ -53,7 +53,11 @@ def get(self): {'release': release}) result['error'] = str(error) - result['guess'] = guessit.guessit(release) + if parse_result: + result['parse'] = parse_result.to_dict() + else: + result['parse'] = guessit(release, cached=False) + result['vanillaGuessit'] = default_api.guessit(release) result['show'] = show return self._ok(data=dict(result)) diff --git a/medusa/server/api/v2/internal.py b/medusa/server/api/v2/internal.py index 8267b2554d..d8789a11fb 100644 --- a/medusa/server/api/v2/internal.py +++ b/medusa/server/api/v2/internal.py @@ -406,7 +406,7 @@ def resource_get_episode_backlog(self): cur_ep_cat_string = Overview.overviewStrings[cur_ep_cat] ep_cats[episode_string] = cur_ep_cat_string ep_counts[cur_ep_cat_string] += 1 - cur_result['airdate'] = air_date.isoformat('T') + cur_result['airdate'] = air_date.isoformat('T') if air_date else '' cur_result['manuallySearched'] = cur_result['manually_searched'] del cur_result['manually_searched'] cur_result['statusString'] = statusStrings[cur_result['status']] diff --git a/medusa/server/api/v2/providers.py b/medusa/server/api/v2/providers.py index 3a1b373094..b4ac32839b 100644 --- a/medusa/server/api/v2/providers.py +++ b/medusa/server/api/v2/providers.py @@ -352,7 +352,7 @@ def _add_torrentrss_provider(self, data): if not data.get('url'): return self._bad_request('No provider url provided') - new_provider = TorrentRssProvider(data.get('name'), data.get('url'), data.get('cookies', ''), data.get('titleTag', 'title')) + new_provider = TorrentRssProvider(data.get('name'), data.get('url'), data.get('cookies', {}).get('values'), data.get('titleTag', 'title')) new_provider = self.provider_name_auto_numbered(new_provider) app.torrentRssProviderList.append(new_provider) @@ -424,6 +424,12 @@ def ordered_providers(names, providers): @staticmethod def _set_common_settings(provider, config): + if hasattr(provider, 'url'): + try: + provider.url = config['url'] + except (AttributeError, KeyError): + pass + if hasattr(provider, 'username'): try: provider.username = config['username'] diff --git a/medusa/server/api/v2/series_mass_edit.py b/medusa/server/api/v2/series_mass_edit.py index 0cadb3cff7..8ea6cae4cf 100644 --- a/medusa/server/api/v2/series_mass_edit.py +++ b/medusa/server/api/v2/series_mass_edit.py @@ -36,7 +36,7 @@ def post(self): """Perform a mass update action.""" required_options = ( 'paused', 'defaultEpisodeStatus', 'anime', 'sports', 'scene', - 'airByDate', 'seasonFolders', 'subtitles', 'qualities' + 'airByDate', 'seasonFolders', 'subtitles', 'qualities', 'language', 'languageKeep' ) data = json_decode(self.request.body) shows = data.get('shows', []) @@ -66,6 +66,8 @@ def post(self): season_folders = options.get('seasonFolders') subtitles = options.get('subtitles') qualities = options.get('qualities') + language = options.get('language') + language_keep = options.get('languageKeep') for show_slug in shows: identifier = SeriesIdentifier.from_slug(show_slug) @@ -96,6 +98,7 @@ def post(self): new_dvd_order = show_obj.dvd_order if dvd_order is None else dvd_order new_season_folders = show_obj.season_folders if season_folders is None else season_folders new_subtitles = show_obj.subtitles if subtitles is None else subtitles + new_language = show_obj.lang if language_keep else language # If both are false (two empty arrays), use the shows current value. if not qualities['allowed'] and not qualities['preferred']: @@ -112,7 +115,7 @@ def post(self): allowed_qualities=new_quality_allowed, preferred_qualities=new_quality_preferred, season_folders=new_season_folders, paused=new_paused, air_by_date=new_air_by_date, sports=new_sports, dvd_order=new_dvd_order, subtitles=new_subtitles, anime=new_anime, scene=new_scene, - default_ep_status=new_default_ep_status, + default_ep_status=new_default_ep_status, language=new_language ) return self._created(data={'errors': errors}) @@ -120,9 +123,9 @@ def post(self): def mass_edit_show( self, show_obj, location=None, allowed_qualities=None, preferred_qualities=None, season_folders=None, paused=None, air_by_date=None, sports=None, dvd_order=None, subtitles=None, - anime=None, scene=None, default_ep_status=None + anime=None, scene=None, default_ep_status=None, language=None ): - """A variation of the original `editShow`, where `directCall` is always true.""" + """Variation of the original `editShow`, where `directCall` is always true.""" allowed_qualities = allowed_qualities or [] preferred_qualities = preferred_qualities or [] @@ -169,6 +172,7 @@ def mass_edit_show( show_obj.air_by_date = air_by_date show_obj.default_ep_status = int(default_ep_status) show_obj.dvd_order = dvd_order + show_obj.lang = language # if we change location clear the db of episodes, change it, write to db, and rescan old_location = path.normpath(show_obj._location) diff --git a/medusa/server/web/home/add_shows.py b/medusa/server/web/home/add_shows.py index fd3dd661d3..6e76f24bf9 100644 --- a/medusa/server/web/home/add_shows.py +++ b/medusa/server/web/home/add_shows.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging +from json.decoder import JSONDecodeError from medusa import app, ui from medusa.helpers import get_showname_from_indexer @@ -86,7 +87,7 @@ def addShowToBlacklist(self, seriesid): ui.notifications.message('Success!', "Added show '{0}' to blacklist".format(show_name)) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: ui.notifications.error('Error!', "Unable to add show '{0}' to blacklist. Check logs.".format(show_name)) log.warning("Error while adding show '{name}' to trakt blacklist: {error}", diff --git a/medusa/server/web/home/handler.py b/medusa/server/web/home/handler.py index e211a2d7a0..8786a4ec7e 100644 --- a/medusa/server/web/home/handler.py +++ b/medusa/server/web/home/handler.py @@ -5,7 +5,7 @@ import json import os import time - +from json.decoder import JSONDecodeError from medusa import ( app, @@ -353,7 +353,7 @@ def requestTraktDeviceCodeOauth(): logger.log('Start a new Oauth device authentication request. Request is valid for 60 minutes.', logger.INFO) try: app.TRAKT_DEVICE_CODE = trakt.get_device_code(app.TRAKT_API_KEY, app.TRAKT_API_SECRET) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: logger.log('Unable to get trakt device code. Error: {error!r}'.format(error=error), logger.WARNING) return json.dumps({'result': False}) @@ -380,7 +380,7 @@ def checkTrakTokenOauth(): response = trakt.get_device_token( app.TRAKT_DEVICE_CODE.get('device_code'), app.TRAKT_API_KEY, app.TRAKT_API_SECRET, store=True ) - except (TraktException, RequestException) as error: + except (TraktException, RequestException, JSONDecodeError) as error: logger.log('Unable to get trakt device token. Error: {error!r}'.format(error=error), logger.WARNING) return json.dumps({'result': 'Trakt error while retrieving device token', 'error': True}) diff --git a/medusa/subtitle_providers/addic7ed.py b/medusa/subtitle_providers/addic7ed.py index a699c1c0e9..a1c7243b1f 100644 --- a/medusa/subtitle_providers/addic7ed.py +++ b/medusa/subtitle_providers/addic7ed.py @@ -166,7 +166,7 @@ def _search_show_id(self, series, year=None): series_year = '%s %d' % (series, year) if year is not None else series params = {'search': series_year, 'Submit': 'Search'} - r = self.session.get('http://www.addic7ed.com/srch.php', params=params, timeout=10, cookies=self.cookies) + r = self.session.get('http://www.addic7ed.com/srch.php', params=params, timeout=30, cookies=self.cookies) # make the search logger.info('Searching show ids with %r', params) @@ -219,7 +219,7 @@ def get_show_id(self, series, year=None, country_code=None): # search as last resort if not show_id: - logger.warning('Series %s not found in show ids', series) + logger.info('Series %s not found in show ids', series) show_id = self._search_show_id(series) return show_id diff --git a/medusa/tv/series.py b/medusa/tv/series.py index 6999a5b763..82affb9ef9 100644 --- a/medusa/tv/series.py +++ b/medusa/tv/series.py @@ -1631,11 +1631,19 @@ def load_from_indexer(self, tvapi=None, limit_seasons=None): if self.indexer_api.indexer == INDEXER_IMDB: self.externals['imdb_id'] = ImdbIdentifier(getattr(indexed_show, 'id')).series_id - self.imdb_id = ImdbIdentifier(self.externals.get('imdb_id')).imdb_id or getattr(indexed_show, 'imdb_id', '') + self.imdb_id = None + if self.externals.get('imdb_id') or getattr(indexed_show, 'imdb_id', ''): + self.imdb_id = ImdbIdentifier(self.externals.get('imdb_id')).imdb_id or getattr(indexed_show, 'imdb_id', '') if getattr(indexed_show, 'airs_dayofweek', '') and getattr(indexed_show, 'airs_time', ''): self.airs = '{airs_day_of_week} {airs_time}'.format(airs_day_of_week=indexed_show['airs_dayofweek'], airs_time=indexed_show['airs_time']) + else: + log.info( + '{id}: We could not determin a specific `airs_dayofweek` or `airs_time` for the show: {show}' + '\n We might start searching early for episodes.', + {'id': self.series_id, 'show': self.name} + ) self.status = self.normalize_status(getattr(indexed_show, 'status', None)) @@ -2047,9 +2055,9 @@ def add_scene_numbering(self): # should go by scene numbering or indexer numbering. Warn the user. if not self.scene and get_xem_numbering_for_show(self): log.warning( - '{id}: while adding the show {title} we noticed thexem.info has an episode mapping available' + '{id}: while adding the show {title} we noticed {xem_url} has an episode mapping available' '\nyou might want to consider enabling the scene option for this show.', - {'id': self.series_id, 'title': self.name} + {'id': self.series_id, 'title': self.name, 'xem_url': app.XEM_URL} ) ui.notifications.message( 'consider enabling scene for this show', diff --git a/medusa/updater/version_checker.py b/medusa/updater/version_checker.py index 9a189b76e8..31f58d3533 100644 --- a/medusa/updater/version_checker.py +++ b/medusa/updater/version_checker.py @@ -347,19 +347,17 @@ def runs_in_docker(): if app.RUNS_IN_DOCKER is not None: return app.RUNS_IN_DOCKER - path = '/proc/{pid}/cgroup'.format(pid=os.getpid()) try: - if not os.path.isfile(path): - return False - - with open(path) as f: - for line in f: - if re.match(r'\d+:[\w=]+:/docker(-[ce]e)?/\w+', line): - log.debug(u'Running in a docker container') - app.RUNS_IN_DOCKER = True - return True - return False + path = '/.dockerenv' + if os.path.isfile(path): + app.RUNS_IN_DOCKER = True + return True except (EnvironmentError, OSError) as error: log.info(u'Tried to check the path {path} if we are running in a docker container, ' u'but an error occurred: {error}', {'path': path, 'error': error}) - return False + + if os.environ.get('MEDUSA_COMMIT_HASH') and os.environ.get('MEDUSA_COMMIT_BRANCH'): + app.RUNS_IN_DOCKER = True + return True + + return False diff --git a/runscripts/init.docker b/runscripts/init.docker new file mode 100755 index 0000000000..fc1ea1823f --- /dev/null +++ b/runscripts/init.docker @@ -0,0 +1,23 @@ +#!/bin/sh + +PUID=${PUID:-0} +PGID=${PGID:-0} + +if [[ $PUID != "0" ]] && [[ $PGID != "0" ]] +then + addgroup -g "$PGID" abc + adduser -u "$PUID" --ingroup abc --disabled-password abc + + echo " + User uid: $(id -u abc) + User gid: $(id -g abc) + ------------------------------------- + " + chown -R abc:abc /app + chown -R abc:abc /config + + exec su abc -s /bin/sh -c "python start.py --nolaunch --datadir /config" +else + echo "Starting medusa as root" + python start.py --nolaunch --datadir /config +fi diff --git a/tests/conftest.py b/tests/conftest.py index 77c27286ee..647277dcb1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -113,7 +113,7 @@ def tvshow(create_tvshow): @pytest.fixture def parse_method(create_tvshow): - def parse(self, name): + def parse(self, name, use_cache=True): """Parse the string and add a TVShow object with the parsed series name.""" result = self._parse_string(name) result.series = create_tvshow(name=result.series_name) diff --git a/tests/test_guessit.py b/tests/test_guessit.py index 027d8922e2..4cb357d526 100644 --- a/tests/test_guessit.py +++ b/tests/test_guessit.py @@ -48,6 +48,8 @@ def show_list(create_tvshow): create_tvshow(indexerid=17, name='An Anime Show 100', anime=1), create_tvshow(indexerid=17, name='Show! Name 2', anime=1), create_tvshow(indexerid=18, name='24'), # expected titles shouldn't contain numbers + create_tvshow(indexerid=18, name='9-1-1'), # The dash in the title makes it an expected title + create_tvshow(indexerid=18, name='Tate no Yuusha no Nariagari Season 2'), # The number 2 will qualify this as an expected title ] diff --git a/tests/test_guessit.yml b/tests/test_guessit.yml index 37a5c5e1f9..02f4f71720 100644 --- a/tests/test_guessit.yml +++ b/tests/test_guessit.yml @@ -4574,3 +4574,28 @@ audio_channels: '5.1' video_profile: 'Advanced Video Codec High Definition' type: episode + +? /series/9-1-1 (2018)/Season 05/9-1-1 (2018) (S05E06) (2021-11-01) (1080p-h264 BluRay AAC-2ch) Brawl in Cell Block 9-1-1.mkv +: title: '9-1-1' + alias: '9-1-1 2018' + year: 2018 + season: 5 + episode: 6 + screen_size: 1080p + video_codec: H.264 + source: 'Blu-ray' + audio_codec: AAC + audio_channels: '2.0' + release_group: "Brawl in Cell Block 9-1-1" + container: mkv + type: episode + +? "[Tsundere-Raws] Tate no Yuusha no Nariagari Season 2 - 09.mkv" +: options: {show_type: anime} + title: Tate no Yuusha no Nariagari Season 2 + absolute_episode: 9 + episode: 9 + release_group: Tsundere-Raws + container: mkv + mimetype: "video/x-matroska" + type: episode diff --git a/themes-default/slim/src/components/change-indexer.vue b/themes-default/slim/src/components/change-indexer.vue index acccc1e5af..6d135f4256 100644 --- a/themes-default/slim/src/components/change-indexer.vue +++ b/themes-default/slim/src/components/change-indexer.vue @@ -34,6 +34,7 @@ import Vue from 'vue'; import { mapState } from 'vuex'; import { ChangeIndexerRow } from './manage'; +import { sortShows } from '../utils/core'; export default { name: 'change-indexer', @@ -64,6 +65,7 @@ export default { }, computed: { ...mapState({ + layout: state => state.config.layout, shows: state => state.shows.shows, queueitems: state => state.shows.queueitems, client: state => state.auth.client @@ -73,14 +75,18 @@ export default { return filteredShows.filter(show => show.checked); }, filteredShows() { - const { allShows, filter } = this; - return allShows.filter( + const { allShows, filter, layout } = this; + const { sortArticle } = layout; + + const filteredShows = allShows.filter( show => (show.indexer === 'tvdb' && filter.tvdb) || (show.indexer === 'tvmaze' && filter.tvmaze) || (show.indexer === 'tmdb' && filter.tmdb) || (show.indexer === 'imdb' && filter.imdb) ); + + return sortShows(filteredShows, sortArticle); } }, methods: { diff --git a/themes-default/slim/src/components/config-general.vue b/themes-default/slim/src/components/config-general.vue index c6b5a1f444..f5adc3a8d3 100644 --- a/themes-default/slim/src/components/config-general.vue +++ b/themes-default/slim/src/components/config-general.vue @@ -241,6 +241,10 @@ + diff --git a/themes-default/slim/src/components/display-show.vue b/themes-default/slim/src/components/display-show.vue index 0251a42035..2094c8a1ef 100644 --- a/themes-default/slim/src/components/display-show.vue +++ b/themes-default/slim/src/components/display-show.vue @@ -6,12 +6,14 @@ -
@@ -128,19 +130,13 @@
- - - search - + + + search subtitles
@@ -277,19 +273,14 @@
- - - search - + + + + search subtitles
@@ -379,7 +370,7 @@ import debounce from 'lodash/debounce'; import Vue from 'vue'; import { mapState, mapGetters, mapActions } from 'vuex'; -import { AppLink, PlotInfo, SceneNumberInput, SceneNumberAnimeInput } from './helpers'; +import { AppLink, PlotInfo, Search, SceneNumberInput, SceneNumberAnimeInput } from './helpers'; import { humanFileSize } from '../utils/core'; import { manageCookieMixin } from '../mixins/manage-cookie'; import { addQTip } from '../utils/jquery'; @@ -396,6 +387,7 @@ export default { Backstretch, PlotInfo, QualityPill, + Search, SceneNumberInput, SceneNumberAnimeInput, ShowHeader, @@ -794,7 +786,7 @@ export default { }; episodes.forEach(episode => { data.episodes.push(episode.slug); - this.$refs[`search-${episode.slug}`].src = 'images/loading16-dark.gif'; + this.$refs[`search-${episode.slug}`].src = 'images/loading16.gif'; }); } @@ -1019,7 +1011,6 @@ export default { this.initializeEpisodes(true); } } - } }; @@ -1099,6 +1090,7 @@ tablesorter.css color: rgb(0, 0, 0); text-align: left; border-spacing: 0; + border-collapse: initial; } .displayShow >>> .vgt-table th, diff --git a/themes-default/slim/src/components/edit-show.vue b/themes-default/slim/src/components/edit-show.vue index 35bcb1ab5e..9c237c16b8 100644 --- a/themes-default/slim/src/components/edit-show.vue +++ b/themes-default/slim/src/components/edit-show.vue @@ -181,8 +181,8 @@
- + diff --git a/themes-default/slim/src/components/helpers/config-custom-prowlarr.vue b/themes-default/slim/src/components/helpers/config-custom-prowlarr.vue index bc06d200bd..34781d32dd 100644 --- a/themes-default/slim/src/components/helpers/config-custom-prowlarr.vue +++ b/themes-default/slim/src/components/helpers/config-custom-prowlarr.vue @@ -2,8 +2,8 @@
- - + + diff --git a/themes-default/slim/src/components/helpers/config-custom-torrentrss.vue b/themes-default/slim/src/components/helpers/config-custom-torrentrss.vue index de731e3c47..7c84e07a86 100644 --- a/themes-default/slim/src/components/helpers/config-custom-torrentrss.vue +++ b/themes-default/slim/src/components/helpers/config-custom-torrentrss.vue @@ -12,8 +12,8 @@
- - + + @@ -87,7 +87,13 @@ export default { async addProvider() { const { name, url, cookies, searchElement } = this; try { - const response = await this.client.api.post('providers/torrentrss', { name, url, cookies, titleTag: searchElement }); + const cookieValues = { + values: cookies + }; + + const response = await this.client.api.post('providers/torrentrss', { + name, url, cookies: cookieValues, titleTag: searchElement + }); this.$store.commit(ADD_PROVIDER, response.data.result); this.$snotify.success( `Saved provider ${name}`, diff --git a/themes-default/slim/src/components/helpers/config-custom-torznab.vue b/themes-default/slim/src/components/helpers/config-custom-torznab.vue index dd27d79a43..dd97d14edc 100644 --- a/themes-default/slim/src/components/helpers/config-custom-torznab.vue +++ b/themes-default/slim/src/components/helpers/config-custom-torznab.vue @@ -12,7 +12,7 @@
- + @@ -109,7 +109,7 @@ export default { }, async getCategories() { const { currentProvider } = this; - if (!currentProvider.name || !currentProvider.url || !currentProvider.config.apikey) { + if (!currentProvider.name || !currentProvider.config.url || !currentProvider.config.apikey) { return; } @@ -118,7 +118,7 @@ export default { type: 'GETCATEGORIES', apikey: currentProvider.config.apikey, name: currentProvider.name, - url: currentProvider.url + url: currentProvider.config.url }); if (response.data.result.success) { this.availableCategories = response.data.result.categories; diff --git a/themes-default/slim/src/components/helpers/config-provider-nzb.vue b/themes-default/slim/src/components/helpers/config-provider-nzb.vue index 4e2685028f..73fddcc74e 100644 --- a/themes-default/slim/src/components/helpers/config-provider-nzb.vue +++ b/themes-default/slim/src/components/helpers/config-provider-nzb.vue @@ -4,10 +4,9 @@
- diff --git a/themes-default/slim/src/components/helpers/search.vue b/themes-default/slim/src/components/helpers/search.vue index cacb91171f..7312e3aa50 100644 --- a/themes-default/slim/src/components/helpers/search.vue +++ b/themes-default/slim/src/components/helpers/search.vue @@ -2,10 +2,11 @@
state.config.search + stateSearch: state => state.config.search, + client: state => state.auth.client, + queueitems: state => state.queue.queueitems }) }, methods: { @@ -116,13 +121,13 @@ export default { episodes: [episode.slug], options: {} }; - this.$refs[`search-${episode.slug}`].src = 'images/loading16-dark.gif'; + this.src = 'images/loading16-dark.gif'; this.client.api.put(`search/${searchType}`, data) // eslint-disable-line no-undef .then(_ => { - console.info(`started search for show: ${showSlug} episode: ${episode.slug}`); - this.$refs[`search-${episode.slug}`].src = 'images/queued.png'; - this.$refs[`search-${episode.slug}`].disabled = true; + console.info(`Queued search for show: ${showSlug} episode: ${episode.slug}`); + this.src = 'images/queued.png'; + this.disabled = true; }).catch(error => { console.error(String(error)); this.$refs[`search-${episode.slug}`].src = 'images/no16.png'; @@ -138,9 +143,8 @@ export default { */ queueSearch(episode) { const { $modal, search, retryDownload } = this; - const episodeIdentifier = episode.slug; if (episode) { - if (this.$refs[`search-${episodeIdentifier}`].disabled === true) { + if (this.disabled === true) { return; } @@ -167,7 +171,33 @@ export default { beforeFailedSearchModalClose(event) { this.failedSearchEpisodes = event.params.episodes; } + }, + watch: { + queueitems(queueitems) { + const episode = queueitems.filter( + q => q.name === 'BACKLOG' && + q.show && + q.show.id.slug === this.showSlug && + q.segment.find(s => s.slug === this.episode.slug) + ); + + if (episode.length === 0) { + return; + } + const lastEp = episode.slice(-1)[0]; + if (lastEp.inProgress && lastEp.success === null) { + // Search is in progress. + console.info(`Search runnning for show: ${this.showSlug} and episode: ${this.episode.slug}`); + this.src = 'images/loading16.gif'; + this.disabled = true; + } else { + // Search finished. + console.log(`Search finished for ${this.episode.slug}`); + this.src = 'images/yes16.png'; + this.disabled = false; + } + } } }; diff --git a/themes-default/slim/src/components/helpers/show-selector.vue b/themes-default/slim/src/components/helpers/show-selector.vue index 4841c82732..90826956dc 100644 --- a/themes-default/slim/src/components/helpers/show-selector.vue +++ b/themes-default/slim/src/components/helpers/show-selector.vue @@ -23,6 +23,7 @@ \n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./lazy-image.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./lazy-image.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./lazy-image.vue?vue&type=template&id=05ed6915&scoped=true&\"\nimport script from \"./lazy-image.vue?vue&type=script&lang=js&\"\nexport * from \"./lazy-image.vue?vue&type=script&lang=js&\"\nimport style0 from \"./lazy-image.vue?vue&type=style&index=0&id=05ed6915&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 \"05ed6915\",\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',{staticClass:\"app-image\",class:_vm.lazyCls,style:(_vm.style),attrs:{\"data-src\":_vm.lazySrc,\"data-srcset\":_vm.lazySrcset},on:{\"error\":function($event){_vm.error = true}}})}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../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=ad0dcc86&scoped=true&\"\nimport script from \"./asset.vue?vue&type=script&lang=js&\"\nexport * from \"./asset.vue?vue&type=script&lang=js&\"\nimport style0 from \"./asset.vue?vue&type=style&index=0&id=ad0dcc86&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 \"ad0dcc86\",\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.lazy)?_c('div',{staticStyle:{\"display\":\"inherit\"}},[(!_vm.link)?_c('img',_vm._b({on:{\"error\":function($event){_vm.error = true}}},'img',{ src: _vm.src, class: _vm.cls, class: _vm.newCls },false)):_c('app-link',{attrs:{\"href\":_vm.href}},[_c('img',_vm._b({on:{\"error\":function($event){_vm.error = true}}},'img',{ src: _vm.src, class: _vm.newCls, style: _vm.imgStyle },false))])],1):_c('div',{staticStyle:{\"display\":\"inherit\"}},[(!_vm.link)?_c('lazy-image',{attrs:{\"lazy-src\":_vm.src,\"lazy-cls\":_vm.newCls,\"lazy-default-src\":_vm.defaultSrc,\"lazy-width\":_vm.imgWidth}}):_c('app-link',{attrs:{\"href\":_vm.href}},[_c('lazy-image',{attrs:{\"lazy-src\":_vm.src,\"lazy-cls\":_vm.newCls,\"lazy-default-src\":_vm.defaultSrc}})],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"select-list max-width\"},[_c('ul',[_vm._l((_vm.items),function(exception){return _c('li',{key:((exception.title) + \"-\" + (exception.season))},[_c('div',{staticClass:\"input-group form-inline\",attrs:{\"disabled\":!exception.custom}},[_c('input',{staticClass:\"form-control input-sm\",attrs:{\"type\":\"text\",\"disabled\":!exception.custom},domProps:{\"value\":exception.title}}),_vm._v(\" \"),_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(exception.season),expression:\"exception.season\"}],staticClass:\"select-season\",attrs:{\"name\":\"scene-exception-season\",\"disabled\":!exception.custom},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.$set(exception, \"season\", $event.target.multiple ? $$selectedVal : $$selectedVal[0])}}},_vm._l((_vm.availableSeasons),function(season){return _c('option',{key:season.value,domProps:{\"value\":season.value}},[_vm._v(\"\\n \"+_vm._s(season.description)+\"\\n \")])}),0),_vm._v(\" \"),(!exception.custom)?_c('div',{directives:[{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('This exception has been automatically added through an automated process sourcing title aliases from medusa github repo, thexem.de or anidb.info'),expression:\"'This exception has been automatically added through an automated process sourcing title aliases from medusa github repo, thexem.de or anidb.info'\",modifiers:{\"right\":true}}],staticClass:\"external-scene-exception\"},[_vm._m(0,true)]):_c('div',{staticClass:\"input-group-btn\",on:{\"click\":function($event){return _vm.removeException(exception)}}},[_vm._m(1,true)])])])}),_vm._v(\" \"),_c('div',{staticClass:\"new-item\"},[_c('div',{staticClass:\"input-group form-inline\"},[_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('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedSeason),expression:\"selectedSeason\"}],staticClass:\"select-season\",attrs:{\"name\":\"add-exception-season\"},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.selectedSeason=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},_vm._l((_vm.availableSeasons),function(season){return _c('option',{key:season.value,domProps:{\"value\":season.value}},[_vm._v(\"\\n \"+_vm._s(season.description)+\"\\n \")])}),0),_vm._v(\" \"),_c('div',{staticClass:\"input-group-btn\",attrs:{\"disabled\":!_vm.unique},on:{\"click\":function($event){return _vm.addException()}}},[_vm._m(2)])])]),_vm._v(\" \"),(!_vm.unique)?_c('div',[_vm._m(3)]):_vm._e(),_vm._v(\" \"),(_vm.newItem.length > 0 && _vm.unique)?_c('div',{staticClass:\"new-item-help\"},[_vm._v(\"\\n Click \"),_c('i',{staticClass:\"glyphicon glyphicon-plus\"}),_vm._v(\" to add your \"),_c('b',[_vm._v(_vm._s(_vm.selectedSeason === -1 ? 'Show Exception' : 'Season Exception'))]),_vm._v(\".\\n \")]):_vm._e()],2)])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"align-center\"},[_c('img',{attrs:{\"src\":\"images/ico/favicon-16.png\",\"width\":\"16\",\"height\":\"16\",\"alt\":\"search\",\"title\":\"This exception has been automatically added through an automated process sourcing title aliases from medusa github repo, thexem.de or anidb.info\"}})])},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\"}})])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('p',[_c('b',[_vm._v(\"This exception has already been added for this show.\"),_c('br'),_vm._v(\"Can't add the same exception twice!\")])])}]\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-scene-exceptions.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-scene-exceptions.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-scene-exceptions.vue?vue&type=template&id=2cd053b2&scoped=true&\"\nimport script from \"./config-scene-exceptions.vue?vue&type=script&lang=js&\"\nexport * from \"./config-scene-exceptions.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-scene-exceptions.vue?vue&type=style&index=0&id=2cd053b2&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 \"2cd053b2\",\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-subtitle-languages\"}},[_c('vue-tags-input',{attrs:{\"tags\":_vm.wantedLanguages,\"autocomplete-items\":_vm.filteredItems,\"add-only-from-autocomplete\":\"\",\"placeholder\":\"Write to search a language and select it\"},on:{\"tags-changed\":_vm.tagsChanged},scopedSlots:_vm._u([{key:\"autocomplete-item\",fn:function(props){return _c('div',{staticClass:\"autocomplete-item\",on:{\"click\":function($event){return props.performAdd(props.item)}}},[_c('img',{staticStyle:{\"vertical-align\":\"middle !important\"},attrs:{\"src\":(\"images/subtitles/flags/\" + (props.item.text) + \".png\"),\"onError\":\"this.onerror=null; this.src='images/flags/unknown.png';\"}}),_vm._v(\"\\n \"+_vm._s(props.item.name)+\"\\n \")])}},{key:\"tag-left\",fn:function(props){return _c('div',{staticClass:\"country-left\",on:{\"click\":function($event){return props.performOpenEdit(props.index)}}},[_c('img',{staticStyle:{\"vertical-align\":\"middle !important\"},attrs:{\"src\":(\"images/subtitles/flags/\" + (props.tag.text) + \".png\"),\"onError\":\"this.onerror=null; this.src='images/flags/unknown.png';\"}})])}}]),model:{value:(_vm.tag),callback:function ($$v) {_vm.tag=$$v},expression:\"tag\"}})],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-subtitle-languages.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-subtitle-languages.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-subtitle-languages.vue?vue&type=template&id=0fbc8b46&scoped=true&\"\nimport script from \"./config-subtitle-languages.vue?vue&type=script&lang=js&\"\nexport * from \"./config-subtitle-languages.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-subtitle-languages.vue?vue&type=style&index=0&id=0fbc8b46&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 \"0fbc8b46\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-textbox.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-textbox.vue?vue&type=template&id=1f6251e9&scoped=true&\"\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&id=1f6251e9&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 \"1f6251e9\",\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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\",attrs:{\"for\":_vm.id}},[_c('span',[_vm._v(_vm._s(_vm.label))])])]),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},[_c('div',{staticClass:\"parent\",class:_vm.inputClass},[((({id: _vm.id, type: _vm.type, name: _vm.id, 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, placeholder: _vm.placeholder, disabled: _vm.disabled},false)):((({id: _vm.id, type: _vm.type, name: _vm.id, 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, 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, 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, placeholder: _vm.placeholder, disabled: _vm.disabled},false)),_vm._v(\" \"),_c('transition',{attrs:{\"name\":\"uri-error\"}},[(_vm.uriError)?_c('div',{staticClass:\"uri-error\"},[_vm._v(\"Make sure to start your URI with http://, https://, scgi://, etc..\")]):_vm._e()]),_vm._v(\" \"),_vm._t(\"warning\")],2),_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 }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-textbox-number.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-textbox-number.vue?vue&type=template&id=69e4e766&\"\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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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 }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.experimental || _vm.experimentalEnabled)?_c('div',{attrs:{\"id\":\"config-toggle-slider-content\"}},[_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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\"),_vm._v(\" \"),(_vm.experimental)?_c('span',{staticStyle:{\"color\":\"red\"}},[_vm._v(\"This is an experimental feature\")]):_vm._e()],2)])])]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-toggle-slider.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-toggle-slider.vue?vue&type=template&id=4bfe1717&\"\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\":\"custom-newznab\"}},[_c('config-template',{attrs:{\"label-for\":\"select_newznab_provider\",\"label\":\"Select Provider\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedProvider),expression:\"selectedProvider\"}],staticClass:\"form-control input-sm max-input350\",attrs:{\"id\":\"select-provider\"},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.selectedProvider=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[_c('option',{attrs:{\"value\":\"#add\"}},[_vm._v(\"--- add new provider ---\")]),_vm._v(\" \"),_vm._l((_vm.newznabProviderOptions),function(option){return _c('option',{key:option.value,domProps:{\"value\":option.value}},[_vm._v(\"\\n \"+_vm._s(option.text)+\"\\n \")])})],2)]),_vm._v(\" \"),(_vm.currentProvider && _vm.selectedProvider !== '#add')?_c('div',{staticClass:\"edit-provider\"},[_c('config-textbox',{attrs:{\"disabled\":\"\",\"label\":\"Provider name\",\"id\":\"edit_provider_name\"},model:{value:(_vm.currentProvider.name),callback:function ($$v) {_vm.$set(_vm.currentProvider, \"name\", $$v)},expression:\"currentProvider.name\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"disabled\":\"\",\"label\":\"Site Url\",\"id\":\"edit_provider_url\"},model:{value:(_vm.currentProvider.url),callback:function ($$v) {_vm.$set(_vm.currentProvider, \"url\", $$v)},expression:\"currentProvider.url\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"Api key\",\"id\":\"edit_provider_api\"},model:{value:(_vm.currentProvider.config.apikey),callback:function ($$v) {_vm.$set(_vm.currentProvider.config, \"apikey\", $$v)},expression:\"currentProvider.config.apikey\"}}),_vm._v(\" \"),_c('config-template',{attrs:{\"label\":\"Categories\",\"label-for\":\"catids\"}},[_c('multiselect',{attrs:{\"value\":_vm.providerCatIds,\"multiple\":true,\"options\":_vm.availableCategories,\"label\":\"id\",\"track-by\":\"id\",\"taggable\":true,\"tag-placeholder\":\"Add this as new cat id\",\"placeholder\":\"Search or add a cat id\"},on:{\"tag\":_vm.addTag,\"input\":function($event){_vm.currentProvider.config.catIds = $event.map(function (cat) { return cat.id; })}},scopedSlots:_vm._u([{key:\"option\",fn:function(props){return [(props.option.isTag)?_c('span',[_c('strong',[_vm._v(_vm._s(props.option.label))])]):_c('span',[_c('strong',[_vm._v(_vm._s(props.option.id))]),_vm._v(\" (\"+_vm._s(props.option.name)+\")\")])]}}],null,false,2742372380)})],1),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa btn-danger newznab_delete\",attrs:{\"disabled\":_vm.currentProvider.default,\"id\":\"newznab_delete\"},on:{\"click\":_vm.removeProvider}},[_vm._v(\"Delete\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter_refresh\",on:{\"click\":function($event){return _vm.$emit('save')}}},[_vm._v(\"Save Changes\")]),_vm._v(\" \"),(_vm.currentProvider.manager === 'prowlarr')?_c('p',{staticClass:\"manager-note\"},[_c('img',{staticStyle:{\"width\":\"16px\"},attrs:{\"src\":\"images/providers/prowlarr.png\"}}),_vm._v(\"\\n Note! This is a provider configured through the 'Configure Custom Prowlarr Providers' tab.\\n \")]):_vm._e()],1):_vm._e(),_vm._v(\" \"),(_vm.selectedProvider === '#add')?_c('div',{staticClass:\"add-provider\"},[_c('config-textbox',{attrs:{\"label\":\"Provider name\",\"id\":\"add_provider_name\"},scopedSlots:_vm._u([{key:\"warning\",fn:function(){return [_c('transition',{attrs:{\"name\":\"warning\"}},[(!_vm.providerIdAvailable)?_c('div',{staticClass:\"warning\"},[_vm._v(\"This provider id is already used.\")]):_vm._e()])]},proxy:true}],null,false,3405532282),model:{value:(_vm.name),callback:function ($$v) {_vm.name=$$v},expression:\"name\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Site Url\",\"id\":\"add_provider_url\"},model:{value:(_vm.url),callback:function ($$v) {_vm.url=$$v},expression:\"url\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"Api key\",\"id\":\"add_provider_api\"},model:{value:(_vm.apikey),callback:function ($$v) {_vm.apikey=$$v},expression:\"apikey\"}}),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",attrs:{\"disabled\":!_vm.providerIdAvailable},on:{\"click\":_vm.addProvider}},[_vm._v(\"Add Provider\")])],1):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-newznab.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-newznab.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-custom-newznab.vue?vue&type=template&id=5a5b710f&scoped=true&\"\nimport script from \"./config-custom-newznab.vue?vue&type=script&lang=js&\"\nexport * from \"./config-custom-newznab.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-custom-newznab.vue?vue&type=style&index=0&id=5a5b710f&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 \"5a5b710f\",\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\":\"custom-prowlarr\"}},[_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-lg-12\"},[_c('config-textbox',{attrs:{\"label\":\"Prowler Url\",\"id\":\"prowler_url\"},model:{value:(_vm.prowlarr.url),callback:function ($$v) {_vm.$set(_vm.prowlarr, \"url\", $$v)},expression:\"prowlarr.url\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Api Key\",\"id\":\"prowler_apikey\"},model:{value:(_vm.prowlarr.apikey),callback:function ($$v) {_vm.$set(_vm.prowlarr, \"apikey\", $$v)},expression:\"prowlarr.apikey\"}}),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",on:{\"click\":_vm.saveConfig}},[_vm._v(\"Save\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",on:{\"click\":_vm.testConnectivity}},[_vm._v(\"Test\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",on:{\"click\":_vm.getAvailableProviders}},[_vm._v(\"Get Providers\")]),_vm._v(\" \"),_c('span',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.testResult),expression:\"testResult\"}],staticClass:\"testresult\"},[_vm._v(_vm._s(_vm.testResult))])],1)]),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-lg-12 vgt-table-styling\"},[_c('h3',[_vm._v(\"Available providers\")]),_vm._v(\" \"),_c('vue-good-table',{attrs:{\"columns\":_vm.columns,\"rows\":_vm.prowlarrProviders,\"search-options\":{\n enabled: false\n },\"sort-options\":{\n enabled: true,\n initialSortBy: { field: 'name', type: 'asc' }\n },\"styleClass\":\"vgt-table condensed\"},scopedSlots:_vm._u([{key:\"table-row\",fn:function(props){return [(props.column.label === 'Added')?_c('span',{staticClass:\"align-center\"},[(props.row.localProvider)?_c('img',{attrs:{\"src\":\"/images/yes16.png\"}}):_vm._e()]):(props.column.label === 'Action')?_c('span',{staticClass:\"align-center\"},[(!props.row.localProvider)?_c('button',{staticClass:\"btn-medusa config_submitter\",on:{\"click\":function($event){return _vm.addProvider(props.row)}}},[_vm._v(\"Add Provider\")]):_c('button',{staticClass:\"btn-medusa btn-danger\",on:{\"click\":function($event){return _vm.removeProvider(props.row)}}},[_vm._v(\"Remove Provider\")])]):_vm._e()]}}])})],1)])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-prowlarr.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-prowlarr.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-custom-prowlarr.vue?vue&type=template&id=b85ae602&scoped=true&\"\nimport script from \"./config-custom-prowlarr.vue?vue&type=script&lang=js&\"\nexport * from \"./config-custom-prowlarr.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-custom-prowlarr.vue?vue&type=style&index=0&id=b85ae602&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 \"b85ae602\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-torrentrss.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-torrentrss.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-custom-torrentrss.vue?vue&type=template&id=52661c90&scoped=true&\"\nimport script from \"./config-custom-torrentrss.vue?vue&type=script&lang=js&\"\nexport * from \"./config-custom-torrentrss.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-custom-torrentrss.vue?vue&type=style&index=0&id=52661c90&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 \"52661c90\",\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\":\"custom-torrentrss\"}},[_c('config-template',{attrs:{\"label-for\":\"select_torrentrss_provider\",\"label\":\"Select Provider\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedProvider),expression:\"selectedProvider\"}],staticClass:\"form-control input-sm max-input350\",attrs:{\"id\":\"select-provider\"},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.selectedProvider=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[_c('option',{attrs:{\"value\":\"#add\"}},[_vm._v(\"--- add new provider ---\")]),_vm._v(\" \"),_vm._l((_vm.torrentrssProviderOptions),function(option){return _c('option',{key:option.value,domProps:{\"value\":option.value}},[_vm._v(\"\\n \"+_vm._s(option.text)+\"\\n \")])})],2)]),_vm._v(\" \"),(_vm.currentProvider && _vm.selectedProvider !== '#add')?_c('div',{staticClass:\"edit-provider\"},[_c('config-textbox',{attrs:{\"disabled\":\"\",\"label\":\"Provider name\",\"id\":\"edit_provider_name\"},model:{value:(_vm.currentProvider.name),callback:function ($$v) {_vm.$set(_vm.currentProvider, \"name\", $$v)},expression:\"currentProvider.name\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"disabled\":\"\",\"label\":\"Rss Url\",\"id\":\"edit_provider_url\"},model:{value:(_vm.currentProvider.url),callback:function ($$v) {_vm.$set(_vm.currentProvider, \"url\", $$v)},expression:\"currentProvider.url\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Cookies (optional)\",\"id\":\"edit_provider_cookies\"},model:{value:(_vm.currentProvider.config.cookies),callback:function ($$v) {_vm.$set(_vm.currentProvider.config, \"cookies\", $$v)},expression:\"currentProvider.config.cookies\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Search element\",\"id\":\"edit_provider_search_element\"},model:{value:(_vm.currentProvider.config.titleTag),callback:function ($$v) {_vm.$set(_vm.currentProvider.config, \"titleTag\", $$v)},expression:\"currentProvider.config.titleTag\"}}),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa btn-danger torrentrss_delete\",attrs:{\"id\":\"torrentrss_delete\"},on:{\"click\":_vm.removeProvider}},[_vm._v(\"Delete\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter_refresh\",on:{\"click\":function($event){return _vm.$emit('save')}}},[_vm._v(\"Save Changes\")])],1):_vm._e(),_vm._v(\" \"),(_vm.selectedProvider === '#add')?_c('div',{staticClass:\"add-provider\"},[_c('config-textbox',{attrs:{\"label\":\"Provider name\",\"id\":\"add_provider_name\"},scopedSlots:_vm._u([{key:\"warning\",fn:function(){return [_c('transition',{attrs:{\"name\":\"warning\"}},[(!_vm.providerIdAvailable)?_c('div',{staticClass:\"warning\"},[_vm._v(\"This provider id is already used.\")]):_vm._e()])]},proxy:true}],null,false,3405532282),model:{value:(_vm.name),callback:function ($$v) {_vm.name=$$v},expression:\"name\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Site Url\",\"id\":\"add_provider_url\"},model:{value:(_vm.url),callback:function ($$v) {_vm.url=$$v},expression:\"url\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Cookies\",\"id\":\"add_provider_cookies\"},model:{value:(_vm.cookies),callback:function ($$v) {_vm.cookies=$$v},expression:\"cookies\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Search element\",\"id\":\"add_provider_search_element\"},model:{value:(_vm.searchElement),callback:function ($$v) {_vm.searchElement=$$v},expression:\"searchElement\"}}),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",attrs:{\"disabled\":!_vm.providerIdAvailable},on:{\"click\":_vm.addProvider}},[_vm._v(\"Add Provider\")])],1):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-torznab.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-custom-torznab.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-custom-torznab.vue?vue&type=template&id=3cf848cc&scoped=true&\"\nimport script from \"./config-custom-torznab.vue?vue&type=script&lang=js&\"\nexport * from \"./config-custom-torznab.vue?vue&type=script&lang=js&\"\nimport style0 from \"./config-custom-torznab.vue?vue&type=style&index=0&id=3cf848cc&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 \"3cf848cc\",\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\":\"custom-torznab\"}},[_c('config-template',{attrs:{\"label-for\":\"select_torznab_provider\",\"label\":\"Select Provider\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedProvider),expression:\"selectedProvider\"}],staticClass:\"form-control input-sm max-input350\",attrs:{\"id\":\"select-provider\"},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.selectedProvider=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},[_c('option',{attrs:{\"value\":\"#add\"}},[_vm._v(\"--- add new provider ---\")]),_vm._v(\" \"),_vm._l((_vm.torznabProviderOptions),function(option){return _c('option',{key:option.value,domProps:{\"value\":option.value}},[_vm._v(\"\\n \"+_vm._s(option.text)+\"\\n \")])})],2)]),_vm._v(\" \"),(_vm.currentProvider && _vm.selectedProvider !== '#add')?_c('div',{staticClass:\"edit-provider\"},[_c('config-textbox',{attrs:{\"disabled\":\"\",\"label\":\"Provider name\",\"id\":\"edit_provider_name\"},model:{value:(_vm.currentProvider.name),callback:function ($$v) {_vm.$set(_vm.currentProvider, \"name\", $$v)},expression:\"currentProvider.name\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"disabled\":\"\",\"label\":\"Site Url\",\"id\":\"edit_provider_url\"},model:{value:(_vm.currentProvider.url),callback:function ($$v) {_vm.$set(_vm.currentProvider, \"url\", $$v)},expression:\"currentProvider.url\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"Api key\",\"id\":\"edit_provider_api\"},model:{value:(_vm.currentProvider.config.apikey),callback:function ($$v) {_vm.$set(_vm.currentProvider.config, \"apikey\", $$v)},expression:\"currentProvider.config.apikey\"}}),_vm._v(\" \"),_c('config-template',{attrs:{\"label\":\"Categories\",\"label-for\":\"catids\"}},[_c('multiselect',{attrs:{\"value\":_vm.providerCatIds,\"multiple\":true,\"options\":_vm.availableCategories,\"label\":\"id\",\"track-by\":\"id\",\"taggable\":true,\"tag-placeholder\":\"Add this as new cat id\",\"placeholder\":\"Search or add a cat id\"},on:{\"tag\":_vm.addTag,\"input\":function($event){_vm.currentProvider.config.catIds = $event.map(function (cat) { return cat.id; })}},scopedSlots:_vm._u([{key:\"option\",fn:function(props){return [(props.option.isTag)?_c('span',[_c('strong',[_vm._v(_vm._s(props.option.label))])]):_c('span',[_c('strong',[_vm._v(_vm._s(props.option.id))]),_vm._v(\" (\"+_vm._s(props.option.name)+\")\")])]}}],null,false,2742372380)})],1),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa btn-danger torznab_delete\",attrs:{\"disabled\":_vm.currentProvider.default,\"id\":\"torznab_delete\"},on:{\"click\":_vm.removeProvider}},[_vm._v(\"Delete\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter_refresh\",on:{\"click\":function($event){return _vm.$emit('save')}}},[_vm._v(\"Save Changes\")]),_vm._v(\" \"),(_vm.currentProvider.manager === 'prowlarr')?_c('p',{staticClass:\"manager-note\"},[_c('img',{staticStyle:{\"width\":\"16px\"},attrs:{\"src\":\"images/providers/prowlarr.png\"}}),_vm._v(\"\\n Note! This is a provider configured through the 'Configure Custom Prowlarr Providers' tab.\\n \")]):_vm._e()],1):_vm._e(),_vm._v(\" \"),(_vm.selectedProvider === '#add')?_c('div',{staticClass:\"add-provider\"},[_c('config-textbox',{attrs:{\"label\":\"Provider name\",\"id\":\"add_provider_name\"},scopedSlots:_vm._u([{key:\"warning\",fn:function(){return [_c('transition',{attrs:{\"name\":\"warning\"}},[(!_vm.providerIdAvailable)?_c('div',{staticClass:\"warning\"},[_vm._v(\"This provider id is already used.\")]):_vm._e()])]},proxy:true}],null,false,3405532282),model:{value:(_vm.name),callback:function ($$v) {_vm.name=$$v},expression:\"name\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"label\":\"Site Url\",\"id\":\"add_provider_url\"},model:{value:(_vm.url),callback:function ($$v) {_vm.url=$$v},expression:\"url\"}}),_vm._v(\" \"),_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"Api key\",\"id\":\"add_provider_api\"},model:{value:(_vm.apikey),callback:function ($$v) {_vm.apikey=$$v},expression:\"apikey\"}}),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",attrs:{\"disabled\":!_vm.providerIdAvailable},on:{\"click\":_vm.addProvider}},[_vm._v(\"Add Provider\")])],1):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"provider-options-nzb\"}},[(Object.keys(_vm.editProvider).length > 0)?_c('div',{staticClass:\"providerDiv\",attrs:{\"id\":((_vm.editProvider.id) + \"Div\")}},[('username' in _vm.editProvider.config && _vm.editProvider.subType !== 'newznab')?_c('config-textbox',{attrs:{\"label\":\"Username\",\"id\":((_vm.editProvider.id) + \"_username\")},model:{value:(_vm.editProvider.config.username),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"username\", $$v)},expression:\"editProvider.config.username\"}}):_vm._e(),_vm._v(\" \"),(_vm.editProvider.default && _vm.editProvider.needsAuth)?[_c('config-template',{attrs:{\"label-for\":((_vm.editProvider.id) + \"_url\"),\"label\":\"URL\"}},[_c('input',{staticClass:\"form-control input-sm max-input350\",attrs:{\"type\":\"text\",\"id\":((_vm.editProvider.id) + \"_url\"),\"disabled\":\"\"},domProps:{\"value\":(\"\" + (_vm.editProvider.url))}})]),_vm._v(\" \"),('apikey' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"API key\",\"id\":((_vm.editProvider.id) + \"_url\"),\"input-class\":\"newznab_api_key\"},model:{value:(_vm.editProvider.config.apikey),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"apikey\", $$v)},expression:\"editProvider.config.apikey\"}}):_vm._e()]:(_vm.editProvider.subType !== 'newznab')?[('apikey' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"API key\",\"id\":((_vm.editProvider.id) + \"_url\"),\"input-class\":\"newznab_api_key\"},model:{value:(_vm.editProvider.config.apikey),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"apikey\", $$v)},expression:\"editProvider.config.apikey\"}}):_vm._e()]:_vm._e(),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable daily searches\",\"name\":((_vm.editProvider.id) + \"_enable_daily\"),\"id\":((_vm.editProvider.id) + \"_enable_daily\")},model:{value:(_vm.editProvider.config.search.daily.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.daily, \"enabled\", $$v)},expression:\"editProvider.config.search.daily.enabled\"}},[_c('p',[_vm._v(\"enable provider to perform daily searches.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable manual searches\",\"name\":((_vm.editProvider.id) + \"_enable_manual\"),\"id\":((_vm.editProvider.id) + \"_enable_manual\")},model:{value:(_vm.editProvider.config.search.manual.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.manual, \"enabled\", $$v)},expression:\"editProvider.config.search.manual.enabled\"}},[_c('p',[_vm._v(\"enable provider to be used in 'Manual Search' feature.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable backlog searches\",\"name\":((_vm.editProvider.id) + \"_enable_backlog\"),\"id\":((_vm.editProvider.id) + \"_enable_backlog\")},model:{value:(_vm.editProvider.config.search.backlog.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.backlog, \"enabled\", $$v)},expression:\"editProvider.config.search.backlog.enabled\"}},[_c('p',[_vm._v(\"enable provider to perform backlog searches.\")])]),_vm._v(\" \"),_c('config-template',{attrs:{\"label-for\":\"backlog_search_mode\",\"label\":\"Backlog search mode\"}},[_c('div',{staticClass:\"radio-item\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.editProvider.config.search.mode),expression:\"editProvider.config.search.mode\"}],attrs:{\"type\":\"radio\",\"name\":((_vm.editProvider.id) + \"_search_mode_sponly\"),\"id\":((_vm.editProvider.id) + \"_search_mode_sponly\"),\"value\":\"sponly\"},domProps:{\"checked\":_vm._q(_vm.editProvider.config.search.mode,\"sponly\")},on:{\"change\":function($event){return _vm.$set(_vm.editProvider.config.search, \"mode\", \"sponly\")}}}),_vm._v(\" \"),_c('label',{attrs:{\"for\":\"one\"}},[_vm._v(\"Season packs only\")])]),_vm._v(\" \"),_c('div',{staticClass:\"radio-item\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.editProvider.config.search.mode),expression:\"editProvider.config.search.mode\"}],attrs:{\"type\":\"radio\",\"name\":((_vm.editProvider.id) + \"_search_mode_eponly\"),\"id\":((_vm.editProvider.id) + \"_search_mode_eponly\"),\"value\":\"eponly\"},domProps:{\"checked\":_vm._q(_vm.editProvider.config.search.mode,\"eponly\")},on:{\"change\":function($event){return _vm.$set(_vm.editProvider.config.search, \"mode\", \"eponly\")}}}),_vm._v(\" \"),_c('label',{attrs:{\"for\":\"one\"}},[_vm._v(\"Episodes only\")])]),_vm._v(\" \"),_c('p',[_vm._v(\"when searching with backlog you can choose to have it look for season packs only, or choose to have it build a complete season from just single episodes.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable fallback\",\"name\":((_vm.editProvider.id) + \"_enable_fallback\"),\"id\":((_vm.editProvider.id) + \"_enable_fallback\")},model:{value:(_vm.editProvider.config.search.fallback),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search, \"fallback\", $$v)},expression:\"editProvider.config.search.fallback\"}},[_c('p',[_vm._v(\"when searching for a complete season depending on search mode you may return no results, this helps by restarting the search using the opposite search mode.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable search delay\",\"name\":((_vm.editProvider.id) + \"_enable_search_delay\"),\"id\":((_vm.editProvider.id) + \"_enable_search_delay\")},model:{value:(_vm.editProvider.config.search.delay.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.delay, \"enabled\", $$v)},expression:\"editProvider.config.search.delay.enabled\"}},[_c('p',[_vm._v(\"Enable to delay downloads for this provider for an x amount of hours. The provider will start snatching results for a specific episode after a delay has expired, compared to when it first got a result for the specific episode.\")]),_vm._v(\" \"),_c('p',[_vm._v(\"A negative value will have the daily search accepts results before the episode scheduled air date/time.\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Proper and Backlog searches are exempted from the delay.\")])]),_vm._v(\" \"),(_vm.editProvider.config.search.delay.enabled)?_c('config-textbox-number',{attrs:{\"value\":_vm.editProvider.config.search.delay.duration / 60.0,\"label\":\"Search delay (hours)\",\"id\":((_vm.editProvider.id) + \"_search_delay_duration\"),\"min\":0.5,\"step\":0.5},on:{\"input\":function($event){_vm.editProvider.config.search.delay.duration = $event * 60}}},[_c('p',[_vm._v(\"Amount of hours to wait for downloading a result compared to the first result for a specific episode.\")])]):_vm._e()],2):_vm._e(),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",staticStyle:{\"float\":\"left\"},attrs:{\"disabled\":_vm.saving},on:{\"click\":_vm.save}},[_vm._v(\"Save Changes\")]),_vm._v(\" \"),_c('test-provider',{attrs:{\"provider-id\":_vm.editProvider.id,\"provider-name\":_vm.editProvider.name}})],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./test-provider.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./test-provider.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./test-provider.vue?vue&type=template&id=a8b1934a&scoped=true&\"\nimport script from \"./test-provider.vue?vue&type=script&lang=js&\"\nexport * from \"./test-provider.vue?vue&type=script&lang=js&\"\nimport style0 from \"./test-provider.vue?vue&type=style&index=0&id=a8b1934a&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 \"a8b1934a\",\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:\"test-provider\"},[_c('button',{staticClass:\"btn-medusa config_submitter\",on:{\"click\":_vm.test}},[_vm._v(\"Test for results\")]),_vm._v(\" \"),(_vm.loading)?_c('state-switch',{attrs:{\"state\":\"loading\",\"theme\":_vm.layout.themeName}}):_c('span',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.testResult),expression:\"testResult\"}],staticClass:\"testresult\"},[_vm._v(_vm._s(_vm.testResult))])],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-provider-nzb.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-provider-nzb.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","import { render, staticRenderFns } from \"./config-provider-nzb.vue?vue&type=template&id=6826977e&\"\nimport script from \"./config-provider-nzb.vue?vue&type=script&lang=js&\"\nexport * from \"./config-provider-nzb.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","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-provider-torrent.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./config-provider-torrent.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./config-provider-torrent.vue?vue&type=template&id=66655510&\"\nimport script from \"./config-provider-torrent.vue?vue&type=script&lang=js&\"\nexport * from \"./config-provider-torrent.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\":\"provider-options-torrent\"}},[(Object.keys(_vm.editProvider).length > 0)?_c('div',{staticClass:\"providerDiv\",attrs:{\"id\":((_vm.editProvider.id) + \"Div\")}},[('customUrl' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"label\":\"Custom Url\",\"id\":((_vm.editProvider.id) + \"_custom_url\")},model:{value:(_vm.editProvider.config.customUrl),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"customUrl\", $$v)},expression:\"editProvider.config.customUrl\"}},[_c('p',[_vm._v(\"The URL should include the protocol (and port if applicable). Examples: http://192.168.1.4/ or http://localhost:3000/\")])]):_vm._e(),_vm._v(\" \"),('apikey' in _vm.editProvider.config && _vm.editProvider.config.subType !== 'torznab')?_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"API key\",\"id\":((_vm.editProvider.id) + \"_api_key\")},model:{value:(_vm.editProvider.config.apikey),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"apikey\", $$v)},expression:\"editProvider.config.apikey\"}}):_vm._e(),_vm._v(\" \"),('digest' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"Digest\",\"id\":((_vm.editProvider.id) + \"_digest\")},model:{value:(_vm.editProvider.config.digest),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"digest\", $$v)},expression:\"editProvider.config.digest\"}}):_vm._e(),_vm._v(\" \"),('hash' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"label\":\"Hash\",\"id\":((_vm.editProvider.id) + \"_hash\")},model:{value:(_vm.editProvider.config.hash),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"hash\", $$v)},expression:\"editProvider.config.hash\"}}):_vm._e(),_vm._v(\" \"),('username' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"label\":\"Username\",\"id\":((_vm.editProvider.id) + \"_username\")},model:{value:(_vm.editProvider.config.username),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"username\", $$v)},expression:\"editProvider.config.username\"}}):_vm._e(),_vm._v(\" \"),('password' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"autocomplete\":\"no\",\"type\":\"password\",\"label\":\"Password\",\"id\":((_vm.editProvider.id) + \"_password\")},model:{value:(_vm.editProvider.config.password),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"password\", $$v)},expression:\"editProvider.config.password\"}}):_vm._e(),_vm._v(\" \"),(_vm.editProvider.config.cookies.enabled || _vm.editProvider.subType === 'torrentrss')?_c('config-textbox',{attrs:{\"label\":\"Cookies\",\"id\":((_vm.editProvider.id) + \"_cookies\")},model:{value:(_vm.editProvider.config.cookies.values),callback:function ($$v) {_vm.$set(_vm.editProvider.config.cookies, \"values\", $$v)},expression:\"editProvider.config.cookies.values\"}},[(_vm.editProvider.config.cookies.required)?[_c('p',[_vm._v(\"eg. \"+_vm._s(_vm.editProvider.config.cookies.required.map(function (cookie) { return cookie + '=xx;'; }).join('').slice(0, -1)))]),_vm._v(\" \"),_c('p',[_vm._v(\"This provider requires the following cookies: \"+_vm._s(_vm.editProvider.config.cookies.required.join(', '))+\".\\n \"),_c('br'),_vm._v(\"For a step by step guide please follow the link to our \"),_c('app-link',{attrs:{\"href\":\"https://github.com/pymedusa/Medusa/wiki/Configure-Providers-with-captcha-protection\"}},[_vm._v(\"WIKI\")])],1)]:_vm._e()],2):_vm._e(),_vm._v(\" \"),('passkey' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"label\":\"Passkey\",\"id\":((_vm.editProvider.id) + \"_passkey\")},model:{value:(_vm.editProvider.config.passkey),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"passkey\", $$v)},expression:\"editProvider.config.passkey\"}}):_vm._e(),_vm._v(\" \"),('pin' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"Pin\",\"id\":((_vm.editProvider.id) + \"_pin\")},model:{value:(_vm.editProvider.config.pin),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"pin\", $$v)},expression:\"editProvider.config.pin\"}}):_vm._e(),_vm._v(\" \"),('pid' in _vm.editProvider.config)?_c('config-textbox',{attrs:{\"type\":\"password\",\"label\":\"Pid\",\"id\":((_vm.editProvider.id) + \"_pid\")},model:{value:(_vm.editProvider.config.pid),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"pid\", $$v)},expression:\"editProvider.config.pid\"}}):_vm._e(),_vm._v(\" \"),('ratio' in _vm.editProvider.config)?_c('config-textbox-number',{attrs:{\"min\":-1,\"step\":0.1,\"label\":\"Seed ratio\",\"id\":((_vm.editProvider.id) + \"_seed_ratio\")},model:{value:(_vm.editProvider.config.ratio),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"ratio\", $$v)},expression:\"editProvider.config.ratio\"}},[_c('p',[_vm._v(\"Configure a desired seeding ratio. Used by the (automated download handler in config - postprocessing)\\n \"),_c('br'),_vm._v(\"-1 for provider specific option is disabled.\\n \"),_c('br'),_vm._v(\"0 for not using a seed ratio. Actions configured in the download handler, will not wait for finished seeding.\\n \"),_c('br'),_vm._v(\"If disabled the global option is used in config - postprocessing (automated download handling))\\n \")])]):_vm._e(),_vm._v(\" \"),('minseed' in _vm.editProvider.config)?_c('config-textbox-number',{attrs:{\"label\":\"Minimum seeders\",\"min\":0,\"step\":1,\"id\":((_vm.editProvider.id) + \"_min_seed\")},model:{value:(_vm.editProvider.config.minseed),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"minseed\", $$v)},expression:\"editProvider.config.minseed\"}}):_vm._e(),_vm._v(\" \"),('minleech' in _vm.editProvider.config)?_c('config-textbox-number',{attrs:{\"label\":\"Minimum leechers\",\"min\":0,\"step\":1,\"id\":((_vm.editProvider.id) + \"_min_leech\")},model:{value:(_vm.editProvider.config.minleech),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"minleech\", $$v)},expression:\"editProvider.config.minleech\"}}):_vm._e(),_vm._v(\" \"),('confirmed' in _vm.editProvider.config)?_c('config-toggle-slider',{attrs:{\"label\":\"Confirmed downloads\",\"name\":((_vm.editProvider.id) + \"_confirmed\"),\"id\":((_vm.editProvider.id) + \"_confirmed\")},model:{value:(_vm.editProvider.config.confirmed),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"confirmed\", $$v)},expression:\"editProvider.config.confirmed\"}},[_c('p',[_vm._v(\"only download torrents from trusted or verified uploaders ?\")])]):_vm._e(),_vm._v(\" \"),('ranked' in _vm.editProvider.config)?_c('config-toggle-slider',{attrs:{\"label\":\"Ranked torrents\",\"name\":((_vm.editProvider.id) + \"_ranked\"),\"id\":((_vm.editProvider.id) + \"_ranked\")},model:{value:(_vm.editProvider.config.ranked),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"ranked\", $$v)},expression:\"editProvider.config.ranked\"}},[_c('p',[_vm._v(\"only download ranked torrents (trusted releases)\")])]):_vm._e(),_vm._v(\" \"),('sorting' in _vm.editProvider.config)?_c('config-template',{attrs:{\"label-for\":\"sorting\",\"label\":\"Sorting results by\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.editProvider.config.sorting),expression:\"editProvider.config.sorting\"}],staticClass:\"form-control input-sm max-input350\",attrs:{\"id\":\"sorting\"},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.$set(_vm.editProvider.config, \"sorting\", $event.target.multiple ? $$selectedVal : $$selectedVal[0])}}},[_c('option',{attrs:{\"value\":\"last\"}},[_vm._v(\"last\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"seeders\"}},[_vm._v(\"seeders\")]),_vm._v(\" \"),_c('option',{attrs:{\"value\":\"leechers\"}},[_vm._v(\"leechers\")])])]):_vm._e(),_vm._v(\" \"),('freeleech' in _vm.editProvider.config)?_c('config-toggle-slider',{attrs:{\"label\":\"Freeleech\",\"name\":((_vm.editProvider.id) + \"_freeleech\"),\"id\":((_vm.editProvider.id) + \"_freeleech\")},model:{value:(_vm.editProvider.config.freeleech),callback:function ($$v) {_vm.$set(_vm.editProvider.config, \"freeleech\", $$v)},expression:\"editProvider.config.freeleech\"}},[_c('p',[_vm._v(\"only download \"),_c('b',[_vm._v(\"\\\"FreeLeech\\\"\")]),_vm._v(\" torrents.\")])]):_vm._e(),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable daily searches\",\"name\":((_vm.editProvider.id) + \"_enable_daily\"),\"id\":((_vm.editProvider.id) + \"_enable_daily\")},model:{value:(_vm.editProvider.config.search.daily.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.daily, \"enabled\", $$v)},expression:\"editProvider.config.search.daily.enabled\"}},[_c('p',[_vm._v(\"enable provider to perform daily searches.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable manual searches\",\"name\":((_vm.editProvider.id) + \"_enable_manual\"),\"id\":((_vm.editProvider.id) + \"_enable_manual\")},model:{value:(_vm.editProvider.config.search.manual.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.manual, \"enabled\", $$v)},expression:\"editProvider.config.search.manual.enabled\"}},[_c('p',[_vm._v(\"enable provider to be used in 'Manual Search' feature.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable backlog searches\",\"name\":((_vm.editProvider.id) + \"_enable_backlog\"),\"id\":((_vm.editProvider.id) + \"_enable_backlog\")},model:{value:(_vm.editProvider.config.search.backlog.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.backlog, \"enabled\", $$v)},expression:\"editProvider.config.search.backlog.enabled\"}},[_c('p',[_vm._v(\"enable provider to perform backlog searches.\")])]),_vm._v(\" \"),_c('config-template',{attrs:{\"label-for\":\"backlog_search_mode\",\"label\":\"Backlog search mode\"}},[_c('div',{staticClass:\"radio-item\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.editProvider.config.search.mode),expression:\"editProvider.config.search.mode\"}],attrs:{\"type\":\"radio\",\"name\":((_vm.editProvider.id) + \"_search_mode_sponly\"),\"id\":((_vm.editProvider.id) + \"_search_mode_sponly\"),\"value\":\"sponly\"},domProps:{\"checked\":_vm._q(_vm.editProvider.config.search.mode,\"sponly\")},on:{\"change\":function($event){return _vm.$set(_vm.editProvider.config.search, \"mode\", \"sponly\")}}}),_vm._v(\" \"),_c('label',{attrs:{\"for\":\"one\"}},[_vm._v(\"Season packs only\")])]),_vm._v(\" \"),_c('div',{staticClass:\"radio-item\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.editProvider.config.search.mode),expression:\"editProvider.config.search.mode\"}],attrs:{\"type\":\"radio\",\"name\":((_vm.editProvider.id) + \"_search_mode_eponly\"),\"id\":((_vm.editProvider.id) + \"_search_mode_eponly\"),\"value\":\"eponly\"},domProps:{\"checked\":_vm._q(_vm.editProvider.config.search.mode,\"eponly\")},on:{\"change\":function($event){return _vm.$set(_vm.editProvider.config.search, \"mode\", \"eponly\")}}}),_vm._v(\" \"),_c('label',{attrs:{\"for\":\"one\"}},[_vm._v(\"Episodes only\")])]),_vm._v(\" \"),_c('p',[_vm._v(\"when searching with backlog you can choose to have it look for season packs only, or choose to have it build a complete season from just single episodes.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable fallback\",\"name\":((_vm.editProvider.id) + \"_enable_fallback\"),\"id\":((_vm.editProvider.id) + \"_enable_fallback\")},model:{value:(_vm.editProvider.config.search.fallback),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search, \"fallback\", $$v)},expression:\"editProvider.config.search.fallback\"}},[_c('p',[_vm._v(\"when searching for a complete season depending on search mode you may return no results, this helps by restarting the search using the opposite search mode.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Enable search delay\",\"name\":((_vm.editProvider.id) + \"_enable_search_delay\"),\"id\":((_vm.editProvider.id) + \"_enable_search_delay\")},model:{value:(_vm.editProvider.config.search.delay.enabled),callback:function ($$v) {_vm.$set(_vm.editProvider.config.search.delay, \"enabled\", $$v)},expression:\"editProvider.config.search.delay.enabled\"}},[_c('p',[_vm._v(\"Enable to delay downloads for this provider for an x amount of hours. The provider will start snatching results for a specific episode after a delay has expired, compared to when it first got a result for the specific episode.\")]),_vm._v(\" \"),_c('p',[_vm._v(\"A negative value will have the daily search accepts results before the episode scheduled air date/time.\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Proper and Backlog searches are exempted from the delay.\")])]),_vm._v(\" \"),(_vm.editProvider.config.search.delay.enabled)?_c('config-textbox-number',{attrs:{\"value\":_vm.editProvider.config.search.delay.duration / 60.0,\"label\":\"Search delay (hours)\",\"id\":((_vm.editProvider.id) + \"_search_delay_duration\"),\"step\":0.5},on:{\"input\":function($event){_vm.editProvider.config.search.delay.duration = $event * 60}}},[_c('p',[_vm._v(\"Amount of hours to wait for downloading a result compared to the first result for a specific episode.\")])]):_vm._e()],1):_vm._e(),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",staticStyle:{\"float\":\"left\"},attrs:{\"disabled\":_vm.saving},on:{\"click\":_vm.save}},[_vm._v(\"Save Changes\")]),_vm._v(\" \"),_c('test-provider',{attrs:{\"provider-id\":_vm.editProvider.id,\"provider-name\":_vm.editProvider.name}})],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./custom-logs.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./custom-logs.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./custom-logs.vue?vue&type=template&id=12aec99f&scoped=true&\"\nimport script from \"./custom-logs.vue?vue&type=script&lang=js&\"\nexport * from \"./custom-logs.vue?vue&type=script&lang=js&\"\nimport style0 from \"./custom-logs.vue?vue&type=style&index=0&id=12aec99f&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 \"12aec99f\",\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:\"select-list\"},[_c('div',{staticClass:\"wrapper\"},_vm._l((_vm.customLogs),function(customLog){return _c('div',{key:customLog.identifier},[_c('div',{staticClass:\"level\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(customLog.level),expression:\"customLog.level\"}],on:{\"input\":function($event){return _vm.saveLogs($event, customLog.identifier)},\"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.$set(customLog, \"level\", $event.target.multiple ? $$selectedVal : $$selectedVal[0])}}},_vm._l((_vm.levels),function(option){return _c('option',{key:option.value,domProps:{\"value\":option.value}},[_vm._v(_vm._s(option.text)+\"\\n \")])}),0)]),_vm._v(\" \"),_c('div',{staticClass:\"identifier\"},[_vm._v(_vm._s(customLog.identifier))])])}),0)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./externals.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./externals.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./externals.vue?vue&type=template&id=7713c2cf&\"\nimport script from \"./externals.vue?vue&type=script&lang=js&\"\nexport * from \"./externals.vue?vue&type=script&lang=js&\"\nimport style0 from \"./externals.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:\"external-ids\"},[_c('span',[_vm._v(\" - \")]),_vm._v(\" \"),(_vm.externals.imdb && _vm.show.imdbInfo)?_c('app-link',{attrs:{\"href\":(\"https://www.imdb.com/title/\" + (_vm.show.imdbInfo.imdbId)),\"title\":(\"https://www.imdb.com/title/\" + (_vm.show.imdbInfo.imdbId))}},[_c('img',{staticStyle:{\"margin-top\":\"-1px\",\"vertical-align\":\"middle\"},attrs:{\"alt\":\"[imdb]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/imdb16.png\"}})]):_vm._e(),_vm._v(\" \"),(_vm.externals.trakt)?_c('app-link',{attrs:{\"href\":(\"https://trakt.tv/shows/\" + (_vm.externals.trakt)),\"title\":(\"https://trakt.tv/shows/\" + (_vm.externals.trakt))}},[_c('img',{attrs:{\"alt\":\"[trakt]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/trakt.png\"}})]):_vm._e(),_vm._v(\" \"),(_vm.externals.tvdb)?_c('app-link',{attrs:{\"href\":(\"https://www.thetvdb.com/dereferrer/series/\" + (_vm.externals.tvdb)),\"title\":(\"https://www.thetvdb.com/dereferrer/series/\" + (_vm.externals.tvdb))}},[_c('img',{attrs:{\"alt\":\"[tvdb]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/thetvdb16.png\"}})]):_vm._e(),_vm._v(\" \"),(_vm.externals.tvmaze)?_c('app-link',{attrs:{\"href\":(\"https://www.tvmaze.com/shows/\" + (_vm.externals.tvmaze)),\"title\":(\"https://www.tvmaze.com/shows/\" + (_vm.externals.tvmaze))}},[_c('img',{attrs:{\"alt\":\"[tvmaze]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/tvmaze16.png\"}})]):_vm._e(),_vm._v(\" \"),(_vm.externals.tmdb)?_c('app-link',{attrs:{\"href\":(\"https://www.themoviedb.org/tv/\" + (_vm.externals.tmdb)),\"title\":(\"https://www.themoviedb.org/tv/\" + (_vm.externals.tmdb))}},[_c('img',{attrs:{\"alt\":\"[tmdb]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/tmdb16.png\"}})]):_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.externals.tvdb)?_c('app-link',{attrs:{\"href\":(\"https://fanart.tv/series/\" + (_vm.externals.tvdb)),\"title\":(\"https://fanart.tv/series/\" + (_vm.externals.tvdb))}},[_c('img',{staticClass:\"fanart\",attrs:{\"alt\":\"[fanart.tv]\",\"height\":\"16\",\"width\":\"16\",\"src\":\"images/fanart.tv.png\"}})]):_vm._e()],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","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.apply(null, arguments)}}},[_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 }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../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=45b52762&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=45b52762&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 \"45b52762\",\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 }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../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","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./load-progress-bar.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./load-progress-bar.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./load-progress-bar.vue?vue&type=template&id=3b82c21e&\"\nimport script from \"./load-progress-bar.vue?vue&type=script&lang=js&\"\nexport * from \"./load-progress-bar.vue?vue&type=script&lang=js&\"\nimport style0 from \"./load-progress-bar.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.display)?_c('div',{staticClass:\"load-progress-bar-container\"},[_c('div',{staticClass:\"border\"},[_c('div',{staticClass:\"msg\"},[_vm._v(_vm._s(_vm.loadMsg))]),_vm._v(\" \"),_c('div',{staticClass:\"progress\",style:(_vm.styleProgress)})])]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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(\" \"),_c('tr',{staticClass:\"even\"},[_vm._m(28),_vm._v(\" \"),_c('td',[_vm._v(\"%SY\")]),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.getDateFormat('yyyy')))])]),_vm._v(\" \"),_vm._m(29),_vm._v(\" \"),_vm._m(30),_vm._v(\" \"),_vm._m(31)])])]):_vm._e()]),_vm._v(\" \"),(_vm.selectedMultiEpStyle)?_c('div',{staticClass:\"form-group\"},[_vm._m(32),_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(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\",\"value\":\"1\"},domProps:{\"checked\":_vm._q(_vm.animeType,\"1\")},on:{\"change\":[function($event){_vm.animeType=\"1\"},_vm.updatePatternSamples]}}),_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(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_only\",\"value\":\"2\"},domProps:{\"checked\":_vm._q(_vm.animeType,\"2\")},on:{\"change\":[function($event){_vm.animeType=\"2\"},_vm.updatePatternSamples]}}),_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(35),_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]}}),_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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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('td',{staticClass:\"align-right\"},[_c('b',[_vm._v(\"Show premiere year:\")])])},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\":\"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',{staticClass:\"even\"},[_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',[_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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"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('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\",attrs:{\"for\":\"naming_anime_none\"}},[_c('span',[_vm._v(\"No Absolute Number\")])])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../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=03072bae&\"\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","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./plot-info.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./plot-info.vue?vue&type=template&id=b01c2572&\"\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","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./progress-bar.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./progress-bar.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./progress-bar.vue?vue&type=template&id=2a6be2d2&scoped=true&\"\nimport script from \"./progress-bar.vue?vue&type=script&lang=js&\"\nexport * from \"./progress-bar.vue?vue&type=script&lang=js&\"\nimport style0 from \"./progress-bar.vue?vue&type=style&index=0&id=2a6be2d2&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 \"2a6be2d2\",\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:\"progressbar hidden-print ui-progressbar ui-corner-all ui-widget ui-widget-content\",attrs:{\"role\":\"progressbar\"}},'div',{ title: _vm.title },false),[(_vm.text)?_c('div',{staticClass:\"progressbarText\"},[_vm._v(_vm._s(_vm.text))]):_vm._e(),_vm._v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.normalisedPercentage >= 1),expression:\"normalisedPercentage >= 1\"}],class:['ui-progressbar-value', 'ui-corner-left', 'ui-widget-header', 'progress-' + _vm.normalisedPercentage],style:({ width: (_vm.percentage + \"%\") })})])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","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:\"show-option pull-right\"},[_vm._v(\"\\n Poster Size:\\n \"),_c('div',{staticStyle:{\"width\":\"100px\",\"display\":\"inline-block\",\"margin-left\":\"7px\"},attrs:{\"id\":\"posterSizeSlider\"}})])}]\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./poster-size-slider.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./poster-size-slider.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./poster-size-slider.vue?vue&type=template&id=2421162a&\"\nimport script from \"./poster-size-slider.vue?vue&type=script&lang=js&\"\nexport * from \"./poster-size-slider.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","\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../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=7daf2e28&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=7daf2e28&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 \"7daf2e28\",\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.apply(null, arguments)}}},[_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 }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./scene-number-input.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./scene-number-input.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./scene-number-input.vue?vue&type=template&id=8a700f60&scoped=true&\"\nimport script from \"./scene-number-input.vue?vue&type=script&lang=js&\"\nexport * from \"./scene-number-input.vue?vue&type=script&lang=js&\"\nimport style0 from \"./scene-number-input.vue?vue&type=style&index=0&id=8a700f60&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 \"8a700f60\",\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('input',{staticClass:\"sceneSeasonXEpisode form-control input-scene addQTip\",class:[\n _vm.isValid === true ? 'isValid' : '',\n _vm.isValid === false ? 'isInvalid' : '',\n _vm.numberingFrom === 'custom' ? 'isCustom' : ''\n ],staticStyle:{\"padding\":\"0\",\"text-align\":\"center\",\"max-width\":\"60px\"},attrs:{\"type\":\"text\",\"placeholder\":((_vm.sceneSeason || _vm.initialEpisode.season) + \"x\" + (_vm.sceneEpisode || _vm.initialEpisode.episode)),\"size\":\"6\",\"maxlength\":\"8\",\"title\":\"Change this value if scene numbering differs from the indexer episode numbering. Generally used for non-anime shows.\"},domProps:{\"value\":((_vm.sceneSeason || _vm.initialEpisode.season) + \"x\" + (_vm.sceneEpisode || _vm.initialEpisode.episode))},on:{\"change\":_vm.changeSceneNumbering}})}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./scene-number-anime-input.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./scene-number-anime-input.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./scene-number-anime-input.vue?vue&type=template&id=9e73c4ba&scoped=true&\"\nimport script from \"./scene-number-anime-input.vue?vue&type=script&lang=js&\"\nexport * from \"./scene-number-anime-input.vue?vue&type=script&lang=js&\"\nimport style0 from \"./scene-number-anime-input.vue?vue&type=style&index=0&id=9e73c4ba&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 \"9e73c4ba\",\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('input',{staticClass:\"sceneAbsolute form-control input-scene addQTip\",class:[\n _vm.isValid === true ? 'isValid' : '',\n _vm.isValid === false ? 'isInvalid' : '',\n _vm.numberingFrom === 'custom' ? 'isCustom' : ''\n ],staticStyle:{\"padding\":\"0\",\"text-align\":\"center\",\"max-width\":\"60px\"},attrs:{\"type\":\"text\",\"placeholder\":_vm.sceneAbsolute,\"size\":\"6\",\"maxlength\":\"8\",\"title\":\"Change this value if scene absolute numbering differs from the indexer absolute numbering. Generally used for anime shows.\"},domProps:{\"value\":_vm.sceneAbsolute},on:{\"change\":_vm.changeSceneNumbering}})}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","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.apply(null, arguments)}}},[_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.apply(null, arguments)}}})])]),_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.apply(null, arguments)}}})])])])}\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 }","\n\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../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","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./search.vue?vue&type=template&id=4a074bc4&scoped=true&\"\nimport script from \"./search.vue?vue&type=script&lang=js&\"\nexport * from \"./search.vue?vue&type=script&lang=js&\"\nimport style0 from \"./search.vue?vue&type=style&index=0&id=4a074bc4&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 \"4a074bc4\",\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:\"search-wrapper\"},[(_vm.searchType === 'backlog')?_c('img',{ref:(\"search-\" + (_vm.episode.slug)),staticClass:\"epForcedSearch\",attrs:{\"id\":(_vm.showSlug + \"x\" + (_vm.episode.season) + \"x\" + (_vm.episode.episode)),\"name\":(_vm.showSlug + \"x\" + (_vm.episode.season) + \"x\" + (_vm.episode.episode)),\"src\":\"images/search16.png\",\"height\":\"16\",\"alt\":_vm.retryDownload(_vm.episode) ? 'retry' : 'search',\"title\":_vm.retryDownload(_vm.episode) ? 'Retry Download' : 'Forced Seach'},on:{\"click\":function($event){return _vm.queueSearch(_vm.episode)}}}):_vm._e(),_vm._v(\" \"),(_vm.searchType === 'manual')?_c('app-link',{staticClass:\"epManualSearch\",attrs:{\"id\":(_vm.showSlug + \"x\" + (_vm.episode.episode)),\"name\":(_vm.showSlug + \"x\" + (_vm.episode.season) + \"x\" + (_vm.episode.episode)),\"href\":(\"home/snatchSelection?showslug=\" + _vm.showSlug + \"&season=\" + (_vm.episode.season) + \"&episode=\" + (_vm.episode.episode))}},[_c('img',{attrs:{\"data-ep-manual-search\":\"\",\"src\":\"images/manualsearch.png\",\"width\":\"16\",\"height\":\"16\",\"alt\":\"search\",\"title\":\"Manual Search\"}})]):_vm._e(),_vm._v(\" \"),_c('modal',{attrs:{\"name\":\"query-start-backlog-search\",\"height\":'auto',\"width\":'80%'},on:{\"before-open\":_vm.beforeBacklogSearchModalClose}},[_c('transition',{attrs:{\"name\":\"modal\"}},[_c('div',{staticClass:\"modal-mask\"},[_c('div',{staticClass:\"modal-wrapper\"},[_c('div',{staticClass:\"modal-content\"},[_c('div',{staticClass:\"modal-header\"},[_c('button',{staticClass:\"close\",attrs:{\"type\":\"button\",\"data-dismiss\":\"modal\",\"aria-hidden\":\"true\"}},[_vm._v(\"×\")]),_vm._v(\" \"),_c('h4',{staticClass:\"modal-title\"},[_vm._v(\"Start search?\")])]),_vm._v(\" \"),_c('div',{staticClass:\"modal-body\"},[_c('p',[_vm._v(\"Some episodes have been changed to 'Wanted'. Do you want to trigger a backlog search for these \"+_vm._s(_vm.backlogSearchEpisodes.length)+\" episode(s)\")])]),_vm._v(\" \"),_c('div',{staticClass:\"modal-footer\"},[_c('button',{staticClass:\"btn-medusa btn-danger\",attrs:{\"type\":\"button\",\"data-dismiss\":\"modal\"},on:{\"click\":function($event){return _vm.$modal.hide('query-start-backlog-search')}}},[_vm._v(\"No\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa btn-success\",attrs:{\"type\":\"button\",\"data-dismiss\":\"modal\"},on:{\"click\":function($event){_vm.search(_vm.backlogSearchEpisodes, 'backlog'); _vm.$modal.hide('query-start-backlog-search')}}},[_vm._v(\"Yes\")])])])])])])],1),_vm._v(\" \"),_c('modal',{attrs:{\"name\":\"query-mark-failed-and-search\",\"height\":'auto',\"width\":'80%'},on:{\"before-open\":_vm.beforeFailedSearchModalClose}},[_c('transition',{attrs:{\"name\":\"modal\"}},[_c('div',{staticClass:\"modal-mask\"},[_c('div',{staticClass:\"modal-wrapper\"},[_c('div',{staticClass:\"modal-content\"},[_c('div',{staticClass:\"modal-header\"},[_vm._v(\"\\n Mark episode as failed and search?\\n \")]),_vm._v(\" \"),_c('div',{staticClass:\"modal-body\"},[_c('p',[_vm._v(\"Starting to search for the episode\")]),_vm._v(\" \"),(_vm.failedSearchEpisodes && _vm.failedSearchEpisodes.length === 1)?_c('p',[_vm._v(\"Would you also like to mark episode \"+_vm._s(_vm.failedSearchEpisodes[0].slug)+\" as \\\"failed\\\"? This will make sure the episode cannot be downloaded again\")]):(_vm.failedSearchEpisodes)?_c('p',[_vm._v(\"Would you also like to mark episodes \"+_vm._s(_vm.failedSearchEpisodes.map(function (ep) { return ep.slug; }).join(', '))+\" as \\\"failed\\\"? This will make sure the episode cannot be downloaded again\")]):_vm._e()]),_vm._v(\" \"),_c('div',{staticClass:\"modal-footer\"},[_c('button',{staticClass:\"btn-medusa btn-danger\",attrs:{\"type\":\"button\",\"data-dismiss\":\"modal\"},on:{\"click\":function($event){_vm.search(_vm.failedSearchEpisodes, 'backlog'); _vm.$modal.hide('query-mark-failed-and-search')}}},[_vm._v(\"No\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa btn-success\",attrs:{\"type\":\"button\",\"data-dismiss\":\"modal\"},on:{\"click\":function($event){_vm.search(_vm.failedSearchEpisodes, 'failed'); _vm.$modal.hide('query-mark-failed-and-search')}}},[_vm._v(\"Yes\")]),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa btn-danger\",attrs:{\"type\":\"button\",\"data-dismiss\":\"modal\"},on:{\"click\":function($event){return _vm.$modal.hide('query-mark-failed-and-search')}}},[_vm._v(\"Cancel\")])])])])])])],1)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"search-template-container\"}},[_c('vue-snotify'),_vm._v(\" \"),_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(0),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},_vm._l((_vm.defaultEpisodeTemplates),function(template){return _c('search-template-pattern',_vm._b({key:template.id},'search-template-pattern',{ template: template, format: _vm.showFormat, animeType: _vm.animeType },false))}),1)])]),_vm._v(\" \"),_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(1),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},_vm._l((_vm.defaultSeasonTemplates),function(template){return _c('search-template-pattern',_vm._b({key:template.id},'search-template-pattern',{ template: template, format: _vm.showFormat, animeType: _vm.animeType },false))}),1)])]),_vm._v(\" \"),_c('div',{staticClass:\"form-group\"},[_c('div',{staticClass:\"row\"},[_vm._m(2),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 content\"},_vm._l((_vm.customTemplates),function(template){return _c('search-template-pattern',_vm._b({key:template.template,on:{\"remove\":_vm.remove}},'search-template-pattern',{ template: template, format: _vm.showFormat, animeType: _vm.animeType },false))}),1)])]),_vm._v(\" \"),_c('search-template-custom',_vm._b({on:{\"input\":_vm.add}},'search-template-custom',{ show: _vm.show, animeType: _vm.animeType, format: _vm.showFormat },false)),_vm._v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.showFormat === 'anime'),expression:\"showFormat === 'anime'\"}]},[_c('div',{staticClass:\"row\"},[(_vm.animeType > 0)?_c('div',{staticClass:\"form-group\"},[_vm._m(3),_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\"},domProps:{\"value\":1,\"checked\":_vm._q(_vm.animeType,1)},on:{\"change\":function($event){_vm.animeType=1}}}),_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(\" \"),_c('div',{staticClass:\"row\"},[(_vm.animeType > 0)?_c('div',{staticClass:\"form-group\"},[_vm._m(4),_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\"},domProps:{\"value\":2,\"checked\":_vm._q(_vm.animeType,2)},on:{\"change\":function($event){_vm.animeType=2}}}),_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(\" \"),_c('div',{staticClass:\"row\"},[(_vm.animeType > 0)?_c('div',{staticClass:\"form-group\"},[_vm._m(5),_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\"},domProps:{\"value\":3,\"checked\":_vm._q(_vm.animeType,3)},on:{\"change\":function($event){_vm.animeType=3}}}),_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._v(\" \"),_c('div',{staticClass:\"nocheck\",attrs:{\"id\":\"naming-key\"}},[_c('table',{staticClass:\"Key\"},[_vm._m(6),_vm._v(\" \"),_vm._m(7),_vm._v(\" \"),_c('tbody',[_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(\" \"),_vm._m(20),_vm._v(\" \"),_vm._m(21),_vm._v(\" \"),_vm._m(22),_vm._v(\" \"),_vm._m(23),_vm._v(\" \"),_c('tr',[_vm._m(24),_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(25),_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(26),_vm._v(\" \"),_vm._m(27),_vm._v(\" \"),_vm._m(28),_vm._v(\" \"),_vm._m(29),_vm._v(\" \"),_vm._m(30),_vm._v(\" \"),_vm._m(31),_vm._v(\" \"),_vm._m(32),_vm._v(\" \"),_vm._m(33),_vm._v(\" \"),_vm._m(34)])])])],1)}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\",attrs:{\"for\":\"default_templates\"}},[_c('span',[_vm._v(\"Default Episode Templates\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\",attrs:{\"for\":\"default_templates\"}},[_c('span',[_vm._v(\"Default Season Templates\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\",attrs:{\"for\":\"default_templates\"}},[_c('span',[_vm._v(\"Custom Templates\")])])])},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\")])])},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(\"\\n Use lower case if you want lower case names (eg.\\n %sn, %e.n, %q_n etc)\\n \")])])])},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\")])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search-template-custom.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search-template-custom.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./search-template-custom.vue?vue&type=template&id=4f60cc3f&scoped=true&\"\nimport script from \"./search-template-custom.vue?vue&type=script&lang=js&\"\nexport * from \"./search-template-custom.vue?vue&type=script&lang=js&\"\nimport style0 from \"./search-template-custom.vue?vue&type=style&index=0&id=4f60cc3f&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 \"4f60cc3f\",\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:\"form-group\",attrs:{\"id\":\"search-template-custom\"}},[_vm._m(0),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-lg-12 content\"},[_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.selectedTitle),expression:\"selectedTitle\"}],staticClass:\"form-control input-sm\",attrs:{\"id\":\"default_page\",\"name\":\"default_page\"},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.selectedTitle=$event.target.multiple ? $$selectedVal : $$selectedVal[0]}}},_vm._l((_vm.selectTitles),function(option){return _c('option',{key:option.title,domProps:{\"value\":option}},[_vm._v(_vm._s(_vm.titleOptionDescription(option))+\"\\n \")])}),0)])]),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_vm._m(2),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10\"},[_c('div',{staticClass:\"checkbox\"},[_c('label',{attrs:{\"for\":\"episode\"}},[_vm._v(\"Episode\")]),_vm._v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.episodeOrSeason),expression:\"episodeOrSeason\"}],attrs:{\"type\":\"radio\",\"id\":\"episode\"},domProps:{\"value\":'episode',\"checked\":_vm._q(_vm.episodeOrSeason,'episode')},on:{\"change\":function($event){_vm.episodeOrSeason='episode'}}}),_vm._v(\" \"),_c('label',{attrs:{\"for\":\"episode\"}},[_vm._v(\"Season\")]),_vm._v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.episodeOrSeason),expression:\"episodeOrSeason\"}],attrs:{\"type\":\"radio\",\"id\":\"season\"},domProps:{\"value\":'season',\"checked\":_vm._q(_vm.episodeOrSeason,'season')},on:{\"change\":function($event){_vm.episodeOrSeason='season'}}})])])]),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_vm._m(3),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 pattern\"},[(_vm.selectedTitle)?_c('span',{ref:\"inputTitle\"},[_vm._v(\"%SN\")]):_vm._e(),_vm._v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.addPattern),expression:\"addPattern\"}],staticClass:\"form-control-inline-max input-sm max-input350 search-pattern\",staticStyle:{\"padding-left\":\"50px\"},attrs:{\"type\":\"text\",\"name\":\"new_pattern\",\"disabled\":!_vm.selectedTitle},domProps:{\"value\":(_vm.addPattern)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.addPattern=$event.target.value}}}),_vm._v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.enabled),expression:\"enabled\"}],attrs:{\"type\":\"checkbox\"},domProps:{\"checked\":Array.isArray(_vm.enabled)?_vm._i(_vm.enabled,null)>-1:(_vm.enabled)},on:{\"change\":function($event){var $$a=_vm.enabled,$$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.enabled=$$a.concat([$$v]))}else{$$i>-1&&(_vm.enabled=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{_vm.enabled=$$c}}}}),_vm._v(\" \"),(!_vm.validated && _vm.isValidMessage)?_c('p',[_vm._v(_vm._s(_vm.isValidMessage))]):_vm._e()])]),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_vm._m(4),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10\",class:{ 'error-message': !_vm.validated }},[_vm._v(\"\\n \"+_vm._s(_vm.patternExample)+\"\\n \")])]),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-sm-2\"}),_vm._v(\" \"),_c('div',{staticClass:\"col-sm-10 vertical-align\"},[_c('input',{staticClass:\"btn-medusa pull-left button\",attrs:{\"id\":\"submit\",\"type\":\"submit\",\"value\":\"Add custom exception\",\"disabled\":!_vm.validated},on:{\"click\":_vm.add}}),_vm._v(\" \"),_c('p',[_vm._v(_vm._s(_vm.notification))])])])])])])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\",attrs:{\"for\":\"default_templates\"}},[_c('span',[_vm._v(\"Add Custom Template\")])])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\"},[_c('span',[_vm._v(\"Title:\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\"},[_c('span',[_vm._v(\"Episode or Season search:\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\"},[_c('span',[_vm._v(\"Search Template:\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-sm-2\"},[_c('label',{staticClass:\"control-label\"},[_c('span',[_vm._v(\"example:\")])])])}]\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search-template-pattern.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search-template-pattern.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./search-template-pattern.vue?vue&type=template&id=007d54df&scoped=true&\"\nimport script from \"./search-template-pattern.vue?vue&type=script&lang=js&\"\nexport * from \"./search-template-pattern.vue?vue&type=script&lang=js&\"\nimport style0 from \"./search-template-pattern.vue?vue&type=style&index=0&id=007d54df&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 \"007d54df\",\n null\n \n)\n\nexport default component.exports","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search-template-container.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./search-template-container.vue?vue&type=script&lang=js&\"","\n\n\n\n","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{\"id\":\"search-template-pattern\"}},[_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-sm-12 content\"},[_c('div',{staticClass:\"template-wrapper\"},[_c('span',{staticClass:\"template-title\"},[_vm._v(_vm._s(_vm.searchTemplate.title))]),_vm._v(\" \"),_c('img',{directives:[{name:\"tooltip\",rawName:\"v-tooltip.right\",value:({\n content: _vm.searchTemplate.season === -1 ? 'Show Exception' : (\"Season \" + (_vm.searchTemplate.season) + \" Exception\")\n }),expression:\"{\\n content: searchTemplate.season === -1 ? 'Show Exception' : `Season ${searchTemplate.season} Exception`\\n }\",modifiers:{\"right\":true}}],staticStyle:{\"margin-bottom\":\"2px\"},attrs:{\"src\":\"images/info32.png\",\"width\":\"16\",\"height\":\"16\",\"alt\":\"\"}}),_vm._v(\" \"),(!_vm.searchTemplate.default)?_c('img',{staticClass:\"template-remove\",attrs:{\"src\":\"images/no16.png\"},on:{\"click\":function($event){return _vm.$emit('remove', _vm.searchTemplate)}}}):_vm._e(),_vm._v(\" \"),_c('div',{directives:[{name:\"tooltip\",rawName:\"v-tooltip.right\",value:({ content: _vm.tooltipContent }),expression:\"{ content: tooltipContent }\",modifiers:{\"right\":true}}],staticClass:\"tooltip-wrapper-pattern\"},[_c('div',{staticClass:\"template-body\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.searchTemplate.template),expression:\"searchTemplate.template\"}],staticClass:\"form-control-inline-max input-sm max-input350 search-pattern\",class:{ invalid: !_vm.validated },attrs:{\"type\":\"text\",\"name\":\"search_pattern\",\"disabled\":\"disabled\"},domProps:{\"value\":(_vm.searchTemplate.template)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.$set(_vm.searchTemplate, \"template\", $event.target.value)}}}),_vm._v(\" \"),_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.searchTemplate.enabled),expression:\"searchTemplate.enabled\"}],attrs:{\"type\":\"checkbox\"},domProps:{\"checked\":Array.isArray(_vm.searchTemplate.enabled)?_vm._i(_vm.searchTemplate.enabled,null)>-1:(_vm.searchTemplate.enabled)},on:{\"change\":function($event){var $$a=_vm.searchTemplate.enabled,$$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(_vm.searchTemplate, \"enabled\", $$a.concat([$$v])))}else{$$i>-1&&(_vm.$set(_vm.searchTemplate, \"enabled\", $$a.slice(0,$$i).concat($$a.slice($$i+1))))}}else{_vm.$set(_vm.searchTemplate, \"enabled\", $$c)}}}}),_vm._v(\" \"),_c('i',{staticClass:\"show-template glyphicon\",class:(\"glyphicon-eye-\" + (_vm.showExample ? 'close' : 'open')),attrs:{\"title\":\"Show template example\"},on:{\"click\":function($event){_vm.showExample = !_vm.showExample}}})])])]),_vm._v(\" \"),(_vm.showExample)?_c('span',{staticClass:\"template-example\",class:{ invalid: !_vm.validated },attrs:{\"name\":\"search_pattern\"}},[_vm._v(\"Example: \"+_vm._s(_vm.searchTemplateExample)+\"\\n \")]):_vm._e()])])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { render, staticRenderFns } from \"./search-template-container.vue?vue&type=template&id=110c2eea&\"\nimport script from \"./search-template-container.vue?vue&type=script&lang=js&\"\nexport * from \"./search-template-container.vue?vue&type=script&lang=js&\"\nimport style0 from \"./search-template-container.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","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./select-list.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./select-list.vue?vue&type=template&id=1843461f&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=1843461f&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 \"1843461f\",\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:{\"disabled\":\"\",\"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\",\"background-color\":\"rgb(238, 238, 238)\"}},[_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??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./select-trakt-lists.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./select-trakt-lists.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./select-trakt-lists.vue?vue&type=template&id=20efe074&scoped=true&\"\nimport script from \"./select-trakt-lists.vue?vue&type=script&lang=js&\"\nexport * from \"./select-trakt-lists.vue?vue&type=script&lang=js&\"\nimport style0 from \"./select-trakt-lists.vue?vue&type=style&index=0&id=20efe074&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 \"20efe074\",\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:\"select-trakt-list max-width\"},[_c('ul',_vm._l((_vm.availableLists),function(availableList){return _c('li',{key:availableList},[_c('div',{staticClass:\"trakt-list-group\"},[_c('input',{staticClass:\"form-control input-sm available-list\",attrs:{\"disabled\":\"\",\"type\":\"text\"},domProps:{\"value\":availableList}}),_vm._v(\" \"),_c('input',{attrs:{\"type\":\"checkbox\"},domProps:{\"checked\":_vm.selectedLists.find(function (list) { return list === availableList; })},on:{\"input\":function($event){return _vm.saveLists($event, availableList)}}})])])}),0)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../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??clonedRuleSet-1[0].rules[0]!../../../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=43633cb1&\"\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.changeRoute($event.target.value);}]}},[(_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.sortedLists && _vm.sortedLists.length > 1)?_vm._l((_vm.sortedLists),function(list){return _c('optgroup',{key:list.listTitle,attrs:{\"label\":list.listTitle}},_vm._l((list.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.sortedLists[0].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 }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',_vm._b({staticClass:\"sorted-select-list max-width\"},'div',{disabled: _vm.disabled},false),[_c('draggable',{staticClass:\"list-group\",attrs:{\"tag\":\"ul\",\"handle\":\".handle\"},on:{\"start\":function($event){_vm.dragging = true},\"end\":function($event){_vm.dragging = false}},model:{value:(_vm.editItems),callback:function ($$v) {_vm.editItems=$$v},expression:\"editItems\"}},_vm._l((_vm.editItems),function(item){return _c('li',{key:item.id,staticClass:\"draggable-list\"},[_c('font-awesome-icon',{staticClass:\"handle\",attrs:{\"icon\":'align-justify'}}),_vm._v(\" \"),_c('div',{staticClass:\"draggable-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)}}},[_c('div',{staticClass:\"btn btn-default input-sm\",staticStyle:{\"font-size\":\"14px\"}},[_c('i',{staticClass:\"glyphicon glyphicon-remove\",attrs:{\"title\":\"Remove\"}})])])])],1)}),0),_vm._v(\" \"),_c('div',{staticClass:\"new-item\"},[_c('div',{staticClass:\"draggable-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(0)])])]),_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()],1)}\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-plus\",attrs:{\"title\":\"Add\"}})])}]\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./sorted-select-list.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./sorted-select-list.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./sorted-select-list.vue?vue&type=template&id=48a6e4c2&scoped=true&\"\nimport script from \"./sorted-select-list.vue?vue&type=script&lang=js&\"\nexport * from \"./sorted-select-list.vue?vue&type=script&lang=js&\"\nimport style0 from \"./sorted-select-list.vue?vue&type=style&index=0&id=48a6e4c2&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 \"48a6e4c2\",\n null\n \n)\n\nexport default component.exports","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./test-guessit.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./test-guessit.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./test-guessit.vue?vue&type=template&id=3796064f&scoped=true&\"\nimport script from \"./test-guessit.vue?vue&type=script&lang=js&\"\nexport * from \"./test-guessit.vue?vue&type=script&lang=js&\"\nimport style0 from \"./test-guessit.vue?vue&type=style&index=0&id=3796064f&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 \"3796064f\",\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._m(0),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_vm._m(1),_vm._v(\" \"),_c('div',{staticClass:\"col-lg-10\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.releaseName),expression:\"releaseName\"}],staticClass:\"form-control input-sm\",attrs:{\"type\":\"text\"},domProps:{\"value\":(_vm.releaseName)},on:{\"input\":function($event){if($event.target.composing){ return; }_vm.releaseName=$event.target.value}}})])]),_vm._v(\" \"),_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-lg-12\"},[_c('pre',[_vm._v(_vm._s(JSON.stringify(_vm.guessitResult, undefined, 4)))])])]),_vm._v(\" \"),(_vm.error)?_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-lg-12\"},[_c('div',{staticClass:\"error\"},[_vm._v(_vm._s(_vm.error))])])]):_vm._e(),_vm._v(\" \"),(_vm.show)?_c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-lg-12 matched-show\"},[_c('div',[_vm._v(\"Matched to show:\")]),_vm._v(\" \"),_c('div',[_c('app-link',{attrs:{\"href\":(\"home/displayShow?showslug=\" + (_vm.show.id.slug))}},[_vm._v(_vm._s(_vm.show.title))])],1)])]):_vm._e(),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",on:{\"click\":function($event){$event.preventDefault();return _vm.testReleaseName.apply(null, arguments)}}},[_vm._v(\"Test Release Name\")])])}\nvar staticRenderFns = [function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"row\"},[_c('div',{staticClass:\"col-lg-12\"},[_c('p',[_vm._v(\"Guessit is a library used for parsing release names. As a minimum Medusa requires a show title, season and episode (if not parsed as a season pack).\")]),_vm._v(\" \"),_c('p',[_vm._v(\"You can fill in your release name and click the `Test Release Name` button, to get the guessit response.\")])])])},function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:\"col-lg-2\"},[_c('span',[_vm._v(\"Release name:\")])])}]\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('config-toggle-slider',{attrs:{\"label\":\"Enable\",\"id\":\"use_trakt\",\"explanations\":['Send Trakt.tv notifications?']},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.enabled),callback:function ($$v) {_vm.$set(_vm.trakt, \"enabled\", $$v)},expression:\"trakt.enabled\"}}),_vm._v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.trakt.enabled),expression:\"trakt.enabled\"}],attrs:{\"id\":\"content-use-trakt-client\"}},[_c('config-template',{attrs:{\"label-for\":\"trakt_request_auth\",\"label\":\"\"}},[_c('input',{staticClass:\"btn-medusa\",attrs:{\"type\":\"button\",\"value\":\"Connect to your trakt account\",\"id\":\"Trakt\"},on:{\"click\":_vm.TraktRequestDeviceCode}}),_vm._v(\" \"),(_vm.traktRequestSend && _vm.traktUserCode)?_c('span',{staticStyle:{\"display\":\"inline\"}},[_vm._v(\"Use this code in the popup: \"+_vm._s(_vm.traktUserCode))]):_vm._e(),_vm._v(\" \"),(_vm.traktRequestSend && _vm.traktUserCode && _vm.traktRequestMessage)?_c('p',[_vm._v(\"Trakt request status: \"+_vm._s(_vm.traktRequestMessage))]):_vm._e(),_vm._v(\" \"),(_vm.traktRequestAuthenticated && _vm.traktRequestMessage)?_c('p',[_vm._v(_vm._s(_vm.traktRequestMessage))]):_vm._e()]),_vm._v(\" \"),(!_vm.authOnly)?[_c('config-textbox-number',{attrs:{\"label\":\"API Timeout\",\"id\":\"trakt_timeout\",\"explanations\":['Seconds to wait for Trakt API to respond. (Use 0 to wait forever)']},model:{value:(_vm.trakt.timeout),callback:function ($$v) {_vm.$set(_vm.trakt, \"timeout\", $$v)},expression:\"trakt.timeout\"}}),_vm._v(\" \"),_c('config-template',{attrs:{\"label-for\":\"trakt_default_indexer\",\"label\":\"Default indexer\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.trakt.defaultIndexer),expression:\"trakt.defaultIndexer\"}],staticClass:\"form-control\",attrs:{\"id\":\"trakt_default_indexer\",\"name\":\"trakt_default_indexer\"},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.$set(_vm.trakt, \"defaultIndexer\", $event.target.multiple ? $$selectedVal : $$selectedVal[0])}}},_vm._l((_vm.traktIndexersOptions),function(option){return _c('option',{key:option.key,domProps:{\"value\":option.value}},[_vm._v(\"\\n \"+_vm._s(option.text)+\"\\n \")])}),0)]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Sync libraries\",\"id\":\"trakt_sync\"},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.sync),callback:function ($$v) {_vm.$set(_vm.trakt, \"sync\", $$v)},expression:\"trakt.sync\"}},[_c('p',[_vm._v(\"Sync your Medusa show library with your Trakt collection.\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Note: Don't enable this setting if you use the Trakt addon for Kodi or any other script that syncs your library.\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Kodi detects that the episode was deleted and removes from collection which causes Medusa to re-add it. This causes a loop between Medusa and Kodi adding and deleting the episode.\")])]),_vm._v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.trakt.sync),expression:\"trakt.sync\"}],attrs:{\"id\":\"content-use-trakt-client\"}},[_c('config-toggle-slider',{attrs:{\"label\":\"Remove Episodes From Collection\",\"id\":\"trakt_remove_watchlist\",\"explanations\":['Remove an Episode from your Trakt Collection if it is not in your Medusa Library.', 'Note:Don\\'t enable this setting if you use the Trakt addon for Kodi or any other script that syncs your library.']},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.removeWatchlist),callback:function ($$v) {_vm.$set(_vm.trakt, \"removeWatchlist\", $$v)},expression:\"trakt.removeWatchlist\"}})],1),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Sync watchlist\",\"id\":\"trakt_sync_watchlist\"},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.syncWatchlist),callback:function ($$v) {_vm.$set(_vm.trakt, \"syncWatchlist\", $$v)},expression:\"trakt.syncWatchlist\"}},[_c('p',[_vm._v(\"Sync your Medusa library with your Trakt Watchlist (either Show and Episode).\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Episode will be added on watch list when wanted or snatched and will be removed when downloaded\")]),_vm._v(\" \"),_c('p',[_vm._v(\"Note: By design, Trakt automatically removes episodes and/or shows from watchlist as soon you have watched them.\")])]),_vm._v(\" \"),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.trakt.syncWatchlist),expression:\"trakt.syncWatchlist\"}],attrs:{\"id\":\"content-use-trakt-client\"}},[_c('config-template',{attrs:{\"label-for\":\"trakt_default_indexer\",\"label\":\"Watchlist add method\"}},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.trakt.methodAdd),expression:\"trakt.methodAdd\"}],staticClass:\"form-control\",attrs:{\"id\":\"trakt_method_add\",\"name\":\"trakt_method_add\"},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.$set(_vm.trakt, \"methodAdd\", $event.target.multiple ? $$selectedVal : $$selectedVal[0])}}},_vm._l((_vm.traktMethodOptions),function(option){return _c('option',{key:option.key,domProps:{\"value\":option.value}},[_vm._v(\"\\n \"+_vm._s(option.text)+\"\\n \")])}),0),_vm._v(\" \"),_c('p',[_vm._v(\"method in which to download episodes for new shows.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Remove episode\",\"id\":\"trakt_remove_watchlist\"},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.removeWatchlist),callback:function ($$v) {_vm.$set(_vm.trakt, \"removeWatchlist\", $$v)},expression:\"trakt.removeWatchlist\"}},[_c('p',[_vm._v(\"remove an episode from your watchlist after it's downloaded.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Remove series\",\"id\":\"trakt_remove_serieslist\"},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.removeSerieslist),callback:function ($$v) {_vm.$set(_vm.trakt, \"removeSerieslist\", $$v)},expression:\"trakt.removeSerieslist\"}},[_c('p',[_vm._v(\"remove the whole series from your watchlist after any download.\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Remove watched show\",\"id\":\"trakt_remove_show_from_application\"},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.removeShowFromApplication),callback:function ($$v) {_vm.$set(_vm.trakt, \"removeShowFromApplication\", $$v)},expression:\"trakt.removeShowFromApplication\"}},[_c('p',[_vm._v(\"remove the show from Medusa if it\\\\'s ended and completely watched\")])]),_vm._v(\" \"),_c('config-toggle-slider',{attrs:{\"label\":\"Start paused\",\"id\":\"trakt_start_paused\"},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.startPaused),callback:function ($$v) {_vm.$set(_vm.trakt, \"startPaused\", $$v)},expression:\"trakt.startPaused\"}},[_c('p',[_vm._v(\"shows grabbed from your trakt watchlist start paused.\")])])],1),_vm._v(\" \"),_c('config-textbox',{class:_vm.traktBlacklistClass,attrs:{\"label\":\"Trakt blackList name\",\"id\":\"trakt_blacklist_name\"},on:{\"change\":function($event){return _vm.save()}},model:{value:(_vm.trakt.blacklistName),callback:function ($$v) {_vm.$set(_vm.trakt, \"blacklistName\", $$v)},expression:\"trakt.blacklistName\"}},[_c('p',[_vm._v(\"Name(slug) of List on Trakt for blacklisting show on 'Add Trending Show' & 'Add Recommended Shows' pages\")])])]:_vm._e(),_vm._v(\" \"),_c('div',{staticClass:\"testNotification\",attrs:{\"id\":\"testTrakt-result\"}},[_vm._v(_vm._s(_vm.testTraktResult))]),_vm._v(\" \"),_c('input',{attrs:{\"type\":\"hidden\",\"id\":\"trakt_pin_url\"},domProps:{\"value\":_vm.trakt.pinUrl}}),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa\",attrs:{\"id\":\"testTrakt\"},on:{\"click\":_vm.testTrakt}},[_vm._v(\"Test Trakt\")]),_vm._v(\" \"),(!_vm.authOnly)?_c('button',{staticClass:\"btn-medusa\",attrs:{\"id\":\"forceSync\"},on:{\"click\":_vm.traktForceSync}},[_vm._v(\"Force Sync\")]):_vm._e(),_vm._v(\" \"),_c('button',{staticClass:\"btn-medusa config_submitter\",attrs:{\"disabled\":_vm.saving},on:{\"click\":_vm.save}},[_vm._v(\"Save Changes\")])],2)],1)}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./trakt-authentication.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./trakt-authentication.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./trakt-authentication.vue?vue&type=template&id=769fed2d&scoped=true&\"\nimport script from \"./trakt-authentication.vue?vue&type=script&lang=js&\"\nexport * from \"./trakt-authentication.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 \"769fed2d\",\n null\n \n)\n\nexport default component.exports","\n\n\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./alerts.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./alerts.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./alerts.vue?vue&type=template&id=4c38c771&\"\nimport script from \"./alerts.vue?vue&type=script&lang=js&\"\nexport * from \"./alerts.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.system.branch !== null)?_c('div',{attrs:{\"id\":\"alerts\"}},[(_vm.system.branch !== 'master' && !_vm.developer)?_c('div',{staticClass:\"text-center\"},[_c('div',{staticClass:\"alert alert-danger upgrade-notification hidden-print\",attrs:{\"role\":\"alert\"}},[_c('span',[_vm._v(\"You're using the \"+_vm._s(_vm.system.branch)+\" branch. Please use 'master' unless specifically asked\")])])]):_vm._e(),_vm._v(\" \"),(_vm.system.newestVersionMessage)?_c('div',{staticClass:\"text-center\"},[_c('div',{staticClass:\"alert alert-success upgrade-notification hidden-print\",attrs:{\"role\":\"alert\"}},[_c('span',{domProps:{\"innerHTML\":_vm._s(_vm.system.newestVersionMessage)}})])]):_vm._e()]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../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??clonedRuleSet-1[0].rules[0]!../../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=2843f38c&\"\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 navbar-mobile\"},[_c('li',{staticClass:\"navbar-split dropdown\",class:{ active: _vm.topMenu === 'home' },attrs:{\"id\":\"NAVhome\"}},[_c('app-link',{staticClass:\"dropdown-toggle\",attrs:{\"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',{staticClass:\"navbar-split\",class:{ active: _vm.topMenu === 'schedule' },attrs:{\"id\":\"NAVschedule\"}},[_c('app-link',{attrs:{\"href\":\"schedule/\"}},[_vm._v(\"Schedule\")])],1),_vm._v(\" \"),_c('li',{staticClass:\"navbar-split\",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:{\"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/changeIndexer/\"}},[_c('i',{staticClass:\"menu-icon-manage-change\"}),_vm._v(\" Change Indexer\")])],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('a',{attrs:{\"href\":\"home/updatePLEX/\"},on:{\"click\":function($event){$event.preventDefault();return _vm.updatePlex.apply(null, arguments)}}},[_c('i',{staticClass:\"menu-icon-plex\"}),_vm._v(\" Update PLEX\")])]):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.kodi)?_c('li',[_c('a',{attrs:{\"href\":\"home/updateKODI/\"},on:{\"click\":function($event){$event.preventDefault();return _vm.updateKodi.apply(null, arguments)}}},[_c('i',{staticClass:\"menu-icon-kodi\"}),_vm._v(\" Update KODI\")])]):_vm._e(),_vm._v(\" \"),(_vm.linkVisible.emby)?_c('li',[_c('a',{attrs:{\"href\":\"home/updateEMBY/\"},on:{\"click\":function($event){$event.preventDefault();return _vm.updateEmby.apply(null, arguments)}}},[_c('i',{staticClass:\"menu-icon-emby\"}),_vm._v(\" Update Emby\")])]):_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(\" \"),_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:{\"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:{\"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.system.news.unread > 0)?_c('span',{staticClass:\"badge\"},[_vm._v(_vm._s(_vm.system.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',{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/update'},nativeOn:{\"click\":function($event){$event.preventDefault();return _vm.checkForupdates($event)}}},[_c('i',{staticClass:\"menu-icon-update\"}),_vm._v(\" Check For Updates\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":'home/restart'}},[_c('i',{staticClass:\"menu-icon-restart\"}),_vm._v(\" Restart\")])],1),_vm._v(\" \"),_c('li',[_c('app-link',{attrs:{\"href\":'home/shutdown'},on:{\"click\":function($event){$event.preventDefault();return _vm.$router.push({ name: '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 }","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:\"col-md-12 shadow\",attrs:{\"id\":\"sub-menu-container\"}},[_c('div',{staticClass:\"submenu-default hidden-print\",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.runMethod($event, menuItem)}])},[_c('span',{class:['pull-left', menuItem.icon]}),_vm._v(\" \"+_vm._s(menuItem.title)+\"\\n \")])}),_vm._v(\" \"),(_vm.showForRoutes)?_c('show-selector',{attrs:{\"show-slug\":_vm.$route.query.showslug,\"follow-selection\":\"\"}}):_vm._e()],2)]),_vm._v(\" \"),_c('div',{staticClass:\"btn-group\"})]):_vm._e()}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../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??clonedRuleSet-1[0].rules[0]!../../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=5844b6a9&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=5844b6a9&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 \"5844b6a9\",\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?status=\" + _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.system.branch || 'Unknown'))]),_vm._v(\" |\\n Now: \"),_c('span',{staticClass:\"footerhighlight\"},[_vm._v(_vm._s(_vm.nowInUserPreset))])],2)],2)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n\n\n","import mod from \"-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../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??clonedRuleSet-1[0].rules[0]!../../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=5cf1e725&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 \"5cf1e725\",\n null\n \n)\n\nexport default component.exports","import mod from \"-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app.vue?vue&type=script&lang=js&\"","\n\n\n\n\n","\n import API from \"!../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\";\n import domAPI from \"!../../node_modules/style-loader/dist/runtime/styleDomAPI.js\";\n import insertFn from \"!../../node_modules/style-loader/dist/runtime/insertBySelector.js\";\n import setAttributes from \"!../../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js\";\n import insertStyleElement from \"!../../node_modules/style-loader/dist/runtime/insertStyleElement.js\";\n import styleTagTransformFn from \"!../../node_modules/style-loader/dist/runtime/styleTagTransform.js\";\n import content, * as namedExport from \"!!../../node_modules/css-loader/dist/cjs.js!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/sass-loader/dist/cjs.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app.vue?vue&type=style&index=0&lang=scss&\";\n \n \n\nvar options = {};\n\noptions.styleTagTransform = styleTagTransformFn;\noptions.setAttributes = setAttributes;\n\n options.insert = insertFn.bind(null, \"head\");\n \noptions.domAPI = domAPI;\noptions.insertStyleElement = insertStyleElement;\n\nvar update = API(content, options);\n\n\n\nexport * from \"!!../../node_modules/css-loader/dist/cjs.js!../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../node_modules/sass-loader/dist/cjs.js!../../node_modules/vue-loader/lib/index.js??vue-loader-options!./app.vue?vue&type=style&index=0&lang=scss&\";\n export default content && content.locals ? content.locals : undefined;\n","import { render, staticRenderFns } from \"./app.vue?vue&type=template&id=2787cf5f&\"\nimport script from \"./app.vue?vue&type=script&lang=js&\"\nexport * from \"./app.vue?vue&type=script&lang=js&\"\nimport style0 from \"./app.vue?vue&type=style&index=0&lang=scss&\"\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\":\"app\"}},[(_vm.isAuthenticated)?_c('div',[(_vm.showsLoading)?_c('load-progress-bar',_vm._b({},'load-progress-bar',{display: _vm.showsLoading.display, current: _vm.showsLoading.current, total: _vm.showsLoading.total},false)):_vm._e(),_vm._v(\" \"),_c('app-header'),_vm._v(\" \"),_c('sub-menu'),_vm._v(\" \"),_c('div',{attrs:{\"id\":\"content-row\"}},[_c('div',{class:_vm.layout.wide ? 'col-lg-12 col-md-12' : 'col-lg-10 col-lg-offset-1 col-md-10 col-md-offset-1',attrs:{\"id\":\"content-col\"}},[_c('vue-snotify'),_vm._v(\" \"),_c('alerts'),_vm._v(\" \"),(_vm.$route.meta.header)?_c('h1',{staticClass:\"header\"},[_vm._v(_vm._s(_vm.$route.meta.header))]):_vm._e(),_vm._v(\" \"),_c('keep-alive',{attrs:{\"exclude\":_vm.excludeFromCaching}},[_c('router-view',{key:_vm.$route.meta.nocache ? (\"\" + (_vm.$route.fullPath)) : _vm.$route.name})],1),_vm._v(\" \"),_c('app-footer'),_vm._v(\" \"),_c('scroll-buttons')],1)])],1):_c('div',[_c('router-view')],1)])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('tr',{ref:\"changeIndexerRow\"},[_c('td',[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.currentShow.checked),expression:\"currentShow.checked\"}],attrs:{\"type\":\"checkbox\",\"data-slug\":_vm.show.id.slug},domProps:{\"checked\":Array.isArray(_vm.currentShow.checked)?_vm._i(_vm.currentShow.checked,null)>-1:(_vm.currentShow.checked)},on:{\"change\":function($event){var $$a=_vm.currentShow.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(_vm.currentShow, \"checked\", $$a.concat([$$v])))}else{$$i>-1&&(_vm.$set(_vm.currentShow, \"checked\", $$a.slice(0,$$i).concat($$a.slice($$i+1))))}}else{_vm.$set(_vm.currentShow, \"checked\", $$c)}}}})]),_vm._v(\" \"),_c('td',[_c('app-link',{attrs:{\"href\":(\"home/displayShow?showslug=\" + (_vm.show.id.slug))}},[_vm._v(_vm._s(_vm.show.name))])],1),_vm._v(\" \"),_c('td',[_vm._v(_vm._s(_vm.show.indexer))]),_vm._v(\" \"),_c('td',[_c('select-indexer',_vm._b({on:{\"change\":_vm.selectIndexerChange}},'select-indexer',{show: _vm.show, searchedShow: _vm.searchedShow},false))],1),_vm._v(\" \"),_c('td',{staticClass:\"align-center\"},[(_vm.state)?_c('div',{staticClass:\"step-container\"},[_c('div',{staticClass:\"state\"},[_c('state-switch',{attrs:{\"state\":_vm.state}})],1),_vm._v(\" \"),_c('div',{staticClass:\"stepdisplay\"},[_c('ul',_vm._l((_vm.show.changeStatus.steps),function(step){return _c('li',{key:step},[_vm._v(_vm._s(step))])}),0)])]):_vm._e()])])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./select-indexer.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./select-indexer.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./select-indexer.vue?vue&type=template&id=75f70d78&scoped=true&\"\nimport script from \"./select-indexer.vue?vue&type=script&lang=js&\"\nexport * from \"./select-indexer.vue?vue&type=script&lang=js&\"\nimport style0 from \"./select-indexer.vue?vue&type=style&index=0&id=75f70d78&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 \"75f70d78\",\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\"},[_c('select',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.selectedIndexer),expression:\"selectedIndexer\"}],attrs:{\"name\":\"indexer\"},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.selectedIndexer=$event.target.multiple ? $$selectedVal : $$selectedVal[0]},function($event){return _vm.$emit('change', _vm.selectedIndexer)}]}},[_c('option',{attrs:{\"disabled\":\"\",\"value\":\"--select--\"}},[_vm._v(\"--select--\")]),_vm._v(\" \"),_vm._l((_vm.externalsOptions),function(option){return _c('option',{key:option.value,domProps:{\"value\":{value: option.value, text: option.text}}},[_vm._v(\"\\n \"+_vm._s(option.text)+\"\\n \")])})],2),_vm._v(\" \"),(_vm.searchedShow && _vm.searchedShow.searched)?_c('div',{staticClass:\"star\",attrs:{\"title\":\"This indexer was manually selected\"}},[_vm._v(\"*\")]):_vm._e()])}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\n\n","import mod from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./change-indexer-row.vue?vue&type=script&lang=js&\"; export default mod; export * from \"-!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./change-indexer-row.vue?vue&type=script&lang=js&\"","import { render, staticRenderFns } from \"./change-indexer-row.vue?vue&type=template&id=2f075332&scoped=true&\"\nimport script from \"./change-indexer-row.vue?vue&type=script&lang=js&\"\nexport * from \"./change-indexer-row.vue?vue&type=script&lang=js&\"\nimport style0 from \"./change-indexer-row.vue?vue&type=style&index=0&id=2f075332&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 \"2f075332\",\n null\n \n)\n\nexport default component.exports","// @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';\nimport { library } from '@fortawesome/fontawesome-svg-core';\nimport { faAlignJustify, faImages } from '@fortawesome/free-solid-svg-icons';\nimport { faTimesCircle } from '@fortawesome/free-regular-svg-icons';\n\nlibrary.add([faAlignJustify, faImages, faTimesCircle]);\n\nimport { App } 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 App\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, { dynamicDefault: { height: 'auto' } });\n Vue.use(VTooltip);\n\n // Set default cookie expire time\n Vue.$cookies.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 pageComponent: false,\n showsLoading: 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.$root.$emit('loaded');\n // Legacy - send config.general to jQuery (received by index.js)\n const event = new CustomEvent('medusa-config-loaded', { detail: { general: config.main, layout: config.layout } });\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 // 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","/**\n * Vue Cookie handler for the vue-good-tables enable/disable columns feature.\n * @param {String} cookiePrefix String used to specify the specific table.\n * @example - Prefix `Home` with the column Label `title` will result in the cookie key `Home-title`\n * @returns {void}\n */\nexport const manageCookieMixin = cookiePrefix => {\n return {\n methods: {\n getCookie(key) {\n if (key.includes(cookiePrefix)) {\n return JSON.parse(key);\n }\n try {\n return JSON.parse(this.$cookies.get(`${cookiePrefix}-${key}`));\n } catch {\n return null;\n }\n },\n setCookie(key, value) {\n return this.$cookies.set(`${cookiePrefix}-${key}`, JSON.stringify(value));\n },\n /**\n * Save vue-good-table sort field and sort order (desc/asc)\n * @param {*} evt - Vue good table sorting event (triggered by the `on-sort-change` event)\n */\n saveSorting(evt) {\n const { setCookie } = this;\n // Store cookies, for sort field and type (asc/desc)\n setCookie('sort-field', evt.map(item => item.field));\n setCookie('sort-type', evt.map(item => item.type));\n },\n /**\n * Get vue-good-table sort field and sort order.\n * @param {string} defaultField - default vue good table field to sort by.\n * @param {string} defaultType - default vue good table sort order (ascending / descending).\n * @returns {object} - Object with the field and type properties.\n */\n getSortBy(defaultField = 'title', defaultType = 'asc') {\n const { getCookie } = this;\n // Try to get cookies, for sort field and type (asc/desc)\n const sortField = getCookie('sort-field');\n const sortType = getCookie('sort-type');\n const sort = [];\n\n if (Array.isArray(sortField) && sortField.length === 2) {\n sortField.forEach((_, index) => {\n sort.push({ field: sortField[index] || defaultField, type: sortType[index] || defaultType });\n });\n return sort;\n }\n\n if (sortField === null || sortType === null) {\n return ({ field: defaultField, type: defaultType });\n }\n\n return ({ field: sortField[0] || defaultField, type: sortType[0] || defaultType });\n }\n },\n created() {\n // Watch the columns property on the VM. This is the default named property for the vue-good-tables columns.\n this.$watch(() => this.columns, columns => {\n // Monitor the columns, to update the cookies, when changed.\n const { setCookie } = this;\n for (const column of columns) {\n if (column) {\n setCookie(column.label, column.hidden);\n }\n }\n }, { deep: true });\n }\n };\n};\n","import pretty from 'pretty-bytes';\nimport { mapGetters, mapState } from 'vuex';\n\n/**\n * Vue bindings for the simple, small poster and banner layouts.\n * @returns {void}\n */\nexport const showlistTableMixin = {\n data() {\n const { getCookie } = this;\n return {\n columns: [{\n label: 'Next Ep',\n field: 'nextAirDate',\n type: 'date',\n sortable: true,\n dateInputFormat: 'yyyy-MM-dd\\'T\\'HH:mm:ssXXX',\n dateOutputFormat: 'yyyy-MM-dd\\'T\\'HH:mm:ssXXX',\n sortFn: this.sortDateNext,\n hidden: getCookie('Next Ep')\n }, {\n label: 'Prev Ep',\n field: 'prevAirDate',\n type: 'date',\n sortable: true,\n dateInputFormat: 'yyyy-MM-dd\\'T\\'HH:mm:ssXXX',\n dateOutputFormat: 'yyyy-MM-dd\\'T\\'HH:mm:ssXXX',\n sortFn: this.sortDatePrev,\n hidden: getCookie('Prev Ep')\n }, {\n label: 'Show',\n field: 'title',\n filterOptions: {\n enabled: true\n },\n sortFn: this.sortTitle,\n hidden: getCookie('Show')\n }, {\n label: 'Network',\n field: 'network',\n filterOptions: {\n enabled: true\n },\n hidden: getCookie('Network')\n }, {\n label: 'Indexer',\n field: 'indexer',\n filterOptions: {\n enabled: true,\n filterDropdownItems: ['tvdb', 'tvmaze', 'tmdb', 'imdb']\n },\n hidden: getCookie('Indexer')\n }, {\n label: 'Quality',\n field: 'config.qualities',\n filterOptions: {\n enabled: true,\n filterFn: this.qualityColumnFilterFn\n },\n sortable: false,\n hidden: getCookie('Quality')\n }, {\n label: 'Downloads',\n field: 'stats.tooltip.percentage',\n sortFn: this.sortDownloads,\n type: 'boolean',\n hidden: getCookie('Downloads')\n }, {\n label: 'Size',\n type: 'number',\n field: 'stats.episodes.size',\n hidden: getCookie('Size')\n }, {\n label: 'Active',\n field: this.fealdFnActive,\n filterOptions: {\n enabled: true,\n filterDropdownItems: [\n { value: true, text: 'yes' },\n { value: false, text: 'no' }\n ]\n },\n type: 'boolean',\n hidden: getCookie('Active')\n }, {\n label: 'Status',\n field: 'status',\n filterOptions: {\n enabled: true\n },\n hidden: getCookie('Status')\n }, {\n label: 'Xem',\n field: this.fealdFnXem,\n filterOptions: {\n enabled: true,\n filterDropdownItems: [\n { value: true, text: 'yes' },\n { value: false, text: 'no' }\n ]\n },\n type: 'boolean',\n hidden: getCookie('Xem')\n }]\n };\n },\n computed: {\n ...mapState({\n config: state => state.config.general,\n indexerConfig: state => state.config.indexers.indexers,\n stateLayout: state => state.config.layout,\n qualityValues: state => state.config.consts.qualities.values\n }),\n ...mapGetters({\n fuzzyParseDateTime: 'fuzzyParseDateTime',\n showsInLists: 'showsInLists',\n getShowIndexerUrl: 'getShowIndexerUrl'\n }),\n maxNextAirDate() {\n const { shows } = this;\n return Math.max(...shows.filter(show => show.nextAirDate).map(show => Date.parse(show.nextAirDate)));\n }\n },\n methods: {\n prettyBytes: bytes => pretty(bytes),\n parsePrevDateFn(row) {\n const { fuzzyParseDateTime } = this;\n if (row.prevAirDate) {\n console.log(`Calculating time for show ${row.title} prev date: ${row.prevAirDate}`);\n return fuzzyParseDateTime(row.prevAirDate);\n }\n\n return '';\n },\n parseNextDateFn(row) {\n const { fuzzyParseDateTime } = this;\n if (row.nextAirDate) {\n console.log(`Calculating time for show ${row.title} next date: ${row.nextAirDate}`);\n return fuzzyParseDateTime(row.nextAirDate);\n }\n\n return '';\n },\n fealdFnXem(row) {\n return row.xemNumbering && row.xemNumbering.length !== 0;\n },\n fealdFnActive(row) {\n return row.config && !row.config.paused && row.status === 'Continuing';\n },\n sortDateNext(x, y) {\n const { maxNextAirDate } = this;\n\n if (x === null && y === null) {\n return 0;\n }\n\n if (x === null || y === null) {\n return x === null ? 1 : -1;\n }\n\n // Convert to timestamps\n x = Date.parse(x);\n y = Date.parse(y);\n\n // This next airdate lies in the past. We need to correct this.\n if (x < Date.now()) {\n x += maxNextAirDate;\n }\n\n if (y < Date.now()) {\n y += maxNextAirDate;\n }\n\n return (x < y ? -1 : (x > y ? 1 : 0));\n },\n sortDatePrev(x, y) {\n if (x === null && y === null) {\n return 0;\n }\n\n // Standardize dates and nulls\n x = x ? Date.parse(x) : 0;\n y = y ? Date.parse(y) : 0;\n\n if (x === null || y === null) {\n return x === null ? -1 : 1;\n }\n\n const xTsDiff = x - Date.now();\n const yTsDiff = y - Date.now();\n\n return xTsDiff < yTsDiff ? -1 : (xTsDiff > yTsDiff ? 1 : 0);\n },\n sortTitle(x, y) {\n const { stateLayout } = this;\n const { sortArticle } = stateLayout;\n\n let titleX = x;\n let titleY = y;\n\n if (!sortArticle) {\n titleX = titleX.replace(/^((?:a(?!\\s+to)n?)|the)\\s/i, '').toLowerCase();\n titleY = titleY.replace(/^((?:a(?!\\s+to)n?)|the)\\s/i, '').toLowerCase();\n }\n\n return (titleX < titleY ? -1 : (titleX > titleY ? 1 : 0));\n },\n sortDownloads(x, y, _, rowX, rowY) {\n if ((x === 0 || x === 100) && x === y) {\n return rowX.stats.episodes.total < rowY.stats.episodes.total ? -1 : (rowX.stats.episodes.total < rowY.stats.episodes.total ? 1 : 0);\n }\n\n return x < y ? -1 : (x > y ? 1 : 0);\n },\n qualityColumnFilterFn(data, filterString) {\n const { qualityValues } = this;\n return [...data.allowed, ...data.preferred].map(q => qualityValues.find(qv => qv.value === q).name.includes(filterString)).some(isTrue => isTrue);\n }\n }\n};\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.general.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 } = $store.state;\n const { notifiers } = config;\n\n const showSlug = $route.params.showSlug || $route.query.showslug;\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?showslug=${showSlug}`,\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?showslug=${showSlug}`,\n icon: `ui-icon ui-icon-${show.config.paused ? 'play' : 'pause'}`\n },\n {\n title: 'Remove',\n path: `home/deleteShow?showslug=${showSlug}`,\n confirm: 'removeshow',\n icon: 'ui-icon ui-icon-trash'\n },\n {\n title: 'Re-scan files',\n path: `home/refreshShow?showslug=${showSlug}`,\n icon: 'ui-icon ui-icon-refresh'\n },\n {\n title: 'Force Full Update',\n path: `home/updateShow?showslug=${showSlug}`,\n icon: 'ui-icon ui-icon-transfer-e-w'\n },\n {\n title: 'Update show in KODI',\n path: `home/updateKODI?showslug=${showSlug}`,\n method: 'updatekodi',\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?showslug=${showSlug}`,\n requires: notifiers.emby.enabled,\n icon: 'menu-icon-emby'\n },\n {\n title: 'Preview Rename',\n path: `home/testRename?showslug=${showSlug}`,\n icon: 'ui-icon ui-icon-tag'\n },\n {\n title: 'Download Subtitles',\n path: `home/subtitleShow?showslug=${showSlug}`,\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 topMenu: 'home',\n converted: true\n },\n component: () => import('../components/home.vue')\n },\n {\n path: '/home/editShow',\n name: 'editShow',\n meta: {\n topMenu: 'home',\n subMenu: showSubMenu,\n converted: true,\n nocache: true // Use this flag, to have the router-view use :key=\"$route.fullPath\"\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 converted: true,\n nocache: true // Use this flag, to have the router-view use :key=\"$route.fullPath\"\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 converted: true,\n nocache: true // Use this flag, to have the router-view use :key=\"$route.fullPath\"\n },\n component: () => import('../components/snatch-selection.vue')\n },\n {\n path: '/home/testRename',\n name: 'testRename',\n meta: {\n topMenu: 'home',\n subMenu: showSubMenu,\n title: 'Preview Rename',\n header: 'Preview Rename',\n converted: true\n },\n component: () => import('../components/test-rename.vue')\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 converted: true\n },\n component: () => import('../components/manual-post-process.vue')\n },\n {\n path: '/home/status',\n name: 'status',\n meta: {\n title: 'Status',\n topMenu: 'system',\n converted: true\n },\n component: () => import('../components/status.vue')\n },\n {\n path: '/home/restart',\n name: 'restart',\n meta: {\n title: 'Restarting...',\n header: 'Performing Restart',\n topMenu: 'system',\n converted: true\n },\n component: () => import('../components/restart.vue')\n },\n {\n path: '/home/shutdown',\n name: 'shutdown',\n meta: {\n header: 'Shutting down',\n topMenu: 'system',\n converted: true\n },\n component: () => import('../components/restart.vue'),\n props: { shutdown: true }\n },\n {\n path: '/home/update',\n name: 'update',\n meta: {\n header: 'Update Medusa',\n topMenu: 'system',\n converted: true\n },\n component: () => import('../components/update.vue')\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 converted: true\n },\n component: () => import('../components/config-anime.vue')\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 converted: true\n },\n component: () => import('../components/config-backup-restore.vue')\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 converted: true\n },\n component: () => import('../components/config-general.vue')\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 converted: true\n },\n component: () => import('../components/config-providers.vue')\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 converted: true\n },\n component: () => import('../components/config-subtitles.vue')\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/existingShows',\n name: 'addExistingShows',\n meta: {\n title: 'Add Existing Shows',\n header: 'Add Existing Shows',\n topMenu: 'home',\n converted: true,\n nocache: true\n },\n component: () => import('../components/new-shows-existing.vue')\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 converted: true,\n nocache: true\n },\n props: route => ({ ...route.params }),\n component: () => import('../components/new-show.vue')\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/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 converted: true\n },\n component: () => import('../components/schedule.vue')\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 converted: true\n },\n component: () => import('../components/history.vue')\n};\n\n/** @type {import('.').Route} */\nconst downloadsRoute = {\n path: '/downloads',\n name: 'downloads',\n meta: {\n title: 'Downloads',\n header: 'Downloads',\n converted: true\n },\n component: () => import('../components/current-downloads.vue')\n};\n\n/** @type {import('.').Route[]} */\nconst manageRoutes = [\n {\n path: '/manage',\n name: 'manage',\n meta: {\n title: 'Mass Update',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/manage-mass-update.vue'),\n props: true\n },\n {\n path: '/manage/changeIndexer',\n name: 'manageChangeIndexer',\n meta: {\n title: 'Change show indexer',\n header: 'Change show indexer',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/change-indexer.vue')\n },\n {\n path: '/manage/backlogOverview',\n name: 'manageBacklogOverview',\n meta: {\n title: 'Backlog Overview',\n header: 'Backlog Overview',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/manage-backlog.vue')\n },\n {\n path: '/manage/episodeStatuses',\n name: 'manageEpisodeOverview',\n meta: {\n title: 'Episode Overview',\n header: 'Episode Overview',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/manage-episode-status.vue')\n },\n {\n path: '/manage/failedDownloads',\n name: 'manageFailedDownloads',\n meta: {\n title: 'Failed Downloads',\n header: 'Failed Downloads',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/manage-failed-downloads.vue')\n },\n {\n path: '/manage/manageSearches',\n name: 'manageManageSearches',\n meta: {\n title: 'Manage Searches',\n header: 'Manage Searches',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/manage-searches.vue')\n },\n {\n path: '/manage/massEdit',\n name: 'manageMassEdit',\n meta: {\n title: 'Mass Edit',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/manage-mass-edit.vue'),\n props: true\n },\n {\n path: '/manage/subtitleMissed',\n name: 'manageSubtitleMissed',\n meta: {\n title: 'Missing Subtitles',\n header: 'Missing Subtitles',\n topMenu: 'manage',\n converted: true\n },\n component: () => import('../components/manage-missing-subtitles.vue')\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 converted: true\n },\n component: () => import('../components/log-reporter.vue'),\n props: true\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 converted: true\n },\n component: () => import('../components/news.vue')\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 converted: true\n },\n component: () => import('../components/changelog.vue')\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/** @type {import('.').Route} */\nconst notFoundRedirect = {\n path: '*',\n redirect: '/not-found'\n};\n\n/** @type {import('.').Route[]} */\nexport default [\n ...homeRoutes,\n ...configRoutes,\n ...addShowRoutes,\n loginRoute,\n addRecommendedRoute,\n scheduleRoute,\n historyRoute,\n downloadsRoute,\n ...manageRoutes,\n ...errorLogsRoutes,\n newsRoute,\n changesRoute,\n ircRoute,\n notFoundRoute,\n notFoundRedirect\n];\n","import Vue from 'vue';\nimport VueRouter from 'vue-router';\n\nimport routes from './routes';\n\nVue.use(VueRouter);\nconst router = new VueRouter({\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","import axios from 'axios';\n\n// This should be more dynamic. As now when we change the apiKey in config-general.vue. This won't work anymore.\n// Because of this, a page reload is required.\n\nexport default function() {\n this.webRoot = document.body.getAttribute('web-root');\n this.token = null;\n this.getToken = () => {\n return axios.get(`${this.webRoot}/token`)\n .then(response => {\n this.token = response.data;\n this.apiRoute = axios.create({\n baseURL: `${this.webRoot}/`,\n timeout: 60000,\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json'\n }\n });\n\n this.api = axios.create({\n baseURL: `${this.webRoot}/api/v2/`,\n timeout: 30000,\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n 'x-auth': `Bearer ${this.token}`\n }\n });\n });\n };\n}\n","import {\n AUTHENTICATE,\n LOGIN_PENDING,\n LOGIN_SUCCESS,\n LOGIN_FAILED,\n LOGOUT,\n REFRESH_TOKEN,\n REMOVE_AUTH_ERROR\n} from '../mutation-types';\nimport ApiClient from '../../api';\nimport VueJwtDecode from 'vue-jwt-decode';\n\nconst state = {\n isAuthenticated: false,\n user: {},\n tokens: {\n access: null,\n refresh: null\n },\n error: null,\n client: null,\n apiKey: null,\n webRoot: null\n};\n\nconst mutations = {\n [LOGIN_PENDING]() { },\n [LOGIN_SUCCESS](state, user) {\n state.user.username = user.username;\n state.user.group = user.group;\n state.apiKey = user.apiKey;\n state.webRoot = user.webRoot;\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 [AUTHENTICATE](state, client) {\n state.client = client;\n state.tokens.access = client.token;\n }\n};\n\nconst getters = {};\n\nconst actions = {\n login({ commit, state }) {\n commit(LOGIN_PENDING);\n\n // Check if we got a token from the /token call.\n const { client } = state;\n const { token } = client;\n if (!token) {\n commit(LOGIN_FAILED, { error: 'Missing token' });\n return { success: false, error: 'Missing token' };\n }\n\n const credentials = VueJwtDecode.decode(token);\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 auth({ commit }) {\n // Get the JWT token\n return new Promise(resolve => {\n const apiClient = new ApiClient();\n apiClient.getToken()\n .then(() => {\n commit(AUTHENTICATE, apiClient);\n resolve();\n });\n });\n }\n};\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 templates: null,\n searchTemplates: []\n },\n countries: null,\n genres: [],\n id: {\n tvdb: null,\n trakt: null,\n imdb: 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 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 provider: {\n id: null,\n name: null,\n config: {},\n cache: []\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 Vue from 'vue';\nimport { ADD_HISTORY, ADD_HISTORY_ROW, ADD_SHOW_HISTORY, ADD_SHOW_EPISODE_HISTORY } from '../mutation-types';\nimport { episodeToSlug } from '../../utils/core';\n\nconst state = {\n remote: {\n rows: [],\n totalRows: 0,\n page: 1,\n perPage: 25,\n sort: [{\n field: 'date',\n type: 'desc'\n }],\n filter: null\n },\n remoteCompact: {\n rows: [],\n totalRows: 0,\n page: 1,\n perPage: 25,\n sort: [{\n field: 'date',\n type: 'desc'\n }],\n filter: null\n },\n episodeHistory: {},\n historyLast: null,\n historyLastCompact: null,\n loading: false\n};\n\nconst mutations = {\n [ADD_HISTORY_ROW](state, { history, compact }) {\n // Only evaluate compact once.\n const historyKey = compact ? 'remoteCompact' : 'remote';\n\n // Update state, add one item at the top.\n state[historyKey].rows.unshift(history);\n },\n [ADD_HISTORY](state, { history, compact }) {\n // Only evaluate compact once.\n const historyKey = compact ? 'remoteCompact' : 'remote';\n\n // Update state\n Vue.set(state[historyKey], 'rows', history);\n },\n [ADD_SHOW_HISTORY](state, { showSlug, history }) {\n // Add history data to episodeHistory, but without passing the show slug.\n for (const row of history) {\n if (!Object.keys(state.episodeHistory).includes(showSlug)) {\n Vue.set(state.episodeHistory, showSlug, {});\n }\n\n const episodeSlug = episodeToSlug(row.season, row.episode);\n if (!state.episodeHistory[showSlug][episodeSlug]) {\n state.episodeHistory[showSlug][episodeSlug] = [];\n }\n\n state.episodeHistory[showSlug][episodeSlug].push(row);\n }\n },\n [ADD_SHOW_EPISODE_HISTORY](state, { showSlug, episodeSlug, history }) {\n // Keep an object of shows, with their history per episode\n // Example: {tvdb1234: {s01e01: [history]}}\n\n if (!Object.keys(state.episodeHistory).includes(showSlug)) {\n Vue.set(state.episodeHistory, showSlug, {});\n }\n\n Vue.set(state.episodeHistory[showSlug], episodeSlug, history);\n },\n setLoading(state, value) {\n state.loading = value;\n },\n setRemoteTotal(state, { total, compact = false }) {\n state[compact ? 'remoteCompact' : 'remote'].totalRows = total;\n }\n};\n\nconst getters = {\n getShowHistoryBySlug: state => showSlug => state.showHistory[showSlug],\n getLastReleaseName: state => ({ showSlug, episodeSlug }) => {\n if (state.episodeHistory[showSlug] !== undefined) {\n if (state.episodeHistory[showSlug][episodeSlug] !== undefined) {\n if (state.episodeHistory[showSlug][episodeSlug].length === 1) {\n return state.episodeHistory[showSlug][episodeSlug][0].resource;\n }\n const filteredHistory = state.episodeHistory[showSlug][episodeSlug]\n .slice()\n .sort((a, b) => (a.actionDate - b.actionDate) * -1)\n .filter(ep => ['Snatched', 'Downloaded'].includes(ep.statusName) && ep.resource !== '');\n if (filteredHistory.length > 0) {\n return filteredHistory[0].resource;\n }\n }\n }\n },\n getEpisodeHistory: state => ({ showSlug, episodeSlug }) => {\n if (state.episodeHistory[showSlug] === undefined) {\n return [];\n }\n\n return state.episodeHistory[showSlug][episodeSlug] || [];\n },\n getSeasonHistory: state => ({ showSlug, season }) => {\n if (state.episodeHistory[showSlug] === undefined) {\n return [];\n }\n\n return Object.values(state.episodeHistory[showSlug]).flat().filter(row => row.season === season) || [];\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 history 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 async getShowHistory({ rootState, commit }, { slug }) {\n const response = await rootState.auth.client.api.get(`/history/${slug}`);\n if (response.data.length > 0) {\n commit(ADD_SHOW_HISTORY, { showSlug: slug, history: response.data });\n }\n },\n /**\n * Get detailed history from API and commit them to the store.\n *\n * @param {*} context - The store context.\n * @param {object} args - arguments.\n */\n async getHistory({ rootState, commit }, args) {\n let url = '/history';\n const page = args?.page || 1;\n const limit = args?.perPage || 1000;\n let sort = args?.sort || [{ field: 'date', type: 'desc' }];\n const filter = args?.filter || {};\n const showSlug = args?.showSlug;\n const compact = args?.compact;\n\n const params = {\n page,\n limit\n };\n\n if (sort) {\n if (!Array.isArray(sort)) {\n sort = [sort];\n }\n params.sort = sort;\n }\n\n if (filter) {\n params.filter = filter;\n }\n\n if (showSlug) {\n url = `${url}/${showSlug}`;\n }\n\n if (compact) {\n params.compact = true;\n }\n\n commit('setLoading', true);\n let response = null;\n try {\n response = await rootState.auth.client.api.get(url, { params }); // eslint-disable-line no-await-in-loop\n if (response) {\n commit('setRemoteTotal', { total: Number(response.headers['x-pagination-count']), compact });\n if (showSlug) {\n commit(ADD_SHOW_HISTORY, { showSlug, history: response.data, compact });\n } else {\n commit(ADD_HISTORY, { history: response.data, compact });\n }\n }\n } catch (error) {\n if (error.response && error.response.status === 404) {\n console.debug(`No history available${showSlug ? ' for show ' + showSlug : ''}`);\n }\n }\n\n commit('setLoading', false);\n },\n /**\n * Get episode history 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 getShowEpisodeHistory({ rootState, commit }, { showSlug, episodeSlug }) {\n return new Promise(resolve => {\n rootState.auth.client.api.get(`/history/${showSlug}/episode/${episodeSlug}`)\n .then(response => {\n if (response.data.length > 0) {\n commit(ADD_SHOW_EPISODE_HISTORY, { showSlug, episodeSlug, history: response.data });\n }\n resolve();\n })\n .catch(() => {\n console.warn(`No episode history found for show ${showSlug} and episode ${episodeSlug}`);\n });\n });\n },\n updateHistory({ rootState, commit }, data) {\n // Update store's search queue item. (provided through websocket)\n const compact = rootState.config.layout.history === 'compact';\n // We can't live update the compact layout, as it requires to aggregate the data.\n if (compact) {\n return;\n }\n commit(ADD_HISTORY_ROW, { history: data });\n }\n};\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 Vue from 'vue';\nimport {\n ADD_PROVIDER,\n ADD_PROVIDERS,\n ADD_PROVIDER_CACHE,\n ADD_SEARCH_RESULTS,\n REMOVE_PROVIDER\n} from '../mutation-types';\n\nconst state = {\n providers: []\n};\n\nconst mutations = {\n [ADD_PROVIDER](state, provider) {\n if (!state.providers.find(p => p.id === provider.id)) {\n state.providers.push(provider);\n }\n },\n [ADD_PROVIDERS](state, providers) {\n providers.forEach(provider => {\n const existingProvider = state.providers.find(p => p.id === provider.id);\n if (existingProvider) {\n Vue.set(state.providers, state.providers.indexOf(existingProvider), provider);\n } else {\n state.providers.push(provider);\n }\n });\n },\n [REMOVE_PROVIDER](state, providerId) {\n state.providers = state.providers.filter(prov => prov.id !== providerId);\n },\n [ADD_PROVIDER_CACHE](state, { providerId, cache }) {\n // Check if this provider has already been added.\n let currentProvider = state.providers.find(prov => prov.id === providerId);\n if (!currentProvider) {\n currentProvider = {\n name: '',\n config: {}\n };\n state.providers.push(currentProvider);\n }\n\n if (currentProvider.cache === undefined) {\n Vue.set(currentProvider, 'cache', []);\n }\n\n const newCache = [];\n\n for (const result of cache) {\n const existingIdentifier = currentProvider.cache.find(item => item.identifier === result.identifier);\n if (existingIdentifier) {\n newCache.push({ ...existingIdentifier, ...result });\n } else {\n newCache.push(result);\n }\n }\n\n Vue.set(currentProvider, 'cache', newCache);\n },\n /**\n * Add search results which have been retreived through the webSocket.\n * @param {*} state - Vue state\n * @param {Array} searchResults - One or more search results.\n */\n [ADD_SEARCH_RESULTS](state, searchResults) {\n for (const searchResult of searchResults) {\n let currentProvider = state.providers.find(prov => prov.id === searchResult.provider.id);\n\n if (!currentProvider) {\n currentProvider = {\n name: '',\n config: {},\n cache: []\n };\n }\n\n const { cache } = currentProvider;\n\n // Check if we don't allready have this result in our store.\n // In that case, we update the existing object.\n const existingSearchResult = (cache || []).find(result => result.identifier === searchResult.identifier);\n if (existingSearchResult) {\n // Because this is an existing result, whe're not overwriting dateAdded field.\n const { dateAdded, ...rest } = searchResult;\n Vue.set(currentProvider.cache, cache.indexOf(existingSearchResult), { ...existingSearchResult, ...rest });\n } else {\n Vue.set(currentProvider, 'cache', [...cache || [], ...[searchResult]]);\n }\n }\n }\n};\n\nconst getters = {\n providerNameToId: _ => providerName => providerName.replace(/[^\\d\\w_]/gi, '_').toLowerCase().trim() // eslint-disable-line unicorn/better-regex\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 providers.\n *\n * @param {*} context The store context.\n * @returns {Promise} The API response.\n */\n getProviders({ rootState, commit }) {\n return new Promise((resolve, reject) => {\n rootState.auth.client.api.get('/providers')\n .then(response => {\n commit(ADD_PROVIDERS, response.data);\n resolve();\n })\n .catch(error => {\n console.error(`Could not get providers with error: ${error}`);\n reject();\n });\n });\n },\n /**\n * Get provider cache results for enabled providers.\n *\n * @param {*} context The store context.\n * @param {String} The provider id.\n * @returns {void}.\n */\n async getProviderCacheResults({ rootState, commit, state }, { showSlug, season, episode }) {\n const limit = 1000;\n const params = { limit, showslug: showSlug, season };\n if (episode) {\n params.episode = episode;\n }\n\n const getProviderResults = async provider => {\n let page = 0;\n let lastPage = false;\n const results = [];\n\n const currentProvider = state.providers.find(prov => prov.id === provider.id);\n if (!currentProvider) {\n return results;\n }\n\n const { id: providerId } = currentProvider;\n\n page = 0;\n lastPage = false;\n\n while (!lastPage) {\n let response = null;\n page += 1;\n\n params.page = page;\n\n try {\n response = await rootState.auth.client.api.get(`/providers/${providerId}/results`, { params }); // eslint-disable-line no-await-in-loop\n } catch (error) {\n if (error.response && error.response.status === 404) {\n console.debug(`No results available for provider ${provider}`);\n }\n\n lastPage = true;\n }\n\n if (response) {\n commit(ADD_PROVIDER_CACHE, { providerId, cache: response.data });\n results.push(...response.data);\n\n if (response.data.length < limit) {\n lastPage = true;\n }\n } else {\n lastPage = true;\n }\n }\n return results;\n };\n\n const result = {\n providersSearched: 0,\n totalSearchResults: []\n };\n\n for (const provider of state.providers) {\n if (!provider.config.enabled) {\n continue;\n }\n\n result.providersSearched += 1;\n const providerResults = await getProviderResults(provider); // eslint-disable-line no-await-in-loop\n result.totalSearchResults.push(...providerResults);\n }\n\n return result;\n },\n /**\n * Get provider cache results for enabled providers.\n *\n * @param {*} {commit} Destructured commit object.\n * @param {Object} searchResult - Search result.\n * @returns {void}.\n */\n addManualSearchResult({ commit }, searchResult) {\n commit(ADD_SEARCH_RESULTS, [searchResult]);\n }\n\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import Vue from 'vue';\nimport {\n ADD_RECOMMENDED_SHOW,\n SET_RECOMMENDED_SHOWS,\n SET_RECOMMENDED_SHOWS_TRAKT_REMOVED,\n SET_RECOMMENDED_SHOWS_CATEGORIES\n} from '../mutation-types';\n\nconst IMDB = 10;\nconst ANIDB = 11;\nconst TRAKT = 12;\nconst ANILIST = 13;\nconst ALL = -1;\n\nconst state = {\n limit: 1000,\n page: {\n [IMDB]: 1,\n [ANIDB]: 1,\n [TRAKT]: 1,\n [ANILIST]: 1,\n [ALL]: 1\n },\n shows: [],\n trakt: {\n removedFromMedusa: [],\n blacklistEnabled: false,\n availableLists: []\n },\n categories: {},\n externals: {\n IMDB,\n ANIDB,\n TRAKT,\n ANILIST\n },\n sourceToString: {\n [IMDB]: 'imdb',\n [ANIDB]: 'anidb',\n [TRAKT]: 'trakt',\n [ANILIST]: 'anilist'\n }\n};\n\nconst mutations = {\n [ADD_RECOMMENDED_SHOW](state, show) {\n const existingShow = state.shows.find(({ seriesId, source }) => Number(show.seriesId[show.source]) === Number(seriesId[source]));\n\n if (!existingShow) {\n console.debug(`Adding ${show.title || show.source + String(show.seriesId)} as it wasn't found in the shows array`, show);\n state.shows.push(show);\n return;\n }\n\n // Merge new recommended 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.source + String(show.seriesId)} 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.source + String(newShow.seriesId)}`, newShow);\n },\n [SET_RECOMMENDED_SHOWS](state, { shows, source }) {\n if (shows.length < state.limit) {\n state.page[source] = -1;\n }\n state.shows = [...state.shows, ...shows];\n },\n [SET_RECOMMENDED_SHOWS_TRAKT_REMOVED](state, data) {\n state.trakt.removedFromMedusa = data.removedFromMedusa;\n state.trakt.blacklistEnabled = data.blacklistEnabled;\n state.trakt.availableLists = data.availableLists;\n },\n [SET_RECOMMENDED_SHOWS_CATEGORIES](state, data) {\n state.categories = data;\n },\n increasePage(state, source) {\n state.page[source] += 1;\n },\n resetPage(state, source) {\n state.page[source] = 1;\n }\n};\n\nconst getters = {};\n\nconst actions = {\n /**\n * Get recommended shows from API and commit them to the store.\n *\n * @param {*} context - The store context.\n * @param {String} source - Identifier for the recommended shows list.\n * @returns {(undefined|Promise)} undefined if `shows` was provided or the API response if not.\n */\n getRecommendedShows({ rootState, state, commit }, source) {\n if (state.page[source] === -1) {\n return;\n }\n const identifier = source ? state.sourceToString[source] : '';\n const { page } = state;\n return rootState.auth.client.api.get(`/recommended/${identifier}?page=${page[source]}&limit=${state.limit}`, { timeout: 90000 })\n .then(response => {\n commit(SET_RECOMMENDED_SHOWS, { shows: response.data, source });\n });\n },\n getRecommendedShowsOptions({ rootState, commit }) {\n rootState.auth.client.api.get('/recommended/trakt/removed', { timeout: 60000 })\n .then(response => {\n commit(SET_RECOMMENDED_SHOWS_TRAKT_REMOVED, response.data);\n });\n rootState.auth.client.api.get('/recommended/categories', { timeout: 60000 })\n .then(response => {\n commit(SET_RECOMMENDED_SHOWS_CATEGORIES, response.data);\n });\n },\n /**\n * Get more recommended shows from the paginated api.\n *\n * This method is triggered through a manual user interaction,\n * clicking on a \"Get More\" button.\n *\n * @param {*} param - Commit and dispatch.\n * @param {*} source - Get a specific source (imdb, trakt, all, ..)\n * @returns {Promise} - A promise from the getRecommendedShows method.\n */\n getMoreShows({ commit, dispatch }, source) {\n commit('increasePage', source);\n return dispatch('getRecommendedShows', source);\n }\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import Vue from 'vue';\nimport {\n ADD_SHOW,\n ADD_SHOW_QUEUE_ITEM,\n ADD_SHOW_CONFIG,\n ADD_SHOWS,\n ADD_SHOW_CONFIG_TEMPLATE,\n ADD_SHOW_EPISODE,\n ADD_SHOW_SCENE_EXCEPTION,\n REMOVE_SHOW_SCENE_EXCEPTION,\n REMOVE_SHOW,\n REMOVE_SHOW_CONFIG_TEMPLATE\n} 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 showSlug: null\n },\n loading: {\n total: null,\n current: null,\n display: false,\n finished: false\n },\n queueitems: []\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 // Repair the searchTemplates\n newShow.config.searchTemplates = show.config.searchTemplates ? show.config.searchTemplates : existingShow.config.searchTemplates;\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 [ADD_SHOWS](state, shows) {\n // If the show is already available, we only want to merge values\n const mergedShows = [];\n for (const newShow of shows) {\n const existing = state.shows.find(stateShow => stateShow.id.slug === newShow.id.slug);\n if (existing) {\n const {\n sceneAbsoluteNumbering,\n xemAbsoluteNumbering,\n sceneNumbering,\n ...showWithoutDetailed\n } = newShow;\n\n // Repair searchTemplates.\n const mergedShow = { ...existing, ...showWithoutDetailed };\n mergedShow.config.searchTemplates = showWithoutDetailed.config.searchTemplates ? showWithoutDetailed.config.searchTemplates : existing.config.searchTemplates;\n\n mergedShows.push(mergedShow);\n } else {\n mergedShows.push(newShow);\n }\n }\n state.shows = mergedShows;\n console.debug(`Added ${shows.length} shows to store`);\n },\n [ADD_SHOW_CONFIG](state, { show, config }) {\n const existingShow = state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer]));\n existingShow.config = { ...existingShow.config, ...config };\n },\n currentShow(state, showSlug) {\n state.currentShow.showSlug = showSlug;\n },\n setLoadingTotal(state, total) {\n state.loading.total = total;\n },\n setLoadingCurrent(state, current) {\n state.loading.current = current;\n },\n updateLoadingCurrent(state, current) {\n state.loading.current += current;\n },\n setLoadingDisplay(state, display) {\n state.loading.display = display;\n },\n setLoadingFinished(state, finished) {\n state.loading.finished = finished;\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 let existingSeason = newShow.seasons.find(season => season.season === episode.season);\n\n if (existingSeason) {\n // Shallow copy\n existingSeason = { ...existingSeason };\n\n const foundIndex = existingSeason.children.findIndex(element => element.slug === episode.slug);\n if (foundIndex === -1) {\n existingSeason.children.push(episode);\n } else {\n existingSeason.children.splice(foundIndex, 1, episode);\n }\n } else {\n const newSeason = {\n season: episode.season,\n children: [],\n html: false,\n mode: 'span',\n label: 1\n };\n newShow.seasons.push(newSeason);\n newSeason.children.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 [ADD_SHOW_SCENE_EXCEPTION](state, { show, exception }) {\n // Get current show object\n const currentShow = Object.assign({}, state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer])));\n\n if (currentShow.config.aliases.find(e => e.title === exception.title && e.season === exception.season)) {\n console.warn(`Can't add exception ${exception.title} with season ${exception.season} to show ${currentShow.title} as it already exists.`);\n return;\n }\n\n currentShow.config.aliases.push(exception);\n },\n [REMOVE_SHOW_SCENE_EXCEPTION](state, { show, exception }) {\n // Get current show object\n const currentShow = Object.assign({}, state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer])));\n\n if (!currentShow.config.aliases.find(e => e.title === exception.title && e.season === exception.season)) {\n console.warn(`Can't remove exception ${exception.title} with season ${exception.season} to show ${currentShow.title} as it does not exist.`);\n return;\n }\n\n currentShow.config.aliases.splice(currentShow.config.aliases.indexOf(exception), 1);\n },\n [ADD_SHOW_QUEUE_ITEM](state, queueItem) {\n const existingQueueItem = state.queueitems.find(item => item.identifier === queueItem.identifier);\n\n if (existingQueueItem) {\n Vue.set(state.queueitems, state.queueitems.indexOf(existingQueueItem), { ...existingQueueItem, ...queueItem });\n } else {\n Vue.set(state.queueitems, state.queueitems.length, queueItem);\n }\n },\n [ADD_SHOW_CONFIG_TEMPLATE](state, { show, template }) {\n // Get current show object\n const currentShow = Object.assign({}, state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer])));\n\n if (currentShow.config.searchTemplates.find(t => t.template === template.pattern)) {\n console.warn(`Can't add template (${template.pattern} to show ${currentShow.title} as it already exists.`);\n return;\n }\n\n currentShow.config.searchTemplates.push(template);\n },\n [REMOVE_SHOW_CONFIG_TEMPLATE](state, { show, template }) {\n // Get current show object\n const currentShow = Object.assign({}, state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer])));\n\n if (template.id) {\n currentShow.config.searchTemplates = currentShow.config.searchTemplates.filter(\n t => t.id !== template.id\n );\n return;\n }\n\n currentShow.config.searchTemplates = currentShow.config.searchTemplates.filter(\n t => !(t.title === template.title && t.season === template.season && t.template === template.template)\n );\n },\n [REMOVE_SHOW](state, removedShow) {\n state.shows = state.shows.filter(existingShow => removedShow.id.slug !== existingShow.id.slug);\n },\n initShowsFromStore(state) {\n // Check if the ID exists\n if (localStorage.getItem('shows')) {\n Vue.set(state, 'shows', JSON.parse(localStorage.getItem('shows')));\n }\n }\n};\n\nconst getters = {\n getShowById: state => {\n /**\n * Get a show from the loaded shows state, identified by show slug.\n *\n * @param {string} showSlug Show identifier.\n * @returns {object|undefined} Show object or undefined if not found.\n */\n const getShowById = showSlug => state.shows.find(show => show.id.slug === showSlug);\n return getShowById;\n },\n getShowByTitle: state => title => state.shows.find(show => show.title === title),\n getSeason: state => ({ showSlug, season }) => {\n const show = state.shows.find(show => show.id.slug === showSlug);\n return show && show.seasons ? show.seasons[season] : undefined;\n },\n getEpisode: state => ({ showSlug, season, episode }) => {\n const show = state.shows.find(show => show.id.slug === showSlug);\n return show && show.seasons && show.seasons.find(s => s.season === season) ? show.seasons.find(s => s.season === season).children.find(ep => ep.episode === episode) : undefined;\n },\n getCurrentShow: (state, _, rootState) => {\n return state.shows.find(show => show.id.slug === state.currentShow.showSlug) || rootState.defaults.show;\n },\n getShowIndexerUrl: (state, getters, rootState) => show => {\n const indexerConfig = rootState.config.indexers.indexers;\n if (!show.indexer || !indexerConfig[show.indexer]) {\n return;\n }\n\n const id = show.id[show.indexer];\n const indexerUrl = indexerConfig[show.indexer].showUrl;\n\n if (show.indexer === 'imdb') {\n return `${indexerUrl}${String(id).padStart(7, '0')}`;\n }\n return `${indexerUrl}${id}`;\n },\n showsWithStats: (state, getters, rootState) => {\n if (!state.shows) {\n return [];\n }\n\n return state.shows.map(show => {\n let showStats = rootState.stats.show.stats.find(stat => stat.indexerId === getters.indexerNameToId(show.indexer) && stat.seriesId === show.id[show.indexer]);\n const newLine = '\\u000D';\n let text = 'Unaired';\n let title = '';\n\n if (!showStats) {\n showStats = {\n epDownloaded: 0,\n epSnatched: 0,\n epTotal: 0,\n seriesSize: 0\n };\n }\n\n if (showStats.epTotal >= 1) {\n text = showStats.epDownloaded;\n title = `Downloaded: ${showStats.epDownloaded}`;\n\n if (showStats.epSnatched) {\n text += `+${showStats.epSnatched}`;\n title += `${newLine}Snatched: ${showStats.epSnatched}`;\n }\n\n text += ` / ${showStats.epTotal}`;\n title += `${newLine}Total: ${showStats.epTotal}`;\n }\n\n show.stats = {\n episodes: {\n total: showStats.epTotal,\n snatched: showStats.epSnatched,\n downloaded: showStats.epDownloaded,\n size: showStats.seriesSize\n },\n tooltip: {\n text,\n title,\n percentage: (showStats.epDownloaded * 100) / (showStats.epTotal || 1)\n }\n };\n return show;\n });\n },\n showsInLists: (state, getters, rootState) => {\n const { layout, general } = rootState.config;\n const { show } = layout;\n const { showListOrder } = show;\n const { rootDirs } = general;\n const { selectedRootIndex, local } = layout;\n const { showFilterByName } = local;\n\n const { showsWithStats } = getters;\n\n let shows = null;\n\n // Filter root dirs\n shows = showsWithStats.filter(show => selectedRootIndex === -1 || show.config.location.includes(rootDirs.slice(1)[selectedRootIndex]));\n\n // Filter by text for the banner, simple and smallposter layouts.\n // The Poster layout uses vue-isotope and this does not respond well to changes to the `list` property.\n if (layout.home !== 'poster') {\n shows = shows.filter(show => show.title.toLowerCase().includes(showFilterByName.toLowerCase()));\n }\n\n const categorizedShows = showListOrder.filter(\n listTitle => shows.filter(\n show => show.config.showLists.map(\n list => list.toLowerCase()\n ).includes(listTitle.toLowerCase())\n ).length > 0\n ).map(\n listTitle => ({ listTitle, shows: shows.filter(\n show => show.config.showLists.map(list => list.toLowerCase()).includes(listTitle.toLowerCase())\n ) })\n );\n\n // Check for shows that are not in any category anymore\n const uncategorizedShows = shows.filter(show => {\n return show.config.showLists.map(item => {\n return showListOrder.map(list => list.toLowerCase()).includes(item.toLowerCase());\n }).every(item => !item);\n });\n\n if (uncategorizedShows.length > 0) {\n categorizedShows.push({ listTitle: 'uncategorized', shows: uncategorizedShows });\n }\n\n if (categorizedShows.length === 0 && uncategorizedShows.length === 0) {\n categorizedShows.push({ listTitle: 'Series', shows: [] });\n }\n\n return categorizedShows;\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({ rootState, commit }, { showSlug, detailed, episodes }) {\n return new Promise((resolve, reject) => {\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 rootState.auth.client.api.get(`/series/${showSlug}`, { 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({ rootState, commit, getters }, { showSlug, season }) {\n return new Promise((resolve, reject) => {\n const { getShowById } = getters;\n const show = getShowById(showSlug);\n\n const limit = 1000;\n const params = {\n limit\n };\n\n if (season !== undefined) {\n params.season = season;\n }\n\n // Get episodes\n rootState.auth.client.api.get(`/series/${showSlug}/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 ${showSlug}, 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, state, rootState } = context;\n\n // If no shows are provided get the first 1000\n if (shows) {\n // Return a specific show list. (not used afaik).\n return shows.forEach(show => dispatch('getShow', show));\n }\n\n return new Promise((resolve, _) => {\n // Loading new shows, commit show loading information to state.\n commit('setLoadingTotal', 0);\n commit('setLoadingCurrent', 0);\n commit('setLoadingDisplay', true);\n\n const limit = 1000;\n const page = 1;\n const params = {\n limit,\n page\n };\n\n const pageRequests = [];\n const newShows = [];\n\n // Get first page\n pageRequests.push(rootState.auth.client.api.get('/series', { params })\n .then(response => {\n commit('setLoadingTotal', Number(response.headers['x-pagination-count']));\n const totalPages = Number(response.headers['x-pagination-total']);\n\n newShows.push(...response.data);\n\n commit('updateLoadingCurrent', response.data.length);\n\n // Optionally get additional pages\n for (let page = 2; page <= totalPages; page++) {\n pageRequests.push(new Promise((resolve, reject) => {\n const newPage = { page };\n newPage.limit = params.limit;\n return rootState.auth.client.api.get('/series', { params: newPage })\n .then(response => {\n newShows.push(...response.data);\n commit('updateLoadingCurrent', response.data.length);\n resolve();\n })\n .catch(error => {\n reject(error);\n });\n }));\n }\n })\n .catch(() => {\n console.log('Could not retrieve a list of shows');\n })\n .finally(() => {\n Promise.all(pageRequests)\n .then(() => {\n // Commit all the found shows to store.\n commit(ADD_SHOWS, newShows);\n\n // Update (namespaced) localStorage\n const namespace = rootState.config.system.webRoot ? `${rootState.config.system.webRoot}_` : '';\n try {\n localStorage.setItem(`${namespace}shows`, JSON.stringify(state.shows));\n } catch (error) {\n console.warn(error);\n }\n resolve();\n });\n })\n );\n });\n },\n setShow({ rootState }, { showSlug, data }) {\n // Update show, updated show will arrive over a WebSocket message\n return rootState.auth.client.api.patch(`series/${showSlug}`, data);\n },\n updateShow(context, show) {\n // Update local store\n const { commit } = context;\n return commit(ADD_SHOW, show);\n },\n addSceneException(context, { show, exception }) {\n const { commit } = context;\n commit(ADD_SHOW_SCENE_EXCEPTION, { show, exception });\n },\n removeSceneException(context, { show, exception }) {\n const { commit } = context;\n commit(REMOVE_SHOW_SCENE_EXCEPTION, { show, exception });\n },\n setCurrentShow(context, showSlug) {\n return new Promise(resolve => {\n // Set current show\n const { commit } = context;\n commit('currentShow', showSlug);\n resolve();\n });\n },\n setShowConfig(context, { show, config }) {\n const { commit } = context;\n commit(ADD_SHOW_CONFIG, { show, config });\n },\n removeShow({ commit, rootState, state }, show) {\n // Remove the show from store and localStorage (provided through websocket)\n commit(REMOVE_SHOW, show);\n\n // Update recentShows.\n rootState.config.general.recentShows = rootState.config.general.recentShows.filter(\n recentShow => recentShow.showSlug !== show.id.slug\n );\n const config = {\n recentShows: rootState.config.general.recentShows\n };\n rootState.auth.client.api.patch('config/main', config);\n\n // Update (namespaced) localStorage\n const namespace = rootState.config.system.webRoot ? `${rootState.config.system.webRoot}_` : '';\n localStorage.setItem(`${namespace}shows`, JSON.stringify(state.shows));\n },\n updateShowQueueItem(context, queueItem) {\n // Update store's search queue item. (provided through websocket)\n const { commit } = context;\n return commit(ADD_SHOW_QUEUE_ITEM, queueItem);\n },\n addSearchTemplate({ rootState, getters, commit }, { show, template }) {\n commit(ADD_SHOW_CONFIG_TEMPLATE, { show, template });\n const data = {\n config: {\n searchTemplates: getters.getCurrentShow.config.searchTemplates\n }\n };\n return rootState.auth.client.api.patch(`series/${show.indexer}${show.id[show.indexer]}`, data);\n },\n removeSearchTemplate({ rootState, getters, commit }, { show, template }) {\n commit(REMOVE_SHOW_CONFIG_TEMPLATE, { show, template });\n const data = {\n config: {\n searchTemplates: getters.getCurrentShow.config.searchTemplates\n }\n };\n return rootState.auth.client.api.patch(`series/${show.indexer}${show.id[show.indexer]}`, data);\n }\n\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import { ADD_SCHEDULE } from '../mutation-types';\n\nconst state = {\n categories: ['later', 'missed', 'soon', 'today'],\n later: [],\n missed: [],\n soon: [],\n today: [],\n displayCategory: {\n later: false,\n missed: false,\n soon: true,\n today: true\n }\n};\n\nconst mutations = {\n [ADD_SCHEDULE](state, schedule) {\n for (const category in schedule) { // eslint-disable-line guard-for-in\n state[category] = schedule[category];\n }\n },\n setDisplayCategory(state, { category, value }) {\n state.displayCategory[category] = value;\n }\n};\n\nconst getters = {\n getScheduleFlattened: state => {\n const flattendedSchedule = [];\n const { categories, displayCategory } = state;\n for (const category of categories) {\n if (!displayCategory[category]) {\n continue;\n }\n\n const episodes = state[category];\n for (const episode of episodes) {\n episode.class = category;\n }\n flattendedSchedule.push(...episodes);\n }\n return flattendedSchedule;\n },\n /**\n * Group the coming episodes into an array of objects with an attibute header (the week day)\n * and an attribute episodes with an array of coming episodes.\n * @param {object} state - local state object.\n * @param {object} getters - local getters object.\n * @param {object} rootState - root state object.\n * @returns {array} - Array of grouped episodes by header.\n */\n groupedSchedule: (state, getters, rootState) => {\n const { missed, soon, today, later, displayCategory } = state;\n const { displayPaused } = rootState.config.layout.comingEps;\n\n const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];\n\n /* Return an array of the days to come */\n const comingDays = (currentDay, nrComingDays) => {\n let currentDayOfTheWeek = currentDay.getDay();\n const returnDays = [];\n for (let i = 0; i < nrComingDays; i++) {\n if (currentDayOfTheWeek > 7) {\n currentDayOfTheWeek = 1;\n }\n returnDays.push(currentDayOfTheWeek);\n currentDayOfTheWeek += 1;\n }\n return returnDays;\n };\n\n const _MS_PER_DAY = 1000 * 60 * 60 * 24;\n\n // A and b are javascript Date objects\n function dateDiffInDays(a, b) {\n // Discard the time and time-zone information.\n const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());\n const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());\n\n return Math.floor((utc2 - utc1) / _MS_PER_DAY);\n }\n\n const newArray = [];\n const combinedEpisodes = [];\n\n if (displayCategory.missed) {\n combinedEpisodes.push(...missed);\n }\n\n if (displayCategory.today) {\n combinedEpisodes.push(...today);\n }\n\n if (displayCategory.soon) {\n combinedEpisodes.push(...soon);\n }\n\n if (displayCategory.later) {\n combinedEpisodes.push(...later);\n }\n\n const filteredEpisodes = combinedEpisodes.filter(item => !item.paused || displayPaused);\n if (filteredEpisodes.length === 0) {\n return [];\n }\n\n let currentDay = new Date(filteredEpisodes[0].airdate);\n\n // Group the coming episodes by day.\n for (const episode of filteredEpisodes) {\n // Calculate the gaps in the week, for which we don't have any scheduled shows.\n if (currentDay !== new Date(episode.airdate)) {\n const diffDays = dateDiffInDays(currentDay, new Date(episode.airdate));\n if (diffDays > 1) {\n let dayHeader = days[comingDays(currentDay, diffDays)[1] - 1];\n\n // Add the elipses if there is a wider gap then 1 day.\n if (diffDays > 2) {\n dayHeader = `${dayHeader} ...`;\n }\n\n newArray.push({\n header: dayHeader,\n class: 'soon',\n episodes: []\n });\n }\n }\n\n currentDay = new Date(episode.airdate);\n\n let weekDay = newArray.find(item => item.airdate === episode.airdate);\n if (!weekDay) {\n weekDay = {\n airdate: episode.airdate,\n header: days[episode.weekday],\n class: 'soon',\n episodes: []\n };\n newArray.push(weekDay);\n }\n\n episode.airsTime = episode.airs.split(' ').slice(-2).join(' ');\n weekDay.episodes.push(episode);\n }\n return newArray;\n },\n /**\n * Group and sort the coming episodes into an array of objects with an attibute header (the week day).\n * Group the coming episodes into groups of missed, today, soon (subgrouped per day) and later.\n * @param {object} state - local state object.\n * @param {object} getters - local getters object.\n * @param {object} rootState - root state object.\n * @returns {array} - Array of grouped episodes by header.\n */\n sortedSchedule: (state, getters, rootState) => sort => {\n const {\n displayCategory,\n missed, today, soon, later\n } = state;\n const { displayPaused } = rootState.config.layout.comingEps;\n const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];\n const newArray = [];\n\n if (sort === 'date') {\n if (displayCategory.missed) {\n // Group missed episodes (one group)\n newArray.push({\n header: 'missed',\n class: 'missed',\n episodes: missed.filter(item => !item.paused || displayPaused)\n });\n }\n\n if (displayCategory.today) {\n // Group missed episodes (one group)\n newArray.push({\n header: 'today',\n class: 'today',\n episodes: today.filter(item => !item.paused || displayPaused)\n });\n }\n\n if (displayCategory.soon) {\n // Group the coming episodes by day.\n for (const episode of soon.filter(item => !item.paused || displayPaused)) {\n let weekDay = newArray.find(item => item.header === days[episode.weekday]);\n if (!weekDay) {\n weekDay = {\n header: days[episode.weekday],\n class: 'soon',\n episodes: []\n };\n newArray.push(weekDay);\n }\n weekDay.episodes.push(episode);\n }\n }\n\n if (displayCategory.later) {\n // Group later episodes (one group)\n newArray.push({\n header: 'later',\n class: 'later',\n episodes: later.filter(item => !item.paused || displayPaused)\n });\n }\n return newArray;\n }\n\n if (sort === 'network') {\n const { getScheduleFlattened } = getters;\n const filteredSchedule = getScheduleFlattened.filter(item => !item.paused || displayPaused);\n\n for (const episode of filteredSchedule.sort((a, b) => a.network.localeCompare(b.network))) {\n let network = newArray.find(item => item.header === episode.network);\n if (!network) {\n network = {\n header: episode.network,\n class: episode.class,\n episodes: []\n };\n newArray.push(network);\n }\n network.episodes.push(episode);\n }\n return newArray;\n }\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 schedule 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 async getSchedule({ rootState, commit, state }) {\n const params = {\n category: state.categories,\n paused: true\n };\n const response = await rootState.auth.client.api.get('/schedule', { params });\n if (response.data) {\n commit(ADD_SCHEDULE, response.data);\n }\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 { ADD_STATS, SET_STATS, SET_MAX_DOWNLOAD_COUNT } 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 show: {\n maxDownloadCount: 0,\n stats: []\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 [SET_STATS](state, stats) {\n state.stats = stats;\n },\n [SET_MAX_DOWNLOAD_COUNT](state, downloadCount) {\n state.maxDownloadCount = downloadCount;\n }\n};\n\nconst getters = {};\n\nconst actions = {\n getStats({ rootState, commit }, type) {\n return rootState.auth.client.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, UPDATE_SHOWLIST_DEFAULT } from '../../mutation-types';\n\nconst state = {\n anidb: {\n enabled: false,\n username: null,\n password: null,\n useMylist: false\n },\n autoAnimeToList: false,\n showlistDefaultAnime: []\n};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'anime') {\n state = Object.assign(state, config);\n }\n },\n [UPDATE_SHOWLIST_DEFAULT](state, value) {\n state.showlistDefaultAnime = value;\n }\n};\n\nconst getters = {};\n\nconst actions = {\n updateShowlistDefault(context, showlistDefaultAnime) {\n const { commit } = context;\n return commit(UPDATE_SHOWLIST_DEFAULT, showlistDefaultAnime);\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 { 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 clientStatuses: []\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 /**\n * Get an episode overview status using the episode status and quality.\n *\n * @typedef {Object} - Episode status\n * @property {Object} quality - Episode quality\n * @property {Object} configQualities - Shows configured qualities (allowed and preferred)\n * @returns {String} The overview status\n */\n // eslint-disable-next-line no-unused-vars\n getOverviewStatus: _state => (status, quality, configQualities) => {\n if (['Unset', 'Unaired'].includes(status)) {\n return 'Unaired';\n }\n\n if (['Skipped', 'Ignored'].includes(status)) {\n return 'Skipped';\n }\n\n if (['Archived'].includes(status)) {\n return 'Preferred';\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 // Check if the show has been configured with only allowed qualities.\n if (configQualities.allowed.length > 0 && configQualities.preferred.length === 0) {\n // This is a hack, because technically the quality does not fit in the Preferred quality.\n // But because 'preferred' translates to the css color \"green\", we use it.\n if (configQualities.allowed.includes(quality)) {\n return 'Preferred';\n }\n }\n\n if (configQualities.preferred.includes(quality)) {\n return 'Preferred';\n }\n\n if (configQualities.allowed.includes(quality)) {\n return 'Allowed';\n }\n\n return 'Wanted';\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","import { ADD_CONFIG } from '../../mutation-types';\n\nconst state = {\n main: {\n externalMappings: {},\n statusMap: {},\n traktIndexers: {},\n validLanguages: [],\n langabbvToId: {},\n recommendedLists: {}\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 imdb: {\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\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'indexers') {\n state = Object.assign(state, config);\n }\n }\n};\n\nconst getters = {\n // Get an indexer's name using its ID.\n indexerIdToName: state => indexerId => {\n if (!indexerId) {\n return undefined;\n }\n const { indexers } = state;\n return Object.keys(indexers).find(name => indexers[name].id === Number.parseInt(indexerId, 10));\n },\n // Get an indexer's ID using its name.\n indexerNameToId: state => indexerName => {\n const { indexers } = state;\n if (!indexerName || !indexers) {\n return undefined;\n }\n return indexers[indexerName].id;\n },\n /**\n * Return the indexers showUrl.\n * @param {object} state - State object.\n * @returns {string|undefined} Indexers show url or undefined if not found.\n */\n getIndexer: state => indexerId => {\n return Object.values(state.indexers).find(indexer => indexer.id === indexerId);\n }\n};\n\nconst actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import { ADD_CONFIG, UPDATE_LAYOUT_LOCAL } from '../../mutation-types';\nimport formatDate from 'date-fns/format';\nimport parseISO from 'date-fns/parseISO';\nimport TimeAgo from 'javascript-time-ago';\nimport en from 'javascript-time-ago/locale/en';\n\nimport { convertDateFormat, divmod } from '../../../utils/core';\n\n// Add locale-specific relative date/time formatting rules.\nTimeAgo.addDefaultLocale(en);\n\nconst state = {\n show: {\n specials: null,\n showListOrder: [],\n pagination: {\n enable: null\n }\n },\n home: null,\n selectedRootIndex: null,\n history: null,\n historyLimit: null,\n schedule: null,\n wide: null,\n timezoneDisplay: null,\n timeStyle: null,\n dateStyle: null,\n themeName: null,\n splitHomeInTabs: null,\n animeSplitHome: null,\n fanartBackground: null,\n fanartBackgroundOpacity: null,\n trimZero: null,\n sortArticle: null,\n fuzzyDating: null,\n comingEps: {\n missedRange: null,\n sort: null,\n displayPaused: null,\n layout: null\n },\n backlogOverview: {\n status: null,\n period: null\n },\n posterSortdir: null,\n posterSortby: null,\n // Local config store properties, are saved to.\n local: {\n showFilterByName: '',\n posterSize: 188,\n currentShowTab: null\n }\n};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'layout') {\n state = Object.assign(state, config);\n }\n },\n [UPDATE_LAYOUT_LOCAL](state, local) {\n state.local = { ...state.local, ...local };\n }\n};\n\nconst getters = {\n fuzzyParseDateTime: state => (airDate, showSeconds = false) => {\n const timeAgo = new TimeAgo('en-US');\n const { dateStyle, fuzzyDating, timeStyle } = state;\n\n if (!airDate || !dateStyle) {\n return '';\n }\n\n if (fuzzyDating) {\n return timeAgo.format(new Date(airDate));\n }\n\n if (dateStyle === '%x') {\n return new Date(airDate).toLocaleString();\n }\n\n // Only the history page should show seconds.\n const formatTimeStyle = showSeconds ? timeStyle : timeStyle.replace(':%S', '');\n const fdate = parseISO(airDate);\n return formatDate(fdate, convertDateFormat(`${dateStyle} ${formatTimeStyle}`));\n },\n getShowFilterByName: state => {\n return state.local.showFilterByName;\n },\n /**\n * PrettyTimeDelta\n *\n * Translate seconds into a pretty hours, minutes, seconds representation.\n * @param {object} state - State object.\n * @returns {number} seconds - Number of seconds to translate.\n */\n prettyTimeDelta: state => seconds => { // eslint-disable-line no-unused-vars\n let signStr = '';\n if (seconds < 0) {\n signStr = '-';\n }\n\n let days = 0;\n let hours = 0;\n let minutes = 0;\n\n const daysSeconds = divmod(seconds, 86400);\n days = daysSeconds.quotient;\n seconds = daysSeconds.remainder;\n\n const hoursSeconds = divmod(seconds, 3600);\n hours = hoursSeconds.quotient;\n seconds = hoursSeconds.remainder;\n\n const minuteSeconds = divmod(seconds, 60);\n minutes = minuteSeconds.quotient;\n seconds = minuteSeconds.remainder;\n\n if (days > 0) {\n signStr += ` ${days}d`;\n }\n\n if (hours > 0) {\n signStr += ` ${hours}h`;\n }\n\n if (minutes > 0) {\n signStr += ` ${minutes}m`;\n }\n\n if (seconds > 0) {\n signStr += ` ${seconds}s`;\n }\n\n return signStr;\n }\n};\n\nconst actions = {\n setLayout({ rootState, commit }, { page, layout }) {\n // Don't wait for the api, just commit to store.\n commit(ADD_CONFIG, {\n section: 'layout', config: { [page]: layout }\n });\n\n return rootState.auth.client.api.patch('config/main', { layout: { [page]: layout } });\n },\n setTheme({ rootState, commit }, { themeName }) {\n return rootState.auth.client.api.patch('config/main', { layout: { themeName } })\n .then(() => {\n return commit(ADD_CONFIG, {\n section: 'layout', config: { themeName }\n });\n });\n },\n setSpecials({ rootState, commit, state }, specials) {\n const show = Object.assign({}, state.show);\n show.specials = specials;\n\n return rootState.auth.client.api.patch('config/main', { layout: { show } })\n .then(() => {\n return commit(ADD_CONFIG, {\n section: 'layout', config: { show }\n });\n });\n },\n setPosterSortBy({ rootState, commit }, { value }) {\n return rootState.auth.client.api.patch('config/main', { layout: { posterSortby: value } })\n .then(() => {\n return commit(ADD_CONFIG, {\n section: 'layout', config: { posterSortby: value }\n });\n });\n },\n setPosterSortDir({ rootState, commit }, { value }) {\n return rootState.auth.client.api.patch('config/main', { layout: { posterSortdir: value } })\n .then(() => {\n return commit(ADD_CONFIG, {\n section: 'layout', config: { posterSortdir: value }\n });\n });\n },\n setLayoutShow({ rootState, commit }, value) {\n return rootState.auth.client.api.patch('config/main', { layout: { show: value } })\n .then(() => {\n return commit(ADD_CONFIG, {\n section: 'layout', config: { show: value }\n });\n });\n },\n setStoreLayout({ rootState, commit }, { key, value }) {\n return rootState.auth.client.api.patch('config/main', { layout: { [key]: value } })\n .then(() => {\n return commit(ADD_CONFIG, {\n section: 'layout', config: { [key]: value }\n });\n });\n },\n setLayoutLocal(context, { key, value }) {\n const { commit } = context;\n return commit(UPDATE_LAYOUT_LOCAL, { [key]: value });\n },\n setBacklogOverview({ rootState, commit, state }, { key, value }) {\n const backlogOverview = { ...state.backlogOverview };\n backlogOverview[key] = value;\n commit(ADD_CONFIG, {\n section: 'layout', config: { backlogOverview }\n });\n return rootState.auth.client.api.patch('config/main', { layout: { backlogOverview } });\n }\n};\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 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 overrideAvatar: 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 anime from './anime';\nimport clients from './clients';\nimport consts from './consts';\nimport indexers from './indexers';\nimport layout from './layout';\nimport general from './general';\nimport metadata from './metadata';\nimport notifiers from './notifiers';\nimport postprocessing from './postprocessing';\nimport search from './search';\nimport subtitles from './subtitles';\nimport system from './system';\n\nconst modules = {\n anime,\n clients,\n consts,\n indexers,\n layout,\n general,\n metadata,\n notifiers,\n postprocessing,\n search,\n subtitles,\n system\n};\n\nconst state = {};\n\nconst mutations = {};\n\nconst getters = {};\n\nconst actions = {};\n\nexport default {\n state,\n mutations,\n getters,\n actions,\n modules\n};\n","import { ADD_CONFIG } from '../../mutation-types';\nimport { arrayUnique, arrayExclude } from '../../../utils/core';\n\nconst state = {\n addTitleWithYear: null,\n wikiUrl: null,\n donationsUrl: null,\n namingForceFolders: null,\n sourceUrl: null,\n rootDirs: [],\n brokenProviders: [],\n logs: {\n debug: null,\n dbDebug: null,\n loggingLevels: {},\n numErrors: null,\n numWarnings: null,\n actualLogDir: null,\n nr: null,\n size: null,\n subliminalLog: null,\n privacyLevel: null,\n custom: {}\n },\n cpuPreset: null,\n anonRedirect: 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 showLists: null\n },\n launchBrowser: null,\n defaultPage: null,\n trashRemoveShow: null,\n indexerDefaultLanguage: null,\n showUpdateHour: null,\n indexerTimeout: null,\n indexerDefault: null,\n plexFallBack: {\n enable: null,\n notifications: null,\n timeout: null\n },\n recommended: {\n cache: {\n shows: null,\n trakt: null,\n imdb: null,\n anidb: null,\n anilist: null,\n purgeAfterDays: null\n },\n trakt: {\n selectedLists: [],\n availableLists: []\n }\n },\n versionNotify: null,\n autoUpdate: null,\n updateFrequency: null,\n notifyOnUpdate: null,\n availableThemes: null,\n timePresets: [],\n datePresets: [],\n webInterface: {\n apiKey: null,\n log: null,\n username: null,\n password: null,\n port: null,\n host: null,\n notifyOnLogin: null,\n ipv6: null,\n httpsEnable: null,\n httpsCert: null,\n httpsKey: null,\n handleReverseProxy: null\n },\n sslVerify: null,\n sslCaBundle: null,\n noRestart: null,\n encryptionVersion: null,\n calendarUnprotected: null,\n calendarIcons: null,\n proxySetting: null,\n proxyProviders: null,\n proxyClients: null,\n proxyIndexers: null,\n proxyOthers: null,\n skipRemovedFiles: null,\n epDefaultDeletedStatus: null,\n developer: null,\n experimental: null,\n git: {\n username: null,\n password: null,\n token: null,\n authType: null,\n remote: null,\n path: null,\n org: null,\n reset: null,\n resetBranches: null,\n url: null\n },\n // Remove backlogOverview after manage_backlogOverview.mako is gone.\n backlogOverview: {\n status: null,\n period: null\n },\n // Remove themeName when we get fully rid of MEDUSA.config.\n themeName: null,\n providers: {\n prowlarr: {\n url: null,\n apikey: null\n }\n },\n backup: {\n cacheDb: null,\n cacheFiles: 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 addRecentShow(state, { show }) {\n state.recentShows = state.recentShows.filter(\n filterShow =>\n !(filterShow.showSlug === show.showSlug && filterShow.name === show.name)\n );\n\n state.recentShows.unshift(show); // Add the new show object to the start of the array.\n state.recentShows = state.recentShows.slice(0, 5); // Cut the array of at 5 items.\n },\n updateTraktSelectedLists(state, selectedLists) {\n state.recommended.trakt.selectedLists = selectedLists;\n }\n};\n\nconst getters = {\n effectiveIgnored: (state, _, rootState) => series => {\n const seriesIgnored = series.config.release.ignoredWords.map(x => x.toLowerCase());\n const globalIgnored = rootState.config.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 seriesRequired = series.config.release.requiredWords.map(x => x.toLowerCase());\n const globalRequired = rootState.config.search.filters.required.map(x => x.toLowerCase());\n if (!series.config.release.requiredWordsExclude) {\n return arrayUnique(globalRequired.concat(seriesRequired));\n }\n return arrayExclude(globalRequired, seriesRequired);\n }\n};\n\nconst actions = {\n getConfig({ rootState, commit }, section) {\n return rootState.auth.client.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({ rootState }, { section, config }) {\n return rootState.auth.client.api.patch(`config/${section}`, config);\n },\n updateConfig({ commit }, { section, config }) {\n return commit(ADD_CONFIG, { section, config });\n },\n getApiKey({ rootState, commit }) {\n const section = 'main';\n const config = { webInterface: { apiKey: '' } };\n return rootState.auth.client.apiRoute.get('config/general/generate_api_key')\n .then(response => {\n config.webInterface.apiKey = response.data;\n return commit(ADD_CONFIG, { section, config });\n });\n },\n setRecentShow({ rootState, commit, state }, show) {\n commit('addRecentShow', { show });\n const config = {\n recentShows: state.recentShows\n };\n return rootState.auth.client.api.patch('config/main', config);\n },\n setCustomLogs({ rootState, commit }, logs) {\n // Convert back to object.\n const reducedLogs = logs.reduce((obj, item) => ({ ...obj, [item.identifier]: item.level }), {});\n\n return rootState.auth.client.api.patch('config/main', { logs: { custom: logs } })\n .then(() => {\n return commit(ADD_CONFIG, {\n section: 'main', config: { logs: { custom: reducedLogs } }\n });\n });\n },\n setTraktSelectedLists({ rootState, commit }, selectedLists) {\n return rootState.auth.client.api.patch('config/main', { recommended: { trakt: { selectedLists } } })\n .then(() => {\n return commit('updateTraktSelectedLists', selectedLists);\n });\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 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 { 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","import { ADD_CONFIG } from '../../mutation-types';\n\nconst state = {\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 specificProcessMethod: null,\n processMethodTorrent: null,\n processMethodNzb: 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 downloadHandler: {\n enabled: null,\n frequency: null,\n minFrequency: null,\n torrentSeedRatio: null,\n torrentSeedAction: null\n },\n ffmpeg: {\n checkStreams: null,\n path: ''\n }\n};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'postprocessing') {\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 { 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 backlogFrequency: 720,\n cacheTrimming: false,\n downloadPropers: true,\n failedDownloads: {\n enabled: null,\n deleteFailed: null\n },\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 { ADD_CONFIG } from '../../mutation-types';\n\nconst state = {\n acceptUnknownEmbeddedSubs: null,\n codeFilter: [],\n enabled: null,\n eraseCache: null,\n extraScripts: [],\n finderFrequency: null,\n hearingImpaired: null,\n ignoreEmbeddedSubs: null,\n keepOnlyWanted: null,\n location: null,\n logHistory: null,\n multiLanguage: null,\n perfectMatch: null,\n preScripts: [],\n providerLogins: {\n addic7ed: { user: '', pass: '' },\n legendastv: { user: '', pass: '' },\n opensubtitles: { user: '', pass: '' }\n },\n services: [],\n stopAtFirst: null,\n wantedLanguages: [],\n wikiUrl: ''\n};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'subtitles') {\n state = Object.assign(state, config);\n }\n }\n};\n\nconst getters = {};\n\nconst actions = {\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import { ADD_CONFIG, ADD_REMOTE_BRANCHES, ADD_SHOW_QUEUE_ITEM } from '../../mutation-types';\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 branch: null,\n memoryUsage: null,\n schedulers: [],\n showQueue: [],\n diskSpace: [],\n sslVersion: null,\n pythonVersion: null,\n pid: null,\n os: null,\n logDir: null,\n dbPath: null,\n configFile: null,\n databaseVersion: {\n major: null,\n minor: null\n },\n locale: null,\n timezone: null,\n localUser: null,\n programDir: null,\n dataDir: null,\n cacheDir: null,\n appArgs: [],\n webRoot: null,\n runsInDocker: null,\n newestVersionMessage: null,\n gitRemoteBranches: [],\n cpuPresets: null,\n news: {\n lastRead: null,\n latest: null,\n unread: null\n },\n ffprobeVersion: null\n};\n\nconst mutations = {\n [ADD_CONFIG](state, { section, config }) {\n if (section === 'system') {\n state = Object.assign(state, config);\n }\n },\n [ADD_REMOTE_BRANCHES](state, branches) {\n state.gitRemoteBranches = branches;\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 getGitRemoteBranches({ rootState, commit }) {\n return rootState.auth.client.apiRoute('home/branchForceUpdate')\n .then(response => {\n if (response.data && response.data.branches.length > 0) {\n commit(ADD_REMOTE_BRANCHES, response.data.branches);\n return response.data.branches;\n }\n });\n },\n getShowQueue({ rootState, commit }) {\n return rootState.auth.client.api.get('/config/system/showQueue').then(res => {\n const showQueue = res.data;\n const config = { showQueue };\n commit(ADD_CONFIG, { section: 'system', config });\n return showQueue;\n });\n },\n updateQueueItemShow({ commit }, queueItem) {\n // Update store's show queue item. (provided through websocket)\n return commit(ADD_SHOW_QUEUE_ITEM, queueItem);\n }\n\n};\n\nexport default {\n state,\n mutations,\n getters,\n actions\n};\n","import Vue from 'vue';\nimport { ADD_QUEUE_ITEM, SET_LAST_ITEM } from '../mutation-types';\n\nconst state = {\n queueitems: [],\n last: null\n};\n\nconst mutations = {\n [ADD_QUEUE_ITEM](state, queueItem) {\n const existingQueueItem = state.queueitems.find(item => item.identifier === queueItem.identifier);\n\n if (existingQueueItem) {\n Vue.set(state.queueitems, state.queueitems.indexOf(existingQueueItem), { ...existingQueueItem, ...queueItem });\n } else {\n Vue.set(state.queueitems, state.queueitems.length, queueItem);\n }\n },\n [SET_LAST_ITEM](state, queueItem) {\n state.last = queueItem;\n }\n};\n\nconst getters = {\n getQueueItemsByName: state => name => state.queueitems.filter(q => name.includes(q.name)),\n getQueueItemsByIdentifier: state => identifier => state.queueitems.filter(q => q.identifier === identifier)\n};\n\nconst actions = {\n updateQueueItem(context, queueItem) {\n // Update store's search queue item. (provided through websocket)\n const { commit } = context;\n commit(SET_LAST_ITEM, queueItem);\n return commit(ADD_QUEUE_ITEM, queueItem);\n }\n};\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 config,\n defaults,\n history,\n notifications,\n provider,\n recommended,\n schedule,\n shows,\n socket,\n stats,\n queue\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 config,\n defaults,\n history,\n notifications,\n provider,\n recommended,\n schedule,\n shows,\n socket,\n stats,\n queue\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' || event === 'showAdded') {\n this.store.dispatch('updateShow', data);\n } else if (event === 'showRemoved') {\n // We need this for the QueueItemChangeIndexer\n this.store.dispatch('removeShow', data);\n } else if (event === 'addManualSearchResult') {\n this.store.dispatch('addManualSearchResult', data);\n } else if (event === 'QueueItemUpdate') {\n this.store.dispatch('updateQueueItem', data);\n } else if (event === 'QueueItemShow') {\n // Used as a generic showqueue item. If you want to know the specific action (update, refresh, remove, etc.)\n // Use queueItem.name. Like queueItem.name === 'REFRESH'.\n if (data.name === 'REMOVE-SHOW') {\n this.store.dispatch('removeShow', data.show);\n } else {\n this.store.dispatch('updateShowQueueItem', data);\n }\n } else if (event === 'historyUpdate') {\n this.store.dispatch('updateHistory', 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 let webRoot = document.body.getAttribute('web-root');\n if (webRoot) {\n if (!webRoot.startsWith('/')) {\n webRoot = `/${webRoot}`;\n }\n }\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","const AUTHENTICATE = '🔒 Authenticating';\nconst 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 UPDATE_LAYOUT_LOCAL = '⚙️ Local layout updated in store';\nconst ADD_REMOTE_BRANCHES = '⚙️ Add git remote branches to store';\nconst ADD_SHOW = '📺 Show added to store';\nconst ADD_SHOW_CONFIG = '📺 Show config updated in store';\nconst ADD_SHOWS = '📺 Multiple Shows added to store in bulk';\nconst ADD_SHOW_EPISODE = '📺 Shows season with episodes added to store';\nconst ADD_STATS = 'ℹ️ Statistics added to store';\nconst ADD_RECOMMENDED_SHOW = '📺 Recommended Show added to store';\nconst SET_RECOMMENDED_SHOWS = '📺 Recommended Shows added to store';\nconst SET_RECOMMENDED_SHOWS_TRAKT_REMOVED = '📺 Recommended Show Trakt removed added to store';\nconst SET_RECOMMENDED_SHOWS_CATEGORIES = '📺 Recommended Show categories added to store';\nconst SET_STATS = 'SET_STATS';\nconst SET_MAX_DOWNLOAD_COUNT = 'SET_MAX_DOWNLOAD_COUNT';\nconst ADD_SHOW_SCENE_EXCEPTION = '📺 Add a scene exception';\nconst REMOVE_SHOW_SCENE_EXCEPTION = '📺 Remove a scene exception';\nconst REMOVE_SHOW_CONFIG_TEMPLATE = '📺 Remove a shows search template';\nconst ADD_HISTORY = '📺 History added to store';\nconst ADD_HISTORY_ROW = '📺 History row added to store';\nconst ADD_SHOW_CONFIG_TEMPLATE = '📺 Search template added to shows templates';\nconst ADD_SHOW_HISTORY = '📺 Show specific History added to store';\nconst ADD_SHOW_EPISODE_HISTORY = \"📺 Show's episode specific History added to store\";\nconst ADD_PROVIDER = '⛽ Provider added to store';\nconst ADD_PROVIDERS = '⛽ Provider list added to store';\nconst ADD_PROVIDER_CACHE = '⛽ Provider cache results added to store';\nconst ADD_SEARCH_RESULTS = '⛽ New search results added for provider';\nconst ADD_QUEUE_ITEM = '🔍 Search queue item updated';\nconst SET_LAST_ITEM = '🔍 Search queue item set for last item';\nconst ADD_SHOW_QUEUE_ITEM = '📺 Show queue item added to store';\nconst REMOVE_SHOW = '📺 Show removed from store';\nconst REMOVE_PROVIDER = '⛽ Provider removed';\nconst UPDATE_SHOWLIST_DEFAULT = '⚙️ Anime config showlist default updated';\nconst ADD_SCHEDULE = '📅 Schedule information added';\n\nexport {\n AUTHENTICATE,\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 UPDATE_LAYOUT_LOCAL,\n ADD_HISTORY,\n ADD_HISTORY_ROW,\n ADD_SHOW,\n ADD_STATS,\n ADD_SHOW_CONFIG,\n ADD_RECOMMENDED_SHOW,\n SET_RECOMMENDED_SHOWS,\n SET_RECOMMENDED_SHOWS_TRAKT_REMOVED,\n SET_RECOMMENDED_SHOWS_CATEGORIES,\n ADD_SHOWS,\n ADD_SHOW_EPISODE,\n ADD_REMOTE_BRANCHES,\n SET_STATS,\n SET_MAX_DOWNLOAD_COUNT,\n ADD_SHOW_SCENE_EXCEPTION,\n REMOVE_SHOW_SCENE_EXCEPTION,\n REMOVE_SHOW_CONFIG_TEMPLATE,\n ADD_SHOW_HISTORY,\n ADD_SHOW_EPISODE_HISTORY,\n ADD_PROVIDER,\n ADD_PROVIDERS,\n ADD_PROVIDER_CACHE,\n ADD_SEARCH_RESULTS,\n ADD_QUEUE_ITEM,\n SET_LAST_ITEM,\n ADD_SHOW_QUEUE_ITEM,\n REMOVE_SHOW,\n REMOVE_PROVIDER,\n UPDATE_SHOWLIST_DEFAULT,\n ADD_SHOW_CONFIG_TEMPLATE,\n ADD_SCHEDULE\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((a, c) => reducer(a, c), 0);\n const preferred = preferredQualities.reduce((a, c) => reducer(a, c), 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\n/**\n * Transform a season and episode number to an episode slug.\n * If the episode number is not provided, return a season slug.\n * @param {number} season - Season number.\n * @param {number} episode - Episode number.\n * @returns {string} Episode or Season slug.\n */\nexport const episodeToSlug = (season, episode) => {\n if (episode) {\n return `s${season.toString().padStart(2, '0')}e${episode.toString().padStart(2, '0')}`;\n }\n return `s${season.toString().padStart(2, '0')}`;\n};\n\n/**\n * Transform a showSlug to separate indexer and showId.\n * @param {string} showSlug - Show slug.\n * @returns {object} Object with attributes indexer and showId.\n */\nexport const showSlugToId = showSlug => {\n const show = { indexer: undefined, showId: undefined };\n if (showSlug === undefined) {\n return show;\n }\n\n const match = showSlug.toLowerCase().match(/([a-z]+)(\\d+)/);\n show.indexer = match[1];\n show.showId = match[2];\n return show;\n};\n\n/**\n * Force reload.\n * Force a reload of the page and ignore local cache.\n * window.location.reload(true) doesn't seem to work on chrome. But the self assign does.\n*/\nexport const forceBrowserReload = () => {\n if (Boolean(window.chrome) && Boolean(window.chrome.webstore)) {\n window.location.href = window.location.href; // eslint-disable-line no-self-assign\n } else {\n window.location.reload(true);\n }\n};\n\n/**\n * Divmod\n *\n * Calculate a divider and mod.\n * @param {number} seconds - Number of seconds to divmod.\n * @param {number} div - Divider and modulus.\n * @returns {object} - And object with keys quotient and remainder.\n */\nexport const divmod = (seconds, div) => {\n const quotient = Math.floor(seconds / div);\n const remainder = seconds % div;\n return { quotient, remainder };\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","// Imports\nimport ___CSS_LOADER_API_SOURCEMAP_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/sourceMaps.js\";\nimport ___CSS_LOADER_API_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/api.js\";\nimport ___CSS_LOADER_GET_URL_IMPORT___ from \"../../node_modules/css-loader/dist/runtime/getUrl.js\";\nvar ___CSS_LOADER_URL_IMPORT_0___ = new URL(\"data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7\", import.meta.url);\nvar ___CSS_LOADER_URL_IMPORT_1___ = new URL(\"data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7\", import.meta.url);\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\nvar ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___);\nvar ___CSS_LOADER_URL_REPLACEMENT_1___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_1___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \".vgt-table-styling .vgt-table{width:100%;margin-right:auto;margin-left:auto;text-align:left;border-spacing:0}.vgt-table-styling .vgt-table th,.vgt-table-styling .vgt-table td{padding:4px;vertical-align:middle}.vgt-table-styling .vgt-table th:first-child,.vgt-table-styling .vgt-table td:first-child{border-left:none}.vgt-table-styling .vgt-table th{text-align:center;border-collapse:collapse;font-weight:normal;position:relative}.vgt-table-styling .vgt-table span.break-word{word-wrap:break-word}.vgt-table-styling .vgt-table thead th.sorting.sorting-asc{background-position-x:right;background-position-y:bottom}.vgt-table-styling .vgt-table thead th.sorting{background-repeat:no-repeat}.vgt-table-styling .vgt-table thead th.sorting.sorting-desc{background-color:#555;background-image:url(\" + ___CSS_LOADER_URL_REPLACEMENT_0___ + \")}.vgt-table-styling .vgt-table thead th.sorting.sorting-asc{background-color:#555;background-image:url(\" + ___CSS_LOADER_URL_REPLACEMENT_1___ + \");background-position-x:right;background-position-y:bottom}.vgt-table-styling .vgt-table th.sortable button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:none;position:absolute;top:0;left:0;width:100%;height:100%}.vgt-table-styling .vgt-table thead th{padding:4px;cursor:default}.vgt-table-styling .vgt-table input.tablesorter-filter{width:98%;height:auto;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.vgt-table-styling .vgt-table tr.tablesorter-filter-row,.vgt-table-styling .vgt-table tr.tablesorter-filter-row td{text-align:center}.vgt-table-styling .vgt-table input.tablesorter-filter-row .disabled{display:none}.vgt-table-styling .vgt-table tfoot tr{text-align:center;border-collapse:collapse}.vgt-table-styling .vgt-table tfoot a{text-decoration:none}.vgt-table-styling .vgt-table th.vgt-row-header{text-align:left}.vgt-table-styling .vgt-table .season-header{display:inline;margin-left:5px}.vgt-table-styling .vgt-table tr.spacer{height:25px}.vgt-table-styling .vgt-table tr:hover{opacity:.9}.vgt-table-styling .vgt-table tr.status td>span{color:#000}.vgt-table-styling .vgt-table span.episode-title>a{text-decoration:none}.vgt-table-styling .vgt-table span.episode-title>a:hover,.vgt-table-styling .vgt-table span.episode-title>a:focus{text-decoration:underline}.vgt-table-styling .vgt-table .skipped{background-color:#bedeed}.vgt-table-styling .vgt-table .snatched{background-color:#ebc1ea}.vgt-table-styling .vgt-table .downloaded{background-color:#ffda8a}.vgt-table-styling .vgt-table .failed{background-color:#f99}.vgt-table-styling .vgt-table .subtitled{background-color:#bedeed}.vgt-table-styling .vgt-table .archived{background-color:#f5f1e4}.vgt-table-styling .tablesorter-header-inner{padding:0 2px;text-align:center}.vgt-table-styling .vgt-dropdown>.button-group{position:relative}.vgt-table-styling .dropdown-toggle{position:absolute;z-index:1;top:.1em;right:.1em;width:1em;transition:width .2s ease-in-out}.vgt-table-styling .dropdown-toggle:hover,.vgt-table-styling .dropdown-toggle:active{width:2em}.vgt-table-styling .vgt-dropdown-menu{position:absolute;z-index:1;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-clip:padding-box;border-radius:3px;right:0;top:2em}.vgt-table-styling .vgt-dropdown-menu>li>span{display:block;padding:3px 5px;clear:both;font-weight:400;line-height:1.42857143;white-space:nowrap}.vgt-table-styling .align-center{display:flex;justify-content:center}.vgt-table-styling .span-center>span{display:flex;justify-content:center}.vgt-table-styling .indexer-image :not(:last-child){margin-right:5px}.vgt-table-styling .button-row{width:100%;display:inline-block}.vgt-table-styling .global-ignored td.release span{color:red}.vgt-table-styling .show-ignored td.release span{color:red;font-style:italic}.vgt-table-styling .global-required td.release span{color:green}.vgt-table-styling .show-required td.release span{color:green;font-style:italic}.vgt-table-styling .global-undesired td.release span{color:orange}.vgt-table-styling td.col-footer{text-align:left !important}.vgt-table-styling .vgt-wrap__footer{color:#fff;padding:1em;background-color:#333;margin-bottom:1em;display:flex;justify-content:space-between}.vgt-table-styling .footer__row-count,.vgt-table-styling .footer__navigation__page-info{display:inline}.vgt-table-styling .footer__row-count__label{margin-right:1em}.vgt-table-styling .vgt-wrap__footer .footer__navigation{font-size:14px}.vgt-table-styling .vgt-pull-right{float:right !important}.vgt-table-styling .footer__navigation__page-btn{display:inline-block;padding:4px 10px;margin-bottom:0;font-size:12px;line-height:16px;vertical-align:middle;border-radius:1px}.vgt-table-styling .footer__navigation__page-btn:hover{text-decoration:none;background-position:0 -150px;transition:background-position 0s linear;background-image:none}.vgt-table-styling .footer__navigation__page-btn.disabled{display:none}.vgt-table-styling .vgt-wrap__footer .footer__navigation__page-btn .chevron{width:24px;height:24px;border-radius:15%;position:relative;margin:0 8px}.vgt-table-styling .vgt-wrap__footer .footer__navigation__info,.vgt-table-styling .vgt-wrap__footer .footer__navigation__page-info{display:inline-flex;color:#909399;margin:0 16px;margin-top:0;margin-right:16px;margin-bottom:0;margin-left:16px}.vgt-table-styling .vgt-input{height:23px;line-height:23px;font-size:.9em;width:100%;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:3px;padding:0 10px;margin:0}.vgt-table-styling .vgt-select{height:23px;line-height:23px;font-size:.9em;width:100%;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:3px;padding:0 10px}.vgt-table-styling .schedule tr.today{background-color:#f5f1e4}.vgt-table-styling .schedule tr.soon{background-color:#dfd}.vgt-table-styling .schedule tr.missed{background-color:#fdd}.vgt-table-styling .schedule tr.later{background-color:#bedeed}.vgt-table-styling .schedule tr,.vgt-table-styling .schedule tr a{color:#000}.tooltip-wrapper{float:left;min-width:340px}.invalid{background-color:#ff5b5b}.tooltip{display:block !important;z-index:10000}.tooltip .tooltip-inner{background:#ffef93;color:#555;border-radius:16px;padding:5px 10px 4px;border:1px solid #f1d031;-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.tooltip .tooltip-arrow{width:0;height:0;position:absolute;margin:5px;border:1px solid #ffef93;z-index:1}.tooltip[x-placement^=top]{margin-bottom:5px}.tooltip[x-placement^=top] .tooltip-arrow{border-width:5px 5px 0 5px;border-left-color:transparent !important;border-right-color:transparent !important;border-bottom-color:transparent !important;bottom:-5px;left:calc(50% - 4px);margin-top:0;margin-bottom:0}.tooltip[x-placement^=bottom]{margin-top:5px}.tooltip[x-placement^=bottom] .tooltip-arrow{border-width:0 5px 5px 5px;border-left-color:transparent !important;border-right-color:transparent !important;border-top-color:transparent !important;top:-5px;left:calc(50% - 4px);margin-top:0;margin-bottom:0}.tooltip[x-placement^=right]{margin-left:5px}.tooltip[x-placement^=right] .tooltip-arrow{border-width:5px 5px 5px 0;border-left-color:transparent !important;border-top-color:transparent !important;border-bottom-color:transparent !important;left:-4px;top:calc(50% - 5px);margin-left:0;margin-right:0}.tooltip[x-placement^=left]{margin-right:5px}.tooltip[x-placement^=left] .tooltip-arrow{border-width:5px 0 5px 5px;border-top-color:transparent !important;border-right-color:transparent !important;border-bottom-color:transparent !important;right:-4px;top:calc(50% - 5px);margin-left:0;margin-right:0}.tooltip.popover .popover-inner{background:#ffef93;color:#555;padding:24px;border-radius:5px;box-shadow:0 5px 30px rgba(0,0,0,.1)}.tooltip.popover .popover-arrow{border-color:#ffef93}.tooltip[aria-hidden=true]{visibility:hidden;opacity:0;transition:opacity .15s,visibility .15s}.tooltip[aria-hidden=false]{visibility:visible;opacity:1;transition:opacity .15s}.modal-container{border:1px solid #111;box-shadow:0 0 12px 0 rgba(0,0,0,.175);border-radius:0}.modal-header{padding:9px 15px;border-bottom:none;border-radius:0;background-color:#373737}.modal-content{background:#222;border-radius:0;border:1px solid rgba(0,0,0,.2);box-shadow:0 5px 15px rgba(0,0,0,.5);color:#fff}.modal-body{background:#222;overflow-y:auto}.modal-footer{border-top:none;text-align:center}.vue-tags-input{width:273px;border:1px solid #ccc;border-radius:3px}.vue-tags-input .ti-tag{display:inline-flex;width:100%}.vue-tags-input .ti-input{border:none}.vue-tags-input .country-left{margin-right:5px}svg.back-arrow{color:#337ab7;width:20px;height:20px;float:left;margin-right:1em;cursor:pointer}svg.back-arrow:hover,svg.back-arrow:focus{color:#23527c;transform:translateX(-2px);transition:transform ease-in-out .2s}#app{padding-top:4rem}@media(max-width: 768px){#app{padding-top:3.8rem}}\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/style/vgt-table.scss\",\"webpack://./src/style/v-tooltip.scss\",\"webpack://./src/style/modal.scss\",\"webpack://./src/style/vue-tags.scss\",\"webpack://./src/style/back-arrow.scss\",\"webpack://./src/components/app.vue\"],\"names\":[],\"mappings\":\"AAEI,8BACI,UAAA,CACA,iBAAA,CACA,gBAAA,CACA,eAAA,CACA,gBAAA,CAEA,kEAEI,WAAA,CACA,qBAAA,CAIJ,0FAEI,gBAAA,CAGJ,iCACI,iBAAA,CACA,wBAAA,CACA,kBAAA,CACA,iBAAA,CAGJ,8CACI,oBAAA,CAGJ,2DACI,2BAAA,CACA,4BAAA,CAGJ,+CACI,2BAAA,CAGJ,4DACI,qBAAA,CACA,wDAAA,CAGJ,2DACI,qBAAA,CACA,wDAAA,CACA,2BAAA,CACA,4BAAA,CAGJ,iDACI,uBAAA,CACA,oBAAA,CACA,eAAA,CACA,sBAAA,CACA,WAAA,CACA,iBAAA,CACA,KAAA,CACA,MAAA,CACA,UAAA,CACA,WAAA,CAGJ,uCACI,WAAA,CACA,cAAA,CAGJ,uDACI,SAAA,CACA,WAAA,CACA,6BAAA,CACA,0BAAA,CACA,qBAAA,CAGJ,mHAEI,iBAAA,CAIJ,qEACI,YAAA,CAGJ,uCACI,iBAAA,CACA,wBAAA,CAGJ,sCACI,oBAAA,CAGJ,gDACI,eAAA,CAGJ,6CACI,cAAA,CACA,eAAA,CAGJ,wCACI,WAAA,CAGJ,uCACI,UAAA,CAMJ,gDACI,UAAA,CAGJ,mDACI,oBAAA,CAEA,kHAEI,yBAAA,CAIR,uCACI,wBAAA,CAGJ,wCACI,wBAAA,CAGJ,0CACI,wBAAA,CAGJ,sCACI,qBAAA,CAGJ,yCACI,wBAAA,CAGJ,wCACI,wBAAA,CAIR,6CACI,aAAA,CACA,iBAAA,CAGJ,+CACI,iBAAA,CAGJ,oCACI,iBAAA,CACA,SAAA,CACA,QAAA,CACA,UAAA,CACA,SAAA,CACA,gCAAA,CAEA,qFAEI,SAAA,CAIR,sCACI,iBAAA,CACA,SAAA,CACA,UAAA,CACA,eAAA,CACA,aAAA,CACA,cAAA,CACA,cAAA,CACA,eAAA,CACA,eAAA,CACA,2BAAA,CACA,iBAAA,CACA,OAAA,CACA,OAAA,CAEA,8CACI,aAAA,CACA,eAAA,CACA,UAAA,CACA,eAAA,CACA,sBAAA,CACA,kBAAA,CAKR,iCACI,YAAA,CACA,sBAAA,CAGJ,qCACI,YAAA,CACA,sBAAA,CAGJ,oDACI,gBAAA,CAGJ,+BACI,UAAA,CACA,oBAAA,CAGJ,mDACI,SAAA,CAGJ,iDACI,SAAA,CACA,iBAAA,CAGJ,oDACI,WAAA,CAGJ,kDACI,WAAA,CACA,iBAAA,CAGJ,qDACI,YAAA,CAGJ,iCACI,0BAAA,CAGJ,qCACI,UAAA,CACA,WAAA,CACA,qBAAA,CACA,iBAAA,CACA,YAAA,CACA,6BAAA,CAGJ,wFAEI,cAAA,CAGJ,6CACI,gBAAA,CAGJ,yDACI,cAAA,CAGJ,mCACI,sBAAA,CAGJ,iDACI,oBAAA,CACA,gBAAA,CACA,eAAA,CACA,cAAA,CACA,gBAAA,CACA,qBAAA,CACA,iBAAA,CAGJ,uDACI,oBAAA,CACA,4BAAA,CACA,wCAAA,CACA,qBAAA,CAGJ,0DACI,YAAA,CAGJ,4EACI,UAAA,CACA,WAAA,CACA,iBAAA,CACA,iBAAA,CACA,YAAA,CAGJ,mIAEI,mBAAA,CACA,aAAA,CACA,aAAA,CACA,YAAA,CACA,iBAAA,CACA,eAAA,CACA,gBAAA,CAGJ,8BACI,WAAA,CACA,gBAAA,CACA,cAAA,CACA,UAAA,CACA,qBAAA,CACA,qBAAA,CACA,qBAAA,CACA,iBAAA,CACA,cAAA,CACA,QAAA,CAGJ,+BACI,WAAA,CACA,gBAAA,CACA,cAAA,CACA,UAAA,CACA,qBAAA,CACA,qBAAA,CACA,qBAAA,CACA,iBAAA,CACA,cAAA,CAIJ,sCACI,wBAAA,CAGJ,qCACI,qBAAA,CAGJ,uCACI,qBAAA,CAGJ,sCACI,wBAAA,CAGJ,kEAEI,UAAA,CCnWR,iBACI,UAAA,CACA,eAAA,CAGJ,SACI,wBARW,CAWf,SACI,wBAAA,CACA,aAAA,CAEA,wBACI,kBAlBO,CAmBP,UAAA,CACA,kBAAA,CACA,oBAAA,CACA,wBAAA,CACA,kDAAA,CACA,+CAAA,CACA,0CAAA,CAGJ,wBACI,OAAA,CACA,QAAA,CACA,iBAAA,CACA,UAAA,CACA,wBAAA,CACA,SAAA,CAKR,2BACI,iBAAA,CACA,0CACI,0BAAA,CACA,wCAAA,CACA,yCAAA,CACA,0CAAA,CACA,WAAA,CACA,oBAAA,CACA,YAAA,CACA,eAAA,CAIR,8BACI,cAAA,CACA,6CACI,0BAAA,CACA,wCAAA,CACA,yCAAA,CACA,uCAAA,CACA,QAAA,CACA,oBAAA,CACA,YAAA,CACA,eAAA,CAIR,6BACI,eAAA,CACA,4CACI,0BAAA,CACA,wCAAA,CACA,uCAAA,CACA,0CAAA,CACA,SAAA,CACA,mBAAA,CACA,aAAA,CACA,cAAA,CAIR,4BACI,gBAAA,CACA,2CACI,0BAAA,CACA,uCAAA,CACA,yCAAA,CACA,0CAAA,CACA,UAAA,CACA,mBAAA,CACA,aAAA,CACA,cAAA,CAKJ,gCACI,kBAjGO,CAkGP,UAAA,CACA,YAAA,CACA,iBAAA,CACA,oCAAA,CAGJ,gCACI,oBAzGO,CA6Gf,2BACI,iBAAA,CACA,SAAA,CACA,uCAAA,CAGJ,4BACI,kBAAA,CACA,SAAA,CACA,uBAAA,CCtHJ,iBACI,qBAAA,CACA,sCAAA,CACA,eAAA,CAGJ,cACI,gBAAA,CACA,kBAAA,CACA,eAAA,CACA,wBAAA,CAGJ,eACI,eAAA,CACA,eAAA,CACA,+BAAA,CACA,oCAAA,CACA,UAAA,CAGJ,YACI,eAAA,CACA,eAAA,CAGJ,cACI,eAAA,CACA,iBAAA,CC7BJ,gBACI,WAAA,CACA,qBAAA,CACA,iBAAA,CAEA,wBACI,mBAAA,CACA,UAAA,CAGJ,0BACI,WAAA,CAGJ,8BACI,gBAAA,CCfR,eACI,aAAA,CACA,UAAA,CACA,WAAA,CACA,UAAA,CACA,gBAAA,CACA,cAAA,CAGJ,0CAEI,aAAA,CACA,0BAAA,CACA,oCAAA,CC6DJ,KACI,gBAAA,CAGJ,yBACI,KACI,kBAAA,CAAA\",\"sourcesContent\":[\"/** Use this as table styling for all table layouts */\\n.vgt-table-styling {\\n .vgt-table {\\n width: 100%;\\n margin-right: auto;\\n margin-left: auto;\\n text-align: left;\\n border-spacing: 0;\\n \\n th,\\n td {\\n padding: 4px;\\n vertical-align: middle;\\n }\\n\\n /* remove extra border from left edge */\\n th:first-child,\\n td:first-child {\\n border-left: none;\\n }\\n\\n th {\\n text-align: center;\\n border-collapse: collapse;\\n font-weight: normal;\\n position: relative;\\n }\\n\\n span.break-word {\\n word-wrap: break-word;\\n }\\n\\n thead th.sorting.sorting-asc {\\n background-position-x: right;\\n background-position-y: bottom;\\n }\\n\\n thead th.sorting {\\n background-repeat: no-repeat;\\n }\\n\\n thead th.sorting.sorting-desc {\\n background-color: rgb(85, 85, 85);\\n background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7);\\n }\\n\\n thead th.sorting.sorting-asc {\\n background-color: rgb(85, 85, 85);\\n background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7);\\n background-position-x: right;\\n background-position-y: bottom;\\n }\\n\\n th.sortable button {\\n -webkit-appearance: none;\\n -moz-appearance: none;\\n appearance: none;\\n background: transparent;\\n border: none;\\n position: absolute;\\n top: 0;\\n left: 0;\\n width: 100%;\\n height: 100%;\\n }\\n\\n thead th {\\n padding: 4px;\\n cursor: default;\\n }\\n\\n input.tablesorter-filter {\\n width: 98%;\\n height: auto;\\n -webkit-box-sizing: border-box;\\n -moz-box-sizing: border-box;\\n box-sizing: border-box;\\n }\\n\\n tr.tablesorter-filter-row,\\n tr.tablesorter-filter-row td {\\n text-align: center;\\n }\\n\\n /* optional disabled input styling */\\n input.tablesorter-filter-row .disabled {\\n display: none;\\n }\\n\\n tfoot tr {\\n text-align: center;\\n border-collapse: collapse;\\n }\\n\\n tfoot a {\\n text-decoration: none;\\n }\\n\\n th.vgt-row-header {\\n text-align: left;\\n }\\n\\n .season-header {\\n display: inline;\\n margin-left: 5px;\\n }\\n\\n tr.spacer {\\n height: 25px;\\n }\\n\\n tr:hover {\\n opacity: 0.9;\\n }\\n\\n /* When using collored rows (using the episode status name, Snatched, Downloaded, Failed, etc.)\\n * we'd like to have the text always black.\\n */\\n tr.status td > span {\\n color: rgb(0, 0, 0);\\n }\\n\\n span.episode-title > a {\\n text-decoration: none;\\n \\n &:hover,\\n &:focus {\\n text-decoration: underline;\\n }\\n }\\n\\n .skipped {\\n background-color: rgb(190, 222, 237);\\n }\\n \\n .snatched {\\n background-color: rgb(235, 193, 234);\\n }\\n \\n .downloaded {\\n background-color: rgb(255, 218, 138);\\n }\\n \\n .failed {\\n background-color: rgb(255, 153, 153);\\n }\\n \\n .subtitled {\\n background-color: rgb(190, 222, 237);\\n }\\n\\n .archived {\\n background-color: rgb(245, 241, 228);\\n }\\n } \\n\\n .tablesorter-header-inner {\\n padding: 0 2px;\\n text-align: center;\\n }\\n\\n .vgt-dropdown > .button-group {\\n position: relative;\\n }\\n\\n .dropdown-toggle {\\n position: absolute;\\n z-index: 1;\\n top: 0.1em;\\n right: 0.1em;\\n width: 1em;\\n transition: width 0.2s ease-in-out;\\n\\n &:hover,\\n &:active {\\n width: 2em;\\n }\\n }\\n\\n .vgt-dropdown-menu {\\n position: absolute;\\n z-index: 1;\\n float: left;\\n min-width: 160px;\\n padding: 5px 0;\\n margin: 2px 0 0;\\n font-size: 14px;\\n text-align: left;\\n list-style: none;\\n background-clip: padding-box;\\n border-radius: 3px;\\n right: 0;\\n top: 2em;\\n\\n > li > span {\\n display: block;\\n padding: 3px 5px;\\n clear: both;\\n font-weight: 400;\\n line-height: 1.42857143;\\n white-space: nowrap;\\n }\\n \\n }\\n\\n .align-center {\\n display: flex;\\n justify-content: center;\\n }\\n\\n .span-center > span {\\n display: flex;\\n justify-content: center;\\n }\\n\\n .indexer-image :not(:last-child) {\\n margin-right: 5px;\\n }\\n\\n .button-row {\\n width: 100%;\\n display: inline-block;\\n }\\n\\n .global-ignored td.release span {\\n color: red;\\n }\\n\\n .show-ignored td.release span {\\n color: red;\\n font-style: italic;\\n }\\n\\n .global-required td.release span {\\n color: green;\\n }\\n\\n .show-required td.release span {\\n color: green;\\n font-style: italic;\\n }\\n\\n .global-undesired td.release span {\\n color: orange;\\n }\\n\\n td.col-footer {\\n text-align: left !important;\\n }\\n\\n .vgt-wrap__footer {\\n color: rgb(255, 255, 255);\\n padding: 1em;\\n background-color: rgb(51, 51, 51);\\n margin-bottom: 1em;\\n display: flex;\\n justify-content: space-between;\\n }\\n\\n .footer__row-count,\\n .footer__navigation__page-info {\\n display: inline;\\n }\\n\\n .footer__row-count__label {\\n margin-right: 1em;\\n }\\n\\n .vgt-wrap__footer .footer__navigation {\\n font-size: 14px;\\n }\\n\\n .vgt-pull-right {\\n float: right !important;\\n }\\n\\n .footer__navigation__page-btn {\\n display: inline-block;\\n padding: 4px 10px;\\n margin-bottom: 0;\\n font-size: 12px;\\n line-height: 16px;\\n vertical-align: middle;\\n border-radius: 1px;\\n }\\n\\n .footer__navigation__page-btn:hover {\\n text-decoration: none;\\n background-position: 0 -150px;\\n transition: background-position 0s linear;\\n background-image: none;\\n }\\n\\n .footer__navigation__page-btn.disabled {\\n display: none;\\n }\\n\\n .vgt-wrap__footer .footer__navigation__page-btn .chevron {\\n width: 24px;\\n height: 24px;\\n border-radius: 15%;\\n position: relative;\\n margin: 0 8px;\\n }\\n\\n .vgt-wrap__footer .footer__navigation__info,\\n .vgt-wrap__footer .footer__navigation__page-info {\\n display: inline-flex;\\n color: #909399;\\n margin: 0 16px;\\n margin-top: 0;\\n margin-right: 16px;\\n margin-bottom: 0;\\n margin-left: 16px;\\n }\\n\\n .vgt-input {\\n height: 23px;\\n line-height: 23px;\\n font-size: 0.9em;\\n width: 100%;\\n background-color: #fff;\\n background-image: none;\\n border: 1px solid #ccc;\\n border-radius: 3px;\\n padding: 0 10px;\\n margin: 0;\\n }\\n \\n .vgt-select {\\n height: 23px;\\n line-height: 23px;\\n font-size: 0.9em;\\n width: 100%;\\n background-color: #fff;\\n background-image: none;\\n border: 1px solid #ccc;\\n border-radius: 3px;\\n padding: 0 10px;\\n }\\n\\n /* Schedule list layout */\\n .schedule tr.today {\\n background-color: rgb(245, 241, 228);\\n }\\n\\n .schedule tr.soon {\\n background-color: rgb(221, 255, 221);\\n }\\n\\n .schedule tr.missed {\\n background-color: rgb(255, 221, 221);\\n }\\n\\n .schedule tr.later {\\n background-color: rgb(190, 222, 237);\\n }\\n\\n .schedule tr,\\n .schedule tr a {\\n color: rgb(0, 0, 0);\\n }\\n}\",\"// variables\\n$tooltipColor: #ffef93;\\n$tooltipBorderColor: #f1d031;\\n$invalidColor: #ff5b5b;\\n\\n.tooltip-wrapper {\\n float: left;\\n min-width: 340px;\\n}\\n\\n.invalid {\\n background-color: $invalidColor;\\n}\\n\\n.tooltip {\\n display: block !important;\\n z-index: 10000;\\n\\n .tooltip-inner {\\n background: $tooltipColor;\\n color: #555;\\n border-radius: 16px;\\n padding: 5px 10px 4px;\\n border: 1px solid $tooltipBorderColor;\\n -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n }\\n\\n .tooltip-arrow {\\n width: 0;\\n height: 0;\\n position: absolute;\\n margin: 5px;\\n border: 1px solid $tooltipColor;\\n z-index: 1;\\n }\\n\\n}\\n\\n.tooltip[x-placement^='top'] {\\n margin-bottom: 5px;\\n .tooltip-arrow {\\n border-width: 5px 5px 0 5px;\\n border-left-color: transparent !important;\\n border-right-color: transparent !important;\\n border-bottom-color: transparent !important;\\n bottom: -5px;\\n left: calc(50% - 4px);\\n margin-top: 0;\\n margin-bottom: 0;\\n }\\n}\\n\\n.tooltip[x-placement^='bottom'] {\\n margin-top: 5px;\\n .tooltip-arrow {\\n border-width: 0 5px 5px 5px;\\n border-left-color: transparent !important;\\n border-right-color: transparent !important;\\n border-top-color: transparent !important;\\n top: -5px;\\n left: calc(50% - 4px);\\n margin-top: 0;\\n margin-bottom: 0;\\n }\\n}\\n\\n.tooltip[x-placement^='right'] {\\n margin-left: 5px;\\n .tooltip-arrow {\\n border-width: 5px 5px 5px 0;\\n border-left-color: transparent !important;\\n border-top-color: transparent !important;\\n border-bottom-color: transparent !important;\\n left: -4px;\\n top: calc(50% - 5px);\\n margin-left: 0;\\n margin-right: 0;\\n }\\n}\\n\\n.tooltip[x-placement^='left'] {\\n margin-right: 5px;\\n .tooltip-arrow {\\n border-width: 5px 0 5px 5px;\\n border-top-color: transparent !important;\\n border-right-color: transparent !important;\\n border-bottom-color: transparent !important;\\n right: -4px;\\n top: calc(50% - 5px);\\n margin-left: 0;\\n margin-right: 0;\\n }\\n}\\n\\n.tooltip.popover {\\n .popover-inner {\\n background: $tooltipColor;\\n color: #555;\\n padding: 24px;\\n border-radius: 5px;\\n box-shadow: 0 5px 30px rgba(black, 0.1);\\n }\\n\\n .popover-arrow {\\n border-color: $tooltipColor;\\n }\\n}\\n\\n.tooltip[aria-hidden='true'] {\\n visibility: hidden;\\n opacity: 0;\\n transition: opacity 0.15s, visibility 0.15s;\\n}\\n\\n.tooltip[aria-hidden='false'] {\\n visibility: visible;\\n opacity: 1;\\n transition: opacity 0.15s;\\n}\",\"/** Style the modal. This should be saved somewhere, where we create one modal template with slots, and style that. */\\n.modal-container {\\n border: 1px solid rgb(17, 17, 17);\\n box-shadow: 0 0 12px 0 rgba(0, 0, 0, 0.175);\\n border-radius: 0;\\n}\\n\\n.modal-header {\\n padding: 9px 15px;\\n border-bottom: none;\\n border-radius: 0;\\n background-color: rgb(55, 55, 55);\\n}\\n\\n.modal-content {\\n background: rgb(34, 34, 34);\\n border-radius: 0;\\n border: 1px solid rgba(0, 0, 0, 0.2);\\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\\n color: white;\\n}\\n\\n.modal-body {\\n background: rgb(34, 34, 34);\\n overflow-y: auto;\\n}\\n\\n.modal-footer {\\n border-top: none;\\n text-align: center;\\n}\\n\",\".vue-tags-input {\\n width: 273px;\\n border: 1px solid rgb(204, 204, 204);\\n border-radius: 3px;\\n\\n .ti-tag {\\n display: inline-flex;\\n width: 100%;\\n }\\n\\n .ti-input {\\n border: none;\\n }\\n\\n .country-left {\\n margin-right: 5px;\\n }\\n}\",\"svg.back-arrow {\\n color: #337ab7;\\n width: 20px;\\n height: 20px;\\n float: left;\\n margin-right: 1em;\\n cursor: pointer;\\n}\\n\\nsvg.back-arrow:hover,\\nsvg.back-arrow:focus {\\n color: #23527c;\\n transform: translateX(-2px);\\n transition: transform ease-in-out 0.2s;\\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\\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/* Global style definitions should go here. */\\n@use '../style/vgt-table.scss';\\n@use '../style/v-tooltip.scss';\\n@use '../style/modal.scss';\\n@use '../style/vue-tags.scss';\\n@use '../style/back-arrow.scss';\\n\\n#app {\\n padding-top: 4rem;\\n}\\n\\n@media (max-width: 768px) {\\n #app {\\n padding-top: 3.8rem;\\n }\\n}\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nexport default ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\ndiv.anidb-release-group-ui-wrapper[data-v-5721f056] {\\n clear: both;\\n margin-bottom: 20px;\\n}\\ndiv.anidb-release-group-ui-wrapper ul[data-v-5721f056] {\\n border-style: solid;\\n border-width: thin;\\n padding: 5px 2px 2px 5px;\\n list-style: none;\\n}\\ndiv.anidb-release-group-ui-wrapper li.active[data-v-5721f056] {\\n background-color: cornflowerblue;\\n}\\ndiv.anidb-release-group-ui-wrapper div.arrow img[data-v-5721f056] {\\n cursor: pointer;\\n height: 32px;\\n width: 32px;\\n}\\ndiv.anidb-release-group-ui-wrapper img.deleteFromWhitelist[data-v-5721f056],\\ndiv.anidb-release-group-ui-wrapper img.deleteFromBlacklist[data-v-5721f056] {\\n float: right;\\n}\\ndiv.anidb-release-group-ui-wrapper #add-new-release-group p > img[data-v-5721f056] {\\n height: 16px;\\n width: 16px;\\n background-color: rgb(204, 204, 204);\\n}\\ndiv.anidb-release-group-ui-wrapper.placeholder[data-v-5721f056] {\\n height: 32px;\\n}\\ndiv.anidb-release-group-ui-wrapper.max-width[data-v-5721f056] {\\n max-width: 960px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/anidb-release-group-ui.vue\"],\"names\":[],\"mappings\":\";AAmOA;IACA,WAAA;IACA,mBAAA;AACA;AAEA;IACA,mBAAA;IACA,kBAAA;IACA,wBAAA;IACA,gBAAA;AACA;AAEA;IACA,gCAAA;AACA;AAEA;IACA,eAAA;IACA,YAAA;IACA,WAAA;AACA;AAEA;;IAEA,YAAA;AACA;AAEA;IACA,YAAA;IACA,WAAA;IACA,oCAAA;AACA;AAEA;IACA,YAAA;AACA;AAEA;IACA,gBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.floating-badge {\\n position: absolute;\\n top: -5px;\\n right: -8px;\\n padding: 0 4px;\\n background-color: #777;\\n border: 2px solid #959595;\\n border-radius: 100px;\\n font-size: 12px;\\n font-weight: bold;\\n text-decoration: none;\\n color: white;\\n}\\n.navbar {\\n margin-bottom: 0;\\n}\\n@media (max-width: 767px) {\\n.navbar-fixed-top .navbar-collapse {\\n max-height: 100%;\\n}\\n.navbar-mobile > li {\\n text-align: center;\\n}\\n.navbar-mobile::before {\\n position: absolute;\\n}\\n.navbar-mobile {\\n display: grid;\\n grid-template-columns: 1fr 1fr 1fr;\\n}\\n.navbar-mobile #NAVsystem > ul {\\n transform: translateX(-6rem);\\n}\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/app-header.vue\"],\"names\":[],\"mappings\":\";AAwTA;IACA,kBAAA;IACA,SAAA;IACA,WAAA;IACA,cAAA;IACA,sBAAA;IACA,yBAAA;IACA,oBAAA;IACA,eAAA;IACA,iBAAA;IACA,qBAAA;IACA,YAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;AACA;QACA,gBAAA;AACA;AAEA;QACA,kBAAA;AACA;AAEA;QACA,kBAAA;AACA;AAEA;QACA,aAAA;QACA,kCAAA;AACA;AAEA;QACA,4BAAA;AACA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n#filter-indexers[data-v-02c73319] {\\n float: right;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/change-indexer.vue\"],\"names\":[],\"mappings\":\";AAuKA;IACA,YAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.display-inline[data-v-e416c744] {\\n display: inline;\\n}\\n.radio-item[data-v-e416c744] {\\n display: block;\\n}\\n.multiselect[data-v-e416c744] {\\n margin-bottom: 10px;\\n}\\n.margin-bottom-10[data-v-e416c744] {\\n margin-bottom: 10px;\\n}\\n.plotInfo[data-v-e416c744] {\\n cursor: help;\\n float: right;\\n position: relative;\\n top: 2px;\\n}\\n.plotInfoNone[data-v-e416c744] {\\n cursor: help;\\n float: right;\\n position: relative;\\n top: 2px;\\n opacity: 0.4;\\n}\\n.v-popover[data-v-e416c744] {\\n display: inline;\\n}\\n.tooltip-base[data-v-e416c744] {\\n position: relative;\\n display: inline-block !important;\\n z-index: 999;\\n max-width: 276px;\\n padding: 1px;\\n font-family: Helvetica Neue, Helvetica, Arial, sans-serif;\\n font-style: normal;\\n font-weight: 400;\\n font-size: 14px;\\n line-height: 1.42857143;\\n line-break: auto;\\n text-align: start;\\n text-decoration: none;\\n text-shadow: none;\\n text-transform: none;\\n letter-spacing: normal;\\n word-break: normal;\\n word-spacing: normal;\\n word-wrap: normal;\\n white-space: normal;\\n background-color: #fff;\\n background-clip: padding-box;\\n border: 1px solid rgba(0, 0, 0, 0.2);\\n border-radius: 6px;\\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\\n}\\n.tooltip-base.tooltip-themed-dark[data-v-e416c744] {\\n background-color: rgb(51, 51, 51);\\n border-color: #111;\\n}\\n.tooltip-base .tooltip-inner[data-v-e416c744] {\\n background-color: inherit;\\n color: inherit;\\n text-align: inherit;\\n max-width: inherit;\\n padding: 0;\\n border-radius: 6px;\\n}\\n.tooltip-title[data-v-e416c744] {\\n padding: 8px 14px;\\n margin: 0;\\n border-radius: 5px 5px 0 0;\\n}\\n.tooltip-content[data-v-e416c744] {\\n padding: 9px 14px;\\n margin: 0;\\n}\\n.tooltip-themed-dark .tooltip-title[data-v-e416c744] {\\n background-color: rgb(35, 35, 35);\\n border-bottom-color: #111;\\n}\\n.tooltip-themed-light .tooltip-title[data-v-e416c744] {\\n padding: 8px 14px;\\n margin: 0;\\n font-size: 14px;\\n background-color: #f7f7f7;\\n border-bottom: 1px solid #ebebeb;\\n border-radius: 5px 5px 0 0;\\n}\\n.tooltip-base[x-placement^=\\\"left\\\"][data-v-e416c744]::after,\\n.tooltip-base[x-placement^=\\\"left\\\"][data-v-e416c744]::before {\\n left: 100%;\\n top: 50%;\\n border: solid transparent;\\n content: \\\" \\\";\\n height: 0;\\n width: 0;\\n position: absolute;\\n pointer-events: none;\\n}\\n.tooltip-base[x-placement^=\\\"left\\\"][data-v-e416c744]::after {\\n border-color: rgba(255, 255, 255, 0);\\n border-left-color: #fff;\\n border-width: 11px;\\n margin-top: -11px;\\n content: \\\" \\\";\\n}\\n.tooltip-base[x-placement^=\\\"left\\\"][data-v-e416c744]::before {\\n border-color: rgba(0, 0, 0, 0);\\n border-left-color: rgba(0, 0, 0, 0.2);\\n border-width: 12px;\\n margin-top: -12px;\\n content: \\\" \\\";\\n}\\n.tooltip-base[x-placement^=\\\"right\\\"][data-v-e416c744]::after,\\n.tooltip-base[x-placement^=\\\"right\\\"][data-v-e416c744]::before {\\n right: 100%;\\n top: 50%;\\n border: solid transparent;\\n content: \\\" \\\";\\n height: 0;\\n width: 0;\\n position: absolute;\\n pointer-events: none;\\n}\\n.tooltip-base[x-placement^=\\\"right\\\"][data-v-e416c744]::after {\\n border-color: rgba(255, 255, 255, 0);\\n border-right-color: #fff;\\n border-width: 11px;\\n margin-top: -11px;\\n content: \\\" \\\";\\n}\\n.tooltip-base[x-placement^=\\\"right\\\"][data-v-e416c744]::before {\\n border-color: rgba(0, 0, 0, 0);\\n border-right-color: rgba(0, 0, 0, 0.2);\\n border-width: 12px;\\n margin-top: -12px;\\n content: \\\" \\\";\\n}\\n\\n/* The background of the arrow, the outline */\\n.tooltip-themed-dark[x-placement^=\\\"left\\\"][data-v-e416c744]::before {\\n border-left-color: #000 !important;\\n}\\n\\n/* The background of the arrow, the outline */\\n.tooltip-themed-dark[x-placement^=\\\"right\\\"][data-v-e416c744]::before {\\n border-right-color: #000 !important;\\n}\\n\\n/* The foreground of the arrow, the filling */\\n.tooltip-themed-dark[x-placement^=\\\"left\\\"][data-v-e416c744]::after {\\n border-left-color: rgb(51, 51, 51) !important;\\n}\\n\\n/* The foreground of the arrow, the filling */\\n.tooltip-themed-dark[x-placement^=\\\"right\\\"][data-v-e416c744]::after {\\n border-right-color: rgb(51, 51, 51) !important;\\n}\\n.tooltip-arrow[data-v-e416c744] {\\n display: none;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/config-general.vue\"],\"names\":[],\"mappings\":\";AAo/BA;IACA,eAAA;AACA;AAEA;IACA,cAAA;AACA;AAEA;IACA,mBAAA;AACA;AAEA;IACA,mBAAA;AACA;AAEA;IACA,YAAA;IACA,YAAA;IACA,kBAAA;IACA,QAAA;AACA;AAEA;IACA,YAAA;IACA,YAAA;IACA,kBAAA;IACA,QAAA;IACA,YAAA;AACA;AAEA;IACA,eAAA;AACA;AAEA;IACA,kBAAA;IACA,gCAAA;IACA,YAAA;IACA,gBAAA;IACA,YAAA;IACA,yDAAA;IACA,kBAAA;IACA,gBAAA;IACA,eAAA;IACA,uBAAA;IACA,gBAAA;IACA,iBAAA;IACA,qBAAA;IACA,iBAAA;IACA,oBAAA;IACA,sBAAA;IACA,kBAAA;IACA,oBAAA;IACA,iBAAA;IACA,mBAAA;IACA,sBAAA;IACA,4BAAA;IACA,oCAAA;IACA,kBAAA;IACA,yCAAA;AACA;AAEA;IACA,iCAAA;IACA,kBAAA;AACA;AAEA;IACA,yBAAA;IACA,cAAA;IACA,mBAAA;IACA,kBAAA;IACA,UAAA;IACA,kBAAA;AACA;AAEA;IACA,iBAAA;IACA,SAAA;IACA,0BAAA;AACA;AAEA;IACA,iBAAA;IACA,SAAA;AACA;AAEA;IACA,iCAAA;IACA,yBAAA;AACA;AAEA;IACA,iBAAA;IACA,SAAA;IACA,eAAA;IACA,yBAAA;IACA,gCAAA;IACA,0BAAA;AACA;AAEA;;IAEA,UAAA;IACA,QAAA;IACA,yBAAA;IACA,YAAA;IACA,SAAA;IACA,QAAA;IACA,kBAAA;IACA,oBAAA;AACA;AAEA;IACA,oCAAA;IACA,uBAAA;IACA,kBAAA;IACA,iBAAA;IACA,YAAA;AACA;AAEA;IACA,8BAAA;IACA,qCAAA;IACA,kBAAA;IACA,iBAAA;IACA,YAAA;AACA;AAEA;;IAEA,WAAA;IACA,QAAA;IACA,yBAAA;IACA,YAAA;IACA,SAAA;IACA,QAAA;IACA,kBAAA;IACA,oBAAA;AACA;AAEA;IACA,oCAAA;IACA,wBAAA;IACA,kBAAA;IACA,iBAAA;IACA,YAAA;AACA;AAEA;IACA,8BAAA;IACA,sCAAA;IACA,kBAAA;IACA,iBAAA;IACA,YAAA;AACA;;AAEA,6CAAA;AACA;IACA,kCAAA;AACA;;AAEA,6CAAA;AACA;IACA,mCAAA;AACA;;AAEA,8CAAA;AACA;IACA,6CAAA;AACA;;AAEA,8CAAA;AACA;IACA,8CAAA;AACA;AAEA;IACA,aAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.metadata {\\n padding-left: 20px;\\n display: flex;\\n}\\n.metadata-options-wrapper {\\n min-width: 190px;\\n}\\n.metadata-example-wrapper {\\n width: 325px;\\n margin-left: 4em;\\n}\\n@media (max-width: 480px) {\\n.metadata {\\n flex-direction: column;\\n}\\n.metadata-example-wrapper {\\n margin-left: 0;\\n}\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/config-post-processing.vue\"],\"names\":[],\"mappings\":\";AA6hBA;IACA,kBAAA;IACA,aAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;AACA;QACA,sBAAA;AACA;AAEA;QACA,cAAA;AACA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\nul.extra-scripts {\\n padding: 0;\\n margin-left: 15px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/config-subtitles.vue\"],\"names\":[],\"mappings\":\";AA8OA;IACA,UAAA;IACA,iBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.infoTable tr td[data-v-0e222bd8]:first-child {\\n vertical-align: top;\\n}\\npre[data-v-0e222bd8] {\\n padding: 5px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/config.vue\"],\"names\":[],\"mappings\":\";AAmEA;IACA,mBAAA;AACA;AAEA;IACA,YAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.defaultTable.displayShow[data-v-4f66e180] {\\n clear: both;\\n}\\n.displayShowTable.displayShow[data-v-4f66e180] {\\n clear: both;\\n}\\n.fanartBackground.displayShow[data-v-4f66e180] {\\n clear: both;\\n opacity: 0.9;\\n}\\n.fanartBackground table[data-v-4f66e180] {\\n table-layout: auto;\\n width: 100%;\\n border-collapse: collapse;\\n border-spacing: 0;\\n text-align: center;\\n border: none;\\n empty-cells: show;\\n color: rgb(0, 0, 0) !important;\\n}\\n.summaryFanArt[data-v-4f66e180] {\\n opacity: 0.9;\\n}\\n.fanartBackground > table th.vgt-row-header[data-v-4f66e180] {\\n border: none !important;\\n background-color: transparent !important;\\n color: rgb(255, 255, 255) !important;\\n padding-top: 15px !important;\\n text-align: left !important;\\n}\\n.fanartBackground td.col-search[data-v-4f66e180] {\\n text-align: center;\\n}\\n\\n/* Trying to migrate this from tablesorter */\\n\\n/* =======================================================================\\ntablesorter.css\\n========================================================================== */\\n.displayShow[data-v-4f66e180] .vgt-global-search__input.vgt-pull-left {\\n float: left;\\n height: 40px;\\n}\\n.displayShow[data-v-4f66e180] .vgt-input {\\n border: 1px solid #ccc;\\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;\\n height: 30px;\\n padding: 5px 10px;\\n font-size: 12px;\\n line-height: 1.5;\\n border-radius: 3px;\\n}\\n.displayShow[data-v-4f66e180] div.vgt-responsive > table tbody > tr > th.vgt-row-header > span {\\n font-size: 24px;\\n margin-top: 20px;\\n margin-bottom: 10px;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table {\\n width: 100%;\\n margin-right: auto;\\n margin-left: auto;\\n color: rgb(0, 0, 0);\\n text-align: left;\\n border-spacing: 0;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table th,\\n.displayShow[data-v-4f66e180] .vgt-table td {\\n padding: 4px;\\n border-top: rgb(34, 34, 34) 1px solid;\\n border-left: rgb(34, 34, 34) 1px solid;\\n vertical-align: middle;\\n}\\n\\n/* remove extra border from left edge */\\n.displayShow[data-v-4f66e180] .vgt-table th:first-child,\\n.displayShow[data-v-4f66e180] .vgt-table td:first-child {\\n border-left: none;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table span.break-word {\\n word-wrap: break-word;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table thead th.sorting.sorting-desc {\\n background-color: rgb(85, 85, 85);\\n background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7);\\n}\\n.displayShow[data-v-4f66e180] .vgt-table thead th.sorting.sorting-asc {\\n background-color: rgb(85, 85, 85);\\n background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7);\\n background-position-x: right;\\n background-position-y: bottom;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table th {\\n text-align: center;\\n text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);\\n background-color: rgb(51, 51, 51);\\n white-space: nowrap;\\n color: rgb(255, 255, 255);\\n border-collapse: collapse;\\n font-weight: normal;\\n position: relative;\\n background-image: none;\\n padding: 4px;\\n cursor: default;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table thead th.sortable button {\\n -webkit-appearance: none;\\n -moz-appearance: none;\\n appearance: none;\\n background: transparent;\\n border: none;\\n position: absolute;\\n top: 0;\\n left: 0;\\n width: 100%;\\n height: 100%;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table thead th.sorting {\\n background-repeat: no-repeat;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table input.tablesorter-filter {\\n width: 98%;\\n height: auto;\\n -webkit-box-sizing: border-box;\\n -moz-box-sizing: border-box;\\n box-sizing: border-box;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table tr.tablesorter-filter-row,\\n.displayShow[data-v-4f66e180] .vgt-table tr.tablesorter-filter-row td {\\n text-align: center;\\n}\\n\\n/* optional disabled input styling */\\n.displayShow[data-v-4f66e180] .vgt-table input.tablesorter-filter-row .disabled {\\n display: none;\\n}\\n.tablesorter-header-inner[data-v-4f66e180] {\\n padding: 0 2px;\\n text-align: center;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table tfoot tr {\\n color: rgb(255, 255, 255);\\n text-align: center;\\n text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.3);\\n background-color: rgb(51, 51, 51);\\n border-collapse: collapse;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table tfoot a {\\n color: rgb(255, 255, 255);\\n text-decoration: none;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table th.vgt-row-header {\\n text-align: left;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table .season-header {\\n display: inline;\\n margin-left: 5px;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table tr.spacer {\\n height: 25px;\\n}\\n.displayShow[data-v-4f66e180] .vgt-table tr:hover {\\n opacity: 0.9;\\n}\\n.displayShow[data-v-4f66e180] .unaired {\\n background-color: rgb(245, 241, 228);\\n}\\n.displayShow[data-v-4f66e180] .skipped {\\n background-color: rgb(190, 222, 237);\\n}\\n.displayShow[data-v-4f66e180] .preferred {\\n background-color: rgb(195, 227, 200);\\n}\\n.displayShow[data-v-4f66e180] .archived {\\n background-color: rgb(195, 227, 200);\\n}\\n.displayShow[data-v-4f66e180] .allowed {\\n background-color: rgb(255, 218, 138);\\n}\\n.displayShow[data-v-4f66e180] .wanted {\\n background-color: rgb(255, 176, 176);\\n}\\n.displayShow[data-v-4f66e180] .snatched {\\n background-color: rgb(235, 193, 234);\\n}\\n.displayShow[data-v-4f66e180] .downloaded {\\n background-color: rgb(255, 218, 138);\\n}\\n.displayShow[data-v-4f66e180] .failed {\\n background-color: rgb(255, 153, 153);\\n}\\n.displayShow[data-v-4f66e180] span.unaired {\\n color: rgb(88, 75, 32);\\n}\\n.displayShow[data-v-4f66e180] span.skipped {\\n color: rgb(29, 80, 104);\\n}\\n.displayShow[data-v-4f66e180] span.preffered {\\n color: rgb(41, 87, 48);\\n}\\n.displayShow[data-v-4f66e180] span.allowed {\\n color: rgb(118, 81, 0);\\n}\\n.displayShow[data-v-4f66e180] span.wanted {\\n color: rgb(137, 0, 0);\\n}\\n.displayShow[data-v-4f66e180] span.snatched {\\n color: rgb(101, 33, 100);\\n}\\n.displayShow[data-v-4f66e180] span.unaired b,\\n.displayShow[data-v-4f66e180] span.skipped b,\\n.displayShow[data-v-4f66e180] span.preferred b,\\n.displayShow[data-v-4f66e180] span.allowed b,\\n.displayShow[data-v-4f66e180] span.wanted b,\\n.displayShow[data-v-4f66e180] span.snatched b {\\n color: rgb(0, 0, 0);\\n font-weight: 800;\\n}\\n.mobile-select[data-v-4f66e180] {\\n width: 110px;\\n font-size: x-small;\\n}\\ntd.col-footer[data-v-4f66e180] {\\n text-align: left !important;\\n}\\n.displayShow[data-v-4f66e180] .vgt-wrap__footer {\\n color: rgb(255, 255, 255);\\n padding: 1em;\\n background-color: rgb(51, 51, 51);\\n margin-bottom: 1em;\\n display: flex;\\n justify-content: space-between;\\n}\\n.displayShow[data-v-4f66e180] .footer__row-count,\\n.displayShow[data-v-4f66e180] .footer__navigation__page-info {\\n display: inline;\\n}\\n.displayShow[data-v-4f66e180] .footer__row-count__label {\\n margin-right: 1em;\\n}\\n.displayShow[data-v-4f66e180] .vgt-wrap__footer .footer__navigation {\\n font-size: 14px;\\n}\\n.displayShow[data-v-4f66e180] .vgt-pull-right {\\n float: right !important;\\n}\\n.displayShow[data-v-4f66e180] .vgt-wrap__footer .footer__navigation__page-btn .chevron {\\n width: 24px;\\n height: 24px;\\n border-radius: 15%;\\n position: relative;\\n margin: 0 8px;\\n}\\n.displayShow[data-v-4f66e180] .vgt-wrap__footer .footer__navigation__info,\\n.displayShow[data-v-4f66e180] .vgt-wrap__footer .footer__navigation__page-info {\\n display: inline-flex;\\n color: #909399;\\n margin: 0 16px;\\n margin-top: 0;\\n margin-right: 16px;\\n margin-bottom: 0;\\n margin-left: 16px;\\n}\\n.select-info span[data-v-4f66e180] {\\n margin-left: 5px;\\n line-height: 40px;\\n}\\n.subtitles > div[data-v-4f66e180] {\\n float: left;\\n}\\n.subtitles > div[data-v-4f66e180]:not(:last-child) {\\n margin-right: 2px;\\n}\\n.displayShow[data-v-4f66e180] .vgt-dropdown > .button-group {\\n position: relative;\\n}\\n.displayShow[data-v-4f66e180] .dropdown-toggle {\\n position: absolute;\\n z-index: 1;\\n top: 0.1em;\\n right: 0.1em;\\n width: 1em;\\n transition: width 0.2s ease-in-out;\\n}\\n.displayShow[data-v-4f66e180] .dropdown-toggle:hover,\\n.displayShow[data-v-4f66e180] .dropdown-toggle:active {\\n width: 2em;\\n}\\n.displayShow[data-v-4f66e180] .vgt-dropdown-menu {\\n position: absolute;\\n z-index: 1;\\n float: left;\\n min-width: 160px;\\n padding: 5px 0;\\n margin: 2px 0 0;\\n font-size: 14px;\\n text-align: left;\\n list-style: none;\\n background-clip: padding-box;\\n border-radius: 3px;\\n right: 0;\\n top: 2em;\\n}\\n.displayShow[data-v-4f66e180] .vgt-dropdown-menu > li > span {\\n display: block;\\n padding: 3px 5px;\\n clear: both;\\n font-weight: 400;\\n line-height: 1.42857143;\\n white-space: nowrap;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/display-show.vue\"],\"names\":[],\"mappings\":\";AAmgCA;IACA,WAAA;AACA;AAEA;IACA,WAAA;AACA;AAEA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,kBAAA;IACA,WAAA;IACA,yBAAA;IACA,iBAAA;IACA,kBAAA;IACA,YAAA;IACA,iBAAA;IACA,8BAAA;AACA;AAEA;IACA,YAAA;AACA;AAEA;IACA,uBAAA;IACA,wCAAA;IACA,oCAAA;IACA,4BAAA;IACA,2BAAA;AACA;AAEA;IACA,kBAAA;AACA;;AAEA,4CAAA;;AAEA;;4EAEA;AAEA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,sBAAA;IACA,8GAAA;IACA,YAAA;IACA,iBAAA;IACA,eAAA;IACA,gBAAA;IACA,kBAAA;AACA;AAEA;IACA,eAAA;IACA,gBAAA;IACA,mBAAA;AACA;AAEA;IACA,WAAA;IACA,kBAAA;IACA,iBAAA;IACA,mBAAA;IACA,gBAAA;IACA,iBAAA;AACA;AAEA;;IAEA,YAAA;IACA,qCAAA;IACA,sCAAA;IACA,sBAAA;AACA;;AAEA,uCAAA;AACA;;IAEA,iBAAA;AACA;AAEA;IACA,qBAAA;AACA;AAEA;IACA,iCAAA;IACA,qHAAA;AACA;AAEA;IACA,iCAAA;IACA,qHAAA;IACA,4BAAA;IACA,6BAAA;AACA;AAEA;IACA,kBAAA;IACA,2CAAA;IACA,iCAAA;IACA,mBAAA;IACA,yBAAA;IACA,yBAAA;IACA,mBAAA;IACA,kBAAA;IACA,sBAAA;IACA,YAAA;IACA,eAAA;AACA;AAEA;IACA,wBAAA;IACA,qBAAA;IACA,gBAAA;IACA,uBAAA;IACA,YAAA;IACA,kBAAA;IACA,MAAA;IACA,OAAA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,4BAAA;AACA;AAEA;IACA,UAAA;IACA,YAAA;IACA,8BAAA;IACA,2BAAA;IACA,sBAAA;AACA;AAEA;;IAEA,kBAAA;AACA;;AAEA,oCAAA;AACA;IACA,aAAA;AACA;AAEA;IACA,cAAA;IACA,kBAAA;AACA;AAEA;IACA,yBAAA;IACA,kBAAA;IACA,2CAAA;IACA,iCAAA;IACA,yBAAA;AACA;AAEA;IACA,yBAAA;IACA,qBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,eAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;AACA;AAEA;IACA,YAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,oCAAA;AACA;AAEA;IACA,sBAAA;AACA;AAEA;IACA,uBAAA;AACA;AAEA;IACA,sBAAA;AACA;AAEA;IACA,sBAAA;AACA;AAEA;IACA,qBAAA;AACA;AAEA;IACA,wBAAA;AACA;AAEA;;;;;;IAMA,mBAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,kBAAA;AACA;AAEA;IACA,2BAAA;AACA;AAEA;IACA,yBAAA;IACA,YAAA;IACA,iCAAA;IACA,kBAAA;IACA,aAAA;IACA,8BAAA;AACA;AAEA;;IAEA,eAAA;AACA;AAEA;IACA,iBAAA;AACA;AAEA;IACA,eAAA;AACA;AAEA;IACA,uBAAA;AACA;AAEA;IACA,WAAA;IACA,YAAA;IACA,kBAAA;IACA,kBAAA;IACA,aAAA;AACA;AAEA;;IAEA,oBAAA;IACA,cAAA;IACA,cAAA;IACA,aAAA;IACA,kBAAA;IACA,gBAAA;IACA,iBAAA;AACA;AAEA;IACA,gBAAA;IACA,iBAAA;AACA;AAEA;IACA,WAAA;AACA;AAEA;IACA,iBAAA;AACA;AAEA;IACA,kBAAA;AACA;AAEA;IACA,kBAAA;IACA,UAAA;IACA,UAAA;IACA,YAAA;IACA,UAAA;IACA,kCAAA;AACA;AAEA;;IAEA,UAAA;AACA;AAEA;IACA,kBAAA;IACA,UAAA;IACA,WAAA;IACA,gBAAA;IACA,cAAA;IACA,eAAA;IACA,eAAA;IACA,gBAAA;IACA,gBAAA;IACA,4BAAA;IACA,kBAAA;IACA,QAAA;IACA,QAAA;AACA;AAEA;IACA,cAAA;IACA,gBAAA;IACA,WAAA;IACA,gBAAA;IACA,uBAAA;IACA,mBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\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\\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\\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\\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\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n/*\\n@NOTE: This fixes the header blocking elements when using a hash link\\ne.g. displayShow?showslug=tvdb83462#season-5\\n*/\\n[false-link]::before {\\n content: '';\\n display: block;\\n position: absolute;\\n height: 100px;\\n margin-top: -100px;\\n z-index: -100;\\n}\\n.router-link,\\n.router-link-active {\\n cursor: pointer;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/app-link.vue\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoKA;;;CAGA;AACA;IACA,WAAA;IACA,cAAA;IACA,kBAAA;IACA,aAAA;IACA,kBAAA;IACA,aAAA;AACA;AAEA;;IAEA,eAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.width-40[data-v-ad0dcc86] {\\n width: 40px;\\n}\\n.width-50[data-v-ad0dcc86] {\\n width: 50px;\\n}\\n\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/asset.vue\"],\"names\":[],\"mappings\":\";AAgGA;IACA,WAAA;AACA;AAEA;IACA,WAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.warning-enter-active[data-v-5a5b710f],\\n.warning-leave-active[data-v-5a5b710f] {\\n -moz-transition-duration: 0.3s;\\n -webkit-transition-duration: 0.3s;\\n -o-transition-duration: 0.3s;\\n transition-duration: 0.3s;\\n -moz-transition-timing-function: ease-in;\\n -webkit-transition-timing-function: ease-in;\\n -o-transition-timing-function: ease-in;\\n transition-timing-function: ease-in;\\n}\\n.warning-enter-to[data-v-5a5b710f],\\n.warning-leave[data-v-5a5b710f] {\\n max-height: 100%;\\n}\\n.warning-enter[data-v-5a5b710f],\\n.warning-leave-to[data-v-5a5b710f] {\\n max-height: 0;\\n}\\n.warning[data-v-5a5b710f] {\\n display: block;\\n overflow: hidden;\\n width: 100%;\\n position: absolute;\\n left: 0;\\n background-color: #e23636;\\n padding: 0 2px 0 2px;\\n}\\n.manager-note[data-v-5a5b710f] {\\n margin: 5px 0;\\n padding: 10px;\\n border: 1px solid #ccc;\\n}\\n.manager-note > img[data-v-5a5b710f] {\\n width: 16px;\\n padding-bottom: 4px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-custom-newznab.vue\"],\"names\":[],\"mappings\":\";AAwOA;;IAEA,8BAAA;IACA,iCAAA;IACA,4BAAA;IACA,yBAAA;IACA,wCAAA;IACA,2CAAA;IACA,sCAAA;IACA,mCAAA;AACA;AAEA;;IAEA,gBAAA;AACA;AAEA;;IAEA,aAAA;AACA;AAEA;IACA,cAAA;IACA,gBAAA;IACA,WAAA;IACA,kBAAA;IACA,OAAA;IACA,yBAAA;IACA,oBAAA;AACA;AAEA;IACA,aAAA;IACA,aAAA;IACA,sBAAA;AACA;AAEA;IACA,WAAA;IACA,mBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.testresult[data-v-b85ae602] {\\n display: inline-block;\\n border-style: solid;\\n border-width: 1px;\\n padding: 1px 4px 4px 4px;\\n border-color: #ccc;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-custom-prowlarr.vue\"],\"names\":[],\"mappings\":\";AA+MA;IACA,qBAAA;IACA,mBAAA;IACA,iBAAA;IACA,wBAAA;IACA,kBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.warning-enter-active[data-v-52661c90],\\n.warning-leave-active[data-v-52661c90] {\\n -moz-transition-duration: 0.3s;\\n -webkit-transition-duration: 0.3s;\\n -o-transition-duration: 0.3s;\\n transition-duration: 0.3s;\\n -moz-transition-timing-function: ease-in;\\n -webkit-transition-timing-function: ease-in;\\n -o-transition-timing-function: ease-in;\\n transition-timing-function: ease-in;\\n}\\n.warning-enter-to[data-v-52661c90],\\n.warning-leave[data-v-52661c90] {\\n max-height: 100%;\\n}\\n.warning-enter[data-v-52661c90],\\n.warning-leave-to[data-v-52661c90] {\\n max-height: 0;\\n}\\n.warning[data-v-52661c90] {\\n display: block;\\n overflow: hidden;\\n width: 100%;\\n position: absolute;\\n left: 0;\\n background-color: #e23636;\\n padding: 0 2px 0 2px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-custom-torrentrss.vue\"],\"names\":[],\"mappings\":\";AAyJA;;IAEA,8BAAA;IACA,iCAAA;IACA,4BAAA;IACA,yBAAA;IACA,wCAAA;IACA,2CAAA;IACA,sCAAA;IACA,mCAAA;AACA;AAEA;;IAEA,gBAAA;AACA;AAEA;;IAEA,aAAA;AACA;AAEA;IACA,cAAA;IACA,gBAAA;IACA,WAAA;IACA,kBAAA;IACA,OAAA;IACA,yBAAA;IACA,oBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.warning-enter-active[data-v-3cf848cc],\\n.warning-leave-active[data-v-3cf848cc] {\\n -moz-transition-duration: 0.3s;\\n -webkit-transition-duration: 0.3s;\\n -o-transition-duration: 0.3s;\\n transition-duration: 0.3s;\\n -moz-transition-timing-function: ease-in;\\n -webkit-transition-timing-function: ease-in;\\n -o-transition-timing-function: ease-in;\\n transition-timing-function: ease-in;\\n}\\n.warning-enter-to[data-v-3cf848cc],\\n.warning-leave[data-v-3cf848cc] {\\n max-height: 100%;\\n}\\n.warning-enter[data-v-3cf848cc],\\n.warning-leave-to[data-v-3cf848cc] {\\n max-height: 0;\\n}\\n.warning[data-v-3cf848cc] {\\n display: block;\\n overflow: hidden;\\n width: 100%;\\n position: absolute;\\n left: 0;\\n background-color: #e23636;\\n padding: 0 2px 0 2px;\\n z-index: 1;\\n}\\n.manager-note[data-v-3cf848cc] {\\n margin: 5px 0;\\n padding: 10px;\\n border: 1px solid #ccc;\\n}\\n.manager-note > img[data-v-3cf848cc] {\\n width: 16px;\\n padding-bottom: 4px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-custom-torznab.vue\"],\"names\":[],\"mappings\":\";AAsOA;;IAEA,8BAAA;IACA,iCAAA;IACA,4BAAA;IACA,yBAAA;IACA,wCAAA;IACA,2CAAA;IACA,sCAAA;IACA,mCAAA;AACA;AAEA;;IAEA,gBAAA;AACA;AAEA;;IAEA,aAAA;AACA;AAEA;IACA,cAAA;IACA,gBAAA;IACA,WAAA;IACA,kBAAA;IACA,OAAA;IACA,yBAAA;IACA,oBAAA;IACA,UAAA;AACA;AAEA;IACA,aAAA;IACA,aAAA;IACA,sBAAA;AACA;AAEA;IACA,WAAA;IACA,mBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\ndiv.select-list ul[data-v-2cd053b2] {\\n padding-left: 0;\\n}\\ndiv.select-list li[data-v-2cd053b2] {\\n list-style-type: none;\\n display: flex;\\n}\\ndiv.select-list .new-item[data-v-2cd053b2] {\\n display: flex;\\n}\\ndiv.select-list .new-item-help[data-v-2cd053b2] {\\n font-weight: bold;\\n padding-top: 5px;\\n}\\ndiv.select-list input[data-v-2cd053b2],\\ndiv.select-list img[data-v-2cd053b2] {\\n display: inline-block;\\n box-sizing: border-box;\\n}\\ndiv.select-list.max-width[data-v-2cd053b2] {\\n max-width: 450px;\\n}\\ndiv.select-list .switch-input[data-v-2cd053b2] {\\n left: -8px;\\n top: 4px;\\n position: absolute;\\n z-index: 10;\\n opacity: 0.6;\\n}\\n.form-inline[data-v-2cd053b2] {\\n display: contents;\\n}\\n.select-season[data-v-2cd053b2] {\\n height: 30px;\\n padding: 0 3px 0 2px;\\n}\\n.select-season[disabled=disabled][data-v-2cd053b2] {\\n background-color: #eee;\\n}\\n.external-scene-exception[data-v-2cd053b2] {\\n display: table-cell;\\n width: 4.5px;\\n border-top-left-radius: 0;\\n border-bottom-left-radius: 0;\\n background-color: #fff;\\n border-color: #ccc;\\n}\\n.external-scene-exception div[data-v-2cd053b2] {\\n display: flex;\\n align-items: center;\\n justify-content: center;\\n width: 40px;\\n height: 30px;\\n border: 1px solid #ccc;\\n border-top-left-radius: 0;\\n border-top-right-radius: 4px;\\n border-bottom-left-radius: 0;\\n border-bottom-right-radius: 4px;\\n z-index: 2;\\n margin-left: -1px;\\n}\\n.external-scene-exception > img[data-v-2cd053b2] {\\n display: block;\\n margin-left: auto;\\n margin-right: auto;\\n}\\n.external-scene-exception:last-child > .div[data-v-2cd053b2] {\\n border-top-left-radius: 0;\\n border-bottom-left-radius: 0;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-scene-exceptions.vue\"],\"names\":[],\"mappings\":\";AA6KA;IACA,eAAA;AACA;AAEA;IACA,qBAAA;IACA,aAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,iBAAA;IACA,gBAAA;AACA;AAEA;;IAEA,qBAAA;IACA,sBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,UAAA;IACA,QAAA;IACA,kBAAA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,iBAAA;AACA;AAEA;IACA,YAAA;IACA,oBAAA;AACA;AAEA;IACA,sBAAA;AACA;AAEA;IACA,mBAAA;IACA,YAAA;IACA,yBAAA;IACA,4BAAA;IACA,sBAAA;IACA,kBAAA;AACA;AAEA;IACA,aAAA;IACA,mBAAA;IACA,uBAAA;IACA,WAAA;IACA,YAAA;IACA,sBAAA;IACA,yBAAA;IACA,4BAAA;IACA,4BAAA;IACA,+BAAA;IACA,UAAA;IACA,iBAAA;AACA;AAEA;IACA,cAAA;IACA,iBAAA;IACA,kBAAA;AACA;AAEA;IACA,yBAAA;IACA,4BAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.autocomplete-item[data-v-0fbc8b46] {\\n background: white;\\n color: black;\\n}\\n\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-subtitle-languages.vue\"],\"names\":[],\"mappings\":\";AA+EA;IACA,iBAAA;IACA,YAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.form-control {\\n color: rgb(0, 0, 0);\\n}\\n.input75 {\\n width: 75px;\\n margin-top: -4px;\\n}\\n.input250 {\\n width: 250px;\\n margin-top: -4px;\\n}\\n.input350 {\\n width: 350px;\\n margin-top: -4px;\\n}\\n.input450 {\\n width: 450px;\\n margin-top: -4px;\\n}\\ninput {\\n margin-bottom: 5px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-textbox-number.vue\"],\"names\":[],\"mappings\":\";AA4FA;IACA,mBAAA;AACA;AAEA;IACA,WAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,kBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.input75[data-v-1f6251e9] {\\n width: 75px;\\n margin-top: -4px;\\n}\\n.input250[data-v-1f6251e9] {\\n width: 250px;\\n margin-top: -4px;\\n}\\n.input350[data-v-1f6251e9] {\\n width: 350px;\\n margin-top: -4px;\\n}\\n.input450[data-v-1f6251e9] {\\n width: 450px;\\n margin-top: -4px;\\n}\\ninput[data-v-1f6251e9] {\\n margin-bottom: 5px;\\n width: 100%;\\n border: none;\\n}\\n.uri-error-enter-active[data-v-1f6251e9],\\n.uri-error-leave-active[data-v-1f6251e9] {\\n -moz-transition-duration: 0.3s;\\n -webkit-transition-duration: 0.3s;\\n -o-transition-duration: 0.3s;\\n transition-duration: 0.3s;\\n -moz-transition-timing-function: ease-in;\\n -webkit-transition-timing-function: ease-in;\\n -o-transition-timing-function: ease-in;\\n transition-timing-function: ease-in;\\n}\\n.uri-error-enter-to[data-v-1f6251e9],\\n.uri-error-leave[data-v-1f6251e9] {\\n max-height: 100%;\\n}\\n.uri-error-enter[data-v-1f6251e9],\\n.uri-error-leave-to[data-v-1f6251e9] {\\n max-height: 0;\\n}\\n.parent[data-v-1f6251e9] {\\n position: relative;\\n}\\ndiv.uri-error[data-v-1f6251e9] {\\n display: block;\\n overflow: hidden;\\n width: 100%;\\n position: absolute;\\n left: 0;\\n background-color: #e23636;\\n padding: 0 2px 0 2px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-textbox.vue\"],\"names\":[],\"mappings\":\";AAiGA;IACA,WAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,kBAAA;IACA,WAAA;IACA,YAAA;AACA;AAEA;;IAEA,8BAAA;IACA,iCAAA;IACA,4BAAA;IACA,yBAAA;IACA,wCAAA;IACA,2CAAA;IACA,sCAAA;IACA,mCAAA;AACA;AAEA;;IAEA,gBAAA;AACA;AAEA;;IAEA,aAAA;AACA;AAEA;IACA,kBAAA;AACA;AAEA;IACA,cAAA;IACA,gBAAA;IACA,WAAA;IACA,kBAAA;IACA,OAAA;IACA,yBAAA;IACA,oBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.input75 {\\n width: 75px;\\n margin-top: -4px;\\n}\\n.input250 {\\n width: 250px;\\n margin-top: -4px;\\n}\\n.input350 {\\n width: 350px;\\n margin-top: -4px;\\n}\\n.input450 {\\n width: 450px;\\n margin-top: -4px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/config-toggle-slider.vue\"],\"names\":[],\"mappings\":\";AAkFA;IACA,WAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,gBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\ndiv.wrapper > div[data-v-12aec99f] {\\n margin-bottom: 5px;\\n}\\ndiv.identifier[data-v-12aec99f] {\\n float: none;\\n width: auto;\\n overflow: hidden;\\n}\\ndiv.level[data-v-12aec99f] {\\n float: right;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/custom-logs.vue\"],\"names\":[],\"mappings\":\";AA0EA;IACA,kBAAA;AACA;AAEA;IACA,WAAA;IACA,WAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.external-ids {\\n display: inline-block;\\n}\\n.external-ids > * {\\n margin-left: 2px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/externals.vue\"],\"names\":[],\"mappings\":\";AA4DA;IACA,qBAAA;AACA;AAEA;IACA,gBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\ndiv.file-browser.max-width[data-v-45b52762] {\\n max-width: 450px;\\n}\\ndiv.file-browser .input-group-no-btn[data-v-45b52762] {\\n display: flex;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/file-browser.vue\"],\"names\":[],\"mappings\":\";AAqTA;IACA,gBAAA;AACA;AAEA;IACA,aAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.app-image[data-v-05ed6915] {\\n max-width: 100%;\\n max-height: 100%;\\n width: auto;\\n height: auto;\\n vertical-align: middle;\\n}\\nimg[data-v-05ed6915]:not([src]) {\\n visibility: hidden;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/lazy-image.vue\"],\"names\":[],\"mappings\":\";AA+HA;IACA,eAAA;IACA,gBAAA;IACA,WAAA;IACA,YAAA;IACA,sBAAA;AACA;AAEA;IACA,kBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.load-progress-bar-container {\\n position: absolute;\\n bottom: 0;\\n right: 0;\\n}\\n.border {\\n box-sizing: border-box;\\n height: 26px;\\n text-size-adjust: 100%;\\n width: 382px;\\n perspective-origin: 191px 13px;\\n transform-origin: 191px 13px;\\n border: 1px solid #ccc;\\n border-radius: 5px;\\n}\\n@media (max-width: 767px) {\\n.border {\\n width: 100%;\\n}\\n.load-progress-bar-container {\\n width: 100%;\\n}\\n}\\n.progress {\\n box-sizing: border-box;\\n height: 24px;\\n text-size-adjust: 100%;\\n width: 76px;\\n perspective-origin: 38px 12px;\\n transform-origin: 38px 12px;\\n background: rgba(99, 177, 137, 0.63) none repeat scroll 0% 0% / auto padding-box border-box;\\n}\\n.msg {\\n position: absolute;\\n padding: 3px 5px 0 5px;\\n}\\n\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/load-progress-bar.vue\"],\"names\":[],\"mappings\":\";AAqCA;IACA,kBAAA;IACA,SAAA;IACA,QAAA;AACA;AAEA;IACA,sBAAA;IACA,YAAA;IACA,sBAAA;IACA,YAAA;IACA,8BAAA;IACA,4BAAA;IACA,sBAAA;IACA,kBAAA;AACA;AAEA;AACA;QACA,WAAA;AACA;AAEA;QACA,WAAA;AACA;AACA;AAEA;IACA,sBAAA;IACA,YAAA;IACA,sBAAA;IACA,WAAA;IACA,6BAAA;IACA,2BAAA;IACA,2FAAA;AACA;AAEA;IACA,kBAAA;IACA,sBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.plotInfo {\\n cursor: help;\\n float: right;\\n position: relative;\\n top: 2px;\\n}\\n.plotInfoNone {\\n cursor: help;\\n float: right;\\n position: relative;\\n top: 2px;\\n opacity: 0.4;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/plot-info.vue\"],\"names\":[],\"mappings\":\";AA0BA;IACA,YAAA;IACA,YAAA;IACA,kBAAA;IACA,QAAA;AACA;AAEA;IACA,YAAA;IACA,YAAA;IACA,kBAAA;IACA,QAAA;IACA,YAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.progressbar[data-v-2a6be2d2] {\\n position: relative;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/progress-bar.vue\"],\"names\":[],\"mappings\":\";AAsCA;IACA,kBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\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\\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\\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\\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\\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\\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\\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\\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\\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\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n/* Put both custom quality selectors in the same row */\\n#customQualityWrapper > div[data-v-7daf2e28] {\\n display: inline-block;\\n text-align: left;\\n}\\n\\n/* Put some distance between the two selectors */\\n#customQualityWrapper > div[data-v-7daf2e28]:first-of-type {\\n padding-right: 30px;\\n}\\n.backlog-link[data-v-7daf2e28] {\\n color: blue;\\n text-decoration: underline;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/quality-chooser.vue\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkVA,sDAAA;AACA;IACA,qBAAA;IACA,gBAAA;AACA;;AAEA,gDAAA;AACA;IACA,mBAAA;AACA;AAEA;IACA,WAAA;IACA,0BAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\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\\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\\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\\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\\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\\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/* Base class */\\n.quality[data-v-27b6c0c4] {\\n font: 12px/13px \\\"Open Sans\\\", verdana, sans-serif;\\n background-image: -webkit-linear-gradient(top, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 50%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.25));\\n background-image: -moz-linear-gradient(top, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 50%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.25));\\n background-image: -o-linear-gradient(top, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 50%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.25));\\n background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0) 50%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.25));\\n box-shadow: inset 0 1px rgba(255, 255, 255, 0.1), inset 0 -1px 3px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 1px 2px rgba(0, 0, 0, 0.15);\\n text-shadow: 0 1px rgba(0, 0, 0, 0.8);\\n color: rgb(255, 255, 255);\\n display: inline-block;\\n padding: 2px 4px;\\n text-align: center;\\n vertical-align: baseline;\\n border-radius: 4px;\\n white-space: nowrap;\\n}\\n\\n/* Custom */\\n.custom[data-v-27b6c0c4] {\\n background-color: rgb(98, 25, 147);\\n}\\n\\n/* HD-720p + FHD-1080p */\\n.hd[data-v-27b6c0c4], \\n.anyhdtv[data-v-27b6c0c4], \\n.anywebdl[data-v-27b6c0c4], \\n.anybluray[data-v-27b6c0c4] { /* AnySet */\\n background-color: rgb(38, 114, 182);\\n background-image:\\n repeating-linear-gradient(\\n -45deg,\\n rgb(38, 114, 182),\\n rgb(38, 114, 182) 10px,\\n rgb(91, 153, 13) 10px,\\n rgb(91, 153, 13) 20px\\n );\\n}\\n\\n/* HD-720p */\\n.hd720p[data-v-27b6c0c4], \\n.hdtv[data-v-27b6c0c4],\\n.hdwebdl[data-v-27b6c0c4],\\n.hdbluray[data-v-27b6c0c4] {\\n background-color: rgb(91, 153, 13);\\n}\\n\\n/* FHD-1080p */\\n.hd1080p[data-v-27b6c0c4], \\n.fullhdtv[data-v-27b6c0c4],\\n.fullhdwebdl[data-v-27b6c0c4],\\n.fullhdbluray[data-v-27b6c0c4] {\\n background-color: rgb(38, 114, 182);\\n}\\n\\n/* UHD-4K + UHD-8K */\\n.uhd[data-v-27b6c0c4] { /* Preset */\\n background-color: rgb(117, 0, 255);\\n background-image:\\n repeating-linear-gradient(\\n -45deg,\\n rgb(117, 0, 255),\\n rgb(117, 0, 255) 10px,\\n rgb(65, 0, 119) 10px,\\n rgb(65, 0, 119) 20px\\n );\\n}\\n\\n/* UHD-4K */\\n.uhd4k[data-v-27b6c0c4], \\n.anyuhd4k[data-v-27b6c0c4], \\n.uhd4ktv[data-v-27b6c0c4],\\n.uhd4kwebdl[data-v-27b6c0c4],\\n.uhd4kbluray[data-v-27b6c0c4] {\\n background-color: rgb(117, 0, 255);\\n}\\n\\n/* UHD-8K */\\n.uhd8k[data-v-27b6c0c4], \\n.anyuhd8k[data-v-27b6c0c4], \\n.uhd8ktv[data-v-27b6c0c4],\\n.uhd8kwebdl[data-v-27b6c0c4],\\n.uhd8kbluray[data-v-27b6c0c4] {\\n background-color: rgb(65, 0, 119);\\n}\\n\\n/* RawHD/RawHDTV */\\n.rawhdtv[data-v-27b6c0c4] {\\n background-color: rgb(205, 115, 0);\\n}\\n\\n/* SD */\\n.sd[data-v-27b6c0c4], \\n.sdtv[data-v-27b6c0c4],\\n.sddvd[data-v-27b6c0c4] {\\n background-color: rgb(190, 38, 37);\\n}\\n\\n/* Any */\\n.any[data-v-27b6c0c4] { /* Preset */\\n background-color: rgb(102, 102, 102);\\n}\\n\\n/* Unknown */\\n.unknown[data-v-27b6c0c4] {\\n background-color: rgb(153, 153, 153);\\n}\\n\\n/* Proper (used on History page) */\\n.proper[data-v-27b6c0c4] {\\n background-color: rgb(63, 127, 0);\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/quality-pill.vue\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8MA,eAAA;AACA;IACA,gDAAA;IACA,gJAAA;IACA,6IAAA;IACA,2IAAA;IACA,8IAAA;IACA,+JAAA;IACA,qCAAA;IACA,yBAAA;IACA,qBAAA;IACA,gBAAA;IACA,kBAAA;IACA,wBAAA;IACA,kBAAA;IACA,mBAAA;AACA;;AAEA,WAAA;AACA;IACA,kCAAA;AACA;;AAEA,wBAAA;AACA;;;8BAGA,WAAA;IACA,mCAAA;IACA;;;;;;;KAOA;AACA;;AAEA,YAAA;AACA;;;;IAIA,kCAAA;AACA;;AAEA,cAAA;AACA;;;;IAIA,mCAAA;AACA;;AAEA,oBAAA;AACA,wBAAA,WAAA;IACA,kCAAA;IACA;;;;;;;KAOA;AACA;;AAEA,WAAA;AACA;;;;;IAKA,kCAAA;AACA;;AAEA,WAAA;AACA;;;;;IAKA,iCAAA;AACA;;AAEA,kBAAA;AACA;IACA,kCAAA;AACA;;AAEA,OAAA;AACA;;;IAGA,kCAAA;AACA;;AAEA,QAAA;AACA,wBAAA,WAAA;IACA,oCAAA;AACA;;AAEA,YAAA;AACA;IACA,oCAAA;AACA;;AAEA,kCAAA;AACA;IACA,iCAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.isValid[data-v-9e73c4ba] {\\n background-color: #90ee90;\\n color: #fff;\\n font-weight: bold;\\n}\\n.isInvalid[data-v-9e73c4ba] {\\n background-color: #f00;\\n color: #fff !important;\\n font-weight: bold;\\n}\\n.isCustom[data-v-9e73c4ba] {\\n background-color: #00ebaf;\\n color: #fff !important;\\n font-weight: bold;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/scene-number-anime-input.vue\"],\"names\":[],\"mappings\":\";AAsIA;IACA,yBAAA;IACA,WAAA;IACA,iBAAA;AACA;AAEA;IACA,sBAAA;IACA,sBAAA;IACA,iBAAA;AACA;AAEA;IACA,yBAAA;IACA,sBAAA;IACA,iBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.isValid[data-v-8a700f60] {\\n background-color: #90ee90;\\n color: #fff;\\n font-weight: bold;\\n}\\n.isInvalid[data-v-8a700f60] {\\n background-color: #f00;\\n color: #fff !important;\\n font-weight: bold;\\n}\\n.isCustom[data-v-8a700f60] {\\n background-color: #00ebaf;\\n color: #fff !important;\\n font-weight: bold;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/scene-number-input.vue\"],\"names\":[],\"mappings\":\";AA+KA;IACA,yBAAA;IACA,WAAA;IACA,iBAAA;AACA;AAEA;IACA,sBAAA;IACA,sBAAA;IACA,iBAAA;AACA;AAEA;IACA,yBAAA;IACA,sBAAA;IACA,iBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.scroll-wrapper {\\n position: fixed;\\n opacity: 0;\\n visibility: hidden;\\n overflow: hidden;\\n text-align: center;\\n font-size: 20px;\\n z-index: 999;\\n background-color: #777;\\n color: #eee;\\n width: 50px;\\n height: 48px;\\n line-height: 48px;\\n right: 30px;\\n bottom: 30px;\\n padding-top: 2px;\\n border-radius: 10px;\\n -webkit-transition: all 0.5s ease-in-out;\\n -moz-transition: all 0.5s ease-in-out;\\n -ms-transition: all 0.5s ease-in-out;\\n -o-transition: all 0.5s ease-in-out;\\n transition: all 0.5s ease-in-out;\\n}\\n.scroll-wrapper.show {\\n visibility: visible;\\n cursor: pointer;\\n opacity: 1;\\n}\\n.scroll-wrapper.left {\\n position: fixed;\\n right: 150px;\\n}\\n.scroll-wrapper.right {\\n position: fixed;\\n right: 90px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/scroll-buttons.vue\"],\"names\":[],\"mappings\":\";AA8FA;IACA,eAAA;IACA,UAAA;IACA,kBAAA;IACA,gBAAA;IACA,kBAAA;IACA,eAAA;IACA,YAAA;IACA,sBAAA;IACA,WAAA;IACA,WAAA;IACA,YAAA;IACA,iBAAA;IACA,WAAA;IACA,YAAA;IACA,gBAAA;IACA,mBAAA;IACA,wCAAA;IACA,qCAAA;IACA,oCAAA;IACA,mCAAA;IACA,gCAAA;AACA;AAEA;IACA,mBAAA;IACA,eAAA;IACA,UAAA;AACA;AAEA;IACA,eAAA;IACA,YAAA;AACA;AAEA;IACA,eAAA;IACA,WAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n#naming-key {\\n margin: 20px 0 20px 0;\\n}\\n.template-container {\\n padding-left: 40px;\\n}\\n.show-template {\\n left: -8px;\\n top: 4px;\\n position: absolute;\\n z-index: 10;\\n opacity: 0.6;\\n}\\n.move-down {\\n transform: translateY(10px);\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/search-template-container.vue\"],\"names\":[],\"mappings\":\";AA6dA;IACA,qBAAA;AACA;AAEA;IACA,kBAAA;AACA;AAEA;IACA,UAAA;IACA,QAAA;IACA,kBAAA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,2BAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.show-template[data-v-4f60cc3f] {\\n left: -8px;\\n top: 4px;\\n position: absolute;\\n z-index: 10;\\n opacity: 0.6;\\n}\\n.template-example[data-v-4f60cc3f] {\\n display: block;\\n line-height: 20px;\\n margin-bottom: 15px;\\n padding: 2px 5px 2px 2px;\\n clear: left;\\n max-width: 338px;\\n}\\n.tooltip-wrapper[data-v-4f60cc3f] {\\n min-width: 340px;\\n}\\n.invalid[data-v-4f60cc3f] {\\n background-color: #ff5b5b;\\n}\\n.error-message[data-v-4f60cc3f] {\\n color: red;\\n}\\n.tooltip[data-v-4f60cc3f] {\\n display: block !important;\\n z-index: 10000;\\n}\\n.tooltip .tooltip-inner[data-v-4f60cc3f] {\\n background: #ffef93;\\n color: #555;\\n border-radius: 16px;\\n padding: 5px 10px 4px;\\n border: 1px solid #f1d031;\\n -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n}\\n.tooltip .tooltip-arrow[data-v-4f60cc3f] {\\n width: 0;\\n height: 0;\\n position: absolute;\\n margin: 5px;\\n border: 1px solid #ffef93;\\n z-index: 1;\\n}\\n.tooltip[x-placement^='top'][data-v-4f60cc3f] {\\n margin-bottom: 5px;\\n}\\n.tooltip[x-placement^='top'] .tooltip-arrow[data-v-4f60cc3f] {\\n border-width: 5px 5px 0 5px;\\n border-left-color: transparent !important;\\n border-right-color: transparent !important;\\n border-bottom-color: transparent !important;\\n bottom: -5px;\\n left: calc(50% - 4px);\\n margin-top: 0;\\n margin-bottom: 0;\\n}\\n.tooltip[x-placement^='bottom'][data-v-4f60cc3f] {\\n margin-top: 5px;\\n}\\n.tooltip[x-placement^='bottom'] .tooltip-arrow[data-v-4f60cc3f] {\\n border-width: 0 5px 5px 5px;\\n border-left-color: transparent !important;\\n border-right-color: transparent !important;\\n border-top-color: transparent !important;\\n top: -5px;\\n left: calc(50% - 4px);\\n margin-top: 0;\\n margin-bottom: 0;\\n}\\n.tooltip[x-placement^='right'][data-v-4f60cc3f] {\\n margin-left: 5px;\\n}\\n.tooltip[x-placement^='right'] .tooltip-arrow[data-v-4f60cc3f] {\\n border-width: 5px 5px 5px 0;\\n border-left-color: transparent !important;\\n border-top-color: transparent !important;\\n border-bottom-color: transparent !important;\\n left: -4px;\\n top: calc(50% - 5px);\\n margin-left: 0;\\n margin-right: 0;\\n}\\n.tooltip[x-placement^='left'][data-v-4f60cc3f] {\\n margin-right: 5px;\\n}\\n.tooltip[x-placement^='left'] .tooltip-arrow[data-v-4f60cc3f] {\\n border-width: 5px 0 5px 5px;\\n border-top-color: transparent !important;\\n border-right-color: transparent !important;\\n border-bottom-color: transparent !important;\\n right: -4px;\\n top: calc(50% - 5px);\\n margin-left: 0;\\n margin-right: 0;\\n}\\n.tooltip.popover .popover-inner[data-v-4f60cc3f] {\\n background: #ffef93;\\n color: #555;\\n padding: 24px;\\n border-radius: 5px;\\n box-shadow: 0 5px 30px rgba(black, 0.1);\\n}\\n.tooltip.popover .popover-arrow[data-v-4f60cc3f] {\\n border-color: #ffef93;\\n}\\n.tooltip[aria-hidden='true'][data-v-4f60cc3f] {\\n visibility: hidden;\\n opacity: 0;\\n transition: opacity 0.15s, visibility 0.15s;\\n}\\n.tooltip[aria-hidden='false'][data-v-4f60cc3f] {\\n visibility: visible;\\n opacity: 1;\\n transition: opacity 0.15s;\\n}\\n.vertical-align[data-v-4f60cc3f] {\\n display: flex;\\n align-items: center;\\n}\\n.vertical-align > p[data-v-4f60cc3f] {\\n margin: auto 10px;\\n}\\n.pattern > span[data-v-4f60cc3f] {\\n position: absolute;\\n top: 0;\\n left: 25px;\\n color: black;\\n background-color: grey;\\n padding: 1px 5px;\\n opacity: 0.8;\\n border-radius: 5px;\\n}\\n.pattern[data-v-4f60cc3f] {\\n position: relative;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/search-template-custom.vue\"],\"names\":[],\"mappings\":\";AA+TA;IACA,UAAA;IACA,QAAA;IACA,kBAAA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,cAAA;IACA,iBAAA;IACA,mBAAA;IACA,wBAAA;IACA,WAAA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,yBAAA;AACA;AAEA;IACA,UAAA;AACA;AAEA;IACA,yBAAA;IACA,cAAA;AACA;AAEA;IACA,mBAAA;IACA,WAAA;IACA,mBAAA;IACA,qBAAA;IACA,yBAAA;IACA,uDAAA;IACA,oDAAA;IACA,+CAAA;AACA;AAEA;IACA,QAAA;IACA,SAAA;IACA,kBAAA;IACA,WAAA;IACA,yBAAA;IACA,UAAA;AACA;AAEA;IACA,kBAAA;AACA;AAEA;IACA,2BAAA;IACA,yCAAA;IACA,0CAAA;IACA,2CAAA;IACA,YAAA;IACA,qBAAA;IACA,aAAA;IACA,gBAAA;AACA;AAEA;IACA,eAAA;AACA;AAEA;IACA,2BAAA;IACA,yCAAA;IACA,0CAAA;IACA,wCAAA;IACA,SAAA;IACA,qBAAA;IACA,aAAA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,2BAAA;IACA,yCAAA;IACA,wCAAA;IACA,2CAAA;IACA,UAAA;IACA,oBAAA;IACA,cAAA;IACA,eAAA;AACA;AAEA;IACA,iBAAA;AACA;AAEA;IACA,2BAAA;IACA,wCAAA;IACA,0CAAA;IACA,2CAAA;IACA,WAAA;IACA,oBAAA;IACA,cAAA;IACA,eAAA;AACA;AAEA;IACA,mBAAA;IACA,WAAA;IACA,aAAA;IACA,kBAAA;IACA,uCAAA;AACA;AAEA;IACA,qBAAA;AACA;AAEA;IACA,kBAAA;IACA,UAAA;IACA,2CAAA;AACA;AAEA;IACA,mBAAA;IACA,UAAA;IACA,yBAAA;AACA;AAEA;IACA,aAAA;IACA,mBAAA;AACA;AAEA;IACA,iBAAA;AACA;AAEA;IACA,kBAAA;IACA,MAAA;IACA,UAAA;IACA,YAAA;IACA,sBAAA;IACA,gBAAA;IACA,YAAA;IACA,kBAAA;AACA;AAEA;IACA,kBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.show-template[data-v-007d54df] {\\n left: -8px;\\n top: 4px;\\n position: absolute;\\n z-index: 10;\\n opacity: 0.6;\\n}\\n.template-example[data-v-007d54df] {\\n display: block;\\n line-height: 20px;\\n margin-bottom: 15px;\\n padding: 2px 5px 2px 2px;\\n clear: left;\\n max-width: 338px;\\n}\\n.template-wrapper[data-v-007d54df] {\\n max-width: 350px;\\n}\\n.tooltip-wrapper-pattern[data-v-007d54df] {\\n max-width: 350px;\\n}\\n.invalid[data-v-007d54df] {\\n background-color: #ff5b5b;\\n}\\n.tooltip[data-v-007d54df] {\\n display: block !important;\\n z-index: 10000;\\n}\\n.tooltip .tooltip-inner[data-v-007d54df] {\\n background: #ffef93;\\n color: #555;\\n border-radius: 16px;\\n padding: 5px 10px 4px;\\n border: 1px solid #f1d031;\\n -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);\\n}\\n.tooltip .tooltip-arrow[data-v-007d54df] {\\n width: 0;\\n height: 0;\\n position: absolute;\\n margin: 5px;\\n border: 1px solid #ffef93;\\n z-index: 1;\\n}\\n.tooltip[x-placement^='top'][data-v-007d54df] {\\n margin-bottom: 5px;\\n}\\n.tooltip[x-placement^='top'] .tooltip-arrow[data-v-007d54df] {\\n border-width: 5px 5px 0 5px;\\n border-left-color: transparent !important;\\n border-right-color: transparent !important;\\n border-bottom-color: transparent !important;\\n bottom: -5px;\\n left: calc(50% - 4px);\\n margin-top: 0;\\n margin-bottom: 0;\\n}\\n.tooltip[x-placement^='bottom'][data-v-007d54df] {\\n margin-top: 5px;\\n}\\n.tooltip[x-placement^='bottom'] .tooltip-arrow[data-v-007d54df] {\\n border-width: 0 5px 5px 5px;\\n border-left-color: transparent !important;\\n border-right-color: transparent !important;\\n border-top-color: transparent !important;\\n top: -5px;\\n left: calc(50% - 4px);\\n margin-top: 0;\\n margin-bottom: 0;\\n}\\n.tooltip[x-placement^='right'][data-v-007d54df] {\\n margin-left: 5px;\\n}\\n.tooltip[x-placement^='right'] .tooltip-arrow[data-v-007d54df] {\\n border-width: 5px 5px 5px 0;\\n border-left-color: transparent !important;\\n border-top-color: transparent !important;\\n border-bottom-color: transparent !important;\\n left: -4px;\\n top: calc(50% - 5px);\\n margin-left: 0;\\n margin-right: 0;\\n}\\n.tooltip[x-placement^='left'][data-v-007d54df] {\\n margin-right: 5px;\\n}\\n.tooltip[x-placement^='left'] .tooltip-arrow[data-v-007d54df] {\\n border-width: 5px 0 5px 5px;\\n border-top-color: transparent !important;\\n border-right-color: transparent !important;\\n border-bottom-color: transparent !important;\\n right: -4px;\\n top: calc(50% - 5px);\\n margin-left: 0;\\n margin-right: 0;\\n}\\n.tooltip.popover .popover-inner[data-v-007d54df] {\\n background: #ffef93;\\n color: #555;\\n padding: 24px;\\n border-radius: 5px;\\n box-shadow: 0 5px 30px rgba(black, 0.1);\\n}\\n.tooltip.popover .popover-arrow[data-v-007d54df] {\\n border-color: #ffef93;\\n}\\n.tooltip[aria-hidden='true'][data-v-007d54df] {\\n visibility: hidden;\\n opacity: 0;\\n transition: opacity 0.15s, visibility 0.15s;\\n}\\n.tooltip[aria-hidden='false'][data-v-007d54df] {\\n visibility: visible;\\n opacity: 1;\\n transition: opacity 0.15s;\\n}\\n.template-title[data-v-007d54df] {\\n padding: 0 0 3px 5px;\\n display: inline-block;\\n}\\n.template-remove[data-v-007d54df] {\\n float: right;\\n margin-right: 4px;\\n margin-top: 2px;\\n}\\n.template-remove[data-v-007d54df]:hover {\\n animation-name: spin-data-v-007d54df;\\n animation-duration: 500ms;\\n animation-timing-function: ease-out;\\n}\\n@keyframes spin-data-v-007d54df {\\nfrom {\\n transform: rotate(0deg);\\n}\\nto {\\n transform: rotate(180deg);\\n}\\n}\\n.template-body[data-v-007d54df] {\\n display: flex;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/search-template-pattern.vue\"],\"names\":[],\"mappings\":\";AAqMA;IACA,UAAA;IACA,QAAA;IACA,kBAAA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,cAAA;IACA,iBAAA;IACA,mBAAA;IACA,wBAAA;IACA,WAAA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,yBAAA;AACA;AAEA;IACA,yBAAA;IACA,cAAA;AACA;AAEA;IACA,mBAAA;IACA,WAAA;IACA,mBAAA;IACA,qBAAA;IACA,yBAAA;IACA,uDAAA;IACA,oDAAA;IACA,+CAAA;AACA;AAEA;IACA,QAAA;IACA,SAAA;IACA,kBAAA;IACA,WAAA;IACA,yBAAA;IACA,UAAA;AACA;AAEA;IACA,kBAAA;AACA;AAEA;IACA,2BAAA;IACA,yCAAA;IACA,0CAAA;IACA,2CAAA;IACA,YAAA;IACA,qBAAA;IACA,aAAA;IACA,gBAAA;AACA;AAEA;IACA,eAAA;AACA;AAEA;IACA,2BAAA;IACA,yCAAA;IACA,0CAAA;IACA,wCAAA;IACA,SAAA;IACA,qBAAA;IACA,aAAA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,2BAAA;IACA,yCAAA;IACA,wCAAA;IACA,2CAAA;IACA,UAAA;IACA,oBAAA;IACA,cAAA;IACA,eAAA;AACA;AAEA;IACA,iBAAA;AACA;AAEA;IACA,2BAAA;IACA,wCAAA;IACA,0CAAA;IACA,2CAAA;IACA,WAAA;IACA,oBAAA;IACA,cAAA;IACA,eAAA;AACA;AAEA;IACA,mBAAA;IACA,WAAA;IACA,aAAA;IACA,kBAAA;IACA,uCAAA;AACA;AAEA;IACA,qBAAA;AACA;AAEA;IACA,kBAAA;IACA,UAAA;IACA,2CAAA;AACA;AAEA;IACA,mBAAA;IACA,UAAA;IACA,yBAAA;AACA;AAEA;IACA,oBAAA;IACA,qBAAA;AACA;AAEA;IACA,YAAA;IACA,iBAAA;IACA,eAAA;AACA;AAEA;IACA,oCAAA;IACA,yBAAA;IACA,mCAAA;AACA;AAEA;AACA;QACA,uBAAA;AACA;AAEA;QACA,yBAAA;AACA;AACA;AAEA;IACA,aAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.mobile-select[data-v-4a074bc4] {\\n width: 110px;\\n font-size: x-small;\\n}\\n.search-wrapper[data-v-4a074bc4] {\\n float: left;\\n}\\n.search-wrapper > img[data-v-4a074bc4] {\\n cursor: pointer;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/search.vue\"],\"names\":[],\"mappings\":\";AA8KA;IACA,YAAA;IACA,kBAAA;AACA;AAEA;IACA,WAAA;AACA;AAEA;IACA,eAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\ndiv.select-list ul[data-v-1843461f] {\\n padding-left: 0;\\n}\\ndiv.select-list li[data-v-1843461f] {\\n list-style-type: none;\\n display: flex;\\n}\\ndiv.select-list .new-item[data-v-1843461f] {\\n display: flex;\\n}\\ndiv.select-list .new-item-help[data-v-1843461f] {\\n font-weight: bold;\\n padding-top: 5px;\\n}\\ndiv.select-list input[data-v-1843461f],\\ndiv.select-list img[data-v-1843461f] {\\n display: inline-block;\\n box-sizing: border-box;\\n}\\ndiv.select-list.max-width[data-v-1843461f] {\\n max-width: 450px;\\n}\\ndiv.select-list .switch-input[data-v-1843461f] {\\n left: -8px;\\n top: 4px;\\n position: absolute;\\n z-index: 10;\\n opacity: 0.6;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/select-list.vue\"],\"names\":[],\"mappings\":\";AA6KA;IACA,eAAA;AACA;AAEA;IACA,qBAAA;IACA,aAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,iBAAA;IACA,gBAAA;AACA;AAEA;;IAEA,qBAAA;IACA,sBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,UAAA;IACA,QAAA;IACA,kBAAA;IACA,WAAA;IACA,YAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\nul[data-v-20efe074] {\\n padding-left: 0;\\n}\\nli[data-v-20efe074] {\\n list-style-type: none;\\n}\\n.trakt-list-group[data-v-20efe074] {\\n display: inline-flex;\\n height: 25px;\\n}\\ninput[data-v-20efe074] {\\n display: inline-block;\\n box-sizing: border-box;\\n}\\ninput.available-list[data-v-20efe074] {\\n height: 22px;\\n}\\ninput[type=checkbox][data-v-20efe074] {\\n width: 20px;\\n height: 20px;\\n}\\n.max-width[data-v-20efe074] {\\n max-width: 450px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/select-trakt-lists.vue\"],\"names\":[],\"mappings\":\";AA2CA;IACA,eAAA;AACA;AAEA;IACA,qBAAA;AACA;AAEA;IACA,oBAAA;IACA,YAAA;AACA;AAEA;IACA,qBAAA;IACA,sBAAA;AACA;AAEA;IACA,YAAA;AACA;AAEA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,gBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\nselect.select-show {\\n display: inline-block;\\n height: 25px;\\n padding: 1px;\\n min-width: 200px;\\n}\\n.show-selector {\\n height: 31px;\\n display: table-cell;\\n left: 20px;\\n margin-bottom: 5px;\\n}\\n@media (max-width: 767px) and (min-width: 341px) {\\n.select-show-group,\\n .select-show {\\n width: 100%;\\n}\\n}\\n@media (max-width: 340px) {\\n.select-show-group {\\n width: 100%;\\n}\\n}\\n@media (max-width: 767px) {\\n.show-selector {\\n float: left;\\n width: 100%;\\n}\\n.select-show {\\n width: 100%;\\n}\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/show-selector.vue\"],\"names\":[],\"mappings\":\";AAuHA;IACA,qBAAA;IACA,YAAA;IACA,YAAA;IACA,gBAAA;AACA;AAEA;IACA,YAAA;IACA,mBAAA;IACA,UAAA;IACA,kBAAA;AACA;AAEA;AACA;;QAEA,WAAA;AACA;AACA;AAEA;AACA;QACA,WAAA;AACA;AACA;AAEA;AACA;QACA,WAAA;QACA,WAAA;AACA;AAEA;QACA,WAAA;AACA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\ndiv.select-list ul[data-v-48a6e4c2] {\\n padding-left: 0;\\n}\\ndiv.select-list li[data-v-48a6e4c2] {\\n list-style-type: none;\\n display: flex;\\n}\\ndiv.select-list .new-item[data-v-48a6e4c2] {\\n display: flex;\\n}\\ndiv.select-list .new-item-help[data-v-48a6e4c2] {\\n font-weight: bold;\\n padding-top: 5px;\\n}\\ndiv.select-list input[data-v-48a6e4c2],\\ndiv.select-list img[data-v-48a6e4c2] {\\n display: inline-block;\\n box-sizing: border-box;\\n}\\ndiv.select-list.max-width[data-v-48a6e4c2] {\\n max-width: 450px;\\n}\\ndiv.select-list .switch-input[data-v-48a6e4c2] {\\n left: -8px;\\n top: 4px;\\n position: absolute;\\n z-index: 10;\\n opacity: 0.6;\\n}\\n.draggable-list[data-v-48a6e4c2] {\\n list-style: none;\\n}\\n.draggable-input-group[data-v-48a6e4c2] {\\n display: inline-flex;\\n width: 300px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/sorted-select-list.vue\"],\"names\":[],\"mappings\":\";AA8IA;IACA,eAAA;AACA;AAEA;IACA,qBAAA;IACA,aAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,iBAAA;IACA,gBAAA;AACA;AAEA;;IAEA,qBAAA;IACA,sBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,UAAA;IACA,QAAA;IACA,kBAAA;IACA,WAAA;IACA,YAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,oBAAA;IACA,YAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.opacity-25 {\\n opacity: 0.25;\\n}\\n.opacity-75 {\\n opacity: 0.75;\\n}\\n.animate-spin {\\n animation: spin 1s linear infinite;\\n color: green;\\n}\\n@keyframes spin {\\nfrom {\\n transform: rotate(0deg);\\n}\\nto {\\n transform: rotate(360deg);\\n}\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/state-switch.vue\"],\"names\":[],\"mappings\":\";AAuEA;IACA,aAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,kCAAA;IACA,YAAA;AACA;AAEA;AACA;QACA,uBAAA;AACA;AAEA;QACA,yBAAA;AACA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\npre[data-v-3796064f] {\\n word-wrap: break-word;\\n}\\n.error[data-v-3796064f] {\\n width: 100%;\\n padding: 0.5rem;\\n background-color: red;\\n font-weight: 700;\\n border-radius: 2px;\\n margin-bottom: 2rem;\\n}\\n.matched-show[data-v-3796064f] {\\n display: flex;\\n margin-bottom: 2rem;\\n}\\n.matched-show > div[data-v-3796064f] {\\n margin-right: 2rem;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/test-guessit.vue\"],\"names\":[],\"mappings\":\";AA6EA;IACA,qBAAA;AACA;AAEA;IACA,WAAA;IACA,eAAA;IACA,qBAAA;IACA,gBAAA;IACA,kBAAA;IACA,mBAAA;AACA;AAEA;IACA,aAAA;IACA,mBAAA;AACA;AAEA;IACA,kBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.test-provider[data-v-a8b1934a] {\\n float: left;\\n}\\n.test-provider > button[data-v-a8b1934a] {\\n margin: 0 5px;\\n}\\n.testresult[data-v-a8b1934a] {\\n border-style: solid;\\n border-width: 1px;\\n padding: 1px 4px 4px 4px;\\n border-color: #ccc;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/helpers/test-provider.vue\"],\"names\":[],\"mappings\":\";AA8DA;IACA,WAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,mBAAA;IACA,iBAAA;IACA,wBAAA;IACA,kBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\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\\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\\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\\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\\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\\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\\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\\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\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n/* History compact */\\nspan.release-group[data-v-09e6ca40] {\\n cursor: help;\\n margin-right: 5px;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/history-compact.vue\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqTA,oBAAA;AACA;IACA,YAAA;IACA,iBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\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\\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\\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\\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\\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\\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\\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\\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\\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\\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\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n/* History tables */\\n.status-name > svg[data-v-499f13b8] {\\n margin-left: 5px;\\n}\\n.vgt-multiselect[data-v-499f13b8] {\\n min-height: 30px;\\n}\\n.multiselect--active[data-v-499f13b8] {\\n min-width: 200px;\\n}\\n.vgt-multiselect[data-v-499f13b8] .multiselect__placeholder {\\n margin-bottom: 0;\\n padding-top: 0;\\n}\\n.vgt-multiselect[data-v-499f13b8] .multiselect__tags {\\n padding-top: 0;\\n min-height: 30px;\\n}\\n:not(tr.status) span.episode-title a[data-v-499f13b8],\\n:not(tr.status) span.show-title a[data-v-499f13b8] {\\n text-decoration: none;\\n color: rgb(255, 255, 255);\\n}\\ntr.status span.episode-title a[data-v-499f13b8],\\ntr span.show-title a[data-v-499f13b8] {\\n text-decoration: none;\\n color: rgb(0, 0, 0);\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/history-detailed.vue\"],\"names\":[],\"mappings\":\";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8XA,mBAAA;AACA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;AACA;AAEA;IACA,gBAAA;IACA,cAAA;AACA;AAEA;IACA,cAAA;IACA,gBAAA;AACA;AAEA;;IAEA,qBAAA;IACA,yBAAA;AACA;AAEA;;IAEA,qBAAA;IACA,mBAAA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\nul.list-group > li[data-v-5e3ed5ae] {\\n list-style: none;\\n margin-bottom: 10px;\\n}\\n.home-header-controls[data-v-5e3ed5ae] {\\n display: flex;\\n}\\n.home-header-controls > h2[data-v-5e3ed5ae] {\\n white-space: nowrap;\\n}\\n.home-options[data-v-5e3ed5ae] {\\n align-self: flex-end;\\n width: 100%;\\n}\\n.home-filter-option[data-v-5e3ed5ae] {\\n margin-left: 10px;\\n line-height: 40px;\\n}\\n@media (max-width: 768px) {\\n.show-option[data-v-5e3ed5ae] {\\n width: 100%;\\n display: inline-block;\\n}\\n.show-option-layout > span[data-v-5e3ed5ae] {\\n display: none;\\n}\\n}\\n\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/home.vue\"],\"names\":[],\"mappings\":\";AAuLA;IACA,gBAAA;IACA,mBAAA;AACA;AAEA;IACA,aAAA;AACA;AAEA;IACA,mBAAA;AACA;AAEA;IACA,oBAAA;IACA,WAAA;AACA;AAEA;IACA,iBAAA;IACA,iBAAA;AACA;AAEA;AACA;QACA,WAAA;QACA,qBAAA;AACA;AAEA;QACA,aAAA;AACA;AACA\",\"sourcesContent\":[\"\\n\\n\\n\\n\\n\"],\"sourceRoot\":\"\"}]);\n// Exports\nmodule.exports = ___CSS_LOADER_EXPORT___;\n","// Imports\nvar ___CSS_LOADER_API_SOURCEMAP_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/sourceMaps.js\");\nvar ___CSS_LOADER_API_IMPORT___ = require(\"../../node_modules/css-loader/dist/runtime/api.js\");\nvar ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_SOURCEMAP_IMPORT___);\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \"\\n.irc-frame[data-v-e1ac0426] {\\n width: 100%;\\n height: 500px;\\n border: 1px #000 solid;\\n}\\n.loading-spinner[data-v-e1ac0426] {\\n background-position: center center;\\n background-repeat: no-repeat;\\n}\\n\", \"\",{\"version\":3,\"sources\":[\"webpack://./src/components/irc.vue\"],\"names\":[],\"mappings\":\";AA2BA;IACA,WAAA;IACA,aAAA;IACA,sBAAA;AACA;AAEA;IACA,kCAAA;IACA,4BAAA;AACA\",\"sourcesContent\":[\"