Exemple #1
0
class ParseTerm(Command):
    """
    Parse a single term

    By default, the following operators are parsed for:
    - '' = ''
    - '<' = 'less'
    - '>' = 'great'

    Returns a namedtuple of strings: Term(namespace, tag, operator)
    """

    parse = CommandEntry("parse", tuple, str)
    parsed = CommandEvent("parsed", Term)

    def __init__(self):
        super().__init__()
        self.filter = ''
        self.term = None

    @parse.default()
    def _parse_term(term):
        s = term.split(':', 1)
        ns = s[0] if len(s) == 2 else ''
        tag = s[1] if len(s) == 2 else term
        operator = ''

        if tag.startswith('<'):
            operator = 'less'
        elif tag.startswith('>'):
            operator = 'great'

        return (ns, tag, operator)

    def main(self, term: str) -> Term:

        self.filter = term

        with self.parse.call(self.filter) as plg:
            t = plg.first()
            if not len(t) == 3:
                t = plg.default()

            self.term = Term(*t)

        self.parsed.emit(self.term)

        return self.term
Exemple #2
0
class RenameGallery(UndoCommand):
    """
    Rename a gallery
    """

    renamed = CommandEvent("renamed", str)
    rename = CommandEntry("rename", None, str, str)

    def __init__(self):
        super().__init__()
        self.title = None
        self.old_title = None

    @rename.default()
    def _set_title(old_title, new_title):
        return new_title

    def main(self, title: db.Title, new_title: str) -> None:

        self.title = title
        self.old_title = title.name

        with self.rename.call(title.name, new_title) as plg:
            title.name = plg.first()

            with utils.session() as s:
                s.add(title)

        self.renamed.emit(title.name)

    def undo(self):
        self.title.name = self.old_title

        with utils.session() as s:
            s.add(self.title)

        self.renamed.emit(self.old_title)
Exemple #3
0
class GetDatabaseSort(Command):
    """
    Returns a name or, a namedtuple(expr, joins) of a data sort expression and additional
    tables to join, for a given sort index

    Args:
        model: a database model
        sort_index: a sort index
        name: set true to only return a dict with sort names

    Returns:
        a :data:`.SortTuple` if ``sort_index`` was given, else a dict with (``int``)``sort index``: :data:`SortTuple`
        or a dict with (``int``)``sort index``:``sort name``(``str``) if ``name`` was set to true
    """

    names: dict = CommandEntry("names",
                               CParam("model_name", str,
                                      "name of a database model"),
                               __capture=(str, "name of database model"),
                               __doc=""",
                               Called to get a dict of sort names
                               """,
                               __doc_return="""
                               A dict of (``int``)``sortindex``:``name of sort``(``str``)
                               """)

    orderby: dict = CommandEntry("orderby",
                                 CParam("model_name", str,
                                        "name of a database model"),
                                 __capture=(str, "name of database model"),
                                 __doc=""",
                               Called to get a dict of database item attributes to order by
                               """,
                                 __doc_return="""
                               A dict of (``int``)``sortindex``:``(item.attribute, ...)``(``tuple``)
                               """)

    groupby: dict = CommandEntry("groupby",
                                 CParam("model_name", str,
                                        "name of a database model"),
                                 __capture=(str, "name of database model"),
                                 __doc=""",
                               Called to get a dict of database item attributes to group by
                               """,
                                 __doc_return="""
                               A dict of (``int``)``sortindex``:``(item.attribute, ...)``(``tuple``)
                               """)

    joins: dict = CommandEntry("joins",
                               CParam("model_name", str,
                                      "name of a database model"),
                               __capture=(str, "name of database model"),
                               __doc=""",
                               Called to get a dict of database items or item attributes to join with
                               """,
                               __doc_return="""
                               A dict of (``int``)``sortindex``:``(item.attribute, item, ...)``(``tuple``)
                               """)

    SortTuple = namedtuple("SortTuple", ["orderby", "joins", "groupby"])

    def main(self,
             model: db.Base,
             sort_index: int = None,
             name: bool = False) -> typing.Union[dict, SortTuple]:
        self.model = model
        model_name = db.model_name(self.model)
        items = {}
        if name:
            with self.names.call_capture(model_name, model_name) as plg:
                for x in plg.all(default=True):
                    items.update(x)
        else:
            orders = {}
            with self.orderby.call_capture(model_name, model_name) as plg:
                for x in plg.all(default=True):
                    orders.update(x)

            groups = {}
            with self.groupby.call_capture(model_name, model_name) as plg:
                for x in plg.all(default=True):
                    groups.update(x)

            joins = {}
            with self.joins.call_capture(model_name, model_name) as plg:
                for x in plg.all(default=True):
                    joins.update(x)

            for k, v in orders.items():
                a = v
                b = tuple()
                c = None
                if k in joins:
                    t = joins[k]
                    if isinstance(t, tuple):
                        b = t
                if k in groups:
                    c = groups[k]
                items[k] = self.SortTuple(a, b, c)

        return items.get(sort_index) if sort_index else items

    def __init__(self):
        super().__init__()
        self.model = None

    ##############################
    # Gallery

    @names.default(capture=True)
    def _gallery_names(model_name, capture=db.model_name(db.Gallery)):
        return {
            ItemSort.GalleryRandom.value: "Random",
            ItemSort.GalleryTitle.value: "Title",
            ItemSort.GalleryArtist.value: "Artist",
            ItemSort.GalleryDate.value: "Date Added",
            ItemSort.GalleryPublished.value: "Date Published",
            ItemSort.GalleryRead.value: "Last Read",
            ItemSort.GalleryUpdated.value: "Last Updated",
            ItemSort.GalleryRating.value: "Rating",
            ItemSort.GalleryReadCount.value: "Read Count",
            ItemSort.GalleryPageCount.value: "Page Count",
        }

    @orderby.default(capture=True)
    def _gallery_orderby(model_name, capture=db.model_name(db.Gallery)):
        return {
            ItemSort.GalleryRandom.value: (func.random(), ),
            ItemSort.GalleryTitle.value: (db.Title.name, ),
            ItemSort.GalleryArtist.value: (db.ArtistName.name, ),
            ItemSort.GalleryDate.value: (db.Gallery.timestamp, ),
            ItemSort.GalleryPublished.value: (db.Gallery.pub_date, ),
            ItemSort.GalleryRead.value: (db.Gallery.last_read, ),
            ItemSort.GalleryUpdated.value: (db.Gallery.last_updated, ),
            ItemSort.GalleryRating.value: (db.Gallery.rating, ),
            ItemSort.GalleryReadCount.value: (db.Gallery.times_read, ),
            ItemSort.GalleryPageCount.value: (db.func.count(db.Page.id), ),
        }

    @groupby.default(capture=True)
    def _gallery_groupby(model_name, capture=db.model_name(db.Gallery)):
        return {
            ItemSort.GalleryPageCount.value: (db.Gallery.id, ),
        }

    @joins.default(capture=True)
    def _gallery_joins(model_name, capture=db.model_name(db.Gallery)):
        return {
            ItemSort.GalleryTitle.value: (db.Gallery.titles, ),
            ItemSort.GalleryArtist.value: (
                db.Gallery.artists,
                db.Artist.names,
            ),
            ItemSort.GalleryPageCount.value: (db.Gallery.pages, ),
        }

    ##############################
    # Collection

    @names.default(capture=True)
    def _collection_names(model_name, capture=db.model_name(db.Collection)):
        return {
            ItemSort.CollectionRandom.value: "Random",
            ItemSort.CollectionName.value: "Name",
            ItemSort.CollectionDate.value: "Date Added",
            ItemSort.CollectionPublished.value: "Date Published",
            ItemSort.CollectionGalleryCount.value: "Gallery Count",
        }

    @orderby.default(capture=True)
    def _collection_orderby(model_name, capture=db.model_name(db.Collection)):
        return {
            ItemSort.CollectionRandom.value: (func.random(), ),
            ItemSort.CollectionName.value: (db.Collection.name, ),
            ItemSort.CollectionDate.value: (db.Collection.timestamp, ),
            ItemSort.CollectionPublished.value: (db.Collection.pub_date, ),
            ItemSort.CollectionGalleryCount.value:
            (db.func.count(db.Gallery.id), ),
        }

    @groupby.default(capture=True)
    def _collection_groupby(model_name, capture=db.model_name(db.Collection)):
        return {
            ItemSort.CollectionGalleryCount.value: (db.Collection.id, ),
        }

    @joins.default(capture=True)
    def _collection_joins(model_name, capture=db.model_name(db.Collection)):
        return {
            ItemSort.CollectionGalleryCount.value: (db.Collection.galleries, ),
        }

    ##############################
    # AliasName

    @names.default(capture=True)
    def _aliasname_names(
        model_name,
        capture=tuple(db.model_name(x) for x in (db.Artist, db.Parody))):
        return {
            ItemSort.ArtistName.value: "Name",
            ItemSort.ParodyName.value: "Name",
        }

    @orderby.default(capture=True)
    def _aliasname_orderby(
        model_name,
        capture=tuple(db.model_name(x) for x in (db.Artist, db.Parody))):
        return {
            ItemSort.ArtistName.value: (db.ArtistName.name, ),
            ItemSort.ParodyName.value: (db.ParodyName.name, ),
        }

    @joins.default(capture=True)
    def _aliasname_joins(
        model_name,
        capture=tuple(db.model_name(x) for x in (db.Artist, db.Parody))):
        return {
            ItemSort.ArtistName.value: (db.Artist.names, ),
            ItemSort.ParodyName.value: (db.Parody.names, ),
        }

    ##############################
    # Circle

    @names.default(capture=True)
    def _circle_names(model_name, capture=db.model_name(db.Circle)):
        return {
            ItemSort.CircleName.value: "Name",
        }

    @orderby.default(capture=True)
    def _circle_orderby(model_name, capture=db.model_name(db.Circle)):
        return {
            ItemSort.CircleName.value: (db.Circle.name, ),
        }

    @joins.default(capture=True)
    def _circle_joins(model_name, capture=db.model_name(db.Circle)):
        return {}

    ##############################
    # NamespaceTags

    @names.default(capture=True)
    def _namespacetags_names(model_name,
                             capture=db.model_name(db.NamespaceTags)):
        return {
            ItemSort.NamespaceTagNamespace.value: "Namespace",
            ItemSort.NamespaceTagTag.value: "Tag",
        }

    @orderby.default(capture=True)
    def _namespacetags_orderby(model_name,
                               capture=db.model_name(db.NamespaceTags)):
        return {
            ItemSort.NamespaceTagNamespace.value:
            (db.Namespace.name, db.Tag.name),
            ItemSort.NamespaceTagTag.value: (db.Tag.name, db.Namespace.name),
        }

    @joins.default(capture=True)
    def _namespacetags_joins(model_name,
                             capture=db.model_name(db.NamespaceTags)):
        return {
            ItemSort.NamespaceTagNamespace.value:
            (db.NamespaceTags.namespace, db.NamespaceTags.tag),
            ItemSort.NamespaceTagTag.value:
            (db.NamespaceTags.tag, db.NamespaceTags.namespace),
        }
