parent
							
								
									63cfaefe2b
								
							
						
					
					
						commit
						8fc06ca351
					
				
				 2 changed files with 845 additions and 633 deletions
			
			
		@ -1,633 +0,0 @@ | 
				
			|||||||
#!/usr/bin/env python3 | 
					 | 
				
			||||||
# (c) 2015 Taeyeon Mori <orochimarufan.x3@gmail.com> | 
					 | 
				
			||||||
# Anime Import V2-4 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import os | 
					 | 
				
			||||||
import sys | 
					 | 
				
			||||||
import re | 
					 | 
				
			||||||
import itertools | 
					 | 
				
			||||||
import logging | 
					 | 
				
			||||||
import argparse | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
logger = logging.getLogger("AnimeImport") | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
## Utilities                                                                 ## | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
def cat_to(filename, text): | 
					 | 
				
			||||||
     with open(filename, "w") as f: | 
					 | 
				
			||||||
         f.write(text) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def cat_from(filename): | 
					 | 
				
			||||||
     with open(filename) as f: | 
					 | 
				
			||||||
         return f.read() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def abspath_from(path, anchor): | 
					 | 
				
			||||||
    return os.path.normpath(os.path.join(anchor, path)) if not os.path.isabs(path) else path | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def transport_path(path, old_anchor, new_anchor): | 
					 | 
				
			||||||
    """ | 
					 | 
				
			||||||
    :brief: Transport a relative path from one anchor to another | 
					 | 
				
			||||||
    """ | 
					 | 
				
			||||||
    return os.path.relpath(abspath_from(path, old_anchor), new_anchor) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def make_symlink(target, at, anchor=None): | 
					 | 
				
			||||||
    """ | 
					 | 
				
			||||||
    :brief: Make a symbolic link | 
					 | 
				
			||||||
    :param target: The link target | 
					 | 
				
			||||||
    :param at: The location of the new symlink | 
					 | 
				
			||||||
    :param anchor: The anchor if <target> is relative. defaults to os.curdir | 
					 | 
				
			||||||
    This function preserves the absoluteness of <target>, meaning that if you pass in | 
					 | 
				
			||||||
        an absolute path, the link will be created using that absolute path. | 
					 | 
				
			||||||
        However, any relative path will be transported to the new link's containing directory. | 
					 | 
				
			||||||
        That is important if the link isn't created in the cwd because posix symlink() can take | 
					 | 
				
			||||||
        any value which will be interpreted relative to the directory containing the link. | 
					 | 
				
			||||||
    """ | 
					 | 
				
			||||||
    if os.path.isabs(target): | 
					 | 
				
			||||||
        os.symlink(target, at) | 
					 | 
				
			||||||
    elif os.path.isdir(at): | 
					 | 
				
			||||||
        os.symlink(transport_path(target, anchor if anchor else os.curdir, at), at) | 
					 | 
				
			||||||
    else: | 
					 | 
				
			||||||
        os.symlink(transport_path(target, anchor if anchor else os.curdir, os.path.dirname(at)), at) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def clean_links(path): | 
					 | 
				
			||||||
    for f in os.listdir(path): | 
					 | 
				
			||||||
        if not f.startswith(".") and os.path.islink(os.path.join(path, f)): | 
					 | 
				
			||||||
            os.unlink(os.path.join(path, f)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def maybe_number(s): | 
					 | 
				
			||||||
    return int(s) if s.isdigit() else s | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def opt_value_str(o): | 
					 | 
				
			||||||
    # since maybe_number converts them back to numbers, we abuse numbers as bools | 
					 | 
				
			||||||
    if o == True: | 
					 | 
				
			||||||
        return "1" | 
					 | 
				
			||||||
    elif o == False: | 
					 | 
				
			||||||
        return "0" | 
					 | 
				
			||||||
    else: | 
					 | 
				
			||||||
        return str(o) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def natural_sort_key(s, _nsre=re.compile(r'(\d+)')): | 
					 | 
				
			||||||
    return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)] | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
