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
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)
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), }
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
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
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
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
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
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()
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
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())
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
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