Exemple #4
0
class GetModelImage(AsyncCommand):
    """
    Fetch a database model item's image

    By default, the following models are supported

    - Gallery
    - Page
    - Grouping
    - Collection
    - GalleryFilter

    Returns a Profile database item

    Args:
        model: a database model
        item_id: id of database item
        image_size: size of image

    Returns:
        a database :class:`.db.Profile` object

    """

    models: tuple = CommandEntry("models",
                                 __doc="""
                                 Called to fetch the supported database models
                                 """,
                                 __doc_return="""
                                 a tuple of database models :class:`.db.Base`
                                 """)
    generate: str = CommandEntry("generate",
                                 CParam("model_name", str,
                                        "name of a database model"),
                                 CParam("item_id", int, "id of database item"),
                                 CParam("image_size", utils.ImageSize,
                                        "size of image"),
                                 __capture=(str, "name of database model"),
                                 __doc="""
                               Called to generate an image file of database item
                               """,
                                 __doc_return="""
                               path to image file
                               """)
    invalidate: bool = CommandEntry("invalidate",
                                    CParam("model_name", str,
                                           "name of a database model"),
                                    CParam("item_id", int,
                                           "id of database item"),
                                    CParam("image_size", utils.ImageSize,
                                           "size of image"),
                                    __capture=(str, "name of database model"),
                                    __doc="""
                                Called to check if a new image should be forcefully generated
                                """,
                                    __doc_return="""
                                bool indicating wether an image should be generated or not
                                """)

    cover_event = CommandEvent('cover',
                               CParam(
                                   "profile_item", object,
                                   "database item with the generated image"),
                               __doc="""
                               Emitted at the end of the process with :class:`.db.Profile` database item or ``None``
                               """)

    def main(self, model: db.Base, item_id: int,
             image_size: enums.ImageSize) -> db.Profile:
        self.model = model

        if image_size == enums.ImageSize.Original:
            image_size = utils.ImageSize(0, 0)
        else:
            image_size = utils.ImageSize(
                *constants.image_sizes[image_size.name.lower()])

        with self.models.call() as plg:
            for p in plg.all(default=True):
                self._supported_models.update(p)

        if self.model not in self._supported_models:
            raise exceptions.CommandError(
                utils.this_command(self),
                "Model '{}' is not supported".format(model))

        img_hash = io_cmd.ImageItem.gen_hash(model, image_size, item_id)

        generate = True
        sess = constants.db_session()

        profile_size = str(tuple(image_size))

        self.cover = sess.query(db.Profile).filter(
            db.and_op(db.Profile.data == img_hash,
                      db.Profile.size == profile_size)).first()

        old_img_hash = None
        if self.cover:
            if io_cmd.CoreFS(self.cover.path).exists:
                generate = False
            else:
                old_img_hash = self.cover.data

        self.next_progress()
        if not generate:
            model_name = db.model_name(model)
            with self.invalidate.call_capture(model_name, model_name, item_id,
                                              image_size) as plg:
                if plg.first_or_default():
                    generate = True

        self.next_progress()
        if generate:
            constants.task_command.thumbnail_cleaner.wake_up()
            self.cover = self.run_native(self._generate_and_add, img_hash,
                                         old_img_hash, generate, model,
                                         item_id, image_size,
                                         profile_size).get()
        self.cover_event.emit(self.cover)
        return self.cover

    def __init__(self, service=None):
        super().__init__(service, priority=constants.Priority.Low)
        self.model = None
        self.cover = None
        self._supported_models = set()

    @models.default()
    def _models():
        return (db.Grouping, db.Collection, db.Gallery, db.Page,
                db.GalleryFilter)

    @generate.default(capture=True)
    def _generate_gallery_and_page(
            model,
            item_id,
            size,
            capture=[db.model_name(x) for x in (db.Page, db.Gallery)]):
        im_path = ""
        model = GetModelClass().run(model)

        if model == db.Gallery:
            page = GetSession().run().query(db.Page.path).filter(
                db.and_op(db.Page.gallery_id == item_id,
                          db.Page.number == 1)).one_or_none()
        else:
            page = GetSession().run().query(
                db.Page.path).filter(db.Page.id == item_id).one_or_none()

        if page:
            im_path = page[0]

        if im_path:
            im_props = io_cmd.ImageProperties(size, 0, constants.dir_thumbs)
            im_path = io_cmd.ImageItem(im_path, im_props).main()
        return im_path

    @invalidate.default(capture=True)
    def _invalidate_gallery_and_page(
            model,
            item_id,
            size,
            capture=[db.model_name(x) for x in (db.Page, db.Gallery)]):
        return False

    @generate.default(capture=True)
    def _generate_collection(model,
                             item_id,
                             size,
                             capture=db.model_name(db.Collection)):
        im_path = ""
        model = GetModelClass().run(model)

        page = GetSession().run().query(db.Page.path).join(
            db.Collection.galleries).join(db.Gallery.pages).filter(
                db.and_op(db.Collection.id == item_id,
                          db.Page.number == 1)).first()

        # gallery sorted by insertion:
        # page = GetSession().run().query(
        #    db.Page.path, db.gallery_collections.c.timestamp.label("timestamp")).join(db.Collection.galleries).join(db.Gallery.pages).filter(
        #    db.and_op(
        #        db.Collection.id == item_id,
        #        db.Page.number == 1)).sort_by("timestamp").first()
        if page:
            im_path = page[0]

        if im_path:
            im_props = io_cmd.ImageProperties(size, 0, constants.dir_thumbs)
            im_path = io_cmd.ImageItem(im_path, im_props).main()
        return im_path

    @invalidate.default(capture=True)
    def _invalidate_collection(model,
                               item_id,
                               size,
                               capture=db.model_name(db.Collection)):
        return False

    @async_utils.defer
    def _update_db(self, stale_cover, item_id, model, old_hash):
        log.d("Updating profile for database item", model)
        s = constants.db_session()
        cover = s.query(db.Profile).filter(
            db.and_op(db.Profile.data == old_hash,
                      db.Profile.size == stale_cover.size)).all()

        if len(cover) > 1:
            cover, *cover_ex = cover
            for x in cover_ex:
                s.delete(x)
        elif cover:
            cover = cover[0]

        new = False

        if cover:
            # sometimes an identical img has already been generated and exists so we shouldnt do anything
            fs = io_cmd.CoreFS(cover.path)
            if (cover.path != stale_cover.path) and fs.exists:
                fs.delete()
        else:
            cover = db.Profile()
            new = True

        cover.data = stale_cover.data
        cover.path = stale_cover.path
        cover.size = stale_cover.size

        if new or not s.query(db.Profile).join(
                db.relationship_column(model, db.Profile)).filter(
                    db.and_op(db.Profile.id == cover.id, model.id
                              == item_id)).scalar():
            log.d("Adding new profile to database item", model,
                  "()".format(item_id))
            i = s.query(model).get(item_id)
            i.profiles.append(cover)

        s.commit()
        self.next_progress()

    def _generate_and_add(self, img_hash, old_img_hash, generate, model,
                          item_id, image_size, profile_size):

        model_name = db.model_name(model)

        cover = db.Profile()
        if generate:
            log.d("Generating new profile", image_size, "for database item",
                  model)
            with self.generate.call_capture(model_name, model_name, item_id,
                                            image_size) as plg:
                p = plg.first_or_default()
                if not p:
                    p = ""
                cover.path = p

            cover.data = img_hash
            cover.size = profile_size
        self.next_progress()
        if cover.path and generate:
            log.d("Updating database")
            self._update_db(cover, item_id, model, old_img_hash)
        elif not cover.path:
            cover = None
        return cover