## Specials patterns                                                         ## | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
class Special: | 
					 | 
				
			||||||
    """ | 
					 | 
				
			||||||
    Specials format: | 
					 | 
				
			||||||
        Specials <= Special '\n' Specials | Special | 
					 | 
				
			||||||
        Special <= Properties '\n' Regexp | 
					 | 
				
			||||||
        Regexp <= regular expression with optional 'ep' group | 
					 | 
				
			||||||
        Properties <= Property '|' Properties | Property | 
					 | 
				
			||||||
        Property <= Name '=' Value | 
					 | 
				
			||||||
        Name <= python identifier | 
					 | 
				
			||||||
        Value <= string value without '\n' and '|' | 
					 | 
				
			||||||
    Valid keys: | 
					 | 
				
			||||||
        type <= "extern" "special" "opening" "ending" "trailer" "parody" "other" | 
					 | 
				
			||||||
        name <= Folder name for "extern" type | 
					 | 
				
			||||||
        offset <= Number to add to the episode number | 
					 | 
				
			||||||
        epnum <= Force all matched episodes to have this episode number (discouraged. use offset instead) | 
					 | 
				
			||||||
        first <= start counting episodes here. This only applies if <ep> is NOT matched in the regexp  | 
					 | 
				
			||||||
        subdir <= Specials are located in a subdir of the source | 
					 | 
				
			||||||
    Regexp: | 
					 | 
				
			||||||
        Should contain an <ep> group to match the (relative to [offset]) episode/special/ending/etc number | 
					 | 
				
			||||||
    """ | 
					 | 
				
			||||||
    def __init__(self, properties, pattern): | 
					 | 
				
			||||||
        self._properties = properties | 
					 | 
				
			||||||
        self.pattern = pattern | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.season = 0 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if "type" in properties: | 
					 | 
				
			||||||
            t = properties["type"].lower() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if t in ("extern", "episode", "episodes"): | 
					 | 
				
			||||||
                self.season = 1 | 
					 | 
				
			||||||
                off = 0 | 
					 | 
				
			||||||
            elif t in ("special", "s", "specials"): | 
					 | 
				
			||||||
                off = 0 | 
					 | 
				
			||||||
            elif t in ("opening", "op", "openings"): | 
					 | 
				
			||||||
                off = 99 | 
					 | 
				
			||||||
            elif t in ("ending", "ed", "endings"): | 
					 | 
				
			||||||
                off = 149 | 
					 | 
				
			||||||
            elif t in ("trailer", "t", "trailers"): | 
					 | 
				
			||||||
                off = 199 | 
					 | 
				
			||||||
            elif t in ("parody", "p", "parodies"): | 
					 | 
				
			||||||
                off = 299 | 
					 | 
				
			||||||
            elif t in ("other", "o", "others"): | 
					 | 
				
			||||||
                off = 399 | 
					 | 
				
			||||||
            else: | 
					 | 
				
			||||||
                off = 499 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            self.type_offset = off | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            self.type_offset = 0 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.is_extern = "name" in properties | 
					 | 
				
			||||||
        self.is_subdir = "subdir" in properties | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if "$custom" in properties: | 
					 | 
				
			||||||
            if not callable(properties["$custom"]): | 
					 | 
				
			||||||
                raise ValueError("$custom Specials must be created from python code and be callable objects") | 
					 | 
				
			||||||
            self.custom = properties["$custom"] | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __getattr__(self, name): | 
					 | 
				
			||||||
        return self._properties[name] | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __contains__(self, name): | 
					 | 
				
			||||||
        return name in self._properties | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def source(self): | 
					 | 
				
			||||||
        return "\n".join(("|".join(map("=".join, self._properties.items())), self.pattern.pattern)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def match(self, f): | 
					 | 
				
			||||||
        return self.pattern.search(f) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def custom(self, m, i): | 
					 | 
				
			||||||
        return None | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def adjust_episode(self, ep): | 
					 | 
				
			||||||
        ep += self.type_offset | 
					 | 
				
			||||||
          | 
					 | 
				
			||||||
        if "offset" in self: | 
					 | 
				
			||||||
            return ep + int(self.offset) | 
					 | 
				
			||||||
        elif "epnum" in self: | 
					 | 
				
			||||||
            return int(self.epnum) | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            return ep | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def get_episode(self, match, last): | 
					 | 
				
			||||||
        if "ep" in self.pattern.groupindex and match.group("ep"): | 
					 | 
				
			||||||
            # TODO: fix this. When doing it properly it breaks the assumption that ...OP is OP1 while OPn is OPn. It makes OP OPn+1 | 
					 | 
				
			||||||
            return self.adjust_episode(int(match.group("ep"))) | 
					 | 
				
			||||||
            #return self.adjust_episode(int(match.group("ep"))) | 
					 | 
				
			||||||
        elif last == 0: | 
					 | 
				
			||||||
            return self.adjust_episode(1) | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            return last + 1 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @staticmethod | 
					 | 
				
			||||||
    def parse_properties(src): | 
					 | 
				
			||||||
        return dict(map(lambda p: p.split("=", 1), src.split("|"))) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod | 
					 | 
				
			||||||
    def parse(cls, props_src, regexp_src): | 
					 | 
				
			||||||
        return cls(cls.parse_properties(props_src), re.compile(regexp_src)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod | 
					 | 
				
			||||||
    def iterparse(cls, src): | 
					 | 
				
			||||||
        for a, b in zip(*[iter(src.split("\n"))]*2): | 
					 | 
				
			||||||
            yield cls.parse(a, b) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @classmethod | 
					 | 
				
			||||||
    def simple(cls, type, regexp): | 
					 | 
				
			||||||
        return cls({"type": type}, re.compile(regexp)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
## The core                                                                  ## | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
class Importer: | 
					 | 
				
			||||||
    # Filenames | 
					 | 
				
			||||||
    source_fn = ".source" | 
					 | 
				
			||||||
    master_fn = ".main" | 
					 | 
				
			||||||
    pattern_fn = ".pattern" | 
					 | 
				
			||||||
    exclude_fn = ".exclude" | 
					 | 
				
			||||||
    special_fn = ".specials" | 
					 | 
				
			||||||
    options_fn = ".options" | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Defaults | 
					 | 
				
			||||||
    default_pattern = re.compile(r"[_ ](E|e|Ep|Episode|ep)?[ _]?(?P<ep>\d+)([Vv]\d+)?[_ ]?[\(\[\.]?") | 
					 | 
				
			||||||
    default_options = { | 
					 | 
				
			||||||
        "auto_specials": 1, | 
					 | 
				
			||||||
        "exclude_parts": 1, | 
					 | 
				
			||||||
        "exclude_playlists": 1, | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    auto_specials = [ | 
					 | 
				
			||||||
        Special.simple("Opening", r"[_ ](NC)?OP[ _]?[^\d]"), # FIXME: HACK | 
					 | 
				
			||||||
        Special.simple("Opening", r"[_ ]((NC)?OP|Opening)[ _]?(?P<ep>\d+)"), | 
					 | 
				
			||||||
        Special.simple("Ending", r"[_ ](NC)?ED[ _]?[^\d]"), # FIXME: HACK | 
					 | 
				
			||||||
        Special.simple("Ending", r"[_ ]((NC)?ED|Ending|Closing)[ _]?(?P<ep>\d+)"), | 
					 | 
				
			||||||
        Special.simple("Special", r"[_ ](Special|OVA|SP)[ _]?(?P<ep>\d+)?[ _]?"), | 
					 | 
				
			||||||
        # .nfo files | 
					 | 
				
			||||||
        Special({"$custom": lambda m, i: ("link", os.path.join(i.destination, i.main_name + ".nfo"))}, re.compile(r".nfo$")) | 
					 | 
				
			||||||
    ] | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    link_template = "{series} S{season:d}E{episode:03d}" | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, destination): | 
					 | 
				
			||||||
        # Find master | 
					 | 
				
			||||||
        master_f = os.path.join(destination, self.master_fn) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        while os.path.islink(master_f): | 
					 | 
				
			||||||
            new_dest = os.readlink(master_f) | 
					 | 
				
			||||||
            if not os.path.isabs(new_dest): | 
					 | 
				
			||||||
                new_dest = transport_path(new_dest, destination, os.curdir) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            logger.info("Destination '%s' belongs to '%s'; Selecting that instead" % (destination, new_dest)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            destination = new_dest | 
					 | 
				
			||||||
            master_f = os.path.join(destination, self.master_fn) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.destination = os.path.abspath(destination) | 
					 | 
				
			||||||
        self.main_name = os.path.basename(self.destination) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        source_f = os.path.join(destination, self.source_fn) | 
					 | 
				
			||||||
        if os.path.islink(source_f): | 
					 | 
				
			||||||
            self.source = os.readlink(source_f) | 
					 | 
				
			||||||
            if not os.path.isabs(self.source): | 
					 | 
				
			||||||
                self.source = transport_path(self.source, destination, os.curdir) | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            self.source = None | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        options_f = os.path.join(destination, self.options_fn) | 
					 | 
				
			||||||
        if os.path.isfile(options_f): | 
					 | 
				
			||||||
            self.options = {k: maybe_number(v) for k, v in (x.split(": ") for x in cat_from(options_f).split("\n"))} | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            self.options = {} | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        pattern_f = os.path.join(destination, self.pattern_fn) | 
					 | 
				
			||||||
        if os.path.isfile(pattern_f): | 
					 | 
				
			||||||
            self.pattern = re.compile(cat_from(pattern_f).rstrip("\n")) | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            self.pattern = self.default_pattern | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        exclude_f = os.path.join(destination, self.exclude_fn) | 
					 | 
				
			||||||
        if os.path.isfile(exclude_f): | 
					 | 
				
			||||||
            self.exclude = re.compile(cat_from(exclude_f).rstrip("\n")) | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            self.exclude = None | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        special_f = os.path.join(destination, self.special_fn) | 
					 | 
				
			||||||
        if os.path.isfile(special_f): | 
					 | 
				
			||||||
            self.specials = list(Special.iterparse(cat_from(special_f))) | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            self.specials = [] | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _save(self, filename, content): | 
					 | 
				
			||||||
        # save data to <destination>/<filename> | 
					 | 
				
			||||||
        path = os.path.join(self.destination, filename) | 
					 | 
				
			||||||
        if content is not None: | 
					 | 
				
			||||||
            with open(path, "w") as f: | 
					 | 
				
			||||||
                f.write(content) | 
					 | 
				
			||||||
        elif os.path.exists(path): | 
					 | 
				
			||||||
            os.unlink(path) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def save(self): | 
					 | 
				
			||||||
        # Write settings to disk | 
					 | 
				
			||||||
        if not os.path.isdir(self.destination): | 
					 | 
				
			||||||
            os.mkdir(self.destination) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self._save(self.pattern_fn, self.pattern.pattern if self.pattern is not self.default_pattern else None) | 
					 | 
				
			||||||
        self._save(self.exclude_fn, self.exclude.pattern if self.exclude is not None else None) | 
					 | 
				
			||||||
        self._save(self.special_fn, "\n".join(map(Special.source, self.specials)) if self.specials else None) | 
					 | 
				
			||||||
        self._save(self.options_fn, "\n".join((": ".join((k, opt_value_str(v))) for k, v in self.options.items())) if self.options else None) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        source_f = os.path.join(self.destination, self.source_fn) | 
					 | 
				
			||||||
        if os.path.islink(source_f): | 
					 | 
				
			||||||
            oldpath = transport_path(os.readlink(source_f), self.destination, os.curdir) | 
					 | 
				
			||||||
            if oldpath != self.source.rstrip("/"): | 
					 | 
				
			||||||
                logger.warn("Updating source link '%s' with '%s'" % (oldpath, self.source)) | 
					 | 
				
			||||||
            os.unlink(source_f) | 
					 | 
				
			||||||
        make_symlink(self.source, source_f) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for sp in self.specials: | 
					 | 
				
			||||||
            if not sp.is_extern: | 
					 | 
				
			||||||
                continue | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            path = os.path.join(self.destination, "..", sp.name) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if not os.path.isdir(path): | 
					 | 
				
			||||||
                os.mkdir(path) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            master_f = os.path.join(path, self.master_fn) | 
					 | 
				
			||||||
            if os.path.islink(master_f): | 
					 | 
				
			||||||
                oldpath = transport_path(os.readlink(master_f), path, os.curdir) | 
					 | 
				
			||||||
                if oldpath != self.destination.rstrip("/"): | 
					 | 
				
			||||||
                    logger.warn("Updating master link '%s' with '%s'" % (oldpath, self.destination)) | 
					 | 
				
			||||||
                os.unlink(master_f) | 
					 | 
				
			||||||
            make_symlink(self.destination, master_f) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @property | 
					 | 
				
			||||||
    def effective_specials(self): | 
					 | 
				
			||||||
        return itertools.chain(self.specials, self.auto_specials) if self.option("auto_specials") else self.specials | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    def option(self, name): | 
					 | 
				
			||||||
        return self.options[name] if name in self.options else self.default_options.get(name, None) | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    def process_file(self, filename, subdir=None): | 
					 | 
				
			||||||
        # Exclude | 
					 | 
				
			||||||
        if self.option("exclude_parts") and filename.endswith(".part"): | 
					 | 
				
			||||||
            return "skip", "partial download" | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif self.option("exclude_playlists") and (filename[-4:] in (".vml", ".m3u", ".pls")): | 
					 | 
				
			||||||
            return "skip", "playlist file" | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif self.exclude and self.exclude.search(filename): | 
					 | 
				
			||||||
            return "skip", "excluded" | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        linkpath = self.destination | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Specials | 
					 | 
				
			||||||
        for special in self.effective_specials: | 
					 | 
				
			||||||
            if bool(subdir) != special.is_subdir or (subdir and subdir != special.subdir): | 
					 | 
				
			||||||
                continue | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            sm = special.match(filename) | 
					 | 
				
			||||||
            if sm: | 
					 | 
				
			||||||
                result = special.custom(sm, self) | 
					 | 
				
			||||||
                if result: | 
					 | 
				
			||||||
                    return result | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                ep = special.get_episode(sm, self.last_special.get(special, 0)) | 
					 | 
				
			||||||
                self.last_special[special] = ep | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if special.is_extern: | 
					 | 
				
			||||||
                    name = special.name | 
					 | 
				
			||||||
                    linkpath = os.path.join(self.destination, "..", name) | 
					 | 
				
			||||||
                else: | 
					 | 
				
			||||||
                    name = self.main_name | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                linkname = self.link_template.format(series=name, season=special.season, episode=ep) | 
					 | 
				
			||||||
                break | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            if subdir: | 
					 | 
				
			||||||
                #logger.warn("Unhandled file in subdir %s: %s" % (subdir, filename)) | 
					 | 
				
			||||||
                return "skip", "in subdirectory" | 
					 | 
				
			||||||
             | 
					 | 
				
			||||||
            m = self.pattern.search(filename) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if m: | 
					 | 
				
			||||||
                self.last_episode = int(m.group("ep")) | 
					 | 
				
			||||||
            else: | 
					 | 
				
			||||||
                self.last_episode += 1 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            linkname = self.link_template.format(series=self.main_name, season=1, episode=self.last_episode) # TODO: allow more seasons? | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return "link", os.path.join(linkpath, linkname + os.path.splitext(filename)[1]) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def clean_all(self): | 
					 | 
				
			||||||
        clean_links(self.destination) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for special in self.specials: # auto_specials doesn't have "extern" specials | 
					 | 
				
			||||||
            if special.is_extern: | 
					 | 
				
			||||||
                clean_links(os.path.join(self.destination, "..", special.name)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def run(self, *, dry=False): | 
					 | 
				
			||||||
        if not dry: | 
					 | 
				
			||||||
            self.clean_all() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.last_episode = 0 # FIXME: global state | 
					 | 
				
			||||||
        self.last_special = {} | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for f in sorted(os.listdir(self.source), key=natural_sort_key): | 
					 | 
				
			||||||
            path = os.path.join(self.source, f) | 
					 | 
				
			||||||
             | 
					 | 
				
			||||||
            if os.path.isdir(path): | 
					 | 
				
			||||||
                if self.specials: | 
					 | 
				
			||||||
                    for ff in sorted(os.listdir(path), key=natural_sort_key): | 
					 | 
				
			||||||
                        if self.handle_file(os.path.join(f, ff), *self.process_file(ff, subdir=f), dry=dry) != 0: | 
					 | 
				
			||||||
                            return 1 | 
					 | 
				
			||||||
            else: | 
					 | 
				
			||||||
                if self.handle_file(f, *self.process_file(f), dry=dry) != 0: | 
					 | 
				
			||||||
                    return 1 | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    def handle_file(self, f, what, where, *, dry=False): | 
					 | 
				
			||||||
        if what == "link": | 
					 | 
				
			||||||
            if os.path.exists(where) and not dry: | 
					 | 
				
			||||||
                logger.error("LINK %s => %s exists!" % (f, os.path.basename(where))) | 
					 | 
				
			||||||
                return 1 | 
					 | 
				
			||||||
            else: | 
					 | 
				
			||||||
                logger.info("LINK %s => %s" % (f, os.path.basename(where))) | 
					 | 
				
			||||||
             | 
					 | 
				
			||||||
            if not dry: | 
					 | 
				
			||||||
                make_symlink(os.path.join(self.source, f), where) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif what == "skip": | 
					 | 
				
			||||||
            logger.info("SKIP %s (%s)" % (f, where)) | 
					 | 
				
			||||||
         | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            assert(False and "Should not be reached") | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return 0 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def reset(self, *things): | 
					 | 
				
			||||||
        if "pattern" in things: | 
					 | 
				
			||||||
            self.pattern = self.default_pattern | 
					 | 
				
			||||||
        if "exclude" in things: | 
					 | 
				
			||||||
            self.exclude = None | 
					 | 
				
			||||||
        if "specials" in things: | 
					 | 
				
			||||||
            self.specials.clear() | 
					 | 
				
			||||||
        if "options" in things: | 
					 | 
				
			||||||
            self.options.clear() | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    def print_info(self): | 
					 | 
				
			||||||
        print("Import Info for %s:" % self.main_name) | 
					 | 
				
			||||||
        print("  Pattern: r'%s'%s" % (self.pattern.pattern, " (default)" if self.pattern is self.default_pattern else "")) | 
					 | 
				
			||||||
        print("  Exclude: %s" % (("r'%s'" % self.exclude.pattern) if self.exclude else "None")) | 
					 | 
				
			||||||
        print("  Options: %s" % self.options) | 
					 | 
				
			||||||
        print("  Specials:%s" % (" (None)" if not self.specials else "")) | 
					 | 
				
			||||||
        for special in self.specials: | 
					 | 
				
			||||||
            print("      %-25s :: %s" % ("r'%s'" % special.pattern.pattern, special._properties)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def flags(self): | 
					 | 
				
			||||||
        return "".join((f for c, f in [ | 
					 | 
				
			||||||
            (self.pattern is not self.default_pattern, "p"), | 
					 | 
				
			||||||
            (self.exclude, "e"), | 
					 | 
				
			||||||
            #(self.option("exclude_parts"), "d"), | 
					 | 
				
			||||||
            (not self.option("exclude_parts"), "D"), | 
					 | 
				
			||||||
            (self.option("auto_specials"), "i"), | 
					 | 
				
			||||||
            (not self.option("auto_specials"), "I"), | 
					 | 
				
			||||||
            (self.specials, str(len(self.specials))), | 
					 | 
				
			||||||
        ] if c)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
## Argument handling                                                         ## | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
class HelpFormatter(argparse.RawTextHelpFormatter): | 
					 | 
				
			||||||
    def __init__(self, prog): | 
					 | 
				
			||||||
        super().__init__(prog, max_help_position=16) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def parse_args(argv): | 
					 | 
				
			||||||
    parser = argparse.ArgumentParser(prog=argv[0], formatter_class=HelpFormatter) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    paths = parser.add_argument_group("Paths") | 
					 | 
				
			||||||
    paths.add_argument("source", | 
					 | 
				
			||||||
            help="The source directory") | 
					 | 
				
			||||||
    paths.add_argument("destination", default=".", nargs="?", | 
					 | 
				
			||||||
            help="The target directory. Note that all visible symlinks inside will be deleted! (default: working dir)") | 
					 | 
				
			||||||
    paths.add_argument("-r", "--recurse", action="store_true", | 
					 | 
				
			||||||
            help="Walk all subdirectories of <destination>") | 
					 | 
				
			||||||
    paths.add_argument("-S", "--check-unknown", action="append", default=[], metavar="PATH", | 
					 | 
				
			||||||
            help="Check The source directory for untracked folders. Use with --recurse") | 
					 | 
				
			||||||
    paths.add_argument("-X", "--check-ignore", action="append", default=[], metavar="FOLDER", | 
					 | 
				
			||||||
            help="Ignore a folder <FOLDER> when checking for untracked folders") | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    patterns = parser.add_argument_group("Patterns", | 
					 | 
				
			||||||
            description="Patterns are Python re patterns: https://docs.python.org/3/library/re.html") | 
					 | 
				
			||||||
    patterns.add_argument("-p", "--pattern", default=None, metavar="PATTERN", | 
					 | 
				
			||||||
            help="Set the episode pattern. Include a named group <ep>") | 
					 | 
				
			||||||
    patterns.add_argument("-x", "--exclude", default=None, metavar="PATTERN", | 
					 | 
				
			||||||
            help="Set the exclusion pattern") | 
					 | 
				
			||||||
    patterns.add_argument("-s", "--special", "--specials", default=[], nargs=2, action="append", metavar=("SPECIAL", "PATTERN"), dest="specials", | 
					 | 
				
			||||||
            help=("Set the special mapping. This takes 2 arguments and can be specified multiple times.\n" | 
					 | 
				
			||||||
                  "1st argument: key=value properties separated by '|'.\n" | 
					 | 
				
			||||||
                  "2nd argument: the matching pattern. It should contain a <ep> group\n" | 
					 | 
				
			||||||
                  "Valid keys so far are 'type', 'offset', 'name'.\n" | 
					 | 
				
			||||||
                  "type can be 'special', 'opening', 'ending', 'trailer', 'parody', 'other' and 'extern'.\n" | 
					 | 
				
			||||||
                  "offset adds a fixed number to all episodes numbers matched by the pattern.\n" | 
					 | 
				
			||||||
                  "name must only be used with 'extern'. It creates a slave Series to hold the matched episodes in the parent directory.\n" | 
					 | 
				
			||||||
                  "It's useful for singling out specials that are recorded as independent series in the metadata provider.")) | 
					 | 
				
			||||||
    patterns.add_argument("-a", "--append", action="store_true", | 
					 | 
				
			||||||
            help="Extend the special mapping instead of replacing it") | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    options = parser.add_argument_group("Options") | 
					 | 
				
			||||||
    options.add_argument("-i", "--auto-specials", action="store_true", dest="auto_specials", default=None, | 
					 | 
				
			||||||
            help="Implicitly add some Specials patterns (default)") | 
					 | 
				
			||||||
    options.add_argument("-I", "--no-auto-specials", action="store_false", dest="auto_specials", default=None, | 
					 | 
				
			||||||
            help="Don't add the implicit Specials patterns") | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parser.add_argument("--clear", default=[], action="append", choices={"pattern", "exclude", "specials", "options"}, | 
					 | 
				
			||||||
            help="Reset a property to the defaults") | 
					 | 
				
			||||||
    parser.add_argument("-D", "--dry-run", action="store_true", | 
					 | 
				
			||||||
            help="Don't save anything to disk Useful combination") | 
					 | 
				
			||||||
    parser.add_argument("-q", "--quiet", action="store_true") | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return parser.parse_args(argv[1:]) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
## Put the pieces together                                                   ## | 
					 | 
				
			||||||
############################################################################### | 
					 | 
				
			||||||
def main(argv): | 
					 | 
				
			||||||
    logging.basicConfig(level=logging.INFO, format="%(levelname)-5s %(message)s") | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    args = parse_args(argv) | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    if args.quiet: | 
					 | 
				
			||||||
        logger.setLevel(logging.WARNING) | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    if args.source == "info": | 
					 | 
				
			||||||
        if args.recurse: | 
					 | 
				
			||||||
            logger.setLevel(logging.WARNING) | 
					 | 
				
			||||||
            have_dirs = set() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for dest in filter(os.path.isdir, (os.path.join(args.destination, x) for x in os.listdir(args.destination))): | 
					 | 
				
			||||||
                i = Importer(dest) | 
					 | 
				
			||||||
                if i.destination not in have_dirs: | 
					 | 
				
			||||||
                    i.print_info() | 
					 | 
				
			||||||
                    print() | 
					 | 
				
			||||||
                    have_dirs.add(i.destination) | 
					 | 
				
			||||||
                # We just ignore dupes from slaves | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            Importer(args.destination).print_info() | 
					 | 
				
			||||||
            print() | 
					 | 
				
			||||||
        return 0 | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    if args.recurse: | 
					 | 
				
			||||||
        if args.pattern or args.exclude or args.specials or args.auto_specials is not None or args.clear: | 
					 | 
				
			||||||
            logger.error("--recurse can only be combined with --check-unknown") | 
					 | 
				
			||||||
            return -1 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        got_dirs = set() | 
					 | 
				
			||||||
        OK = 0 | 
					 | 
				
			||||||
        dirs = filter(os.path.isdir, (os.path.join(args.destination, x) for x in os.listdir(args.destination))) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if args.source == "update": | 
					 | 
				
			||||||
            fin_dirs = set() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for dest in dirs: | 
					 | 
				
			||||||
                i = Importer(dest) | 
					 | 
				
			||||||
                logger.info("Processing '%s' (%s)" % (dest, i.flags())) | 
					 | 
				
			||||||
                if not i.source: | 
					 | 
				
			||||||
                    logger.info("'%s' doesn't seem to be an import. Skipping" % os.path.basename(dest)) | 
					 | 
				
			||||||
                elif i.destination in fin_dirs: | 
					 | 
				
			||||||
                    logger.info("Already processed '%s'. Skipping" % os.path.basename(dest)) | 
					 | 
				
			||||||
                elif not os.path.exists(i.source): | 
					 | 
				
			||||||
                    logger.error("Source directory doesn't exist: '%s'" % i.source) | 
					 | 
				
			||||||
                    OK += 1 | 
					 | 
				
			||||||
                elif not i.run(dry=args.dry_run): # returns 0 (False) on success | 
					 | 
				
			||||||
                    got_dirs.add(os.path.abspath(i.source)) | 
					 | 
				
			||||||
                    fin_dirs.add(i.destination) | 
					 | 
				
			||||||
                else: | 
					 | 
				
			||||||
                    OK += 1 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif args.source == "check": | 
					 | 
				
			||||||
            for dest in dirs: | 
					 | 
				
			||||||
                i = Importer(dest) | 
					 | 
				
			||||||
                if i.source: | 
					 | 
				
			||||||
                    got_dirs.add(os.path.abspath(i.source)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        else: | 
					 | 
				
			||||||
            logger.error("--recurse can only be used with 'update', 'check' and 'info'") | 
					 | 
				
			||||||
            return -1 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if args.check_unknown: | 
					 | 
				
			||||||
            dirs = set(map(os.path.abspath, filter(os.path.isdir, itertools.chain.from_iterable(((os.path.join(f, x) for x in os.listdir(f)) for f in args.check_unknown))))) | 
					 | 
				
			||||||
            ignore = set(map(os.path.abspath, filter(os.path.isdir, itertools.chain.from_iterable(((os.path.join(f, x) for x in args.check_ignore) for f in args.check_unknown))))) | 
					 | 
				
			||||||
            missing = dirs - got_dirs - ignore | 
					 | 
				
			||||||
            if missing: | 
					 | 
				
			||||||
                print("Found missing directories: %s" % "\n                           ".join(missing)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return OK | 
					 | 
				
			||||||
     | 
					 | 
				
			||||||
    elif args.source == "check": | 
					 | 
				
			||||||
        logger.error("'check' only makes sense in combination with --recurse.") | 
					 | 
				
			||||||
        return -1 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    else: | 
					 | 
				
			||||||
        if args.check_unknown: | 
					 | 
				
			||||||
            logger.error("--check-unknown must be used with --recurse") | 
					 | 
				
			||||||
            return -1 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        i = Importer(args.destination) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if args.source != "update": | 
					 | 
				
			||||||
            i.source = args.source | 
					 | 
				
			||||||
        elif not args.source: | 
					 | 
				
			||||||
            logger.error("'%s' doesn't look like a previously imported directory" % args.destination) | 
					 | 
				
			||||||
            return -2 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        i.reset(*args.clear) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if args.pattern: | 
					 | 
				
			||||||
            i.pattern = re.compile(args.pattern) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if args.exclude: | 
					 | 
				
			||||||
            i.exclude = re.compile(args.exclude) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if args.specials: | 
					 | 
				
			||||||
            if args.append: | 
					 | 
				
			||||||
                i.specials.extend(itertools.starmap(Special.parse, args.specials)) | 
					 | 
				
			||||||
            else: | 
					 | 
				
			||||||
                i.specials = list(itertools.starmap(Special.parse, args.specials)) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for opt in ("auto_specials",): | 
					 | 
				
			||||||
            if getattr(args, opt) is not None: | 
					 | 
				
			||||||
                i.options[opt] = getattr(args, opt) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not args.dry_run: | 
					 | 
				
			||||||
            i.save() | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return i.run(dry=args.dry_run) | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if __name__ == "__main__": | 
					 | 
				
			||||||
    sys.exit(main(sys.argv)) | 
					 | 
				
			||||||
@ -0,0 +1 @@ | 
				
			|||||||
 | 
					animelib.py | 
				
			||||||
@ -0,0 +1,844 @@ | 
				
			|||||||
 | 
					#!/usr/bin/env python3 | 
				
			||||||
 | 
					# (c) 2015-2017 Taeyeon Mori <orochimarufan.x3@gmail.com> | 
				
			||||||
 | 
					# Anime Import V5 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os | 
				
			||||||
 | 
					import sys | 
				
			||||||
 | 
					import re | 
				
			||||||
 | 
					import itertools | 
				
			||||||
 | 
					import functools | 
				
			||||||
 | 
					import logging | 
				
			||||||
 | 
					import argparse | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					logger = logging.getLogger("AnimeImport") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					## Utilities                                                                 ## | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					def cat_to(filename, text): | 
				
			||||||
 | 
					     with open(filename, "w") as f: | 
				
			||||||
 | 
					         f.write(text) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def cat_from(filename): | 
				
			||||||
 | 
					     with open(filename) as f: | 
				
			||||||
 | 
					         return f.read() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def abspath_from(path, anchor): | 
				
			||||||
 | 
					    return os.path.normpath(os.path.join(anchor, path)) if not os.path.isabs(path) else path | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def transport_path(path, old_anchor, new_anchor): | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    :brief: Transport a relative path from one anchor to another | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    return os.path.relpath(abspath_from(path, old_anchor), new_anchor) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def make_symlink(target, at, anchor=None): | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    :brief: Make a symbolic link | 
				
			||||||
 | 
					    :param target: The link target | 
				
			||||||
 | 
					    :param at: The location of the new symlink | 
				
			||||||
 | 
					    :param anchor: The anchor if <target> is relative. defaults to os.curdir | 
				
			||||||
 | 
					    This function preserves the absoluteness of <target>, meaning that if you pass in | 
				
			||||||
 | 
					        an absolute path, the link will be created using that absolute path. | 
				
			||||||
 | 
					        However, any relative path will be transported to the new link's containing directory. | 
				
			||||||
 | 
					        That is important if the link isn't created in the cwd because posix symlink() can take | 
				
			||||||
 | 
					        any value which will be interpreted relative to the directory containing the link. | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    if os.path.isabs(target): | 
				
			||||||
 | 
					        os.symlink(target, at) | 
				
			||||||
 | 
					    elif os.path.isdir(at): | 
				
			||||||
 | 
					        os.symlink(transport_path(target, anchor if anchor else os.curdir, at), at) | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					        os.symlink(transport_path(target, anchor if anchor else os.curdir, os.path.dirname(at)), at) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def make_symlink_in(target, in_directory, linkname, anchor=None): | 
				
			||||||
 | 
					    if os.path.isabs(target): | 
				
			||||||
 | 
					        os.symlink(target, os.path.join(in_directory, linkname)) | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					        os.symlink(transport_path(target, anchor if anchor else os.curdir, in_directory), | 
				
			||||||
 | 
					                   os.path.join(in_directory, linkname)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def clean_links(path): | 
				
			||||||
 | 
					    for f in os.listdir(path): | 
				
			||||||
 | 
					        if not f.startswith(".") and os.path.islink(os.path.join(path, f)): | 
				
			||||||
 | 
					            os.unlink(os.path.join(path, f)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def maybe_number(s): | 
				
			||||||
 | 
					    return int(s) if s.isdigit() else s | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def opt_value_str(o): | 
				
			||||||
 | 
					    # since maybe_number converts them back to numbers, we abuse numbers as bools | 
				
			||||||
 | 
					    if o == True: | 
				
			||||||
 | 
					        return "1" | 
				
			||||||
 | 
					    elif o == False: | 
				
			||||||
 | 
					        return "0" | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					        return str(o) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def natural_sort_key(s, _nsre=re.compile(r'(\d+)')): | 
				
			||||||
 | 
					    return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					## Specials patterns                                                         ## | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					class Special: | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    Specials format: | 
				
			||||||
 | 
					        Specials <= Special '\n' Specials | Special | 
				
			||||||
 | 
					        Special <= Properties '\n' Regexp | 
				
			||||||
 | 
					        Regexp <= regular expression with optional 'ep' group | 
				
			||||||
 | 
					        Properties <= Property '|' Properties | Property | 
				
			||||||
 | 
					        Property <= Name '=' Value | 
				
			||||||
 | 
					        Name <= python identifier | 
				
			||||||
 | 
					        Value <= string value without '\n' and '|' | 
				
			||||||
 | 
					    Valid keys: | 
				
			||||||
 | 
					        type <= "extern" "special" "opening" "ending" "trailer" "parody" "other" | 
				
			||||||
 | 
					        name <= Folder name for "extern" type | 
				
			||||||
 | 
					        offset <= Number to add to the episode number | 
				
			||||||
 | 
					        epnum <= Force all matched episodes to have this episode number (discouraged. use offset instead) | 
				
			||||||
 | 
					        first <= start counting episodes here. This only applies if <ep> is NOT matched in the regexp | 
				
			||||||
 | 
					        subdir <= Specials are located in a subdir of the source | 
				
			||||||
 | 
					    Regexp: | 
				
			||||||
 | 
					        Should contain an <ep> group to match the (relative to [offset]) episode/special/ending/etc number | 
				
			||||||
 | 
					    """ | 
				
			||||||
 | 
					    def __init__(self, properties, pattern): | 
				
			||||||
 | 
					        self._properties = properties | 
				
			||||||
 | 
					        self.pattern = pattern | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.season = 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if "type" in properties: | 
				
			||||||
 | 
					            t = properties["type"].lower() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if t in ("extern", "episode", "episodes"): | 
				
			||||||
 | 
					                self.season = 1 | 
				
			||||||
 | 
					                off = 0 | 
				
			||||||
 | 
					            elif t in ("special", "s", "specials"): | 
				
			||||||
 | 
					                off = 0 | 
				
			||||||
 | 
					            elif t in ("opening", "op", "openings"): | 
				
			||||||
 | 
					                off = 99 | 
				
			||||||
 | 
					            elif t in ("ending", "ed", "endings"): | 
				
			||||||
 | 
					                off = 149 | 
				
			||||||
 | 
					            elif t in ("trailer", "t", "trailers"): | 
				
			||||||
 | 
					                off = 199 | 
				
			||||||
 | 
					            elif t in ("parody", "p", "parodies"): | 
				
			||||||
 | 
					                off = 299 | 
				
			||||||
 | 
					            elif t in ("other", "o", "others"): | 
				
			||||||
 | 
					                off = 399 | 
				
			||||||
 | 
					            else: | 
				
			||||||
 | 
					                off = 499 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.type_offset = off | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            self.type_offset = 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.is_extern = "name" in properties | 
				
			||||||
 | 
					        self.is_subdir = "subdir" in properties | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if "$custom" in properties: | 
				
			||||||
 | 
					            if not callable(properties["$custom"]): | 
				
			||||||
 | 
					                raise ValueError("$custom Specials must be created from python code and be callable objects") | 
				
			||||||
 | 
					            self.custom = properties["$custom"] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __getattr__(self, name): | 
				
			||||||
 | 
					        return self._properties[name] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __contains__(self, name): | 
				
			||||||
 | 
					        return name in self._properties | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def source(self): | 
				
			||||||
 | 
					        return "\n".join(("|".join(map("=".join, self._properties.items())), self.pattern.pattern)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def match(self, f): | 
				
			||||||
 | 
					        return self.pattern.search(f) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def custom(self, m, i): | 
				
			||||||
 | 
					        return None | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def adjust_episode(self, ep): | 
				
			||||||
 | 
					        ep += self.type_offset | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if "offset" in self: | 
				
			||||||
 | 
					            return ep + int(self.offset) | 
				
			||||||
 | 
					        elif "epnum" in self: | 
				
			||||||
 | 
					            return int(self.epnum) | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            return ep | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_episode(self, match, last): | 
				
			||||||
 | 
					        if "ep" in self.pattern.groupindex and match.group("ep"): | 
				
			||||||
 | 
					            # TODO: fix this. When doing it properly it breaks the assumption that ...OP is OP1 while OPn is OPn. It makes OP OPn+1 | 
				
			||||||
 | 
					            return self.adjust_episode(int(match.group("ep"))) | 
				
			||||||
 | 
					            #return self.adjust_episode(int(match.group("ep"))) | 
				
			||||||
 | 
					        elif last == 0: | 
				
			||||||
 | 
					            return self.adjust_episode(1) | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            return last + 1 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod | 
				
			||||||
 | 
					    def parse_properties(src): | 
				
			||||||
 | 
					        return dict(map(lambda p: p.split("=", 1), src.split("|"))) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod | 
				
			||||||
 | 
					    def parse(cls, props_src, regexp_src): | 
				
			||||||
 | 
					        return cls(cls.parse_properties(props_src), re.compile(regexp_src)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod | 
				
			||||||
 | 
					    def iterparse(cls, src): | 
				
			||||||
 | 
					        for a, b in zip(*[iter(src.split("\n"))]*2): | 
				
			||||||
 | 
					            yield cls.parse(a, b) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod | 
				
			||||||
 | 
					    def simple(cls, type, regexp): | 
				
			||||||
 | 
					        return cls({"type": type}, re.compile(regexp)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					## The core                                                                  ## | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					class Importer: | 
				
			||||||
 | 
					    # Filenames | 
				
			||||||
 | 
					    source_fn = ".source" | 
				
			||||||
 | 
					    master_fn = ".main" | 
				
			||||||
 | 
					    pattern_fn = ".pattern" | 
				
			||||||
 | 
					    exclude_fn = ".exclude" | 
				
			||||||
 | 
					    special_fn = ".specials" | 
				
			||||||
 | 
					    options_fn = ".options" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Defaults | 
				
			||||||
 | 
					    default_pattern = re.compile(r"[\s_.](?:[Ss](?:eason)?(?P<season>\d+))?[\s_.]*(?:[Ee](?:[Pp]|pisode)?)?[\s_.]?(?P<ep>\d+)(?:\-(?P<untilep>\d+))?([Vv]\d+)?[\s_.]*[\(\[\.]?") | 
				
			||||||
 | 
					    default_options = { | 
				
			||||||
 | 
					        "auto_specials": 1, | 
				
			||||||
 | 
					        "exclude_parts": 1, | 
				
			||||||
 | 
					        "exclude_playlists": 1, | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    auto_specials = [ | 
				
			||||||
 | 
					        Special.simple("Opening", r"[_ ](NC|TV)?OP[ _]?[^\d]"), # FIXME: HACK | 
				
			||||||
 | 
					        Special.simple("Opening", r"[_ ]((NC|TV)?OP|Opening)[ _]?(?P<ep>\d+)"), | 
				
			||||||
 | 
					        Special.simple("Ending", r"[_ ](NC|TV)?ED[ _]?[^\d]"), # FIXME: HACK | 
				
			||||||
 | 
					        Special.simple("Ending", r"[_ ]((NC|TV)?ED|Ending|Closing)[ _]?(?P<ep>\d+)"), | 
				
			||||||
 | 
					        Special.simple("Special", r"[_ ](Special|OVA|SP)[ _]?(?P<ep>\d+)?[ _]?"), | 
				
			||||||
 | 
					        # .nfo files | 
				
			||||||
 | 
					        Special({"$custom": lambda m, i: ("link", os.path.join(i.destination, i.main_name + ".nfo"))}, re.compile(r".nfo$")) | 
				
			||||||
 | 
					    ] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    link_template = "{series} S{season:d}E{episode:03d}" | 
				
			||||||
 | 
					    until_template = "-E{episode:03d}" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def format_linkname(self, series, season, episode, *, until_ep=None): | 
				
			||||||
 | 
					        linkname = self.link_template.format(series=series, season=season, episode=episode) | 
				
			||||||
 | 
					        if until_ep: | 
				
			||||||
 | 
					            linkname += self.until_template.format(episode=until_ep) | 
				
			||||||
 | 
					        return linkname | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, destination): | 
				
			||||||
 | 
					        # Find master | 
				
			||||||
 | 
					        master_f = os.path.join(destination, self.master_fn) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while os.path.islink(master_f): | 
				
			||||||
 | 
					            new_dest = os.readlink(master_f) | 
				
			||||||
 | 
					            if not os.path.isabs(new_dest): | 
				
			||||||
 | 
					                new_dest = transport_path(new_dest, destination, os.curdir) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            logger.info("Destination '%s' belongs to '%s'; Selecting that instead" % (destination, new_dest)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            destination = new_dest | 
				
			||||||
 | 
					            master_f = os.path.join(destination, self.master_fn) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.destination = os.path.abspath(destination) | 
				
			||||||
 | 
					        self.main_name = os.path.basename(self.destination) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def get_source_loc(fn): | 
				
			||||||
 | 
					            source = os.readlink(fn) | 
				
			||||||
 | 
					            if not os.path.isabs(source): | 
				
			||||||
 | 
					                source = transport_path(source, os.path.dirname(fn), os.curdir) | 
				
			||||||
 | 
					            return source | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        source_f = os.path.join(destination, self.source_fn) | 
				
			||||||
 | 
					        if os.path.islink(source_f): | 
				
			||||||
 | 
					            self.sources = [get_source_loc(source_f)] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif os.path.isdir(source_f): | 
				
			||||||
 | 
					            self.sources = list( | 
				
			||||||
 | 
					                map(get_source_loc, | 
				
			||||||
 | 
					                    sorted( | 
				
			||||||
 | 
					                        filter(os.path.islink, | 
				
			||||||
 | 
					                           (os.path.join(source_f, f) | 
				
			||||||
 | 
					                                for f in os.listdir(source_f)))))) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            self.sources = [] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        options_f = os.path.join(destination, self.options_fn) | 
				
			||||||
 | 
					        if os.path.isfile(options_f): | 
				
			||||||
 | 
					            self.options = {k: maybe_number(v) for k, v in (x.split(": ") for x in cat_from(options_f).split("\n"))} | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            self.options = {} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pattern_f = os.path.join(destination, self.pattern_fn) | 
				
			||||||
 | 
					        if os.path.isfile(pattern_f): | 
				
			||||||
 | 
					            self.pattern = re.compile(cat_from(pattern_f).rstrip("\n")) | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            self.pattern = self.default_pattern | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        exclude_f = os.path.join(destination, self.exclude_fn) | 
				
			||||||
 | 
					        if os.path.isfile(exclude_f): | 
				
			||||||
 | 
					            self.exclude = re.compile(cat_from(exclude_f).rstrip("\n")) | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            self.exclude = None | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        special_f = os.path.join(destination, self.special_fn) | 
				
			||||||
 | 
					        if os.path.isfile(special_f): | 
				
			||||||
 | 
					            self.specials = list(Special.iterparse(cat_from(special_f))) | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            self.specials = [] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _save(self, filename, content): | 
				
			||||||
 | 
					        # save data to <destination>/<filename> | 
				
			||||||
 | 
					        path = os.path.join(self.destination, filename) | 
				
			||||||
 | 
					        if content is not None: | 
				
			||||||
 | 
					            with open(path, "w") as f: | 
				
			||||||
 | 
					                f.write(content) | 
				
			||||||
 | 
					        elif os.path.exists(path): | 
				
			||||||
 | 
					            os.unlink(path) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def save(self): | 
				
			||||||
 | 
					        # Write settings to disk | 
				
			||||||
 | 
					        if not os.path.isdir(self.destination): | 
				
			||||||
 | 
					            os.mkdir(self.destination) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._save(self.pattern_fn, self.pattern.pattern if self.pattern is not self.default_pattern else None) | 
				
			||||||
 | 
					        self._save(self.exclude_fn, self.exclude.pattern if self.exclude is not None else None) | 
				
			||||||
 | 
					        self._save(self.special_fn, "\n".join(map(Special.source, self.specials)) if self.specials else None) | 
				
			||||||
 | 
					        self._save(self.options_fn, "\n".join((": ".join((k, opt_value_str(v))) for k, v in self.options.items())) if self.options else None) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        source_f = os.path.join(self.destination, self.source_fn) | 
				
			||||||
 | 
					        if os.path.islink(source_f): | 
				
			||||||
 | 
					            oldpath = transport_path(os.readlink(source_f), self.destination, os.curdir) | 
				
			||||||
 | 
					            if oldpath != self.sources[0].rstrip("/"): | 
				
			||||||
 | 
					                logger.warn("Updating source link '%s' with '%s'" % (oldpath, self.sources[0])) | 
				
			||||||
 | 
					            os.unlink(source_f) | 
				
			||||||
 | 
					        if len(self.sources) > 1 or os.path.isdir(source_f): | 
				
			||||||
 | 
					            if os.path.isdir(source_f): | 
				
			||||||
 | 
					                for i, link in enumerate( | 
				
			||||||
 | 
					                        filter(os.path.islink, | 
				
			||||||
 | 
					                            sorted((os.path.join(source_f, f) for f in os.listdir(source_f)), key=natural_sort_key))): | 
				
			||||||
 | 
					                    oldpath = transport_path(os.readlink(link), source_f, os.curdir) | 
				
			||||||
 | 
					                    if i >= len(self.sources): | 
				
			||||||
 | 
					                        logger.warn("Removing source link '%s'" % oldpath) | 
				
			||||||
 | 
					                    elif oldpath != self.sources[i].rstrip("/"): | 
				
			||||||
 | 
					                        logger.warn("Updating source link '%s' with '%s'" % (oldpath, self.sources[i])) | 
				
			||||||
 | 
					                    os.unlink(link) | 
				
			||||||
 | 
					            else: | 
				
			||||||
 | 
					                os.mkdir(source_f) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for i, source in enumerate(self.sources): | 
				
			||||||
 | 
					                make_symlink_in(source, source_f, str(i)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            make_symlink(self.sources[0], source_f) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for sp in self.specials: | 
				
			||||||
 | 
					            if not sp.is_extern: | 
				
			||||||
 | 
					                continue | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            path = os.path.join(self.destination, "..", sp.name) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if not os.path.isdir(path): | 
				
			||||||
 | 
					                os.mkdir(path) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            master_f = os.path.join(path, self.master_fn) | 
				
			||||||
 | 
					            if os.path.islink(master_f): | 
				
			||||||
 | 
					                oldpath = transport_path(os.readlink(master_f), path, os.curdir) | 
				
			||||||
 | 
					                if oldpath != self.destination.rstrip("/"): | 
				
			||||||
 | 
					                    logger.warn("Updating master link '%s' with '%s'" % (oldpath, self.destination)) | 
				
			||||||
 | 
					                os.unlink(master_f) | 
				
			||||||
 | 
					            make_symlink(self.destination, master_f) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property | 
				
			||||||
 | 
					    def effective_specials(self): | 
				
			||||||
 | 
					        return itertools.chain(self.specials, self.auto_specials) if self.option("auto_specials") else self.specials | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def option(self, name): | 
				
			||||||
 | 
					        return self.options[name] if name in self.options else self.default_options.get(name, None) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def process_file(self, filename, subdir=None): | 
				
			||||||
 | 
					        # Exclude | 
				
			||||||
 | 
					        if self.option("exclude_parts") and filename.endswith(".part"): | 
				
			||||||
 | 
					            return "skip", "partial download" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif self.option("exclude_playlists") and (filename[-4:] in (".vml", ".m3u", ".pls")): | 
				
			||||||
 | 
					            return "skip", "playlist file" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif self.exclude and self.exclude.search(filename): | 
				
			||||||
 | 
					            return "skip", "excluded" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        linkpath = self.destination | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Specials | 
				
			||||||
 | 
					        for special in self.effective_specials: | 
				
			||||||
 | 
					            if bool(subdir) != special.is_subdir or (subdir and subdir != special.subdir): | 
				
			||||||
 | 
					                continue | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            sm = special.match(filename) | 
				
			||||||
 | 
					            if sm: | 
				
			||||||
 | 
					                result = special.custom(sm, self) | 
				
			||||||
 | 
					                if result: | 
				
			||||||
 | 
					                    return result | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                ep = special.get_episode(sm, self.last_special.get(special, 0)) | 
				
			||||||
 | 
					                self.last_special[special] = ep | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if special.is_extern: | 
				
			||||||
 | 
					                    name = special.name | 
				
			||||||
 | 
					                    linkpath = os.path.join(self.destination, "..", name) | 
				
			||||||
 | 
					                else: | 
				
			||||||
 | 
					                    name = self.main_name | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                linkname = self.link_template.format(series=name, season=special.season, episode=ep) | 
				
			||||||
 | 
					                break | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            if subdir: | 
				
			||||||
 | 
					                #logger.warn("Unhandled file in subdir %s: %s" % (subdir, filename)) | 
				
			||||||
 | 
					                return "skip", "in subdirectory" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Default values | 
				
			||||||
 | 
					            data = { | 
				
			||||||
 | 
					                "series": self.main_name, | 
				
			||||||
 | 
					                "season": 1, | 
				
			||||||
 | 
					                "episode": self.last_episode + 1 | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Try to extract info from filename | 
				
			||||||
 | 
					            m = self.pattern.search(filename) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if m: | 
				
			||||||
 | 
					                # Season number (optional) | 
				
			||||||
 | 
					                if "season" in self.pattern.groupindex and m.group("season"): | 
				
			||||||
 | 
					                    season = int(m.group("season")) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Episode number (mandatory) | 
				
			||||||
 | 
					                data["episode"] = int(m.group("ep")) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Until episode number for files containing multiple episodes (optional) | 
				
			||||||
 | 
					                if "untilep" in self.pattern.groupindex and m.group("untilep"): | 
				
			||||||
 | 
					                    data["until_ep"] = int(m.group("untilep")) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            linkname = self.format_linkname(**data) | 
				
			||||||
 | 
					            self.last_episode = (data["until_ep"] if "until_ep" in data else data["episode"]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return "link", os.path.join(linkpath, linkname + os.path.splitext(filename)[1]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean_all(self): | 
				
			||||||
 | 
					        clean_links(self.destination) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for special in self.specials: # auto_specials doesn't have "extern" specials | 
				
			||||||
 | 
					            if special.is_extern: | 
				
			||||||
 | 
					                clean_links(os.path.join(self.destination, "..", special.name)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self, *, dry=False): | 
				
			||||||
 | 
					        if not dry: | 
				
			||||||
 | 
					            self.clean_all() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.last_episode = 0 # FIXME: global state is bad | 
				
			||||||
 | 
					        self.last_special = {} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for source in self.sources: | 
				
			||||||
 | 
					            for f in sorted(os.listdir(source), key=natural_sort_key): | 
				
			||||||
 | 
					                path = os.path.join(source, f) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if os.path.isdir(path): | 
				
			||||||
 | 
					                    if self.specials: | 
				
			||||||
 | 
					                        for ff in sorted(os.listdir(path), key=natural_sort_key): | 
				
			||||||
 | 
					                            if self.handle_file(source, os.path.join(f, ff), *self.process_file(ff, subdir=f), dry=dry) != 0: | 
				
			||||||
 | 
					                                return 1 | 
				
			||||||
 | 
					                else: | 
				
			||||||
 | 
					                    if self.handle_file(source, f, *self.process_file(f), dry=dry) != 0: | 
				
			||||||
 | 
					                        return 1 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle_file(self, source, f, what, where, *, dry=False): | 
				
			||||||
 | 
					        if what == "link": | 
				
			||||||
 | 
					            if os.path.exists(where) and not dry: | 
				
			||||||
 | 
					                logger.error("LINK %s => %s exists!" % (f, os.path.basename(where))) | 
				
			||||||
 | 
					                return 1 | 
				
			||||||
 | 
					            else: | 
				
			||||||
 | 
					                logger.info("LINK %s => %s" % (f, os.path.basename(where))) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if not dry: | 
				
			||||||
 | 
					                make_symlink(os.path.join(source, f), where) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif what == "skip": | 
				
			||||||
 | 
					            logger.info("SKIP %s (%s)" % (f, where)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            assert(False and "Should not be reached") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def reset(self, *things): | 
				
			||||||
 | 
					        if "pattern" in things: | 
				
			||||||
 | 
					            self.pattern = self.default_pattern | 
				
			||||||
 | 
					        if "exclude" in things: | 
				
			||||||
 | 
					            self.exclude = None | 
				
			||||||
 | 
					        if "specials" in things: | 
				
			||||||
 | 
					            self.specials.clear() | 
				
			||||||
 | 
					        if "options" in things: | 
				
			||||||
 | 
					            self.options.clear() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def print_info(self): | 
				
			||||||
 | 
					        print("Series Info for %s:" % self.main_name) | 
				
			||||||
 | 
					        print("-" * (17 + len(self.main_name))) | 
				
			||||||
 | 
					        print("Sources    |  %s" % ("\n           |  ".join(self.sources))) | 
				
			||||||
 | 
					        print("Pattern    |  r'%s'%s" % (self.pattern.pattern, " (default)" if self.pattern is self.default_pattern else "")) | 
				
			||||||
 | 
					        print("Exclude    |  %s" % (("r'%s'" % self.exclude.pattern) if self.exclude else "None")) | 
				
			||||||
 | 
					        print("Options    |  %s" % self.options) | 
				
			||||||
 | 
					        print("Specials   |  %s" % ("None" | 
				
			||||||
 | 
					                                      if not self.specials else | 
				
			||||||
 | 
					                                      "\n           |  ".join("%-35s :: %s" % ("r'%s'" % special.pattern.pattern, special._properties) | 
				
			||||||
 | 
					                                                                for special in self.specials))) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def flags(self): | 
				
			||||||
 | 
					        return "".join((f for c, f in [ | 
				
			||||||
 | 
					            (self.pattern is not self.default_pattern, "p"), | 
				
			||||||
 | 
					            (self.exclude, "e"), | 
				
			||||||
 | 
					            #(self.option("exclude_parts"), "d"), | 
				
			||||||
 | 
					            (not self.option("exclude_parts"), "D"), | 
				
			||||||
 | 
					            (self.option("auto_specials"), "i"), | 
				
			||||||
 | 
					            (not self.option("auto_specials"), "I"), | 
				
			||||||
 | 
					            (self.specials, str(len(self.specials))), | 
				
			||||||
 | 
					        ] if c)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					## Argument handling                                                         ## | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					class HelpFormatter(argparse.RawTextHelpFormatter): | 
				
			||||||
 | 
					    def __init__(self, prog): | 
				
			||||||
 | 
					        super().__init__(prog, max_help_position=16) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_args(argv): | 
				
			||||||
 | 
					    parser = argparse.ArgumentParser(prog=argv[0], formatter_class=HelpFormatter) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.add_argument("-l", "--library", default=".", | 
				
			||||||
 | 
					                        help="The library folder to work on [%(default)s]") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    commands = parser.add_subparsers(title="Commands", dest="command") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # import | 
				
			||||||
 | 
					    importc = commands.add_parser("import", description="Add a new source location") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    importc.add_argument("source", | 
				
			||||||
 | 
					                         help="The new source directory") | 
				
			||||||
 | 
					    importc.add_argument("series", | 
				
			||||||
 | 
					                         help="The series (directory) name. All non-hidden symlinks inside will be deleted!") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # unlink | 
				
			||||||
 | 
					    unlink = commands.add_parser("unlink", description="Remove a source location") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    unlink.add_argument("series", | 
				
			||||||
 | 
					                        help="The series name") | 
				
			||||||
 | 
					    unlink.add_argument("source", | 
				
			||||||
 | 
					                        help="The source directory to remove") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # config | 
				
			||||||
 | 
					    config = commands.add_parser("config", description="Modify series configuration") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    config.add_argument("series", | 
				
			||||||
 | 
					                        help="The series name") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    patterns = config.add_argument_group("Patterns", | 
				
			||||||
 | 
					            description="Patterns are Python re patterns: https://docs.python.org/3/library/re.html") | 
				
			||||||
 | 
					    patterns.add_argument("-p", "--pattern", default=None, metavar="PATTERN", | 
				
			||||||
 | 
					            help="Set the episode pattern. Include a named group <ep>; Optionally <season> and <untilep>") | 
				
			||||||
 | 
					    patterns.add_argument("-x", "--exclude", default=None, metavar="PATTERN", | 
				
			||||||
 | 
					            help="Set the exclusion pattern") | 
				
			||||||
 | 
					    patterns.add_argument("-s", "--special", "--specials", default=[], nargs=2, action="append", metavar=("SPECIAL", "PATTERN"), dest="specials", | 
				
			||||||
 | 
					            help=("Set the special mapping. This takes 2 arguments and can be specified multiple times.\n" | 
				
			||||||
 | 
					                  "1st argument: key=value properties separated by '|'.\n" | 
				
			||||||
 | 
					                  "2nd argument: the matching pattern. It should contain a <ep> group\n" | 
				
			||||||
 | 
					                  "Valid keys so far are 'type', 'offset', 'name'.\n" | 
				
			||||||
 | 
					                  "type can be 'special', 'opening', 'ending', 'trailer', 'parody', 'other' and 'extern'.\n" | 
				
			||||||
 | 
					                  "offset adds a fixed number to all episodes numbers matched by the pattern.\n" | 
				
			||||||
 | 
					                  "name must only be used with 'extern'. It creates a slave Series to hold the matched episodes in the parent directory.\n" | 
				
			||||||
 | 
					                  "It's useful for singling out specials that are recorded as independent series in the metadata provider.")) | 
				
			||||||
 | 
					    patterns.add_argument("-a", "--append", action="store_true", | 
				
			||||||
 | 
					            help="Extend the special mapping instead of replacing it") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    options = config.add_argument_group("Options") | 
				
			||||||
 | 
					    options.add_argument("-i", "--auto-specials", action="store_true", dest="auto_specials", default=None, | 
				
			||||||
 | 
					            help="Implicitly add some Specials patterns (default)") | 
				
			||||||
 | 
					    options.add_argument("-I", "--no-auto-specials", action="store_false", dest="auto_specials", default=None, | 
				
			||||||
 | 
					            help="Don't add the implicit Specials patterns") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    config.add_argument("--clear", default=[], action="append", choices={"pattern", "exclude", "specials", "options"}, | 
				
			||||||
 | 
					            help="Reset a property to the defaults") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check | 
				
			||||||
 | 
					    check = commands.add_parser("check", description="Check library and report untracked sources", fromfile_prefix_chars="@") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    check.add_argument("source_roots", nargs="*", metavar="SOURCE_ROOT", | 
				
			||||||
 | 
					                       help="Directory path containing source directories") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    check.add_argument("-X", "--ignore", action="append", default=[], metavar="DIR", | 
				
			||||||
 | 
					                       help="Ignore a directory DIR when checking for untracked sources") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    check.add_argument("-I", "--interactive-import", action="store_true", | 
				
			||||||
 | 
					                       help="Interactively import any untracked sources") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Update | 
				
			||||||
 | 
					    update = commands.add_parser("update", description="Update the symlinks") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    update.add_argument("series", nargs="*", default=[], | 
				
			||||||
 | 
					                        help="Update only these series (Default: all)") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Info | 
				
			||||||
 | 
					    info = commands.add_parser("info", description="Show information about a series") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    info.add_argument("series", nargs="+", | 
				
			||||||
 | 
					                      help="Update only these series") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Global misc | 
				
			||||||
 | 
					    parser.add_argument("-U", "--no-update", action="store_true", | 
				
			||||||
 | 
					                        help="Don't automatically update symlinks. Use '%(prog)s update' later (applicable to import, unlink, config)") | 
				
			||||||
 | 
					    parser.add_argument("-D", "--dry-run", action="store_true", | 
				
			||||||
 | 
					            help="Don't save anything to disk Useful combination") | 
				
			||||||
 | 
					    parser.add_argument("-q", "--quiet", action="store_true") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return parser.parse_args(argv[1:]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					## Put the pieces together                                                   ## | 
				
			||||||
 | 
					############################################################################### | 
				
			||||||
 | 
					# Helpers | 
				
			||||||
 | 
					def run_update(i, args): | 
				
			||||||
 | 
					    """ Update the symlinks for a Series Importer """ | 
				
			||||||
 | 
					    return i.run(dry=args.dry_run) == 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_series_importer(args, series=None): | 
				
			||||||
 | 
					    if series is None: | 
				
			||||||
 | 
					        series = args.series | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if series is ".": | 
				
			||||||
 | 
					        if args.library == "..": | 
				
			||||||
 | 
					            series = os.path.dirname(os.getcwd()) | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            raise ValueError("Using '.' as series is only valid when using '..' for library") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if args.library != ".": | 
				
			||||||
 | 
					        return Importer(os.path.join(args.library, series)) | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					        return Importer(series) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def list_series_paths(library): | 
				
			||||||
 | 
					    return [de.path | 
				
			||||||
 | 
					            for de in os.scandir(library) | 
				
			||||||
 | 
					            if de.is_dir(follow_symlinks=False)] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_series_importers(args, series=None): | 
				
			||||||
 | 
					    if series: | 
				
			||||||
 | 
					        return map(functools.partial(get_series_importer, args), series) | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					        return map(Importer, list_series_paths(args.library)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check(i): | 
				
			||||||
 | 
					    if not i.sources: | 
				
			||||||
 | 
					        logger.warn("'%s' doesn't have any sources" % i.main_name) | 
				
			||||||
 | 
					        return False | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    have_sources = list(map(os.path.isdir, i.sources)) | 
				
			||||||
 | 
					    if not all(have_sources): | 
				
			||||||
 | 
					        for source, isdir in zip(i.sources, have_sources): | 
				
			||||||
 | 
					            if not isdir: | 
				
			||||||
 | 
					                logger.error("Source link for '%s' doesn't exist: '%s'" % (i.main_name, source)) | 
				
			||||||
 | 
					        return False | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return True | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def do_interactive_import(args, sources): | 
				
			||||||
 | 
					    for source in sources: | 
				
			||||||
 | 
					        source = os.path.relpath(source, args.library) | 
				
			||||||
 | 
					        print("Importing from %s:" % source) | 
				
			||||||
 | 
					        series = input("  Enter Series Name or hit return to skip --> ") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not series: | 
				
			||||||
 | 
					            continue | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        i = get_series_importer(args, series) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if i.sources: | 
				
			||||||
 | 
					            print("Adding source '%s' to existing '%s'" % (source, series)) | 
				
			||||||
 | 
					        i.sources.append(source) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not args.dry_run: | 
				
			||||||
 | 
					            i.save() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not args.no_update: | 
				
			||||||
 | 
					            run_update(i, args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Command Mains | 
				
			||||||
 | 
					def import_main(args): | 
				
			||||||
 | 
					    i = get_series_importer(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if args.source not in i.sources: | 
				
			||||||
 | 
					        if i.sources: | 
				
			||||||
 | 
					            logger.info("Adding source '%s' to '%s'" % (args.source, args.series)) | 
				
			||||||
 | 
					        i.sources.append(args.source) | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					        logger.warn("Source '%s' already linked to '%s'" % (args.source, args.series)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not args.dry_run: | 
				
			||||||
 | 
					        i.save() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not args.no_update: | 
				
			||||||
 | 
					        run_update(i, args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def unlink_main(args): | 
				
			||||||
 | 
					    i = get_series_importer(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    source = transport_path(args.source, os.getcwd(), args.library) | 
				
			||||||
 | 
					    print (i.sources) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if source not in i.sources: | 
				
			||||||
 | 
					        logger.error("Source '%s' not linked to '%s'" % (args.source, args.series)) | 
				
			||||||
 | 
					        return 1 | 
				
			||||||
 | 
					    else: | 
				
			||||||
 | 
					        i.sources.remove(source) | 
				
			||||||
 | 
					        logger.warn("Unlinking Source '%s' from '%s'" % (args.source, args.series)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not args.dry_run: | 
				
			||||||
 | 
					        i.save() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not args.no_update: | 
				
			||||||
 | 
					        run_update(i, args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def config_main(args): | 
				
			||||||
 | 
					    i = get_series_importer(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    i.reset(*args.clear) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if args.pattern: | 
				
			||||||
 | 
					        i.pattern = re.compile(args.pattern) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if args.exclude: | 
				
			||||||
 | 
					        i.exclude = re.compile(args.exclude) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if args.specials: | 
				
			||||||
 | 
					        if args.append: | 
				
			||||||
 | 
					            i.specials.extend(itertools.starmap(Special.parse, args.specials)) | 
				
			||||||
 | 
					        else: | 
				
			||||||
 | 
					            i.specials = list(itertools.starmap(Special.parse, args.specials)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for opt in ("auto_specials",): | 
				
			||||||
 | 
					        if getattr(args, opt) is not None: | 
				
			||||||
 | 
					            i.options[opt] = getattr(args, opt) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not args.dry_run: | 
				
			||||||
 | 
					        i.save() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not args.no_update: | 
				
			||||||
 | 
					        run_update(i, args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check_main(args): | 
				
			||||||
 | 
					    got_dirs = set() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for i in get_series_importers(args): | 
				
			||||||
 | 
					        if check(i): | 
				
			||||||
 | 
					            got_dirs.update(map(os.path.abspath, i.sources)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if args.source_roots: | 
				
			||||||
 | 
					        dirs = set(map(os.path.abspath, filter(os.path.isdir, itertools.chain.from_iterable(((os.path.join(f, x) for x in os.listdir(f)) for f in args.source_roots))))) | 
				
			||||||
 | 
					        ignore = set(map(os.path.abspath, filter(os.path.isdir, itertools.chain.from_iterable(((os.path.join(f, x) for x in args.ignore) for f in args.source_roots))))) | 
				
			||||||
 | 
					        print(ignore) | 
				
			||||||
 | 
					        missing = dirs - got_dirs - ignore | 
				
			||||||
 | 
					        if missing: | 
				
			||||||
 | 
					            if args.interactive_import: | 
				
			||||||
 | 
					                return do_interactive_import(args, missing) | 
				
			||||||
 | 
					            else: | 
				
			||||||
 | 
					                print("Found missing directories:\n  %s" % "\n  ".join(os.path.relpath(m) for m in missing)) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def update_main(args): | 
				
			||||||
 | 
					    fin_dirs = set() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for i in get_series_importers(args, args.series): | 
				
			||||||
 | 
					        logger.info("Processing '%s' (%s)" % (i.main_name, i.flags())) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if i.destination in fin_dirs: | 
				
			||||||
 | 
					            logger.info("Already processed '%s'. Skipping" % i.main_name) | 
				
			||||||
 | 
					            continue | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not check(i): | 
				
			||||||
 | 
					            continue | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if run_update(i, args): | 
				
			||||||
 | 
					            got_dirs.update(map(os.path.abspath, i.sources)) | 
				
			||||||
 | 
					            fin_dirs.add(i.destination) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def info_main(args): | 
				
			||||||
 | 
					    logger.setLevel(logging.WARNING) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for i in get_series_importers(args, args.series): | 
				
			||||||
 | 
					        i.print_info() | 
				
			||||||
 | 
					        print() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return 0 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Program Main | 
				
			||||||
 | 
					def main(argv): | 
				
			||||||
 | 
					    logging.basicConfig(level=logging.INFO, format="%(levelname)-5s %(message)s") | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    args = parse_args(argv) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if args.quiet: | 
				
			||||||
 | 
					        logger.setLevel(logging.WARNING) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try: | 
				
			||||||
 | 
					        if args.command == "info": | 
				
			||||||
 | 
					            return info_main(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if args.command == "import": | 
				
			||||||
 | 
					            return import_main(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if args.command == "unlink": | 
				
			||||||
 | 
					            return unlink_main(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if args.command == "config": | 
				
			||||||
 | 
					            return config_main(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if args.command == "update": | 
				
			||||||
 | 
					            return update_main(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if args.command == "check": | 
				
			||||||
 | 
					            return check_main(args) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    except: | 
				
			||||||
 | 
					        logger.exception("An Exception occured") | 
				
			||||||
 | 
					        return 255 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__": | 
				
			||||||
 | 
					    sys.exit(main(sys.argv)) | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue