class GetModelItemByID(Command): """ Fetch model items from the database by a set of ids Returns a tuple of model items """ fetched = CommandEvent("fetched", str, tuple) def __init__(self): super().__init__() self.fetched_items = tuple() def _query(self, q, limit, offset): if offset: q = q.offset(offset) return q.limit(limit).all() def main(self, model: db.Base, ids: set, limit: int = 999, filter: str = "", order_by: str = "", offset: int = 0) -> tuple: log.d("Fetching items from a set with", len(ids), "ids", "offset:", offset, "limit:", limit) if not ids: return tuple() s = constants.db_session() q = s.query(model) if filter: q = q.filter(db.sa_text(filter)) if order_by: q = q.order_by(db.sa_text(order_by)) id_amount = len(ids) # TODO: only SQLite has 999 variables limit _max_variables = 900 if id_amount > _max_variables: fetched_list = [x for x in q.all() if x.id in ids] fetched_list = fetched_list[offset:][:limit] self.fetched_items = tuple(fetched_list) elif id_amount == 1: self.fetched_items = (q.get(ids.pop()), ) else: q = q.filter(model.id.in_(ids)) self.fetched_items = tuple(self._query(q, limit, offset)) self.fetched.emit(db.model_name(model), self.fetched_items) log.d("Returning", len(self.fetched_items), "fetched items") return self.fetched_items
class GetModelItems(Command): """ Fetch model items from the database Returns a tuple of model items """ fetched = CommandEvent("fetched", str, tuple) def __init__(self): super().__init__() self.fetched_items = tuple() def _query(self, q, limit, offset): if offset: q = q.offset(offset) return q.limit(limit).all() def main(self, model: db.Base, limit: int = 999, filter: str = "", order_by: str = "", offset: int = 0, join: str = "") -> tuple: s = constants.db_session() q = s.query(model) if join: if not isinstance(join, (list, tuple)): join = [join] for j in join: if isinstance(j, str): q = q.join(db.sa_text(j)) else: q = q.join(j) if filter: if isinstance(filter, str): q = q.filter(db.sa_text(filter)) else: q = q.filter(filter) if order_by: q = q.order_by(db.sa_text(order_by)) self.fetched_items = tuple(self._query(q, limit, offset)) self.fetched.emit(db.model_name(model), self.fetched_items) return self.fetched_items
class InitApplication(Command): """ Initialize the appplication """ init = CommandEvent("init", __doc=""" Emitted on application startup where everything has been initialized after the server has started. """) def __init__(self, priority=constants.Priority.Normal): super().__init__(priority) def main(self): self.init.emit()
class AddGallery(UndoCommand): """ Add a gallery """ added = CommandEvent("added", db.Gallery) def __init__(self): super().__init__() def main(self, gallery: db.Gallery) -> None: pass def undo(self): return super().undo()
class ShutdownApplication(Command): """ Shutdown the appplication """ shutdown = CommandEvent("shutdown", __doc=""" Emitted when about to shutdown """) def __init__(self, priority=constants.Priority.Normal): super().__init__(priority) def main(self): self.shutdown.emit()
class RestartApplication(Command): """ Restart the appplication """ restart = CommandEvent("restart", __doc=""" Emitted when about to restart """) def __init__(self, priority=constants.Priority.Normal): super().__init__(priority) def main(self): self.restart.emit()
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 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 GetModelItemByID(Command): """ Fetch model items from the database by a set of ids Returns a tuple of model items """ fetched = CommandEvent("fetched", str, tuple) count = CommandEvent("count", str, int) def __init__(self): super().__init__() self.fetched_items = tuple() def _query(self, q, limit, offset): if offset: q = q.offset(offset) return q.limit(limit).all() def _get_sql(self, expr): if isinstance(expr, str): return db.sa_text(expr) else: return expr def main(self, model: db.Base, ids: set = None, limit: int = 999, filter: str = None, order_by: str = None, offset: int = 0, columns: tuple = tuple(), join: str = None, count: bool = False) -> tuple: if ids is None: log.d("Fetching items", "offset:", offset, "limit:", limit) else: log.d("Fetching items from a set with", len(ids), "ids", "offset:", offset, "limit:", limit) if ids is not None and not ids: return tuple() s = constants.db_session() if count: q = s.query(model.id) elif columns: q = s.query(*columns) else: q = s.query(model) if join is not None: if not isinstance(join, (list, tuple)): join = [join] for j in join: q = q.join(self._get_sql(j)) if filter is not None: q = q.filter(self._get_sql(filter)) if order_by is not None: q = q.order_by(self._get_sql(order_by)) if ids: id_amount = len(ids) # TODO: only SQLite has 999 variables limit _max_variables = 900 if id_amount > _max_variables: if count: fetched_list = [x for x in q.all() if x[0] in ids] else: fetched_list = [x for x in q.all() if x.id in ids] fetched_list = fetched_list[offset:][:limit] self.fetched_items = tuple(fetched_list) if not count else len( fetched_list) elif id_amount == 1: self.fetched_items = (q.get( ids.pop()), ) if not count else q.count() else: q = q.filter(model.id.in_(ids)) self.fetched_items = tuple(self._query( q, limit, offset)) if not count else q.count() else: self.fetched_items = tuple(self._query( q, limit, offset)) if not count else q.count() if count: self.fetched_items = q.count() self.count.emit(db.model_name(model), self.fetched_items) log.d("Returning items count ", self.fetched_items) else: self.fetched.emit(db.model_name(model), self.fetched_items) self.count.emit(db.model_name(model), len(self.fetched_items)) log.d("Returning", len(self.fetched_items), "fetched items") return self.fetched_items
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 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
class GetModelItems(Command): """ Fetch model items from the database Args: model: a database model item ids: fetch items in this set of item ids or set ``None`` to fetch all columns: a tuple of database item columns to fetch limit: amount to limit the results, set ``None`` for no limit offset: amount to offset the results count: only return the count of items filter: either a textual SQL criterion or a database criterion expression (can also be a tuple) order_by: either a textual SQL criterion or a database model item attribute (can also be a tuple) group_by: either a textual SQL criterion or a database model item attribute (can also be a tuple) join: either a textual SQL criterion or a database model item attribute (can also be a tuple) count: return count of items cache: cache results Returns: a tuple of database model items or ``int`` if ``count`` was set to true """ fetched = CommandEvent( "fetched", CParam("model_name", str, "name of a database model"), CParam("items", tuple, "fetched items"), __doc=""" Emitted when items were fetched successfully """, ) count = CommandEvent("count", CParam("model_name", str, "name of a database model"), CParam("item_count", int, "count of items"), __doc=""" Emitted when query was successful """) def __init__(self): super().__init__() self.fetched_items = tuple() self.cache = True def _query(self, q, limit, offset): if offset: q = q.offset(offset) if limit: q = q.limit(limit) self._invalidate_query(q) return q.all() def _invalidate_query(self, q): if self.cache and constants.invalidator.dirty_database: q.invalidate() def _get_sql(self, expr): if isinstance(expr, str): return db.sa_text(expr) else: return expr def _get_count(self, q): self._invalidate_query(q) return q.count() def main(self, model: db.Base, ids: set = None, limit: int = 999, filter: str = None, order_by: str = None, group_by: str = None, offset: int = 0, columns: tuple = tuple(), join: str = None, count: bool = False, cache=True) -> typing.Union[tuple, int]: self.cache = cache if ids is None: log.d("Fetching items", "offset:", offset, "limit:", limit) else: log.d("Fetching items from a set with", len(ids), "ids", "offset:", offset, "limit:", limit) if not offset: offset = 0 if not limit: limit = 0 if (ids is not None and not ids) or\ (ids and len(ids) == 1 and all(x == 0 for x in ids)): return 0 if count else tuple() criteria = False s = constants.db_session() if count: q = s.query(model.id) elif columns: q = s.query(*columns) else: q = s.query(model) if cache: q.options(db_cache.FromCache('db')) if join is not None: criteria = True if not isinstance(join, (list, tuple)): join = [join] for j in join: q = q.join(self._get_sql(j)) if group_by is not None: criteria = True if not isinstance(group_by, (list, tuple)): group_by = [group_by] q = q.group_by(*(self._get_sql(g) for g in group_by)) if order_by is not None: criteria = True if not isinstance(order_by, (list, tuple)): order_by = [order_by] q = make_order_by_deterministic( q.order_by(*(self._get_sql(o) for o in order_by))) if filter is not None: criteria = True q = q.filter(self._get_sql(filter)) if ids: id_amount = len(ids) _max_variables = 900 if id_amount > _max_variables and config.dialect.value == constants.Dialect.SQLITE: if count: fetched_list = [ x for x in self._query(q, None, None) if x[0] in ids ] else: fetched_list = [ x for x in self._query(q, None, None) if x.id in ids ] if not limit: limit = len(fetched_list) fetched_list = fetched_list[offset:][:limit] self.fetched_items = tuple(fetched_list) if not count else len( fetched_list) elif id_amount == 1 and (not columns and not criteria): self._invalidate_query(q) self.fetched_items = (q.get( ids.pop()), ) if not count else self._get_count(q) else: q = q.filter(model.id.in_(ids)) self.fetched_items = tuple(self._query( q, limit, offset)) if not count else self._get_count(q) else: self.fetched_items = tuple(self._query( q, limit, offset)) if not count else self._get_count(q) if count: self.count.emit(db.model_name(model), self.fetched_items) log.d("Returning items count ", self.fetched_items) else: self.fetched.emit(db.model_name(model), self.fetched_items) self.count.emit(db.model_name(model), len(self.fetched_items)) log.d("Returning", len(self.fetched_items), "fetched items") return self.fetched_items
class UpdateApplication(AsyncCommand): """ Check for new release and update the application Args: download_url: url to file which is to be downloaded, if ``None`` the url will be retrieved with :func:`CheckUpdate` restart: call :func:`RestartApplication` when the update has been registered silent: supress all errors push: push notifications on update Returns: bool indicating whether the update has been registered or not """ update = CommandEvent( "update", CParam("status", bool, "whether the update has been registered or not"), CParam( "restart", bool, "whether the call :func:`RestartApplication` if the update was registered" ), __doc=""" Emitted at the end of the process """) def __init__(self, service=None, priority=constants.Priority.Low): return super().__init__(service, priority) def main(self, download_url: str = None, restart: bool = True, silent: bool = True, push: bool = False) -> bool: self.set_progress(type_=enums.ProgressType.UpdateApplication) self.set_max_progress(3) st = False if download_url: rel = download_url else: rel = updater.check_release(silent=silent, cmd=self) if rel: rel = rel['url'] self.set_progress(1) if rel: new_rel = updater.get_release(rel, silent=silent, cmd=self) self.set_progress(2) if new_rel: st = updater.register_release(new_rel['path'], silent, restart=restart) self.set_progress(3) if push: if restart: m = "Restarting and installing new update..." else: m = "The update will be installed on the next startup" msg = message.Notification( m, "A new update is pending to be installed") msg.id = constants.PushID.Update.value self.push(msg) self.update.emit(st, restart) return st