Exemple #5
0
class ParseSearch(Command):
    """
    Parse a search query

    Dividies term into ns:tag pieces, returns a tuple of ns:tag pieces
    """

    parse = CommandEntry("parse", tuple, str)
    parsed = CommandEvent("parsed", tuple)

    def __init__(self):
        super().__init__()
        self.filter = ''
        self.pieces = tuple()

    @parse.default()
    def _get_terms(term):

        # some variables we will use
        pieces = []
        piece = ''
        qoute_level = 0
        bracket_level = 0
        brackets_tags = {}
        current_bracket_ns = ''
        end_of_bracket = False
        blacklist = ['[', ']', '"', ',']

        for n, x in enumerate(term):
            # if we meet brackets
            if x == '[':
                bracket_level += 1
                brackets_tags[piece] = set()  # we want unique tags!
                current_bracket_ns = piece
            elif x == ']':
                bracket_level -= 1
                end_of_bracket = True

            # if we meet a double qoute
            if x == '"':
                if qoute_level > 0:
                    qoute_level -= 1
                else:
                    qoute_level += 1

            # if we meet a whitespace, comma or end of term and are not in a
            # double qoute
            if (x == ' ' or x == ','
                    or n == len(term) - 1) and qoute_level == 0:
                # if end of term and x is allowed
                if (n == len(term) - 1) and x not in blacklist and x != ' ':
                    piece += x
                if piece:
                    if bracket_level > 0 or end_of_bracket:  # if we are inside a bracket we put piece in the set
                        end_of_bracket = False
                        if piece.startswith(current_bracket_ns):
                            piece = piece[len(current_bracket_ns):]
                        if piece:
                            try:
                                brackets_tags[current_bracket_ns].add(piece)
                            except KeyError:  # keyerror when there is a closing bracket without a starting bracket
                                pass
                    else:
                        pieces.append(piece)  # else put it in the normal list
                piece = ''
                continue

            # else append to the buffers
            if x not in blacklist:
                if qoute_level > 0:  # we want to include everything if in double qoute
                    piece += x
                elif x != ' ':
                    piece += x

        # now for the bracket tags
        for ns in brackets_tags:
            for tag in brackets_tags[ns]:
                ns_tag = ns
                # if they want to exlucde this tag
                if tag[0] == '-':
                    if ns_tag[0] != '-':
                        ns_tag = '-' + ns
                    tag = tag[1:]  # remove the '-'

                # put them together
                ns_tag += tag

                # done
                pieces.append(ns_tag)

        return tuple(pieces)

    def main(self, search_filter: str) -> tuple:

        self.filter = search_filter

        pieces = set()

        with self.parse.call(search_filter) as plg:
            for p in plg.all(default=True):
                for x in p:
                    pieces.add(x)
        self.pieces = tuple(pieces)

        self.parsed.emit(self.pieces)

        return self.pieces
Exemple #6
0
class ModelFilter(Command):
    """
    Perform a full search on database model
    Returns a set of ids of matched model items
    """

    separate = CommandEntry("separate", tuple, tuple)
    include = CommandEntry("include", set, str, set)
    exclude = CommandEntry("exclude", set, str, set)
    empty = CommandEntry("empty", set, str)

    included = CommandEvent("included", str, set)
    excluded = CommandEvent("excluded", str, set)
    matched = CommandEvent("matched", str, set)

    def __init__(self):
        super().__init__()
        self._model = None
        self.parsesearchfilter = None
        self.included_ids = set()
        self.excluded_ids = set()
        self.matched_ids = set()

    @separate.default()
    def _separate(pecies):

        include = []
        exclude = []

        for p in pecies:
            if p.startswith('-'):
                exclude.append(p[1:])  # remove '-' at the start
            else:
                include.append(p)

        return tuple(include), tuple(exclude)

    @staticmethod
    def _match(model_name, pieces):
        ""
        model = database_cmd.GetModelClass().run(model_name)
        partialfilter = PartialModelFilter()
        matched = set()

        for p in pieces:
            m = partialfilter.run(model, p)
            matched.update(m)

        return matched

    @include.default()
    def _include(model_name, pieces):
        return ModelFilter._match(model_name, pieces)

    @exclude.default()
    def _exclude(model_name, pieces):
        return ModelFilter._match(model_name, pieces)

    @empty.default()
    def _empty(model_name):
        model = database_cmd.GetModelClass().run(model_name)
        s = constants.db_session()
        return set(x[0] for x in s.query(model.id).all())

    def main(self, model: db.Base, search_filter: str) -> set:
        assert issubclass(model, db.Base)

        self._model = model
        model_name = db.model_name(self._model)

        if search_filter:

            self.parsesearchfilter = ParseSearch()

            pieces = self.parsesearchfilter.run(search_filter)

            options = get_search_options()

            include = set()
            exclude = set()

            with self.separate.call(pieces) as plg:

                for p in plg.all():
                    if len(p) == 2:
                        include.update(p[0])
                        exclude.update(p[1])

            with self.include.call(model_name, include) as plg:

                for i in plg.all():
                    if options.get("all"):
                        if self.included_ids:
                            self.included_ids.intersection_update(i)
                        else:
                            self.included_ids.update(i)
                    else:
                        self.included_ids.update(i)

            self.included.emit(model_name, self.included_ids)

            with self.exclude.call(model_name, exclude) as plg:

                for i in plg.all():
                    self.excluded_ids.update(i)

            self.excluded.emit(self._model.__name__, self.excluded_ids)

            self.matched_ids = self.included_ids
            self.matched_ids.difference_update(self.excluded_ids)

        else:

            with self.empty.call(model_name) as plg:
                for i in plg.all():
                    self.matched_ids.update(i)

        self.matched.emit(self._model.__name__, self.matched_ids)

        return self.matched_ids
