You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

485 lines
17 KiB

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;
}