import { create_photo, create_album, create_tag } from './media.js'; import { sqljs_async_queries } from './database.js'; export function escape_regex(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); }; export function do_image_query(query, database, collection_path, collection_thumbs_path) { return new Promise(function (resolve, reject) { var queries = []; queries.push(query); sqljs_async_queries(database, queries).then(res => { console.log("response: ", res); var photos = []; if (res && Array.isArray(res) && res.length > 0) { var cols = res[0].columns; var data = res[0].values; data.forEach(row => { var imagepath = process.env.PUBLIC_URL + collection_path + "/" + row[cols.indexOf("relativePath")] + "/" + row[cols.indexOf("name")]; var thumbpath = process.env.PUBLIC_URL + collection_thumbs_path + "/" + row[cols.indexOf("uniqueHash")] + ".jpg"; photos.push( create_photo( row[cols.indexOf("id")], row[cols.indexOf("name")], imagepath, thumbpath, [row[cols.indexOf("width")], row[cols.indexOf("height")]] )); }); } resolve(photos); }) .catch(err => { throw err; }); }); } export function do_album_query(query, database) { return new Promise(function (resolve, reject) { var queries = []; queries.push(query); sqljs_async_queries(database, queries).then(res => { var albums = []; if (res && Array.isArray(res) && res.length > 0) { var cols = res[0].columns; var data = res[0].values; data.forEach(row => { var album_name = row[cols.indexOf("relativePath")].substring(row[cols.indexOf("relativePath")].lastIndexOf('/') + 1); albums.push(create_album(row[cols.indexOf("id")], album_name, row[cols.indexOf("relativePath")])); }); } resolve(albums); }); }); } export function do_tag_query(query, database) { return new Promise(function (resolve, reject) { var queries = []; queries.push(query); console.log("Provided DB tags schema before query:", database.exec("PRAGMA table_info([Tags]);")); sqljs_async_queries(database, queries).then(res => { var tags = []; if (res && Array.isArray(res) && res.length > 0) { var cols = res[0].columns; var data = res[0].values; data.forEach(row => { tags.push(create_tag(row[cols.indexOf("id")], row[cols.indexOf("name")], row[cols.indexOf("fullname")])); }); } resolve(tags); }); }); } export const MatchTypeEnum = { MATCH_IMAGE_NAME_EQUALS: 1, MATCH_IMAGE_NAME_NATURAL: 2, MATCH_ALBUM_EQUALS: 3, // Match on the full name (relative path) of the relevant album, if any MATCH_ALBUM_EQUALS_OR_CHILD: 4, // Match on the full name (relative path) of the relevant album, if any, or any of its children MATCH_ALBUM_NATURAL: 5, MATCH_ALBUM_NAME_EQUALS: 6, // Match on the local album name (excluding parents) MATCH_TAG_EQUALS: 7, // Match on the full name (relative path) of the relevant tag, if any MATCH_TAG_EQUALS_OR_CHILD: 8, // Match on the full name (path) of the relevant tag, if any, or any of its children MATCH_TAG_NATURAL: 9, MATCH_TAG_NAME_EQUALS: 10, // Match on the local tag name (excluding parents) } export const ResultTypeEnum = { IMAGE: 1, ALBUM: 2, TAG: 3, } export class ResultFilter { constructor(type) { this.result_type = type; } result_type = ResultTypeEnum.IMAGE; to_sql_where() { return "(1=1)"; } is_true() { return false; } is_false() { return false; } is_negation() { return false; } simplify() { return this; } } export class ConstFilter extends ResultFilter { constructor(type, val) { super(type); this.constval = val; } constval = true; // True lets everything through, false rejects everything to_sql_where() { return this.constval ? "(1=1)" : "(1=0)"; } is_true() { return this.constval; } is_false() { return !this.constval; } simplify() { return this; } } export class NegationFilter extends ResultFilter { constructor(rtype, body) { super(rtype); this.body = body; } body = new ConstFilter(this.return_type, false); to_sql_where() { return "NOT (" + this.body.to_sql_where() + ")"; } is_negation() { return true; } simplify() { var f = this.body.simplify(); console.log("NOT body:", f); if (f.is_true()) { return new ConstFilter(this.result_type, false); } if (f.is_false()) { return new ConstFilter(this.result_type, true); } if (f.is_negation()) { return f.body; } return this; } } export const TimeFilterTypeEnum = { BEFORE: 1, AFTER: 2, } export class TimeFilter extends ResultFilter { constructor(rtype, time, type) { super(rtype); this.time = time; this.type = type; } type = TimeFilterTypeEnum.AFTER; time = Date.now(); to_sql_where() { var operator = ''; if(this.type == TimeFilterTypeEnum.AFTER) { operator = '>='; } else if(this.type == TimeFilterTypeEnum.BEFORE) { operator = '<='; } else { throw new Error("Unsupported time filter type."); } return '(ImageInformation.creationDate' + operator + '"' + this.time.toLocaleString() + '")'; } simplify() { return this; } } export class MatchingFilter extends ResultFilter { constructor(rtype, from, mtype) { super(rtype); this.match_from = from; this.match_type = mtype; } // optional string used in the filtering match_from = ""; // What and how to match match_type = MatchTypeEnum.MATCH_IMAGE_NAME_EQUALS; to_sql_where() { if (this.match_type == MatchTypeEnum.MATCH_IMAGE_NAME_EQUALS) { return '(Images.name="' + this.match_from + '")'; } else if (this.match_type == MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL) { return '(Images.name NOT NULL AND REGEXP(LOWER(Images.name), LOWER(".*' + escape_regex(this.match_from) + '.*")))'; } else if (this.match_type == MatchTypeEnum.MATCH_ALBUM_EQUALS) { return '(Albums.relativePath="' + this.match_from + '")'; } else if (this.match_type == MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD) { return '(Albums.relativePath NOT NULL AND REGEXP(Albums.relativePath, "' + escape_regex(this.match_from) + '(\/[^\/]+)*"))'; } else if (this.match_type == MatchTypeEnum.MATCH_ALBUM_NATURAL) { throw new Error("Natural matching on album names is not yet supported."); } else if (this.match_type == MatchTypeEnum.MATCH_ALBUM_NAME_EQUALS) { return '(Albums.relativePath NOT NULL AND REGEXP(Albums.relativePath, "\/(.*\/)*' + escape_regex(this.match_from) + '(\/[^\/]+)*"))'; } else if (this.match_type == MatchTypeEnum.MATCH_TAG_EQUALS) { return '(Tags.fullname="' + this.match_from + '")'; } else if (this.match_type == MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD) { return '(Tags.fullname NOT NULL AND REGEXP(Tags.fullname, "' + escape_regex(this.match_from) + '(\/[^\/]+)*"))'; } else if (this.match_type == MatchTypeEnum.MATCH_TAG_NATURAL) { throw new Error("Natural matching on tag names is not yet supported."); } else if (this.match_type == MatchTypeEnum.MATCH_TAG_NAME_EQUALS) { return '(Tags.fullname NOT NULL AND REGEXP(Tags.fullname, "\/(.*\/)*' + escape_regex(this.match_from) + '(\/[^\/]+)*"))'; } console.log(this); throw new Error("Unsupported matching filter for SQL generation."); } simplify() { return this; } } export const LogicalOperatorEnum = { AND: 1, OR: 2, } export class LogicalOperatorFilter extends ResultFilter { constructor(type, a, b, op) { super(type); this.sub_filter_a = a; this.sub_filter_b = b; this.operator = op; } sub_filter_a = false; sub_filter_b = false; operator = LogicalOperatorEnum.AND; to_sql_where() { var where1 = this.sub_filter_a.to_sql_where(); var where2 = this.sub_filter_b.to_sql_where(); var operator_str = ""; if (this.operator === LogicalOperatorEnum.AND) { operator_str = " AND "; } else if (this.operator === LogicalOperatorEnum.OR) { operator_str = " OR "; } else { throw new Error('Unsupported logical operator: ' + this.operator); } return "(" + where1 + operator_str + where2 + ")"; } simplify() { var a = this.sub_filter_a.simplify(); var b = this.sub_filter_b.simplify(); if (this.operator === LogicalOperatorEnum.OR) { if (a.is_true() || b.is_true()) { return new ConstFilter(this.return_type, true); } if (a.is_false()) { return b; } if (b.is_false()) { return a; } } if (this.operator === LogicalOperatorEnum.AND) { if (a.is_false() || b.is_false()) { return new ConstFilter(this.return_type, false); } if (a.is_true()) { return b; } if (b.is_true()) { return a; } } return this; } } export class UserQuery { image_filter = new ConstFilter(ResultTypeEnum.IMAGE, false); album_filter = new ConstFilter(ResultTypeEnum.ALBUM, false); tag_filter = new ConstFilter(ResultTypeEnum.TAG, false); } // This query will return database entries with the fields "id", "uniqueHash", "relativePath" (of the album) and "name" for each matching image. export function image_query_with_where(maybe_where) { return "SELECT Images.id, Images.name, Images.uniqueHash, Albums.relativePath, " + "ImageInformation.width, ImageInformation.height " + "FROM Images INNER JOIN Albums ON Images.album=Albums.id " + "LEFT JOIN ImageTags ON Images.id=ImageTags.imageid " + "LEFT JOIN ImageInformation ON Images.id=ImageInformation.imageid " + "LEFT JOIN Tags ON ImageTags.tagid=Tags.id " + (maybe_where ? maybe_where : "") + " GROUP BY Images.id;"; } // This query will return database entries with the fields "id" and "relativePath" for each matching album. export function album_query_with_where(maybe_where) { var query = "SELECT Albums.id, Albums.relativePath FROM Albums"; // If there is tags/images stuff in the where clause, we need to do a join on those tables. if(maybe_where && (maybe_where.includes("Tags.") || maybe_where.includes("Images."))) { query = query + " LEFT JOIN Images ON Images.album=Albums.id " + " LEFT JOIN ImageTags ON ImageTags.imageid=Images.id " + " LEFT JOIN Tags ON Tags.id=ImageTags.tagid"; } query = query + " " + (maybe_where ? maybe_where : "") + " GROUP BY Albums.id;"; return query; } // This query will return database entries with the fields "id" and "name" for each matching tag. export function tag_query_with_where(maybe_where) { var query = "SELECT Tags.id, Tags.name, Tags.fullname FROM Tags LEFT JOIN TagProperties ON Tags.id=TagProperties.tagid"; // Add a clause to the WHERE to hide internal tags. var exclude_internal = ' (Tags.name="People" OR TagProperties.property IS NULL OR TagProperties.property<>"internalTag")'; var where = maybe_where ? maybe_where + ' AND' + exclude_internal : "WHERE " + exclude_internal; // If there is albums/images stuff in the where clause, we need to do a join on those tables. if(where.includes("Albums.") || where.includes("Images.")) { query = query + " LEFT JOIN ImageTags ON ImageTags.tagid=Tags.id " + " LEFT JOIN Images ON Images.id=ImageTags.imageid " + " LEFT JOIN Albums ON Albums.id=Images.album"; } query = query + " " + where + " GROUP BY Tags.id;"; return query; } export function maybe_image_query(user_query, database) { var where = false; if (user_query.image_filter) { where = "WHERE " + user_query.image_filter.to_sql_where(); } return image_query_with_where(where, database); } export function maybe_album_query(user_query, database) { var where = false; if (user_query.album_filter) { where = "WHERE " + user_query.album_filter.to_sql_where(); } return album_query_with_where(where, database); } export function maybe_tag_query(user_query) { var where = false; if (user_query.tag_filter) { where = "WHERE " + user_query.tag_filter.to_sql_where(); } return tag_query_with_where(where); } export function filter_is_const_false(filter) { if (filter instanceof ConstFilter && filter.constval === false) { return true; } // TODO resolve recursively return false; } function filter_from_text_segment(result_type, segment) { var filter = false; var name_filter; if (result_type === ResultTypeEnum.IMAGE) { // Option 1: match on image name name_filter = new MatchingFilter( result_type, segment['text'], MatchTypeEnum.MATCH_IMAGE_NAME_NATURAL ); // Option 2: match on album path (TODO: need natural matching) var album_filter = new MatchingFilter( result_type, segment['text'], MatchTypeEnum.MATCH_ALBUM_NAME_EQUALS_OR_CHILD ); filter = new LogicalOperatorFilter(result_type, name_filter, album_filter, LogicalOperatorEnum.OR); if (segment['negated']) { filter = new NegationFilter(result_type, filter); } } else if (result_type === ResultTypeEnum.ALBUM) { // TODO: We need a natural matcher for album names name_filter = new MatchingFilter( result_type, segment['text'], MatchTypeEnum.MATCH_ALBUM_NAME_EQUALS_OR_CHILD ); filter = name_filter; if (segment['negated']) { filter = new NegationFilter(result_type, filter); } } else if (result_type === ResultTypeEnum.TAG) { // TODO: We need a natural matcher for tag names name_filter = new MatchingFilter( result_type, segment['text'], MatchTypeEnum.MATCH_TAG_NAME_EQUALS_OR_CHILD ); filter = name_filter; if (segment['negated']) { filter = new NegationFilter(result_type, filter); } } return filter.simplify(); } export function user_query_from_search_string(search_string) { const parser = require('search-string'); var parsed = parser.parse(search_string); //var conditions = parsed.getParsedQuery(); var texts = parsed.getTextSegments(); var r = new UserQuery(); texts.forEach(text => { r.image_filter = new LogicalOperatorFilter(ResultTypeEnum.IMAGE, r.image_filter, filter_from_text_segment(ResultTypeEnum.IMAGE, text), LogicalOperatorEnum.AND).simplify(); r.album_filter = new LogicalOperatorFilter(ResultTypeEnum.ALBUM, r.album_filter, filter_from_text_segment(ResultTypeEnum.ALBUM, text), LogicalOperatorEnum.AND).simplify(); r.tag_filter = new LogicalOperatorFilter(ResultTypeEnum.TAG, r.tag_filter, filter_from_text_segment(ResultTypeEnum.TAG, text), LogicalOperatorEnum.AND).simplify(); }); return r; } export function user_query_from_browsed_album(album_path) { var r = new UserQuery(); r.image_filter = new MatchingFilter( ResultTypeEnum.IMAGE, album_path, MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD, false).simplify(); r.album_filter = new MatchingFilter( ResultTypeEnum.ALBUM, album_path, MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD, false).simplify(); r.tag_filter = new MatchingFilter( ResultTypeEnum.TAG, album_path, MatchTypeEnum.MATCH_ALBUM_EQUALS_OR_CHILD, false).simplify(); return r; } export function user_query_from_browsed_tag(tag_path) { var r = new UserQuery(); r.image_filter = new MatchingFilter( ResultTypeEnum.IMAGE, tag_path, MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD, false).simplify(); r.album_filter = new MatchingFilter( ResultTypeEnum.ALBUM, tag_path, MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD, false).simplify(); r.tag_filter = new MatchingFilter( ResultTypeEnum.TAG, tag_path, MatchTypeEnum.MATCH_TAG_EQUALS_OR_CHILD, false).simplify(); return r; }