Exemple #7
0
class PartialModelFilter(Command):
    """
    Perform a partial search on database model with a single term

    Accepts any term

    By default, the following models are supported:

    - User
    - NamespaceTags
    - Tag
    - Namespace
    - Artist
    - Circle
    - Status
    - Grouping
    - Language
    - Category
    - Collection
    - Gallery
    - Title
    - GalleryUrl

    Returns a set with ids of matched model items
    """

    models = CommandEntry("models", tuple)

    match_model = CommandEntry("match_model", set, str, str, str, dict)
    matched = CommandEvent("matched", set)

    def __init__(self):
        super().__init__()
        self.model = None
        self.term = ''
        self._supported_models = set()
        self.matched_ids = set()

    @models.default()
    def _models():
        return (db.NamespaceTags, db.Tag, db.Namespace, db.Artist, db.Circle,
                db.Status, db.Grouping, db.Language, db.Category,
                db.Collection, db.Gallery, db.Title, db.GalleryUrl)

    @staticmethod
    def _match_string_column(column, term, options):

        expr = None
        tag = term.tag

        if options.get("regex"):
            if options.get("case"):
                expr = column.regexp
            else:
                expr = column.iregexp
        else:
            if not options.get("whole"):
                tag = '%' + tag + '%'

            if options.get("case"):
                expr = column.like
            else:
                expr = column.ilike

        return expr(tag)

    @staticmethod
    def _match_integer_column(session, parent_model, column, term, options):

        return []

    @match_model.default(capture=True)
    def _match_gallery(parent_model,
                       child_model,
                       term,
                       options,
                       capture=db.model_name(db.Gallery)):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)

        match_string = PartialModelFilter._match_string_column
        match_int = PartialModelFilter._match_integer_column
        term = ParseTerm().run(term)
        ids = set()

        s = constants.db_session()

        if term.namespace:
            lower_ns = term.namespace.lower()
            if lower_ns == 'path':
                ids.update(x[0] for x in s.query(parent_model.id).filter(
                    match_string(db.Gallery.path, term, options)).all())
            elif lower_ns in ("rating", "stars"):
                ids.update(x[0] for x in s.query(parent_model.id).filter(
                    match_int(db.Gallery.rating, term, options)).all())

        return ids

    @match_model.default(capture=True)
    def _match_title(parent_model,
                     child_model,
                     term,
                     options,
                     capture=db.model_name(db.Title)):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)

        match_string = PartialModelFilter._match_string_column
        term = ParseTerm().run(term)
        ids = set()

        if issubclass(parent_model, db.Gallery):
            if term.namespace.lower() == 'title' or not term.namespace:
                s = constants.db_session()
                ids.update(x[0] for x in s.query(parent_model.id).join(
                    parent_model.titles).filter(
                        match_string(child_model.name, term, options)).all())
        else:
            raise NotImplementedError(
                "Title on {} has not been implemented".format(parent_model))

        return ids

    @match_model.default(capture=True)
    def _match_namemixin(parent_model,
                         child_model,
                         term,
                         options,
                         capture=[
                             db.model_name(x) for x in _models()
                             if issubclass(x, db.NameMixin)
                         ]):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)
        match_string = PartialModelFilter._match_string_column
        term = ParseTerm().run(term)
        ids = set()

        s = constants.db_session()

        col = db.relationship_column(parent_model, child_model)

        ids.update(x[0] for x in s.query(parent_model.id).join(col).filter(
            match_string(child_model.name, term, options)).all())

        return ids

    def main(self, model: db.Base, term: str) -> set:

        self.model = model
        model_name = db.model_name(self.model)
        self.term = term

        with self.models.call() as plg:
            for p in plg.all(default=True):
                self._supported_models.update(p)

        if self.model not in self._supported_models:
            raise exceptions.CommandError(
                utils.this_command(self),
                "Model '{}' is not supported".format(model))

        related_models = db.related_classes(model)

        sess = constants.db_session()

        model_count = sess.query(model).count()

        with self.match_model.call_capture(model_name, model_name,
                                           model_name, self.term,
                                           get_search_options()) as plg:
            for i in plg.all():
                self.matched_ids.update(i)
                if len(self.matched_ids) == model_count:
                    break

        has_all = False
        for m in related_models:
            if m in self._supported_models:
                with self.match_model.call_capture(
                        db.model_name(m), model_name, db.model_name(m),
                        self.term, get_search_options()) as plg:
                    for i in plg.all():
                        self.matched_ids.update(i)
                        if len(self.matched_ids) == model_count:
                            has_all = True
                            break
            if has_all:
                break

        self.matched.emit(self.matched_ids)

        return self.matched_ids
Exemple #8
0
class GetModelImage(AsyncCommand):
    """
    Fetch a database model item's image

    By default, the following models are supported

    - Gallery
    - Page
    - Grouping
    - Collection
    - GalleryFilter

    Returns a Profile database item
    """

    models = CommandEntry("models", tuple)
    generate = CommandEntry("generate", str, str, int, utils.ImageSize)

    cover_event = CommandEvent('cover', object)

    def __init__(self, service=None):
        super().__init__(service, priority=constants.Priority.Low)
        self.model = None
        self.cover = None
        self._supported_models = set()

    @models.default()
    def _models():
        return (db.Grouping, db.Collection, db.Gallery, db.Page,
                db.GalleryFilter)

    @generate.default(capture=True)
    def _generate(model,
                  item_id,
                  size,
                  capture=[db.model_name(x) for x in (db.Page, db.Gallery)]):
        im_path = ""
        model = GetModelClass().run(model)

        if model == db.Gallery:
            page = GetSession().run().query(db.Page.path).filter(
                db.and_op(db.Page.gallery_id == item_id,
                          db.Page.number == 1)).one_or_none()
        else:
            page = GetSession().run().query(
                db.Page.path).filter(db.Page.id == item_id).one_or_none()

        if page:
            im_path = page[0]

        if im_path:
            im_props = io_cmd.ImageProperties(size, 0, constants.dir_thumbs)
            im_path = io_cmd.ImageItem(None, im_path, im_props).main()
        return im_path

    def main(self, model: db.Base, item_id: int,
             image_size: enums.ImageSize) -> db.Profile:

        self.model = model

        if image_size == enums.ImageSize.Original:
            image_size = utils.ImageSize(0, 0)
        else:
            image_size = utils.ImageSize(
                *constants.image_sizes[image_size.name.lower()])

        with self.models.call() as plg:
            for p in plg.all(default=True):
                self._supported_models.update(p)

        if self.model not in self._supported_models:
            raise exceptions.CommandError(
                utils.this_command(self),
                "Model '{}' is not supported".format(model))

        img_hash = io_cmd.ImageItem.gen_hash(model, image_size, item_id)

        cover_path = ""
        generate = True
        sess = constants.db_session()
        self.cover = sess.query(
            db.Profile).filter(db.Profile.data == img_hash).one_or_none()

        if self.cover:
            if io_cmd.CoreFS(self.cover.path).exists:
                generate = False
            else:
                cover_path = self.cover.path
        if generate:
            self.cover = self.run_native(self._generate_and_add, img_hash,
                                         generate, cover_path, model, item_id,
                                         image_size).get()
        self.cover_event.emit(self.cover)
        return self.cover

    def _generate_and_add(self, img_hash, generate, cover_path, model, item_id,
                          image_size):

        sess = constants.db_session()

        model_name = db.model_name(model)

        new = False
        if cover_path:
            self.cover = sess.query(
                db.Profile).filter(db.Profile.data == img_hash).one_or_none()
        else:
            self.cover = db.Profile()
            new = True

        if generate:
            with self.generate.call_capture(model_name, model_name, item_id,
                                            image_size) as plg:
                self.cover.path = plg.first()

            self.cover.data = img_hash
            self.cover.size = str(tuple(image_size))

        if self.cover.path and generate:
            if new:
                s = constants.db_session()
                i = s.query(model).get(item_id)
                i.profiles.append(self.cover)
            sess.commit()
        elif not self.cover.path:
            self.cover = None
        return self.cover
