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