summaryrefslogtreecommitdiff
path: root/lib/VNWeb/AdvSearch.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/VNWeb/AdvSearch.pm')
-rw-r--r--lib/VNWeb/AdvSearch.pm365
1 files changed, 267 insertions, 98 deletions
diff --git a/lib/VNWeb/AdvSearch.pm b/lib/VNWeb/AdvSearch.pm
index 82030ffd..6f226b7f 100644
--- a/lib/VNWeb/AdvSearch.pm
+++ b/lib/VNWeb/AdvSearch.pm
@@ -14,12 +14,13 @@ use warnings;
use B;
use POSIX 'strftime';
use List::Util 'max';
-use TUWF;
+use TUWF ':html5_';
use VNWeb::Auth;
use VNWeb::DB;
use VNWeb::Validation;
use VNWeb::HTML ();
use VNDB::Types;
+use VNDB::ExtLinks ();
use Exporter 'import';
our @EXPORT = qw/advsearch_default/;
@@ -302,36 +303,40 @@ sub f {
$f{vndbid} = ref $v eq 'HASH' && $v->{vndbid} && !ref $v->{vndbid} && $v->{vndbid};
$f{int} = ref $f{value} && ($v->{fuzzyrdate} || $f{value}->analyze->{type} eq 'int' || $f{value}->analyze->{type} eq 'bool');
$FIELDS{$t}{$n} = \%f;
+ die "Duplicate number $num for $t\n" if $NUMFIELDS{$t}{$num};
$NUMFIELDS{$t}{$num} = $n;
}
my @TYPE; # stack of query types, $TYPE[0] is the top-level query, $TYPE[$#TYPE] the query currently being processed.
+f v => 80 => 'id', { vndbid => 'v' }, sql => sub { sql 'v.id', $_[0], \$_ };
+f v => 81 => 'search', { searchquery => 1 }, '=' => sub { $_->sql_where('v', 'v.id') };
f v => 2 => 'lang', { enum => \%LANGUAGE }, '=' => sub { sql 'v.c_languages && ARRAY', \$_, '::language[]' };
f v => 3 => 'olang', { enum => \%LANGUAGE }, '=' => sub { sql 'v.olang =', \$_ };
f v => 4 => 'platform', { enum => \%PLATFORM }, '=' => sub { sql 'v.c_platforms && ARRAY', \$_, '::platform[]' };
-f v => 6 => 'developer-id',{ vndbid => 'p' }, '=' => sub { sql 'v.c_developers && ARRAY', \$_, '::vndbid[]' };
-f v => 5 => 'length', { uint => 1, enum => \%VN_LENGTH }, '=' => sub { sql 'v.length =', \$_ };
+f v => 5 => 'length', { uint => 1, enum => \%VN_LENGTH },
+ '=' => sub { sql 'COALESCE(v.c_length BETWEEN', \$VN_LENGTH{$_}{low}, 'AND', \$VN_LENGTH{$_}{high}, ', v.length =', \$_, ')' };
f v => 7 => 'released', { fuzzyrdate => 1 }, sql => sub { sql 'v.c_released', $_[0], \($_ == 1 ? strftime('%Y%m%d', gmtime) : $_) };
-f v => 9 => 'popularity',{ uint => 1, range => [ 0, 100] }, sql => sub { sql 'v.c_popularity', $_[0], \($_*100) };
+f v => 9 => 'popularity',{ uint => 1, range => [ 0, 100] }, sql => sub { sql 'v.c_votecount', $_[0], \($_*150) }; # XXX: Deprecated
f v => 10 => 'rating', { uint => 1, range => [10, 100] }, sql => sub { sql 'v.c_rating', $_[0], \($_*10) };
-f v => 11 => 'vote-count',{ uint => 1, range => [ 0,1<<30] }, sql => sub { sql 'v.c_votecount', $_[0], \$_ };
-f v => 61 => 'has-description', { uint => 1, range => [1,1] }, '=' => sub { 'v."desc" <> \'\'' };
-f v => 62 => 'has-anime', { uint => 1, range => [1,1] }, '=' => sub { 'EXISTS(SELECT 1 FROM vn_anime va WHERE va.id = v.id)' };
-f v => 63 => 'has-screenshot', { uint => 1, range => [1,1] }, '=' => sub { 'EXISTS(SELECT 1 FROM vn_screenshots vs WHERE vs.id = v.id)' };
-f v => 64 => 'has-review', { uint => 1, range => [1,1] }, '=' => sub { 'EXISTS(SELECT 1 FROM reviews r WHERE r.vid = v.id AND NOT r.c_flagged)' };
-f v => 65 => 'on-list', { uint => 1, range => [1,1] }, '=' => sub { auth ? sql 'v.id IN(SELECT vid FROM ulist_vns WHERE uid =', \auth->uid, ')' : '1=0' };
-
-f v => 8 => 'tag', { type => 'any', func => \&_validate_tag },
- compact => sub { my $id = ($_->[0] =~ s/^g//r)*1; $_->[1] == 0 && $_->[2] == 0 ? $id : [ $id, int($_->[2]*5)*3 + $_->[1] ] },
- sql_list => \&_sql_where_tag;
+f v => 11 => 'votecount', { uint => 1, range => [ 0,1<<30] }, sql => sub { sql 'v.c_votecount', $_[0], \$_ };
+f v => 61 => 'has_description', { uint => 1, range => [1,1] }, '=' => sub { 'v.description <> \'\'' };
+f v => 62 => 'has_anime', { uint => 1, range => [1,1] }, '=' => sub { 'EXISTS(SELECT 1 FROM vn_anime va WHERE va.id = v.id)' };
+f v => 63 => 'has_screenshot', { uint => 1, range => [1,1] }, '=' => sub { 'EXISTS(SELECT 1 FROM vn_screenshots vs WHERE vs.id = v.id)' };
+f v => 64 => 'has_review', { uint => 1, range => [1,1] }, '=' => sub { 'EXISTS(SELECT 1 FROM reviews r WHERE r.vid = v.id AND NOT r.c_flagged)' };
+f v => 65 => 'on_list', { uint => 1, range => [1,1] },
+ '=' => sub { auth ? sql 'v.id IN(SELECT vid FROM ulist_vns WHERE uid =', \auth->uid, auth->api2Listread ? () : 'AND NOT c_private', ')' : '1=0' };
+f v => 66 => 'devstatus', { uint => 1, enum => \%DEVSTATUS }, '=' => sub { 'v.devstatus =', \$_ };
+
+f v => 8 => 'tag', { type => 'any', func => \&_validate_tag }, compact => \&_compact_tag, sql_list => _sql_where_tag('tags_vn_inherit');
+f v => 14 => 'dtag', { type => 'any', func => \&_validate_tag }, compact => \&_compact_tag, sql_list => _sql_where_tag('tags_vn_direct');
f v => 12 => 'label', { type => 'any', func => \&_validate_label },
compact => sub { [ ($_->[0] =~ s/^u//r)*1, $_->[1]*1 ] },
sql_list => \&_sql_where_label, sql_list_grp => sub { $_->[1] == 0 ? undef : $_->[0] };
-f v => 13 => 'anime-id', { id => 1 },
+f v => 13 => 'anime_id', { id => 1 },
sql_list => sub {
my($neg, $all, $val) = @_;
sql 'v.id', $neg ? 'NOT' : '', 'IN(SELECT id FROM vn_anime WHERE aid IN', $val, $all && @$val > 1 ? ('GROUP BY id HAVING COUNT(aid) =', \scalar @$val) : (), ')';
@@ -343,19 +348,24 @@ f v => 52 => 'staff', 's', '=' => sub {
# The "Staff" filter includes both vn_staff and vn_seiyuu. Union those tables together and filter on that.
sql 'v.id IN(SELECT vs.id
FROM (SELECT id, aid, role FROM vn_staff UNION ALL SELECT id, aid, NULL FROM vn_seiyuu) vs
- JOIN staff_alias sa ON sa.aid = vs.aid
- JOIN staff s ON s.id = sa.id
+ JOIN staff_aliast s ON s.aid = vs.aid
WHERE NOT s.hidden AND', $_, ')' };
+f v => 55 => 'developer', 'p', '=' => sub { sql 'EXISTS(SELECT 1 FROM producers p, unnest(v.c_developers) vcd(x) WHERE p.id = vcd.x AND NOT p.hidden AND', $_, ')' };
+
+# Deprecated.
+f v => 6 => 'developer-id', { vndbid => 'p' }, '=' => sub { sql 'v.c_developers && ARRAY', \$_, '::vndbid[]' };
+f r => 80 => 'id', { vndbid => 'r' }, sql => sub { sql 'r.id', $_[0], \$_ };
+f r => 81 => 'search', { searchquery => 1 }, '=' => sub { $_->sql_where('r', 'r.id') };
f r => 2 => 'lang', { enum => \%LANGUAGE },
sql_list => sub {
my($neg, $all, $val) = @_;
- sql 'r.id', $neg ? 'NOT' : '', 'IN(SELECT id FROM releases_lang WHERE NOT mtl AND lang IN', $val, $all && @$val > 1 ? ('GROUP BY id HAVING COUNT(lang) =', \scalar @$val) : (), ')';
+ sql 'r.id', $neg ? 'NOT' : '', 'IN(SELECT id FROM releases_titles WHERE NOT mtl AND lang IN', $val, $all && @$val > 1 ? ('GROUP BY id HAVING COUNT(lang) =', \scalar @$val) : (), ')';
};
-f r => 4 => 'platform', { required => 0, default => undef, enum => \%PLATFORM },
+f r => 4 => 'platform', { default => undef, enum => \%PLATFORM },
sql_list_grp => sub { defined $_ },
sql_list => sub {
my($neg, $all, $val) = @_;
@@ -363,71 +373,77 @@ f r => 4 => 'platform', { required => 0, default => undef, enum => \%PLATFORM }
sql 'r.id', $neg ? 'NOT' : '', 'IN(SELECT id FROM releases_platforms WHERE platform IN', $val, $all && @$val > 1 ? ('GROUP BY id HAVING COUNT(platform) =', \scalar @$val) : (), ')';
};
-f r => 6 => 'developer-id',{ vndbid => 'p' }, '=' => sub { sql 'r.id IN(SELECT id FROM releases_producers WHERE developer AND pid =', \$_, ')' };
-f r => 17 => 'producer-id', { vndbid => 'p' }, '=' => sub { sql 'r.id IN(SELECT id FROM releases_producers WHERE pid =', \$_, ')' };
f r => 7 => 'released', { fuzzyrdate => 1 }, sql => sub { sql 'r.released', $_[0], \($_ == 1 ? strftime('%Y%m%d', gmtime) : $_) };
f r => 8 => 'resolution', { type => 'array', length => 2, values => { uint => 1, max => 32767 } },
sql => sub { sql 'NOT r.patch AND r.reso_x', $_[0], \$_->[0], 'AND r.reso_y', $_[0], \$_->[1], $_->[0] ? 'AND r.reso_x > 0' : () };
f r => 9 => 'resolution-aspect', { type => 'array', length => 2, values => { uint => 1, max => 32767 } },
sql => sub { sql 'NOT r.patch AND r.reso_x', $_[0], \$_->[0], 'AND r.reso_y', $_[0], \$_->[1], 'AND r.reso_x*1000/GREATEST(1, r.reso_y) =', \(int ($_->[0]*1000/max(1,$_->[1]))), $_->[0] ? 'AND r.reso_x > 0' : () };
-f r => 10 => 'minage', { required => 0, default => undef, uint => 1, enum => \%AGE_RATING },
+f r => 10 => 'minage', { default => undef, uint => 1, enum => \%AGE_RATING },
sql => sub { defined $_ ? sql 'r.minage', $_[0], \$_ : $_[0] eq '=' ? 'r.minage IS NULL' : 'r.minage IS NOT NULL' };
-f r => 11 => 'medium', { required => 0, default => undef, enum => \%MEDIUM },
+f r => 11 => 'medium', { default => undef, enum => \%MEDIUM },
'=' => sub { !defined $_ ? 'NOT EXISTS(SELECT 1 FROM releases_media rm WHERE rm.id = r.id)' : sql 'EXISTS(SELECT 1 FROM releases_media rm WHERE rm.id = r.id AND rm.medium =', \$_, ')' };
-f r => 12 => 'voiced', { uint => 1, enum => \%VOICED }, '=' => sub { sql 'NOT r.patch AND r.voiced =', \$_ };
+f r => 12 => 'voiced', { default => 0, uint => 1, enum => \%VOICED }, '=' => sub { sql 'NOT r.patch AND r.voiced =', \$_ };
f r => 13 => 'animation-ero', { uint => 1, enum => \%ANIMATED }, '=' => sub { sql 'NOT r.patch AND r.ani_ero =', \$_ };
f r => 14 => 'animation-story', { uint => 1, enum => \%ANIMATED }, '=' => sub { sql 'NOT r.patch AND r.ani_story =', \$_ };
-f r => 15 => 'engine', { required => 0, default => '' }, '=' => sub { sql 'r.engine =', \$_ };
-f r => 16 => 'rtype', { enum => \%RELEASE_TYPE }, '=' => sub { sql 'r.type =', \$_ };
+f r => 15 => 'engine', { default => '' }, '=' => sub { sql 'r.engine =', \$_ };
+f r => 16 => 'rtype', { enum => \%RELEASE_TYPE }, '=' => sub { $#TYPE && $TYPE[$#TYPE-1] eq 'v' ? sql 'rv.rtype =', \$_ : sql 'r.id IN(SELECT id FROM releases_vn WHERE rtype =', \$_, ')' };
f r => 18 => 'rlist', { uint => 1, enum => \%RLIST_STATUS }, sql_list => sub {
my($neg, $all, $val) = @_;
return '1=0' if !auth;
sql 'r.id', $neg ? 'NOT' : '', 'IN(SELECT rid FROM rlists WHERE uid =', \auth->uid, 'AND status IN', $val, $all && @$val > 1 ? ('GROUP BY rid HAVING COUNT(status) =', \scalar @$val) : (), ')';
};
+f r => 19 => 'extlink', _extlink_filter('r');
+f r => 20 => 'drm', { default => '' }, '=' => sub { sql 'EXISTS(SELECT 1 FROM drm JOIN releases_drm rd ON rd.drm = drm.id WHERE drm.name =', \$_, 'AND rd.id = r.id)' };
f r => 61 => 'patch', { uint => 1, range => [1,1] }, '=' => sub { 'r.patch' };
f r => 62 => 'freeware', { uint => 1, range => [1,1] }, '=' => sub { 'r.freeware' };
-# TODO: This flag has been deprecated, replace with a subquery that checks the publisher instead?
-f r => 63 => 'doujin', { uint => 1, range => [1,1] }, '=' => sub { 'r.doujin' };
f r => 64 => 'uncensored',{uint => 1, range => [1,1] }, '=' => sub { 'r.uncensored' };
f r => 65 => 'official', { uint => 1, range => [1,1] }, '=' => sub { 'r.official' };
-f r => 53 => 'vn', 'v', '=' => sub { sql 'r.id IN(SELECT rv.id FROM releases_vn rv JOIN vn v ON v.id = rv.vid WHERE NOT v.hidden AND', $_, ')' };
+f r => 66 => 'has_ero', { uint => 1, range => [1,1] }, '=' => sub { 'r.has_ero' };
+f r => 53 => 'vn', 'v', '=' => sub { sql 'r.id IN(SELECT rv.id FROM releases_vn rv JOIN vn v ON v.id = rv.vid WHERE NOT v.hidden AND', $_, ')' };
+f r => 55 => 'producer', 'p', '=' => sub { sql 'r.id IN(SELECT rp.id FROM releases_producers rp JOIN producers p ON p.id = rp.pid WHERE NOT p.hidden AND', $_, ')' };
+
+# Deprecated.
+f r => 6 => 'developer-id',{ vndbid => 'p' }, '=' => sub { sql 'r.id IN(SELECT id FROM releases_producers WHERE developer AND pid =', \$_, ')' }; # Does not have a new equivalent
+f r => 17 => 'producer-id', { vndbid => 'p' }, '=' => sub { sql 'r.id IN(SELECT id FROM releases_producers WHERE pid =', \$_, ')' };
+f r => 63 => 'doujin', { uint => 1, range => [1,1] }, '=' => sub { 'r.doujin' }; # Not recognized by Elm anymore.
+f c => 80 => 'id', { vndbid => 'c' }, sql => sub { sql 'c.id', $_[0], \$_ };
+f c => 81 => 'search', { searchquery => 1 }, '=' => sub { $_->sql_where('c', 'c.id') };
f c => 2 => 'role', { enum => \%CHAR_ROLE }, '=' => sub { $#TYPE && $TYPE[$#TYPE-1] eq 'v' ? sql 'cv.role =', \$_ : sql 'c.id IN(SELECT id FROM chars_vns WHERE role =', \$_, ')' };
-f c => 3 => 'blood-type', { enum => \%BLOOD_TYPE }, '=' => sub { sql 'c.bloodt =', \$_ };
+f c => 3 => 'blood_type', { enum => \%BLOOD_TYPE }, '=' => sub { sql 'c.bloodt =', \$_ };
f c => 4 => 'sex', { enum => \%GENDER }, '=' => sub { sql 'c.gender =', \$_ };
-f c => 5 => 'sex-spoil', { enum => \%GENDER }, '=' => sub { sql '(c.gender =', \$_, 'AND c.spoil_gender IS NULL) OR c.spoil_gender IS NOT DISTINCT FROM', \$_ };
-f c => 6 => 'height', { required => 0, default => undef, uint => 1, max => 32767 },
+f c => 5 => 'sex_spoil', { enum => \%GENDER }, '=' => sub { sql '(c.gender =', \$_, 'AND c.spoil_gender IS NULL) OR c.spoil_gender IS NOT DISTINCT FROM', \$_ };
+f c => 6 => 'height', { default => undef, uint => 1, max => 32767 },
sql => sub { !defined $_ ? sql 'c.height', $_[0], 0 : sql 'c.height <> 0 AND c.height', $_[0], \$_ };
-f c => 7 => 'weight', { required => 0, default => undef, uint => 1, max => 32767 },
+f c => 7 => 'weight', { default => undef, uint => 1, max => 32767 },
sql => sub { !defined $_ ? sql('c.weight IS', $_[0] eq '=' ? '' : 'NOT', 'NULL') : sql 'c.weight', $_[0], \$_ };
-f c => 8 => 'bust', { required => 0, default => undef, uint => 1, max => 32767 },
+f c => 8 => 'bust', { default => undef, uint => 1, max => 32767 },
sql => sub { !defined $_ ? sql 'c.s_bust', $_[0], 0 : sql 'c.s_bust <> 0 AND c.s_bust', $_[0], \$_ };
-f c => 9 => 'waist', { required => 0, default => undef, uint => 1, max => 32767 },
+f c => 9 => 'waist', { default => undef, uint => 1, max => 32767 },
sql => sub { !defined $_ ? sql 'c.s_waist', $_[0], 0 : sql 'c.s_waist <> 0 AND c.s_waist', $_[0], \$_ };
-f c => 10 => 'hips', { required => 0, default => undef, uint => 1, max => 32767 },
+f c => 10 => 'hips', { default => undef, uint => 1, max => 32767 },
sql => sub { !defined $_ ? sql 'c.s_hip', $_[0], 0 : sql 'c.s_hip <> 0 AND c.s_hip', $_[0], \$_ };
-f c => 11 => 'cup', { required => 0, default => undef, enum => \%CUP_SIZE },
+f c => 11 => 'cup', { default => undef, enum => \%CUP_SIZE },
sql => sub { !defined $_ ? sql 'c.cup_size', $_[0], "''" : sql 'c.cup_size <> \'\' AND c.cup_size', $_[0], \$_ };
-f c => 12 => 'age', { required => 0, default => undef, uint => 1, max => 32767 },
+f c => 12 => 'age', { default => undef, uint => 1, max => 32767 },
sql => sub { !defined $_ ? sql('c.age IS', $_[0] eq '=' ? '' : 'NOT', 'NULL') : sql 'c.age', $_[0], \$_ };
-f c => 13 => 'trait', { type => 'any', func => \&_validate_trait },
- compact => sub { my $id = ($_->[0] =~ s/^i//r)*1; $_->[1] == 0 ? $id : [ $id, int $_->[1] ] },
- sql_list => \&_sql_where_trait;
-f c => 14 => 'birthday', { type => 'array', length => 2, values => { uint => 1, max => 31 } },
+f c => 13 => 'trait', { type => 'any', func => \&_validate_trait }, compact => \&_compact_trait, sql_list => _sql_where_trait('traits_chars', 'cid');
+f c => 15 => 'dtrait', { type => 'any', func => \&_validate_trait }, compact => \&_compact_trait, sql_list => _sql_where_trait('chars_traits', 'id');
+f c => 14 => 'birthday', { default => [0,0], type => 'array', length => 2, values => { uint => 1, max => 31 } },
'=' => sub { sql 'c.b_month =', \$_->[0], $_->[1] ? ('AND c.b_day =', \$_->[1]) : () };
# XXX: When this field is nested inside a VN query, it may match seiyuu linked to other VNs.
# This can be trivially fixed by adding an (AND vs.id = v.id) clause, but that results in extremely slow queries that I've no clue how to optimize.
-f c => 52 => 'seiyuu', 's', '=' => sub { sql 'c.id IN(SELECT vs.cid FROM vn_seiyuu vs JOIN staff_alias sa ON sa.aid = vs.aid JOIN staff s ON s.id = sa.id WHERE NOT s.hidden AND', $_, ')' };
+f c => 52 => 'seiyuu', 's', '=' => sub { sql 'c.id IN(SELECT vs.cid FROM vn_seiyuu vs JOIN staff_aliast s ON s.aid = vs.aid WHERE NOT s.hidden AND', $_, ')' };
f c => 53 => 'vn', 'v', '=' => sub { sql 'c.id IN(SELECT cv.id FROM chars_vns cv JOIN vn v ON v.id = cv.vid WHERE NOT v.hidden AND', $_, ')' };
-# Staff filters need both 'staff s' and 'staff_alias sa' - aliases are treated as separate rows.
+# Staff filters need 'staff_aliast s', aliases are treated as separate rows.
f s => 2 => 'lang', { enum => \%LANGUAGE }, '=' => sub { sql 's.lang =', \$_ };
-f s => 3 => 'id', { vndbid => 's' }, '=' => sub { sql 's.id = ', \$_ };
+f s => 3 => 'id', { vndbid => 's' }, sql => sub { sql 's.id', $_[0], \$_ };
f s => 4 => 'gender', { enum => \%GENDER }, '=' => sub { sql 's.gender =', \$_ };
f s => 5 => 'role', { enum => [ 'seiyuu', keys %CREDIT_TYPE ] },
sql_list_grp => sub { $_ eq 'seiyuu' ? undef : '' },
@@ -437,18 +453,94 @@ f s => 5 => 'role', { enum => [ 'seiyuu', keys %CREDIT_TYPE ] },
if($#TYPE && $TYPE[$#TYPE-1] eq 'v') {
# Shortcut referencing the vn_staff table we're already querying
return $val->[0] eq 'seiyuu' ? 'vs.role IS NULL' : sql 'vs.role IN', $val if !@grp && !$neg;
- return sql $neg ? 'NOT' : '', 'EXISTS(SELECT 1 FROM vn_seiyuu vs WHERE vs.id = v.id AND vs.aid = sa.aid)' if $val->[0] eq 'seiyuu';
- sql 'sa.aid', $neg ? 'NOT' : '', 'IN(SELECT vs.aid FROM vn_staff vs WHERE vs.id = v.id AND vs.role IN', $val, @grp, ')';
+ return sql $neg ? 'NOT' : '', 'EXISTS(SELECT 1 FROM vn_seiyuu vs WHERE vs.id = v.id AND vs.aid = s.aid)' if $val->[0] eq 'seiyuu';
+ sql 's.aid', $neg ? 'NOT' : '', 'IN(SELECT vs.aid FROM vn_staff vs WHERE vs.id = v.id AND vs.role IN', $val, @grp, ')';
} else {
- return sql $neg ? 'NOT' : '', 'EXISTS(SELECT 1 FROM vn_seiyuu vs JOIN vn v ON v.id = vs.id WHERE NOT v.hidden AND vs.aid = sa.aid)' if $val->[0] eq 'seiyuu';
- sql 'sa.aid', $neg ? 'NOT' : '', 'IN(SELECT vs.aid FROM vn_staff vs JOIN vn v ON v.id = vs.id WHERE NOT v.hidden AND vs.role IN', $val, @grp, ')';
+ return sql $neg ? 'NOT' : '', 'EXISTS(SELECT 1 FROM vn_seiyuu vs JOIN vn v ON v.id = vs.id WHERE NOT v.hidden AND vs.aid = s.aid)' if $val->[0] eq 'seiyuu';
+ sql 's.aid', $neg ? 'NOT' : '', 'IN(SELECT vs.aid FROM vn_staff vs JOIN vn v ON v.id = vs.id WHERE NOT v.hidden AND vs.role IN', $val, @grp, ')';
}
};
+f s => 6 => 'extlink', _extlink_filter('s');
+f s => 61 => 'ismain', { uint => 1, range => [1,1] }, '=' => sub { 's.aid = s.main' };
+f s => 80 => 'search', { searchquery => 1 }, '=' => sub { $_->sql_where('s', 's.id', 's.aid') };
+f s => 81 => 'aid', { id => 1 }, '=' => sub { sql 's.aid =', \$_ };
+
+f p => 2 => 'lang', { enum => \%LANGUAGE }, '=' => sub { sql 'p.lang =', \$_ };
+f p => 3 => 'id', { vndbid => 'p' }, sql => sub { sql 'p.id', $_[0], \$_ };
+f p => 4 => 'type', { enum => \%PRODUCER_TYPE }, '=' => sub { sql 'p.type =', \$_ };
+f p => 80 => 'search', { searchquery => 1 }, '=' => sub { $_->sql_where('p', 'p.id') };
+
+
+f g => 2 => 'id', { vndbid => 'g' }, sql => sub { sql 't.id', $_[0], \$_ };
+f g => 3 => 'category', { enum => \%TAG_CATEGORY }, '=' => sub { sql 't.cat =', \$_ };
+f g => 80 => 'search', { searchquery => 1 }, '=' => sub { $_->sql_where('g', 't.id') };
+
+
+f i => 2 => 'id', { vndbid => 'i' }, sql => sub { sql 't.id', $_[0], \$_ };
+f i => 80 => 'search', { searchquery => 1 }, '=' => sub { $_->sql_where('i', 't.id') };
+
+
+
+# 'extlink' filter accepts the following values:
+# - $name - Whether the entry has a link of site $name
+# - [ $name, $val ] - Whether the entry has a link of site $name with the given $val
+# - "$name,$val" - Compact version of above (not really *compact* by any means, but this filter isn't common anyway)
+# - "http://..." - Auto-detect version of [$name,$val]
+# TODO: This only handles links defined in %LINKS, but it would be nice to also support links from Wikidata & PlayAsia.
+sub _extlink_filter {
+ my($type) = @_;
+ my $schema = (grep +($_->{dbentry_type}||'') eq $type, values VNDB::Schema::schema->%*)[0];
+ my %links = map {
+ my $n = $_;
+ my $l = $VNDB::ExtLinks::LINKS{$type}{$n};
+ my $s = (grep $_->{name} eq $n, $schema->{cols}->@*)[0];
+ (s/^l_//r, +{ %$l,
+ _col => $n,
+ _schema => $s,
+ _regex => $l->{regex} && VNDB::ExtLinks::full_regex($l->{regex}),
+ _hasval => $s->{type} =~ /\[\]/ ? "<> '{}'" : $s->{decl} !~ /not\s+null/i ? 'is not null' : $s->{type} =~ /^(big)?int/i ? '<> 0' : "<> ''"
+ })
+ } keys $VNDB::ExtLinks::LINKS{$type}->%*;
+
+ my sub _val {
+ return 1 if !ref $_[0] && $links{$_[0]}; # just $name
+ if(!ref $_[0] && $_[0] =~ /^https?:/) { # URL
+ for (keys %links) {
+ if($links{$_}{_regex} && $_[0] =~ $links{$_}{_regex}) {
+ $_[0] = [ $_, $1 ];
+ last;
+ }
+ }
+ return { msg => 'Unrecognized URL format' } if !ref $_[0];
+ }
+ $_[0] = [ split /,/, $_[0], 2 ] if !ref $_[0]; # compact $name,$val form
+
+ # normalized $name,$val form
+ return 0 if ref $_[0] ne 'ARRAY' || $_[0]->@* != 2 || ref $_[0][0] || ref $_[0][1] || !defined $_[0][1];
+ my $l = $links{$_[0][0]};
+ return { msg => "Unknown field '$_[0][0]'" } if !$l;
+ return { msg => "Invalid value '$_[0][1]'" } if $l->{_schema}{type} =~ /^int/i && ($_[0][1] !~ /^-?[0-9]+$/ || $_[0][1] >= (1<<31) || $_[0][1] <= -(1<<31));
+ return { msg => "Invalid value '$_[0][1]'" } if $l->{_schema}{type} =~ /^bigint/i && ($_[0][1] !~ /^-?[0-9]+$/ || $_[0][1] >= (1<<63) || $_[0][1] <= -(1<<63));
+ $_[0][1] *= 1 if $l->{_schema}{type} =~ /^(big)?int/i;
+ 1
+ }
+ my sub _sql {
+ return "$type.$links{$_}{_col} $links{$_}{_hasval}" if !ref; # just name
+ my($l, $v) = ($links{$_->[0]}, $_->[1]);
+ sql "$type.$l->{_col}", $l->{_schema}{type} =~ /\[\]/ ? ('&& ARRAY[', \$v, ']::', $l->{_schema}{type}) : ('=', \$v);
+ }
+ my sub _comp { ref $_ ? $_->[0].','.(my $x=$_->[1]) : $_ }
+ ({ type => 'any', func => \&_val }, '=' => \&_sql, compact => \&_comp)
+}
-
-# Accepts either $tag or [$tag, int($minlevel*5)*3+$maxspoil] (for compact form) or [$tag, $maxspoil, $minlevel]. Normalizes to the latter.
+# Accepts either:
+# - $tag
+# - [$tag, $exclude_lies*16*3 + int($minlevel*5)*3 + $maxspoil] (compact form)
+# - [$tag, $maxspoil, $minlevel]
+# - [$tag, $maxspoil, $minlevel, $exclude_lies]
+# Normalizes to the latter two.
sub _validate_tag {
$_[0] = [$_[0],0,0] if ref $_[0] ne 'ARRAY'; # just a tag id
my $v = tuwf->compile({ vndbid => 'g' })->validate($_[0][0]);
@@ -456,29 +548,51 @@ sub _validate_tag {
$_[0][0] = $v->data;
if($_[0]->@* == 2) { # compact form
return 0 if !defined $_[0][1] || ref $_[0][1] || $_[0][1] !~ /^[0-9]+$/;
- ($_[0][1],$_[0][2]) = ($_[0][1]%3, int($_[0][1]/3)/5);
+ ($_[0][1],$_[0][2],$_[0][3]) = ($_[0][1]%3, int($_[0][1]%(3*16)/3)/5, int($_[0][1]/3/16) == 1 ? 1 : 0);
}
# normalized form
- return 0 if $_[0]->@* != 3;
+ return 0 if $_[0]->@* < 3 || $_[0]->@* > 4;
return 0 if !defined $_[0][1] || ref $_[0][1] || $_[0][1] !~ /^[0-2]$/;
return 0 if !defined $_[0][2] || ref $_[0][2] || $_[0][2] !~ /^(?:[0-2](?:\.[0-9]+)?|3(?:\.0+)?)$/;
+ $_[0][1] *= 1;
+ $_[0][2] *= 1;
+ if ($_[0]->@* == 4) {
+ return 0 if !defined $_[0][3] || ref $_[0][3] || $_[0][3] !~ /^[0-1]$/;
+ $_[0][3] *= 1;
+ pop $_[0]->@* if !$_[0][3];
+ }
1
}
+sub _compact_tag { my $id = ($_->[0] =~ s/^g//r)*1; $_->[1] == 0 && $_->[2] == 0 && !$_->[3] ? $id : [ $id, ($_->[3]?16*3:0) + int($_->[2]*5)*3 + $_->[1] ] }
+sub _compact_trait { my $id = ($_->[0] =~ s/^i//r)*1; $_->[1] == 0 && !$_->[2] ? $id : [ $id, ($_->[2]?3:0) + $_->[1] ] }
-# Accepts either $trait or [$trait, $maxspoil]. Normalizes to the latter.
+# Accepts either:
+# - $trait
+# - [$trait, $exclude_lies*3 + $maxspoil] (compact form)
+# - [$trait, $maxspoil]
+# - [$trait, $maxspoil, $exclude_lies]
+# Normalizes to the latter two.
sub _validate_trait {
$_[0] = [$_[0],0] if ref $_[0] ne 'ARRAY'; # just a trait id
my $v = tuwf->compile({ vndbid => 'i' })->validate($_[0][0]);
return 0 if $v->err;
$_[0][0] = $v->data;
- $_[0]->@* == 2 && defined $_[0][1] && !ref $_[0][1] && $_[0][1] =~ /^[0-2]$/
+ return 0 if !defined $_[0][1] || ref $_[0][1] || $_[0][1] !~ /^[0-9]+$/;
+ ($_[0][1], $_[0][2]) = ($_[0][1]%3, int($_[0][1]/3) == 1 ? 1 : 0) if $_[0]->@* == 2;
+ return 0 if $_[0]->@* != 3;
+ return 0 if $_[0][1] > 2;
+ return 0 if !defined $_[0][2] || ref $_[0][2] || $_[0][2] !~ /^[01]$/;
+ $_[0][1] *= 1;
+ $_[0][2] *= 1;
+ pop $_[0]->@* if $_[0]->@* == 3 && !$_[0][2];
+ 1
}
# Accepts either $label or [$uid, $label]. Normalizes to the latter. $label=0 is used for 'Unlabeled'.
sub _validate_label {
- $_[0] = [auth->uid(), $_[0]] if ref $_[0] ne 'ARRAY';
+ $_[0] = [tuwf->req->{advsearch_uid}||auth->uid(), $_[0]] if ref $_[0] ne 'ARRAY';
my $v = tuwf->compile({ vndbid => 'u' })->validate($_[0][0]);
return 0 if $v->err;
$_[0][0] = $v->data;
@@ -524,25 +638,27 @@ sub _validate_adv {
return { msg => 'Invalid compact encoded form', character_index => $i } if !($_[0] = _dec_query($v, \$i));
return { msg => 'Trailing garbage' } if $i != length $v;
}
+ if(ref $_[0] eq 'ARRAY' && $_[0]->@* == 0) {
+ $_[0] = bless {type=>$t}, __PACKAGE__;
+ return 1;
+ }
my $v = _validate($t, @_);
$_[0] = bless { type => $t, query => $_[0] }, __PACKAGE__ if $v;
$v
}
+
# 'advsearch' validation, accepts either a compact encoded string, JSON string or an already decoded array.
-TUWF::set('custom_validations')->{advsearch} = sub { my($t) = @_; +{ required => 0, type => 'any', default => bless({type=>$t}, __PACKAGE__), func => sub { _validate_adv $t, @_ } } };
+TUWF::set('custom_validations')->{advsearch} = sub { my($t) = @_; +{ type => 'any', default => bless({type=>$t}, __PACKAGE__), func => sub { _validate_adv $t, @_ } } };
# 'advsearch_err' validation; Same as the 'advsearch' validation except it never throws an error.
# If the validation failed, this will log a warning and return an empty query that will cause elm_() to display a warning message.
TUWF::set('custom_validations')->{advsearch_err} = sub {
my ($t) = @_;
- +{ required => 0, type => 'any', default => bless({type=>$t}, __PACKAGE__), func => sub {
+ +{ type => 'any', default => bless({type=>$t}, __PACKAGE__), func => sub {
my $r = _validate_adv $t, @_;
- if(!$r || ref $r eq 'HASH') {
- warn "advsearch validation failed\n";
- $_[0] = bless {type=>$t,error=>1}, __PACKAGE__;
- }
+ $_[0] = bless {type=>$t,error=>1}, __PACKAGE__ if !$r || ref $r eq 'HASH';
1
} }
};
@@ -573,34 +689,46 @@ sub _canon {
}
-# sql_list function for tags
+# returns an sql_list function for tags
sub _sql_where_tag {
- my($neg, $all, $val) = @_;
- my %f; # spoiler -> rating -> list
- my @l;
- push $f{$_->[1]}{$_->[2]}->@*, $_->[0] for @$val;
- for my $s (keys %f) {
- for my $r (keys $f{$s}->%*) {
- push @l, sql_and
- $s < 2 ? sql('spoiler <=', \$s) : (),
- $r > 0 ? sql('rating >=', \$r) : (),
- sql('tag IN', $f{$s}{$r});
+ my($table) = @_;
+ sub {
+ my($neg, $all, $val) = @_;
+ my %f; # spoiler -> rating -> lie -> list
+ my @l;
+ push $f{$_->[1]*1}{$_->[2]*1}{$_->[3]?1:''}->@*, $_->[0] for @$val;
+ for my $s (keys %f) {
+ for my $r (keys $f{$s}->%*) {
+ for my $l (keys $f{$s}{$r}->%*) {
+ push @l, sql_and
+ $s < 2 ? sql('spoiler <=', \$s) : (),
+ $r > 0 ? sql('rating >=', \$r) : (),
+ $l ? ('NOT lie') : (),
+ sql('tag IN', $f{$s}{$r}{$l});
+ }
+ }
}
+ sql 'v.id', $neg ? 'NOT' : (), 'IN(SELECT vid FROM', $table, 'WHERE', sql_or(@l), $all && @$val > 1 ? ('GROUP BY vid HAVING COUNT(tag) =', \scalar @$val) : (), ')'
}
- sql 'v.id', $neg ? 'NOT' : (), 'IN(SELECT vid FROM tags_vn_inherit WHERE', sql_or(@l), $all && @$val > 1 ? ('GROUP BY vid HAVING COUNT(tag) =', \scalar @$val) : (), ')'
}
sub _sql_where_trait {
- my($neg, $all, $val) = @_;
- my %f; # spoiler -> list
- my @l;
- push $f{$_->[1]}->@*, $_->[0] for @$val;
- for my $s (keys %f) {
- push @l, sql_and
- $s < 2 ? sql('spoil <=', \$s) : (),
- sql('tid IN', $f{$s});
+ my($table, $cid) = @_;
+ sub {
+ my($neg, $all, $val) = @_;
+ my %f; # spoiler -> list
+ my @l;
+ push $f{$_->[1]*1}{$_->[2]?1:''}->@*, $_->[0] for @$val;
+ for my $s (keys %f) {
+ for my $l (keys $f{$s}->%*) {
+ push @l, sql_and
+ $s < 2 ? sql('spoil <=', \$s) : (),
+ $l ? ('NOT lie') : (),
+ sql('tid IN', $f{$s}{$l});
+ }
+ }
+ sql 'c.id', $neg ? 'NOT' : (), 'IN(SELECT', $cid, 'FROM', $table, 'WHERE', sql_or(@l), $all && @$val > 1 ? ('GROUP BY', $cid, 'HAVING COUNT(tid) =', \scalar @$val) : (), ')'
}
- sql 'c.id', $neg ? 'NOT' : (), 'IN(SELECT cid FROM traits_chars WHERE', sql_or(@l), $all && @$val > 1 ? ('GROUP BY cid HAVING COUNT(tid) =', \scalar @$val) : (), ')'
}
@@ -608,27 +736,32 @@ sub _sql_where_trait {
sub _sql_where_label {
my($neg, $all, $val) = @_;
my $uid = $val->[0][0];
+ require VNWeb::ULists::Lib;
my $own = VNWeb::ULists::Lib::ulists_own($uid);
my @lbl = map $_->[1], @$val;
# Unlabeled
if($lbl[0] == 0) {
return '1=0' if !$own;
- my $onlist = sql 'EXISTS(SELECT 1 FROM ulist_vns WHERE vid = v.id AND uid =', \$uid, ')';
- my $haslbl = sql 'EXISTS(SELECT 1 FROM ulist_vns_labels WHERE vid = v.id AND uid =', \$uid, 'AND lbl <>', \7, ')';
- return $neg ? sql 'NOT', $onlist, 'OR', $haslbl
- : sql $onlist,' AND NOT', $haslbl;
+ return sql $neg ? 'NOT' : (), 'EXISTS(SELECT 1 FROM ulist_vns WHERE vid = v.id AND uid =', \$uid, "AND labels IN('{}','{7}'))";
}
- # Simple, stupid and safe: Don't attempt to query anything if there's a private label.
- # This can be improved to allow querying/displaying results that *are* visible, but it's more complex and not that often needed.
if(!$own) {
- tuwf->req->{lblvis}{$uid} ||= { map +($_->{id},1), tuwf->dbAlli('SELECT id FROM ulist_labels WHERE NOT private AND uid =', \$uid)->@* };
+ # Label 7 can always be queried, do a lookup for the rest.
+ tuwf->req->{lblvis}{$uid} ||= { 7, 1, map +($_->{id},1), tuwf->dbAlli('SELECT id FROM ulist_labels WHERE NOT private AND uid =', \$uid)->@* };
my $vis = tuwf->req->{lblvis}{$uid};
- return '1=0' if grep !$vis->{$_}, @lbl;
+ return $neg ? '1=1' : '1=0' if $all && grep !$vis->{$_}, @lbl; # AND query but one label is private -> no match
+ @lbl = grep $vis->{$_}, @lbl;
+ return $neg ? '1=1' : '1=0' if !@lbl; # All requested labels are private -> no match
}
- sql 'v.id', $neg ? 'NOT' : (), 'IN(SELECT vid FROM ulist_vns_labels WHERE uid =', \$uid, 'AND lbl IN', \@lbl, $all && @lbl > 1 ? ('GROUP BY vid HAVING COUNT(lbl) =', \scalar @lbl) : (), ')'
+ sql 'v.id', $neg ? 'NOT' : (), 'IN(
+ SELECT vid
+ FROM ulist_vns
+ WHERE uid =', \$uid,
+ 'AND labels', $all ? '@>' : '&&', sql_array(@lbl), '::smallint[]',
+ $own ? () : 'AND NOT c_private',
+ ')'
}
@@ -714,7 +847,7 @@ sub _extract_ids {
} else {
my $f = $FIELDS{$t}{$q->[0]};
$ids->{$q->[2]} = 1 if $f->{vndbid};
- $ids->{"anime$q->[2]"} = 1 if $q->[0] eq 'anime-id';
+ $ids->{"anime$q->[2]"} = 1 if $q->[0] eq 'anime_id';
$ids->{$q->[2][0]} = 1 if ref $f->{value} && ref $q->[2] eq 'ARRAY'; # Ugly heuristic, may have false positives
_extract_ids($f->{value}, $q->[2], $ids) if !ref $f->{value};
}
@@ -729,17 +862,17 @@ sub elm_search_query {
_extract_ids($self->{type}, $self->{query}, \%ids) if $self->{query};
$o{producers} = [ map +{id => $_}, grep /^p/, keys %ids ];
- enrich_merge id => 'SELECT id, name, original, hidden FROM producers WHERE id IN', $o{producers};
+ enrich_merge id => sql('SELECT id, title[1+1] AS name, title[1+1+1+1] AS altname FROM', VNWeb::TitlePrefs::producerst(), 'p WHERE id IN'), $o{producers};
$o{staff} = [ map +{id => $_}, grep /^s/, keys %ids ];
- enrich_merge id => 'SELECT s.id, sa.aid, sa.name, sa.original FROM staff s JOIN staff_alias sa ON sa.aid = s.aid WHERE s.id IN', $o{staff};
+ enrich_merge id => sql('SELECT id, lang, aid, title[1+1], title[1+1+1+1] AS alttitle FROM', VNWeb::TitlePrefs::staff_aliast(), 's WHERE aid = main AND id IN'), $o{staff};
$o{tags} = [ map +{id => $_}, grep /^g/, keys %ids ];
enrich_merge id => 'SELECT id, name, searchable, applicable, hidden, locked FROM tags WHERE id IN', $o{tags};
$o{traits} = [ map +{id => $_}, grep /^i/, keys %ids ];
enrich_merge id => 'SELECT t.id, t.name, t.searchable, t.applicable, t.defaultspoil, t.hidden, t.locked, g.id AS group_id, g.name AS group_name
- FROM traits t LEFT JOIN traits g ON g.id = t.group WHERE t.id IN', $o{traits};
+ FROM traits t LEFT JOIN traits g ON g.id = t.gid WHERE t.id IN', $o{traits};
$o{anime} = [ map +{id => $_=~s/^anime//rg}, grep /^anime/, keys %ids ];
enrich_merge id => 'SELECT id, title_romaji AS title, title_kanji AS original FROM anime WHERE id IN', $o{anime};
@@ -751,11 +884,11 @@ sub elm_search_query {
sub elm_ {
- my($self) = @_;
+ my($self, $count, $time) = @_;
# TODO: labels can be lazily loaded to reduce page weight
state $schema ||= tuwf->compile({ type => 'hash', keys => {
- uid => { vndbid => 'u', required => 0 },
+ uid => { vndbid => 'u', default => undef },
labels => { aoh => { id => { uint => 1 }, label => {} } },
defaultSpoil => { uint => 1 },
saved => { aoh => { name => {}, query => {} } },
@@ -770,6 +903,21 @@ sub elm_ {
error => $self->{error}?1:0,
query => $self->elm_search_query(),
};
+
+ if (@_ > 1) {
+ p_ class => 'center', sub {
+ input_ type => 'submit', value => 'Search';
+ txt_ sprintf ' %d result%s in %.3fs', $count, $count == 1 ? '' : 's', $time if defined $count;
+ };
+ div_ class => 'warning', sub {
+ h2_ 'ERROR: Query timed out.';
+ p_ q{
+ This usually happens when your combination of filters is too complex for the server to handle.
+ This may also happen when the server is overloaded with other work, but that's much less common.
+ You can adjust your filters or try again later.
+ };
+ } if !defined $count;
+ }
}
@@ -781,6 +929,27 @@ sub query_encode {
}
+sub extract_searchquery {
+ my($self) = @_;
+ my $q = $self->{query};
+ return ($self) if !$q;
+ return (bless({type => $self->{type}}, __PACKAGE__), $q->[2]) if @$q == 3 && $q->[1] eq '=' && ref $q->[2] eq 'VNWeb::Validate::SearchQuery';
+ if($q->[0] eq 'and') {
+ my(@newq, $s);
+ for (@{$q}[1..$#$q]) {
+ if(@$_ == 3 && $_->[1] eq '=' && ref $_->[2] eq 'VNWeb::Validate::SearchQuery') {
+ return ($self) if $s;
+ $s = $_->[2];
+ } else {
+ push @newq, $_;
+ }
+ }
+ return (bless({type => $self->{type}, query => ['and',@newq]}, __PACKAGE__), $s) if $s;
+ }
+ return ($self);
+}
+
+
# Returns the saved default query for the current user, or an empty query if none has been set.
sub advsearch_default {
my($t) = @_;