Exemple #9
0
class Archive(CoreCommand):
    """
    Work with archive files

    Args:
        fpath: path to archive file

    """
    _init = CommandEntry('init', object, pathlib.Path)
    _path_sep = CommandEntry('path_sep', str, object)
    _test_corrupt = CommandEntry('test_corrupt', bool, object)
    _is_dir = CommandEntry('is_dir', bool, object, str)
    _extract = CommandEntry("extract", str, object, str, pathlib.Path)
    _namelist = CommandEntry("namelist", tuple, object)
    _open = CommandEntry("open", object, object, str, tuple, dict)
    _close = CommandEntry("close", None, object)

    def _def_formats():
        return (CoreFS.ZIP, CoreFS.RAR, CoreFS.CBZ, CoreFS.CBR)

    def __init__(self, fpath):
        self._archive = None
        self._path = pathlib.Path(fpath)
        self._ext = self._path.suffix.lower()
        if not self._path.exists():
            raise exceptions.ArchiveError(
                "Archive file does not exist. File '{}' not found.".format(
                    str(self._path)))
        if not self._path.suffix.lower() in CoreFS.archive_formats():
            raise exceptions.UnsupportedArchiveError(str(self._path))

        try:
            with self._init.call_capture(self._ext, self._path) as plg:
                self._archive = plg.first_or_none()

            if not self._archive:
                raise exceptions.CoreError(
                    utils.this_function(),
                    "No valid archive handler found for this archive type: '{}'"
                    .format(self._ext))

            with self._test_corrupt.call_capture(self._ext,
                                                 self._archive) as plg:
                r = plg.first_or_none()
                if r is not None:
                    if r:
                        raise exceptions.BadArchiveError(str(self._path))

            with self._path_sep.call_capture(self._ext, self._archive) as plg:
                p = plg.first_or_none()
                self.path_separator = p if p else '/'
        except:
            if self._archive:
                self.close()
            raise

    def __del__(self):
        if hasattr(self, '_archive') and self._archive:
            self.close()

    @_test_corrupt.default(capture=True)
    def _test_corrupt_def(archive, capture=_def_formats()):
        if isinstance(archive, ZipFile):
            return bool(archive.testzip())
        elif isinstance(archive, RarFile):
            return bool(archive.testrar())

    @_init.default(capture=True)
    def _init_def(path, capture=_def_formats()):
        if path.suffix.lower() in ('.zip', '.cbz'):
            o = ZipFile(str(path))
        elif path.suffix.lower() in ('.rar', '.cbr'):
            o = RarFile(str(path))
        o.hpx_path = path
        return o

    @_namelist.default(capture=True)
    def _namelist_def(archive, capture=_def_formats()):
        filelist = archive.namelist()
        return filelist

    @_is_dir.default(capture=True)
    def _is_dir_def(archive, filename, capture=_def_formats()):
        if not filename:
            return False
        if filename not in Archive._namelist_def(
                archive) and filename + '/' not in Archive._namelist_def(
                    archive):
            raise exceptions.FileInArchiveNotFoundError(
                filename, archive.hpx_path)
        if isinstance(archive, ZipFile):
            if filename.endswith('/'):
                return True
        elif isinstance(archive, RarFile):
            info = archive.getinfo(filename)
            return info.isdir()
        return False

    @_extract.default(capture=True)
    def _extract_def(archive, filename, target, capture=_def_formats()):
        temp_p = ""
        if isinstance(archive, ZipFile):
            membs = []
            for name in Archive._namelist_def(archive):
                if name.startswith(filename) and name != filename:
                    membs.append(name)
            temp_p = archive.extract(filename, str(target))
            for m in membs:
                archive.extract(m, str(target))
        elif isinstance(archive, RarFile):
            temp_p = target.join(filename)
            archive.extract(filename, str(target))
        return temp_p

    @_open.default(capture=True)
    def _open_def(archive, filename, args, kwargs, capture=_def_formats()):
        if filename not in Archive._namelist_def(archive):
            raise exceptions.FileInArchiveNotFoundError(
                filename, archive.hpx_path)
        return archive.open(filename, *args, **kwargs)

    @_close.default(capture=True)
    def _close_def(archive, capture=_def_formats()):
        archive.close()

    def namelist(self):
        ""
        with self._namelist.call_capture(self._ext, self._archive) as plg:
            return plg.first()

    def is_dir(self, filename):
        """
        Checks if the provided name in the archive is a directory or not
        """
        with self._is_dir.call_capture(self._ext, self._archive,
                                       filename) as plg:
            return plg.first()

    def extract(self, filename, target):
        """
        Extracts file from archive to target path
        Returns path to the extracted file
        """

        p = pathlib.Path(target)

        if not p.exists():
            raise exceptions.ExtractArchiveError(
                "Target path does not exist: '{}'".format(str(p)))

        with self._extract.call_capture(self._ext, self._archive, filename,
                                        p) as plg:
            extract_path = plg.first()

        return pathlib.Path(extract_path)

    def extract_all(self, target):
        """
        Extracts all files to given path, and returns path
        """
        pass

    def open(self, filename, *args, **kwargs):
        """
        Open file in archive, returns a file-like object.
        """
        with self._open.call_capture(self._ext, self._archive, filename, args,
                                     kwargs) as plg:
            r = plg.first()
            if not hasattr(r, 'read') or not hasattr(r, 'write'):
                raise exceptions.PluginHandlerError(
                    plg.get_node(0),
                    "Expected a file-like object from archive.open")
            return r

    def close(self):
        """
        Close archive, releases all open resources
        """
        with self._close.call_capture(self._ext, self._archive) as plg:
            plg.first()
Exemple #10
0
class CoreFS(CoreCommand):
    """
    Encapsulates path on the filesystem

    Default supported archive types are:
        ZIP, RAR, CBR and CBZ

    Default supported image types are:
        JPG/JPEG, BMP, PNG and GIF

    """

    ZIP = '.zip'
    RAR = '.rar'
    CBR = '.cbr'
    CBZ = '.cbz'

    JPG = '.jpg'
    JPEG = '.jpeg'
    BMP = '.bmp'
    PNG = '.png'
    GIF = '.gif'

    _archive_formats = CommandEntry("archive_formats", tuple)
    _image_formats = CommandEntry("image_formats", tuple)

    def __init__(self, path=pathlib.Path(), archive=None):
        assert isinstance(path, (pathlib.Path, str, CoreFS))
        super().__init__()
        self._path = None
        self._o_path = path
        self._filetype = None
        self._archive = archive
        self._extacted_file = None
        self._resolve_path(path)

    @_archive_formats.default()
    def _archive_formats_def():
        return (CoreFS.ZIP, CoreFS.RAR, CoreFS.CBR, CoreFS.CBZ)

    @_image_formats.default()
    def _image_formats_def():
        return (CoreFS.JPG, CoreFS.JPEG, CoreFS.BMP, CoreFS.PNG, CoreFS.GIF)

    @classmethod
    def archive_formats(cls):
        "Get supported archive formats"
        cls._get_commands()
        with cls._archive_formats.call() as plg:
            formats = set()
            for p in plg.all(default=True):
                formats.update(p)
            return tuple(formats)

    @classmethod
    def image_formats(cls):
        "Get supported image formats"
        cls._get_commands()
        with cls._image_formats.call() as plg:
            formats = set()
            for p in plg.all(default=True):
                formats.update(p)
            return tuple(formats)

    @property
    def name(self):
        "Get name of file or directory"
        return self._path.name

    @property
    def path(self):
        "Get path as string"
        return str(self._path)

    @property
    def archive_path(self):
        "Get path to the archive as string"
        if self.is_archive:
            return self.path
        elif self.inside_archive:
            parts = list(self._path.parents)
            while parts:
                p = parts.pop()
                if p.is_file() and p.suffix.lower() in self.archive_formats():
                    return str(p)
        return ""

    @property
    def archive_name(self):
        "Get the full filename inside the archive"
        if self.inside_archive:
            self._init_archive()
            if isinstance(
                    self._o_path,
                    str) and self._archive.path_separator in self._o_path:
                parts = self._o_path.split(self._archive.path_separator)
            else:
                parts = list(self._path.parts)
            while parts:
                p = parts.pop(0)
                if p.lower().endswith(self.archive_formats()):
                    return self._archive.path_separator.join(parts)
        return ""

    @property
    def is_file(self):
        "Check if path is pointed to a file"
        return not self.is_dir

    @property
    def is_dir(self):
        "Check if path is pointed to a directory"
        if self.inside_archive:
            self._init_archive()
            return self._archive.is_dir(self.archive_name)
        else:
            return self._path.is_dir()

    @property
    def is_archive(self):
        "Check if path is pointed to an archive file"
        if self.ext in self.archive_formats():
            return True
        return False

    @property
    def inside_archive(self):
        "Check if path is pointed to an object inside an archive"
        parts = list(self._path.parents)

        while parts:
            p = parts.pop()
            if p.is_file() and p.suffix.lower() in self.archive_formats():
                return True
        return False

    @property
    def is_image(self):
        "Check if path is pointed to an image file"
        if self.is_file and self.ext in self.image_formats():
            return True
        return False

    @property
    def exists(self):
        "Check if path exists"
        self._init_archive()
        if self.inside_archive:
            return self.archive_name in self._archive.namelist()
        else:
            return self._path.exists()

    @property
    def ext(self):
        "Get file extension. An empty string is returned if path is a directory"
        return self._path.suffix.lower()

    @property
    def count(self):
        "Get the amount of files and folders inside this path. 0 is returned if path is a file"
        raise NotImplementedError
        return self._path.suffix.lower()

    def contents(self):
        "If this is an archive or folder, return a tuple of CoreFS objects else return None"
        if self.is_archive:
            self._init_archive()
            root = self._archive.path_separator.join(self._path.parts)
            return tuple(
                CoreFS(self._archive.path_separator.join((root, x)))
                for x in self._archive.namelist())
        elif self.is_dir:
            if self.inside_archive:
                raise NotImplementedError
            else:
                return tuple(CoreFS(x) for x in self._path.iterdir())

    def get(self):
        "Get path as string. If path is inside an archive it will get extracted"
        self._init_archive()
        if self.inside_archive:
            if not self._extacted_file:
                self._extacted_file = self.extract()
            return self._extacted_file.path
        else:
            return self.path

    @contextmanager  # TODO: Make usable without contextmanager too
    def open(self, *args, **kwargs):
        self._init_archive()
        try:
            if self.inside_archive:
                f = self._archive.open(self.archive_name, *args, **kwargs)
            else:
                f = open(self.get(), *args, **kwargs)
            f = fileobject.FileObject(f)
        except PermissionError:
            if self.is_dir:
                raise exceptions.CoreError(
                    utils.this_function(),
                    "Tried to open a folder which is not possible")
            raise
        yield f
        f.close()

    def close(self):
        ""
        if self._archive:
            self._archive.close()

    def extract(self, filename=None, target=None):
        """
        Extract files if path is an archive

        Args:
            filename: a string or a tuple of strings of contents inside the archive, leave None to extract all contents
            target: a path to where contents will be extracted to, leave None to use a temp directory

        Returns:
            a CoreFS object pointed to the extracted files
        """
        assert isinstance(filename, (str, tuple)) or filename is None
        assert isinstance(target, str) or target is None
        self._init_archive()
        if isinstance(filename, str):
            filename = (filename, )

        if self._archive:

            if not target:
                target = pathlib.Path(utils.create_temp_dir().name)

            if filename:
                p = []
                for f in filename:
                    p.append(CoreFS(self._archive.extract(f, target)))
                if len(p) == 1:
                    return p[0]
                else:
                    return CoreFS(target)
            else:
                if self.inside_archive:
                    return CoreFS(
                        self._archive.extract(self.archive_name, target))
                else:
                    self._archive.extract_all(target)
                    return CoreFS(target)
        return ""

    def _resolve_path(self, p):
        if isinstance(p, CoreFS):
            self._path = p._path
            self._o_path = p._o_path
            self._archive = p._archive
            self._filetype = p._filetype
            return
        self._path = p
        if isinstance(p, str):
            self._path = pathlib.Path(p)

        if not self.inside_archive:  # TODO: resolve only the parts not in archive
            if self._path.exists():
                self._path = self._path.resolve()

    def _init_archive(self):
        if not self._archive:
            if self.inside_archive or self.is_archive:
                self._archive = Archive(self.archive_path)

    def __lt__(self, other):
        if isinstance(other, CoreFS):
            return self.path < other.path
        return super().__lt__(other)
class ScanGallery(AsyncCommand):
    """
    """

    _discover: tuple = CommandEntry("discover",
                                    CParam("path", str, "path to folder or archive"),
                                    CParam("options", dict, "a of options to be applied to the scan"),
                                    __doc="""
                            Called to find any valid galleries in the given path
                            """,
                                    __doc_return="""
                            a tuple of absolute paths or tuples to valid galleries found in the given directory/archive,
                            related galleries are put in their own tuple
                            """)

    @_discover.default()
    def _find_galleries(path, options):
        path = io_cmd.CoreFS(path)
        archive_formats = io_cmd.CoreFS.archive_formats()
        found_galleries = []
        if path.is_archive or path.inside_archive:
            raise NotImplementedError
        else:
            contents = os.scandir(str(path))
            for p in contents:
                if p.is_file() and not p.path.endswith(archive_formats):
                    continue
                found_galleries.append(os.path.abspath(p.path))

        return tuple(found_galleries)

    @async_utils.defer
    def _generate_gallery_fs(self, found_paths, options):
        paths_len = len(found_paths)
        galleries = []
        sess = constants.db_session()
        with db.no_autoflush(sess):
            for n, p in enumerate(found_paths, 1):
                self.next_progress(text=f"[{n}/{paths_len}] {p}")
                if options.get(config.skip_existing_galleries.fullname):
                    if db.Gallery.exists_by_path(p):
                        continue
                g = io_cmd.GalleryFS(p)
                if g.evaluate():
                    g.load_all()
                    self.set_progress(text=f"[{n}/{paths_len}] {g.path.path} .. OK")
                    galleries.append(g)
        return galleries

    def main(self, path: typing.Union[str, io_cmd.CoreFS], options: dict={},
             view_id: int=None, auto_add: bool=False) -> typing.List[io_cmd.GalleryFS]:
        fs = io_cmd.CoreFS(path)
        galleries = []
        self.set_progress(title=fs.path, text=fs.path, type_=enums.ProgressType.GalleryScan)
        self.set_max_progress(1)
        if fs.is_dir or fs.is_archive:
            scan_options = _get_scan_options()
            scan_options.update(options)
            found_paths = set()
            if fs.exists:
                with self._discover.call(fs.path, scan_options) as plg:
                    for p in plg.all(default=True):
                        [found_paths.add(os.path.normpath(x)) for x in p]

            paths_len = len(found_paths)
            log.d("Found", paths_len, "gallery candidates")

            self.set_max_progress(paths_len, add=True)
            view_id = view_id if view_id else constants.default_temp_view_id
            galleries = self._generate_gallery_fs(found_paths, scan_options).get()
            [x.add(view_id=view_id) for x in galleries]
            if auto_add:
                raise NotImplementedError
                #add_cmd = AddGallery()

        self.next_progress(text="")

        return galleries
class OpenGallery(Command):
    """
    Open a gallery in an external viewer

    Args:
        gallery_or_id: a :class:`.db.Gallery` database item or an item id thereof
        number: page number
        args: arguments to pass to the external program

    Returns:
        bool indicating wether the gallery was successfully opened
    """

    _opened = CommandEvent("opened",
                           CParam("parent_path", str, "path to parent folder or archive"),
                           CParam("child_path", str, "path to opened file in folder or archive"),
                           CParam("gallery", db.Gallery, "database item object that was opened"),
                           CParam("number", int, "page number"),
                           __doc="""
                           Emitted when a gallery or page was successfully opened
                           """)

    _open: bool = CommandEntry("open",
                               CParam("parent_path", str, "path to parent folder or archive"),
                               CParam("child_path", str, "path to opened file in folder or archive"),
                               CParam("gallery", db.Gallery, "database item object that was opened"),
                               CParam("arguments", tuple, "a tuple of arguments to pass to the external program"),
                               __doc="""
                               Called to open the given file in an external program
                               """,
                               __doc_return="""
                               a bool indicating whether the file could be opened or not
                               """)
    _resolve: tuple = CommandEntry("resolve",
                                   CParam("gallery", db.Gallery, "database item object that was opened"),
                                   CParam("number", int, "page number"),
                                   __doc="""
                                   Called to resolve the parent (containing folder or archive) and child (the file, usually the first) paths
                                   """,
                                   __doc_return="""
                                   a tuple with two items, the parent (``str``) and child (``str``) paths
                                   """
                                   )

    def main(self, gallery_or_id: db.Gallery=None, number: int=None, args=tuple()) -> bool:
        assert isinstance(gallery_or_id, (db.Gallery, int))
        if isinstance(gallery_or_id, int):
            gallery = database_cmd.GetModelItems().run(db.Gallery, {gallery_or_id})
            if gallery:
                gallery = gallery[0]
        else:
            gallery = gallery_or_id
        self.gallery = gallery
        if number is None:
            number = 1
        opened = False
        if self.gallery.pages.count():
            with self._resolve.call(self.gallery, number) as plg:
                r = plg.first_or_default()
                if len(r) == 2:
                    self.path, self.first_file = r

            args = args if args else tuple(x.strip() for x in config.external_image_viewer_args.value.split())

            with self._open.call(self.path, self.first_file, self.gallery, args) as plg:
                try:
                    opened = plg.first_or_default()
                except OSError as e:
                    raise exceptions.CommandError(utils.this_command(self),
                                                  "Failed to open gallery with external viewer: {}".format(e.args[1]))

        else:
            log.w("Error opening gallery (), no page count".format(self.gallery.id))

        if opened:
            self._opened.emit(self.path, self.first_file, self.gallery, number)

        return opened

    def __init__(self):
        super().__init__()
        self.path = ""
        self.first_file = ""
        self.gallery = None

    @_open.default()
    def _open_gallery(parent, child, gallery, args):
        ex_path = config.external_image_viewer.value.strip()
        log.d("Opening gallery ({}):\n\tparent:{}\n\tchild:{}".format(gallery.id, parent, child))
        log.d("External viewer:", ex_path)
        log.d("External args:", args)
        opened = False
        path = parent
        if child and config.send_path_to_first_file.value:
            path = child

        if ex_path:
            subprocess.Popen((ex_path, path, *args))
            opened = True
        else:
            io_cmd.CoreFS.open_with_default(path)
            opened = True

        return opened

    @_resolve.default()
    def _resolve_gallery(gallery, number):
        parent = ""
        child = ""
        if gallery.single_source:
            if gallery.pages.count():
                # TODO: when number 1 doesnt exist?
                first_page = gallery.pages.filter(db.Page.number == number).first()
                if first_page:
                    if first_page.in_archive:
                        p = io_cmd.CoreFS(first_page.path)
                        parent = p.archive_path
                        child = p.path
                    else:
                        child = first_page.path
                        parent = os.path.split(first_page.path)[0]
        else:
            raise NotImplementedError

        return parent, child
Exemple #13
0
class ModelFilter(Command):
    """
    Perform a full search on database model

    Args:
        model: a database model item
        search_filter: a search query
        match_options: search options, refer to :ref:`Settings`

    Returns:
        a set of ids of matched database model items
    """

    separate: tuple = CommandEntry("separate",
                                   CParam("pieces", tuple, "a tuple of terms"),
                                   __doc="""
                                   Called to separate terms that include and terms that exclude from eachother
                                   """,
                                   __doc_return="""
                                   a tuple of two tuples where the first tuple contains terms that include
                                   and the second contains terms that exclude
                                   """
                                   )
    include: set = CommandEntry("include",
                                CParam("model_name", str, "name of a database model"),
                                CParam("pieces", set, "a set of terms"),
                                CParam("options", ChainMap, "search options"),
                                __doc="""
                                Called to match database items of the given model to include in the final results
                                """,
                                __doc_return="""
                                a ``set`` of ids of the database items that match
                                """
                                )
    exclude: set = CommandEntry("exclude",
                                CParam("model_name", str, "name of a database model"),
                                CParam("pieces", set, "a set of terms"),
                                CParam("options", ChainMap, "search options"),
                                __doc="""
                                Called to match database items of the given model to exclude in the final results
                                """,
                                __doc_return="""
                                a ``set`` of ids of the database items that match
                                """
                                )
    empty: set = CommandEntry("empty",
                              CParam("model_name", str, "name of a database model"),
                              __doc="""
                                Called when the search query is empty
                                """,
                              __doc_return="""
                                a ``set`` of ids of the database items that match when a search query is empty
                                """)

    included = CommandEvent("included",
                            CParam("model_name", str, "name of a database model"),
                            CParam("matched_ids", set, "a ``set`` of ids of the database items that match for inclusion"),
                            __doc="""
                            Emitted after the match
                            """)

    excluded = CommandEvent("excluded",
                            CParam("model_name", str, "name of a database model"),
                            CParam("matched_ids", set, "a ``set`` of ids of the database items that match for exclusion"),
                            __doc="""
                            Emitted after the match
                            """)

    matched = CommandEvent("matched",
                           CParam("model_name", str, "name of a database model"),
                           CParam("matched_ids", set, "a ``set`` of ids of the database items that match"),
                           __doc="""
                            Emitted at the end of the process with the final results
                            """)

    def main(self, model: db.Base, search_filter: str, match_options: dict = {}) -> typing.Set[int]:
        assert issubclass(model, db.Base)

        self._model = model
        model_name = db.model_name(self._model)

        if search_filter:

            self.parsesearchfilter = ParseSearch()

            pieces = self.parsesearchfilter.run(search_filter)

            options = get_search_options(match_options)

            include = set()
            exclude = set()

            with self.separate.call(pieces) as plg:

                for p in plg.all(True):
                    if len(p) == 2:
                        include.update(p[0])
                        exclude.update(p[1])

            if options.get("all"):
                for n, p in enumerate(include):
                    with self.include.call(model_name, {p}, options) as plg:
                        for i in plg.all_or_default():
                            if n != 0:
                                self.included_ids.intersection_update(i)
                            else:
                                self.included_ids.update(i)
            else:
                with self.include.call(model_name, include, options) as plg:

                    for n, i in enumerate(plg.all_or_default()):
                        self.included_ids.update(i)

            self.included.emit(model_name, self.included_ids)

            with self.exclude.call(model_name, exclude, options) as plg:

                for i in plg.all_or_default():
                    self.excluded_ids.update(i)

            self.excluded.emit(self._model.__name__, self.excluded_ids)

            self.matched_ids = self.included_ids
            self.matched_ids.difference_update(self.excluded_ids)

        else:

            with self.empty.call(model_name) as plg:
                for i in plg.all_or_default():
                    self.matched_ids.update(i)

        self.matched.emit(self._model.__name__, self.matched_ids)

        return self.matched_ids

    def __init__(self):
        super().__init__()
        self._model = None
        self.parsesearchfilter = None
        self.included_ids = set()
        self.excluded_ids = set()
        self.matched_ids = set()

    @separate.default()
    def _separate(pecies):

        include = []
        exclude = []

        for p in pecies:
            if p.startswith('-'):
                exclude.append(p[1:])  # remove '-' at the start
            else:
                include.append(p)

        return tuple(include), tuple(exclude)

    @staticmethod
    def _match(model_name, pieces, options):
        ""
        model = database_cmd.GetModelClass().run(model_name)
        partialfilter = PartialModelFilter()
        matched = set()

        for p in pieces:
            m = partialfilter.run(model, p, options)
            matched.update(m)

        return matched

    @include.default()
    def _include(model_name, pieces, options):
        return ModelFilter._match(model_name, pieces, options)

    @exclude.default()
    def _exclude(model_name, pieces, options):
        return ModelFilter._match(model_name, pieces, options)

    @empty.default()
    def _empty(model_name):
        model = database_cmd.GetModelClass().run(model_name)
        s = constants.db_session()
        return set(x[0] for x in s.query(model.id).all())
Exemple #14
0
class ParseTerm(Command):
    """
    Parse a single term

    By default, the following operators are parsed for:
    - '' to ''
    - '<' to 'less'
    - '>' to 'great'

    Args:
        term: a single term

    Returns:
        a namedtuple of namespace, tag and operator
    """

    parse: tuple = CommandEntry("parse",
                                CParam('term', str, "a single term"),
                                __doc="""
                                Called to parse the term into ``namespace``, ``tag`` and ``operator``
                                """,
                                __doc_return="""
                                a tuple of strings (namespace, tag, operator)
                                """)

    parsed = CommandEvent("parsed",
                          CParam('term', Term, "the parsed term"),
                          __doc="""
                          Emitted after a term has been parsed
                          """)

    def __init__(self):
        super().__init__()
        self.filter = ''
        self.term = None

    @parse.default()
    def _parse_term(term):
        s = term.split(':', 1)
        ns = s[0] if len(s) == 2 else ''
        tag = s[1] if len(s) == 2 else term
        operator = ''

        if tag.startswith('<'):
            operator = 'less'
        elif tag.startswith('>'):
            operator = 'great'
        return (ns, tag, operator)

    def main(self, term: str) -> Term:

        self.filter = term

        with self.parse.call(self.filter) as plg:
            t = plg.first_or_default()
            if not len(t) == 3:
                t = plg.default()
            self.term = Term(*t)

        self.parsed.emit(self.term)

        return self.term
Exemple #15
0
class PartialModelFilter(Command):
    """
    Perform a partial search on database model with a single term

    Accepts any term

    By default, the following models are supported:

    - User
    - NamespaceTags
    - Tag
    - Namespace
    - Artist
    - Circle
    - Parody
    - Status
    - Grouping
    - Language
    - Category
    - Collection
    - Gallery
    - Title
    - Url

    Args:
        model: a database model item
        term: a single term, like ``rating:5``
        match_options: search options, refer to :ref:`Settings`

    Returns:
        a ``set`` with ids of matched database model items
    """

    models: tuple = CommandEntry("models",
                                 __doc="""
                                 Called to get a tuple of supported database models
                                 """,
                                 __doc_return="""
                                 a tuple of database model items
                                 """)

    match_model: set = CommandEntry("match_model",
                                    CParam("parent_model_name", str, "name of parent database model"),
                                    CParam("child_model_name", str, "name of child database model"),
                                    CParam("term", str, "a single term"),
                                    CParam("options", ChainMap, "search options"),
                                    __capture=(str, "a database model name"),
                                    __doc="""
                                  Called to perform the matching on database items of the given model
                                  """,
                                    __doc_return="""
                                  a ``set`` of ids of the database items that match
                                  """)
    matched = CommandEvent("matched",
                           CParam("matched_ids", set, "a ``set`` of ids of the database items that match"),
                           __doc="""
                           Emitted at the end of the process
                           """
                           )

    def main(self, model: db.Base, term: str, match_options: dict = {}) -> typing.Set[int]:

        self.model = model
        model_name = db.model_name(self.model)
        self.term = term
        with self.models.call() as plg:
            for p in plg.all(default=True):
                self._supported_models.update(p)

        if self.model not in self._supported_models:
            raise exceptions.CommandError(utils.this_command(self),
                                          "Model '{}' is not supported".format(model))

        options = get_search_options(match_options)
        log.d("Match options", options)

        related_models = db.related_classes(model)

        sess = constants.db_session()

        model_count = sess.query(model).count()

        with self.match_model.call_capture(model_name, model_name, model_name, self.term, options) as plg:
            for i in plg.all_or_default():
                self.matched_ids.update(i)
                if len(self.matched_ids) == model_count:
                    break

        has_all = False
        for m in related_models:
            if m in self._supported_models:
                with self.match_model.call_capture(db.model_name(m), model_name, db.model_name(m), self.term, options) as plg:
                    for i in plg.all_or_default():
                        self.matched_ids.update(i)
                        if len(self.matched_ids) == model_count:
                            has_all = True
                            break
            if has_all:
                break

        self.matched.emit(self.matched_ids)

        return self.matched_ids

    def __init__(self):
        super().__init__()
        self.model = None
        self.term = ''
        self._supported_models = set()
        self.matched_ids = set()

    @models.default()
    def _models():
        return (db.Taggable,
                db.Artist,
                db.Circle,
                db.Parody,
                db.Status,
                db.Grouping,
                db.Language,
                db.Category,
                db.Collection,
                db.Gallery,
                db.Title,
                db.Namespace,
                db.Tag,
                db.NamespaceTags,
                db.Url)

    @staticmethod
    def _match_string_column(column, value, options, **kwargs):

        options.update(kwargs)

        expr = None

        if options.get("regex"):
            if options.get("case"):
                expr = column.regexp
            else:
                expr = column.iregexp
        else:
            if not options.get("whole"):
                value = '%' + value + '%'
            if options.get("case"):
                expr = column.like
            else:
                expr = column.ilike

        return expr(value)

    @staticmethod
    def _match_integer_column(column, value, options, **kwargs):

        options.update(kwargs)

        return None

    @match_model.default(capture=True)
    def _match_gallery(parent_model, child_model, term,
                       options, capture=db.model_name(db.Gallery)):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)

        match_string = PartialModelFilter._match_string_column
        match_int = PartialModelFilter._match_integer_column
        term = ParseTerm().run(term)
        ids = set()

        col_on_parent = db.relationship_column(parent_model, child_model)
        s = constants.db_session()
        q = s.query(parent_model.id)
        if col_on_parent:
            q = q.join(col_on_parent)
        if term.namespace:
            lower_ns = term.namespace.lower()
            if lower_ns == 'path':
                ids.update(x[0] for x in q.filter(match_string(db.Gallery.path,
                                                               term.tag,
                                                               options)).all())
            elif lower_ns in ("rating", "stars"):
                ids.update(x[0] for x in q.filter(match_int(db.Gallery.rating,
                                                            term.tag,
                                                            options)).all())

        return ids

    @match_model.default(capture=True)
    def _match_title(parent_model, child_model, term, options,
                     capture=db.model_name(db.Title)):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)

        match_string = PartialModelFilter._match_string_column
        term = ParseTerm().run(term)
        ids = set()

        if term.namespace.lower() == 'title' or not term.namespace:

            col_on_parent = db.relationship_column(parent_model, child_model)

            s = constants.db_session()
            q = s.query(parent_model.id)
            if col_on_parent:
                q = q.join(col_on_parent)
            ids.update(x[0] for x in q.filter(match_string(child_model.name, term.tag, options)).all())
        return ids

    @match_model.default(capture=True)
    def _match_tags(parent_model, child_model, term, options,
                    capture=[db.model_name(x) for x in (db.Taggable, db.NamespaceTags)]):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)

        match_string = PartialModelFilter._match_string_column
        term = ParseTerm().run(term)
        ids = set()

        col_on_parent = db.relationship_column(parent_model, child_model)
        col_on_child = db.relationship_column(child_model, db.NamespaceTags)
        col_tag = db.relationship_column(db.NamespaceTags, db.Tag)
        s = constants.db_session()
        q = s.query(parent_model.id)
        if col_on_parent and parent_model != child_model:
            q = q.join(col_on_parent)
        if col_on_child and parent_model != child_model:
            q = q.join(col_on_child)
        if term.namespace:
            col_ns = db.relationship_column(db.NamespaceTags, db.Namespace)
            items = q.join(col_ns).join(col_tag).filter(db.and_op(
                match_string(db.Namespace.name, term.namespace, options, whole=True),
                match_string(db.Tag.name, term.tag, options))).all()
        else:
            items = q.join(col_tag).filter(
                match_string(db.Tag.name, term.tag, options)).all()

        ids.update(x[0] for x in items)
        return ids

    @match_model.default(capture=True)
    def _match_artist(parent_model, child_model, term, options,
                      capture=db.model_name(db.Artist)):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)

        match_string = PartialModelFilter._match_string_column
        term = ParseTerm().run(term)
        ids = set()

        if term.namespace.lower() == 'artist' or not term.namespace:
            col_on_parent = db.relationship_column(parent_model, child_model)

            s = constants.db_session()
            q = s.query(parent_model.id)
            if col_on_parent:
                q = q.join(col_on_parent)
            ids.update(
                x[0] for x in q.join(
                    child_model.names).filter(
                    match_string(
                        db.ArtistName.name,
                        term.tag,
                        options)).all())
        return ids

    @match_model.default(capture=True)
    def _match_parody(parent_model, child_model, term, options,
                      capture=db.model_name(db.Parody)):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)

        match_string = PartialModelFilter._match_string_column
        term = ParseTerm().run(term)
        ids = set()

        if term.namespace.lower() == 'parody' or not term.namespace:
            col_on_parent = db.relationship_column(parent_model, child_model)

            s = constants.db_session()
            q = s.query(parent_model.id)
            if col_on_parent:
                q = q.join(col_on_parent)
            ids.update(
                x[0] for x in q.join(
                    child_model.names).filter(
                    match_string(
                        db.ParodyName.name,
                        term.tag,
                        options)).all())
        return ids

    @match_model.default(capture=True)
    def _match_namemixin(parent_model, child_model, term, options,
                         capture=[db.model_name(x) for x in _models() if issubclass(x, (db.NameMixin, db.Url))]):
        get_model = database_cmd.GetModelClass()
        parent_model = get_model.run(parent_model)
        child_model = get_model.run(child_model)
        match_string = PartialModelFilter._match_string_column
        term = ParseTerm().run(term)
        ids = set()

        if term.namespace.lower() == child_model.__name__.lower() or not term.namespace:
            col_on_parent = db.relationship_column(parent_model, child_model)
            s = constants.db_session()
            q = s.query(parent_model.id)
            if col_on_parent:
                q = q.join(col_on_parent)
            ids.update(x[0] for x in q.filter(match_string(child_model.name,
                                                           term.tag,
                                                           options)).all())
        return ids