summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoryorhel <yorhel@1fe2e327-d9db-4752-bcf7-ef0cb4a1748b>2008-04-13 13:45:20 +0000
committeryorhel <yorhel@1fe2e327-d9db-4752-bcf7-ef0cb4a1748b>2008-04-13 13:45:20 +0000
commitd7046f5d38004ff20739798c18f5796c31676546 (patch)
tree1639e6a8c3b74588bff7be6aaf6cf5e04e3bc63f
W00t, VNDB on SVN!
git-svn-id: svn://vndb.org/vndb@1 1fe2e327-d9db-4752-bcf7-ef0cb4a1748b
-rw-r--r--data/tpl/defs.pl482
-rw-r--r--data/tpl/docs298
-rw-r--r--data/tpl/error45
-rw-r--r--data/tpl/faq75
-rw-r--r--data/tpl/hist103
-rw-r--r--data/tpl/home67
-rw-r--r--data/tpl/main14
-rw-r--r--data/tpl/myvotes30
-rw-r--r--data/tpl/page140
-rw-r--r--data/tpl/pbrowse45
-rw-r--r--data/tpl/pedit45
-rw-r--r--data/tpl/ppage58
-rw-r--r--data/tpl/redit70
-rw-r--r--data/tpl/rpage61
-rw-r--r--data/tpl/useredit34
-rw-r--r--data/tpl/userlist54
-rw-r--r--data/tpl/userlogin14
-rw-r--r--data/tpl/userpage13
-rw-r--r--data/tpl/userpass21
-rw-r--r--data/tpl/userreg38
-rw-r--r--data/tpl/vnbrowse87
-rw-r--r--data/tpl/vnedit94
-rw-r--r--data/tpl/vnlist74
-rw-r--r--data/tpl/vnpage171
-rw-r--r--data/tpl/vnpage_rel51
-rw-r--r--data/tpl/vnpage_rg11
-rw-r--r--data/tpl/vnpage_stats68
-rw-r--r--lib/ChangeLog215
-rw-r--r--lib/VNDB.pm338
-rw-r--r--lib/VNDB/HomePages.pm286
-rw-r--r--lib/VNDB/Producers.pm188
-rw-r--r--lib/VNDB/Releases.pm178
-rw-r--r--lib/VNDB/Users.pm230
-rw-r--r--lib/VNDB/Util/Auth.pm131
-rw-r--r--lib/VNDB/Util/DB.pm1268
-rw-r--r--lib/VNDB/Util/Request.pm46
-rw-r--r--lib/VNDB/Util/Response.pm238
-rw-r--r--lib/VNDB/Util/Template.pm235
-rw-r--r--lib/VNDB/Util/Tools.pm145
-rw-r--r--lib/VNDB/VN.pm380
-rw-r--r--lib/VNDB/VNLists.pm96
-rw-r--r--lib/VNDB/Votes.pm61
-rw-r--r--lib/global.pl569
-rw-r--r--static/files/def.js239
-rw-r--r--static/files/dyna.js579
-rw-r--r--static/files/footer.gifbin0 -> 91 bytes
-rw-r--r--static/files/graph.pngbin0 -> 601 bytes
-rw-r--r--static/files/headerbg.jpgbin0 -> 6068 bytes
-rw-r--r--static/files/headerbot.pngbin0 -> 2343 bytes
-rw-r--r--static/files/platforms.pngbin0 -> 2353 bytes
-rw-r--r--static/files/rss.pngbin0 -> 735 bytes
-rw-r--r--static/files/select.pngbin0 -> 1165 bytes
-rw-r--r--static/files/sidebarbg.jpgbin0 -> 3035 bytes
-rw-r--r--static/files/sidebarbot.jpgbin0 -> 1642 bytes
-rw-r--r--static/files/sidebg.jpgbin0 -> 553 bytes
-rw-r--r--static/files/style.css729
-rw-r--r--static/files/warning.pngbin0 -> 2348 bytes
-rw-r--r--util/cleanimg.pl102
-rwxr-xr-xutil/cron_daily.sh31
-rw-r--r--util/cron_daily.sql15
-rwxr-xr-xutil/relgraph.pl237
-rwxr-xr-xutil/sitemap.pl94
-rwxr-xr-xutil/updates/update_1.1.pl18
-rw-r--r--util/updates/update_1.1.sql13
-rw-r--r--util/updates/update_1.10.sql92
-rw-r--r--util/updates/update_1.11.sql4
-rw-r--r--util/updates/update_1.12.sql34
-rw-r--r--util/updates/update_1.13.sql229
-rwxr-xr-xutil/updates/update_1.14.pl57
-rw-r--r--util/updates/update_1.14.sql76
-rw-r--r--util/updates/update_1.2.sql9
-rw-r--r--util/updates/update_1.4.sql37
-rw-r--r--util/updates/update_1.5.sql10
-rw-r--r--util/updates/update_1.6.sql21
-rw-r--r--util/updates/update_1.7.sql23
-rw-r--r--util/updates/update_1.8.sql27
-rw-r--r--util/updates/update_1.9.sql375
77 files changed, 9918 insertions, 0 deletions
diff --git a/data/tpl/defs.pl b/data/tpl/defs.pl
new file mode 100644
index 00000000..b68faab0
--- /dev/null
+++ b/data/tpl/defs.pl
@@ -0,0 +1,482 @@
+[[!
+
+use Time::CTime ();
+use Algorithm::Diff 'sdiff';
+use POSIX ('ceil', 'floor');
+
+my %p; # $X->{page} global page data
+my %d; # $X->{page}->{$p} local page data
+
+# redefine _hchar - usually a bad idea, but who cares
+sub _hchar {local$_=shift||return'';s/&/&amp;/g;s/</&lt;/g;s/>/&gt;/g;s/"/&quot;/g;s/\r?\n/ <br \/>\n/g;return$_;}
+
+sub formatdate {return _hchar(Time::CTime::strftime($_[0],gmtime($_[1]||0)))||'';}
+sub txt {local$_=shift||return'';s/&/&amp;/g;s/</&lt;/g;s/>/&gt;/g;return$_;}
+sub art2str {my$r='';$r.=($r?' & ':'').$_->{name}foreach (@{$_[0]->{artists}});return $_[1]?$r:_hchar($r);}
+sub calctime {my$r=shift;return'0:00:00'if!$r;my$x=sprintf'%d:%02d:%02d',int($r/3600),int(($r%3600)/60),($r%3600)%60;return $x;}
+sub shorten {local$_=shift||return'';return length>$_[0]?substr($_,0,$_[0]-3).'...':$_};
+
+# Date string format: yyyy-mm-dd
+# y = 0 -> Unknown
+# y = 9999 -> TBA (To Be Announced)
+# m = 0 -> Month + day unknown, year known
+# d = 0 -> Day unknown, month + year known
+sub datestr {
+ my $d = $_[0]||'00000000';
+ my @d = map { int } $1, $2, $3 if $d =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/;
+ return 'unknown' if $d[0] == 0;
+ my $r = sprintf !$d[1] ? '%04d' : !$d[2] ? '%04d-%02d' : '%04d-%02d-%02d', @d;
+ my $b = $r gt Time::CTime::strftime("%Y-%m-%d", gmtime());
+ $r = 'TBA' if $d[0] == 9999;
+ return ($b?'<b class="future">':'').$r.($b?'</b>':'');
+}
+sub mediastr {
+ return join(', ', map {
+ $_->{medium} =~ /^(cd|dvd|gdr|blr)$/
+ ? sprintf('%d %s%s', $_->{qty}, $VNDB::MED->{$_->{medium}}, $_->{qty}>1?'s':'')
+ : $VNDB::MED->{$_->{medium}}
+ } @{$_[0]});
+}
+sub sortbut { # url, col
+ my $r=' '; my $u = _hchar($_[0]);
+ $u .= $u =~ /\?/ ? ';' : '?';
+ for ('a', 'd') {
+ my $chr = $_ eq 'd' ? "\x{25BE}" : "\x{25B4}";
+ $r .= $d{order}[0] eq $_[1] && $d{order}[1] eq $_ ? $chr :
+ sprintf '<a href="%ss=%s;o=%s">%s</a>', $u, $_[1], $_, $chr;
+ }
+ return $r;
+}
+sub pagebut { # url
+ my @br; my $ng = $_[0] =~ /\?/ ? ';' : '?';
+ push @br, sprintf '<a href="%s">&lt;- previous</a>', $_[0].($d{page}-2 ? $ng.'p='.($d{page}-1) : '') if $d{page} > 1;
+ push @br, sprintf '<a href="%s">next -&gt;</a>', $_[0].$ng.'p='.($d{page}+1) if $d{npage};
+ return $#br >= 0 ? ('<p class="browse">( '.join(' | ', @br).' )</p>') : '';
+}
+sub wraplong { # text, margin
+ local $_ = $_[0];
+ my $m = $_[1]/2;
+ s/([^\s\r\n]{$m})([^\s\r\n])/$1 $2/g;
+ return $_;
+}
+
+
+sub wordsplit { # split a string into an array of words, but make sure to not split HTML tags
+# return [ split //, $_[0] ];
+ my @a;
+ my $in='';
+ for (split /\s+/, $_[0]) {
+ my $gt = () = />/g;
+ my $lt = () = /</g;
+ if($in && $gt > $lt) {
+ push @a, $in.$_;
+ $in='';
+ } elsif($lt > $gt || $in) {
+ $in .= $_.' ';
+ } else {
+ push @a, $_;
+ };
+ }
+ push @a, $in if $in;
+ return \@a;
+}
+
+sub cdiff { # obj1, obj2, @items->[ short, name, serialise, diff, [parsed_x, parsed_y] ]
+ my($x, $y, @items, @c) = @_;
+ # serialise = 0 -> integer, 1 -> string, CODEref -> code
+
+ my $type = defined $$y{minage} ? 'r' : defined $$y{length} ? 'v' : 'p';
+ my $pre = '<div id="revbrowse">'.
+ ($$y{next} ? qq|<a href="/$type$$y{id}?rev=$$y{next}" id="revnext">later revision -&gt;</a>| : '').
+ ($x ? qq|<a href="/$type$$y{id}?rev=$$x{cid}" id="revprev">&lt;- earlier revision</a>| : '').
+ qq|<a href="/$type$$y{id}" id="revmain">$type$$y{id}</a>&nbsp;</div>|;
+
+ if(!$x) { # just show info about the revision if there is no previous edit
+ return $pre.qq|<div id="tmc"><b>Revision $$y{cid}</b> (<a href="/$type$$y{id}/edit?rev=$$y{cid}">edit</a>)<br />By <a href="/u$$y{requester}">$$y{username}</a> on |.
+ formatdate('%Y-%m-%d at %R', $$y{added}).'<br /><b>Edit summary:</b><br /><br />'.
+ summary($$y{comments}, 0, '[no summary]').'</div>';
+ }
+ for (@items) {
+ $_->[4] = !$_->[2] ? $x->{$_->[0]}||'0' : !ref($_->[2]) ? _hchar(wraplong($x->{$_->[0]}||'[empty]',60)) : &{$_->[2]}($x->{$_->[0]})||'[empty]';
+ $_->[5] = !$_->[2] ? $y->{$_->[0]}||'0' : !ref($_->[2]) ? _hchar(wraplong($y->{$_->[0]}||'[empty]',60)) : &{$_->[2]}($y->{$_->[0]})||'[empty]';
+ push(@c, $_) if $_->[4] ne $_->[5];
+ if($_->[3] && $_->[4] ne $_->[5]) {
+ my($rx,$ry,$ch) = ('','','u');
+ for (sdiff(wordsplit($_->[4]), wordsplit($_->[5]))) {
+ if($ch ne $_->[0]) {
+ if($ch ne 'u') {
+ $rx .= '</b>';
+ $ry .= '</b>';
+ }
+ $rx .= '<b class="diff_del">' if $_->[0] eq '-' || $_->[0] eq 'c';
+ $ry .= '<b class="diff_add">' if $_->[0] eq '+' || $_->[0] eq 'c';
+ }
+ $ch = $_->[0];
+ $rx .= $_->[1].' ' if $ch ne '+';
+ $ry .= $_->[2].' ' if $ch ne '-';
+ }
+ $_->[4] = $rx;
+ $_->[5] = $ry;
+ }
+ }
+ return $pre.'<table id="tmc"><thead><tr><td class="tc1">&nbsp;</td>'.
+ qq|<td class="tc2"><b>Revision $$x{cid}</b> (<a href="/$type$$y{id}/edit?rev=$$x{cid}">edit</a>)<br />By <a href="/u$$x{requester}">$$x{username}</a> on |.formatdate('%Y-%m-%d at %R', $$x{added}).'</td>'.
+ qq|<td class="tc3"><b>Revision $$y{cid}</b> (<a href="/$type$$y{id}/edit?rev=$$y{cid}">edit</a>)<br />By <a href="/u$$y{requester}">$$y{username}</a> on |.formatdate('%Y-%m-%d at %R', $$y{added}).'</td>'.
+ '</tr><tr></tr><tr><td>&nbsp;</td><td colspan="2"><b>Edit summary of revision '.$$y{cid}.'</b><br /><br />'.summary($$y{comments}, 0, '[no summary]').'<br /><br /></td></tr></thead>'.
+ join('',map{
+ '<tr><td class="tc1">'.$_->[1].'</td><td class="tc2">'.$_->[4].'</td><td class="tc3">'.$_->[5].'</td></tr>'
+ } @c).'</table>';
+}
+
+
+sub summary { # cmd, len, def
+ return $_[2]||'' if !$_[0];
+ my $res = '';
+ my $len = 0;
+ my $as = 0;
+ for (split / /, $_[0]) {
+ next if !$_;
+ my $l = length;
+ s/\&/&amp;/g;
+ s/>/&gt;/g;
+ s/</&lt;/g;
+ while(s/\[url=((https?:\/\/|\/)[^\]>]+)\]/<a href="$1" rel="nofollow">/) {
+ $l -= length($1)+6;
+ $as++;
+ }
+ if(!$as && s/(http|https):\/\/(.+[0-9a-zA-Z\/])/<a href="$1:\/\/$2" rel="nofollow">link<\/a>/) {
+ $l = 4;
+ } elsif(!$as) {
+ s/^([uvpr][0-9]+)[^\w]*$/<a href="\/$1">$1<\/a>/;
+ }
+ while(s/\[\/url\]/<\/a>/) {
+ $l -= 6;
+ $as--;
+ }
+ $len += $l + 1;
+ last if $_[1] && $len > $_[1];
+ $res .= "$_ ";
+ }
+ $res =~ y/\r\n/ / if $_[1];
+ $res =~ s/\r?\n/<br \/>/g if !$_[1];
+ $res =~ s/ +$//;
+ $res .= '</a>' x $as if $as;
+ $res .= '...' if $_[1] && $len > $_[1];
+ return $res;
+}
+
+
+sub ttabs { # [vrp], obj, sel
+ my($t, $o, $s) = @_;
+ $s||='';
+ my @act = (
+ !$s?'%s':'<a href="/%s">%1$s</a>',
+ $$o{locked} ?
+ '<b>locked for editing</b>' : (),
+ $p{Authlock} ?
+ sprintf('<a href="/%%s/lock">%s</a>', $$o{locked} ? 'unlock' : 'lock') : (),
+ $p{Authdel} ? (
+ '<a href="/%s/del" id="idel">del</a>',
+ sprintf('<a href="/%%s/hide"%s>%s</a>', $t eq 'v' ? ' id="vhide"' : '', $$o{hidden} ? 'unhide' : 'hide')
+ ) : (),
+ !$$o{locked} || ($p{Authedit} && $p{Authlock}) ?
+ ($s eq 'edit' ? 'edit' : '<a href="/%s/edit" '.($t eq 'v' || $t eq 'r' ? 'class="dropdown" rel="editDD"':'').'>edit</a>') : (),
+
+ $p{Authhist} ?
+ ($s eq 'hist' ? 'history' : '<a href="/%s/hist">history</a>') : (),
+ );
+ return '<p class="mod">&lt; '.join(' - ', map { sprintf $_, $t.$$o{id} } @act).' &gt;</p>'.(
+ $t eq 'v' ? qq|
+<div id="editDD" class="dropdown">
+ <ul>
+ <li><a href="/v$$o{id}/edit" rel="nofollow">Edit all</a></li>
+ <li><a href="/v$$o{id}/edit?fh=info" rel="nofollow">General info</a></li>
+ <li><a href="/v$$o{id}/edit?fh=cat" rel="nofollow">Categories</a></li>
+ <li><a href="/v$$o{id}/edit?fh=rel" rel="nofollow">Relations</a></li>
+ <li><a href="/v$$o{id}/edit?fh=img" rel="nofollow">Upload image</a></li>
+ <li><a href="/v$$o{id}/add" rel="nofollow">Add release</a></li>
+ </ul>
+</div>| : $t eq 'r' ? qq|
+<div id="editDD" class="dropdown">
+ <ul>
+ <li><a href="/r$$o{id}/edit" rel="nofollow">Edit all</a></li>
+ <li><a href="/r$$o{id}/edit?fh=info" rel="nofollow">General info</a></li>
+ <li><a href="/r$$o{id}/edit?fh=pnm" rel="nofollow">Platforms &amp; media</a></li>
+ <li><a href="/r$$o{id}/edit?fh=prod" rel="nofollow">Producers</a></li>
+ <li><a href="/r$$o{id}/edit?fh=rel" rel="nofollow">Relations</a></li>
+ </ul>
+</div>| : ''
+ );
+}
+
+
+
+my %pagetitles = (
+ faq => 'Frequently Asked Questions',
+ userlogin => 'Login',
+ userreg => 'Register a new account',
+ userpass => 'Forgot your password?',
+ home => 'Visual Novel Database',
+ pbrowse => 'Browse producers',
+ userlist => 'Browse users',
+ myvotes => sub {
+ return $p{myvotes}{user}{username} eq $p{AuthUsername} ? 'My votes' : ('Votes by '.$p{myvotes}{user}{username}); },
+ userpage => sub {
+ return 'User: '.$p{userpage}{user}{username} },
+ vnlist => sub {
+ return $p{vnlist}{user}{username} eq $p{AuthUsername} ? 'My visual novel list' : ($p{vnlist}{user}{username}.'\'s visual novel list'); },
+ useredit => sub {
+ return !$p{useredit}{adm} ? 'My account' : 'Edit '.$p{useredit}{form}{username}.'\'s account'; },
+ ppage => sub {
+ return $p{ppage}{prod}{name} },
+ pedit => sub {
+ return $p{pedit}{id} ? sprintf('Edit %s', $p{pedit}{form}{name}) : 'Add a new producer'; },
+ vnedit => sub {
+ return $p{vnedit}{id} ? sprintf('Edit %s', $p{vnedit}{form}{title}) : 'Add a new visual novel'; },
+ redit => sub {
+ return $p{redit}{id} ? sprintf('Edit %s', $p{redit}{rel}{title}) : sprintf('Add release to %s', $p{redit}{vn}{title}); },
+ vnpage => sub { return $p{vnpage}{vn}{title}; },
+ vnrg => sub { return 'Relations for '.$p{vnrg}{vn}{title} },
+ vnstats => sub { return 'User statistics for '.$p{vnstats}{vn}{title} },
+ vnbrowse => sub {
+ return $p{vnbrowse}{chr} eq 'search' ? sprintf 'Search results for "%s"', $p{searchquery} :
+ $p{vnbrowse}{chr} eq 'cat' ? 'Browse categories' :
+ $p{vnbrowse}{chr} eq 'mod' ? 'Visual Novels awaiting moderation' :
+ $p{vnbrowse}{chr} eq 'all' ? 'Browse all visual novels' :
+ $p{vnbrowse}{chr} eq '0' ? 'Browse by char: Other' :
+ sprintf 'Browse by char: %s', uc $p{vnbrowse}{chr}; },
+ rpage => sub {
+ return $p{rpage}{rel}{romaji} || $p{rpage}{rel}{title} },
+ hist => sub {
+ return !$p{hist}{id} || !$p{hist}{type} ? 'Recent changes' :
+ $p{hist}{type} eq 'u' ? 'Recent changes by '.$p{hist}{title} : 'Edit history of '.$p{hist}{title}; },
+ docs => sub {
+ return (
+ 'Categories', 'Adding/editing a visual novel', 'Adding/editing a release',
+ 'Adding/editing a producer', 'General guidelines', 'Error parsing form',
+ )[$p{docs}{p}-1]||'' }
+);
+sub gettitle{$p{$_}&&($p{PageTitle}=ref($pagetitles{$_}) eq 'CODE' ? &{$pagetitles{$_}} : $pagetitles{$_}) for (keys%pagetitles);}
+
+
+#
+# F O R M E R R O R H A N D L I N G
+#
+my %formerr_names = (
+ mail => 'Email',
+ username => 'Username',
+ userpass => 'Password',
+ pass1 => 'Password',
+ pass2 => 'Password (second)',
+ title => 'Title',
+ desc => 'Description',
+ rel => 'Relation',
+ romaji => 'Romanized title',
+ lang => 'Language',
+ web => 'Website',
+ released => 'Release date',
+ platforms => 'Platforms',
+ media => 'Media',
+ name => 'Name',
+ vn => 'Visual novel relations',
+);
+my @formerr_msgs = (
+ sub { return sprintf 'Field "%s" is required.', @_ },
+ sub { return sprintf '%s should have at least %d characters.', @_ },
+ sub { return sprintf '%s is too large! Only %d characters allowed.', @_ },
+ sub { return
+ $_[1] eq 'mail' ? 'Invalid email address' :
+ $_[1] eq 'url' ? 'Invalid URL' :
+ $_[1] eq 'pname' ? sprintf('%s can only contain alfanumeric characters!', $_[0]) :
+ $_[1] eq 'asciiprint' ? sprintf('Only ASCII characters are allowed at %s', $_[0]) :
+ $_[1] eq 'int' ? sprintf('%s should be a number!', $_[0]) : '';
+ },
+ sub { return sprintf '%s: invalid item selected', @_ },
+ sub { return 'Invalid unicode, are you sure your browser works fine?' },
+);
+my %formerr_exeptions = (
+ loginerr => 'Invalid username or password',
+ badpass => 'Passwords do not match',
+ usrexists => 'Username already exists, please choose an other one',
+ mailexists => 'There already is a user with that email address, please request a new password if you forgot it',
+ nomail => 'No user found with that email address',
+ nojpeg => 'Image is not in JPEG format!',
+ toolarge => 'Image is too large (in filesize), try to compress it a little',
+ imgsize => 'Image is too large (in height/width), try to resize it a little',
+);
+sub formerr {
+ my @err = ref $_[0] eq 'ARRAY' ? @{$_[0]} : ();
+ return '' if $#err < 0;
+ my @msgs;
+ my $ret = '<span class="warning">
+ Error:<ul>';
+ $ret .= sprintf " <li>%s</li>\n",
+ /^([a-z0-9]+)_([0-9]+)_?(.*)$/ ? &{$formerr_msgs[$2-1]}($formerr_names{$1}, $3?$3:'') : $formerr_exeptions{$_}
+ foreach (@err);
+ $ret .= "</ul>\n</span>\n";
+}
+
+#
+# F O R M C R E A T I N G
+#
+
+# args = [
+# {
+# type => $type,
+# %options
+# }, ...
+# ], $formobj
+#
+# $type $formobj %options ( required, [ optional ] )
+# error X ( )
+# startform ( action, [ upload ] )
+# endform ( )
+# input X ( short, name, [ class, default ] )
+# pass ( short, name )
+# upload ( short, name, [ class ] )
+# hidden X ( short, [ value ] )
+# textarea X ( short, name, [ rows, cols, class ] )
+# select X ( short, name, options, [ class ] ) # options = arrayref of hashes with keys: short, name
+# as X ( name )
+# trans X ( )
+# submit ( [ text, short ] )
+# sub ( title )
+# check X ( short, name, [ value ] )
+# static ( text, raw [ name, class ] )
+# date X ( short, name )
+#
+sub cform {
+ my $obj = shift;
+ my $frm = shift;
+ my $ret = '';
+ my $csub = '';
+ for (@$obj) {
+ $_->{class} ||= '';
+ $_->{class} .= ' sf_'.$csub if $csub && $_->{class} !~ /nohid/;
+ $_->{class} .= ' formhid' if $csub && $frm->{_hid} && !$frm->{_hid}{$csub} && $_->{class} !~ /nohid/;
+ $_->{name} = '<i>*</i> '.$_->{name} if $_->{r};
+
+ # error
+ if($_->{type} eq 'error') {
+ $ret .= formerr($frm->{_err});
+ # startform
+ } elsif($_->{type} eq 'startform') {
+ $ret .= sprintf qq|<form action="/nospam?%s" method="post" accept-charset="utf-8"%s>\n|,
+ $_->{action}, $_->{upload} ? ' enctype="multipart/form-data"' : '';
+ $ret .= sprintf qq| <input type="hidden" class="hidden" name="fh" id="_hid" value="%s" />\n|,
+ $frm->{_hid} ? _hchar(join(',', keys %{$frm->{_hid}})) : '' if $_->{fh};
+ $ret .= qq|<p class="formnotice">Items denoted by a red asterisk (<i>*</i>) are required.</p>\n|
+ if scalar grep { $_->{r} } @$obj;
+ $ret .= "<ul>\n";
+ # endform
+ } elsif($_->{type} eq 'endform') {
+ $ret .= qq|</ul></form>\n|;
+ # input
+ } elsif($_->{type} eq 'input') {
+ $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n %s<input type="text" class="text" name="%2\$s" id="%2\$s" value="%s" />\n</li>\n|,
+ $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name}, $_->{pre} ? '<i>'.$_->{pre}.'</i>' : '',
+ _hchar($frm->{$_->{short}}?$frm->{$_->{short}}:$_->{default});
+ # pass
+ } elsif($_->{type} eq 'pass') {
+ $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <input type="password" class="text" name="%2\$s" id="%2\$s" />\n</li>\n|,
+ $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name};
+ # upload
+ } elsif($_->{type} eq 'upload') {
+ $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <input type="file" class="text" name="%2\$s" id="%2\$s" />\n</li>\n|,
+ $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name};
+ # hidden
+ } elsif($_->{type} eq 'hidden') {
+ $ret .= sprintf qq| <input type="hidden" class="hidden" name="%s" id="%1\$s" value="%s" />\n|,
+ $_->{short}, _hchar($_->{value} || $frm->{$_->{short}});
+ # textarea
+ } elsif($_->{type} eq 'textarea') {
+ $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <textarea name="%2\$s" id="%2\$s" rows="%s" cols="%s">%s</textarea>\n</li>\n|,
+ $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name}, $_->{rows}||15, $_->{cols}||70, txt($frm->{$_->{short}});
+ # select
+ } elsif($_->{type} eq 'select') {
+ $ret .= sprintf qq|<li%s>\n <label for="%s">%s</label>\n <select name="%2\$s" id="%2\$s">\n%s</select>\n</li>\n|,
+ $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{short}, $_->{name}, eval {
+ my $r='';
+ for my $s (@{$_->{options}}) {
+ $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
+ $s->{short}, defined $frm->{$_->{short}} && $frm->{$_->{short}} eq $s->{short} ? ' selected="selected"' : '', $s->{name};
+ }
+ return $r;
+ };
+ # jssel
+ } elsif($_->{type} eq 'jssel') {
+ (my $oname = $_->{name}) =~ s/^<i>\*<\/i>//;
+ $ret .= sprintf
+ qq|<li%s>\n|
+ .qq| <label for="%s_select">%s</label>\n|
+ .qq| <select name="%s_select" id="%s_select" multiple="multiple" size="5" class="multiple">\n|
+ .qq| <option value="0_new" style="font-style: italic">Add %s...</option>\n|
+ .qq| </select>\n|
+ .qq| <div id="%s_conts">\n|
+ .qq| Loading...\n|
+ .qq| </div>\n|
+ .qq| <input type="hidden" name="%s" id="%s" class="hidden" value="%s" />\n|
+ .qq|</li>\n|,
+ $_->{class} ? ' class="'.$_->{class}.'"' : '',
+ $_->{sh}, $_->{name}, $_->{sh}, $_->{sh}, $oname, $_->{sh}, $_->{short}, $_->{short}, _hchar($frm->{$_->{short}});
+ # submit
+ } elsif($_->{type} eq 'submit') {
+ $ret .= sprintf qq|<li class="nolabel">\n <br /><input type="submit" class="submit" value="%s"%s />\n </li>\n|,
+ $_->{text} || 'Verstuur', $_->{short} ? sprintf(' name="%s" id="%1$s"', $_->{short}) : '';
+ # sub
+ } elsif($_->{type} eq 'sub') {
+ $ret .= sprintf qq|<li class="subform">\n <a href="#" class="s_%s">%s %s</a>\n</li>\n|,
+ $_->{short}, $frm->{_hid} && !$frm->{_hid}{$_->{short}} ? '&#9656;' : '&#9662;', $_->{title};
+ $csub = $_->{short};
+ # check
+ } elsif($_->{type} eq 'check') {
+ $ret .= sprintf qq|<li class="nolabel%s">\n <input type="checkbox" name="%s" id="%2\$s" value="%s"%s />\n <label for="%2\$s" class="checkbox">%s</label>\n</li>\n|,
+ $_->{class} ? ' '.$_->{class} : '',
+ $_->{short}, $_->{value} || 'true', $frm->{$_->{short}} ? ' checked="checked"' : '', $_->{name};
+ # static
+ } elsif($_->{type} eq 'static') {
+ $ret .= $_->{name}
+ ? sprintf qq|<li%s>\n <label>%s</label>\n <p>%s</p>\n</li>|, $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{name}, $_->{text}
+ : $_->{raw}
+ ? sprintf qq|<li%s>\n %s\n</li>|, $_->{class} ? ' class="'.$_->{class}.'"' : '', $_->{text}
+ : sprintf qq|<li class="nolabel%s">\n %s\n</li>|, $_->{class} ? ' '.$_->{class} : '', $_->{text};
+ # date
+ } elsif($_->{type} eq 'date') {
+ $ret .= sprintf qq|<li class="date%s">\n <label for="%s">%s</label>\n|,
+ $_->{class} ? ' '.$_->{class} : '', $_->{short}, $_->{name};
+ $ret .= sprintf qq| <select name="%s" id="%s">\n%s</select>\n|,
+ $_->{short}, $_->{short}, eval {
+ my $r='';
+ for my $s (0, 1990..((localtime())[5]+1905), 9999) {
+ $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
+ $s, $frm->{$_->{short}} && ($frm->{$_->{short}}[0]||0) == $s ? ' selected="selected"' : '',
+ !$s ? '-year-' : $s < 9999 ? $s : 'TBA';
+ }
+ return $r;
+ };
+ $ret .= sprintf qq| <select name="%s" id="%s_m">\n%s</select>\n|,
+ $_->{short}, $_->{short}, eval {
+ my $r='';
+ for my $s (0..12) {
+ $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
+ $s, $frm->{$_->{short}} && ($frm->{$_->{short}}[1]||0) == $s ? ' selected="selected"' : '',
+ $s ? $Time::CTime::MonthOfYear[$s-1] : '-month-';
+ }
+ return $r;
+ };
+ $ret .= sprintf qq| <select name="%s" id="%s_d">\n%s</select>\n</li>\n|,
+ $_->{short}, $_->{short}, eval {
+ my $r='';
+ for my $s (0..31) {
+ $r .= sprintf qq| <option value="%s"%s>%s</option>\n|,
+ $s, $frm->{$_->{short}} && ($frm->{$_->{short}}[2]||0) == $s ? ' selected="selected"' : '',
+ $s ? $s : '-day-';
+ }
+ return $r;
+ };
+ }
+ }
+ return $ret;
+}
+
+]]
diff --git a/data/tpl/docs b/data/tpl/docs
new file mode 100644
index 00000000..1ca812c2
--- /dev/null
+++ b/data/tpl/docs
@@ -0,0 +1,298 @@
+[[ if(0) { ]]
+<p class="mod">&lt; <a href="/d1">categories</a> - <a href="/d2">visual novels</a> - <a href="/d3">releases</a> - <a href="/d4">producers</a> - <a href="/d5">general guidelines</a> &gt;</p>
+[[ } ]]
+<h2>[[: $p{PageTitle} ]]</h2>
+<div id="dpage">
+
+[[ # C A T E G O R I E S
+ if($d{p} == 1) { ]]
+
+<h3>Elements</h3>
+<p>
+ ...own interpretation for now... (Should be documented at some time, too)
+</p>
+
+
+<h3>Gameplay</h3>
+<p>
+ This category is used to describe the gameplay or game engine.
+</p>
+<dl>
+ <dt>Visual Novel</dt><dd>
+ All games where the text is overlaid on the background and there is no special
+ dialog-box fall under this category. Can be abbreviated as VN or NVL.
+ </dd><dt>Adventure</dt><dd>
+ This is the opposite of the <i>Visual Novel</i> category: The text is presented
+ in a special window, usually at the bottom of the screen. In some (rare) cases
+ a game will switch between both styles, for these games both the <i>Visual Novel</i>
+ and <i>Adventure</i> categories should be selected. Can be abbreviated as ADV or AVG.
+ </dd><dt>Action</dt><dd>
+ This category indicates that the game includes a gameplay that challenges the
+ player's speed, dexterity and reaction time. Common examples are fighting games,
+ puzzles that should be solved within a short time limit, and shooter games.
+ </dd><dt>RPG</dt><dd>
+ Abbreviation for Role Playing Game. An RPG is a game in which you assume the
+ role of a character introduced to a vast world to be explored. Games typically
+ place emphasis on gaining equipment and experience points through fighting enemies
+ in order to advance through different levels.
+ </dd><dt>Strategy</dt><dd>
+ A strategy game is one that challenges the player to think critically in order
+ to achieve victory.
+ </dd><dt>Simulation</dt><dd>
+ A simulation game attempts to recreate aspects of reality and puts the player in
+ control.
+ </dd>
+</dl>
+
+<h3>Plot</h3>
+<p>
+ Indicates the plot type of a game. There are only two options: <i>Branching</i> and
+ <i>Linear</i>.
+</p>
+<dl>
+ <dt>Linear</d><dd>
+ A game with a linear plot has a static story; it is not possible to get different paths
+ or endings. Many games in this category do not prompt the player with choices and simply
+ tell the story as it is. This is, however, not a rule: it is also possible for a game
+ to provide choises, but they have no influence on the story itself. (e.g.
+ <a href="/v3">Utawarerumono</a>)
+ </dd><dt>Branching</dt><dd>
+ A game with a branching plot has a story whose path is directly affected by choices
+ made by the player during the game. These different paths are sometimes referred to
+ as "arcs" when they pertain to the stories of different female characters within a game.
+ </dd>
+</dl>
+
+<h3>Time</h3>
+<p>
+ Indicates the time period in which the story has been set.
+</p>
+<dl>
+ <dt>Future</dt><dd>
+ The game is set in a time beyond that of our own. Games may incorperate elements of
+ future technologies or events yet-to-come.
+ </dd><dt>Present</dt><dd>
+ The game is set in the current day.
+ </dd><dt>Past</dt><dd>
+ The game is set in a time before our own. Games may or may not adhere to historic fact.
+ </dd>
+</dl>
+
+<h3>Place</h3>
+<p>
+ Indicates the place in which the story is told.
+</p>
+<dl>
+ <dt>Earth</dt><dd>
+ The game takes place on our own planet.
+ </dd><dt>Fantasy World</dt><dd>
+ The game takes place on another world. The game's environment could be similar
+ to that of our own with a few significant changes, but it could also be
+ radically different.
+ </dd><dt>Space</dt><dd>
+ The game takes place in the vacuum of space between celestial bodies. For example,
+ this category can be used to define games where the characters may inhabit
+ spaceships that journey across the universe.
+ </dd>
+</dl>
+
+<h3>Sexual content</h3>
+<p>
+ Indicates the types of sexual content that the game contains.
+</p>
+<dl>
+ <dt>Sexual content</dt><dd>
+ This is a generic category to indicate the presence of any sexual content in the
+ game. If there is any such content, this category should be selected.
+ </dd><dt>Bestiality</dt><dd>
+ Sexual activity between characters and animals.<br />
+ <i>No catgirls, I guess?</i>
+ </dd><dt>Incest</dt><dd>
+ Sexual activity between members of the same family. Most of the time under the
+ justification of participants not blood related (step-sister etc.).
+ </dd><dt>Lolicon</dt><dd>
+ The usage of female characters with childlike features in sexual situations.
+ </dd><dt>Shotacon</dt><dd>
+ The usage of male characters with childlike features in sexual situations.
+ </dd><dt>Yaoi</dt><dd>
+ Sexual content depicting activity between males.
+ </dd><dt>Yuri</dt><dd>
+ Sexual content depicting activity between females.
+ </dd><dt>Rape</dt><dd>
+ Situation in which a character is made to engage in sexual activities against
+ their will.
+ </dd>
+</dl>
+
+
+
+
+[[ } # V I S U A L N O V E L A D D / E D I T
+ if($d{p} == 2) { ]]
+
+<p>
+ Blahblah about what we define as VN? Or should that be in <a href="/d5">General guidelines</a>?
+</p>
+
+<h3>General info</h3>
+<dl>
+ <dt>*Title</dt><dd>
+ ..
+ </dd><dt>Aliases</dt><dd>
+ ..
+ </dd><dt>*Description</dt><dd>
+ ..
+ </dd><dt>Length</dt><dd>
+ ..
+ </dd><dt>External links</dt><dd>
+ ..
+ </dd>
+</dl>
+
+<h3>Categories</h3>
+<p>
+ See <a href="/d1">Categories</a>.
+</p>
+
+<h3>Image</h3>
+<p>
+ <i>General image guidelines and when to use the NSFW warning</i>
+</p>
+
+<h3>Relations</h3>
+<p>
+ <i>When to add relation, and document direct and reverse relations</i><br />
+ <i>(Stolen from AniDB, needs some rewriting)</i>
+</p>
+<dl>
+ <dt>Sequel</dt><dd>
+ Continuation of the story. &lt;=&gt;<i>Prequel</i>.
+ </dd><dt>Prequel</dt><dd>
+ The story happens before the original story.&lt;=&gt;<i>Sequel</i>.
+ </dd><dt>Same setting</dt><dd>
+ Same universe/world/reality/timeline, completely different characters.
+ </dd><dt>Alternative setting</dt><dd>
+ Same characters, different universe/world/reality/timeline.
+ </dd><dt>Alternative version</dt><dd>
+ Same setting, same characters, story is told differently.
+ </dd><dt>Same characters</dt><dd>
+ Shares one or more characters, story is unrelated.
+ </dd><dt>Side story</dt><dd>
+ Takes place sometime during the parent storyline. &lt;=&gt;<i>Parent story</i>
+ </dd><dt>Parent story</dt><dd>
+ .. &lt;=&gt;<i>Side story</i>.
+ </dd><dt>Summary</dt><dd>
+ Summarizes full story, may contain additional stuff. &lt;=&gt;<i>Full story</i>.
+ </dd><dt>Full story</dt><dd>
+ Full version of the summarized story. &lt;=&gt;<i>Summary</i>.
+ </dd><dt>Other</dt><dd>
+ ..
+ </dd>
+</dl>
+
+
+
+
+
+
+[[ } # R E L E A S E A D D / E D I T
+ if($d{p} == 3) { ]]
+
+<p>
+ <i>When to add a release</i>
+</p>
+
+<h3>General info</h3>
+<dl>
+ <dt>*Type</dt><dd>
+ ..
+ </dd><dt>*Title (romaji)</dt><dd>
+ ..
+ </dd><dt>Original title</dt><dd>
+ ..
+ </dd><dt>*Language</dt><dd>
+ ..
+ </dd><dt>Official website</dt><dd>
+ ..
+ </dd><dt>Release date</dt><dd>
+ ..
+ </dd><dt>Age rating</dt><dd>
+ ..
+ </dd><dt>Notes</dt><dd>
+ ..
+ </dd>
+</dl>
+
+<h3>Platforms &amp; Media</h3>
+<dl>
+ <dt>Platforms</dt><dd>
+ ..
+ </dd><dt>Media</dt><dd>
+ ..
+ </dd>
+</dl>
+
+<h3>Producers</h3>
+..
+
+<h3>Visual novel relations</h3>
+..
+
+
+
+
+
+
+[[ } # P R O D U C E R A D D / E D I T
+ if($d{p} == 4) { ]]
+
+<p>
+ <i>When to add a producer and what to do with producer relations...</i>
+</p>
+
+<h3>General info</h3>
+<dl>
+ <dt>*Type</dt><dd>
+ ..
+ </dd><dt>*Name (romaji)</dt><dd>
+ ..
+ </dd><dt>Original name</dt><dd>
+ ..
+ </dd><dt>*Primary language</dt><dd>
+ ..
+ </dd><dt>Website</dt><dd>
+ ..
+ </dd><dt>Description</dt><dd>
+ ..
+ </dd>
+</dl>
+
+
+
+
+[[ } # G E N E R A L G U I D E L I N E S
+ if($d{p} == 5) { ]]
+
+
+Misc documentation:<br />
+- Romanisation and capitalization (http://wiki.anidb.net/w/Romanisation)<br />
+- What to do with fandisks<br />
+- Edit summary<br />
+- Quoting sources in descriptions<br />
+- Piracy<br />
+- Spoilers<br />
+
+
+
+
+[[ } # N O S P A M M E S S A G E
+ if($d{p} == 6) { ]]
+
+<span class="warning">
+ <b>Error:</b> The form could not be sent, please make sure you have Javascript
+ enabled in your browser!
+</span>
+
+
+[[ } ]]
+</div>
diff --git a/data/tpl/error b/data/tpl/error
new file mode 100644
index 00000000..76bd9462
--- /dev/null
+++ b/data/tpl/error
@@ -0,0 +1,45 @@
+<head>
+ <title>
+ [[ if($X->{error}->{code} == 1) { ]]VNDB offline
+ [[ } else { ]] ERROR: [[= $X->{error}->{code} ]][[ } ]]
+ </title>
+
+ <meta name="Robots" content="index, follow" />
+ <link href="/favicon.ico" type="image/x-icon" rel="shortcut icon" />
+
+ <style type="text/css">
+ body { background-color: #e0e0e0; padding: 0px; text-align: center; font-family: "Arial", Sans-serif; font-size: 11px; line-height: 16px; font-weight: normal; color: #242424; }
+ #wrapper { margin: 0px auto; margin-bottom: 20px; background-color: #fff; text-align: center; width: 672px; border: 1px solid #b8b8b8; }
+ #pre { background-color: #fff; text-align: left; padding: 0px 10px 5px 10px; margin: 5px 5px 5px 5px; border: 1px solid #e9e9e9; }
+ p { font-size: 11px; color: #242424; text-align: justify; padding-left: 17px; padding-right: 17px; }
+ h1 { font-size: 16px; font-weight: bold; color: #242424; padding-left: 17px; }
+ a { color: #33659E; text-decoration: none; }
+ a:hover { text-decoration: underline; }
+ img { border: 0; margin: 0; padding: 0; }
+ [[ if($X->{error}->{code} == 500) { ]]
+ #wrapper { width: 530px; }
+ [[ } ]]
+ </style>
+</head>
+<body>
+ <div id="wrapper">
+ <div id="pre">
+ [[ if($X->{error}->{code} > 300 && $X->{error}->{code} < 310) { ]]
+ <h1>Moved</h1>
+ <p>
+ Check <a href="[[% $X->{error}->{url} ]]">[[: $X->{error}->{url} ]]</a> for the new location.
+ </p>
+ [[ } elsif($X->{error}->{code} == 401) { ]]
+ <h1>Login required</h1>
+ <p>
+ <a href="/">Please login</a>
+ </p>
+ [[ } elsif($X->{error}->{code} == 1) { ]]
+ <h1>VNDB offline</h1>
+ <p>
+ [[: $X->{error}->{msg} ]]
+ </p>
+ [[ } ]]
+ </div>
+ </div>
+</body>
diff --git a/data/tpl/faq b/data/tpl/faq
new file mode 100644
index 00000000..1c64056e
--- /dev/null
+++ b/data/tpl/faq
@@ -0,0 +1,75 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+<br />
+<h3>What is a Visual Novel?</h3>
+<p>
+ A visual novel can be seen as a combination of a novel and a computer game:
+ they're computer games with a large text based storyline and only little
+ interaction of the player. A typical visual novel consists of text over
+ an anime-style background image and is accompanied by background music.
+ Throughout the game, the player usually has to answer a few questions which will
+ have an effect on the story, thus playing a visual novel a second time while
+ giving other answers may result in an entirely different plot.<br />
+ <br />
+ For more information see <a href="http://en.wikipedia.org/wiki/Visual_Novel">
+ the Wikipedia article on visual novels</a> or the description on
+ <a href="http://visual-novels.net/vn/index.php?option=com_content&task=view&id=259&Itemid=47">Visual-Novels.net</a>.
+ To get a general idea of the genre, try one of the free short visual novels from
+ <a href="http://at2006.haeleth.net/release.php">al|together 2006</a>.
+ <br /><br /><br />
+</p>
+
+
+<h3>How about Eroge, H-Games and Dating Sims?</h3>
+<p>
+ An eroge or H-game is basically any Japanese game that features sexual
+ content. Many visual novels are eroge and many eroge are visual novels,
+ but this is not a rule. The definition of dating sim is a bit more vague,
+ but it's usually the same as a visual novel, except that a dating sim
+ generally uses a gameplay based on statistics.<br />
+ <br />
+ There are no strict bounds to the definition of "visual novel", most
+ eroge and dating sims include elements of visual novels, but may -
+ strictly speaking - not be visual novels themselves. As VNDB aims to
+ be comprehensive, we simply accept any game that contains elements of a
+ visual novel and is produced by a Japanese or Japan-related company or
+ doujin cicle.
+ <br /><br /><br />
+</p>
+
+
+<h3>Why a Visual Novel Database?</h3>
+<p>
+ The internet is large, very large, but the number of English resources
+ related to visual novels is only very limited. VNDB attempts to collect
+ and present as much information as possible that would otherwise be very
+ hard to find for the English speaking audience. This way fans can easily
+ keep track of new releases and localizations of their favorite games,
+ while not having to browse numerous of indistinct Japanese websites.
+ <br /><br /><br />
+</p>
+
+
+<h3>How can I help VNDB?</h3>
+</p>
+ There are many ways to contribute to VNDB. First of all you can freely
+ edit all information found on this website, so if you find any errors
+ just click the "edit" link on the top right of the page. You can also
+ add new information (visual novels, producers, releases) to the database,
+ though please search the database before you do in order to prevent
+ duplicate pages.<br />
+ <br />
+ To discuss about new features or to help the development of the website
+ itself, feel free to browse the <a href="http://forum.vndb.org/">forums</a>
+ or join us on IRC at <a href="irc://irc.synirc.net/vndb">#vndb @ irc.synirc.net</a>.
+ If you aren't used to IRC or are just to lazy to install a client, you can
+ still join the chat using <a href="http://cgiirc.synirc.net/">the Webchat</a>.
+ Just choose a nickname, specify #vndb as channel and hit Login!
+ <br /><br /><br />
+</p>
+
+
+<h3>Where can I download the Visual Novels?</h3>
+<p>
+ Not here. We do not provide downloads nor links to resources that encourage
+ the illegal spreading of visual novels.
+</p>
diff --git a/data/tpl/hist b/data/tpl/hist
new file mode 100644
index 00000000..e02a149f
--- /dev/null
+++ b/data/tpl/hist
@@ -0,0 +1,103 @@
+[[= $d{type} && $d{type} ne 'u' ? ttabs($d{type}, $d{obj}, 'hist') : '' ]]-
+<h2 class="rss">[[: $p{PageTitle} ]]</h2>
+[[ if($d{type} eq 'u' && $#{$d{hist}} < 0) { ]]
+<p>
+ You haven't made any changes yet.
+</p>
+[[ } ]]
+<br /><br />
+
+[[
+ my $url = !$d{type} ? '/hist' : '/'.$d{type}.$d{id}.'/hist';
+ my $furl = $url.'?e='.$d{sele}.';t=';
+ my $eurl = $url.'?t='.$d{selt}.';e=';
+ my $purl = !$d{type}?$eurl.$d{sele}:$d{type} eq 'v' && $d{seli} ? $url.'?i=1' : $url;
+ my $rurl = $url.'/rss'.(!$d{type}?'?t='.$d{selt}.';e='.$d{sele}:$d{type} eq 'v' && $d{seli} ? '?i=1' : '');
+ local $_ = $d{selt};
+ my @fil = (
+ /a/ ? 'all items' : '<a href="%sa">all items</a>',
+ /v/ ? 'visual novels' : '<a href="%sv">visual novels</a>',
+ /r/ ? 'releases' : '<a href="%sr">releases</a>',
+ /p/ ? 'producers' : '<a href="%sp">producers</a>',
+ );
+ local $_ = $d{sele};
+ my @edi = (
+ /0/ ? 'all changes' : '<a href="%s0">all changes</a>',
+ /2/ ? 'edits only' : '<a href="%s2">edits only</a>',
+ /1/ ? 'newly created pages only' : '<a href="%s1">newly created pages only</a>',
+ );
+ local $_ = $d{seli};
+ my @inc = (
+ /0/ ? 'exclude' : '<a href="'.$url.'">exclude</a>',
+ /1/ ? 'include' : '<a href="'.$url.'?i=1">include</a>',
+ );
+]]
+
+[[ if(!$d{type}) { ]]-
+<p class="browse">
+ [[= join(' | ', map { sprintf $_, $furl } @fil) ]]<br />
+ [[= join(' | ', map { sprintf $_, $eurl } @edi) ]]<br /><br /><br />
+</p>
+[[ } if($d{type} eq 'v') { ]]-
+<p class="browse">
+ ([[= join(' | ', @inc) ]]) edits of releases.
+</p>
+[[ } ]]
+
+[[ if($d{act} eq 'r') { ]]
+<span class="msg">
+ Performed the mass-revert, please see the following list for details.
+</span>
+[[ } elsif($d{act} eq 'd') { ]]
+<span class="msg">
+ The following edits have been completely deleted.
+</span>
+[[ } ]]-
+
+
+<a class="rss" href="[[= $rurl ]]">RSS</a>
+[[= pagebut($purl) ]]
+[[ if(0 and $p{Authmod} || $p{Authdel}) { ]]
+<form method="post" action="[[= $purl ]]" class="tblf">
+[[ } ]]
+<table id="thi">
+ <thead><tr>
+ <td class="tc1">Rev.</td>
+ <td class="tc2">Date</td>
+ [[ if($d{type} ne 'u' || $d{act}) { ]]-
+ <td class="tc3">User</td>[[ } ]]-
+ [[ if(!$d{type} || $d{type} eq 'u' || $d{act} || ($d{type} eq 'v' && $d{seli})) { ]]-
+ <td class="tc4">Page</td>[[ } ]]-
+ [[ if($d{type} && !$d{act}) { ]]-
+ <td class="tc5">Summary</td>[[ } ]]-
+ [[ if($d{act} eq 'r') { ]]-
+ <td class="tc5">Action</td>[[ } ]]-
+ [[ if(0 and $p{Authmod}) { ]]-
+ <td class="tc6"><input type="checkbox" id="checkall" name="sel" value="all" /></td>[[ } ]]-
+ </tr></thead>
+
+ [[ for (@{$d{hist}}) { my $t = (qw|v r p|)[$_->{type}]; ]]-
+ <tr>
+ <td class="tc1"><a href="/[[= $t.$_->{iid} ]]?rev=[[= $_->{id} ]]">[[= $_->{id} ]]</a></td>
+ <td class="tc2">[[= formatdate('%Y-%m-%d %R', $_->{added}, 'dh') ]]</td>
+ [[ if($d{type} ne 'u' || $d{act}) { ]]-
+ <td class="tc3"><a href="/u[[= $_->{requester} ]]">[[: $_->{username} ]]</a></td>[[ } ]]-
+ [[ if(!$d{type} || $d{type} eq 'u' || $d{act}) { ]]-
+ <td class="tc4">[[= $_->{prev} ? $t.$_->{iid} : '<b>'.$t.$_->{iid}.'</b>' ]]:<a href="/[[= $t.$_->{iid} ]]?rev=[[= $_->{id} ]]" title="[[: $_->{ititle} ]]">[[: length($_->{ititle}) > 30 ? substr($_->{ititle},0,27).'...' : $_->{ititle} ]]</a></td>[[ } ]]-
+ [[ if($d{type} eq 'v' && $d{seli}) { ]]-
+ <td class="tc4"><a href="/[[= $t.$_->{iid} ]]" title="[[: $_->{ititle} ]]">[[= $_->{prev} ? $t.$_->{iid} : '<b>'.$t.$_->{iid}.'</b>' ]]</a></td>[[ } ]]-
+ [[ if($d{type} && !$d{act}) { ]]-
+ <td class="tc5">[[= summary($_->{comments}, $d{type} eq 'u' ? 40 : 60)||'[empty]' ]]</td>[[ } ]]-
+ [[ if($d{act} eq 'r') { ]]-
+ <td class="tc5">[[: $_->{_status} ]]</td>[[ } ]]-
+ [[ if(0 and $p{Authmod} && !$d{act}) { ]]-
+ <td class="tc6"><input type="checkbox" name="sel" value="[[= $_->{id} ]]" /></td>[[ } ]]-
+ </tr>
+ [[ } ]]
+
+</table>
+[[ if(0 and $p{Authmod}) { ]]<input type="submit" class="right" name="post" value="Mass revert" />[[ } ]]
+[[ if(0 and $p{Authdel}) { ]]<input type="submit" class="right" name="post" value="Mass delete" id="massdel" />[[ } ]]
+[[ if(0 and $p{Authmod} || $p{Authdel}) { ]]</form>[[ } ]]
+[[= pagebut($purl) ]]
+
diff --git a/data/tpl/home b/data/tpl/home
new file mode 100644
index 00000000..5d8cd763
--- /dev/null
+++ b/data/tpl/home
@@ -0,0 +1,67 @@
+<h2>Welcome to VNDB - The Visual Novel Database!</h2>
+<p class="desc">
+ <br />
+ VNDB.org strives to be a comprehensive database for information about visual novels and
+ eroge.<br />
+ This website is built as a wiki, meaning that anyone can freely add and contribute information
+ to the database, allowing us to create the largest, most accurate and most up-to-date visual novel
+ database on the web.<br />
+ Registered users are also able to keep track of a personal list of games they want to play or have finished
+ and they can vote on all visual novels.<br /><br />
+
+ Feel free to <a href="/v">browse around</a>, <a href="/u/register">register an account</a>
+ or to discuss about the database at our <a href="http://forum.vndb.org/">forums</a>.
+</p>
+
+<h3 class="home">VNDB 1.13!</h3>
+<p class="desc">
+ And it's time for an update again: This update makes it possible to specify how much
+ a category applies to a visual novel, adds a language filter to the category browser,
+ and fixes many, many bugs.
+ <br />
+ <a href="http://forum.vndb.org/index.php?topic=43.0">Read more...</a> - <a href="http://forum.vndb.org/index.php?board=7.0">news archive</a>.
+</p>
+
+<ul class="home">
+ <li><b>Recent changes</b></li>
+ [[ for (@{$d{recentedits}}) { my $t = (qw|v r p|)[$_->{type}]; ]]-
+ <li>[[= $t ]]:<a href="/[[= $t.$_->{iid}.'?rev='.$_->{id} ]]" title="[[: $_->{ititle} ]]">[[: shorten $_->{ititle}, 30 ]]</a></li>
+ [[ } ]]-
+</ul>
+
+<ul class="home">
+ <li><b>Recent votes</b></li>
+ [[ for (@{$d{recentvotes}}) { ]]-
+ <li><a href="/v[[= $_->{vid} ]]" title="[[: $_->{title} ]]">[[: shorten $_->{title}, 30 ]]</a> ([[= $_->{vote} ]])</li>
+ [[ } ]]
+</ul>
+
+<ul class="home">
+ <li><b>Most popular</b></li>
+ [[ for (@{$d{popular}}) { $_->{c_votes} =~ s#^([0-9]{2}.[0-9]{2}).+$#sprintf '%.1f', $1#e; ]]-
+ <li><a href="/v[[= $_->{id} ]]" title="[[: $_->{title} ]]">[[: shorten $_->{title}, 30 ]]</a> ([[= $_->{c_votes} ]])</li>
+ [[ } ]]
+</ul>
+
+<ul class="home break">
+ <li><b>Recently added visual novels</b></li>
+ [[ for (@{$d{recentvns}}) { ]]-
+ <li><a href="/v[[= $_->{iid} ]]" title="[[: $_->{ititle} ]]">[[: shorten $_->{ititle}, 30 ]]</a></li>
+ [[ } ]]-
+</ul>
+
+<ul class="home">
+ <li><b>Recently added producers</b></li>
+ [[ for (@{$d{recentps}}) { ]]-
+ <li><a href="/p[[= $_->{iid} ]]" title="[[: $_->{ititle} ]]">[[: shorten $_->{ititle}, 30 ]]</a></li>
+ [[ } ]]-
+</ul>
+
+<ul class="home">
+ <li><b>Random visual novels</b></li>
+ [[ for (@{$d{randomvns}}) { ]]-
+ <li><a href="/v[[= $_->{id} ]]" title="[[: $_->{title} ]]">[[: shorten $_->{title}, 30 ]]</a></li>
+ [[ } ]]-
+</ul>
+
+
diff --git a/data/tpl/main b/data/tpl/main
new file mode 100644
index 00000000..d52a576a
--- /dev/null
+++ b/data/tpl/main
@@ -0,0 +1,14 @@
+[[+ defs.pl ]]
+
+<!DOCTYPE html
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+
+-[[ if($X->{error}) { ]]
+ [[+ error ]]
+[[ } if($X->{page}) { %p = %{$X->{page}}; gettitle(); ]]
+ [[+ page ]]
+[[ } ]]-
+
+</html>
diff --git a/data/tpl/myvotes b/data/tpl/myvotes
new file mode 100644
index 00000000..9379e98e
--- /dev/null
+++ b/data/tpl/myvotes
@@ -0,0 +1,30 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+[[ if($#{$d{votes}} < 0) { ]]-
+<p>
+[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
+ You haven't voted on anything yet...
+[[ } else { ]]
+ [[: $d{user}{username} ]]- hasn't voted on anything yet...
+[[ } ]]
+</p>
+[[ } else {
+ my $url = sprintf '/u%d/votes', $d{user}{id};
+ my $surl = sprintf '%s?s=%s&amp;o=%s', $url, $d{order}[0], $d{order}[1];
+]]
+[[= pagebut($surl) ]]-
+<table id="tmv">
+ <thead><tr>
+ <td class="tc1">Title [[= sortbut($url, 'title') ]]</td>
+ <td class="tc2">Vote [[= sortbut($url, 'vote') ]]</td>
+ <td class="tc3">Date [[= sortbut($url, 'date') ]]</td>
+ </tr></thead>
+ [[ for (@{$d{votes}}) { ]]-
+ <tr>
+ <td class="tc1"><a href="/v[[= $_->{vid} ]]">[[: $_->{title} ]]</a></td>
+ <td class="tc2">[[: $_->{vote} ]]</td>
+ <td class="tc3">[[= formatdate('%Y-%m-%d', $_->{date}, 'dh') ]]</td>
+ </tr>
+ [[ } ]]-
+</table>
+-[[= pagebut($surl) ]]
+[[ } ]]
diff --git a/data/tpl/page b/data/tpl/page
new file mode 100644
index 00000000..e6ce9e99
--- /dev/null
+++ b/data/tpl/page
@@ -0,0 +1,140 @@
+<head>
+ <title>[[: $p{PageTitle} ]]- :: VNDB</title>
+ <link href="[[: $p{st} ]]/files/style.css?[[= $VNDB::VERSION ]]" rel="stylesheet" type="text/css" media="screen" />
+ <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
+[[ if($p{redit} || $p{vnedit}) { ]]-
+ <script src="[[: $p{st} ]]/files/dyna.js?[[= $VNDB::VERSION ]]" type="text/javascript"></script>
+[[ } ]]-
+ <script src="[[: $p{st} ]]/files/def.js?[[= $VNDB::VERSION ]]" type="text/javascript"></script>
+[[ if($p{devshit}) { ]]-
+ <meta name="robots" content="noindex, nofollow" />
+[[ } elsif($p{userlist} || $p{userpage} || $p{myvotes} || $p{vnlist} || $p{hist} || ($p{vnpage} && $p{vnpage}{page} eq 'stats')
+ || grep { $p{$_} && $p{$_}{change} } qw|vnpage ppage rpage|) { ]]-
+ <meta name="robots" content="noindex, follow" />
+[[ }]]-
+
+[[if($p{hist}){ ]]
+ <link rel="alternate" type="application/rss+xml" title="Recent changes" href="
+ [[= (!$p{hist}{type}?'/hist':'/'.$p{hist}{type}.$p{hist}{id}.'/hist').'/rss'.(!$p{hist}{type}?'?t='.$p{hist}{selt}.';e='.$p{hist}{sele}:$p{hist}{type} eq 'v' && $p{hist}{seli} ? '?i=1':'') ]]" />
+[[ } ]]-
+</head>
+
+<body>
+<div id="header">
+ <form id="search" method="get" action="/v/search">
+ <fieldset>
+ <legend>Search</legend>
+ <input id="searchfield" type="text" name="q" value="[[: $p{searchquery} || 'search' ]]"[[= !$p{searchquery} ? ' style="color: #999"': '' ]]- />
+ <input id="searchsubmit" type="submit" value="Search" />
+ </fieldset>
+ </form>
+ <h1><a href="/">vndb.org</a> / #vndb @ irc.synirc.net <a href="http://vndb.org/">
+ [[ if($p{devshit}) { ]]<b style="color: red">The VNDB.org Testing Grounds</b>[[ } else { ]]<b>The Visual Novel Database</b>[[ } ]]</a></h1>
+</div>
+
+
+<div id="page">
+
+<div id="content">
+[[ # = noindex-tag (see above) ]]
+[[ if($p{home}) { %d = %{$p{home}}; ]] [[+ home ]][[ } ]]
+[[ if($p{faq}) { %d = %{$p{faq}}; ]] [[+ faq ]][[ } ]]
+[[ if($p{userlogin}) { %d = %{$p{userlogin}}; ]] [[+ userlogin ]][[ } ]]
+[[ if($p{userreg}) { %d = %{$p{userreg}}; ]] [[+ userreg ]][[ } ]]
+[[ if($p{userpass}) { %d = %{$p{userpass}}; ]] [[+ userpass ]][[ } ]]
+[[ if($p{useredit}) { %d = %{$p{useredit}}; ]] [[+ useredit ]][[ } ]]
+[[ if($p{userlist}) { %d = %{$p{userlist}}; ]] [[+ userlist ]][[ }# ]]
+[[ if($p{userpage}) { %d = %{$p{userpage}}; ]] [[+ userpage ]][[ }# ]]
+[[ if($p{vnpage}) { %d = %{$p{vnpage}}; ]] [[+ vnpage ]][[ } ]]
+[[ if($p{vnedit}) { %d = %{$p{vnedit}}; ]] [[+ vnedit ]][[ } ]]
+[[ if($p{redit}) { %d = %{$p{redit}}; ]] [[+ redit ]][[ } ]]
+[[ if($p{vnbrowse}) { %d = %{$p{vnbrowse}}; ]] [[+ vnbrowse ]][[ } ]]
+[[ if($p{pbrowse}) { %d = %{$p{pbrowse}}; ]] [[+ pbrowse ]][[ } ]]
+[[ if($p{pedit}) { %d = %{$p{pedit}}; ]] [[+ pedit ]][[ } ]]
+[[ if($p{ppage}) { %d = %{$p{ppage}}; ]] [[+ ppage ]][[ } ]]
+[[ if($p{myvotes}) { %d = %{$p{myvotes}}; ]] [[+ myvotes ]][[ }# ]]
+[[ if($p{vnlist}) { %d = %{$p{vnlist}}; ]] [[+ vnlist ]][[ }# ]]
+[[ if($p{hist}) { %d = %{$p{hist}}; ]] [[+ hist ]][[ }# ]]
+[[ if($p{rpage}) { %d = %{$p{rpage}}; ]] [[+ rpage ]][[ } ]]
+[[ if($p{docs}) { %d = %{$p{docs}}; ]] [[+ docs ]][[ } ]]
+</div>
+
+
+<div id="side"><div><div>
+
+ <h2>Menu</h2>
+ <ul>
+ <li><a href="/">Home</a></li>
+ <li><a href="/v">Visual Novels</a></li>
+ <li><a href="/p">Producers</a></li>
+ <li><a href="/v/cat">Categories</a></li>
+ <li><a href="/u/list">Users</a></li>
+ <li><a href="/hist">Recent changes</a></li>
+ <li><a href="/faq">FAQ</a></li>
+ <li><a href="http://forum.vndb.org/">Forum</a></li>
+ </ul>
+
+-[[ if(!$p{AuthLoggedin}) { ]]-
+ <h2>Login</h2>
+ <form method="post" action="/nospam?/u/login" id="loginform">
+ <fieldset>
+ <legend>Login</legend>
+ <input type="text" id="usrname" name="username" />
+ <input type="password" id="usrpass" name="userpass" />
+ <input type="submit" value="Login" />
+ </fieldset>
+ </form>
+ <p>
+ <a href="/u/register">register</a> or <a href="/u/newpass">forgot password?</a>
+ </p>
+[[ } else { ]]-
+ <h2>User menu</h2>
+ <ul>
+ <li>[[: $p{AuthUsername} ]]- ([[: $p{AuthRankname} ]])</li>
+ <li><a href="/u[[= $p{AuthId} ]]/edit">My profile</a></li>
+ <li><a href="/u[[= $p{AuthId} ]]/votes">My votes</a></li>
+ <li><a href="/u[[= $p{AuthId} ]]/list">My visual novel list</a></li>
+ <li><a href="/u[[= $p{AuthId} ]]/hist">My recent changes</a></li>
+ [[ if($p{Authedit}) { ]]-
+ <li>&nbsp;</li>
+ <li><a href="/v/new">Add visual novel</a></li>
+ <li><a href="/p/add">Add producer</a></li>
+ [[ } ]]
+ <li>&nbsp;</li>
+ <li><a href="/u/logout">Logout</a></li>
+ </ul>
+[[ } ]]-
+
+-[[ #</div></div><div><div> ]]
+ <h2>Statistics</h2>
+ <ul>
+ <li><b>[[= $p{Statvn}||0 ]]</b> visual novels</li>
+ <li><b>[[= $p{Statproducers}||0 ]]</b> producers</li>
+ <li><b>[[= $p{Statreleases}||0 ]]</b> releases</li>
+ <li><b>[[= $p{Statvotes}||0 ]]</b> votes</li>
+ <li><b>[[= $p{Statusers}||0 ]]</b> users</li>
+ </ul>
+[[ if(0) { ]] <h2>Most popular</h2>
+ <ul>[[ for (@{$p{popular}}) { ]]-
+ <li><a href="/v[[: $_->{id} ]]" title="[[: $_->{title} ]]">[[: length($_->{title})>30 ? (substr($_->{title}, 0, 27).'...') : $_->{title} ]]</a></li>[[ } ]]-
+ <li class="more"><a href="/v/all?s=votes&amp;o=d">More...</a></li>
+ </ul>[[ } ]]-
+</div></div></div>
+
+</div>
+
+<div id="footer">
+ <p>
+ vndb v[[: $VNDB::VERSION ]]- |
+ <a href="mailto:contact@vndb.org">contact@vndb.org</a> |
+ designed by <a href="http://www.freecsstemplates.org/">free css templates</a>.
+ </p>
+</div>
+
+-[[ if(0 && $p{devshit}) { ]]-
+ <pre id="debug">SQL Queries used:<br />
+[[= $p{devshit} ]]
+</pre>
+[[ } ]]-
+
+</body>
diff --git a/data/tpl/pbrowse b/data/tpl/pbrowse
new file mode 100644
index 00000000..71b40c82
--- /dev/null
+++ b/data/tpl/pbrowse
@@ -0,0 +1,45 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+<p class="chr">
+ -[[= $d{chr} ne 'all' ? '<a href="/p/all">all</a>' : 'all' ]]- |
+ [[ for('a'..'z', 0) { ]]-
+ -[[ if($d{chr} eq $_) { ]][[= $_?$_:'#' ]][[ } else { ]]<a href="/p/[[= $_ ]]">[[= $_?$_:'#' ]]</a>[[ } ]]
+ [[ } ]]-
+ <form id="psearch" method="get" action="/p" accept-charset="UTF-8">
+ <fieldset>
+ <input type="text" name="q" id="q" value="[[: $d{query} ]]" class="text" />
+ <input type="submit" value="Search!" />
+ </fieldset>
+ </form>
+</p>
+
+-[[ if($#{$d{prods}} < 0) { ]]
+<p>
+ No results again, life sucks... :'(
+</p>
+[[ } else {
+ my $url = sprintf '/p/%s', $d{chr};
+ $url .= '?q='.$d{query} if $d{query};
+]]
+[[= pagebut($url) ]]
+<table id="tpd">
+ <thead><tr>
+ <td class="tc1">Name</td>
+ <td class="tc2">Type</td>
+ <td class="tc3">Main language</td>
+ <td class="tc4">Website</td>
+ </tr></thead>
+[[ for (@{$d{prods}}) { ]]-
+ <tr>
+ <td class="tc1"><a href="/p[[= $_->{id} ]]">[[: $_->{name} ]]</a></td>
+ <td class="tc2">[[: $VNDB::PROT->{$_->{type}} ]]</td>
+ <td class="tc3">[[: $VNDB::LANG->{$_->{lang}} ]]</td>
+ <td class="tc4">
+ [[ if($_->{website}) { ]]
+ <a href="[[: $_->{website} ]]">[[: length($_->{website}) > 30 ? substr($_->{website}, 0, 27).'...' : $_->{website} ]]
+ [[ } else { ]]---[[ } ]]
+ </td>
+ </tr>
+[[ } ]]-
+</table>
+[[= pagebut($url) ]]
+[[ } ]]
diff --git a/data/tpl/pedit b/data/tpl/pedit
new file mode 100644
index 00000000..6ef398cf
--- /dev/null
+++ b/data/tpl/pedit
@@ -0,0 +1,45 @@
+[[= $d{id} ? ttabs('p', $d{prod}, 'edit') : '' ]]
+<h2>[[: $p{PageTitle} ]]</h2>
+-[[ if(!$d{id}) { ]]
+ <span class="msg">
+ Please search the database before adding a new producer in order to prevent duplicate entries.
+ </span>
+[[ } else { ]]
+ <span class="msg">
+ It is currently not possible to delete producers from the database, please
+ use the <a href="http://forum.vndb.org/index.php?board=5.0">forums</a> to request
+ a deletion. Also refer to the forums for more serious edits or discussions about changes.
+ </span>
+[[ } if($d{id} && $d{prod}{cid} != $d{prod}{latest}) { ]]
+ <span class="warning">
+ You are editing an old revision of this producer. If you save it, all changes made after
+ -[[= formatdate('%Y-%m-%d %R', $d{prod}{added}) ]]- will be removed!
+ </span>
+[[ } ]]
+
+-[[= cform([
+ { type => 'error' },
+ { type => 'startform', action => $d{id} ? '/p'.$d{id}.'/edit' : '/p/add' },
+
+ { type => 'sub', title => 'General info', short => 'info' },
+ { type => 'select', name => 'Type', short => 'type', r=>1, options => [ map {
+ { short => $_, name => $VNDB::PROT->{$_} } } sort keys %$VNDB::PROT ] },
+ { type => 'input', name => 'Name (romaji)', short => 'name', r=>1 },
+ { type => 'input', name => 'Original name', short => 'original' },
+ { type => 'static', text => q|
+ The original name of the producer, leave blank if it is already in the Latin alphabet.<br /><br />| },
+
+ { type => 'select', name => 'Primary language', short => 'lang', r=>1, options => [ map {
+ ({ short => $_, name => sprintf '%s (%s)', $_, $VNDB::LANG->{$_} }) } sort keys %{$VNDB::LANG} ] },
+
+ { type => 'input', name => 'Website', short => 'website' },
+ { type => 'textarea', name => 'Description', short => 'desc', rows => 7, cols => 60 },
+
+ { type => 'sub', title => 'Edit summary', short => 'com' },
+ { type => 'textarea', name => 'Edit summary', short => 'comm', rows => 3, cols => 60 },
+ { type => 'static', text => 'Please motivate your modifications and cite all sources.' },
+
+ { type => 'submit', text => $d{id} ? 'Edit' : 'Add' },
+ { type => 'endform' },
+
+], $d{form}) ]]
diff --git a/data/tpl/ppage b/data/tpl/ppage
new file mode 100644
index 00000000..e829682a
--- /dev/null
+++ b/data/tpl/ppage
@@ -0,0 +1,58 @@
+[[= ttabs('p', $d{prod}) ]]
+<h2>[[: $p{PageTitle} ]]</h2>
+
+[[ if($d{prod}{hidden}) { ]]-
+ <span class="warning">
+ This item has been deleted from the database. File a request on the
+ <a href="http://forum.vndb.org/index.php?board=5.0">forums</a>
+ to undelete this page.
+ </span>
+[[ } ]]
+[[ if(!$d{prod}{hidden} || $p{Authdel}) { ]]-
+
+
+
+[[ if($d{change}) { ]]
+[[= cdiff($d{prev}, $d{prod},
+ [ type => 'Type', sub { $VNDB::PROT->{$_[0]} } ],
+ [ name => 'Name (romaji)', 1 ],
+ [ original => 'Original name', 1 ],
+ [ lang => 'Language', sub { $VNDB::LANG->{$_[0]} } ],
+ [ website => 'Website', 1 ],
+ [ desc => 'Description', 1, 1 ],
+ ) ]]
+[[ } ]]
+
+<dl>
+ <dt>Name</dt><dd>[[ if($d{prod}{original}) { ]]
+ [[: $d{prod}{original} ]]- ([[: $d{prod}{name} ]])
+ [[ } else { ]][[: $d{prod}{name} ]][[ } ]]</dd>
+ <dt>Type</dt><dd>[[: $VNDB::PROT->{$d{prod}{type}} ]]</dd>
+ <dt>Primary lang.</dt><dd>[[: $VNDB::LANG->{$d{prod}{lang}} ]]</dd>
+[[ if($d{prod}{website}) { ]]-
+ <dt>Links</dt><dd><a href="[[: $d{prod}{website} ]]">Official homepage</a></dd>[[ } ]]-
+</dl>
+
+-[[ if($d{prod}{desc}) { ]]
+<p>[[= summary($d{prod}{desc}) ]]<br /><br /></p>
+[[ } ]]
+
+
+<h3>Visual novel relations</h3>
+[[ if($#{$d{vn}} < 0) { ]]-
+<p>
+ We have currently no visual novels related to this producer.
+</p>
+[[ } else { ]]-
+<ul>
+ [[ for (@{$d{vn}}) { ]]-
+ <li><a href="/v[[= $_->{id} ]]">[[: $_->{title} ]]</a>
+ [[ if($_->{date} ne "0000-00-00") { ]]- ([[= datestr($_->{date}) ]])[[ } ]]
+ </li>
+ [[ } ]]-
+</ul>
+[[ } ]]
+
+
+
+[[ } ]]
diff --git a/data/tpl/redit b/data/tpl/redit
new file mode 100644
index 00000000..0e83b670
--- /dev/null
+++ b/data/tpl/redit
@@ -0,0 +1,70 @@
+[[= $d{id} ? ttabs('r', $d{rel}, 'edit') : ttabs('v', $d{vn}, 'edit') ]]-
+<h2>[[: $p{PageTitle} ]]</h2>
+
+[[ if($d{id}) { ]]
+ <span class="msg">
+ It is currently not possible to delete releases from the database, please
+ use the <a href="http://forum.vndb.org/index.php?board=5.0">forums</a> to request
+ a deletion. Also refer to the forums for more serious edits or discussions about changes.
+ </span>
+[[ } if($d{id} && $d{rel}{cid} != $d{rel}{latest}) { ]]
+ <span class="warning">
+ You are editing an old revision of this producer. If you save it, all changes made after
+ -[[= formatdate('%Y-%m-%d %R', $d{rel}{added}) ]]- will be removed!
+ </span>
+[[ } ]]
+
+[[= cform( [
+ { type => 'error' },
+ { type => 'startform', action => $d{id} ? sprintf('/r%d/edit', $d{rel}{id}) : '/v'.$d{vn}{id}.'/add', fh => 1 },
+
+ { type => 'sub', title => 'General info', short => 'info' },
+ { type => 'select', name => 'Type', short => 'type', r=>1, options => [ map {
+ ({ short => $_, name => $VNDB::RTYP->[$_] }) } 0..$#{$VNDB::RTYP} ] },
+
+ { type => 'input', name => 'Title (romaji)', short => 'title', r=>1 },
+ { type => 'input', name => 'Original title', short => 'original' },
+ { type => 'static', text => q|
+ The original title of this release, leave blank if it already is in the Latin alphabet.<br /><br />| },
+
+ { type => 'select', name => 'Language', short => 'language', r=>1, options => [ map {
+ ({ short => $_, name => sprintf '%s (%s)', $_, $VNDB::LANG->{$_} }) } sort keys %{$VNDB::LANG} ] },
+
+ { type => 'input', name => 'Official website', short => 'website' },
+ { type => 'date', name => 'Release date', short => 'released' },
+ { type => 'static', text => 'Leave month or day blank if they are unknown<br /><br />' },
+ { type => 'select', name => 'Age rating', short => 'minage', options => [ map
+ { { name => $VNDB::VRAGES->{$_}, short => $_ } } sort { $a <=> $b } keys %$VNDB::VRAGES ] },
+ { type => 'textarea', name => 'Notes', short => 'notes', rows => 3, cols => 50 },
+ { type => 'static', text => 'Miscellaneous notes/comments, information that does not fit in the above fields. E.g.: Censored/uncensored or for which releases this patch applies. Max. 250 characters.' },
+
+ { type => 'sub', title => 'Platforms & Media', short => 'pnm' },
+ { type => 'static', raw => 1, text => '<label>Platforms</label><ul class="platforms">'.join('', map { my $p = $_;
+ '<li><input type="checkbox" name="platforms" value="'.$_.'" id="'.$_.'" '.
+ (($d{form}{platforms} && grep { $p eq $_ } @{$d{form}{platforms}}) ? 'checked="checked" ':'').'/>'.
+ '<acronym class="plat '.$_.'" title="'.$VNDB::PLAT->{$_}.'">'.$_.'</acronym>'.
+ '<label for="'.$_.'">'.$VNDB::PLAT->{$_}.'</label></li>'
+ } sort { $VNDB::PLAT->{$a} cmp $VNDB::PLAT->{$b} } keys %$VNDB::PLAT).'</ul>' },
+
+ { type => 'static', text => '<br />' },
+ { type => 'jssel', name => 'Media', sh => 'md', short => 'media' },
+
+ { type => 'sub', title => 'Producers', short => 'prod' },
+ { type => 'jssel', name => 'Producers', sh => 'pd', short => 'producers' },
+
+ { type => 'sub', title => 'Visual novel relations', short => 'rel'},
+ { type => 'jssel', name => 'Relations', sh => 'vn', short => 'vn', r=>1 },
+ { type => 'static', text => q|
+ Although a release usually contains only one visual novel, it is also possible
+ for one release to include several games. Use this field to specify which
+ visual novels are included in this release.| },
+
+
+ { type => 'sub', title => 'Edit summary', short => 'com' },
+ { type => 'textarea', name => 'Edit summary', short => 'comm', rows => 3, cols => 60 },
+ { type => 'static', text => 'Please motivate your modifications and cite all sources.' },
+
+ { type => 'submit', text => $d{id} ? 'Edit' : 'Add' },
+ { type => 'endform' },
+
+], $d{form}) ]]
diff --git a/data/tpl/rpage b/data/tpl/rpage
new file mode 100644
index 00000000..7ad3c4ea
--- /dev/null
+++ b/data/tpl/rpage
@@ -0,0 +1,61 @@
+[[= ttabs('r', $d{rel}) ]]
+<h2>[[: $p{PageTitle} ]]</h2>
+
+[[ if($d{rel}{hidden}) { ]]-
+ <span class="warning">
+ This item has been deleted from the database. File a request on the
+ <a href="http://forum.vndb.org/index.php?board=5.0">forums</a>
+ to undelete this page.
+ </span>
+[[ } ]]
+[[ if(!$d{rel}{hidden} || $p{Authdel}) { ]]-
+
+
+
+[[ if($d{change}) { ]]
+[[= cdiff($d{prev}, $d{rel},
+ [ vn => 'Relations', sub { join("<br />\n", map { $_->{title} } @{$_[0]}) } ],
+ [ type => 'Type', sub { $VNDB::RTYP->[$_[0] ] } ],
+ [ title => 'Title', 1 ],
+ [ original => 'Orig. title', 1 ],
+ [ language => 'Language', sub { $VNDB::LANG->{$_[0]} } ],
+ [ website => 'Website', \&summary ],
+ [ released => 'Release date', \&datestr ],
+ [ minage => 'Age rating', sub { $VNDB::VRAGES->{$_[0]} } ],
+ [ notes => 'Notes', 1 ],
+ [ platforms => 'Platforms', sub { join(', ', sort @{$_[0]}) } ],
+ [ media => 'Media', \&mediastr ],
+ [ producers => 'Producers', sub { join(', ', map { _hchar($_->{name}) } sort { $a->{name} cmp $b->{name} } @{$_[0]}) } ],
+ ) ]]
+[[ } ]]
+
+<dl>
+ <dt>Relation</dt><dd>[[= join('<br />', map { '<a href="/v'.$_->{vid}.'">'._hchar($_->{title}).'</a>' } @{$d{rel}{vn}}) ]]</dd>
+ <dt>Type</dt><dd>[[: $VNDB::RTYP->[$d{rel}{type}] ]]</dd>
+ <dt>Title</dt><dd>[[: $d{rel}{title} ]]</dd>
+[[ if($d{rel}{original}) { ]]-
+ <dt>Original Title</dt><dd>[[: $d{rel}{original} ]]</dd>[[ } ]]-
+ <dt>Language</dt><dd>[[: $VNDB::LANG->{$d{rel}{language}} ]]</dd>
+ <dt>Release date</dt><dd>[[= datestr($d{rel}{released}) ]]</dd>
+[[ if($d{rel}{minage} >= 0) { ]]-
+ <dt>Age rating</dt><dd>[[: $VNDB::VRAGES->{$d{rel}{minage}} ]]</dd>[[ } ]]-
+[[ if($#{$d{rel}{producers}} >= 0) { ]]-
+ <dt>Producer[[: $#{$d{rel}{producers}} > 0 ? 's' : '' ]]</dt><dd>[[= join(', ', map {
+ sprintf('<a href="/p%d">%s</a>', $_->{id}, _hchar($_->{name})) } @{$d{rel}{producers}})
+ ]]</dd>[[ } ]]-
+[[ if($#{$d{rel}{platforms}} >= 0) { ]]-
+ <dt>Platform[[: $#{$d{rel}{platforms}} > 0 ? 's' : '' ]]</dt><dd>[[: join(', ', map {
+ $VNDB::PLAT->{$_} } @{$d{rel}{platforms}}) ]]</dd>[[ } ]]-
+[[ if($#{$d{rel}{media}} >= 0) { ]]-
+ <dt>Medi[[: $#{$d{rel}{media}} > 0 ? 'a' : 'um' ]]</dt><dd>[[: mediastr($d{rel}{media}) ]]</dd>[[ } ]]-
+[[ if($d{rel}{website}) { ]]-
+ <dt>Links</dt><dd><a href="[[: $d{rel}{website} ]]">Official website</a></dd>[[ } ]]-
+</dl>
+
+[[ if($d{rel}{notes}) { ]]-
+<p>[[= summary($d{rel}{notes}) ]]<br /><br /></p>
+[[ } ]]-
+
+
+
+[[ } ]]
diff --git a/data/tpl/useredit b/data/tpl/useredit
new file mode 100644
index 00000000..f470ce0a
--- /dev/null
+++ b/data/tpl/useredit
@@ -0,0 +1,34 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+
+-[[ if($d{done}) { ]]
+<span class="msg">
+ Settings succesfully saved.
+</span>
+[[ } ]]
+-[[= cform( [
+ { type => 'error' },
+ { type => 'startform', action => '/u'.$d{user}.'/edit' },
+
+ { type => 'sub', title => 'General info', short => 'info' },
+ { type => 'static', name => 'Username', text => _hchar($d{form}{username}) },
+ { type => 'input', name => 'Email', short => 'mail' },
+
+ { type => 'sub', title => 'Change password', short => 'pass' },
+ { type => 'static', text => 'Leave blank to keep your current password.' },
+ { type => 'pass', name => 'Password', short => 'pass1' },
+ { type => 'pass', name => 'Confirm', short => 'pass2' },
+
+ { type => 'sub', title => 'Miscellaneous options', short => 'misc' },
+ { type => 'check', short => 'pvotes', name => sprintf 'Allow other people to see my votes (<a href="/u%d/votes">/u%1$d/votes</a>)', $d{user} },
+ { type => 'check', short => 'plist', name => sprintf 'Allow other people to see my visual novel list (<a href="/u%d/list">/u%1$d/list</a>)', $d{user} },
+ { type => 'check', short => 'pign_nsfw', name => 'Disable warnings for images that are not safe for work.' },
+
+ $d{adm} ? (
+ { type => 'sub', title => 'Admin', short => 'adm' },
+ { type => 'select', name => 'Rank', short => 'rank', options => [
+ map { { name => $VNDB::VNDBopts{ranks}[0][0][$_], short => $_ } } 1..($#{$VNDB::VNDBopts{ranks}}-1) ] },
+ ) : (),
+
+ { type => 'submit', text => 'Save' },
+ { type => 'endform' },
+], $d{form}) ]]
diff --git a/data/tpl/userlist b/data/tpl/userlist
new file mode 100644
index 00000000..4fcb12c7
--- /dev/null
+++ b/data/tpl/userlist
@@ -0,0 +1,54 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+<p class="chr">
+ -[[= $d{chr} ne 'all' ? '<a href="/u/list/all">all</a>' : 'all' ]]- |
+ [[ for('a'..'z', 0) { ]]-
+ -[[ if($d{chr} eq $_) { ]][[= $_?$_:'#' ]][[ } else { ]]<a href="/u/list/[[= $_ ]]">[[= $_?$_:'#' ]]</a>[[ } ]]
+ [[ } ]]-
+ <br /><br />
+</p>
+
+[[ if($#{$d{users}} < 0) { ]]-
+<p>
+ No users found...
+</p>
+[[ } else {
+ my $url = sprintf '/u/list/%s', $d{chr};
+ my $surl = sprintf '%s?s=%s&amp;o=%s', $url, $d{order}[0], $d{order}[1];
+]]
+[[= pagebut($surl) ]]-
+<table id="tul">
+ <thead><tr>
+ <td class="tc1">Username [[= sortbut($url, 'username') ]]</td>
+[[ if($p{Authuserlist}) { ]]-
+ <td class="tc2">Mail [[= sortbut($url, 'mail') ]]</td>
+ <td class="tc3">Rank [[= sortbut($url, 'rank') ]]</td>[[ } ]]-
+ <td class="tc4">Registered [[= sortbut($url, 'registered') ]]</td>
+ <td class="tc5">VN list</td>
+ <td class="tc6">Votes</td>
+ <td class="tc7">Changes</td>
+[[ if($p{Authuseredit}) { ]]-
+ <td class="tc8">&nbsp;</td>[[ } ]]-
+ </tr></thead>
+ [[ for (@{$d{users}}) { ]]-
+ <tr>
+ <td class="tc1"><a href="/u[[= $_->{id} ]]">[[: $_->{username} ]]</a></td>
+[[ if($p{Authuserlist}) { ]]-
+ <td class="tc2">[[: $_->{mail} ]]</td>
+ <td class="tc3">[[: $VNDB::VNDBopts{ranks}[0][0][$_->{rank}] ]]</td>[[ } ]]-
+ <td class="tc4">[[= formatdate('%Y-%m-%d', $_->{registered}, 'wd') ]]</td>
+ <td class="tc5">[[ if($_->{flags} & $VNDB::UFLAGS->{list} && $_->{vnlist}) { ]]
+ <a href="/u[[= $_->{id} ]]/list" title="[[: $_->{username} ]]'s visual novel list">[[= $_->{vnlist} ]]</a>
+ [[ } else { ]][[= $_->{flags} & $VNDB::UFLAGS->{list} ? 0 : '-' ]][[ } ]]</td>
+ <td class="tc6">[[ if($_->{flags} & $VNDB::UFLAGS->{votes} && $_->{votes}) { ]]
+ <a href="/u[[= $_->{id} ]]/votes" title="[[: $_->{username} ]]'s votes">[[= $_->{votes} ]]</a>
+ [[ } else { ]][[= $_->{flags} & $VNDB::UFLAGS->{votes} ? 0 : '-' ]][[ } ]]</td>
+ <td class="tc7">[[ if($_->{changes}) { ]]
+ <a href="/u[[= $_->{id} ]]/hist" title="Recent changes by -[[: $_->{username} ]]">[[= $_->{changes} ]]</a>
+ [[ } else { ]]0[[ } ]]</td>
+[[ if($p{Authuseredit}) { ]]-
+ <td class="tc8">( <a href="/u[[= $_->{id} ]]/edit">edit</a> )</td>[[ } ]]-
+ </tr>
+ [[ } ]]-
+</table>
+-[[= pagebut($surl) ]]-
+[[ } ]]
diff --git a/data/tpl/userlogin b/data/tpl/userlogin
new file mode 100644
index 00000000..b4af29d2
--- /dev/null
+++ b/data/tpl/userlogin
@@ -0,0 +1,14 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+-[[= cform( [
+ { type => 'error' },
+ { type => 'startform', action => '/u/login' },
+ { type => 'input', short => 'username', name => 'Username' },
+ { type => 'pass', short => 'userpass', name => 'Password' },
+ { type => 'submit', text => 'Login!' },
+ { type => 'endform' },
+], $d{log}) ]]-
+
+<p>
+ <br /><br />
+ <a href="/u/register">No account yet</a>, or <a href="/u/newpass">forgot your username or password?</a>
+</p>
diff --git a/data/tpl/userpage b/data/tpl/userpage
new file mode 100644
index 00000000..9b14efc9
--- /dev/null
+++ b/data/tpl/userpage
@@ -0,0 +1,13 @@
+[[
+ ($d{pv}, $d{pl}) = ($d{user}{flags} & $VNDB::UFLAGS->{votes}, $d{user}{flags} & $VNDB::UFLAGS->{list});
+]]
+<h2>[[: $p{PageTitle} ]]</h2>
+<dl>
+ <dt>Username</dt><dd>[[: $d{user}{username} ]]- (<a href="/u[[= $d{user}{id} ]]">u[[= $d{user}{id} ]]</a>)</dd>
+ <dt>Registered</dt><dd>[[= formatdate('%Y-%m-%d', $d{user}{registered}) ]]</dd>
+ <dt>Votes</dt><dd>[[= $d{pv} ? $d{user}{votes}.' (<a href="/u'.$d{user}{id}.'/votes">view all</a>)' : '(hidden)' ]]</dd>
+ <dt>VN List</dt><dd>[[= $d{pl} ? $d{user}{vnlist}.' (<a href="/u'.$d{user}{id}.'/list">view all</a>)' : '(hidden)' ]]</dd>
+ <dt>Changes</dt><dd>[[= $d{user}{changes}.($d{user}{changes}>0?' (<a href="/u'.$d{user}{id}.'/hist">recent changes</a>)':'') ]]</dd>
+</dl>
+
+[[= T_vnpage_stats($X) ]]
diff --git a/data/tpl/userpass b/data/tpl/userpass
new file mode 100644
index 00000000..c3b04840
--- /dev/null
+++ b/data/tpl/userpass
@@ -0,0 +1,21 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+<p>
+ You're lucky that vndb has a very advanced password recovery tool! Just
+ type your email address (the same one you used for your account), and
+ wait for an email!
+</p>
+
+-[[ if(!$d{done}) { ]]
+[[= cform( [
+ { type => 'error', },
+ { type => 'startform', action => '/u/newpass' },
+ { type => 'input', short => 'mail', name => 'Email' },
+ { type => 'submit', text => 'Gimme my password!' },
+ { type => 'endform' },
+], $d{pas} ) ]]
+
+[[ } else { ]]
+<span class="msg">
+ Your password succesfully been reset. Check your mail for instructions.
+</span>
+[[ } ]]
diff --git a/data/tpl/userreg b/data/tpl/userreg
new file mode 100644
index 00000000..68565470
--- /dev/null
+++ b/data/tpl/userreg
@@ -0,0 +1,38 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+
+-[[ if($d{denied}) { ]]
+[[ } ]]-
+
+<br /><br />
+<h3>Why should I register?</h3>
+<p>
+ Registered users have access to special features on this site:
+</p>
+<ul>
+ <li>You can keep track of the visual novels you'd like to play or have
+ finnished playing,</li>
+ <li>Vote on visual novels,</li>
+ <li>And more importantly: you can add and edit all information on the
+ website!</li>
+</ul>
+<p>
+ <br />
+ And of course, registering an account is (and will always remain)
+ completely free!
+ <br /><br />
+</p>
+
+-[[= cform( [
+ { type => 'error' },
+ { type => 'startform', action => '/u/register' },
+ { type => 'input', short => 'username', name => 'Username' },
+ { type => 'input', short => 'mail', name => 'Email' },
+ { type => 'static', text => q|
+ Your email address will only be used in case you lose your password, at least for now.
+ We will never send spam or newsletters unless you explicitly ask us for it.
+ | },
+ { type => 'pass', short => 'pass1', name => 'Password' },
+ { type => 'pass', short => 'pass2', name => 'Confirm pass.' },
+ { type => 'submit', text => 'Register!' },
+ { type => 'endform' },
+], $d{reg}) ]]
diff --git a/data/tpl/vnbrowse b/data/tpl/vnbrowse
new file mode 100644
index 00000000..79dd122e
--- /dev/null
+++ b/data/tpl/vnbrowse
@@ -0,0 +1,87 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+
+[[ if($d{chr} eq 'cat') { ]]-
+<ul id="cat">
+[[ for my $c (qw| e g p t l s |) { ]]-
+ -[[= $c ne 'l' && $c ne 'p' ? '<li>' : '<br />' ]][[: $VNDB::CAT->{$c}[0] ]]-
+ <ul>
+ [[ for (sort keys %{$VNDB::CAT->{$c}[1]}) { ]]-
+ <li class="cat_[[= $c.$_ ]][[= $d{incl} =~ /$c$_/ ? ' inc' : $d{excl} =~ /$c$_/ ? ' exc' : '' ]]">
+ [[: $VNDB::CAT->{$c}[1]{$_} ]]- ([[= $d{cat}{$c.$_} || 0 ]])</li>
+ [[ } ]]
+ </ul>[[= $c ne 't' && $c ne 'g' ? '</li>' : '' ]]-
+[[ } ]]-
+</ul>
+<div id="lfilter">
+ <b>Languages</b> (none selected means all)<br />
+[[ for (sort keys %{$d{lang}}) { next if !$d{lang}{$_}; ]]-
+ <input type="checkbox" name="lang_[[= $_ ]]" id="lang_[[= $_ ]]" value="1" -[[= $d{slang}=~/$_/?'checked="checked"':'' ]]>
+ <label for="lang_[[= $_ ]]">[[: $VNDB::LANG->{$_} ]]- ([[= $d{lang}{$_} ]])</label>
+[[ } ]]-
+</div>
+<br style="clear: left" />
+<input type="button" class="right" id="catsearch" name="catsearch" value="Search!" />
+<br style="clear: left" />
+<br />
+<br />
+
+[[ } elsif($d{chr} ne 'search') { ]]-
+<p class="chr">
+ -[[= $d{chr} ne 'all' ? '<a href="/v/all">all</a>' : 'all' ]]- |
+ [[ for('a'..'z', 0) { ]]-
+ -[[ if($d{chr} eq $_) { ]][[= $_?$_:'#' ]][[ } else { ]]<a href="/v/[[= $_ ]]">[[= $_?$_:'#' ]]</a>[[ } ]]
+ [[ } ]]-
+ <br /><br />
+</p>
+[[ } ]]-
+
+-[[ if($#{$d{vn}} < 0) { ]]
+<p>
+ -[[ if($d{chr} eq 'cat' && !$d{scat}[0][0] && !$d{scat}[0][1]) { ]]
+ Select some categories and hit the "Search" button to get a list of visual novels. Click on a
+ category again to exclude it.<br />
+ Please keep in mind that not all visual novels have the correct categories set, so you
+ may not always find what you are looking for.
+ [[ } else { ]]
+ No results again, life sucks... :'(
+ [[ } ]]-
+</p>
+[[ } else {
+ my %url = (
+ $p{searchquery} ? ( q => $p{searchquery} ) : (),
+ $d{incl} ? ( i => $d{incl} ) : (),
+ $d{excl} ? ( e => $d{excl} ) : (),
+ $d{slang} ? ( l => $d{slang} ) : (),
+ );
+ my %urls = ( %url,
+ $d{order}[0] ne 'title' ? ( s => $d{order}[0] ) : (),
+ $d{order}[1] ne 'a' ? ( o => $d{order}[1] ) : (),
+ );
+ my $url = sprintf '/v/%s', $d{chr};
+ my $urls = $url;
+ $urls .= '?'.join(';', map { $_.'='.$urls{$_} } keys %urls) if keys %urls;
+ $url .= '?'.join(';', map { $_.'='.$url{$_} } keys %url) if keys %url;
+]]
+
+[[= pagebut($urls) ]]
+<table id="tbv">
+ <thead><tr>
+ <td class="tc1">Title [[= sortbut($url, 'title') ]]</td>
+ <td class="tc2">Released [[= sortbut($url, 'released') ]]</td>
+ <td class="tc3">Languages</td>
+ <td class="tc4">Rating [[= sortbut($url, 'votes') ]]</td>
+ </thead></tr>
+ [[ for (@{$d{vn}}) {
+ $_->{c_votes} =~ s#^([0-9]{2}.[0-9]{2})\|([0-9]{4})$#$1 == 0 ? sprintf '- (%d)', $2 : sprintf '%.2f (%d)', $1, $2#e;
+ $_->{c_released} =~ s#^([0-9]{4})([0-9]{2}).+#$1==0?'N/A':$1==9999?'TBA':(($2&&$2>0?($Time::CTime::MoY[$2-1].' '):'').$1)#e;
+ ]]-
+ <tr>
+ <td class="tc1"><a href="/v[[= $_->{id} ]]">[[: $_->{title} ]]</a></td>
+ <td class="tc2">[[: $_->{c_released} ]]</td>
+ <td class="tc3">[[: $_->{c_languages} || 'N/A' ]]</td>
+ <td class="tc4">[[: $_->{c_votes} ]]</td>
+ </tr>
+ [[ } ]]-
+</table>
+[[= pagebut($urls) ]]
+[[ } ]]
diff --git a/data/tpl/vnedit b/data/tpl/vnedit
new file mode 100644
index 00000000..f3ae245c
--- /dev/null
+++ b/data/tpl/vnedit
@@ -0,0 +1,94 @@
+[[= $d{id} ? ttabs('v', $d{vn}, 'edit') : '' ]]-
+<h2>[[: $p{PageTitle} ]]</h2>
+
+[[ if(!$d{id}) { ]]
+ <span class="msg">Please search the database before adding a new visual novel
+ in order to prevent duplicate entries.</span>
+[[ } else { ]]
+ <span class="msg">
+ It is currently not possible to delete visual novels from the database, please
+ use the <a href="http://forum.vndb.org/index.php?board=5.0">forums</a> to request
+ a deletion. Also refer to the forums for more serious edits or discussions about changes.
+ </span>
+[[ } if($d{id} && $d{vn}{cid} != $d{vn}{latest}) { ]]
+ <span class="warning">
+ You are editing an old revision of this producer. If you save it, all changes made after
+ -[[= formatdate('%Y-%m-%d %R', $d{vn}{added}) ]]- will be removed!
+ </span>
+[[ } ]]
+
+
+-[[= cform([
+ { type => 'error' },
+ { type => 'startform', action => $d{id} ?( '/v'.$d{id}.'/edit') : '/v/new', upload => 1, fh => 1 },
+
+ { type => 'sub', title => 'General info', short => 'info' },
+ { type => 'input', name => 'Title', short => 'title', r=>1 },
+ { type => 'static', text => q|
+ Use official English title if available, use the romanized version of the official title otherwise.
+ Other titles can be added at a later time when specifying releases.<br /><br />| },
+
+ { type => 'textarea', name => 'Aliases', short => 'alias', rows => 2, cols => 60 },
+ { type => 'static', text => q|
+ Comma seperated list of alternative titles or abbreviations. Can include both official
+ (japanese/english) titles and unofficial titles used around net. <b>Titles that are listed in the releases do not have to be added here.</b><br /><br />| },
+
+ { type => 'textarea', name => 'Description', short => 'desc', rows => 7, cols => 70, r=>1 },
+ { type => 'static', text => q|
+ Short description of the main story. Please do not include spoilers, and don't forget to list the source
+ in case you didn't write the description yourself. ([url] BBCode tag is allowed)<br /><br />| },
+
+ { type => 'select', name => 'Length', short => 'length', class => 'longopts', options => [ map {
+ { short => $_,
+ name => !$_?$VNDB::VNLEN->[$_][0]:($VNDB::VNLEN->[$_][0].', '.$VNDB::VNLEN->[$_][1].' ('.$VNDB::VNLEN->[$_][2].')') } } 0..$#$VNDB::VNLEN
+ ] },
+ { type => 'static', text => '<br />' },
+ { type => 'input', name => 'External links', short => 'l_wp', pre => 'http://en.wikipedia.org/wiki/' },
+ { type => 'input', name => '&nbsp;', short => 'l_vnn', pre => 'http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id=', class => 'shortopts' },
+ { type => 'input', name => '&nbsp;', short => 'l_cisv', pre => 'http://cisvisual.net/title/', class => 'shortopts' },
+
+ { type => 'sub', title => 'Categories', short => 'cat' },
+ { type => 'hidden', short => 'categories' },
+ { type => 'static', raw => 1, text => eval {
+ my $r = '<ul id="cat">';
+ for my $c (qw| e g p t l s |) {
+ $r .= ($c ne 'l' && $c ne 'p' ? '<li>' : '<br />').$VNDB::CAT->{$c}[0].'<ul>';
+ for (sort keys %{$VNDB::CAT->{$c}[1]}) {
+ $r .= sprintf '<li><a href="#" id="cat_%1$s"><b id="b_%1$s">-</b> %2$s</a></li>',
+ $c.$_, $VNDB::CAT->{$c}[1]{$_};
+ }
+ $r .= '</ul>'.($c ne 't' && $c ne 'g' ? '</li>' : '');
+ }
+ $r.'</ul>';
+ } },
+
+ { type => 'sub', title => 'Image', short => 'img' },
+ $d{id} ? (
+ { type => 'static', text => $d{vn}{image} ?
+ sprintf '<img src="%s/cv/%02d/%d.jpg" style="float: right" />', $p{st}, $d{vn}{image}%50, $d{vn}{image} :
+ 'No image uploaded yet...' },
+ ) : (),
+ { type => 'upload', name => $d{vn}{image} ? 'Change' : 'Upload', short => 'img' },
+ { type => 'static', text => q|
+ Preferably the cover of the CD/DVD/package. Image must be in JPEG format and at most 256x400px and 50KB.<br /><br />| },
+ { type => 'check', short => 'img_nsfw', name => '<b>NSFW.</b> Please check this option if the image contains nudity, gore, or is otherwise not safe in a work-friendly environment.' },
+
+ { type => 'sub', title => 'Visual novel relations', short => 'rel' },
+ { type => 'jssel', name => 'Relations', short => 'relations', sh => 'rl' },
+ { type => 'static', text => q|
+ <b>Direct relations:</b> Please only add direct relations. E.g. the sequel of a sequel does not have to be listed
+ here because it's already listed on an other visual novel that is in turn listed here. VNDB will handle these
+ relations automatically.<br />
+ <b>Reverse relations:</b> If you add a relation with an other visual novel here, the same (or "reverse") relation
+ will automatically be added to the other visual novel. For example: if you add Tsukihime as a prequel of Kagetsu Tohya,
+ Kagetsu Tohya will automatically be added as a sequel for Tsukihime.
+ |},
+
+ { type => 'sub', title => 'Edit summary', short => 'com' },
+ { type => 'textarea', name => 'Edit summary', short => 'comm', rows => 3, cols => 60 },
+ { type => 'static', text => 'Please motivate your modifications and cite all sources.' },
+
+ { type => 'submit', text => $d{id} ? 'Edit' : 'Add' },
+ { type => 'endform' },
+
+], $d{form}) ]]
diff --git a/data/tpl/vnlist b/data/tpl/vnlist
new file mode 100644
index 00000000..64db1c05
--- /dev/null
+++ b/data/tpl/vnlist
@@ -0,0 +1,74 @@
+<h2>[[: $p{PageTitle} ]]</h2>
+[[
+ my $url = sprintf '/u%d/list', $d{user}{id};
+ my $surl = sprintf '%s?s=%s;o=%s', $url, $d{order}[0], $d{order}[1];
+ my $purl = $surl . ';t='.$d{status};
+ my $sourl = $url . '?t='.$d{status};
+ my $furl = $purl . ';p='.$d{page};
+]]
+<p class="chr">
+ status: -[[ for (-1..$#$VNDB::LSTAT) { if($_ >= 0) { ]]- | -[[ }
+ if($d{status} == $_) { ]]<b>[[= $_ eq -1 ? 'all' : lc $VNDB::LSTAT->[$_] ]]</b>[[ }
+ else { ]]<a href="[[= $surl ]]&amp;t=[[= $_ ]]">[[= $_ eq -1 ? 'all' : lc $VNDB::LSTAT->[$_] ]]</a>[[ } } ]]
+ <br /><br />
+</p>
+
+
+[[ if($#{$d{list}} < 0) { ]]-
+<p>
+[[ if($d{status} >= 0) { ]]
+ No results found...
+[[ } elsif($d{user}{username} eq $p{AuthUsername}) { ]]
+ Your visual novel list is empty. You can keep track of all the visual novels
+ you'd like to play, you're currently playing, or you've finished. Just go to
+ a visual novel page and add it to your VN list!
+[[ } else { ]]
+ [[: $d{user}{username} ]]'s visual novel list is empty...
+[[ } ]]
+</p>
+
+[[ } else { ]]
+[[= pagebut($purl) ]]-
+[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
+<form method="post" action="[[= $furl ]]" class="tblf">
+<input type="hidden" class="hidden" name="comments" id="comments" value="" />[[ } ]]
+<table id="tvl">
+ <thead><tr>
+ <td class="tc1">Title [[= sortbut($sourl, 'title') ]]</td>
+ <td class="tc2">Status</td>
+ <td class="tc3">Added [[= sortbut($sourl, 'date') ]]</td>
+ [[ if($d{user}{username} eq $p{AuthUsername}) { ]]-
+ <td class="tc4">Personal note</td>
+ <td class="tc5">&nbsp;</td>[[ } ]]-
+ </tr></thead>
+ [[ for (@{$d{list}}) { ]]-
+ <tr>
+ <td class="tc1"><a href="/v[[= $_->{vid} ]]" title="[[: $_->{title} ]]">[[: length($_->{title})>40 ? substr($_->{title},0, 37).'...' : $_->{title} ]]</a></td>
+ <td class="tc2">[[= $VNDB::LSTAT->[$_->{status}] ]]</td>
+ <td class="tc3">[[= formatdate('%Y-%m-%d', $_->{date}, 'dh') ]]</td>
+ [[ if($d{user}{username} eq $p{AuthUsername}) { ]]
+ <td class="tc4">[[: $_->{comments}||'-' ]]</td>
+ <td class="tc5"><input type="checkbox" name="sel" value="[[= $_->{vid} ]]" /></td>[[ } ]]
+ </tr>
+ [[ } ]]-
+</table>
+[[ if($d{user}{username} eq $p{AuthUsername}) { ]]
+<select id="vnlistchange" name="vnlistchange" class="right">
+ <option value="-3">- with selected -</option>
+ <option value="-1">Delete</option>
+ <option value="-2">Update personal note</option>
+ <optgroup label="Update status:">
+ [[ for (0..$#$VNDB::LSTAT) { ]]-
+ <option value="[[= $_ ]]">[[: $VNDB::LSTAT->[$_] ]]</option>
+ [[ } ]]
+ </optgroup>
+</select>
+</form>[[ } ]]
+-[[= pagebut($purl) ]]
+[[ } ]]-
+
+[[ if($d{user}{username} eq $p{AuthUsername}) { ]]-
+<p>
+ <br /><br />
+ NOTE: Your personal notes are only visible to you, other people can't see them.
+</p>[[ } ]]
diff --git a/data/tpl/vnpage b/data/tpl/vnpage
new file mode 100644
index 00000000..15cb3235
--- /dev/null
+++ b/data/tpl/vnpage
@@ -0,0 +1,171 @@
+[[= ttabs('v', $d{vn}) ]]
+<h2>[[: $d{vn}{title} ]]</h2>
+
+[[ if($d{vn}{hidden}) { ]]-
+ <span class="warning">
+ This item has been deleted from the database. File a request on the
+ <a href="http://forum.vndb.org/index.php?board=5.0">forums</a>
+ to undelete this page.
+ </span>
+[[ } ]]
+[[ if(!$d{vn}{hidden} || $p{Authdel}) { ]]-
+
+
+[[ if($d{change}) { ]]
+[[= cdiff($d{prev}, $d{vn},
+ [ title => 'Title', 1 ],
+ [ alias => 'Alias', 1 ],
+ [ desc => 'Description', 1, 1 ],
+ [ length => 'Length', sub { $VNDB::VNLEN->[$_[0] ][0] } ],
+ [ l_wp => 'Wikipedia link', sub { $_[0] ? '<a href="http://en.wikipedia.org/wiki/'.$_[0].'">'.$_[0].'</a>' : 'No link' } ],
+ [ l_vnn => 'V-N.net link', sub { $_[0] ? '<a href="http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id='.$_[0].'">'.$_[0].'</a>' : 'No link' } ],
+ [ l_cisv => 'CISVisual link', sub { $_[0] ? '<a href="http://cisvisual.net/title/'.$_[0].'">'.$_[0].'</a>' : 'No link' } ],
+ [ categories => 'Categories', sub { join(' ', map { $VNDB::CAT->{substr($_->[0],0,1)}[1]{substr($_->[0],1,2)}.'('.$_->[1].')' } sort { $a->[0] cmp $b->[0] } @{$_[0]}) || 'No categories selected' }, 1 ],
+ [ relations => 'Relations', sub { join("<br />\n", map { $VNDB::VREL->[$_->{relation}].': '._hchar($_->{title}) } sort { $a->{id} <=> $b->{id} } @{$_[0]}) } ],
+ [ image => 'Image', sub { $_[0] ? sprintf '<img src="%s/cv/%02d/%d.jpg" />', $p{st}, $_[0]%50, $_[0] : 'No image'; } ],
+ [ img_nsfw => 'NSFW', sub { $_[0] ? 'Not safe' : 'Safe' } ]
+ ) ]]
+[[ } ]]-
+
+[[
+ my @lang;
+ for (@{$d{rel}}) {
+ my $l = $_->{language};
+ next if grep { $_ eq $l } @lang;
+ push @lang, $l;
+ }
+
+]]
+
+
+<div id="vnheader">
+<div>
+[[ if($d{vn}{image}) { ]]
+ [[ if($d{vn}{img_nsfw} && !$p{AuthNsfw}) { ]]
+ <img src="[[: $p{st} ]]/cv/nsfw.png" id="nsfw" class="[[: $p{st} ]]/cv/[[= sprintf '%02d/%d', $d{vn}{image}%50, $d{vn}{image} ]].jpg" />
+ [[ } else { ]]
+ <img src="[[: $p{st} ]]/cv/[[= sprintf '%02d/%d', $d{vn}{image}%50, $d{vn}{image} ]].jpg" alt="[[: $p{PageTitle} ]]" />
+ [[ } ]]
+[[ } else { ]]-
+ No image uploaded yet...
+[[ } ]]-
+</div>
+
+
+-[[ if($p{AuthLoggedin}) { ]]
+<p class="mod">&lt; user options -
+ <a href="/u[[= $p{AuthId} ]]/votes" rel="voteDD" class="dropdown">[[= $d{vote}{vid} ? 'your vote: '.$d{vote}{vote} : 'vote' ]]</a>
+- <a href="/u[[= $p{AuthId} ]]/list" rel="listDD" class="dropdown">[[= !$d{list}{vid} ? 'add to vn list' : 'status: '.lc $VNDB::LSTAT->[$d{list}{status}] ]]</a>
+&gt;</p>
+[[ } ]]-
+
+-[[
+ $d{vn}{c_votes} =~ s#^([0-9]{2}.[0-9]{2})\|([0-9]{4})$#$2 == 0 ? 'No votes yet' :
+ $1 == 0 ? sprintf 'N/A (%d vote%s)', $2, $2>1?'s':'' : sprintf '%.2f (%d vote%s)', $1, $2, $2>1?'s':''#e;
+
+ my @links = (
+ $d{vn}{l_wp} ? [ 'Wikipedia', 'http://en.wikipedia.org/wiki/%s', $d{vn}{l_wp} ] : (),
+ $d{vn}{l_vnn} ? [ 'V-N.net', 'http://visual-novels.net/vn/index.php?option=com_content&amp;task=view&amp;id=%d', $d{vn}{l_vnn} ] : (),
+ $d{vn}{l_cisv} ? [ 'CISVisual', 'http://cisvisual.net/title/%d', $d{vn}{l_cisv} ] : (),
+ );
+
+if($d{vn}{length} || $d{vn}{alias} || @links) { ]]
+ <h3>General info</h3>
+ <dl>
+ [[ if($d{vn}{length}) { ]]-
+ <dt>Length</dt><dd>[[: $VNDB::VNLEN->[$d{vn}{length}][0] ]]- ([[: $VNDB::VNLEN->[$d{vn}{length}][1] ]])</dd>[[ } ]]-
+ [[ if($d{vn}{alias}) { ]]-
+ <dt>Aliases</dt><dd>[[: $d{vn}{alias} ]]</dd>[[ } ]]-
+ [[ if(@links > 0) { ]]
+ <dt>Links</dt><dd>[[= join(', ', map { '<a href="'.sprintf($_->[1],$_->[2]).'">'.$_->[0].'</a>' } @links) ]]</dd>[[ } ]]-
+ </dl>
+[[ } ]]-
+
+ [[ if(@{$d{vn}{categories}}) { my %nolvl = (pli=>1,pbr=>1,gaa=>1,gab=>1); ]]-
+ <h3>Categories</h3>
+ <dl class="vncat">
+ [[ for (sort keys %$VNDB::CAT) {
+ my $c = $_;
+ my @c = map { my $s=$_;
+ my ($cs) = grep { $_->[0] eq $c.$s } @{$d{vn}{categories}};
+ $cs ? sprintf('<i class="crgn%d">%s</i>', $nolvl{$c.$_}?0:$cs->[1], $VNDB::CAT->{$c}[1]{$s})
+ : ()
+ } sort keys %{$VNDB::CAT->{$c}[1]};
+ if(@c) { ]]-
+ <dt>[[: $VNDB::CAT->{$c}[0] ]]</dt><dd>[[= join(', ', @c) ]]</dd>
+ [[ } } ]]
+ </dl>
+ [[ } ]]-
+
+ [[ if($#{$d{vn}{relations}} >= 0) { ]]-
+ <h3>[[= $d{page} eq 'rg' ? 'Relations' : '<a href="/v'.$d{vn}{id}.'/rg">Relations</a>' ]]</h3>
+ <dl class="vnrel">
+ [[ my $lrel = -1; my $i=0; for (sort { $a->{relation} <=> $b->{relation} } @{$d{vn}{relations}}) {
+ if($_->{relation} != $lrel) { $lrel=$_->{relation}; if($i) { ]]</dd>[[ } ]]-
+ <dt>[[: $VNDB::VREL->[$lrel] ]]</dt><dd><a href="/v[[= $_->{id} ]]">[[: $_->{title} ]]</a>
+ [[ } else { ]]<br /><a href="/v[[= $_->{id} ]]" title="[[: $_->{title} ]]">[[: shorten $_->{title}, 40 ]]</a>[[ }
+ ++$i;} ]]
+ </dl>
+ [[ } ]]-
+
+ [[ if(@lang && grep { @{$_->{producers}} } @{$d{rel}}) { ]]-
+ <h3>Producers</h3>
+ <dl>
+ [[ for my $l (@lang) { my %l;
+ $_->{language} eq $l && (%l = ( %l, map {
+ sprintf('<a href="/p%d" title="%s">%s</a>',
+ $_->{id}, _hchar($_->{name}), _hchar shorten $_->{name}, 30) => 1
+ } @{$_->{producers}} )) for (@{$d{rel}});
+ if(keys %l) { ]]-
+ <dt>[[: $VNDB::LANG->{$l} ]]</dt><dd>[[= join(' &amp; ', keys %l) ]]</dd>
+ [[ } } ]]
+ </dl>
+ [[ } ]]-
+
+ <h3>[[= $d{page} eq 'stats' ? 'User stats' : '<a href="/v'.$d{vn}{id}.'/stats">User stats</a>' ]]</h3>
+ <dl>
+ <dt>Rating</dt><dd>[[: $d{vn}{c_votes} ]]</dd>
+ </dl>
+</div>
+
+-[[
+ my @lnks = (
+ !$d{page} ? '<b>description &amp; releases</b>' : '<a href="/v'.$d{vn}{id}.'">description &amp; releases</a>',
+ $d{page} eq 'stats' ? '<b>stats</b>' : '<a href="/v'.$d{vn}{id}.'/stats">stats</a>',
+ $d{vn}{rgraph} ? (
+ $d{page} eq 'rg' ? '<b>relations</b>' : '<a href="/v'.$d{vn}{id}.'/rg">relations</a>',
+ ) : (),
+ );
+]]
+<p class="opts">- -[[= join(' - ', @lnks) ]]- -</p>
+
+[[ if(!$d{page}) { ]][[+ vnpage_rel ]][[ } ]]
+[[ if($d{page} eq 'stats') { ]][[+ vnpage_stats ]][[ } ]]
+[[ if($d{page} eq 'rg') { ]][[+ vnpage_rg ]][[ } ]]
+
+[[ if($p{AuthLoggedin}) { ]]-
+
+<div class="dropdown" id="voteDD">
+ <ul>
+ [[ if($d{vote}{vid}) { ]]-
+ <li><a href="/v[[= $d{vn}{id} ]]/vote?v=-1">revoke</a></li>
+ [[ } for (reverse 1..10) { ]]-
+ <li class="center"><a href="/v[[= $d{vn}{id} ]]/vote?v=[[= $_ ]]">[[= $_ ]]</a></li>
+ [[ } ]]
+ </ul>
+</div>
+
+<div class="dropdown" id="listDD">
+ <ul>
+ [[ for (0..$#$VNDB::LSTAT) { ]]-
+ <li><a href="/v[[= $d{vn}{id} ]]/list?s=[[= $_ ]]" [[= $_ == 6 ? ' id="askcomment"' : '' ]]>[[: $VNDB::LSTAT->[$_] ]]</a></li>
+ [[ } if($d{list}{vid}) { ]]-
+ <li><a href="/v[[= $d{vn}{id} ]]/list?s=-1">Remove</a></li>
+ [[ } ]]-
+ </ul>
+</div>
+
+[[ } ]]
+
+
+[[ } ]]
diff --git a/data/tpl/vnpage_rel b/data/tpl/vnpage_rel
new file mode 100644
index 00000000..f2570548
--- /dev/null
+++ b/data/tpl/vnpage_rel
@@ -0,0 +1,51 @@
+<h3>Description</h3>
+<p class="desc">
+ [[= summary($d{vn}{desc}) ]]
+ <br /><br /><br />
+</p>
+
+
+
+[[
+ my @lang;
+ for (@{$d{rel}}) {
+ my $l = $_->{language};
+ next if grep { $_ eq $l } @lang;
+ push @lang, $l;
+ }
+
+]]
+
+
+<h3>Releases
+[[ if((!$d{vn}{locked} && $p{Authedit}) || $p{Authlock}) { ]]- <p class="actions">(<a href="/v[[= $d{vn}{id} ]]/add">add release</a>)</p>[[ } ]]</h3>
+[[ if(@{$d{rel}}) { ]]-
+<table id="tre">
+[[ for(@lang) { my $l = $_; ]]-
+<tr class="lang">
+ <td colspan="6">[[: $VNDB::LANG->{$l} ]]</td>
+</tr>
+[[ for (@{$d{rel}}) { next if $l ne $_->{language}; ]]-
+ <tr>
+ <td class="tc1">[[= datestr($_->{released}) ]]</td>
+ <td class="tc2">[[= $_->{minage}<0 ? '' : $VNDB::VRAGES->{$_->{minage}} ]]</td>
+ <td class="tc3">[[= join('', map { $_ ne 'oth' ? '<acronym class="plat '.$_.'" title="'._hchar($VNDB::PLAT->{$_}).'">'.$_.'</acronym>' : () } sort @{$_->{platforms}}) ]]</td>
+ <td class="tc4"><acronym title="[[= $VNDB::RTYP->[$_->{type}] ]]- release">[[= lc substr($VNDB::RTYP->[$_->{type}],0,1) ]]</acronym></td>
+ <td class="tc5"><a href="/r[[= $_->{id} ]]" title="[[: $_->{original} || $_->{title} ]]">[[: shorten $_->{title},60 ]]</a></td>
+<!-- <td class="tc6">[[=
+ join(', ',
+ map {
+ sprintf('<a href="/p%d" title="%s">%s</a>', $_->{id}, _hchar($_->{name}), _hchar(shorten $_->{name},20)) } @{$_->{producers}}) ]]</td>-->
+ <td class="tc7">[[ if($_->{website}) { ]]<a href="[[: $_->{website} ]]"><acronym class="plat ext" title="WWW">www</acronym></a>[[ } ]]</td>
+ </tr>
+[[ } ]]-
+[[ } ]]-
+</table>
+[[ } else { ]]-
+<p>
+ This game has either not been released yet, or we just don't have information about
+ any releases.
+</p>
+[[ } ]]
+
+
diff --git a/data/tpl/vnpage_rg b/data/tpl/vnpage_rg
new file mode 100644
index 00000000..d988e226
--- /dev/null
+++ b/data/tpl/vnpage_rg
@@ -0,0 +1,11 @@
+<h3>Relations</h3>
+[[ if(!$d{vn}{rgraph}) { ]]
+ <p>
+ Relation graph has not been generated yet...
+ </p>
+[[ } else { ]]
+ [[= $d{vn}{rmap} ]]
+ <p id="relations">
+ <img src="[[= sprintf "%s/rg/%02d/%d.gif", $p{st}, $d{vn}{rgraph}%50, $d{vn}{rgraph} ]]" usemap="#rgraph" />
+ </p>
+[[ } ]]
diff --git a/data/tpl/vnpage_stats b/data/tpl/vnpage_stats
new file mode 100644
index 00000000..dde9aed3
--- /dev/null
+++ b/data/tpl/vnpage_stats
@@ -0,0 +1,68 @@
+<ul id="stats">
+[[
+ my $max = 1; my $total = 0; my $sum = 0;
+ for (0..$#{$d{votes}{graph}}) {
+ $total += $d{votes}{graph}[$_];
+ $max = $d{votes}{graph}[$_] if $d{votes}{graph}[$_] > $max;
+ $sum += ($_+1) * $d{votes}{graph}[$_];
+ }
+]]
+[[ if(!$d{user} || ($d{pv} && $d{user}{votes})) { ]]-
+<li><h3>Vote graph <b class="actions">[[= $total ]]- vote[[= $total==1?'':'s' ]]- total
+ [[= $total ? sprintf(', average: %.1f.', $sum/$total) : '' ]]</b></h3>
+<table id="tvg">
+[[ for (0..$#{$d{votes}{graph}}) { ]]-
+ <tr>
+ <td class="tc1">[[= $_+1 ]]</td>
+ <td class="tc2"><div style="width: -[[= ($d{votes}{graph}[$_]/$max)*270 + 5 ]]px">&nbsp;</div>[[= $d{votes}{graph}[$_] ]]</td>
+ </tr>
+[[ } ]]-
+</table></li>
+
+[[ if($#{$d{votes}{latest}} >= 0) { ]]
+<li><h3>Recent votes</h3>
+<table id="tvr">
+[[ for (@{$d{votes}{latest}}) { ]]-
+ <tr>
+ [[ if(!$d{user}) { ]]-
+ <td class="tc1"><a href="/u[[= $_->{uid} ]]">[[: $_->{username} ]]</a></td>
+ [[ } else { ]]-
+ <td class="tc1"><a href="/v[[= $_->{vid} ]]">[[: length($_->{title})>30?substr($_->{title},0,27).'...':$_->{title} ]]</a></td>
+ [[ } ]]-
+ <td class="tc2">[[= $_->{vote} ]]</td>
+ <td class="tc3">[[= formatdate('%Y-%m-%d %R', $_->{date}, 'dh') ]]</td>
+ </tr>
+[[ } ]]-
+</table></li>
+[[ } } ]]-
+
+-[[ $max = 1; $total = 0;
+ for (@{$d{lists}{graph}}) { $total += $_; $max = $_ if $_ > $max; } ]]
+[[ if(!$d{user} || ($d{pl} && $d{user}{vnlist})) { ]]-
+<li class="break"><h3>VN List stats <b class="actions">[[= $total ]]- -[[= $d{user}?'visual novel':'user' ]][[= $total==1?'':'s' ]]- total</b></h3>
+<table id="tus">
+ [[ for (0..$#$VNDB::LSTAT) { ]]-
+ <tr>
+ <td class="tc1">[[= $VNDB::LSTAT->[$_] ]]</td>
+ <td class="tc2"><div style="width: -[[= ($d{lists}{graph}[$_]/$max)*235 + 5 ]]px">&nbsp;</div>[[= $d{lists}{graph}[$_] ]]</td>
+ </tr>
+ [[ } ]]-
+</table></li>
+
+[[ if($#{$d{lists}{latest}} >= 0) { ]]
+<li><h3>Recent VN list additions</h3>
+<table id="tur">
+[[ for (@{$d{lists}{latest}}) { ]]-
+ <tr>
+ [[ if(!$d{user}) { ]]-
+ <td class="tc1"><a href="/u[[= $_->{uid} ]]">[[: $_->{username} ]]</a></td>
+ [[ } else { ]]-
+ <td class="tc1"><a href="/v[[= $_->{vid} ]]">[[: length($_->{title})>25?substr($_->{title},0,23).'...':$_->{title} ]]</a></td>
+ [[ } ]]-
+ <td class="tc2">[[= $VNDB::LSTAT->[$_->{status}] ]]</td>
+ <td class="tc3">[[= formatdate('%Y-%m-%d %R', $_->{date}, 'dh') ]]</td>
+ </tr>
+[[ } ]]-
+</table></li>
+[[ } } ]]-
+</ul>
diff --git a/lib/ChangeLog b/lib/ChangeLog
new file mode 100644
index 00000000..eb47140c
--- /dev/null
+++ b/lib/ChangeLog
@@ -0,0 +1,215 @@
+TODO:
+ + Remove all references to an item when it's hidden
+
+1.14 - ?
+ - Removed the ID gap prevention method
+ - Moved static content to static.vndb.org (and rely on lighty for js/css
+ compression)
+ - relation graphs and cover images now get an ID instead of MD5-sum
+ - Added Nintendo Wii to platforms
+ - Added 'hidden' flag, which should now be used instead of the delete option
+
+1.13 - 2008-04-04
+ - Fixed update_prev
+ - Split revision insert queries into a seperate function for code reuse
+ - Fixed wiki links
+ - Fixed search for VN's without releases
+ - Fixed bug with accepting zero-padded VNDB ID's
+ - Fixed bug with V-N.net link getting lost after reverse relation update
+ - Added .xml extension to AJAX requests
+ - Switched to ';' seperator instead of '&' for some URL's (=cleaner)
+ - Added language filter to category browser
+ - Stored release dates as integers and added NOT NULL constraint
+ - Used a newline to seperate multiple relations on a VN page
+ - Multi will get credits for a reverse relation edit
+ - Going to an edit-page without logging in will redirect
+ - Added rankings to the categories
+ - Fixed automated relation graph updates
+ - Added /nospam page
+ - Changed vote treshold to 3
+
+1.12 - 2008-03-09
+ - Color coded diffs
+ - Added noindex on ?ref= pages
+ - Added TBA to release dates
+ - Possibility to change vote without revoking first
+ - Added VN/ADV categories
+ - Replaced the Release summary with Producers on VN pages
+ - Added foreign key constrains
+
+1.11 - 2008-02-29
+ - [bug] Home page layout got screwed up when line wrapping occurs
+ - [bug] Multiple revisions got counted at the category browser
+ - Added GBA platform
+ - Added Gameplay and Plot categories
+ - Added link to V-N.net review
+ - Added vote count to the global statistics in the main menu
+ - [hidden] Added language filter to category browser
+ - Created user pages
+ - Redirect to VN page if someone visits an rX page from google/yahoo
+ - Added link to latest revision in the diff-browser
+ - Renamed "comments" to "Personal note" at VN List
+
+1.10 - 2008-02-09
+ - [bug] Long revision summaries incorrectly chopped
+ - Added GD-ROM and Blu-ray disk to media
+ - Platform icons will be kept in a consistent order
+ - ?rev= pages now show information about the change + diffs + links to
+ previous/next revisions
+ - Removed diff and revert links on history pages
+ - Added rel="nofollow" to edit links
+ - Changed lowest selectable year at releases to 1990
+ - Use Bayesian ratings and added extra char to c_votes
+ - A few small internal DB changes
+ - Allowed [url]-tag in edit summary, and used same function to parse vn/p/r
+ descriptions
+ - Added line wrapping on long words at diff-viewer
+ - VN search matches on release titles again
+ - Added producer search
+ - [bug] Releases in the future don't count as new language
+ - Release dates in the future are now red
+ - multiple vns for releases
+ - Redirect to specific revision after editing
+ - Redirect to the page you were at after logging in
+ - Added "Other" status and "comments" field to VN lists
+
+1.9 - 2008-02-01
+ - Redirect to VN when changing VN List status
+ - [bug] All ages was not automatically selected
+ - [bug] Description field ignored when adding or requesting edit of producer
+ - Rewrote diff calculation
+ - Added wildcard support to URI-mappings
+ - Changed some URI's:
+ /vn/* -> /v/*
+ /u/_* -> /u/*
+ /u/[username] -> /u[uid]
+ - id-gaps for producers and releases are now also filled automatically
+ - Switched producers name and romaji
+ - Added visitor as rank for non-logged in visitors, and losers for banned
+ users
+ - Added history pages & feeds
+ - Removed everything related to "pending changes"
+ - Producers are lockable
+ - Combined DBGetVN and DBGetVNs
+ - Moved code for releases from VN.pm to Releases.pm
+ - Denormalized vn_categories
+ - Added "tabs" to visual novels, releases & producers
+ - Made several changes to the visual novel page layout
+ - Added mass-change/delete option to vnlists
+ - Renamed vnr* to releases*
+ - Fixed relation graphs generator to work with the new DB structure, and to
+ delete graphs for VN's where the relation was deleted
+ - Removed option to hide a user from the userlist
+ - ResDenied will show the regiser-new-account-page
+ - Usernames linkified at history and vn-stats pages
+ - Added noindex tag on pages that include usernames
+ - Swapped title <-> romaji for releases
+ - Removed relation field and added type field for releases
+ - Also allow [url]-bbcode tag for the notes field for releases and producers
+ - [bug] Self-refering vn relations are not possible anymore
+ - Wrote update_vncache as a plpgsql function
+ - Updated homepage layout: added a few lists
+ - Added filters to recent changes pages
+ - Added platform icons to releases
+ - Added user menu to vn pages
+ - De-JS'ed the platform select form, used checkboxes instead
+ - Updated FAQ
+
+1.8 - 2007-12-05
+ - Added [url]-tag to vn description field
+ - Changed category input to checkboxes
+ - Used image sprites for category browser icons
+ - Fixed bug with media-select-form
+ - Fixed bug with pending producer changes showing up in the producer search
+ - Added hack to exclude trial versions in the release dates
+ - Removed audience category and added age rating field to releases
+ - Fixed typo: "game hes either" -> "game has either"
+ - Added Wikipedia & CISVisual link
+ - Added small vertical padding between releases
+ - Added length of visual novel
+ - Renamed continues back to Sequel/Prequel
+
+1.7 - 2007-11-25
+ - Bugfix: The visual novel itself is now also listed at the Pending Changes
+ under the releases
+ - Bugfix: Comments and Moderation subforms cannot be automatically hidden
+ - Made release and vn-links in the edit-dropdown clickable, to edit all
+ - Added "show all pending changes" option for moderators
+ - Removed official (japanese) titles from producer list
+ - Added description field for producers
+ - Added a red asterisk for fields that are required
+ - Combined 4 flag-columns in the users table to one
+ - Added cronjob to delete unused relation graphs
+
+1.6 - 2007-11-11
+ - vnr.released accepts NULL
+ - vn.c_years renamed to vn.c_released, and only stores year+month of first
+ release
+ - Removed vn_releases.lastmod
+ - Fixed CSS bug in releases layout
+ - Renamed Sequel/Prequel to Continuation/continues...
+ - Added relation graphs (/vX/rg)
+
+1.5 - 2007-11-04
+ - Automatically hiding form parts is now done server-side
+ - Release id's are hidden for not logged in visitors
+ - Added cron job to compress images and remove Exif information
+ - Possibility to add planned releases to 5 years in the future
+ - Bugfix: When editing a VN that's waiting for moderation, the 'added'
+ column won't be updated
+ - Added NSFW-option to VN-images
+ - Added small edit-dropdown when clicked on release-id
+ - Pending changes tab for VN removed and contents moved to relations tab
+ - Added Visual Novel Relations
+
+1.4 - 2007-10-28
+ - 'Mina' category renamed to 'All Ages'
+ - Added 'Clear selection' button to the category browser
+ - New visual novels will get unused/lower ID's
+ - Added notes-field to releases
+ - Subforms can be dynamically hidden/shown
+ - Bugfix: user stats will always stay under the votes at /vX/stats
+ - Bugfix: syntax error in dyna.js in Opera
+ - Combined all the add/edit/del-buttons into one menu
+ - Changed VN page layout: description moved to relations page and categories
+ have their own sub-item
+
+1.3 - 2007-10-21
+ - Bugfix: checkbox at producer-search now works
+ - VN ratings don't count of only one user has voted
+ - Added VN list size and number of votes to user list
+ - Added categories 'Drama' & 'Mystery'
+ - Added exclude filters to the category browser
+ - Added a few statistics to the right bottom of the page
+
+1.2 - 2007-10-14
+ - Bugfix: vnr_producers rows weren't deleted when deleting a release
+ - Added number of pending changes at "Pending changes" menu item
+ - Long items (>30 chars) at the top 5's (right bottom) will be shortened
+ - Added visual novel descriptions to the RSS feed
+ - Bugfix: fixed msg when browsing votes of someone who hasn't voted yet
+ - Bugfix: Voting now also works when viewing the vote stats of a VN
+ - Added user VN lists
+ - Added profile option to hide VN list
+ - Changed 'votes' tab on VN page to 'stats' and added user stats.
+
+1.1 - 2007-10-07
+ - Bugfix: you can now empty columns of the vn table
+ - Japanese is automatically selected when adding a release or producer
+ - User list has been made public
+ - Possible to browse other people's votes
+ - Added two options to "my account" to hide in user list and votes
+ - Bugfix: username is now shown when accepting a producer
+ - Bugfix: variable typo in tpl->pedit
+ - Bugfix: c_*-update-function wasn't called correctly when changing/deleting
+ releases
+ - Bugfix: 'added' column in releases, vn and vnr is now updated at accepting
+ - Added "Most Popular" vns to every page, and added "More..."-links.
+ - Added RSS feed for recent additions
+ - Changes visual novel page layout
+ - Added vote graph + latest votes to the visual novel pages
+ - Added compression on javascript files
+ - Replaced relation-selection-box with an input field
+
+1.0 - 2007-09-30
+ - First release
diff --git a/lib/VNDB.pm b/lib/VNDB.pm
new file mode 100644
index 00000000..9779d571
--- /dev/null
+++ b/lib/VNDB.pm
@@ -0,0 +1,338 @@
+package VNDB;
+
+use strict;
+use warnings;
+
+our($VERSION, $DEBUG, %VNDBopts, @WARN);
+
+$DEBUG = 1;
+$VERSION = '1.14';
+%VNDBopts = (
+ CookieDomain => '.vndb.org',
+ root_url => $DEBUG ? 'http://beta.vndb.org' : 'http://vndb.org',
+ static_url => $DEBUG ? 'http://static.beta.vndb.org' : 'http://static.vndb.org',
+ debug => $DEBUG,
+ sqlopts => {
+ user => 'vndb',
+ passwd => 'passwd',
+ database => 'vndb',
+ },
+ tplopts => {
+ filename => 'main',
+ searchdir => '/www/vndb/data/tpl',
+ compiled => '/www/vndb/data/tplcompiled.pm',
+ namespace => 'VNDB::Util::Template::tpl',
+ pre_chomp => 1,
+ post_chomp => 1,
+ rm_newlines => 0,
+ deep_reload => $DEBUG,
+ },
+ ranks => [
+ [ [ qw| visitor loser user mod admin | ], [] ],
+ {map{$_,1}qw| hist |}, # 0 - visitor (not logged in)
+ {map{$_,1}qw| hist |}, # 1 - loser
+ {map{$_,1}qw| hist edit |}, # 2 - user
+ {map{$_,1}qw| hist edit mod lock |}, # 3 - mod
+ {map{$_,1}qw| hist edit mod lock del userlist useredit |}, # 4 - admin
+ ],
+ imgpath => '/www/vndb/static/cv',
+ mappath => '/www/vndb/data/rg',
+ grapher => '/www/vndb/util/relgraph.pl',
+);
+$VNDBopts{ranks}[0][1] = { (map{$_,1} map { keys %{$VNDBopts{ranks}[$_]} } 1..5) };
+
+
+require 'global.pl';
+
+require Time::HiRes if $DEBUG;
+require Data::Dumper if $DEBUG;
+use VNDB::Util::Template;
+use VNDB::Util::Request;
+use VNDB::Util::Response;
+use VNDB::Util::DB;
+use VNDB::Util::Tools;
+use VNDB::Util::Auth;
+use VNDB::HomePages;
+use VNDB::Producers;
+use VNDB::Releases;
+use VNDB::VNLists;
+use VNDB::Users;
+use VNDB::Votes;
+use VNDB::VN;
+
+
+my %VNDBuris = ( # wildcards: * -> (.+), + -> ([0-9]+)
+ '/' => sub { shift->HomePage },
+ faq => sub { shift->FAQ },
+ 'd+' => sub { shift->DocPage(shift) },
+ nospam => sub { shift->DocPage(6) },
+ hist => {'*'=> sub { shift->History(undef, undef, $_[1]) } },
+ # users
+ u => {
+ login => sub { shift->UsrLogin },
+ logout => sub { shift->UsrLogout },
+ register => sub { shift->UsrReg },
+ newpass => sub { shift->UsrPass },
+ list => {
+ '/' => sub { shift->UsrList },
+ '*' => sub { $_[3] =~ /^([a-z0]|all)$/ ? shift->UsrList($_[2]) : shift->ResNotFound },
+ },
+ },
+ 'u+' => {
+ '/' => sub { shift->UsrPage(shift) },
+ votes => sub { shift->VNVotes(shift) },
+ edit => sub { shift->UsrEdit(shift) },
+ pending => sub { shift->UsrPending(shift) },
+ list => sub { shift->VNMyList(shift) },
+ hist => {'*'=> sub { shift->History('u', shift, $_[1]) } },
+ },
+ # visual novels
+ v => {
+ '/' => sub { shift->VNBrowse },
+ new => sub { shift->VNEdit(0); },
+ '*' => sub { $_[2] =~ /^([a-z0]|all|search|cat)$/ ? shift->VNBrowse($_[1]) : shift->ResNotFound; },
+ },
+ 'v+' => {
+ '/' => sub { shift->VNPage(shift) },
+ stats => sub { shift->VNPage(shift, shift) },
+ rg => sub { shift->VNPage(shift, shift) },
+ edit => sub { shift->VNEdit(shift) },
+ del => sub { shift->VNDel(shift) },
+ vote => sub { shift->VNVote(shift) },
+ list => sub { shift->VNListMod(shift) },
+ add => sub { shift->REdit('v', shift) },
+ lock => sub { shift->VNLock(shift) },
+ hide => sub { shift->VNHide(shift) },
+ hist => {'*'=> sub { shift->History('v', shift, $_[1]) } },
+ },
+ # releases
+ 'r+' => {
+ '/' => sub { shift->RPage(shift) },
+ edit => sub { shift->REdit('r', shift) },
+ lock => sub { shift->RLock(shift) },
+ del => sub { shift->RDel(shift) },
+ hide => sub { shift->RHide(shift) },
+ hist => {'*'=> sub { shift->History('r', shift, $_[1]) } },
+ },
+ # producers
+ p => {
+ '/' => sub { shift->PBrowse },
+ add => sub { shift->PEdit(0) },
+ '*' => sub { $_[2] =~ /^([a-z0]|all)$/ ? shift->PBrowse($_[1]) : shift->ResNotFound; }
+ },
+ 'p+' => {
+ '/' => sub { shift->PPage(shift) },
+ edit => sub { shift->PEdit(shift) },
+ del => sub { shift->PDel(shift) },
+ lock => sub { shift->PLock(shift) },
+ hide => sub { shift->PHide(shift) },
+ hist => {'*'=> sub { shift->History('p', shift, $_[1]) } },
+ },
+ # stuff (.xml extension to make sure they aren't counted as pageviews)
+ xml => {
+ 'producers.xml' => sub { shift->PXML },
+ 'vn.xml' => sub { shift->VNXML },
+ },
+);
+
+
+# provide redirects for old URIs
+my %OLDuris = (
+ vn => {
+ rss => sub { shift->ResRedirect('/hist/rss?t=v&e=1', 'perm') },
+ '*' => sub { shift->ResRedirect('/v/'.$_[1], 'perm') },
+ },
+ 'v+' => {
+ votes => sub { shift->ResRedirect('/v'.(shift).'/stats', 'perm') },
+ },
+ u => {
+ '*' => {
+ '*' => sub {
+ if($_[2] =~ /^_(login|logout|register|newpass|list)$/) {
+ $_[3] eq '/' ? $_[0]->ResRedirect('/u/'.$1, 'perm') : $_[0]->ResRedirect('/u/'.$1.'/'.$_[3], 'perm');
+ } else {
+ my $id = $_[0]->DBGetUser(username => $_[2])->[0]{id};
+ $id ? $_[0]->ResRedirect('/u'.$id.'/'.$_[3], 'perm') : $_[0]->ResNotFound;
+ }
+ },
+ }
+ }
+);
+
+
+
+sub new {
+ my $self = shift;
+ my $type = ref($self) || $self;
+ my %args = @_;
+
+ my $me = bless {
+ %args,
+ _DB => VNDB::Util::DB->new(%{$args{sqlopts}}),
+ _TPL => VNDB::Util::Template->new(%{$args{tplopts}}),
+ }, $type;
+
+ return $me;
+}
+
+
+sub get_page {
+ my $self = shift;
+ my $r = shift;
+
+ $self->{_Req} = VNDB::Util::Request->new($r);
+ $self->{_Res} = VNDB::Util::Response->new($self->{_TPL});
+
+ $self->AuthCheckCookie();
+ $self->checkuri();
+
+ my $res = $self->ResSetModPerl($r);
+ $self->DBCommit();
+
+ return($self, $res);
+}
+
+
+sub checkuri {
+ my $self = shift;
+ (my $uri = lc($self->ReqUri)) =~ s/^\/+//;
+ $uri =~ s/\?.*$//;
+ return $self->ResRedirect("/$uri", 'perm') if $uri =~ s/\/+$//;
+ $uri =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; # ugly hack, but we only accept ASCII anyway
+ return $self->ResNotFound() if $uri !~ /^[a-z0-9\-\._~\/]*$/; # rfc3986 section 2.3, "Unreserved Characters"
+ my @uri;
+ defined $_ and push(@uri, $_) for (split(/\/+/, $uri));
+ my @ouri = @uri; # items in @uri can be modified by uri2page
+ $self->uri2page(\%VNDBuris, \@uri, 0);
+ $self->uri2page(\%OLDuris, \@ouri, 0) # provide redirects for old uris
+ if $self->{_Res}->{whattouse} == 4 && $self->{_Res}->{rc} == 404;
+}
+
+
+sub uri2page {
+ my($s, $o, $u, $i) = @_;
+ $u->[$i] = '/' if !defined $u->[$i];
+ my $n = $o->{$u->[$i]} ? $u->[$i] : ((map {
+ if(/[\*\+]/) {
+ my $t = "^$_\$";
+ /\*/ ? ($t =~ s/\*/(.+)/) : ($t =~ s/\+/([1-9][0-9]*)/);
+ $u->[$i] =~ /$t/ ? ($u->[$i] = $1) && $_ : ();
+ } else { () } }
+ sort { length($b) <=> length($a) } keys %$o)[0] || '*');
+ ref($o->{$n}) eq 'HASH' && $n ne '/' ?
+ $s->uri2page($o->{$n}, $u, ++$i) :
+ ref($o->{$n}) eq 'CODE' && $i == $#$u ?
+ &{$o->{$n}}($s, @$u) :
+ $s->ResNotFound();
+}
+
+
+1;
+
+
+__END__
+
+# O L D C O D E - N O T U S E D A N Y M O R E
+
+
+# Apache 2 handler
+sub handler ($$) {
+ my $r = shift;
+
+ # we don't handle internal redirects! (fixes ErrorDocument directives)
+ return Apache2::Const::DECLINED
+ if $r->prev || $r->next;
+
+ my $start = [Time::HiRes::gettimeofday()] if $DEBUG;
+ @WARN = ();
+ my($code, $res, $err);
+ $SIG{__WARN__} = sub { push(@VNDB::WARN, @_); warn @_; };
+
+ $err = eval {
+
+ @Time::CTime::DoW = qw|Sun Mon Tue Wed Thu Fri Sat|;
+ @Time::CTime::DayOfWeek = qw|Sunday Monday Tuesday Wednesday Thursday Friday Saturday|;
+ @Time::CTime::MoY = qw|Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec|;
+ @Time::CTime::MonthOfYear = qw|January February March April May June July August September October November December|;
+
+ $VNDB = VNDB->new(%VNDBopts) if !$VNDB;
+ $VNDB->{r} = $r;
+
+ # let apache handle static files
+ (my $uri = lc($r->uri())) =~ s/\/+//;
+ if(index($uri, '..') == -1 && -f '/www/vndb/www/' . $uri) {
+ $code = Apache2::Const::DECLINED;
+ return $code;
+ }
+
+ $VNDB->DBCheck();
+ ($res, $code) = $VNDB->get_page($r);
+ if($DEBUG) {
+ my($sqlt, $sqlc) = (0, 0);
+ foreach (@{$res->{_DB}->{Queries}}) {
+ if($_->[0]) {
+ $sqlc++;
+ $sqlt += $_->[1];
+ }
+ }
+ my $time = Time::HiRes::tv_interval($start);
+ my $tpl = $res->{_Res}->{_tpltime} ? $res->{_Res}->{_tpltime}/$time*100 : 0;
+ my $gzip = 0;
+ $gzip = 100 - $res->{_Res}->{_gzip}->[1]/$res->{_Res}->{_gzip}->[0]*100
+ if($res->{_Res}->{_gzip} && ref($res->{_Res}->{_gzip}) eq 'ARRAY' && $res->{_Res}->{_gzip}->[0] > 0);
+ printf STDERR "Took %3dms (SQL/TPL/perl: %4.1f%% %4.1f%% %4.1f%%) (GZIP: %4.1f%%) to parse %s\n",
+ $time*1000, $sqlt/$time*100, $tpl, 100-($sqlt/$time*100)-$tpl, $gzip, $r->uri();
+ }
+
+ };
+
+ # error occured, create a dump file
+ if(!defined $err && $@ && $DEBUG) {
+ undef $res->{_Res};
+ undef $res->{_Req};
+ die $@;
+ } elsif(!defined $err && $@) {
+ if(open(my $E, sprintf '>/www/vndb/data/errors/%04d-%02d-%02d-%d',
+ (localtime)[5]+1900, (localtime)[4]+1, (localtime)[3], time)) {
+ print $E 'Error @ ' . scalar localtime;
+
+ print $E "\n\nRequest:\n" . $r->the_request . "\n";
+ print $E "$_: " . $r->headers_in->{$_} . "\n"
+ for (keys %{$r->headers_in});
+
+ print $E "\nParams:\n";
+ my $re = Apache2::Request->new($r);
+ print $E "$_: " . $re->param($_) . "\n"
+ for ($re->param());
+
+ print $E "\nError:\n$@\n\n";
+ print $E "Warnings:\n".join('', @WARN)."\n";
+ close($E);
+ }
+ $VNDB->DBRollBack();
+ undef $res->{_Res};
+ undef $res->{_Req};
+ die "Error, check dumpfile!\n";
+ }
+
+ undef $res->{_Res};
+ undef $res->{_Req};
+ # let apache handle 404's
+ $code = Apache2::Const::DECLINED if $code == 404;
+ return $code;
+}
+
+
+sub mod_perl_init {
+ require Apache2::RequestRec;
+ require Apache2::RequestIO;
+ $VNDB = __PACKAGE__->new(%VNDBopts);
+ return 0;
+}
+
+
+sub mod_perl_exit {
+ $VNDB->DBExit() if defined $VNDB && ref $VNDB eq __PACKAGE__;
+ return 0;
+}
+
diff --git a/lib/VNDB/HomePages.pm b/lib/VNDB/HomePages.pm
new file mode 100644
index 00000000..c79b3ac6
--- /dev/null
+++ b/lib/VNDB/HomePages.pm
@@ -0,0 +1,286 @@
+
+package VNDB::HomePages;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+@EXPORT = qw| HomePage FAQ DocPage History HistRevert HistDelete |;
+
+
+sub HomePage {
+ my $self = shift;
+
+ # recent edits
+ # recently added visual novels
+ # recently added producers
+ # random visual novels
+ # recent votes
+ # popular visual novels
+
+ $self->ResAddTpl(home => {
+ recentedits => scalar $self->DBGetHist( results => 10, what => 'iid ititle'),
+ recentvns => scalar $self->DBGetHist( results => 10, what => 'iid ititle', edits => 0, type => 'v'),
+ recentps => scalar $self->DBGetHist( results => 10, what => 'iid ititle', edits => 0, type => 'p'),
+ randomvns => scalar $self->DBGetVN( results => 10, order => 'RANDOM()'),
+ recentvotes => scalar $self->DBGetVotes(results => 10),
+ popular => scalar $self->DBGetVN( results => 10, order => 'v.c_votes DESC'),
+ });
+}
+
+
+sub FAQ {
+ shift->ResAddTpl(faq => {});
+}
+
+sub DocPage {
+ shift->ResAddTpl(docs => { p => shift });
+}
+
+
+sub History { # type(p,v,r,u), id, [rss|/]
+ my($self, $type, $id, $fmt) = @_;
+ $type ||= '';
+ $id ||= 0;
+
+ $fmt = undef if !$fmt || $fmt eq '/';
+ return $self->ResNotFound if $fmt && $fmt ne 'rss';
+
+ my $f = $self->FormCheck(
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ { name => 'ip', required => 0, default => 0 }, # hidden option
+ { name => 't', required => 0, default => 'a', enum => [ qw| v r p a | ] },
+ { name => 'e', required => 0, default => 0, enum => [ 0..2 ] },
+ { name => 'r', required => 0, default => $fmt ? 10 : 50, template => 'int' },
+ { name => 'i', required => 0, default => 0, enum => [ 0..1 ] },
+ { name => 'h', required => 0, default => 0, enum => [ 0..2 ] }, # hidden option
+ );
+
+ my $o =
+ $type eq 'u' ? $self->DBGetUser(uid => $id)->[0] :
+ $type eq 'v' ? $self->DBGetVN(id => $id)->[0] :
+ $type eq 'r' ? $self->DBGetRelease(id => $id)->[0] :
+ $type eq 'p' ? $self->DBGetProducer(id => $id)->[0] :
+ undef;
+ return $self->ResNotFound if $type && !$o;
+ my $t =
+ $type eq 'u' ? $o->{username} :
+ $type eq 'v' ? $o->{title} :
+ $type eq 'r' ? $o->{romaji} || $o->{title} :
+ $type eq 'p' ? $o->{name} :
+ undef;
+
+ my($h, $np, $act);
+
+ if($self->ReqMethod ne 'POST' || $fmt) {
+ ($h, $np) = $self->DBGetHist(
+ what => 'iid ititle user',
+ type => $type,
+ !$type && $f->{t} ne 'a' ? (
+ type => $f->{t} ) : (),
+ $f->{e} ? (
+ edits => $f->{e} == 1 ? 0 : 1 ) : (),
+ id => $id,
+ page => $fmt ? 0 : $f->{p},
+ results => $f->{r},
+ releases => $type eq 'v' ? $f->{i} : 0,
+ showhid => $f->{h},
+ $f->{ip} ? (
+ ip => $f->{ip} ) : (),
+ );
+ }
+ else {
+ my $frm = $self->FormCheck(
+ { name => 'sel', required => 1, multi => 1 },
+ { name => 'post', required => 1, default => 'Mass revert', enum => [ 'Mass revert', 'Mass delete' ] },
+ );
+ my @s = grep /^[0-9]+$/, @{$frm->{sel}};
+ if(!$frm->{_err} && @s) {
+ $np = 0;
+ $h = $frm->{post} =~ /revert/ ? $self->HistRevert(\@s) : $self->HistDelete(\@s);
+ $act = $frm->{post} =~ /revert/ ? 'r' : 'd';
+ }
+ }
+
+ if(!$fmt) {
+ $self->ResAddTpl(hist => {
+ title => $t,
+ selt => $f->{t},
+ sele => $f->{e},
+ seli => $f->{i},
+ type => $type,
+ id => $id,
+ hist => $h,
+ page => $f->{p},
+ npage => $np,
+ obj => $o,
+ act => $act || '',
+ });
+ } else {
+ my $x = $self->ResStartXML;
+ $x->startTag('rss', version => '2.0');
+ $x->startTag('channel');
+ $x->dataElement('language', 'en');
+ $x->dataElement('title', !$type ? 'Recent changes at VNDB.org' : $type eq 'u' ? 'Recent changes by '.$t : 'Edit history of '.$t);
+ $x->dataElement('link', $self->{root_url}.(!$type ? '/hist' : '/'.$type.$id.'/hist'));
+
+ for (@$h) {
+ my $t = (qw| v r p |)[$_->{type}];
+ my $url = $self->{root_url}.'/'.$t.$_->{iid}.'?rev='.$_->{id};
+ $_->{comments} = VNDB::Util::Template::tpl::summary($_->{comments})||'[no summary]';
+ $x->startTag('item');
+ $x->dataElement(title => $_->{ititle});
+ $x->dataElement(link => $url);
+ $x->dataElement(pubDate => VNDB::time2str($_->{requested}));
+ $x->dataElement(guid => $url);
+ $x->dataElement(description => $_->{comments});
+ $x->endTag('item');
+ }
+
+ $x->endTag('channel');
+ $x->endTag('rss');
+ }
+}
+
+
+
+
+1;
+
+__END__
+
+
+#############################################################
+# E X P E R I M E N T A L S T U F F #
+# #
+
+# !WARNING!: this code has not been updated to reflect the recent database changes!
+
+
+# !WARNING!: this code uses rather many large SQL queries, use with care...
+sub HistRevert { # \@ids
+ my($self, $l) = @_;
+ my $comm = 'Mass revert to revision %d by %s';
+
+ # first, get objects, remove newly created items and causedby edits and add original edits
+ $l = $self->DBGetHist(cid => $l, results => 1000, what => 'iid');
+ my @todo;
+ for (@$l) {
+ next if !$_->{prev}; # remove newly created items
+ if($_->{causedby}) { # remove causedby edits
+ push @todo, $self->DBGetHist(cid => [ $_->{causedby} ], what => 'iid')->[0]; # add original edit
+ } else {
+ push @todo, $_;
+ }
+ }
+
+ # second, group all items and remove duplicate edits
+ my %todo; # key=type.iid, value = [objects]
+ for my $t (@todo) {
+ my $k = $t->{type}.$t->{iid};
+ $todo{$k} = [ $t ] and next
+ if !$todo{$k};
+ push @{$todo{$k}}, $t
+ if !grep { $_->{id} == $t->{id} } @{$todo{$k}};
+ }
+
+ # third, make sure we don't revert edits we don't want to revert
+ #TODO
+
+ # fourth, get the lowest revision of each item to revert to (ignoring intermetiate edits)
+ @todo = map { (sort { $a->{id} <=> $b->{id} } @{$todo{$_}})[0] } keys %todo;
+
+ # fifth, actually revert the edits
+ my @relupd;
+ for (@todo) {
+
+ if($_->{type} == 0) { # visual novel
+ my $v = $self->DBGetVN(id => $_->{iid}, rev => $_->{prev}, what => 'extended changes relations')->[0];
+ my $old = $self->DBGetVN(id => $_->{iid}, rev => $_->{id}, what => 'relations')->[0];
+ my $cid = $self->DBEditVN($_->{iid},
+ (map { $_ => $v->{$_} } qw| title desc alias categories comm length l_wp l_cisv l_vnn img_nsfw image|),
+ relations => [ map { [ $_->{relation}, $_->{id} ] } @{$v->{relations}} ],
+ comm => sprintf($comm, $v->{cid}, $v->{username}),
+ );
+ my %old = map { $_->{id} => $_->{relation} } @{$old->{relations}};
+ my %new = map { $_->{id} => $_->{relation} } @{$v->{relations}};
+ push @relupd, $self->VNUpdReverse(\%old, \%new, $_->{iid}, $cid);
+ }
+
+ if($_->{type} == 1) { # release
+ my $r = $self->DBGetRelease(id => $_->{iid}, rev => $_->{prev}, what => 'producers platforms media vn changes')->[0];
+ $self->DBEditRelease($_->{iid},
+ (map { $_ => $r->{$_} } qw| title original language website notes minage type released platforms |),
+ media => [ map { [ $_->{medium}, $_->{qty} ] } @{$r->{media}} ],
+ producers => [ map { $_->{id} } @{$r->{producers}} ],
+ comm => sprintf($comm, $r->{cid}, $r->{username}),
+ vn => [ map { $_->{vid} } @{$r->{vn}} ],
+ );
+ }
+
+ if($_->{type} == 2) { # producer
+ my $p = $self->DBGetProducer(id => $_->{iid}, rev => $_->{prev}, what => 'changes')->[0];
+ $self->DBEditProducer($_->{iid},
+ (map { $_ => $p->{$_} } qw| name original website type lang desc |),
+ comm => sprintf($comm, $p->{cid}, $p->{username}),
+ );
+ }
+ }
+ # update relation graphs
+ $self->VNRecreateRel(@relupd) if @relupd;
+
+ # sixth, create report of what happened
+ my @done;
+ for my $t (@todo, @$l) {
+ next if $t->{_status};
+ $t->{_status} =
+ (scalar grep { $t->{id} == $_->{id} } @todo) ? 'reverted' :
+ $t->{causedby} ? 'automated' :
+ 'skipped';
+ push @done, $t;
+ }
+ return \@done;
+}
+
+
+# ONLY DELETES NEWLY CREATED PAGES (for now...)
+sub HistDelete { # \@ids
+ my ($self, $l) = @_;
+
+ # get objects and add causedby edits
+ $l = $self->DBGetHist(cid => $l, results => 1000, what => 'iid');
+ my @todo = @$l;
+# for (@$l) {
+# if($_->{causedby}) { # remove causedby edits
+# my $n = $self->DBGetHist(cid => [ $_->{causedby} ])->[0]; # add original edit
+# push @todo, $n, $self->DBGetHist(causedby => $n->{id} ])->[0]; # add causedby edits
+# } else {
+# push @todo, $_;
+# }
+# }
+
+ # remove duplicate edit
+ # (not necessary now)
+
+ # completely delete newly created items (sort on type to make sure we delete vn's before releases, which is faster)
+ my @vns;
+ for my $t (sort { $a->{type} <=> $b->{type} } @todo) {
+ next if $t->{prev};
+ $self->DBDelVN($t->{iid}) if $t->{type} == 0;
+ $self->DBDelProducer($t->{iid}) if $t->{type} == 2;
+ if($t->{type} == 1) { # we need to know the vn's to remove a release
+ my $r = $self->DBGetRelease(id => $t->{iid}, what => 'vn')->[0];
+ next if !$r; # we could have deleted this release by deleting the related vn
+ $self->DBDelRelease([ map { $_->{vid} } @{$r->{vn}} ], $t->{iid});
+ }
+ }
+
+ # delete individual edits
+ #TODO
+
+ return \@todo;
+}
+
+
diff --git a/lib/VNDB/Producers.pm b/lib/VNDB/Producers.pm
new file mode 100644
index 00000000..55a02d3d
--- /dev/null
+++ b/lib/VNDB/Producers.pm
@@ -0,0 +1,188 @@
+
+package VNDB::Producers;
+
+use strict;
+use warnings;
+use Exporter 'import';
+use Digest::MD5;
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+@EXPORT = qw| PPage PBrowse PEdit PDel PLock PHide PXML |;
+
+
+sub PPage {
+ my $self = shift;
+ my $id = shift;
+
+
+ my $r = $self->FormCheck(
+ { name => 'rev', required => 0, default => 0, template => 'int' },
+ { name => 'diff', required => 0, default => 0, template => 'int' },
+ );
+
+ my $p = $self->DBGetProducer(
+ id => $id,
+ $r->{rev} ? ( what => 'changes' ) : (),
+ $r->{rev} ? ( rev => $r->{rev} ) : ()
+ )->[0];
+ return $self->ResNotFound if !$p->{id};
+
+ $r->{diff} ||= $p->{prev} if $r->{rev};
+ my $c = $r->{diff} && $self->DBGetProducer(id => $id, rev => $r->{diff}, what => 'changes')->[0];
+ $p->{next} = $self->DBGetHist(type => 'p', id => $id, next => $p->{cid}, showhid => 1)->[0]{id} if $r->{rev};
+
+ return $self->ResAddTpl(ppage => {
+ prod => $p,
+ prev => $c,
+ change => $r->{diff} || $r->{rev},
+ vn => $self->DBGetProducerVN($id),
+ });
+}
+
+
+sub PBrowse {
+ my $self = shift;
+ my $chr = shift;
+ $chr = 'all' if !defined $chr;
+
+ my $p = $self->FormCheck(
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ { name => 'q', required => 0, default => '' }
+ );
+
+ my($r, $np) = $self->DBGetProducer(
+ $chr ne 'all' ? (
+ char => $chr ) : (),
+ $p->{q} ? (
+ search => $p->{q} ) : (),
+ page => $p->{p},
+ results => 50,
+ );
+
+ $self->ResAddTpl(pbrowse => {
+ prods => $r,
+ page => $p->{p},
+ npage => $np,
+ query => $p->{q},
+ chr => $chr,
+ });
+}
+
+
+sub PEdit {
+ my $self = shift;
+ my $id = shift || 0; # 0 = new
+
+ my $rev = $self->FormCheck({ name => 'rev', required => 0, default => 0, template => 'int' })->{rev};
+
+ my $p = $self->DBGetProducer(id => $id, what => 'changes', $rev ? ( rev => $rev ) : ())->[0] if $id;
+ return $self->ResNotFound() if $id && !$p;
+
+ return $self->ResDenied if !$self->AuthCan('edit') || ($p->{locked} && !$self->AuthCan('lock'));
+
+
+ my %b4 = $id ? (
+ map { $_ => $p->{$_} } qw|name original website type lang desc|
+ ) : ();
+
+ my $frm = {};
+ if($self->ReqMethod() eq 'POST') {
+ $frm = $self->FormCheck(
+ { name => 'type', required => 1, enum => [ keys %$VNDB::PROT ] },
+ { name => 'name', required => 1, maxlength => 200 },
+ { name => 'original', required => 0, maxlength => 200, default => '' },
+ { name => 'lang', required => 1, enum => [ keys %$VNDB::LANG ] },
+ { name => 'website', required => 0, maxlength => 200, template => 'url', default => '' },
+ { name => 'desc', required => 0, maxlength => 10240, default => '' },
+ { name => 'comm', required => 0, default => '' },
+ );
+
+ return $self->ResRedirect('/p'.$id, 'post')
+ if $id && 6 == scalar grep { $_ ne 'comm' && $b4{$_} eq $frm->{$_} } keys %b4;
+
+ if(!$frm->{_err}) {
+ my $cid;
+ $cid = $self->DBEditProducer($id, %$frm) if $id; # edit
+ ($id, $cid) = $self->DBAddProducer(%$frm) if !$id; # add
+ return $self->ResRedirect('/p'.$id.'?rev='.$cid, 'post');
+ }
+ }
+
+ if($id) {
+ $frm->{$_} ||= $b4{$_} for (keys %b4);
+ $frm->{comm} = sprintf 'Reverted to revision %d by %s.', $p->{cid}, $p->{username} if $p->{cid} != $p->{latest};
+ } else {
+ $frm->{lang} ||= 'ja';
+ }
+
+ $self->ResAddTpl(pedit => {
+ form => $frm,
+ id => $id,
+ prod => $p,
+ });
+}
+
+
+sub PDel {
+ my $self = shift;
+ my $id = shift;
+
+ my $p = $self->DBGetProducer(id => $id)->[0];
+ return $self->ResNotFound if !$p;
+ return $self->ResDenied if !$self->AuthCan('del');
+ $self->DBDelProducer($id);
+ return $self->ResRedirect('/p', 'perm');
+}
+
+
+sub PLock {
+ my $self = shift;
+ my $id = shift;
+
+ my $p = $self->DBGetProducer(id => $id)->[0];
+ return $self->ResNotFound() if !$p;
+ return $self->ResDenied if !$self->AuthCan('lock');
+ $self->DBLockItem('producers', $id, $p->{locked}?0:1);
+ return $self->ResRedirect('/p'.$id, 'perm');
+}
+
+
+sub PHide {
+ my $self = shift;
+ my $id = shift;
+
+ my $p = $self->DBGetProducer(id => $id)->[0];
+ return $self->ResNotFound() if !$p;
+ return $self->ResDenied if !$self->AuthCan('del');
+ $self->DBHideProducer($id, $p->{hidden}?0:1);
+ return $self->ResRedirect('/p'.$id, 'perm');
+}
+
+sub PXML {
+ my $self = shift;
+
+ my $q = $self->FormCheck(
+ { name => 'q', required => 0, maxlength => 100 }
+ )->{q};
+
+ my $r = [];
+ if($q) {
+ $r = $self->DBGetProducer(results => 10,
+ $q =~ /^p([0-9]+)$/ ? (id => $1) : (search => $q));
+ }
+
+ my $x = $self->ResStartXML;
+ $x->startTag('producers', results => $#$r+1, query => $q);
+ for (@$r) {
+ $x->startTag('item');
+ $x->dataElement(id => $_->{id});
+ $x->dataElement(name => $_->{name});
+ $x->dataElement(original => $_->{original}) if $_->{original};
+ $x->dataElement(website => $_->{website}) if $_->{website};
+ $x->endTag('item');
+ }
+ $x->endTag('producers');
+}
+
+
diff --git a/lib/VNDB/Releases.pm b/lib/VNDB/Releases.pm
new file mode 100644
index 00000000..0012d324
--- /dev/null
+++ b/lib/VNDB/Releases.pm
@@ -0,0 +1,178 @@
+
+package VNDB::Releases;
+
+use strict;
+use warnings;
+use Exporter 'import';
+use Digest::MD5;
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+@EXPORT = qw| RPage REdit RLock RDel RHide |;
+
+
+sub RPage {
+ my $self = shift;
+ my $id = shift;
+
+ my $r = $self->FormCheck(
+ { name => 'rev', required => 0, default => 0, template => 'int' },
+ { name => 'diff', required => 0, default => 0, template => 'int' },
+ );
+
+ my $v = $self->DBGetRelease(
+ id => $id,
+ what => 'producers platforms media vn'.($r->{rev} ? ' changes':''),
+ $r->{rev} ? ( rev => $r->{rev} ) : ()
+ )->[0];
+ return $self->ResNotFound if !$v->{id};
+
+ $r->{diff} ||= $v->{prev} if $r->{rev};
+ my $c = $r->{diff} && $self->DBGetRelease(id => $id, rev => $r->{diff}, what => 'changes producers platforms media vn')->[0];
+ $v->{next} = $self->DBGetHist(type => 'r', id => $id, next => $v->{cid}, showhid => 1)->[0]{id} if $r->{rev};
+
+ $self->ResRedirect('/v'.$v->{vn}[0]{vid})
+ if ($self->ReqHeader('Referer')||'') =~ m{^http://[^/]*(yahoo|google)} && @{$v->{vn}} == 1;
+
+ return $self->ResAddTpl(rpage => {
+ rel => $v,
+ prev => $c,
+ change => $r->{diff}||$r->{rev},
+ });
+}
+
+
+sub REdit {
+ my $self = shift;
+ my $act = shift||'v';
+ my $id = shift || 0;
+
+ my $rid = $act eq 'r' ? $id : 0;
+
+ my $rev = $self->FormCheck({ name => 'rev', required => 0, default => 0, template => 'int' })->{rev};
+
+ my $r = $self->DBGetRelease(id => $rid, what => 'changes producers platforms media vn', $rev ? ( rev => $rev ) : ())->[0] if $rid;
+ my $ivn = $self->DBGetVN(id => $id)->[0] if !$rid;
+ return $self->ResNotFound() if ($rid && !$r) || (!$rid && !$ivn);
+
+ my $vn = $rid ? $r->{vn} : [ { vid => $id, title => $ivn->{title} } ];
+
+ return $self->ResDenied if !$self->AuthCan('edit') || ($r->{locked} && !$self->AuthCan('lock'));
+
+ my %b4 = $rid ? (
+ (map { $_ => $r->{$_} } qw|title original language website notes minage type platforms|),
+ released => $r->{released} =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/ ? [ $1, $2, $3 ] : [ 0, 0, 0 ],
+ media => join(',', map { $_->{medium} =~ /^(cd|dvd|gdr|blr)$/ ? ($_->{medium}.'_'.$_->{qty}) : $_->{medium} } @{$r->{media}}),
+ producers => join('|||', map { $_->{id}.','.$_->{name} } @{$r->{producers}}),
+ ) : ();
+ $b4{vn} = join('|||', map { $_->{vid}.','.$_->{title} } @$vn);
+
+ my $frm = {};
+ if($self->ReqMethod() eq 'POST') {
+ $frm = $self->FormCheck(
+ { name => 'type', required => 1, enum => [ 0..$#{$VNDB::RTYP} ] },
+ { name => 'title', required => 1, maxlength => 250 },
+ { name => 'original', required => 0, maxlength => 250, default => '' },
+ { name => 'language', required => 1, enum => [ keys %{$VNDB::LANG} ] },
+ { name => 'website', required => 0, template => 'url', default => '' },
+ { name => 'released', required => 0, multi => 1, template => 'int', default => 0 },
+ { name => 'minage' , required => 0, enum => [ keys %{$VNDB::VRAGES} ], default => -1 },
+ { name => 'notes', required => 0, maxlength => 10240, default => '' },
+ { name => 'platforms', required => 0, multi => 1, enum => [ keys %$VNDB::PLAT ], default => '' },
+ { name => 'media', required => 0, default => '' },
+ { name => 'producers', required => 0, default => '' },
+ { name => 'vn', required => 1, maxlength => 10240 },
+ { name => 'comm', required => 0, default => '' },
+ );
+
+ my $released = !$frm->{released}[0] ? 0 :
+ $frm->{released}[0] == 9999 ? 99999999 :
+ sprintf '%04d%02d%02d', $frm->{released}[0]||0, $frm->{released}[1]||0, $frm->{released}[2]||0;
+ my $media = [ map { /_/ ? [ split /_/ ] : [ $_, 0 ] } split /,/, $frm->{media} ];
+ my $producers = [ map { /^([0-9]+)/ ? $1 : () } split /\|\|\|/, $frm->{producers} ];
+ my $new_vn = [ map { /^([0-9]+)/ ? $1 : () } split /\|\|\|/, $frm->{vn} ];
+
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'vn_1' ] : [ 'vn_1' ]
+ if !@$new_vn;
+
+ # weed out empty string
+ $frm->{platforms} = [ map { $_ ? $_ : () } @{$frm->{platforms}} ];
+
+ return $self->ResRedirect('/r'.$rid, 'post')
+ if $rid && $released == $r->{released} &&
+ (join(',', sort @{$b4{platforms}}) eq join(',', sort @{$frm->{platforms}})) &&
+ 10 == scalar grep { $_ ne 'comm' && $_ ne 'released' && $_ ne 'platforms' && $frm->{$_} eq $b4{$_} } keys %b4;
+
+ if(!$frm->{_err}) {
+ my %opts = (
+ vn => $new_vn,
+ (map { $_ => $frm->{$_} } qw|title original language website notes minage type comm platforms|),
+ released => $released,
+ media => $media,
+ producers => $producers,
+ );
+ my $cid;
+ $cid = $self->DBEditRelease($rid, %opts) if $rid; # edit
+ ($rid, $cid) = $self->DBAddRelease(%opts) if !$rid; # add
+ return $self->ResRedirect('/r'.$rid.'?rev='.$cid, 'post');
+ }
+ }
+
+ if($rid) {
+ $frm->{$_} ||= $b4{$_} for (keys %b4);
+ $frm->{comm} = sprintf 'Reverted to revision %d by %s.', $r->{cid}, $r->{username} if $r->{cid} != $r->{latest};
+ } else {
+ $frm->{language} = 'ja';
+ $frm->{vn} = $b4{vn};
+ }
+
+ $self->AddHid($frm);
+ $frm->{_hid} = {map{$_=>1} qw| info pnm prod |}
+ if !$frm->{_hid} && !$rid;
+ $self->ResAddTpl(redit => {
+ form => $frm,
+ id => $rid,
+ rel => $r,
+ vn => !$rid ? $ivn : $vn,
+ });
+}
+
+
+sub RLock {
+ my $self = shift;
+ my $id = shift;
+
+ my $r = $self->DBGetRelease(id => $id)->[0];
+ return $self->ResNotFound() if !$r;
+ return $self->ResDenied if !$self->AuthCan('lock');
+ $self->DBLockItem('releases', $id, $r->{locked}?0:1);
+ return $self->ResRedirect('/r'.$id, 'perm');
+}
+
+
+sub RDel {
+ my $self = shift;
+ my $id = shift;
+
+ return $self->ResDenied if !$self->AuthCan('del');
+ my $r = $self->DBGetRelease(id => $id, what => 'vn')->[0];
+ return $self->ResNotFound if !$r;
+ $self->DBDelRelease([ map { $_->{vid} } @{$r->{vn}} ], $id);
+ return $self->ResRedirect('/v'.$r->{vn}[0]{id}, 'perm');
+}
+
+
+sub RHide {
+ my $self = shift;
+ my $id = shift;
+
+ return $self->ResDenied if !$self->AuthCan('del');
+ my $r = $self->DBGetRelease(id => $id, what => 'vn')->[0];
+ return $self->ResNotFound if !$r;
+ $self->DBHideRelease($id, $r->{hidden}?0:1, [ map { $_->{vid} } @{$r->{vn}} ]);
+ return $self->ResRedirect('/r'.$id, 'perm');
+}
+
+
+1;
+
diff --git a/lib/VNDB/Users.pm b/lib/VNDB/Users.pm
new file mode 100644
index 00000000..ea4b46ed
--- /dev/null
+++ b/lib/VNDB/Users.pm
@@ -0,0 +1,230 @@
+
+package VNDB::Users;
+
+use strict;
+use warnings;
+use Exporter 'import';
+use Digest::MD5 'md5_hex';
+
+our $VERSION = $VNDB::VERSION;
+our @EXPORT = qw| UsrLogin UsrLogout UsrReg UsrPass UsrEdit UsrList UsrPage |;
+
+
+sub UsrLogin {
+ my $self = shift;
+
+ (return $self->ResRedirect('/', 'temp')) if $self->AuthInfo()->{id};
+
+ my $frm = {};
+ if($self->ReqMethod() eq 'POST') {
+ $frm = $self->FormCheck(
+ { name => 'username', required => 1, minlength => 2, maxlength => 15, template => 'pname' },
+ { name => 'userpass', required => 1, minlength => 4, maxlength => 15, template => 'asciiprint' },
+ );
+ if(!$frm->{_err}) {
+ (my $ref = $self->ReqHeader('Referer')||'/') =~ s/^$self->{root_url}//;
+ my $r = $self->AuthLogin($frm->{username}, $frm->{userpass}, 1, $ref);
+ $r == 1 ? (return) : ($frm->{_err} = [ 'loginerr' ]);
+ }
+ }
+
+ $self->ResAddTpl(userlogin => {
+ log => $frm,
+ } );
+}
+
+
+sub UsrLogout {
+ shift->AuthLogout();
+}
+
+
+sub UsrReg {
+ my $self = shift;
+
+ (return $self->ResRedirect('/', 'temp')) if $self->AuthInfo()->{id};
+
+ my $frm = {};
+ if($self->ReqMethod() eq 'POST') {
+ $frm = $self->FormCheck(
+ { name => 'username', required => 1, minlength => 2, maxlength => 15, template => 'pname' },
+ { name => 'mail', required => 1, template => 'mail' },
+ { name => 'pass1', required => 1, minlength => 4, maxlength => 15, template => 'asciprint' },
+ { name => 'pass2', required => 1, minlength => 4, maxlength => 15, template => 'asciprint' },
+ );
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'badpass' ] : [ 'badpass' ]
+ if $frm->{pass1} ne $frm->{pass2};
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'usrexists' ] : [ 'usrexists' ]
+ if $frm->{username} eq 'anonymous' || $self->DBGetUser(username => $frm->{username})->[0];
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'mailexists' ] : [ 'mailexists' ]
+ if $frm->{mail} && $self->DBGetUser(mail => $frm->{mail})->[0];
+
+ if(!$frm->{_err}) {
+ $self->DBAddUser($frm->{username}, md5_hex($frm->{pass1}), $frm->{mail}, 2);
+ return $self->AuthLogin($frm->{username}, $frm->{pass1}, 1, '/');
+ }
+ }
+ $self->ResAddTpl(userreg => {
+ reg => $frm,
+ });
+}
+
+
+sub UsrPass {
+ my $self = shift;
+
+ (return $self->ResRedirect('/', 'temp')) if $self->AuthInfo()->{id};
+
+ my $d = $self->ReqParam('d');
+
+ my $frm = {};
+ if(!$d && $self->ReqMethod() eq 'POST') {
+ $frm = $self->FormCheck({ name => 'mail', required => 1, template => 'mail' });
+ my $unfo;
+ if(!$frm->{_err}) {
+ $frm->{mail} =~ s/%//g;
+ $unfo = $self->DBGetUser(mail => $frm->{mail})->[0];
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'nomail' ] : [ 'nomail' ]
+ if !$unfo;
+ }
+ if(!$frm->{_err}) {
+ my @chars = ( 'A'..'Z', 'a'..'z', 0..9 );
+ my $pass = join('', map $chars[int rand $#chars+1], 0..8);
+ $self->DBUpdateUser($unfo->{id}, passwd => md5_hex($pass));
+ $self->SendMail(sprintf(<<__, $unfo->{username}, $unfo->{username}, $pass),
+Hello %s,
+
+Your password has been reset, you can now login at http://vndb.org/ with the
+following information:
+
+Username: %s
+Password: %s
+
+Now don't forget your password again! :-)
+
+vndb.org
+__
+ To => $frm->{mail},
+ Subject => sprintf('Password request for %s', $unfo->{username}),
+ );
+ return $self->ResRedirect('/u/newpass?d=1', 'post');
+ }
+ }
+
+ $self->ResAddTpl(userpass => {
+ pas => $frm,
+ done => $d,
+ });
+}
+
+
+sub UsrEdit {
+ my $self = shift;
+ my $user = shift;
+
+ my $u = $self->AuthInfo();
+ return $self->ResDenied if !$u->{id};
+ my $adm = $u->{id} != $user;
+ return $self->ResDenied if $adm && !$self->AuthCan('useredit');
+ $u = $self->DBGetUser(uid => $user)->[0] if $adm;
+ return $self->ResNotFound if !$u->{id};
+
+ my $d = $self->ReqParam('d');
+
+ my $frm = {};
+ if(!$d && $self->ReqMethod() eq 'POST') {
+ $frm = $self->FormCheck(
+ { name => 'mail', required => 1, template => 'mail' },
+ { name => 'pass1', required => 0, template => 'asciiprint' },
+ { name => 'pass2', required => 0, template => 'asciiprint' },
+ { name => 'rank', required => $adm, enum => [ '1'..($#{$self->{ranks}}-1) ] },
+ { name => 'pvotes',required => 0 },
+ { name => 'plist', required => 0 },
+ { name => 'pign_nsfw', required => 0 },
+ );
+ if(($frm->{pass1} || $frm->{pass2}) && $frm->{pass1} ne $frm->{pass2}) {
+ $frm->{_err} = [] if !$frm->{_err};
+ push(@{$frm->{_err}}, 'badpass');
+ }
+ if(!$frm->{_err}) {
+ my $pass = $frm->{pass1} ? md5_hex($frm->{pass1}) : '';
+ my %opts = (
+ 'mail' => $frm->{mail},
+ );
+ $opts{passwd} = $pass if $pass;
+ $opts{rank} = $frm->{rank} if $adm;
+ $opts{flags} = $frm->{pvotes} ? $VNDB::UFLAGS->{votes} : 0;
+ $opts{flags} += $VNDB::UFLAGS->{list} if $frm->{plist};
+ $opts{flags} += $VNDB::UFLAGS->{nsfw} if $frm->{pign_nsfw};
+ $self->DBUpdateUser($u->{id}, %opts);
+ return $adm ? $self->ResRedirect('/u'.$user.'/edit?d=1', 'post') :
+ $pass ? $self->AuthLogin($user, $frm->{pass1}, 1, '/u'.$user.'/edit?d=1') :
+ $self->ResRedirect('/u'.$user.'/edit?d=1', 'post');
+ }
+ }
+
+ $frm->{$_} ||= $u->{$_}
+ for (qw| username mail rank |);
+ $frm->{pvotes} ||= $u->{flags} & $VNDB::UFLAGS->{votes};
+ $frm->{plist} ||= $u->{flags} & $VNDB::UFLAGS->{list};
+ $frm->{pign_nsfw} ||= $u->{flags} & $VNDB::UFLAGS->{nsfw};
+ $self->ResAddTpl(useredit => {
+ form => $frm,
+ done => $d,
+ adm => $adm,
+ user => $user,
+ });
+}
+
+
+sub UsrList {
+ my $self = shift;
+ my $chr = shift;
+ $chr = 'all' if !defined $chr;
+
+ my $f = $self->FormCheck(
+ { name => 's', required => 0, default => 'username', enum => [ qw|username mail rank registered| ] },
+ { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ );
+
+ my($unfo, $np) = $self->DBGetUser(
+ order => $f->{s}.($f->{o} eq 'a' ? ' ASC' : ' DESC'),
+ $chr ne 'all' ? (
+ firstchar => $chr ) : (),
+ results => 50,
+ page => $f->{p},
+ what => 'list',
+ );
+
+ $self->ResAddTpl(userlist => {
+ users => $unfo,
+ chr => $chr,
+ page => $f->{p},
+ npage => $np,
+ order => [ $f->{s}, $f->{o} ],
+ } );
+}
+
+
+sub UsrPage {
+ my($self, $id) = @_;
+
+ my $u = $self->DBGetUser(uid => $id, what => 'list')->[0];
+ return $self->ResNotFound if !$u;
+
+ $self->ResAddTpl(userpage => {
+ user => $u,
+ lists => {
+ latest => scalar $self->DBGetVNList(uid => $id, results => 7),
+ graph => $self->DBVNListStats(uid => $id),
+ },
+ votes => {
+ latest => scalar $self->DBGetVotes(uid => $id, results => 10),
+ graph => $self->DBVoteStats(uid => $id),
+ },
+ });
+}
+
+1;
+
diff --git a/lib/VNDB/Util/Auth.pm b/lib/VNDB/Util/Auth.pm
new file mode 100644
index 00000000..dba5ba72
--- /dev/null
+++ b/lib/VNDB/Util/Auth.pm
@@ -0,0 +1,131 @@
+
+
+
+
+
+# N E E D S M O A R S A L T !
+
+
+package VNDB::Util::Auth;
+
+use strict;
+use warnings;
+use Exporter 'import';
+use Digest::MD5 'md5_hex';
+use Crypt::Lite; # simple, small and easy encryption for cookies
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+@EXPORT = qw| AuthCheckCookie AuthLogin AuthLogout AuthInfo AuthCan AuthAddTpl |;
+
+
+{ # local data for these 2 methods only
+ my $crl = Crypt::Lite->new(debug => 0);
+ my $scrt = md5_hex("73jkS39Sal2)"); # just a random string, as long as it doesn't change
+
+sub AuthCheckCookie {
+ my $self = shift;
+ my $info = $self->{_Req} || $self;
+ $info->{_auth} = {} if !exists $info->{_auth};
+
+ my $cookie = $self->ReqCookie('vndb_auth');
+ return 0 if !$cookie;
+ my $str = $crl->decrypt($cookie, $scrt);
+ return 0 if length($str) < 36;
+ my $pass = substr($str, 4, 32);
+ my $user = substr($str, 36);
+ return _AuthCheck($self, $user, $pass);
+}
+
+sub AuthLogin {
+ my $self = shift;
+ my $user = lc(scalar shift);
+ my $psbk = shift;
+ my $pass = md5_hex($psbk);
+ my $keep = shift;
+ my $to = shift;
+ my $status = _AuthCheck($self, $user, $pass);
+ if($status == 1) {
+ (my $cookie = $crl->encrypt("VNDB$pass$user", $scrt)) =~ s/\r?\n//g;
+ $self->ResRedirect($to, "post");
+ $self->ResAddHeader('Set-Cookie', "vndb_auth=$cookie; " . ($keep ? 'expires=Sat, 01-Jan-2030 00:00:00 GMT; ' : ' ') . "path=/; domain=$self->{CookieDomain}");
+ return 1;
+ }
+ return $status;
+}
+} # end of local data
+
+sub AuthLogout {
+ my $self = shift;
+ $self->ResRedirect('/', 'temp');
+ $self->ResAddHeader('Set-Cookie', "vndb_auth= ; expires=Sat, 01-Jan-2000 00:00:00 GMT; path=/; domain=$self->{CookieDomain}");
+}
+
+sub AuthInfo {
+ my $self = shift;
+ my $info = $self->{_Req} || shift;
+ return $info->{_auth} || {};
+}
+
+sub AuthCan {
+ my $self = shift;
+ my $act = shift;
+ my $info = $self->{_Req} || shift;
+ return $self->{ranks}[($info->{_auth}{rank}||0)+1]{$act};
+}
+
+sub _AuthCheck {
+ my $self = shift;
+ my $user = shift;
+ my $pass = shift;
+ my $info = $self->{_Req} || shift;
+
+ $info->{_auth} = undef;
+
+ return 2 if !$user || length($user) > 15 || length($user) < 2;
+ return 3 if !$pass || length($pass) != 32;
+
+ my $d = $self->DBGetUser(username => $user, passwd => $pass)->[0];
+ return 4 if !defined $d->{id};
+ return 5 if !$d->{rank};
+
+ $info->{_auth} = $d;
+
+ return 1;
+}
+
+
+# adds the keys AuthLoggedin, AuthRank, AuthUsername, AuthMail, AuthId
+sub AuthAddTpl {
+ my $self = shift;
+ my $info = $self->{_Req} || shift;
+ my %tpl;
+
+ if($info->{_auth}{id}) {
+ %tpl = (
+ AuthLoggedin => 1,
+ AuthRank => $info->{_auth}{rank},
+ AuthRankname => $self->{ranks}[0][0][$info->{_auth}{rank}],
+ AuthUsername => $info->{_auth}{username},
+ AuthMail => $info->{_auth}{mail},
+ AuthId => $info->{_auth}{id},
+ AuthNsfw => $info->{_auth}{flags} & $VNDB::UFLAGS->{nsfw},
+ );
+ } else {
+ %tpl = (
+ AuthLoggedin => 0,
+ AuthRank => '',
+ AuthRankname => '',
+ AuthUsername => '',
+ AuthMail => '',
+ AuthId => 0,
+ AuthNsfw => 0,
+ );
+ }
+ $tpl{'Auth'.$_} = $self->{ranks}[($info->{_auth}{rank}||0)+1]{$_}
+ for (keys %{$self->{ranks}[0][1]});
+ $self->ResAddTpl(%tpl);
+}
+
+1;
+
diff --git a/lib/VNDB/Util/DB.pm b/lib/VNDB/Util/DB.pm
new file mode 100644
index 00000000..59088387
--- /dev/null
+++ b/lib/VNDB/Util/DB.pm
@@ -0,0 +1,1268 @@
+
+package VNDB::Util::DB;
+
+use strict;
+use warnings;
+use DBI;
+use Exporter 'import';
+use Storable 'nfreeze', 'thaw';
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+
+@EXPORT = qw|
+ DBInit DBCheck DBCommit DBRollBack DBExit
+ DBLanguageCount DBCategoryCount DBTableCount DBGetHist DBLockItem DBIncId
+ DBGetUser DBAddUser DBUpdateUser
+ DBGetVotes DBVoteStats DBAddVote DBDelVote
+ DBGetVNList DBVNListStats DBAddVNList DBEditVNList DBDelVNList
+ DBGetVN DBAddVN DBEditVN DBDelVN DBHideVN
+ DBGetRelease DBAddRelease DBEditRelease DBDelRelease DBHideRelease
+ DBGetProducer DBGetProducerVN DBAddProducer DBEditProducer DBDelProducer DBHideProducer
+ DBExec DBRow DBAll DBLastId
+|;
+
+
+
+
+
+#-----------------------------------------------------------------------------#
+# I M P O R T A N T S T U F F #
+#-----------------------------------------------------------------------------#
+
+
+sub new {
+ my $me = shift;
+
+ my $type = ref($me) || $me;
+ $me = bless { @_ }, $type;
+
+ $me->DBInit();
+
+ return $me;
+}
+
+
+sub DBInit {
+ my $self = shift;
+ my $info = $self->{_DB} || $self;
+
+ my $settings;
+ $settings .= "host=$info->{host};" if $info->{host};
+ $settings .= "port=$info->{port};" if $info->{port};
+ $settings .= "dbname=$info->{database}";
+
+ $info->{sql} = DBI->connect("dbi:Pg:$settings",
+ $info->{user}, $info->{passwd}, {
+ PrintError => 0, RaiseError => 1,
+ AutoCommit => 0, pg_enable_utf8 => 1,
+ }
+ );
+}
+
+
+sub DBCheck {
+ my $self = shift;
+ my $info = $self->{_DB} || $self;
+
+ require Time::HiRes
+ if $self->{debug} && !$Time::Hires::VERSION;
+ $info->{Queries} = [] if $self->{debug};
+ my $start = [Time::HiRes::gettimeofday()] if $self->{debug};
+
+ if(!$info->{sql}->ping) {
+ warn "Ping failed, reconnecting";
+ $self->DBInit;
+ }
+ $info->{sql}->rollback();
+ push(@{$info->{Queries}},
+ [ 'ping/rollback', Time::HiRes::tv_interval($start) ])
+ if $self->{debug};
+}
+
+
+sub DBCommit {
+ my $self = shift;
+ my $info = $self->{_DB} || $self;
+ my $start = [Time::HiRes::gettimeofday()] if $self->{debug};
+ $info->{sql}->commit();
+ push(@{$info->{Queries}},
+ [ 'commit', Time::HiRes::tv_interval($start) ])
+ if $self->{debug};
+}
+
+
+sub DBRollBack {
+ my $self = shift;
+ my $info = $self->{_DB} || $self;
+ $info->{sql}->rollback();
+}
+
+
+sub DBExit {
+ my $self = shift;
+ my $info = $self->{_DB} || $self;
+ $info->{sql}->disconnect();
+}
+
+
+# XXX: this function should be disabled when performance is going to be a problem
+sub DBCategoryCount {
+ return {
+ (map { map { $_, 0 } keys %{$VNDB::CAT->{$_}[1]} } keys %{$VNDB::CAT}),
+ map { $_->{cat}, $_->{cnt} } @{shift->DBAll(q|
+ SELECT cat, COUNT(vid) AS cnt
+ FROM vn_categories vc
+ JOIN vn v ON v.latest = vc.vid
+ GROUP BY cat
+ ORDER BY cnt|
+ )}
+ };
+}
+
+
+# XXX: Above comment also applies to this function
+sub DBLanguageCount {
+ return { (map { $_ => 0 } keys %$VNDB::LANG ),
+ map { $_->{language} => $_->{count} } @{shift->DBAll(q|
+ SELECT rr.language, COUNT(DISTINCT rv.vid) AS count
+ FROM releases_rev rr
+ JOIN releases r ON r.latest = rr.id
+ JOIN releases_vn rv ON rv.rid = rr.id
+ GROUP BY rr.language|)} };
+}
+
+
+sub DBTableCount { # table (users, producers, vn, releases, votes)
+ return $_[0]->DBRow(q|
+ SELECT COUNT(*) as cnt
+ FROM %s
+ %s|,
+ $_[1],
+ $_[1] =~ /producers|vn|releases/ ? 'WHERE hidden = 0' : '',
+ )->{cnt};
+}
+
+
+
+# XXX: iid, ititle and hidden columns should be cached if performance will be a problem
+sub DBGetHist { # %options->{ type, id, cid, caused, next, page, results, ip, edits, showhid, what } (Item hist)
+ my($s, %o) = @_;
+
+ $o{results} ||= $o{next} ? 1 : 50;
+ $o{page} ||= 1;
+ $o{type} ||= '';
+ $o{what} ||= ''; #flags: user iid ititle
+ $o{showhid} ||= $o{type} && $o{type} ne 'u' && $o{id} || $o{cid} ? 1 : 0;
+
+ my %where = (
+ $o{cid} ? (
+ 'c.id IN(!l)' => $o{cid} ) : (),
+ $o{type} eq 'u' ? (
+ 'c.requester = %d' => $o{id} ) : (),
+
+ $o{type} eq 'v' && !$o{releases} ? ( 'c.type = 0' => 1,
+ $o{id} ? ( 'vr.vid = %d' => $o{id} ) : () ) : (),
+ $o{type} eq 'v' && $o{releases} ? (
+ '((c.type = 0 AND vr.vid = %d) OR (c.type = 1 AND rv.vid = %1$d))' => $o{id} ) : (),
+
+ $o{type} eq 'r' ? ( 'c.type = 1' => 1,
+ $o{id} ? ( 'rr.rid = %d' => $o{id} ) : () ) : (),
+ $o{type} eq 'p' ? ( 'c.type = 2' => 1,
+ $o{id} ? ( 'pr.pid = %d' => $o{id} ) : () ) : (),
+
+ $o{next} ? (
+ 'c.id > %d' => $o{next} ) : (),
+ $o{caused} ? (
+ 'c.causedby = %d' => $o{caused} ) : (),
+ $o{ip} ? (
+ 'c.ip = !s' => $o{ip} ) : (),
+ defined $o{edits} && !$o{edits} ? (
+ 'c.prev = 0' => 1 ) : (),
+ $o{edits} ? (
+ 'c.prev > 0' => 1 ) : (),
+
+ # get rid of 'hidden' items
+ !$o{showhid} ? (
+ '(v.hidden IS NOT NULL AND v.hidden = 0 OR r.hidden IS NOT NULL AND r.hidden = 0 OR p.hidden IS NOT NULL AND p.hidden = 0)' => 1,
+ ) : $o{showhid} == 2 ? (
+ '(v.hidden IS NOT NULL AND v.hidden = 1 OR r.hidden IS NOT NULL AND r.hidden = 1 OR p.hidden IS NOT NULL AND p.hidden = 1)' => 1,
+ ) : (),
+ );
+
+ my $where = keys %where ? 'WHERE !W' : '';
+
+ my $select = 'c.id, c.type, c.added, c.requester, c.comments, c.prev, c.causedby';
+ $select .= ', u.username' if $o{what} =~ /user/;
+ $select .= ', COALESCE(vr.vid, rr.rid, pr.pid) AS iid' if $o{what} =~ /iid/;
+ $select .= ', COALESCE(vr2.title, rr2.title, pr2.name) AS ititle' if $o{what} =~ /ititle/;
+
+ my $join = '';
+ $join .= ' JOIN users u ON u.id = c.requester' if $o{what} =~ /user/;
+ $join .= ' LEFT JOIN vn_rev vr ON c.type = 0 AND c.id = vr.id'.
+ ' LEFT JOIN releases_rev rr ON c.type = 1 AND c.id = rr.id'.
+ ' LEFT JOIN producers_rev pr ON c.type = 2 AND c.id = pr.id' if $o{what} =~ /(iid|ititle)/ || $o{releases} || $o{id} || !$o{showhid};
+ # these joins should be optimised away at some point (cache the required columns in changes as mentioned above)
+ $join .= ' LEFT JOIN vn v ON v.id = vr.vid'.
+ ' LEFT JOIN vn_rev vr2 ON vr2.id = v.latest'.
+ ' LEFT JOIN releases r ON r.id = rr.rid'.
+ ' LEFT JOIN releases_rev rr2 ON rr2.id = r.latest'.
+ ' LEFT JOIN producers p ON p.id = pr.pid'.
+ ' LEFT JOIN producers_rev pr2 ON pr2.id = p.latest' if $o{what} =~ /ititle/ || $o{releases} || !$o{showhid};
+ $join .= ' LEFT JOIN releases_vn rv ON c.id = rv.rid' if $o{type} eq 'v' && $o{releases};
+
+ my $r = $s->DBAll(qq|
+ SELECT $select
+ FROM changes c
+ $join
+ $where
+ ORDER BY c.id %s
+ LIMIT %d OFFSET %d|,
+ $where ? \%where : (),
+ $o{next} ? 'ASC' : 'DESC',
+ $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
+ );
+ return $r if !wantarray;
+ return ($r, 0) if $#$r != $o{results};
+ pop @$r;
+ return ($r, 1);
+}
+
+
+sub DBLockItem { # table, id, locked
+ my($s, $tbl, $id, $l) = @_;
+ $s->DBExec(q|
+ UPDATE %s
+ SET locked = %d
+ WHERE id = %d|,
+ $tbl, $l, $id);
+}
+
+
+sub DBHideItem { # table, id, hidden
+ my($s, $tbl, $id, $h) = @_;
+ $s->DBExec(q|
+ UPDATE %s
+ SET hidden = %d
+ WHERE id = %d|,
+ $tbl, $h, $id);
+}
+
+
+sub DBIncId { # sequence (this is a rather low-level function... aww heck...)
+ return $_[0]->DBRow(q|SELECT nextval(!s) AS ni|, $_[1])->{ni};
+}
+
+
+
+#-----------------------------------------------------------------------------#
+# A U T H / U S E R S T U F F #
+#-----------------------------------------------------------------------------#
+
+
+sub DBGetUser { # %options->{ username mail passwd order firstchar uid results page what }
+ my $s = shift;
+ my %o = (
+ order => 'username ASC',
+ page => 1,
+ results => 10,
+ what => '',
+ @_
+ );
+
+ my %where = (
+ $o{username} ? (
+ 'username = !s' => $o{username} ) : (),
+ $o{mail} ? (
+ 'mail = !s' => $o{mail} ) : (),
+ $o{passwd} ? (
+ 'passwd = decode(!s, \'hex\')' => $o{passwd} ) : (),
+ $o{firstchar} ? (
+ 'SUBSTRING(username from 1 for 1) = !s' => $o{firstchar} ) : (),
+ !$o{firstchar} && defined $o{firstchar} ? (
+ 'ASCII(username) < 97 OR ASCII(username) > 122' => 1 ) : (),
+ $o{uid} ? (
+ 'id = %d' => $o{uid} ) : (),
+ );
+
+ my $where = keys %where ? 'AND !W' : '';
+ my $r = $s->DBAll(qq|
+ SELECT *
+ FROM users u
+ WHERE id > 0 $where
+ ORDER BY %s
+ LIMIT %d OFFSET %d|,
+ $where ? \%where : (),
+ $o{order},
+ $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
+ );
+
+ if($o{what} =~ /list/ && $#$r >= 0) {
+ my %r = map {
+ $r->[$_]{votes} = 0;
+ $r->[$_]{vnlist} = 0;
+ $r->[$_]{changes} = 0;
+ ($r->[$_]{id}, $_)
+ } 0..$#$r;
+
+ $r->[$r{$_->{uid}}]{votes} = $_->{cnt} for (@{$s->DBAll(q|
+ SELECT uid, COUNT(vid) AS cnt
+ FROM votes
+ WHERE uid IN(!l)
+ GROUP BY uid|,
+ [ keys %r ]
+ )});
+
+ $r->[$r{$_->{uid}}]{vnlist} = $_->{cnt} for (@{$s->DBAll(q|
+ SELECT uid, COUNT(vid) AS cnt
+ FROM vnlists
+ WHERE uid IN(!l)
+ GROUP BY uid|,
+ [ keys %r ]
+ )});
+
+ $r->[$r{$_->{requester}}]{changes} = $_->{cnt} for (@{$s->DBAll(q|
+ SELECT requester, COUNT(id) AS cnt
+ FROM changes
+ WHERE requester IN(!l)
+ GROUP BY requester|,
+ [ keys %r ]
+ )});
+ }
+
+ return $r if !wantarray;
+ return ($r, 0) if $#$r != $o{results};
+ pop @$r;
+ return ($r, 1);
+}
+
+
+sub DBAddUser { # username, passwd, mail, rank
+ return $_[0]->DBExec(q|
+ INSERT INTO users
+ (username, passwd, mail, rank, registered)
+ VALUES (!s, decode(!s, 'hex'), !s, %d, %d)|,
+ lc($_[1]), $_[2], $_[3], $_[4], time
+ );
+}
+
+
+sub DBUpdateUser { # uid, %options->{ columns in users table }
+ my $s = shift;
+ my $user = shift;
+ my %opt = @_;
+ my %h;
+
+ defined $opt{$_} && ($h{$_.' = !s'} = $opt{$_})
+ for (qw| username mail |);
+ defined $opt{$_} && ($h{$_.' = %d'} = $opt{$_})
+ for (qw| rank flags |);
+ $h{'passwd = decode(!s, \'hex\')'} = $opt{passwd}
+ if defined $opt{passwd};
+
+ return 0 if scalar keys %h <= 0;
+ return $s->DBExec(q|
+ UPDATE users
+ SET !H
+ WHERE id = %d|,
+ \%h, $user);
+}
+
+
+
+
+
+
+#-----------------------------------------------------------------------------#
+# V O T E S #
+#-----------------------------------------------------------------------------#
+
+
+sub DBGetVotes { # %options->{ uid vid order results page }
+ my($s, %o) = @_;
+ $o{order} ||= 'n.date DESC';
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+
+ my %where = (
+ $o{uid} ? ( 'n.uid = %d' => $o{uid} ) : (),
+ $o{vid} ? ( 'n.vid = %d' => $o{vid} ) : (),
+ );
+
+ my $where = scalar keys %where ? 'WHERE !W' : '';
+ my $r = $s->DBAll(qq|
+ SELECT n.vid, vr.title, n.vote, n.date, n.uid, u.username
+ FROM votes n
+ JOIN vn v ON v.id = n.vid
+ JOIN vn_rev vr ON vr.id = v.latest
+ JOIN users u ON u.id = n.uid
+ $where
+ ORDER BY %s
+ LIMIT %d OFFSET %d|,
+ $where ? \%where : (),
+ $o{order},
+ $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
+ );
+ return $r if !wantarray;
+ return ($r, 0) if $#$r < $o{results};
+ pop @$r;
+ return ($r, 1);
+}
+
+
+sub DBVoteStats { # uid|vid => id
+ my($s, $col, $id) = @_;
+ my $r = [ qw| 0 0 0 0 0 0 0 0 0 0 | ],
+ my $where = $col ? 'WHERE '.$col.' = '.$id : '';
+ $r->[$_->{vote}-1] = $_->{votes} for (@{$s->DBAll(qq|
+ SELECT vote, COUNT(vote) as votes
+ FROM votes
+ $where
+ GROUP BY vote|,
+ )});
+ return $r;
+}
+
+
+sub DBAddVote { # vid, uid, vote
+ $_[0]->DBExec(q|
+ UPDATE votes
+ SET vote = %d
+ WHERE vid = %d
+ AND uid = %d|,
+ $_[3], $_[1], $_[2]
+ ) || $_[0]->DBExec(q|
+ INSERT INTO votes
+ (vid, uid, vote, date)
+ VALUES (%d, %d, %d, %d)|,
+ $_[1], $_[2], $_[3], time
+ );
+ # XXX: performance improvement: let a cron job handle this
+ $_[0]->DBExec('SELECT calculate_rating()');
+}
+
+
+sub DBDelVote { # uid, vid # uid = 0 to delete all
+ my $uid = $_[1] ? 'uid = '.$_[1].' AND' : '';
+ $_[0]->DBExec(q|
+ DELETE FROM votes
+ WHERE %s vid = %d|,
+ $uid, $_[2]);
+ $_[0]->DBExec('SELECT calculate_rating()');
+}
+
+
+
+
+
+#-----------------------------------------------------------------------------#
+# U S E R V I S U A L N O V E L L I S T S #
+#-----------------------------------------------------------------------------#
+
+
+sub DBGetVNList { # %options->{ uid vid order results page status }
+ my($s, %o) = @_;
+ $o{results} ||= 10;
+ $o{page} ||= 1;
+ $o{order} ||= 'l.date DESC';
+
+ my %where = (
+ $o{uid} ? (
+ 'l.uid = %d' => $o{uid} ) : (),
+ $o{vid} ? (
+ 'l.vid = %d' => $o{vid} ) : (),
+ defined $o{status} ? (
+ 'l.status = %d' => $o{status} ) : (),
+ );
+
+ return wantarray ? ([], 0) : [] if !keys %where;
+
+ my $r = $s->DBAll(q|
+ SELECT l.vid, vr.title, l.status, l.comments, l.date, l.uid, u.username
+ FROM vnlists l
+ JOIN vn v ON l.vid = v.id
+ JOIN vn_rev vr ON vr.id = v.latest
+ JOIN users u ON l.uid = u.id
+ WHERE !W
+ ORDER BY %s
+ LIMIT %d OFFSET %d|,
+ \%where,
+ $o{order},
+ $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
+ );
+ return $r if !wantarray;
+ return ($r, 0) if $#$r < $o{results};
+ pop @$r;
+ return ($r, 1);
+}
+
+
+sub DBVNListStats { # uid|vid => id
+ my($s, $col, $id) = @_;
+ my $r = [ map 0, 0..$#$VNDB::LSTAT ],
+ my $where = $col ? 'WHERE '.$col.' = '.$id : '';
+ $r->[$_->{status}] = $_->{cnt} for (@{$s->DBAll(qq|
+ SELECT status, COUNT(uid) as cnt
+ FROM vnlists
+ $where
+ GROUP BY status|
+ )});
+ return $r;
+}
+
+
+sub DBAddVNList { # uid, vid, status, [comments]
+ $_[0]->DBExec(q|
+ INSERT INTO vnlists (uid, vid, status, date, comments)
+ VALUES (!l, !s)|,
+ [ @_[1..3], time ], $_[4]||'');
+}
+
+
+sub DBEditVNList { # %options->{ uid status comments vid }
+ my($s, %o) = @_;
+ my %set;
+ $set{'status = %d'} = $o{status} if defined $o{status};
+ $set{'comments = !s'} = $o{comments} if defined $o{comments};
+ return if !keys %set;
+ $s->DBExec(q|
+ UPDATE vnlists
+ SET !H
+ WHERE uid = %d
+ AND vid IN(!l)|,
+ \%set, $o{uid}, $o{vid}
+ );
+}
+
+
+sub DBDelVNList { # uid, @vid # uid = 0 to delete all
+ my($s, $uid, @vid) = @_;
+ $uid = $uid ? 'uid = '.$uid.' AND ' : '';
+ $s->DBExec(q|
+ DELETE FROM vnlists
+ WHERE %s vid IN(!l)|,
+ $uid, \@vid
+ );
+}
+
+
+
+
+
+#-----------------------------------------------------------------------------#
+# V I S U A L N O V E L S #
+#-----------------------------------------------------------------------------#
+
+
+sub DBGetVN { # %options->{ id rev char search order results page what cati cate lang }
+ my $s = shift;
+ my %o = (
+ page => 1,
+ results => 50,
+ order => 'vr.title ASC',
+ what => '',
+ @_ );
+
+ my %where = (
+ !$o{id} && !$o{rev} ? ( # don't fetch hidden items unless we ask for an ID
+ 'v.hidden = 0' => 1 ) : (),
+ $o{id} ? (
+ 'v.id = %d' => $o{id} ) : (),
+ $o{rev} ? (
+ 'vr.id = %d' => $o{rev} ) : (),
+ $o{char} ? (
+ 'LOWER(SUBSTR(vr.title, 1, 1)) = !s' => $o{char} ) : (),
+ defined $o{char} && !$o{char} ? (
+ '(ASCII(vr.title) < 97 OR ASCII(vr.title) > 122) AND (ASCII(vr.title) < 65 OR ASCII(vr.title) > 90)' => 1 ) : (),
+ $o{cati} && @{$o{cati}} ? ( q|
+ v.id IN(SELECT iv.id
+ FROM vn_categories ivc
+ JOIN vn iv ON iv.latest = ivc.vid
+ WHERE cat IN(!L)
+ GROUP BY iv.id
+ HAVING COUNT(cat) = |.($#{$o{cati}}+1).')' => $o{cati} ) : (),
+ $o{cate} && @{$o{cate}} ? ( q|
+ v.id NOT IN(SELECT iv.id
+ FROM vn_categories ivc
+ JOIN vn iv ON iv.latest = ivc.vid
+ WHERE cat IN(!L)
+ GROUP BY iv.id)| => $o{cate} ) : (),
+ $o{lang} && @{$o{lang}} ? ( q|
+ v.id IN(SELECT irv.vid
+ FROM releases_rev irr
+ JOIN releases ir ON irr.id = ir.latest
+ JOIN releases_vn irv ON irv.rid = irr.id
+ WHERE irr.language IN(!L)
+ AND irr.type <> 2
+ AND irr.released <= TO_CHAR('today'::timestamp, 'YYYYMMDD')::integer)| => $o{lang} ) : (),
+ );
+
+ if($o{search}) {
+ my %w;
+ for (split /[ -,]/, $o{search}) {
+ s/%//g;
+ next if length($_) < 2;
+ $w{ sprintf '(ivr.title ILIKE %s OR ivr.alias ILIKE %1$s OR irr.title ILIKE %1$s OR irr.original ILIKE %1$s)',
+ qs('%%'.$_.'%%') } = 1;
+ }
+ $where{ q|
+ v.id IN(SELECT iv.id
+ FROM vn iv
+ JOIN vn_rev ivr ON iv.latest = ivr.id
+ LEFT JOIN releases_vn irv ON irv.vid = iv.id
+ LEFT JOIN releases_rev irr ON irr.id = irv.rid
+ LEFT JOIN releases ir ON ir.latest = irr.id
+ WHERE !W
+ GROUP BY iv.id)| } = \%w if keys %w;
+ }
+
+ my $where = scalar keys %where ? 'WHERE !W' : '';
+
+ my @join = (
+ $o{rev} ?
+ 'JOIN vn v ON v.id = vr.vid' :
+ 'JOIN vn v ON vr.id = v.latest',
+ $o{what} =~ /changes/ ? (
+ 'JOIN changes c ON c.id = vr.id',
+ 'JOIN users u ON u.id = c.requester' ) : (),
+ );
+
+ my $sel = 'v.id, v.locked, v.hidden, v.c_released, v.c_languages, v.c_votes, vr.title, vr.id AS cid, v.rgraph';
+ $sel .= ', vr.alias, vr.image AS image, vr.img_nsfw, vr.length, vr.desc, vr.l_wp, vr.l_cisv, vr.l_vnn' if $o{what} =~ /extended/;
+ $sel .= ', c.added, c.requester, c.comments, v.latest, u.username, c.prev, c.causedby' if $o{what} =~ /changes/;
+
+ my $r = $s->DBAll(qq|
+ SELECT $sel
+ FROM vn_rev vr
+ @join
+ $where
+ ORDER BY %s
+ LIMIT %d OFFSET %d|,
+ $where ? \%where : (),
+ $o{order},
+ $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
+ );
+ $_->{c_released} = sprintf '%08d', $_->{c_released} for @$r;
+
+ if($o{what} =~ /(relations|categories)/ && $#$r >= 0) {
+ my %r = map {
+ $r->[$_]{relations} = [];
+ $r->[$_]{categories} = [];
+ ($r->[$_]{cid}, $_)
+ } 0..$#$r;
+
+ if($o{what} =~ /categories/) {
+ push(@{$r->[$r{$_->{vid}}]{categories}}, [ $_->{cat}, $_->{lvl} ]) for (@{$s->DBAll(q|
+ SELECT vid, cat, lvl
+ FROM vn_categories
+ WHERE vid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /relations/) {
+ my $rel = $s->DBAll(q|
+ SELECT rel.vid1, rel.vid2, rel.relation, vr.title
+ FROM vn_relations rel
+ JOIN vn v ON rel.vid2 = v.id
+ JOIN vn_rev vr ON v.latest = vr.id
+ WHERE rel.vid1 IN(!l)|,
+ [ keys %r ]);
+ push(@{$r->[$r{$_->{vid1}}]{relations}}, {
+ relation => $_->{relation},
+ id => $_->{vid2},
+ title => $_->{title}
+ }) for (@$rel);
+ }
+ }
+
+ return $r if !wantarray;
+ return ($r, 0) if $#$r != $o{results};
+ pop @$r;
+ return ($r, 1);
+}
+
+
+sub DBAddVN { # %options->{ columns in vn_rev + comm + relations }
+ my($s, %o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO changes (type, requester, ip, comments)
+ VALUES (%d, %d, !s, !s)|,
+ 0, $s->AuthInfo->{id}, $s->ReqIP, $o{comm});
+
+ my $id = $s->DBLastId('changes');
+
+ $s->DBExec(q|
+ INSERT INTO vn (latest)
+ VALUES (%d)|, $id);
+ my $vid = $s->DBLastId('vn');
+
+ _insert_vn_rev($s, $id, $vid, \%o);
+
+ return ($vid, $id);
+}
+
+
+sub DBEditVN { # id, %options->( columns in vn_rev + comm + relations + categories + uid + causedby }
+ my($s, $vid, %o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO changes (type, requester, ip, comments, prev, causedby)
+ VALUES (%d, %d, !s, !s, (
+ SELECT c.id
+ FROM changes c
+ JOIN vn_rev vr ON vr.id = c.id
+ WHERE vr.vid = %d
+ ORDER BY c.id DESC
+ LIMIT 1
+ ), %d)|,
+ 0, $o{uid}||$s->AuthInfo->{id}, $s->ReqIP, $o{comm}, $vid, $o{causedby}||0);
+
+ my $id = $s->DBLastId('changes');
+
+ _insert_vn_rev($s, $id, $vid, \%o);
+
+ $s->DBExec(q|UPDATE vn SET latest = %d WHERE id = %d|, $id, $vid);
+ return $id;
+}
+
+
+sub _insert_vn_rev {
+ my($s, $cid, $vid, $o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO vn_rev (id, vid, title, "desc", alias, image, img_nsfw, length, l_wp, l_cisv, l_vnn)
+ VALUES (%d, %d, !s, !s, !s, %d, %d, %d, !s, %d, %d)|,
+ $cid, $vid, @$o{qw|title desc alias image img_nsfw length l_wp l_cisv l_vnn|});
+
+ $s->DBExec(q|
+ INSERT INTO vn_categories (vid, cat, lvl)
+ VALUES (%d, !s, %d)|,
+ $cid, $_->[0], $_->[1]
+ ) for (@{$o->{categories}});
+
+ $s->DBExec(q|
+ INSERT INTO vn_relations (vid1, vid2, relation)
+ VALUES (%d, %d, %d)|,
+ $cid, $_->[1], $_->[0]
+ ) for (@{$o->{relations}});
+}
+
+
+sub DBDelVN { # id
+ my($s, $vid) = @_;
+
+ # delete or update relations
+ my $rels = $s->DBAll(q|
+ SELECT r.id, COUNT(rv2.vid) AS vids
+ FROM releases r
+ JOIN releases_vn rv ON rv.rid = r.latest
+ JOIN releases_vn rv2 ON rv2.rid = r.latest
+ WHERE rv.vid = %d
+ GROUP BY r.id|,
+ $vid
+ );
+ # delete if no other VN's were found
+ $s->DBDelRelease(0, map { $_->{vids} == 1 ? $_->{id} : () } @$rels);
+ # remove relation otherwise
+ $s->DBExec(q|
+ DELETE FROM releases_vn
+ WHERE vid = %d|,
+ $vid);
+
+ $s->DBExec($_, $vid) for(
+ q|DELETE FROM changes c WHERE c.id IN(SELECT v.id FROM vn_rev v WHERE v.vid = %d)|,
+ q|DELETE FROM vn WHERE id = %d|,
+ q|DELETE FROM vn_categories WHERE vid IN(SELECT v.id FROM vn_rev v WHERE v.vid = %d)|,
+ q|DELETE FROM vn_relations WHERE vid1 IN(SELECT v.id FROM vn_rev v WHERE v.vid = %d)|,
+ q|DELETE FROM vn_rev WHERE vid = %d|,
+ q|DELETE FROM vn_relations WHERE vid2 = %d|,
+ q|DELETE FROM votes WHERE vid = %d|,
+ q|DELETE FROM vnlists WHERE vid = %d|,
+ );
+}
+
+
+sub DBHideVN { # id, hidden
+ my($s, $id, $h) = @_;
+ $s->DBExec(q|
+ UPDATE vn
+ SET hidden = %d
+ WHERE id = %d|,
+ $h, $id);
+
+# $s->DBExec(q|
+# DELETE FROM vn_relations
+# WHERE vid2 = %d
+# OR vid1 IN(SELECT id FROM vn_rev WHERE vid = %d)|,
+# $id, $id);
+# $s->DBDelVNList(0, $id);
+# $s->DBDelVote(0, $id);
+}
+
+
+
+
+#-----------------------------------------------------------------------------#
+# R E L E A S E S #
+#-----------------------------------------------------------------------------#
+
+
+sub DBGetRelease { # %options->{ id vid results page rev }
+ my($s, %o) = @_;
+
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+ $o{what} ||= '';
+ my %where = (
+ !$o{id} && !$o{rev} ? (
+ 'r.hidden = 0' => 1 ) : (),
+ $o{id} ? (
+ 'r.id = %d' => $o{id} ) : (),
+ $o{rev} ? (
+ 'rr.id = %d' => $o{rev} ) : (),
+ $o{vid} ? (
+ 'rv.vid = %d' => $o{vid} ) : (),
+ );
+
+ my $where = scalar keys %where ? 'WHERE !W' : '';
+ my @join;
+ push @join, $o{rev} ? 'JOIN releases r ON r.id = rr.rid' : 'JOIN releases r ON rr.id = r.latest';
+ push @join, 'JOIN changes c ON c.id = rr.id' if $o{what} =~ /changes/;
+ push @join, 'JOIN users u ON u.id = c.requester' if $o{what} =~ /changes/;
+ push @join, 'JOIN releases_vn rv ON rv.rid = rr.id' if $o{vid};
+
+ my $select = 'r.id, r.locked, r.hidden, rr.id AS cid, rr.title, rr.original, rr.language, rr.website, rr.released, rr.notes, rr.minage, rr.type';
+ $select .= ', c.added, c.requester, c.comments, r.latest, u.username, c.prev' if $o{what} =~ /changes/;
+
+ my $r = $s->DBAll(qq|
+ SELECT $select
+ FROM releases_rev rr
+ @join
+ $where
+ ORDER BY rr.released ASC
+ LIMIT %d OFFSET %d|,
+ $where ? \%where : (),
+ $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
+ );
+ $_->{released} = sprintf '%08d', $_->{released} for @$r;
+
+ if($#$r >= 0 && $o{what} =~ /(vn|producers|platforms|media)/) {
+ my %r = map {
+ $r->[$_]{producers} = [];
+ $r->[$_]{platforms} = [];
+ $r->[$_]{media} = [];
+ $r->[$_]{vn} = [];
+ ($r->[$_]{cid}, $_)
+ } 0..$#$r;
+
+ if($o{what} =~ /vn/) {
+ push(@{$r->[$r{$_->{rid}}]{vn}}, $_) for (@{$s->DBAll(q|
+ SELECT rv.rid, vr.vid, vr.title
+ FROM releases_vn rv
+ JOIN vn v ON v.id = rv.vid
+ JOIN vn_rev vr ON vr.id = v.latest
+ WHERE rv.rid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+
+ if($o{what} =~ /producers/) {
+ push(@{$r->[$r{$_->{rid}}]{producers}}, $_) for (@{$s->DBAll(q|
+ SELECT rp.rid, p.id, pr.name, pr.type
+ FROM releases_producers rp
+ JOIN producers p ON rp.pid = p.id
+ JOIN producers_rev pr ON pr.id = p.latest
+ WHERE rp.rid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+ if($o{what} =~ /platforms/) {
+ ($_->{platform}=~s/\s+//||1)&&push(@{$r->[$r{$_->{rid}}]{platforms}}, $_->{platform}) for (@{$s->DBAll(q|
+ SELECT rid, platform
+ FROM releases_platforms
+ WHERE rid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+ if($o{what} =~ /media/) {
+ ($_->{medium}=~s/\s+//||1)&&push(@{$r->[$r{$_->{rid}}]{media}}, $_) for (@{$s->DBAll(q|
+ SELECT rid, medium, qty
+ FROM releases_media
+ WHERE rid IN(!l)|,
+ [ keys %r ]
+ )});
+ }
+ }
+
+ return $r if !wantarray;
+ return ($r, 0) if $#$r < $o{results};
+ pop @$r;
+ return ($r, 1);
+}
+
+
+sub DBAddRelease { # options -> { columns in releases_rev table + comm + vn + producers + media + platforms }
+ my($s, %o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO changes (type, requester, ip, comments)
+ VALUES (%d, %d, !s, !s)|,
+ 1, $s->AuthInfo->{id}, $s->ReqIP, $o{comm});
+
+ my $id = $s->DBLastId('changes');
+ $s->DBExec(q|
+ INSERT INTO releases (latest)
+ VALUES (%d)|, $id);
+ my $rid = $s->DBLastId('releases');
+
+ _insert_release_rev($s, $id, $rid, \%o);
+
+ $s->DBExec('SELECT update_vncache(%d)', $_) for (@{$o{vn}});
+ return ($rid, $id);
+}
+
+
+sub DBEditRelease { # id, %opts->{ columns in releases_rev table + comm + vn + producers + media + platforms }
+ my($s, $rid, %o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO changes (type, requester, ip, comments, prev)
+ VALUES (%d, %d, !s, !s, (
+ SELECT c.id
+ FROM changes c
+ JOIN releases_rev rr ON rr.id = c.id
+ WHERE rr.rid = %d
+ ORDER BY c.id DESC
+ LIMIT 1
+ ))|,
+ 1, $s->AuthInfo->{id}, $s->ReqIP, $o{comm}, $rid);
+
+ my $id = $s->DBLastId('changes');
+
+ _insert_release_rev($s, $id, $rid, \%o);
+
+ $s->DBExec(q|UPDATE releases SET latest = %d WHERE id = %d|, $id, $rid);
+
+ $s->DBExec('SELECT update_vncache(%d)', $_) for (@{$o{vn}});
+ return $id;
+}
+
+
+sub _insert_release_rev {
+ my($s, $cid, $rid, $o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO releases_rev (id, rid, title, original, language, website, released, notes, minage, type)
+ VALUES (%d, %d, !s, !s, !s, !s, %d, !s, %d, %d)|,
+ $cid, $rid, @$o{qw| title original language website released notes minage type|});
+
+ $s->DBExec(q|
+ INSERT INTO releases_producers (rid, pid)
+ VALUES (%d, %d)|,
+ $cid, $_
+ ) for (@{$o->{producers}});
+
+ $s->DBExec(q|
+ INSERT INTO releases_platforms (rid, platform)
+ VALUES (%d, !s)|,
+ $cid, $_
+ ) for (@{$o->{platforms}});
+
+ $s->DBExec(q|
+ INSERT INTO releases_vn (rid, vid)
+ VALUES (%d, %d)|,
+ $cid, $_
+ ) for (@{$o->{vn}});
+
+ $s->DBExec(q|
+ INSERT INTO releases_media (rid, medium, qty)
+ VALUES (%d, !s, %d)|,
+ $cid, $_->[0], $_->[1]
+ ) for (@{$o->{media}});
+}
+
+
+sub DBDelRelease { # $vns, @ids
+ my($s, $vn, @rid) = @_;
+ return if !@rid;
+ $s->DBExec($_, \@rid) for(
+ q|DELETE FROM changes WHERE id IN(SELECT rr.id FROM releases_rev rr WHERE rr.rid IN(!l))|,
+ q|DELETE FROM releases_producers WHERE rid IN(SELECT rr.id FROM releases_rev rr WHERE rr.rid IN(!l))|,
+ q|DELETE FROM releases_platforms WHERE rid IN(SELECT rr.id FROM releases_rev rr WHERE rr.rid IN(!l))|,
+ q|DELETE FROM releases_media WHERE rid IN(SELECT rr.id FROM releases_rev rr WHERE rr.rid IN(!l))|,
+ q|DELETE FROM releases_rev WHERE rid IN(!l)|,
+ q|DELETE FROM releases_vn WHERE rid IN(!l)|,
+ q|DELETE FROM releases WHERE id IN(!l)|,
+ );
+
+ if($vn) {
+ $s->DBExec('SELECT update_vncache(%d)', $_) for (@$vn);
+ }
+}
+
+
+sub DBHideRelease { # id, hidden, vns
+ my($s, $id, $h, $vn) = @_;
+ $s->DBExec(q|
+ UPDATE releases
+ SET hidden = %d
+ WHERE id = %d|,
+ $h, $id);
+ if(@$vn) {
+ $s->DBExec('SELECT update_vncache(%d)', $_) for (@$vn);
+ }
+}
+
+
+
+#-----------------------------------------------------------------------------#
+# P R O D U C E R S #
+#-----------------------------------------------------------------------------#
+
+
+sub DBGetProducer { # %options->{ id search char results page rev }
+ my($s, %o) = @_;
+
+ $o{results} ||= 50;
+ $o{page} ||= 1;
+ $o{search} =~ s/%//g if $o{search};
+ $o{what} ||= '';
+ my %where = (
+ !$o{id} && !$o{rev} ? (
+ 'p.hidden = 0' => 1 ) : (),
+ $o{id} ? (
+ 'p.id = %d' => $o{id} ) : (),
+ $o{search} ? (
+ sprintf('(pr.name ILIKE %s OR pr.original ILIKE %1$s)', qs('%%'.$o{search}.'%%')), 1
+ ) : (),
+ $o{char} ? (
+ 'LOWER(SUBSTR(pr.name, 1, 1)) = !s' => $o{char} ) : (),
+ defined $o{char} && !$o{char} ? (
+ '(ASCII(pr.name) < 97 OR ASCII(pr.name) > 122) AND (ASCII(pr.name) < 65 OR ASCII(pr.name) > 90)' => 1 ) : (),
+ $o{rev} ? (
+ 'pr.id = %d' => $o{rev} ) : (),
+ );
+
+ my $where = scalar keys %where ? 'WHERE !W' : '';
+ my @join;
+ push @join, $o{rev} ? 'JOIN producers p ON p.id = pr.pid' : 'JOIN producers p ON pr.id = p.latest';
+ push @join, 'JOIN changes c ON c.id = pr.id' if $o{what} =~ /changes/;
+ push @join, 'JOIN users u ON u.id = c.requester' if $o{what} =~ /changes/;
+
+ my $select = 'p.id, p.locked, p.hidden, pr.type, pr.name, pr.original, pr.website, pr.lang, pr.desc';
+ $select .= ', c.added, c.requester, c.comments, p.latest, pr.id AS cid, u.username, c.prev' if $o{what} =~ /changes/;
+
+ my $r = $s->DBAll(qq|
+ SELECT $select
+ FROM producers_rev pr
+ @join
+ $where
+ ORDER BY pr.name ASC
+ LIMIT %d OFFSET %d|,
+ $where ? \%where : (),
+ $o{results}+(wantarray?1:0), $o{results}*($o{page}-1)
+ );
+
+ return $r if !wantarray;
+ return ($r, 0) if $#$r < $o{results};
+ pop @$r;
+ return ($r, 1);
+}
+
+
+# XXX: This query is killing me!
+sub DBGetProducerVN { # pid
+ return $_[0]->DBAll(q|
+ SELECT v.id, MAX(vr.title) AS title, MIN(rr.released) AS date
+ FROM releases_producers vp
+ JOIN releases_rev rr ON rr.id = vp.rid
+ JOIN releases r ON r.latest = rr.id
+ JOIN releases_vn rv ON rv.rid = rr.id
+ JOIN vn v ON v.id = rv.vid
+ JOIN vn_rev vr ON vr.id = v.latest
+ WHERE vp.pid = %d
+ AND v.hidden = 0
+ GROUP BY v.id
+ ORDER BY date|,
+ $_[1]);
+}
+
+
+sub DBAddProducer { # %opts->{ columns in producers_rev + comm }
+ my($s, %o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO changes (type, requester, ip, comments)
+ VALUES (%d, %d, !s, !s)|,
+ 2, $s->AuthInfo->{id}, $s->ReqIP, $o{comm});
+
+ my $id = $s->DBLastId('changes');
+ $s->DBExec(q|
+ INSERT INTO producers (latest)
+ VALUES (%d)|, $id);
+ my $pid = $s->DBLastId('producers');
+
+ _insert_producer_rev($s, $id, $pid, \%o);
+
+ return ($pid, $id);
+}
+
+
+sub DBEditProducer { # id, %opts->{ columns in producers_rev + comm }
+ my($s, $pid, %o) = @_;
+
+ $s->DBExec(q|
+ INSERT INTO changes (type, requester, ip, comments, prev)
+ VALUES (%d, %d, !s, !s, (
+ SELECT c.id
+ FROM changes c
+ JOIN producers_rev pr ON pr.id = c.id
+ WHERE pr.pid = %d
+ ORDER BY c.id DESC
+ LIMIT 1
+ ))|,
+ 2, $s->AuthInfo->{id}, $s->ReqIP, $o{comm}, $pid);
+
+ my $id = $s->DBLastId('changes');
+
+ _insert_producer_rev($s, $id, $pid, \%o);
+
+ $s->DBExec(q|UPDATE producers SET latest = %d WHERE id = %d|, $id, $pid);
+ return $id;
+}
+
+
+sub _insert_producer_rev {
+ my($s, $cid, $pid, $o) = @_;
+ $s->DBExec(q|
+ INSERT INTO producers_rev (id, pid, name, original, website, type, lang, "desc")
+ VALUES (%d, %d, !s, !s, !s, !s, !s, !s)|,
+ $cid, $pid, @$o{qw| name original website type lang desc|});
+}
+
+
+sub DBDelProducer { # id
+ my($s, $pid) = @_;
+ $s->DBExec($_, $pid) for (
+ q|DELETE FROM changes c WHERE c.id IN(SELECT p.id FROM producers_rev p WHERE p.pid = %d)|,
+ q|DELETE FROM producers_rev WHERE pid = %d|,
+ q|DELETE FROM releases_producers WHERE pid = %d|,
+ q|DELETE FROM producers WHERE id = %d|,
+ );
+}
+
+
+sub DBHideProducer { # id, hidden
+ my($s, $id, $h) = @_;
+ $s->DBExec(q|
+ UPDATE producers
+ SET hidden = %d
+ WHERE id = %d|,
+ $h, $id);
+}
+
+
+
+
+#-----------------------------------------------------------------------------#
+# U T I L I T I E S #
+#-----------------------------------------------------------------------------#
+
+
+sub DBExec { return sqlhelper(shift, 0, @_); }
+sub DBRow { return sqlhelper(shift, 1, @_); }
+sub DBAll { return sqlhelper(shift, 2, @_); }
+
+
+sub DBLastId { # table
+ return $_[0]->{_DB}->{sql}->last_insert_id(undef, undef, $_[1], undef);
+}
+
+
+sub sqlhelper { # type, query, @list
+ my $self = shift;
+ my $type = shift;
+ my $sqlq = shift;
+ my $s = $self->{_DB}->{sql};
+
+ my $start = [Time::HiRes::gettimeofday()] if $self->{debug};
+
+ $sqlq =~ s/\r?\n/ /g;
+ $sqlq =~ s/ +/ /g;
+ $sqlq = sqlprint($sqlq, @_) if exists $_[0];
+# warn "$sqlq\n";
+
+ my $q = $s->prepare($sqlq);
+ $q->execute();
+ my $r = $type == 1 ? $q->fetchrow_hashref :
+ $type == 2 ? $q->fetchall_arrayref({}) :
+ $q->rows;
+ $q->finish();
+
+ push(@{$self->{_DB}->{Queries}}, [ $sqlq, Time::HiRes::tv_interval($start) ]) if $self->{debug};
+
+ $r = 0 if $type == 0 && !$r;
+ $r = {} if $type == 1 && (!$r || ref($r) ne 'HASH');
+ $r = [] if $type == 2 && (!$r || ref($r) ne 'ARRAY');
+
+ return $r;
+}
+
+
+# Added features:
+# !s SQL-quote
+# !l listify
+# !L SQL-quote-and-listify
+# !H list of SET-items: key = format, value = replacement
+# !W same as !H, but for WHERE clauses
+sub sqlprint {
+ my $i = -1;
+ my @arg;
+ my $sq = my $s = shift;
+ while($sq =~ s/([%!])(.)//) {
+ $i++;
+ my $t = $1; my $d = $2;
+ if($t eq '%') {
+ if($d eq '%') {
+ $i--; next
+ }
+ $arg[$i] = $_[$i];
+ next;
+ }
+ if($d !~ /[slLHW]/) {
+ $i--; next
+ }
+ $arg[$i] = qs($_[$i]) if $d eq 's';
+ $arg[$i] = join(',', @{$_[$i]}) if $d eq 'l';
+ $arg[$i] = join(',', (qs(@{$_[$i]}))) if $d eq 'L';
+ if($d eq 'H' || $d eq 'W') {
+ my @i;
+ defined $_[$i]{$_} && push(@i, sqlprint($_, $_[$i]{$_})) for keys %{$_[$i]};
+ $arg[$i] = join($d eq 'H' ? ', ' : ' AND ', @i);
+ }
+ }
+ $s =~ s/![sSlLHW]/%s/g;
+ $s =~ s/!!/!/g;
+ return sprintf($s, @arg);
+}
+
+
+sub qs { # ISO SQL2-quoting, with some PgSQL-specific stuff
+ my @r = @_;
+ # NOTE: we use E''-style strings because backslash escaping in the normal ''-style
+ # depends on the standard_conforming_strings configuration option of PgSQL,
+ # while E'' will always behave the same regardless of the server configuration.
+ for (@r) {
+ (!defined $_ or $_ eq '_NULL_') && ($_ = 'NULL') && next;
+ s/'/''/g;
+ s/\\/\\\\/g;
+ $_ = "E'$_'";
+ }
+ return wantarray ? @r : $r[0];
+}
+
+
+1;
+
diff --git a/lib/VNDB/Util/Request.pm b/lib/VNDB/Util/Request.pm
new file mode 100644
index 00000000..fe631484
--- /dev/null
+++ b/lib/VNDB/Util/Request.pm
@@ -0,0 +1,46 @@
+
+package VNDB::Util::Request;
+
+use strict;
+use warnings;
+use Encode;
+use Exporter 'import';
+
+our @EXPORT;
+@EXPORT = qw| ReqParam ReqSaveUpload ReqCookie
+ ReqMethod ReqHeader ReqUri ReqIP |;
+
+sub new {
+ return bless {}, ref($_[0]) || $_[0];
+}
+sub ReqParam {
+ my($s,$n) = @_;
+ return wantarray
+ ? map { decode 'UTF-8', defined $_ ? $_ : '' } $FCGI::Handler::c->param($n)
+ : decode 'UTF-8', defined $FCGI::Handler::c->param($n) ? $FCGI::Handler::c->param($n) : '';
+}
+sub ReqSaveUpload {
+ my($s,$n,$f) = @_;
+ open my $F, '>', $f or die "Unable to write to $f: $!";
+ print $F $FCGI::Handler::c->param($n);
+ close $F;
+}
+sub ReqCookie {
+ my $c = Cookie::XS->fetch;
+ return $c && ref($c) eq 'HASH' && $c->{$_[1]} ? $c->{$_[1]}[0] : '';
+}
+sub ReqMethod {
+ return ($ENV{REQUEST_METHOD}||'') =~ /post/i ? 'POST' : 'GET';
+}
+sub ReqHeader {
+ (my $v = uc $_[1]) =~ tr/-/_/;
+ return $ENV{"HTTP_$v"}||'';
+}
+sub ReqUri {
+ return $ENV{REQUEST_URI};
+}
+sub ReqIP {
+ return $ENV{REMOTE_ADDR};
+}
+
+1;
diff --git a/lib/VNDB/Util/Response.pm b/lib/VNDB/Util/Response.pm
new file mode 100644
index 00000000..619429ae
--- /dev/null
+++ b/lib/VNDB/Util/Response.pm
@@ -0,0 +1,238 @@
+
+package VNDB::Util::Response;
+
+
+use strict;
+use warnings;
+use POSIX ();
+use Encode;
+use XML::Writer;
+use Compress::Zlib;
+use Exporter 'import';
+require bytes;
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $NTL::VERSION;
+@EXPORT = qw| ResRedirect ResNotFound ResDown ResDenied ResFile
+ ResForceBody ResSetContentType ResAddHeader ResAddTpl ResAddDefaultStuff
+ ResStartXML ResGetXML ResGetBody ResGet ResGetCGI ResSetModPerl |;
+
+sub new {
+ my $self = shift;
+ my $tplo = shift;
+ my $type = ref($self) || $self;
+ my $me = bless {
+ headers => [ ],
+ contenttype => 'text/html; charset=UTF-8',
+ code => 200,
+ tplo => $tplo,
+ tpl => { },
+ body => undef,
+ xmlobj => undef,
+ xmldata => undef,
+ whattouse => 1,
+ rc => 0,
+ }, $type;
+
+ return $me;
+}
+
+
+## Some ready-to-use methods
+sub ResRedirect {
+ my $self = shift;
+ my $url = shift; # should start with '/', if no URL specified, use referer or '/'
+ my $type = shift;
+ my $info = $self->{_Res} || $self;
+
+ if(!$url) {
+ $url = "/";
+ my $ref = $self->ReqHeader('Referer');
+ ($url = $ref) =~ s/^$self->{root_url}// if $ref;
+ }
+
+ my $code = !$type ? 301 :
+ $type eq 'temp' ? 307 :
+ $type eq 'post' ? 303 : 301;
+ $info->{code} = $code;
+ $info->{tpl} = {
+ error => {
+ url => $url,
+ code => $code,
+ }
+ };
+ $info->{headers} = [ 'Location', "$self->{root_url}$url" ];
+ $info->{contenttype} = 'text/html; charset=UTF-8';
+ $info->{whattouse} = 2;
+}
+
+sub ResNotFound {
+ my $s = shift;
+ my $i = $s->{_Res};
+ $i->{code} = 404;
+ $i->{whattouse} = 4;
+ push @{$i->{headers}}, 'X-Sendfile' => '/www/vndb/www/files/notfound.html';
+}
+
+sub ResDown {
+ my $self = shift;
+ my $msg = shift || '';
+ my $info = $self->{_Res} || $self;
+
+ $info->{code} = 200;
+ $info->{tpl} = {
+ error => {
+ code => 1,
+ msg => $msg, # specifies which message should be displayed
+ }
+ };
+ $info->{contenttype} = 'text/html; charset=UTF-8';
+ $info->{whattouse} = 2;
+}
+
+sub ResDenied {
+ my $self = shift;
+ $self->ResRedirect('/u/register?n=1', 'temp');
+}
+
+sub ResFile {
+ my($s,$f,@h) = @_;
+ my $i = $s->{_Res};
+ $i->{whattouse} = 4;
+ $i->{code} = 200;
+ $i->{contenttype} = '';
+ push @{$i->{headers}},
+ 'X-Sendfile' => $f,
+ 'Cache-Control' => sprintf('max-age=%d, public', 7*24*3600),
+ @h;
+}
+
+## And some often-used methods
+sub ResForceBody {
+ my $self = shift;
+ my $body = shift;
+ my $info = $self->{_Res} || $self;
+ $info->{whattouse} = 1;
+ $info->{body} = $body;
+}
+
+sub ResSetContentType {
+ my $self = shift;
+ my $ctype = shift;
+ my $info = $self->{_Res} || $self;
+ $info->{contenttype} = $ctype;
+ return 1;
+}
+
+sub ResAddHeader {
+ my $self = shift;
+ die("Odd number in parameters, must be in key => value format!") unless ((@_ % 2) == 0);
+ my $info = $self->{_Res} || $self;
+ $info->{headers} = [ @{$info->{headers}}, @_ ];
+ return 1;
+}
+
+sub ResAddTpl {
+ my $self = shift;
+ die("Odd number in parameters, must be in key=>value format") unless ((@_ % 2) == 0);
+ my $info = $self->{_Res} || $self;
+ $info->{tpl} = { page => { } } if !$info->{tpl}->{page};
+ $info->{tpl}->{page} = { %{$info->{tpl}->{page}}, @_ };
+ $info->{whattouse} = 2;
+ return 1;
+}
+
+sub ResStartXML {
+ my $self = shift;
+ my $info = $self->{_Res} || $self;
+ $info->{xmldata} = undef;
+ $info->{xmlobj} = XML::Writer->new(
+ OUTPUT => \$info->{xmldata},
+ NEWLINES => 0,
+ ENCODING => 'UTF-8',
+ DATA_MODE => 1,
+ DATA_INDENT => 2,
+ );
+ $info->{xmlobj}->xmlDecl();
+ $info->{contenttype} = "text/xml; charset=UTF-8";
+ # disable caching on XML content, IE < 7 has "some" bugs...
+ $self->ResAddHeader('Cache-Control' => 'must-revalidate, post-check=0, pre-check=0',
+ 'Pragma' => 'public');
+ $info->{whattouse} = 3;
+ return $info->{xmlobj};
+}
+
+## And of course some methods to get the information
+sub ResGetXML {
+ my $self = shift;
+ my $info = $self->{_Res} || $self;
+ return undef if !$info->{xmlobj} || !$info->{xmldata};
+ $info->{xmlobj}->end();
+ my $tmpvar = $info->{xmldata};
+ undef $info->{xmldata};
+ return $tmpvar;
+}
+
+sub ResGetBody {
+ my $self = shift;
+ my $info = $self->{_Res} || $self;
+ my $whattouse = shift || $info->{whattouse};
+ if($whattouse == 1) { return $info->{body}; }
+ if($whattouse == 2) {
+ $self->AddDefaultStuff() if exists $info->{tpl}->{page};
+ my $start = [Time::HiRes::gettimeofday()] if $self->{debug} && $Time::HiRes::VERSION;
+ my $output = $info->{tplo}->compile($info->{tpl});
+ $info->{_tpltime} = Time::HiRes::tv_interval($start) if $self->{debug} && $Time::HiRes::VERSION;
+ return $output;
+ }
+ if($whattouse == 3) { return $self->ResGetXML; }
+}
+
+sub ResGet {
+ my $self = shift;
+ my $info = $self->{_Res} || $self;
+ my $whattouse = shift || $info->{whattouse};
+
+ return ($info->{code}, $info->{headers}, $info->{contenttype}, $self->ResGetBody($whattouse));
+}
+
+
+my %scodes = (
+ # just a few useful codes
+ 200 => 'OK',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 307 => 'Temporary Redirect',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 500 => 'Internal Server Error'
+);
+
+# don't rename!
+sub ResSetModPerl {
+ my $s = shift;
+ my $i = $s->{_Res};
+ printf "Status: %d %s\r\n", $i->{code}, $scodes{$i->{code}};
+ print "X-Powered-By: Perl\r\n";
+ printf "Content-Type: %s\r\n", $i->{contenttype} if $i->{contenttype};
+ my $c=0;
+ printf "%s: %s\r\n", $i->{headers}[$c++], $i->{headers}[$c++]
+ while ($c<$#{$i->{headers}});
+
+ my $b = $s->ResGetBody||'';
+ if($b && $s->ReqHeader('Accept-Encoding') =~ /gzip/ && $i->{contenttype} =~ /^text/) {
+ my $ol = bytes::length($b) if $s->{debug};
+ $b = Compress::Zlib::memGzip(Encode::encode_utf8($b));
+ $i->{_gzip} = [ $ol, bytes::length($b) ];
+ print "Content-Encoding: gzip\n";
+ }
+ my $l = bytes::length($b);
+ printf "Content-Length: %d\r\n", $l if $l;
+ print "\r\n";
+ print $b;
+ $FCGI::Handler::outputted = 1;
+}
+
+1;
diff --git a/lib/VNDB/Util/Template.pm b/lib/VNDB/Util/Template.pm
new file mode 100644
index 00000000..f9c63998
--- /dev/null
+++ b/lib/VNDB/Util/Template.pm
@@ -0,0 +1,235 @@
+# VNDB::Util::Template - A direct copy of NTL::Util::Template
+
+# This file has not been edited for at least a year,
+# and there's probably no need to do so in the near future
+
+# template specific stuff:
+# [[ perl code to execute at the specified place ]]
+# [[= perl code, append return value to the template at the specified place ]]
+# [[: same as above, but escape special HTML chars (<, >, &, " and \n) ]]
+# [[% same as above, but also escape as an URL (expects UTF-8 strings) ]]
+# [[! perl code, append at the top of the script (useful for subroutine-declarations etc) ]]
+# [[+ path to a file to include, relative to $searchdir ]]
+
+package VNDB::Util::Template;
+
+use strict;
+use warnings;
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+
+
+sub new {
+ my $pack = shift;
+ my %ops = @_;
+ my $me = bless {
+ namespace => __PACKAGE__ . '::tpl',
+ pre_chomp => 0,
+ post_chomp => 0,
+ rm_newlines => 0,
+ %ops,
+ lastreload => 0
+ }, ref($pack) || $pack;
+
+ $me->{mainfile} = sprintf '%s/%s', $me->{searchdir}, $me->{filename};
+
+ die "No filename specified!" if !$me->{filename};
+ die "No searchdir specified!" if !$me->{searchdir};
+ die "Filename does not exist!" if !-e $me->{mainfile};
+ die "No place for the compiled script specified!" if !$me->{compiled};
+
+ $me->includescript();
+
+ return $me;
+}
+
+sub includescript {
+ my $self = shift;
+
+ my $dt = 0;
+ my $dc = (stat($self->{compiled}))[9] || 0;
+
+ if(-s $self->{compiled} && !exists $INC{$self->{compiled}}) {
+ eval { require $self->{compiled}; };
+ if(!$@) {
+ $self->{lastreload} = $dc;
+ } else {
+ # make sure we can fix the problem and try again
+ $INC{$self->{compiled}} = $self->{compiled};
+ die $@;
+ }
+ }
+
+ my $T_version = eval(sprintf '$%s::VERSION;', $self->{namespace});
+
+ if($dc > $self->{lastreload} || !$T_version) {
+ $dt = 1;
+ }
+ elsif($self->{deep_reload} && $T_version >= 0.1) {
+ my @T_files = @{ eval(sprintf '\@%s::T_FILES;', $self->{namespace}) };
+ if($#T_files >= 0) {
+ foreach (@T_files) {
+ if((stat(sprintf('%s/%s', $self->{searchdir}, $_)))[9] > $dc) {
+ $dt = 2;
+ last;
+ }
+ }
+ }
+ } elsif((stat($self->{mainfile}))[9] > $dc) {
+ $dt = 2;
+ }
+ if($dt) {
+ $self->compiletpl() if $dt == 2 || $dc <= $self->{lastreload};
+ delete $INC{$self->{compiled}};
+ eval { require $self->{compiled}; };
+ if(!$@) {
+ warn "Reloaded template\n";
+ } else {
+ $INC{$self->{compiled}} = $self->{compiled};
+ warn "Template contains errors, not reloading\n";
+ }
+ $self->{lastreload} = (stat($self->{compiled}))[9];
+ }
+}
+
+sub compile {
+ my $self = shift;
+ my $X = shift;
+ $self->includescript();
+
+ return $self->{namespace}->compile($X);
+}
+
+sub compiletpl {
+ my $self = shift;
+ open(my $T, '>', $self->{compiled}) || die sprintf '%s: %s', $self->{compiled}, $!;
+ printf $T <<__, __PACKAGE__, $self->{namespace}, ($self->compilefile());
+# Compiled from a template by %s
+package %s;
+
+use strict;
+use warnings;
+no warnings qw(redefine);
+use URI::Escape \'uri_escape_utf8\';
+
+our \$VERSION = 0.1;
+our \@T_FILES = qw| %s |;
+
+sub _hchar { local\$_=shift||return\'\';s/&/&amp;/g;s/</&lt;/g;s/>/&gt;/g;s/"/&quot;/g;s/\\r?\\n/<br \\/>/g;return\$_; }
+sub _huri { _hchar(uri_escape_utf8((scalar shift)||return \'\')) }
+%s
+%s
+%s
+1;
+__
+ close($T);
+ warn "Recompiled template\n";
+}
+
+sub compilefile {
+ my $self = shift;
+ my $file = shift||$self->{filename};
+ my $func = shift||'compile';
+
+ my $files = $file;
+ $file = sprintf('%s/%s', $self->{searchdir}, $file);
+ open(my $F, '<', $file) || die "$file: $!";
+ my $tpl = '';
+ $tpl .= $_ while(<$F>);
+ close($F);
+ my @t = split(//, $tpl);
+ $tpl = undef;
+
+ my $inperl = 0;
+ my $top = '';
+ my $R = '';
+ my $bottom = '';
+ my $dat = '';
+ my $perl = '';
+
+ for(my $i=0; $i<=$#t; $i++) {
+ # [[= (2), [[: (3) and [[% (4)
+ if(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[' && $t[$i+2] =~ /[=:%]/) {
+ $i+=2;
+ if($t[$i] eq '=') {
+ $inperl=2;
+ $perl = '\' . ( scalar ';
+ } elsif($t[$i] eq ':') {
+ $inperl=3;
+ $perl = '\' . _hchar( scalar ';
+ } else {
+ $inperl=4;
+ $perl = '\' . _huri( scalar ';
+ }
+ $R .= $self->_pd($dat);
+ } elsif($inperl >= 2 && $inperl <= 4 && $t[$i] eq ']' && $t[$i+1] eq ']') {
+ $inperl=0; $i++;
+ $R .= $perl . "\n) . '";
+ $dat = '';
+ # [[! (5)
+ } elsif(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[' && $t[$i+2] eq '!') {
+ $inperl=5; $i+=2;
+ $perl = '';
+ $R .= $self->_pd($dat);
+ } elsif($inperl == 5 && $t[$i] eq ']' && $t[$i+1] eq ']') {
+ $inperl=0; $i++;
+ $top .= $perl . "\n";
+ $dat = '';
+ # [[+ (6)
+ } elsif(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[' && $t[$i+2] eq '+') {
+ $inperl=6; $i+=2;
+ $R .= $self->_pd($dat);
+ $perl = '';
+ } elsif($inperl == 6 && $t[$i] eq ']' && $t[$i+1] eq ']') {
+ $inperl=0;$i++;
+ $perl =~ s/[\r\n\s]//g;
+ die "Invalid file specified: $perl\n" if $perl !~ /^[a-zA-Z0-9-_\.\/]+$/;
+ (my $func = $perl) =~ s/[^a-zA-Z0-9_]/_/g;
+ my($ifiles, $itop, $imid, $ibot) = $self->compilefile($perl, "T_$func");
+ $files .= ' ' . $ifiles;
+ $top .= $itop;
+ $bottom .= "\n\n$imid\n$ibot\n";
+ $R .= "' . T_$func(\$X) . '";
+ $dat = '';
+ # [[ (1)
+ } elsif(!$inperl && $t[$i] eq '[' && $t[$i+1] eq '[') {
+ $inperl = 1; $i++;
+ $R .= $self->_pd($dat);
+ $perl = "';\n";
+ } elsif($inperl == 1 && $t[$i] eq ']' && $t[$i+1] eq ']') {
+ $inperl=0; $i++;
+ $R .= $perl . "\n \$R .= '";
+ $dat = '';
+ # data
+ } elsif(!$inperl) {
+ (my $l = $t[$i]) =~ s/'/\\'/;
+ $dat .= $l;
+ } else {
+ $perl .= $t[$i];
+ }
+ }
+ if(!$inperl) {
+ $R .= $self->_pd($dat) . "';\n";
+ } else {
+ die "Error, no ']]' found at $file!\n";
+ }
+ $R = "sub $func {
+ my \$X = \$_[". ($func eq 'compile' ? 1 : 0) . "];
+ my \$R = '".$R."
+ return \$R;
+ }";
+ return($files, $top, $R, $bottom);
+}
+
+sub _pd { # Parse Dat
+ my $self = shift;
+ local $_ = shift;
+
+ s/[\r\n\s]+$//g if $_ !~ s/-$// && $self->{pre_chomp};
+ s/^[\r\n\s]+//g if $_ !~ s/^-// && $self->{post_chomp};
+ s/([\s\t]*)[\r\n]+([\s\t]*)/{ $1||$2?' ':'' }/eg if $self->{rm_newlines};
+ return $_;
+}
+
+1;
diff --git a/lib/VNDB/Util/Tools.pm b/lib/VNDB/Util/Tools.pm
new file mode 100644
index 00000000..4c873b55
--- /dev/null
+++ b/lib/VNDB/Util/Tools.pm
@@ -0,0 +1,145 @@
+
+package VNDB::Util::Tools;
+
+use strict;
+use warnings;
+use Encode;
+use Exporter 'import';
+
+our $VERSION = $VNDB::VERSION;
+our @EXPORT = qw| FormCheck AddHid SendMail AddDefaultStuff |;
+
+
+# Improved version of ParamsCheck
+# - hashref instead of hash
+# - parameters don't start with form*
+sub FormCheck {
+ my $self = shift;
+ my @ps = @_;
+ my %hash; my @err;
+
+ foreach my $i (0..$#ps) {
+ next if !$ps[$i] || ref($ps[$i]) ne 'HASH';
+ my $k = $ps[$i]{name};
+ $hash{$k} = [ ( $self->ReqParam($k) ) ];
+ $hash{$k}[0] = '' if !defined $hash{$k}[0];
+ foreach my $j (0..$#{$hash{$k}}) {
+ my $val = \$hash{$k}[$j]; my $e = 0;
+ $e = 1 if !$e && $ps[$i]{required} && !$$val && length($$val) < 1 && $$val ne '0';
+ $e = 2 if !$e && $ps[$i]{minlength} && length($$val) < $ps[$i]{minlength};
+ $e = 3 if !$e && $ps[$i]{maxlength} && length($$val) > $ps[$i]{maxlength};
+ if(!$e && $ps[$i]{template}) {
+ my $t = $ps[$i]{template};
+ $hash{$k}[$j] = lc $hash{$k}[$j] if $t eq 'pname';
+ $e = 4 if ($t eq 'mail' && $$val !~ # From regexlib.com, author: Gavin Sharp
+ /^(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+\@((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6}$/)
+ || ($t eq 'url' && $$val !~ # From regexlib.com, author: M H
+ /^(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?$/)
+ || ($t eq 'pname' && $$val !~ /^[a-z0-9][a-z0-9\-]*$/)
+ || ($t eq 'asciiprint' && $$val !~ /^[\x20-\x7E]*$/)
+ || ($t eq 'int' && $$val !~ /^\-?[0-9]+$/)
+ || ($t eq 'date' && $$val !~ /^[0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?$/);
+ }
+ $e = 5 if !$e && $ps[$i]{enum} && ref($ps[$i]{enum}) eq "ARRAY" && !_inarray($$val, $ps[$i]{enum});
+ if($e) {
+ if($ps[$i]{required}) {
+ my $errc = $ps[$i]{name}.'_'.$e;
+ $errc .= '_'.$ps[$i]{minlength} if $e == 2;
+ $errc .= '_'.$ps[$i]{maxlength} if $e == 3;
+ $errc .= '_'.$ps[$i]{template} if $e == 4;
+ push(@err, $errc);
+ last;
+ } else {
+ $hash{$k}[$j] = exists $ps[$i]{default} ? $ps[$i]{default} : undef;
+ }
+ }
+ last if !$ps[$i]{multi};
+ }
+ $hash{$k} = $hash{$k}[0] if !$ps[$i]{multi};
+ }
+ $hash{_err} = $#err >= 0 ? \@err : 0;
+
+ return \%hash;
+}
+
+sub AddHid {
+ my $fh = $_[0]->FormCheck({ name => 'fh', required => 0, maxlength => 30 })->{fh};
+ $_[1]->{_hid} = { map { $_ => 1 } 'com', 'mod', split /,/, $fh }
+ if $fh;
+}
+
+sub _inarray { # errr... this is from when I didn't know about grep
+ foreach (@{$_[1]}) {
+ (return 1) if $_[0] eq $_;
+ }
+ return 0;
+}
+
+
+sub SendMail {
+ my $self = shift;
+ my $body = shift;
+ my %hs = @_;
+
+ die "No To: specified!\n" if !$hs{To};
+ die "No Subject specified!\n" if !$hs{Subject};
+ $hs{'Content-Type'} ||= 'text/plain; charset=\'UTF-8\'';
+ $hs{From} ||= 'vndb <noreply@vndb.org>';
+ $hs{'X-mailer'} ||= "VNDB $VERSION";
+ $body =~ s/\r?\n/\n/g; # force a '\n'-linebreak
+
+ my $mail = '';
+ foreach (keys %hs) {
+ $hs{$_} =~ s/[\r\n]//g;
+ $mail .= sprintf "%s: %s\n", $_, $hs{$_};
+ }
+ $mail .= sprintf "\n%s", $body;
+
+ if(open(my $mailer, "|/usr/sbin/sendmail -t -f '$hs{From}'")) {
+ print $mailer encode('UTF-8', $mail);
+ die "Error running sendmail ($!)"
+ if !close($mailer);
+ } else {
+ die "Error opening sendail: $!";
+ }
+}
+
+sub AddDefaultStuff {
+ my $self = shift;
+
+ $self->AuthAddTpl;
+ $self->ResAddTpl(st => $self->{static_url});
+
+ $self->ResAddTpl('Stat'.$_, $self->DBTableCount($_))
+ for (qw|users producers vn releases votes|);
+
+ # development shit
+ if($self->{debug}) {
+ my $sqls;
+ for (@{$self->{_DB}->{Queries}}) {
+ $_->[0] =~ s/^\s//g;
+ $sqls .= sprintf("[%6.2fms] %s\n", $_->[1]*1000, $_->[0] || '[undef]');
+ }
+ $self->ResAddTpl(devshit => $sqls);
+ }
+}
+
+1;
+
+__END__
+# from HTTP::Date, small function, so why load an entire module?
+{
+ my @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);
+ my @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
+
+ sub time2str {
+ my $time = shift;
+ $time = time unless defined $time;
+ my($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($time);
+ sprintf('%s, %02d %s %04d %02d:%02d:%02d GMT',
+ $DoW[$wday],
+ $mday, $MoY[$mon], $year+1900,
+ $hour, $min, $sec);
+ }
+}
+
diff --git a/lib/VNDB/VN.pm b/lib/VNDB/VN.pm
new file mode 100644
index 00000000..f2340037
--- /dev/null
+++ b/lib/VNDB/VN.pm
@@ -0,0 +1,380 @@
+
+package VNDB::VN;
+
+use strict;
+use warnings;
+use Exporter 'import';
+use Digest::MD5;
+require bytes;
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+@EXPORT = qw| VNPage VNEdit VNLock VNDel VNHide VNBrowse VNXML VNUpdReverse VNRecreateRel |;
+
+
+sub VNPage {
+ my $self = shift;
+ my $id = shift;
+ my $page = shift || '';
+
+ my $r = $self->FormCheck(
+ { name => 'rev', required => 0, default => 0, template => 'int' },
+ { name => 'diff', required => 0, default => 0, template => 'int' },
+ );
+
+ my $v = $self->DBGetVN(
+ id => $id,
+ what => 'extended relations categories'.($r->{rev} ? ' changes' : ''),
+ $r->{rev} ? ( rev => $r->{rev} ) : ()
+ )->[0];
+ return $self->ResNotFound if !$v->{id};
+
+ $r->{diff} ||= $v->{prev} if $r->{rev};
+ my $c = $r->{diff} && $self->DBGetVN(id => $id, rev => $r->{diff}, what => 'extended changes relations categories')->[0];
+ $v->{next} = $self->DBGetHist(type => 'v', id => $id, next => $v->{cid}, showhid => 1)->[0]{id} if $r->{rev};
+
+ if($page eq 'rg' && $v->{rgraph}) {
+ open(my $F, '<', sprintf '%s/%02d/%d.cmap', $self->{mappath}, $v->{rgraph}%50, $v->{rgraph}) || die $!;
+ $v->{rmap} = join('', (<$F>));
+ close($F);
+ }
+
+ $self->ResAddTpl(vnpage => {
+ vote => $self->AuthInfo->{id} ? $self->DBGetVotes(uid => $self->AuthInfo->{id}, vid => $id)->[0] : {},
+ list => $self->AuthInfo->{id} ? $self->DBGetVNList(uid => $self->AuthInfo->{id}, vid => $id)->[0] : {},
+ rel => scalar $self->DBGetRelease(vid => $id, what => 'producers platforms'),
+ vn => $v,
+ prev => $c,
+ page => $page,
+ change => $r->{diff}||$r->{rev},
+ $page eq 'stats' ? (
+ lists => {
+ latest => scalar $self->DBGetVNList(vid => $id, results => 7),
+ graph => $self->DBVNListStats(vid => $id),
+ },
+ votes => {
+ latest => scalar $self->DBGetVotes(vid => $id, results => 10),
+ graph => $self->DBVoteStats(vid => $id),
+ },
+ ) : (),
+ });
+}
+
+
+sub VNEdit {
+ my $self = shift;
+ my $id = shift; # 0 = new
+
+ my $rev = $self->FormCheck({ name => 'rev', required => 0, default => 0, template => 'int' })->{rev};
+
+ my $v = $self->DBGetVN(id => $id, what => 'extended changes relations categories', $rev ? ( rev => $rev ) : ())->[0] if $id;
+ return $self->ResNotFound() if $id && !$v;
+
+ return $self->ResDenied if !$self->AuthCan('edit') || ($v->{locked} && !$self->AuthCan('lock'));
+
+ my %b4 = $id ? (
+ ( map { $_ => $v->{$_} } qw| title desc alias img_nsfw length l_wp l_cisv l_vnn | ),
+ relations => join('|||', map { $_->{relation}.','.$_->{id}.','.$_->{title} } @{$v->{relations}}),
+ categories => join(',', map { $_->[0].$_->[1] } sort { $a->[0] cmp $b->[0] } @{$v->{categories}}),
+ ) : ();
+
+ my $frm = {};
+ if($self->ReqMethod() eq 'POST') {
+ $frm = $self->FormCheck(
+ { name => 'title', required => 1, maxlength => 250 },
+ { name => 'alias', required => 0, maxlength => 500, default => '' },
+ { name => 'desc', required => 1, maxlength => 10240 },
+ { name => 'length', required => 0, enum => [ 0..($#$VNDB::VNLEN+1) ], default => 0 },
+ { name => 'l_wp', required => 0, default => '', maxlength => 150 },
+ { name => 'l_cisv', required => 0, default => 0, template => 'int' },
+ { name => 'l_vnn', required => 0, default => 0, template => 'int' },
+ { name => 'img_nsfw', required => 0 },
+ { name => 'categories', required => 0, default => '' },
+ { name => 'relations', required => 0, default => 0 },
+ { name => 'comm', required => 0, default => '' },
+ );
+ $frm->{img_nsfw} = $frm->{img_nsfw} ? 1 : 0;
+
+ return $self->ResRedirect('/v'.$id, 'post')
+ if $id && !$self->ReqParam('img') && 10 == scalar grep { $b4{$_} eq $frm->{$_} } keys %b4;
+
+ my $relations = [ map { /^([0-9]+),([0-9]+)/ && $2 != $id ? ( [ $1, $2 ] ) : () } split /\|\|\|/, $frm->{relations} ];
+ my $cat = [ map { [ substr($_,0,3), substr($_,3,1) ] } split /,/, $frm->{categories} ];
+
+ # upload image
+ my $imgid = '';
+ if($self->ReqParam('img')) {
+ my $tmp = sprintf '%s/00/tmp.%d.jpg', $self->{imgpath}, $$*int(rand(1000)+1);
+ $self->ReqSaveUpload('img', $tmp);
+
+ my $l;
+ open(my $T, '<:raw:bytes', $tmp) || die $1;
+ read $T, $l, 2;
+ seek $T, 0, 0;
+ my($x, $y) = jpegsize($T);
+ close($T);
+
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'nojpeg' ] : [ 'nojpeg' ]
+ if $l ne pack('H*', 'ffd8');
+ if(!$frm->{_err}) {
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'toolarge' ] : [ 'toolarge' ]
+ if -s $tmp > 51200; # 50 KB max.
+ $frm->{_err} = $frm->{_err} ? [ @{$frm->{_err}}, 'imgsize' ] : [ 'imgsize' ]
+ if $x > 256 || $y > 400; # 256x400 max
+ }
+
+ if($frm->{_err}) {
+ unlink $tmp;
+ } else {
+ $imgid = $self->DBIncId('covers_seq');
+ my $new = sprintf '%s/%02d/%d.jpg', $self->{imgpath}, $imgid%50, $imgid;
+ rename $tmp, $new or die $!;
+ chmod 0666, $new;
+ }
+ } elsif($id) {
+ $imgid = $v->{image};
+ }
+
+ my %args = (
+ ( map { $_ => $frm->{$_} } qw| title desc alias comm length l_wp l_cisv l_vnn img_nsfw| ),
+ image => $imgid,
+ relations => $relations,
+ categories => $cat,
+ );
+
+ if(!$frm->{_err}) {
+ my($oid, $cid) = ($id, 0);
+ $cid = $self->DBEditVN($id, %args) if $id; # edit
+ ($id, $cid) = $self->DBAddVN(%args) if !$id; # add
+
+ # update reverse relations and relation graph
+ if((!$oid && $#$relations >= 0) || ($oid && $frm->{relations} ne $b4{relations})) {
+ my %old = $oid ? (map { $_->{id} => $_->{relation} } @{$v->{relations}}) : ();
+ my %new = map { $_->[1] => $_->[0] } @$relations;
+ $self->VNRecreateRel($id, $self->VNUpdReverse(\%old, \%new, $id, $cid));
+ }
+
+ return $self->ResRedirect('/v'.$id.'?rev='.$cid, 'post');
+ }
+ }
+
+ if($id) {
+ $frm->{$_} ||= $b4{$_} for (keys %b4);
+ $frm->{comm} = sprintf 'Reverted to revision %d by %s.', $v->{cid}, $v->{username} if $v->{cid} != $v->{latest};
+ } else {
+ $frm->{categories} = 0;
+ }
+
+ $self->AddHid($frm);
+ $frm->{_hid} = {map{$_=>1} qw| info cat img |}
+ if !$frm->{_hid} && !$id;
+ $self->ResAddTpl(vnedit => {
+ form => $frm,
+ id => $id,
+ vn => $v,
+ });
+}
+
+
+sub VNDel {
+ my $self = shift;
+ my $id = shift;
+
+ my $v = $self->DBGetVN(id => $id)->[0];
+ return $self->ResNotFound if !$v;
+ return $self->ResDenied if !$self->AuthCan('del');
+ $self->DBDelVN($id);
+ return $self->ResRedirect('/v', 'perm');
+}
+
+
+sub VNLock {
+ my $self = shift;
+ my $id = shift;
+
+ my $v = $self->DBGetVN(id => $id)->[0];
+ return $self->ResNotFound() if !$v;
+ return $self->ResDenied if !$self->AuthCan('lock');
+ $self->DBLockItem('vn', $id, $v->{locked}?0:1);
+ $self->DBLockItem('releases', $_->{id}, $v->{locked}?0:1)
+ for (@{$self->DBGetRelease(vid => $id)});
+ return $self->ResRedirect('/v'.$id, 'perm');
+}
+
+
+sub VNHide {
+ my $self = shift;
+ my $id = shift;
+
+ my $v = $self->DBGetVN(id => $id, what => 'relations')->[0];
+ return $self->ResNotFound() if !$v;
+ return $self->ResDenied if !$self->AuthCan('del');
+ $self->DBHideVN($id, $v->{hidden}?0:1);
+ $self->VNRecreateRel($id, $self->VNUpdReverse({ map { $_->{id} => $_->{relation} } @{$v->{relations}} }, {}, $id, 0))
+ if @{$v->{relations}};
+ return $self->ResRedirect('/v'.$id, 'perm');
+}
+
+
+sub VNBrowse {
+ my $self = shift;
+ my $chr = shift;
+ $chr = 'all' if !defined $chr;
+
+ my $f = $self->FormCheck(
+ { name => 's', required => 0, default => 'title', enum => [ qw|title released votes| ] },
+ { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
+ { name => 'i', required => 0, default => '' },
+ { name => 'e', required => 0, default => '' },
+ { name => 'l', required => 0, default => '' },
+ { name => 'q', required => 0},
+ { name => 'p', required => 0, template => 'int', default => 1},
+ );
+
+ my($r, $np) = $chr ne 'cat' || $f->{e} || $f->{i} || $f->{l} ? ($self->DBGetVN(
+ $chr =~ /^[a-z0]$/ ? (
+ char => $chr ) : (),
+ $chr eq 'search' && $f->{q} ? (
+ search => $f->{q} ) : (),
+ page => $f->{p},
+ $chr eq 'cat' ? (
+ cati => [ split /,/, $f->{i} ],
+ cate => [ split /,/, $f->{e} ],
+ lang => [ grep { $VNDB::LANG->{$_} } split /,/, $f->{l} ],
+ ) : (),
+ results => 50,
+ order => {title => 'vr.title', released => 'v.c_released', votes => 'v.c_votes'
+ }->{$f->{s}}.{a=>' ASC',d=>' DESC'}->{$f->{o}},
+ )) : ([], 0);
+
+ $self->ResRedirect('/v'.$r->[0]{id}, 'temp')
+ if $chr eq 'search' && $#$r == 0;
+
+ $self->ResAddTpl(vnbrowse => {
+ vn => $r,
+ npage => $np,
+ page => $f->{p},
+ chr => $chr,
+ $chr eq 'cat' ? (
+ incl => $f->{i},
+ excl => $f->{e},
+ cat => $self->DBCategoryCount,
+ lang => $self->DBLanguageCount,
+ slang => $f->{l},
+ ) : (),
+ order => [ $f->{s}, $f->{o} ],
+ },
+ searchquery => $f->{q});
+}
+
+
+sub VNXML {
+ my $self = shift;
+
+ my $q = $self->FormCheck(
+ { name => 'q', required => 0, maxlength => 100 }
+ )->{q};
+
+ my $r = [];
+ if($q) {
+ ($r,undef) = $self->DBGetVN(results => 10,
+ $q =~ /^v([0-9]+)$/ ? (id => $1) : (search => $q));
+ }
+
+ my $x = $self->ResStartXML;
+ $x->startTag('vn', results => $#$r+1, query => $q);
+ for (@$r) {
+ $x->startTag('item');
+ $x->dataElement(id => $_->{id});
+ $x->dataElement(title => $_->{title});
+ $x->endTag('item');
+ }
+ $x->endTag('vn');
+}
+
+
+
+sub jpegsize {
+ my $stream = shift;
+
+ my $MARKER = "\xFF"; # Section marker.
+
+ my $SIZE_FIRST = 0xC0; # Range of segment identifier codes
+ my $SIZE_LAST = 0xC3; # that hold size info.
+
+ my ($x, $y, $id) = (undef, undef, "could not determine JPEG size");
+
+ my ($marker, $code, $length, $data);
+ my $segheader;
+
+ seek $stream, 2, 0;
+ while (1) {
+ $length = 4;
+ read $stream, $segheader, $length;
+
+ ($marker, $code, $length) = unpack("a a n", $segheader);
+
+ if ($marker ne $MARKER) {
+ $id = "JPEG marker not found";
+ last;
+ } elsif((ord($code) >= $SIZE_FIRST) && (ord($code) <= $SIZE_LAST)) {
+ $length = 5;
+ read $stream, $data, $length;
+ ($y, $x) = unpack("xnn", $data);
+ $id = 'JPG';
+ last;
+ } else {
+ seek $stream, ($length - 2), 1;
+ }
+ }
+ return ($x, $y, $id);
+}
+
+
+# Update reverse relations
+sub VNUpdReverse { # old, new, id, cid
+ my($self, $old, $new, $id, $cid) = @_;
+ my %upd;
+ for (keys %$old, keys %$new) {
+ if(exists $$old{$_} and !exists $$new{$_}) {
+ $upd{$_} = -1;
+ } elsif((!exists $$old{$_} and exists $$new{$_}) || ($$old{$_} != $$new{$_})) {
+ $upd{$_} = $$new{$_};
+ if($VNDB::VRELW->{$upd{$_}}) { $upd{$_}-- }
+ elsif($VNDB::VRELW->{$upd{$_}+1}) { $upd{$_}++ }
+ }
+ }
+
+ for my $i (keys %upd) {
+ my $r = $self->DBGetVN(id => $i, what => 'extended relations categories')->[0];
+ my @newrel;
+ $_->{id} != $id && push @newrel, [ $_->{relation}, $_->{id} ]
+ for (@{$r->{relations}});
+ push @newrel, [ $upd{$i}, $id ] if $upd{$i} != -1;
+ $self->DBEditVN($i,
+ relations => \@newrel,
+ comm => 'Reverse relation update caused by revision '.$cid.' of v'.$id,
+ causedby => $cid,
+ uid => 1, # Multi - hardcoded
+ ( map { $_ => $r->{$_} } qw| title desc alias categories img_nsfw length l_wp l_cisv l_vnn image | )
+ );
+ }
+
+ return keys %upd;
+}
+
+
+sub VNRecreateRel { # @ids
+ my($s, @id) = @_;
+ $s->DBCommit; # creates deadlock otherwise
+ my $c = sprintf "%s %s", $s->{grapher}, join(' ', @id);
+ my $o = `$c`;
+ chomp $o;
+ warn "$$s{grapher}: $o\n" if $o;
+}
+
+
+
+1;
+
+
diff --git a/lib/VNDB/VNLists.pm b/lib/VNDB/VNLists.pm
new file mode 100644
index 00000000..c0f1ac1d
--- /dev/null
+++ b/lib/VNDB/VNLists.pm
@@ -0,0 +1,96 @@
+
+package VNDB::VNLists;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+@EXPORT = qw| VNListMod VNMyList |;
+
+
+sub VNListMod {
+ my $self = shift;
+ my $vid = shift;
+
+ my $uid = $self->AuthInfo()->{id};
+ return $self->ResDenied() if !$uid;
+
+ my $f = $self->FormCheck(
+ { name => 's', required => 1, enum => [ -1..$#$VNDB::LSTAT ] },
+ { name => 'c', required => 0, default => '', maxlength => 500 },
+ );
+ return $self->ResNotFound if $f->{_err};
+
+ if($f->{s} == -1) {
+ $self->DBDelVNList($uid, $vid);
+ } elsif($self->DBGetVNList(uid => $uid, vid => $vid)->[0]{vid}) {
+ $self->DBEditVNList(uid => $uid, status => $f->{s}, vid => [ $vid ],
+ $f->{s} == 6 ? ( comments => $f->{c} ) : ());
+ } else {
+ $self->DBAddVNList($uid, $vid, $f->{s}, $f->{c});
+ }
+
+ $self->ResRedirect('/v'.$vid, 'temp');
+}
+
+
+sub VNMyList {
+ my $self = shift;
+ my $user = shift;
+
+ my $u = $self->DBGetUser(uid => $user)->[0];
+ return $self->ResNotFound if !$user || !$u || (($self->AuthInfo->{id}||0) != $user && !($u->{flags} & $VNDB::UFLAGS->{list}));
+
+ my $f = $self->FormCheck(
+ { name => 's', required => 0, default => 'title', enum => [ qw|title date| ] },
+ { name => 'o', required => 0, default => 'a', enum => [ 'a','d' ] },
+ { name => 'p', required => 0, template => 'int', default => 1 },
+ { name => 't', required => 0, enum => [ -1..$#$VNDB::LSTAT ], default => -1 },
+ );
+
+ if($self->ReqMethod eq 'POST') {
+ my $frm = $self->FormCheck(
+ { name => 'vnlistchange', required => 1, enum => [ -2..$#$VNDB::LSTAT ] },
+ { name => 'comments', required => 0, default => '', maxlength => 500 },
+ { name => 'sel', required => 1, multi => 1 },
+ );
+ if(!$frm->{_err}) {
+ my @change = map { /^[0-9]+$/ ? $_ : () } @{$frm->{sel}};
+ $self->DBDelVNList($user, @change) if @change && $frm->{vnlistchange} eq '-1';
+ $self->DBEditVNList(
+ uid => $user,
+ vid => \@change,
+ $frm->{vnlistchange} eq '-2' ? (
+ comments => $frm->{comments}
+ ) : (
+ status => $frm->{vnlistchange}
+ ),
+ ) if @change && $frm->{vnlistchange} ne '-1';
+ }
+ }
+
+ my $order = $f->{s} . ($f->{o} eq 'a' ? ' ASC' : ' DESC');
+ my($list, $np) = $self->DBGetVNList(
+ uid => $u->{id},
+ order => $order,
+ results => 50,
+ page => $f->{p},
+ $f->{t} >= 0 ? (
+ status => $f->{t} ) : ()
+ );
+
+ $self->ResAddTpl(vnlist => {
+ npage => $np,
+ page => $f->{p},
+ list => $list,
+ order => [ $f->{s}, $f->{o} ],
+ user => $u,
+ status => $f->{t},
+ });
+}
+
+
+
+1;
diff --git a/lib/VNDB/Votes.pm b/lib/VNDB/Votes.pm
new file mode 100644
index 00000000..a6089b3d
--- /dev/null
+++ b/lib/VNDB/Votes.pm
@@ -0,0 +1,61 @@
+
+package VNDB::Votes;
+
+use strict;
+use warnings;
+use Exporter 'import';
+
+use vars ('$VERSION', '@EXPORT');
+$VERSION = $VNDB::VERSION;
+@EXPORT = qw| VNVote VNVotes |;
+
+
+sub VNVote {
+ my $self = shift;
+ my $id = shift;
+
+ my $uid = $self->AuthInfo()->{id};
+ return $self->ResDenied() if !$uid;
+
+ my $f = $self->FormCheck(
+ { name => 'v', required => 0, default => 0, enum => [ '-1','1'..'10'] }
+ );
+ return $self->ResNotFound() if !$f->{v};
+
+
+ $self->DBDelVote($uid, $id) if $f->{v} == -1 || $self->DBGetVotes(uid => $uid, vid => $id)->[0]{vid};
+ $self->DBAddVote($id, $uid, $f->{v}) if $f->{v} > 0;
+
+ $self->ResRedirect('/v'.$id, 'temp');
+}
+
+
+sub VNVotes {
+ my $self = shift;
+ my $user = shift;
+
+ my $u = $self->DBGetUser(uid => $user)->[0];
+ return $self->ResNotFound if !$user || !$u || (($self->AuthInfo->{id}||0) != $user && !($u->{flags} & $VNDB::UFLAGS->{votes}));
+
+ my $f = $self->FormCheck(
+ { name => 's', required => 0, default => 'date', enum => [ qw|date title vote| ] },
+ { name => 'o', required => 0, default => 'd', enum => [ 'a','d' ] },
+ { name => 'p', required => 0, default => 1, template => 'int' },
+ );
+
+ my $order = $f->{s} . ($f->{o} eq 'a' ? ' ASC' : ' DESC');
+ my ($votes, $np) = $self->DBGetVotes(
+ uid => $u->{id},
+ order => $order,
+ results => 50,
+ page => $f->{p}
+ );
+
+ $self->ResAddTpl(myvotes => {
+ user => $u,
+ votes => $votes,
+ page => $f->{p},
+ npage => $np,
+ order => [ $f->{s}, $f->{o} ],
+ });
+}
diff --git a/lib/global.pl b/lib/global.pl
new file mode 100644
index 00000000..aea66f75
--- /dev/null
+++ b/lib/global.pl
@@ -0,0 +1,569 @@
+package VNDB;
+
+our $PLAT = {
+ win => 'Windows',
+ lin => 'Linux',
+ mac => 'Mac OS',
+ dvd => 'DVD Player',
+ gba => 'Game Boy Advanced',
+ nds => 'Nintendo DS',
+ psp => 'Playstation Portable',
+ ps => 'Playstation',
+ ps2 => 'Playstation 2',
+ dc => 'Dreamcast',
+ sfc => 'Super Nintendo',
+ wii => 'Nintendo Wii',
+ oth => 'Other'
+};
+
+# NOTE: don't forget to update dyna.js
+our $MED = {
+ cd => 'CD',
+ dvd => 'DVD',
+ gdr => 'GD-ROM',
+ blr => 'Blu-Ray disk',
+ in => 'Internet download',
+ pa => 'Patch',
+ otc => 'Other (console)',
+};
+
+our $PROT = {
+ co => 'Company',
+ in => 'Individual',
+ ng => 'Amateur group',
+};
+
+our $RTYP = [
+ 'Complete',
+ 'Partial',
+ 'Trial'
+];
+
+# Yes, this is the category list. No, changing something here may
+# not change it on the entire site - many things are still hardcoded
+our $CAT = {
+ g => [ 'Gameplay', {
+ aa => 'Visual Novel', # 0..1
+ ab => 'Adventure', # 0..1
+ ac => 'Action',
+ rp => 'RPG',
+ st => 'Strategy',
+ si => 'Simulation',
+ } ],
+ p => [ 'Plot', {
+ li => 'Linear', # 0..1
+ br => 'Branching', # 0..1
+ } ],
+ e => [ 'Elements', {
+ ac => 'Action',
+ co => 'Comedy',
+ dr => 'Drama',
+ fa => 'Fantasy',
+ ho => 'Horror',
+ my => 'Mystery',
+ ro => 'Romance',
+ sf => 'SciFi',
+ sj => 'Shoujo Ai',
+ sn => 'Shounen Ai',
+ } ],
+ t => [ 'Time', {
+ fu => 'Future',
+ pa => 'Past',
+ pr => 'Present',
+ } ],
+ l => [ 'Place', {
+ ea => 'Earth',
+ fa => 'Fantasy World',
+ sp => 'Space',
+ } ],
+ s => [ 'Sexual content', {
+ aa => 'Sexual content',
+ be => 'Bestiality',
+ in => 'Incest',
+ lo => 'Lolicon',
+ sh => 'Shotacon',
+ ya => 'Yaoi',
+ yu => 'Yuri',
+ ra => 'Rape',
+ } ],
+};
+
+
+our $LSTAT = [
+ 'Wishlist',
+ 'Blacklist',
+ 'Playing',
+ 'Finished',
+ 'Stalled',
+ 'Dropped',
+ 'Other', # XXX: hardcoded at 6
+];
+
+our $VREL = [
+ 'Sequel',
+ 'Prequel', # 1
+ 'Same setting',
+ 'Alternative setting',
+ 'Alternative version',
+ 'Same characters',
+ 'Side story',
+ 'Parent story',# 7
+ 'Summary',
+ 'Full story', # 9
+ 'Other',
+];
+# these reverse relations need a [relation]-1
+our $VRELW = {map{$_=>1}qw| 1 7 9 |};
+
+
+# users.flags
+our $UFLAGS = {
+ votes => 1,
+ list => 4,
+ nsfw => 8,
+};
+
+
+our $VNLEN = [
+ [ 'Unkown', '', '' ],
+ [ 'Very short', '< 2 hours', 'OMGWTFOTL, A Dream of Summer' ],
+ [ 'Short', '2 - 10 hours', 'Narcissu, Planetarian' ],
+ [ 'Medium', '10 - 30 hours', 'Kana: Little Sister' ],
+ [ 'Long', '30 - 50 hours', 'Tsukihime' ],
+ [ 'Very long', '> 50 hours', 'Clannad' ],
+];
+
+
+our $VRAGES = {
+ -1 => 'Unknown',
+ 0 => 'All ages',
+ map { $_ => $_.'+' } 6..18
+};
+
+
+
+
+
+
+
+our $LANG = {
+# 'aa' => q|Afar|,
+# 'ab' => q|Abkhazian|,
+# 'ace' => q|Achinese|,
+# 'ach' => q|Acoli|,
+# 'ada' => q|Adangme|,
+# 'ady' => q|Adyghe|,
+# 'ae' => q|Avestan|,
+# 'af' => q|Afrikaans|,
+# 'afh' => q|Afrihili|,
+# 'ak' => q|Akan|,
+# 'akk' => q|Akkadian|,
+# 'ale' => q|Aleut|,
+# 'alg' => q|Algonquian languages|,
+# 'am' => q|Amharic|,
+# 'an' => q|Aragonese|,
+# 'apa' => q|Apache languages|,
+# 'ar' => q|Arabic|,
+# 'arc' => q|Aramaic|,
+# 'arn' => q|Araucanian|,
+# 'arp' => q|Arapaho|,
+# 'arw' => q|Arawak|,
+# 'as' => q|Assamese|,
+# 'ast' => q|Asturian|,
+# 'ath' => q|Athapascan languages|,
+# 'aus' => q|Australian languages|,
+# 'av' => q|Avaric|,
+# 'awa' => q|Awadhi|,
+# 'ay' => q|Aymara|,
+# 'az' => q|Azerbaijani|,
+# 'ba' => q|Bashkir|,
+# 'bad' => q|Banda|,
+# 'bai' => q|Bamileke languages|,
+# 'bal' => q|Baluchi|,
+# 'ban' => q|Balinese|,
+# 'bas' => q|Basa|,
+# 'be' => q|Belarusian|,
+# 'bej' => q|Beja|,
+# 'bem' => q|Bemba|,
+# 'bg' => q|Bulgarian|,
+# 'bh' => q|Bihari|,
+# 'bho' => q|Bhojpuri|,
+# 'bi' => q|Bislama|,
+# 'bik' => q|Bikol|,
+# 'bin' => q|Bini|,
+# 'bla' => q|Siksika|,
+# 'bm' => q|Bambara|,
+# 'bn' => q|Bengali|,
+# 'bo' => q|Tibetan|,
+# 'br' => q|Breton|,
+# 'bra' => q|Braj|,
+# 'bs' => q|Bosnian|,
+# 'btk' => q|Batak (Indonesia)|,
+# 'bua' => q|Buriat|,
+# 'bug' => q|Buginese|,
+# 'ca' => q|Catalan|,
+# 'cad' => q|Caddo|,
+# 'car' => q|Carib|,
+# 'ce' => q|Chechen|,
+# 'ceb' => q|Cebuano|,
+# 'ch' => q|Chamorro|,
+# 'chb' => q|Chibcha|,
+# 'chg' => q|Chagatai|,
+# 'chk' => q|Chuukese|,
+# 'chm' => q|Mari|,
+# 'chn' => q|Chinook Jargon|,
+# 'cho' => q|Choctaw|,
+# 'chp' => q|Chipewyan|,
+# 'chr' => q|Cherokee|,
+# 'chy' => q|Cheyenne|,
+# 'cmc' => q|Chamic languages|,
+# 'co' => q|Corsican|,
+# 'cop' => q|Coptic|,
+# 'cr' => q|Cree|,
+# 'crh' => q|Crimean Turkish|,
+ 'cs' => q|Czech|,
+# 'csb' => q|Kashubian|,
+# 'cu' => q|Church Slavic|,
+# 'cv' => q|Chuvash|,
+# 'cy' => q|Welsh|,
+ 'da' => q|Danish|,
+# 'dak' => q|Dakota|,
+# 'dar' => q|Dargwa|,
+# 'day' => q|Dayak|,
+ 'de' => q|German|,
+# 'del' => q|Delaware|,
+# 'dgr' => q|Dogrib|,
+# 'din' => q|Dinka|,
+# 'doi' => q|Dogri|,
+# 'dua' => q|Duala|,
+# 'dv' => q|Divehi|,
+# 'dyu' => q|Dyula|,
+# 'dz' => q|Dzongkha|,
+# 'ee' => q|Ewe|,
+# 'efi' => q|Efik|,
+# 'eka' => q|Ekajuk|,
+# 'el' => q|Modern Greek|,
+# 'elx' => q|Elamite|,
+ 'en' => q|English|,
+# 'eo' => q|Esperanto|,
+ 'es' => q|Spanish|,
+# 'et' => q|Estonian|,
+# 'eu' => q|Basque|,
+# 'ewo' => q|Ewondo|,
+# 'fa' => q|Persian|,
+# 'fan' => q|Fang|,
+# 'fat' => q|Fanti|,
+# 'ff' => q|Fulah|,
+ 'fi' => q|Finnish|,
+# 'fj' => q|Fijian|,
+# 'fo' => q|Faroese|,
+# 'fon' => q|Fon|,
+ 'fr' => q|French|,
+# 'fur' => q|Friulian|,
+# 'fy' => q|Frisian|,
+ 'ga' => q|Irish|,
+# 'gaa' => q|Ga|,
+# 'gay' => q|Gayo|,
+# 'gba' => q|Gbaya|,
+# 'gd' => q|Scots Gaelic|,
+# 'gez' => q|Geez|,
+# 'gil' => q|Gilbertese|,
+# 'gl' => q|Gallegan|,
+# 'gn' => q|Guarani|,
+# 'gon' => q|Gondi|,
+# 'gor' => q|Gorontalo|,
+# 'got' => q|Gothic|,
+# 'grb' => q|Grebo|,
+# 'grc' => q|Ancient Greek|,
+# 'gu' => q|Gujarati|,
+# 'gv' => q|Manx|,
+# 'gwi' => q|Gwich'in|,
+# 'ha' => q|Hausa|,
+# 'hai' => q|Haida|,
+# 'haw' => q|Hawaiian|,
+# 'he' => q|Hebrew|,
+# 'hi' => q|Hindi|,
+# 'hil' => q|Hiligaynon|,
+# 'him' => q|Himachali|,
+# 'hit' => q|Hittite|,
+# 'hmn' => q|Hmong|,
+# 'ho' => q|Hiri Motu|,
+# 'hr' => q|Croatian|,
+# 'ht' => q|Haitian|,
+# 'hu' => q|Hungarian|,
+# 'hup' => q|Hupa|,
+# 'hy' => q|Armenian|,
+# 'hz' => q|Herero|,
+# 'i-ami' => q|Ami|,
+# 'i-bnn' => q|Bunun|,
+# 'i-klingon' => q|Klingon|,
+# 'i-mingo' => q|Mingo|,
+# 'i-pwn' => q|Paiwan|,
+# 'i-tao' => q|Tao|,
+# 'i-tay' => q|Tayal|,
+# 'i-tsu' => q|Tsou|,
+# 'iba' => q|Iban|,
+# 'id' => q|Indonesian|,
+# 'ie' => q|Interlingue|,
+# 'ig' => q|Igbo|,
+# 'ii' => q|Sichuan Yi|,
+# 'ijo' => q|Ijo|,
+# 'ik' => q|Inupiaq|,
+# 'ilo' => q|Iloko|,
+# 'inh' => q|Ingush|,
+# 'io' => q|Ido|,
+# 'iro' => q|Iroquoian languages|,
+# 'is' => q|Icelandic|,
+ 'it' => q|Italian|,
+# 'iu' => q|Inuktitut|,
+ 'ja' => q|Japanese|,
+# 'jpr' => q|Judeo-Persian|,
+# 'jrb' => q|Judeo-Arabic|,
+# 'jv' => q|Javanese|,
+# 'ka' => q|Georgian|,
+# 'kaa' => q|Kara-Kalpak|,
+# 'kab' => q|Kabyle|,
+# 'kac' => q|Kachin|,
+# 'kam' => q|Kamba|,
+# 'kar' => q|Karen|,
+# 'kaw' => q|Kawi|,
+# 'kbd' => q|Kabardian|,
+# 'kg' => q|Kongo|,
+# 'kha' => q|Khasi|,
+# 'kho' => q|Khotanese|,
+# 'ki' => q|Kikuyu|,
+# 'kj' => q|Kuanyama|,
+# 'kk' => q|Kazakh|,
+# 'kl' => q|Kalaallisut|,
+# 'km' => q|Khmer|,
+# 'kmb' => q|Kimbundu|,
+# 'kn' => q|Kannada|,
+ 'ko' => q|Korean|,
+# 'kok' => q|Konkani|,
+# 'kos' => q|Kosraean|,
+# 'kpe' => q|Kpelle|,
+# 'kr' => q|Kanuri|,
+# 'krc' => q|Karachay-Balkar|,
+# 'kro' => q|Kru|,
+# 'kru' => q|Kurukh|,
+# 'ks' => q|Kashmiri|,
+# 'ku' => q|Kurdish|,
+# 'kum' => q|Kumyk|,
+# 'kut' => q|Kutenai|,
+# 'kv' => q|Komi|,
+# 'kw' => q|Cornish|,
+# 'ky' => q|Kirghiz|,
+# 'la' => q|Latin|,
+# 'lad' => q|Ladino|,
+# 'lah' => q|Lahnda|,
+# 'lam' => q|Lamba|,
+# '#lb' => q|Letzeburgesch|,
+# 'lez' => q|Lezghian|,
+# 'lg' => q|Ganda|,
+# 'li' => q|Limburgish|,
+# 'ln' => q|Lingala|,
+# 'lo' => q|Lao|,
+# 'lol' => q|Mongo|,
+# 'loz' => q|Lozi|,
+# 'lt' => q|Lithuanian|,
+# 'lu' => q|Luba-Katanga|,
+# 'lua' => q|Luba-Lulua|,
+# 'lui' => q|Luiseno|,
+# 'lun' => q|Lunda|,
+# 'luo' => q|Luo (Kenya and Tanzania)|,
+# 'lus' => q|Lushai|,
+# 'lv' => q|Latvian|,
+# 'mad' => q|Madurese|,
+# 'mag' => q|Magahi|,
+# 'mai' => q|Maithili|,
+# 'mak' => q|Makasar|,
+# 'man' => q|Mandingo|,
+# 'mas' => q|Masai|,
+# 'mdf' => q|Moksha|,
+# 'mdr' => q|Mandar|,
+# 'men' => q|Mende|,
+# 'mg' => q|Malagasy|,
+# 'mh' => q|Marshall|,
+# 'mi' => q|Maori|,
+# 'mic' => q|Micmac|,
+# 'min' => q|Minangkabau|,
+# 'mk' => q|Macedonian|,
+# 'ml' => q|Malayalam|,
+# 'mn' => q|Mongolian|,
+# 'mnc' => q|Manchu|,
+# 'mni' => q|Manipuri|,
+# 'mno' => q|Manobo languages|,
+# 'mo' => q|Moldavian|,
+# 'moh' => q|Mohawk|,
+# 'mos' => q|Mossi|,
+# 'mr' => q|Marathi|,
+# 'ms' => q|Malay|,
+# 'mt' => q|Maltese|,
+# 'mul' => q|Multiple languages|,
+# 'mun' => q|Munda languages|,
+# 'mus' => q|Creek|,
+# 'mwr' => q|Marwari|,
+# 'my' => q|Burmese|,
+# 'myn' => q|Mayan languages|,
+# 'myv' => q|Erzya|,
+# 'na' => q|Nauru|,
+# 'nah' => q|Nahuatl|,
+# 'nap' => q|Neapolitan|,
+# 'nb' => q|Norwegian Bokmal|,
+# 'nd' => q|North Ndebele|,
+# 'ne' => q|Nepali|,
+# 'new' => q|Newari|,
+# 'ng' => q|Ndonga|,
+# 'nia' => q|Nias|,
+# 'niu' => q|Niuean|,
+ 'nl' => q|Dutch|,
+ 'no' => q|Norwegian|,
+# 'nog' => q|Nogai|,
+# 'non' => q|Old Norse|,
+# 'nr' => q|South Ndebele|,
+# 'nso' => q|Northern Sotho|,
+# 'nub' => q|Nubian languages|,
+# 'nv' => q|Navajo|,
+# 'ny' => q|Chichewa|,
+# 'nym' => q|Nyamwezi|,
+# 'nyn' => q|Nyankole|,
+# 'nyo' => q|Nyoro|,
+# 'nzi' => q|Nzima|,
+# 'oj' => q|Ojibwa|,
+# 'om' => q|Oromo|,
+# 'or' => q|Oriya|,
+# 'os' => q|Ossetian; Ossetic|,
+# 'osa' => q|Osage|,
+# 'oto' => q|Otomian languages|,
+# 'pa' => q|Panjabi|,
+# 'pag' => q|Pangasinan|,
+# 'pal' => q|Pahlavi|,
+# 'pam' => q|Pampanga|,
+# 'pap' => q|Papiamento|,
+# 'pau' => q|Palauan|,
+# 'phn' => q|Phoenician|,
+# 'pi' => q|Pali|,
+ 'pl' => q|Polish|,
+# 'pon' => q|Pohnpeian|,
+# 'pra' => q|Prakrit languages|,
+# 'ps' => q|Pushto|,
+ 'pt' => q|Portuguese|,
+# 'pt-br' => q|Brazilian Portuguese|,
+# 'pt-pt' => q|Portugal Portuguese|,
+# 'qu' => q|Quechua|,
+# 'raj' => q|Rajasthani|,
+# 'rap' => q|Rapanui|,
+# 'rar' => q|Rarotongan|,
+# 'rm' => q|Raeto-Romance|,
+# 'rn' => q|Rundi|,
+# 'ro' => q|Romanian|,
+# 'rom' => q|Romany|,
+ 'ru' => q|Russian|,
+# 'rw' => q|Kinyarwanda|,
+# 'sa' => q|Sanskrit|,
+# 'sad' => q|Sandawe|,
+# 'sah' => q|Yakut|,
+# 'sal' => q|Salishan languages|,
+# 'sam' => q|Samaritan Aramaic|,
+# 'sas' => q|Sasak|,
+# 'sat' => q|Santali|,
+# 'sc' => q|Sardinian|,
+# 'sco' => q|Scots|,
+# 'sd' => q|Sindhi|,
+# 'se' => q|Northern Sami|,
+# 'sel' => q|Selkup|,
+# 'sg' => q|Sango|,
+# 'shn' => q|Shan|,
+# 'si' => q|Sinhalese|,
+# 'sid' => q|Sidamo|,
+# 'sio' => q|Siouan languages|,
+# 'sk' => q|Slovak|,
+# 'sl' => q|Slovenian|,
+# 'sm' => q|Samoan|,
+# 'sma' => q|Southern Sami|,
+# 'smj' => q|Lule Sami|,
+# 'smn' => q|Inari Sami|,
+# 'sms' => q|Skolt Sami|,
+# 'sn' => q|Shona|,
+# 'snk' => q|Soninke|,
+# 'so' => q|Somali|,
+# 'sog' => q|Sogdian|,
+# 'son' => q|Songhai|,
+# 'sq' => q|Albanian|,
+# 'sr' => q|Serbian|,
+# 'srr' => q|Serer|,
+# 'ss' => q|Swati|,
+# 'st' => q|Southern Sotho|,
+# 'su' => q|Sundanese|,
+# 'suk' => q|Sukuma|,
+# 'sus' => q|Susu|,
+# 'sux' => q|Sumerian|,
+ 'sv' => q|Swedish|,
+# 'sw' => q|Swahili|,
+# 'syr' => q|Syriac|,
+# 'ta' => q|Tamil|,
+# 'te' => q|Telugu|,
+# 'tem' => q|Timne|,
+# 'ter' => q|Tereno|,
+# 'tet' => q|Tetum|,
+# 'tg' => q|Tajik|,
+# 'th' => q|Thai|,
+# 'ti' => q|Tigrinya|,
+# 'tig' => q|Tigre|,
+# 'tiv' => q|Tiv|,
+# 'tk' => q|Turkmen|,
+# 'tkl' => q|Tokelau|,
+# 'tl' => q|Tagalog|,
+# 'tli' => q|Tlingit|,
+# 'tmh' => q|Tamashek|,
+# 'tn' => q|Tswana|,
+# 'to' => q|Tonga (Tonga Islands)|,
+# 'tog' => q|Tonga (Nyasa)|,
+# 'tpi' => q|Tok Pisin|,
+ 'tr' => q|Turkish|,
+# 'ts' => q|Tsonga|,
+# 'tsi' => q|Tsimshian|,
+# 'tt' => q|Tatar|,
+# 'tum' => q|Tumbuka|,
+# 'tup' => q|Tupi languages|,
+# 'tvl' => q|Tuvalu|,
+# 'tw' => q|Twi|,
+# 'ty' => q|Tahitian|,
+# 'tyv' => q|Tuvinian|,
+# 'udm' => q|Udmurt|,
+# 'ug' => q|Uighur|,
+# 'uga' => q|Ugaritic|,
+# 'uk' => q|Ukrainian|,
+# 'umb' => q|Umbundu|,
+# 'ur' => q|Urdu|,
+# 'uz' => q|Uzbek|,
+# 'vai' => q|Vai|,
+# 've' => q|Venda|,
+# 'vi' => q|Vietnamese|,
+# 'vo' => q|Volapuk|,
+# 'vot' => q|Votic|,
+# 'wa' => q|Walloon|,
+# 'wak' => q|Wakashan languages|,
+# 'wal' => q|Walamo|,
+# 'war' => q|Waray|,
+# 'was' => q|Washo|,
+# 'wen' => q|Sorbian languages|,
+# 'wo' => q|Wolof|,
+# 'xal' => q|Kalmyk|,
+# 'xh' => q|Xhosa|,
+# 'yao' => q|Yao|,
+# 'yap' => q|Yapese|,
+# 'yi' => q|Yiddish|,
+# 'yo' => q|Yoruba|,
+# 'ypk' => q|Yupik languages|,
+# 'za' => q|Zhuang|,
+# 'zap' => q|Zapotec|,
+# 'zen' => q|Zenaga|,
+ 'zh' => q|Chinese|,
+# 'znd' => q|Zande|,
+# 'zu' => q|Zulu|,
+# 'zun' => q|Zuni|,
+};
+
+1;
+
diff --git a/static/files/def.js b/static/files/def.js
new file mode 100644
index 00000000..fcb98421
--- /dev/null
+++ b/static/files/def.js
@@ -0,0 +1,239 @@
+
+
+/* G L O B A L S T U F F */
+
+function x(y){return document.getElementById(y)}
+function cl(o,f){if(x(o))x(o).onclick=f}
+function DOMLoad(y){var d=0;var f=function(){if(d++)return;y()};
+if(document.addEventListener)document.addEventListener("DOMCont"
++"entLoaded",f,false);document.write("<script id=_ie defer src="
++"javascript:void(0)><\/script>");document.getElementById('_ie')
+.onreadystatechange=function(){if(this.readyState=="complete")f()
+};if(/WebKit/i.test(navigator.userAgent))var t=setInterval(
+function(){if(/loaded|complete/.test(document.readyState)){
+clearInterval(t);f()}},10);window.onload=f;}
+
+
+
+
+/* F O R M S U B S */
+
+var formsubs = [];
+function formhid() {
+ var i;
+ var j;
+ var l = document.forms[1].getElementsByTagName('a');
+ for(i=0; i<l.length; i++)
+ if(l[i].className.indexOf('s_') != -1) {
+ formsubs[ l[i].className.substr(l[i].className.indexOf('s_')+2) ] = 0;
+ l[i].onclick = function() {
+ formtoggle(this.className.substr(this.className.indexOf('s_')+2));
+ return false;
+ };
+ }
+
+ if(x('_hid') && x('_hid').value.length > 1) {
+ l = x('_hid').value.split(/,/);
+ for(i in formsubs) {
+ var inz=0;
+ for(j=0; j<l.length; j++)
+ if(l[j] == i)
+ inz = 1;
+ if(!inz)
+ formsubs[i] = !formsubs[i];
+ }
+ }
+}
+function formtoggle(n) {
+ formsubs[n] = !formsubs[n];
+ var i;
+ var l = document.forms[1].getElementsByTagName('a');
+ for(i=0; i<l.length; i++)
+ if(l[i].className.indexOf('s_'+n) != -1)
+ l[i].innerHTML = (formsubs[n] ? '&#9656;' : '&#9662;') + l[i].innerHTML.substr(1);
+
+ l = document.forms[1].getElementsByTagName('li');
+ for(i=0; i<l.length; i++)
+ if(l[i].className.indexOf('sf_'+n) != -1) {
+ if(formsubs[n])
+ l[i].className += ' formhid';
+ else
+ l[i].className = l[i].className.replace(/formhid/g, '');
+ }
+
+ if(x('_hid')) {
+ l = [];
+ for(i in formsubs)
+ if(!formsubs[i])
+ l[l.length] = i;
+ x('_hid').value = l.toString();
+ }
+}
+
+
+
+
+/* D R O P D O W N M E N U S */
+
+
+var ddx;var ddy;var dds=null;
+function dropDown(e) {
+ e = e || window.event;
+ var tg = e.target || e.srcElement; // get target element
+ if(tg.nodeType == 3)
+ tg = tg.parentNode;
+ if(!dds && (tg.nodeName.toLowerCase() != 'a' || !tg.rel || tg.className.indexOf('dropdown') < 0))
+ return;
+ var mouseX = e.pageX || (e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft);
+ var mouseY = e.pageY || (e.clientY + document.body.scrollTop + document.documentElement.scrollTop);
+ if(!dds) {
+ var obj = x(tg.rel);
+ ddx = mouseX-20;
+ ddy = mouseY+10;
+ obj.style.left = ddx+'px';
+ obj.style.top = ddy+'px';
+ dds = tg;
+ }
+ if(dds) {
+ var obj = x(dds.rel);
+ if((mouseX < ddx || mouseX > ddx+obj.offsetWidth || mouseY < ddy-20 || mouseY > ddy + obj.offsetHeight)
+ || (mouseY < ddy && tg.nodeName.toLowerCase() == 'a' && tg != dds)) {
+ obj.style.left = '-500px';
+ dds = null;
+ }
+ return;
+ }
+ return true;
+}
+
+
+
+
+/* O N L O A D */
+
+DOMLoad(function() {
+ var i;
+
+ // search box
+ i = x('searchfield');
+ i.onfocus = function () {
+ if(this.value == 'search') {
+ this.value = '';
+ this.style.color = '#000'; } };
+ i.onblur = function () {
+ if(this.value.length < 1) {
+ this.value = 'search';
+ this.style.color = '#999';} };
+
+ // browse categories
+ if(x('catsearch')) {
+ x('catsearch').onclick = function () {
+ var u = { i:'',e:'',l:'' };
+ var l = x('cat').getElementsByTagName('li');
+ var y;var j;
+ for(y=0;y<l.length;y++)
+ if((j = l[y].className.indexOf('cat_')) != -1) {
+ var k = l[y].className.substr(j+4, 3);
+ if(l[y].className.indexOf(' inc') != -1)
+ u.i += (u.i?',':'')+k;
+ if(l[y].className.indexOf(' exc') != -1)
+ u.e += (u.e?',':'')+k;
+ }
+ l = x('lfilter').getElementsByTagName('input');
+ for(y=0;y<l.length;y++)
+ if(l[y].checked)
+ u.l+=(u.l!=''?',':'')+l[y].name.substr(5);
+ var url = '/v/cat';
+ for (y in u)
+ if(u[y])
+ url+=(url.indexOf('?')<0?'?':';')+y+'='+u[y];
+ location.href=url;
+ return false;
+ };
+ var l = x('cat').getElementsByTagName('li');
+ for(i=0;i<l.length;i++)
+ if(l[i].className.indexOf('cat_') != -1)
+ l[i].onclick = function () {
+ try { document.selection.empty() } catch(e) { try { window.getSelection().collapse(this, 0) } catch(e) {} };
+ var sel = this.className.substr(this.className.indexOf('cat_'), 7);
+ this.className = this.className.indexOf(' inc') != -1 ? (sel+' exc') : this.className.indexOf(' exc') != -1 ? sel : (sel + ' inc');
+ };
+ }
+
+ // vnlist
+ cl('askcomment', function() {
+ this.href = this.href + '&amp;c=' + encodeURIComponent(prompt("Enter personal note (optional)", '')||'');
+ return true;
+ });
+
+ // mass-change vnlist status
+ if(x('vnlistchange')) {
+ x('vnlistchange').onchange = function() {
+ var val = this.options[this.selectedIndex].value;
+ if(val == '-3')
+ return;
+ var l = document.getElementsByTagName('input');
+ var y; var ch=0;
+ for(y=0;y<l.length;y++)
+ if(l[y].type == 'checkbox' && l[y].checked)
+ ch++;
+ if(!ch)
+ return alert('Nothing selected...');
+ if(val == '-1' && !confirm('Are you sure you want to remove the selected items from your visual novel list?'))
+ return;
+ if(val == '-2')
+ x('comments').value = prompt('Enter personal note (leave blank to delete note)','')||'';
+ document.forms[1].submit();
+ }
+ }
+
+ // autocheck
+ cl('checkall', function () {
+ var l = document.getElementsByTagName('input');
+ var y;
+ for(y=0;y<l.length;y++)
+ if(l[y].type == 'checkbox' && l[y].name == this.name)
+ l[y].checked = this.checked;
+ });
+
+ // a few confirm popups
+ cl('idel', function () {
+ return confirm('Are you sure you want to delete this item?\n\nAll previous edits will be deleted, this action can not be reverted!') });
+// cl('vhide', function () {
+// return confirm('!WARNING!\nHiding a visual novel also DELETES the following information:\n - VN Relations of ALL revisions\n - VN lists\n - Votes\nThis is NOT recoverable!'); });
+ cl('massdel', function () {
+ return confirm('Are you sure you want to mass-delete all the selected changes?\n\nThis action can not be reverted!') });
+
+ // NSFW
+ cl('nsfw', function () {
+ this.src = this.className;
+ this.id = '';
+ });
+
+ // spam protection on all forms
+ if(document.forms.length > 1)
+ for(i=1; i<document.forms.length; i++)
+ document.forms[i].action = document.forms[i].action.replace(/\/nospam\?/,'');
+
+ // dropdown menus
+ var z = document.getElementsByTagName('a');
+ for(i=0;i<z.length;i++)
+ if(z[i].rel && z[i].className.indexOf('dropdown') >= 0) {
+ document.onmousemove = dropDown;
+ break;
+ }
+
+ // form-stuff
+ if(document.forms.length > 1)
+ formhid();
+
+ // init dyna
+// if(x('vn_select') || x('md_select') || x('pd_select') || x('rl_select'))
+ if(window.dInit)
+ dInit();
+
+ // zebra-striped tables (client side!? yes... client side :3)
+ var sub = document.getElementsByTagName('tr');
+ for(i=1; i<sub.length; i+=2)
+ sub[i].style.backgroundColor = '#f5f5f5';
+});
diff --git a/static/files/dyna.js b/static/files/dyna.js
new file mode 100644
index 00000000..b4d64203
--- /dev/null
+++ b/static/files/dyna.js
@@ -0,0 +1,579 @@
+var med = {
+ cd: 'CD',
+ dvd: 'DVD',
+ gdr: 'GD-ROM',
+ blr: 'Blu-Ray disk',
+ 'in':'Internet download',
+ pa: 'Patch',
+ otc: 'Other (console)'
+};
+var vrel = [
+ 'Sequel',
+ 'Prequel',
+ 'Same setting',
+ 'Alternative setting',
+ 'Alternative version',
+ 'Same characters',
+ 'Side story',
+ 'Parent story',
+ 'Summary',
+ 'Full story',
+ 'Other'
+];
+
+var md;var pd;var rl;var vn;var ct;
+
+function dInit() {
+ md = x('md_select');
+ if(md) {
+ md.onclick = mdChangeSel;
+ mdLoad();
+ md.selectedIndex = 0;
+ mdChangeSel();
+ }
+
+ pd = x('pd_select');
+ if(pd) {
+ pd.onclick = pdChangeSel;
+ pdLoad();
+ pd.selectedIndex = 0;
+ pdChangeSel();
+ }
+
+ rl = x('rl_select');
+ if(rl) {
+ rl.onclick = rlChangeSel;
+ rlLoad();
+ rl.selectedIndex = 0;
+ rlChangeSel();
+ }
+
+ vn = x('vn_select');
+ if(vn) {
+ vn.onclick = vnChangeSel;
+ vnLoad();
+ vn.selectedIndex = 0;
+ vnChangeSel();
+ }
+
+ ct = x('categories');
+ if(ct)
+ catLoad();
+}
+
+function qq(v) {
+ return v.replace(/&/g,"&amp;").replace(/</,"&lt;").replace(/>/,"&gt;").replace(/'/g,/*'*/ "\\'").replace(/"/g,/*"*/'&quot;');
+}
+
+// small AJAX wapper
+var hr = false;
+function ajax(url, func) {
+ if(hr)
+ hr.abort();
+ hr = (window.ActiveXObject) ? new ActiveXObject('Microsoft.XMLHTTP') : new XMLHttpRequest();
+ if(hr == null) {
+ alert("Your browse does not support the functionality this website requires.");
+ return;
+ }
+ hr.onreadystatechange = func;
+ hr.open('GET', url, true);
+ hr.send(null);
+}
+
+
+
+
+ /************************\
+ * M E D I A *
+ \************************/
+
+
+function mdChangeSel() {
+ var sel = md.options[md.selectedIndex || 0];
+ var o = x('md_conts');
+ var i;
+ if(sel.value == '0_new') {
+ var l = ''; var q = '<option value="0">Qty</option>';
+ for(i in med)
+ l += '<option value="'+i+'">'+med[i]+'</option>';
+ for(i=1;i<10;i++)
+ q += '<option value="'+i+'">'+i+'</option>';
+ o.innerHTML = '<select id="md_Q" name="md_Q" style="width: 50px;">'+q+'</select>'
+ + '<select id="md_S" name="md_S" style="width: 150px;">'+l+'</select>'
+ + '<br style="clear: both" />'
+ + '<button type="button" onclick="mdAddRem()">add/remove</button>'
+ + '<br />Qty is only required for CD & DVD';
+ } else {
+ o.innerHTML = 'Selected "' + sel.text + '"<br />'
+ + '<button type="button" onclick="mdAddRem(\'' + sel.value + '\')">remove</button>';
+ }
+}
+
+function mdAddRem(id) {
+ var i;
+ var d = 0;
+ var o = id ? null : x('md_S').options[x('md_S').selectedIndex];
+ var qty = id ? null : x('md_Q').options[x('md_Q').selectedIndex].value;
+ var v = id ? id : (o.value != 'cd' && o.value != 'dvd' && o.value != 'gdr' && o.value != 'blr' ? o.value : (o.value + '_' + qty));
+ for(i=0;i<md.options.length;i++)
+ if(md.options[i].value == v) {
+ md.options[i] = null;
+ d = 1;
+ }
+ if(!d && !id) {
+ if(v.indexOf('_') >= 0 && qty == 0) {
+ alert('Please specify the quantity');
+ return;
+ }
+ md.options[md.options.length] = new Option(mdString(qty, o.value), v);
+ }
+ else if(id) {
+ md.options[0].selected = true;
+ mdChangeSel();
+ }
+ mdSerialize();
+}
+
+function mdSerialize() {
+ var dest = x('media');
+ var str = '';
+ var i;
+ for(i=0;i<md.options.length;i++)
+ md.options[i].value != '0_new' && (str += (str.length>0 ? ',' : '') + md.options[i].value);
+ dest.value = str;
+}
+
+function mdLoad() {
+ var me = x('media').value.split(',');
+ var i, j;
+ for(i=0;i<me.length;i++) {
+ var m = me[i].split('_');
+ if(med[m[0]])
+ md.options[md.options.length] = new Option(mdString(m[1], m[0]), me[i]);
+ }
+}
+
+function mdString(qty, medium) {
+ if(medium != 'cd' && medium != 'dvd' && medium != 'gdr' && medium != 'blr')
+ return med[medium];
+ else
+ return qty + ' ' + med[medium] + (qty > 1 ? 's' : '');
+}
+
+
+
+
+
+
+ /************************\
+ * P R O D U C E R S *
+ \************************/
+
+
+function pdChangeSel() {
+ var sel = pd.options[pd.selectedIndex || 0];
+ var o = x('pd_conts');
+ var i;
+ if(sel.value == '0_new') {
+ o.innerHTML = '<input type="text" name="pd_S" id="pd_S" onkeyup="pdDoSearch(0)" onkeydown="return pdEnter(event)" style="width: 150px;" />'
+ + '<button type="button" onclick="pdDoSearch(1)" style="width: 55px;">Search!</button><br style="clear: both" />'
+ + '<span id="pd_R" style="display: block; width: 220px; height: 70px; overflow: auto"></span>'
+ + '<a href="/p/add" target="_blank">Add new producer</a>';
+ pdDoSearch('');
+ } else {
+ o.innerHTML = 'Selected "' + sel.text + '"<br />'
+ + '<button type="button" onclick="pdAddRem(\'' + sel.value + '\')">remove</button>';
+ }
+}
+
+function pdEnter(ev) {
+ var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
+ if(c == 13) {
+ pdDoSearch(0);
+ return false;
+ }
+ return true;
+}
+
+function pdDoSearch(f) {
+ var v = x('pd_S').value;
+ var d = x('pd_R');
+ if(v.length < 1)
+ d.innerHTML = 'Hint: type pX if you know the producer id.';
+ else {
+ if(f)
+ d.innerHTML = '...searching...';
+ ajax('/xml/producers.xml?q='+escape(v)+'&r='+(Math.floor(Math.random()*999)+1), function () {
+ if(!hr || hr.readyState != 4 || !hr.responseText)
+ return;
+ if(hr.status != 200)
+ return alert('Whoops, error! :(');
+ var items = hr.responseXML.getElementsByTagName('item');
+ if(!items || items.length < 1) {
+ d.innerHTML = 'No results';
+ return false;
+ }
+ var res = '';
+ var i,j;
+ for(i=0; i<items.length; i++) {
+ var id = items[i].getElementsByTagName('id')[0].firstChild.nodeValue;
+ var name = items[i].getElementsByTagName('name')[0].firstChild.nodeValue;
+ var cid = id + ',' + name;
+ var s = '';
+ for(j=0; j<pd.options.length; j++)
+ if(pd.options[j].value == cid)
+ s = ' checked="checked"';
+ res += '<input type="checkbox" id="pd_I'+id+'"'+s+' onclick="pdAddRem(\''+qq(cid)+'\', \''+qq(name)+'\')" />'
+ + '<label style="width: auto" for="pd_I'+id+'">'+name+'</label><br style="clear: left" />';
+ }
+ d.innerHTML = res;
+ });
+ }
+}
+
+function pdAddRem(id, name) {
+ var i;
+ var d = 0;
+ for(i=0;i<pd.options.length;i++)
+ if(pd.options[i].value == id) {
+ pd.options[i] = null;
+ d = 1;
+ }
+ if(!d && name)
+ pd.options[pd.options.length] = new Option(name, id);
+ else if(!name) {
+ pd.options[0].selected = true;
+ pdChangeSel();
+ }
+ pdSerialize();
+}
+
+// id,name|||id,name
+function pdSerialize() {
+ var dest = x('producers');
+ var str = '';
+ var i;
+ for(i=0;i<pd.options.length;i++)
+ pd.options[i].value != '0_new' && (str += (str.length>0 ? '|||' : '') + pd.options[i].value);
+ dest.value = str;
+}
+
+function pdLoad() {
+ var pds = x('producers').value.split('|||');
+ if(!pds[0])
+ return;
+ var i;
+ for(i=0;i<pds.length;i++)
+ pd.options[pd.options.length] = new Option(pds[i].split(',',2)[1], pds[i]);
+}
+
+
+
+
+
+
+
+
+ /************************\
+ * R E L A T I O N S *
+ \************************/
+
+
+var rlsel = ''; var rlname = '';
+function rlChangeSel() {
+ var sel = rl.options[rl.selectedIndex || 0];
+ var o = x('rl_conts');
+ var i;
+ rlsel = '';
+ var ops='';
+ for(i=0;i<vrel.length;i++)
+ ops += '<option value="'+i+'">'+vrel[i]+'</option>';
+ if(sel.value == '0_new') {
+ o.innerHTML = '<input type="text" name="rl_S" id="rl_S" onkeyup="rlDoSearch(0)" onkeydown="return rlEnter(event)" style="width: 150px;" />'
+ + '<button type="button" onclick="rlDoSearch(1)" style="width: 60px;">Search!</button><br style="clear: both" />'
+ + '<span id="rl_R" style="display: block; width: 250px; height: 70px; overflow: auto"></span>'
+ + '<select id="rl_L" name="rl_L" onchange="rlAddRem(0)"><option value="-1">...is a [..] of this visual novel</option>'+ops+'</select>';
+ rlDoSearch('');
+ } else {
+ o.innerHTML = sel.value.split(',', 3)[2] + '<br />'
+ + '<select id="rl_L" name="rl_L" onchange="rlAddRem(\''+qq(sel.value)+'\')">'
+ + '<option value="-1"> - change - </option>'+ops+'<option value="-2"> - remove relation - </option></select>';
+ }
+}
+
+function rlEnter(ev) {
+ var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
+ if(c == 13) {
+ rlDoSearch(0);
+ return false;
+ }
+ return true;
+}
+
+function rlDoSearch(f) {
+ var v = x('rl_S').value;
+ var d = x('rl_R');
+ if(v.length < 1)
+ d.innerHTML = 'Search for a visual novel to add a relation.<br /><br />'
+ + 'Hint: type vX if you know the VN id.';
+ else {
+ if(f)
+ d.innerHTML = '...searching...';
+ ajax('/xml/vn.xml?q='+escape(v)+'&r='+(Math.floor(Math.random()*999)+1), function () {
+ if(!hr || hr.readyState != 4 || !hr.responseText)
+ return;
+ if(hr.status != 200)
+ return alert('Whoops, error! :(');
+ rlsel = '';
+ var items = hr.responseXML.getElementsByTagName('item');
+ if(!items || items.length < 1) {
+ d.innerHTML = 'No results';
+ return false;
+ }
+ var res = '';
+ var i,j;
+ for(i=0; i<items.length; i++) {
+ var id = items[i].getElementsByTagName('id')[0].firstChild.nodeValue;
+ var title = items[i].getElementsByTagName('title')[0].firstChild.nodeValue;
+ var cid = id + ',' + title;
+ res += '<input type="radio" name="rl_rad" id="pd_I'+id+'" value="rl_I'+id+'" onclick="rlAddRem(\''+qq(cid)+'\', \''+qq(title)+'\')" />'
+ + '<label style="width: auto" for="rl_I'+id+'">'+title+'</label><br style="clear: left" />';
+ }
+ d.innerHTML = res;
+ });
+ }
+}
+
+function rlAddRem(id, name) {
+ var i;
+ var rs = x('rl_L').selectedIndex;
+ if(id && name) {
+ rlsel = id;
+ rlname = name;
+ } else if(id) {
+ if(!rs)
+ return;
+ if(rs == x('rl_L').options.length-1) { // remove
+ for(i=0;i<rl.options.length;i++)
+ if(rl.options[i].value == id)
+ rl.options[i] = null;
+ rl.options[0].selected = true;
+ } else {
+ var cur = id.split(',', 3);
+ i = rl.selectedIndex;
+ rs--;
+ rl.options[i] = new Option(vrel[rs]+': '+cur[2], (rs)+','+cur[1]+','+cur[2]);
+ rl.options[i].selected = true;
+ }
+ rlChangeSel();
+ rlSerialize();
+ return;
+ } else if(!rlsel) {
+ alert('No visual novel selected');
+ return;
+ }
+
+ if(!id && rlsel && !rs) { // remove
+ for(i=0;i<rl.options.length;i++)
+ if(rl.options[i].value.indexOf(rlsel) != -1)
+ rl.options[i] = null;
+ rlSerialize();
+ return;
+ }
+ if(!rs)
+ return;
+
+ // add/edit
+ var mod = rl.options.length;
+ rs--;
+ for(i=0;i<rl.options.length;i++)
+ if(rl.options[i].value.indexOf(rlsel) != -1)
+ mod = i;
+ rl.options[mod] = new Option(vrel[rs]+': '+rlname, rs+','+rlsel);
+
+ rlSerialize();
+}
+
+// rel,id,name|||rel,id,name
+function rlSerialize() {
+ var dest = x('relations');
+ var str = '';
+ var i;
+ for(i=0;i<rl.options.length;i++)
+ rl.options[i].value != '0_new' && (str += (str.length>0 ? '|||' : '') + rl.options[i].value);
+ dest.value = str;
+}
+
+function rlLoad() {
+ var rls = x('relations').value.split('|||');
+ if(!rls[0])
+ return;
+ var i;
+ for(i=0;i<rls.length;i++)
+ rl.options[rl.options.length] = new Option(vrel[rls[i].split(',',3)[0]]+': '+rls[i].split(',',3)[2], rls[i]);
+}
+
+
+
+
+
+
+
+
+ /************************\
+ * VISUAL NOVELS *
+ \************************/
+
+
+function vnChangeSel() {
+ var sel = vn.options[vn.selectedIndex || 0];
+ var o = x('vn_conts');
+ var i;
+ var ops='';
+ for(i=0;i<vrel.length;i++)
+ ops += '<option value="'+i+'">'+vrel[i]+'</option>';
+ if(sel.value == '0_new') {
+ o.innerHTML = '<input type="text" name="vn_S" id="vn_S" onkeyup="vnDoSearch(0)" onkeydown="return vnEnter(event)" style="width: 150px;" />'
+ + '<button type="button" onclick="vnDoSearch(1)" style="width: 60px;">Search!</button><br style="clear: both" />'
+ + '<span id="vn_R" style="display: block; width: 250px; height: 90px; overflow: auto"></span>';
+ vnDoSearch('');
+ } else {
+ o.innerHTML = 'Selected "' + sel.text + '"<br />'
+ + '<button type="button" onclick="vnAddRem(\'' + sel.value + '\')">remove</button>';
+ }
+}
+
+function vnEnter(ev) {
+ var c = document.layers ? ev.which : document.all ? event.keyCode : ev.keyCode;
+ if(c == 13) {
+ vnDoSearch(0);
+ return false;
+ }
+ return true;
+}
+
+function vnDoSearch(f) {
+ var v = x('vn_S').value;
+ var d = x('vn_R');
+ if(v.length < 1)
+ d.innerHTML = 'Hint: type vX if you know the visual novel id.';
+ else {
+ if(f)
+ d.innerHTML = '...searching...';
+ ajax('/xml/vn.xml?q='+escape(v)+'&r='+(Math.floor(Math.random()*999)+1), function () {
+ if(!hr || hr.readyState != 4 || !hr.responseText)
+ return;
+ if(hr.status != 200)
+ return alert('Whoops, error! :(');
+ var items = hr.responseXML.getElementsByTagName('item');
+ if(!items || items.length < 1) {
+ d.innerHTML = 'No results';
+ return false;
+ }
+ var res = '';
+ var i,j;
+ for(i=0; i<items.length; i++) {
+ var id = items[i].getElementsByTagName('id')[0].firstChild.nodeValue;
+ var title = items[i].getElementsByTagName('title')[0].firstChild.nodeValue;
+ var s = '';
+ for(j=0; j<vn.options.length; j++)
+ if(vn.options[j].value == id)
+ s = ' checked="checked"';
+ res += '<input type="checkbox" id="vn_I'+id+'"'+s+' onclick="vnAddRem(\''+qq(id)+'\', \''+qq(title)+'\')" />'
+ + '<label style="width: auto" for="vn_I'+id+'">'+title+'</label><br style="clear: left" />';
+ }
+ d.innerHTML = res;
+ });
+ }
+}
+
+function vnAddRem(id, title) {
+ var i;
+ var d = 0;
+ for(i=0;i<vn.options.length;i++)
+ if(vn.options[i].value == id) {
+ vn.options[i] = null;
+ d = 1;
+ }
+ if(!d && title)
+ vn.options[vn.options.length] = new Option(title, id);
+ else if(!title) {
+ vn.options[0].selected = true;
+ vnChangeSel();
+ }
+ vnSerialize();
+}
+
+// id,title|||id,title
+function vnSerialize() {
+ var dest = x('vn');
+ var str = '';
+ var i;
+ for(i=0;i<vn.options.length;i++)
+ vn.options[i].value != '0_new' && (str += (str.length>0 ? '|||' : '') + vn.options[i].value + ',' + vn.options[i].text);
+ dest.value = str;
+}
+
+function vnLoad() {
+ var vns = x('vn').value.split('|||');
+ if(!vns[0])
+ return;
+ var i;
+ for(i=0;i<vns.length;i++)
+ vn.options[vn.options.length] = new Option(vns[i].split(',',2)[1], vns[i].split(',',2)[0]);
+}
+
+
+
+
+
+
+ /************************\
+ * C A T E G O R I E S *
+ \************************/
+
+
+function catLoad() {
+ var i;var cats=[];
+ var l = ct.value.split(',');
+ for(i=0;i<l.length;i++)
+ cats[l[i].substr(0,3)] = Math.floor(l[i].substr(3,1));
+
+ var l=x('cat').getElementsByTagName('a');
+ for(i=0;i<l.length;i++) {
+ catSet(l[i].id.substr(4), cats[l[i].id.substr(4)]||0);
+ l[i].onclick = function() {
+ var c = this.id.substr(4);
+ if(!cats[c]) cats[c] = 0;
+ if(c.substr(0,1) == 'p' || c == 'gaa' || c == 'gab') {
+ if(cats[c]++)
+ cats[c] = 0;
+ } else if(++cats[c] == 4)
+ cats[c] = 0;
+ catSet(c, cats[c]);
+
+ // has to be ordered before serializing!
+ var r;l=[];i=0;
+ for(r in cats)
+ l[i++] = r;
+ l = l.sort();
+ r='';
+ for(i=0;i<l.length;i++)
+ if(cats[l[i]] > 0)
+ r+=(r?',':'')+l[i]+cats[l[i]];
+ ct.value = r;
+ return false;
+ };
+ }
+}
+
+function catSet(id, rnk) {
+ var c = rnk == 0 ? '#000' :
+ rnk == 1 ? '#090' :
+ rnk == 2 ? '#990' : '#900';
+ x('b_'+id).style.color = c;
+ x('cat_'+id).style.color = c;
+ x('b_'+id).innerHTML = rnk;
+}
+
+
diff --git a/static/files/footer.gif b/static/files/footer.gif
new file mode 100644
index 00000000..c87fb121
--- /dev/null
+++ b/static/files/footer.gif
Binary files differ
diff --git a/static/files/graph.png b/static/files/graph.png
new file mode 100644
index 00000000..bb56f758
--- /dev/null
+++ b/static/files/graph.png
Binary files differ
diff --git a/static/files/headerbg.jpg b/static/files/headerbg.jpg
new file mode 100644
index 00000000..81f4dd75
--- /dev/null
+++ b/static/files/headerbg.jpg
Binary files differ
diff --git a/static/files/headerbot.png b/static/files/headerbot.png
new file mode 100644
index 00000000..6e04ab05
--- /dev/null
+++ b/static/files/headerbot.png
Binary files differ
diff --git a/static/files/platforms.png b/static/files/platforms.png
new file mode 100644
index 00000000..66951fe2
--- /dev/null
+++ b/static/files/platforms.png
Binary files differ
diff --git a/static/files/rss.png b/static/files/rss.png
new file mode 100644
index 00000000..923c3822
--- /dev/null
+++ b/static/files/rss.png
Binary files differ
diff --git a/static/files/select.png b/static/files/select.png
new file mode 100644
index 00000000..ac219e05
--- /dev/null
+++ b/static/files/select.png
Binary files differ
diff --git a/static/files/sidebarbg.jpg b/static/files/sidebarbg.jpg
new file mode 100644
index 00000000..00eb5697
--- /dev/null
+++ b/static/files/sidebarbg.jpg
Binary files differ
diff --git a/static/files/sidebarbot.jpg b/static/files/sidebarbot.jpg
new file mode 100644
index 00000000..49884ded
--- /dev/null
+++ b/static/files/sidebarbot.jpg
Binary files differ
diff --git a/static/files/sidebg.jpg b/static/files/sidebg.jpg
new file mode 100644
index 00000000..65fd3306
--- /dev/null
+++ b/static/files/sidebg.jpg
Binary files differ
diff --git a/static/files/style.css b/static/files/style.css
new file mode 100644
index 00000000..5f78116e
--- /dev/null
+++ b/static/files/style.css
@@ -0,0 +1,729 @@
+
+body {
+ margin: 15px 0 0 0;
+ padding: 0 0 60px 0;
+ background: #fff;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 13px;
+ color: #203C36;
+}
+
+form {
+ margin: 0;
+ padding: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+ border: none;
+}
+
+legend {
+ display: none;
+}
+
+input, textarea {
+ padding: 2px 5px;
+ border: 1px solid #B8E0D7;
+ font: normal 1em Arial, Helvetica, sans-serif;
+ color: #203C36;
+}
+
+h1, h2 {
+ color: #203C36;
+ margin: 0;
+}
+
+h1 {
+ text-transform: lowercase;
+ letter-spacing: -1px;
+ font-size: 3em;
+}
+
+h2 {
+ font-size: 1.7em;
+ clear: right;
+}
+
+h3 {
+ font-size: 1.1em;
+ margin: 0;
+}
+
+p, ul, ol {
+ margin: 0;
+}
+
+blockquote {
+ font-style: italic;
+}
+a {
+/* color: #7AB9AB;*/
+ color: #69A89A;
+}
+a:hover {
+ text-decoration: none;
+}
+
+img {
+ border: none;
+}
+
+/* Header */
+
+#header {
+ width: 960px;
+ height: 80px;
+ margin: 0 auto;
+ background: url(/files/headerbg.jpg);
+}
+
+#header h1 {
+ margin: 0;
+ padding: 15px 0 0 20px;
+ letter-spacing: normal;
+ font-size: 1em;
+ color: #FFFFFF;
+}
+
+#header h1 a {
+ text-decoration: none;
+ color: #FFFFFF;
+}
+
+#header h1 a:hover {
+ text-decoration: underline;
+}
+
+#header b {
+ display: block;
+ letter-spacing: -2px;
+ font-size: 2.4em;
+}
+
+/* Search */
+
+#search {
+ float: right;
+ width: 180px;
+ padding-top: 30px;
+}
+
+#searchfield {
+ width: 150px;
+}
+
+#searchsubmit {
+ display: none;
+}
+
+/* Login */
+
+#loginform {
+ padding: 0;
+ margin: 0 0 0 20px;
+}
+#loginform input {
+ width: 40px;
+ margin: 0;
+ padding: 1px;
+ position: relative;
+}
+#loginform #usrname, #loginform #usrpass {
+ width: 70px;
+ border: 0;
+}
+
+/* Page */
+
+#page {
+ width: 960px;
+ margin: 0 auto;
+ padding: 40px 0;
+ background: url(/files/headerbot.png) no-repeat;
+}
+
+#content {
+ float: left;
+ width: 700px;
+}
+
+/* Side */
+
+#side {
+ float: right;
+ padding: 0;
+ background: url(/files/sidebg.jpg);
+ color: #fff;
+}
+#side div {
+ background: url(/files/sidebarbg.jpg) no-repeat;
+}
+#side div div {
+ width: 240px;
+ padding: 0 0 60px 0;
+ background: url(/files/sidebarbot.jpg) no-repeat left bottom;
+}
+
+#side a {
+ color: #fff;
+}
+
+#side h2 {
+ padding: 20px 20px 0 15px;
+ text-transform: uppercase;
+ font-family: "Arial Black", Arial, Helvetica, sans-serif;
+ font-size: .8em;
+ color: #fff;
+}
+
+#side ul {
+ margin: 0;
+ padding: 0 20px 0 20px;
+ list-style: none;
+}
+
+#side li {
+ padding: 0;
+}
+
+#side p {
+ margin: 0;
+ padding: 0 0 0 20px;
+}
+#side li.more {
+ font-style: italic;
+}
+
+/* Footer */
+
+#footer {
+ clear: both;
+ padding: 10px 0;
+}
+
+#footer p {
+ margin: 0;
+ text-align: center;
+ color: #999999;
+ font-size: 0.8em;
+}
+
+#footer a {
+ color: #999999;
+}
+
+/* Forms */
+#content form {
+ display: block;
+ margin: 20px 0 10px 0;
+ padding: 0;
+}
+#content form.tblf {
+ margin: 0;
+}
+form ul {
+ margin: 0;
+ padding: 0;
+}
+form li {
+ display: block;
+ clear: left;
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+}
+form ul ul {
+ margin: 0; padding: 0;
+ clear: left;
+}
+form li.nolabel, form ul ul {
+ padding: 0 0 0 110px;
+}
+form li.nextpart {
+ padding: 10px 0 0 0;
+}
+#content label, #content input, #content textarea, #content select, form li b, form li p {
+ display: block;
+ float: left;
+}
+form li label i, form p.formnotice i {
+ display: inline;
+ color: #f00;
+}
+input.text, select {
+ width: 200px;
+}
+select, input.text, textarea {
+ border: 1px solid #B8E0D7;
+ font: normal 1em Arial, Helvetica, sans-serif;
+ color: #203C36;
+}
+form label.checkbox {
+ width: auto;
+}
+select.multiple {
+ height: 120px;
+ width: 230px;
+}
+#md_conts, #pd_conts, #rl_conts, #vn_conts {
+ display: block;
+ float: left;
+ background-color: #e3ecff;
+ border: 1px dashed #b4b4ff;
+ padding: 2px;
+}
+#rl_select {
+ width: 280px;
+}
+form li.longopts select {
+ width: 350px;
+}
+form li.shortopts input {
+ width: 50px;
+}
+form li i {
+ float: left;
+}
+input.hidden {
+ display: none;
+ position: absolute;
+ top: -30px;
+}
+label, form li b {
+ width: 110px;
+ font-weight: normal;
+}
+form li.nolabel b {
+ display: inline;
+ width: auto;
+ font-weight: bold;
+ float: none;
+}
+form li.subform {
+ padding: 10px 0 0 0;
+ clear: both;
+}
+form li.subform a {
+ display: block;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ background-color: #f0f0f0;
+ text-decoration: none;
+ font-weight: bold;
+ color: #7AB9AB;
+}
+li.formhid {
+ float: none;
+ position: absolute;
+ top: -2000px;
+ left: -2000px;
+ width: 10px;
+}
+form li.date select {
+ width: 100px;
+}
+
+/* Platform selecter */
+form ul.platforms {
+ clear: none;
+ padding: 0;
+ margin: 0 0 0 110px;
+}
+form ul.platforms li {
+ float: left;
+ clear: none;
+}
+form ul.platforms li label {
+ width: 150px;
+}
+form ul.platforms input {
+ padding: 0;
+ margin: 0 2px 0 2px;
+ border: 0;
+ height: 14px;
+}
+
+/* Warning & msg box */
+span.warning {
+ display: block;
+ margin: 5px 10% 5px 10%;
+ padding: 3px 5px 3px 60px;
+ background: #ffece3 url('/files/warning.png') no-repeat;
+ border: 1px dashed #ffb4b4;
+ min-height: 57px;
+}
+* html span.warning { height: 57px; }
+span.msg {
+ display: block;
+ margin: 5px 10% 5px 10%;
+ padding: 3px 5px 3px 5px;
+ border: 1px dashed #3cb700;
+ background: #bfffb5;
+}
+span.warning ul, span.msg ul {
+ padding: 0 0 0 15px;
+}
+
+dt {
+ float: left;
+ font-style: italic;
+}
+dd {
+ padding: 0 0 0 50px;
+}
+dl.vnrel dd, dl.vncat dd {
+ padding: 0 0 0 75px;
+}
+ul {
+ padding: 0 0 0 17px;
+}
+p.actions, #vnheader p.actions, b.actions {
+ display: inline;
+ margin: 0 0 0 5px;
+ font-weight: normal;
+ font-size: 12px;
+ color: #999;
+}
+a.relch {
+ color: #900;
+}
+a.rss {
+ display: block;
+ height: 16px;
+ width: 16px;
+ background: url(/files/rss.png) no-repeat;
+ text-indent: -9999px;
+ overflow: hidden;
+ float: right;
+}
+#vnheader div {
+ width: 256px;
+ float: left;
+ text-align: center;
+}
+#vnheader div img {
+ margin: 0 auto 0 auto;
+}
+#vnheader #nsfw {
+ cursor: pointer;
+}
+img.left {
+ float: left;
+ margin: 0 15px 0 0;
+}
+
+img.right {
+ float: left;
+ margin: 0 0 0 15px;
+}
+#vnheader dl {
+ margin: 0 0 0 270px;
+}
+#vnheader h3 {
+ margin: 5px 0 0 260px;
+ font-size: 13px;
+}
+#vnheader p {
+ margin: 0 0 0 270px;
+}
+#vnheader select {
+ width: 60px;
+ height: 16px;
+ display: inline;
+ float: none;
+ text-align: center;
+}
+#vnheader #vnlist {
+ width: 150px;
+ text-align: left;
+}
+
+#vnheader p.mod {
+ margin: 0;
+}
+
+/* producer search */
+form#psearch {
+ display: block;
+ margin: 10px 0 20px 210px;
+}
+* html form#psearch { margin-left: 105px; }
+
+
+ul.home {
+ float: left;
+ list-style-type: none;
+ width: 210px;
+ margin-top: 30px;
+}
+ul.home li { padding: 0; }
+ul.home.break { clear: left; }
+h3.home {
+ clear: left;
+ padding-top: 30px;
+}
+
+
+p.mod {
+ float: right;
+ color: #999;
+}
+div.dropdown {
+ width: 100px;
+ position: absolute;
+ left: -500px;
+ top: 0;
+ border: 1px solid #d0d0d0;
+ border-bottom: none;
+}
+div.dropdown ul, div.dropdown ul li {
+ list-style-type: none;
+ margin: 0;
+ padding: 0;
+ width: 100px;
+ background-color: #f0f0f0;
+ border-bottom: 1px solid #d0d0d0;
+}
+div.dropdown a, div.dropdown b {
+ display: block;
+ width: 90px;
+ margin: 0;
+ padding: 2px 5px;
+ text-decoration: none;
+}
+div.dropdown b {
+ font-weight: normal;
+ font-style: italic;
+ color: #666;
+}
+div.dropdown li.center a {
+ text-align: center;
+}
+div.dropdown a:hover {
+ background-color: #e0e0e0;
+}
+
+
+p#relations {
+ margin: 20px 0 0 0;
+ width: 100%;
+ text-align: center;
+}
+
+
+p.desc {
+ padding: 0 10px 0 10px;
+}
+p.chr {
+ width: 100%;
+ text-align: center;
+ font-size: 1.1em;
+}
+p.browse {
+ width: 100%;
+ text-align: center;
+}
+b.mod, a.mod {
+ color: #f00;
+}
+
+/* CATEGORIES */
+ul#cat {
+ margin: 0;
+ padding: 0;
+}
+ul#cat ul {
+ list-style-type: none;
+ padding: 0;
+}
+ul#cat input {
+ padding: 0;
+ margin: 0 2px 0 2px;
+ border: 0;
+ height: 14px;
+}
+ul#cat li, form ul#cat li {
+ display: block;
+ width: 170px;
+ float: left;
+ clear: none;
+ font-weight: bold;
+}
+ul#cat li li {
+ display: list-item;
+ width: auto;
+ float: none;
+ clear: left;
+ font-weight: normal;
+ padding: 0 0 0 20px;
+ margin: 0;
+ cursor: pointer;
+ list-style-type: none;
+ background: url(/files/select.png) no-repeat;
+}
+form ul#cat li li {
+ padding: 0;
+ background: none;
+}
+form ul#cat li li b { width: 13px; font-weight: bold; }
+form ul#cat li li a { color: #000; text-decoration: none; display: block; width: 160px; }
+ul#cat li li.inc { background-position: 0px -16px; color: #090; }
+ul#cat li li.exc { background-position: 0px -33px; color: #900; }
+
+div#lfilter {
+ clear: left;
+ padding: 10px 0 0 0;
+}
+div#lfilter input {
+ float: left; margin: 3px 3px 0 10px;
+}
+div#lfilter label {
+ float: left; margin-top: 3px;
+ width: 90px;
+}
+
+i.crgn0 { font-style: normal; }
+i.crgn1 { font-style: normal; color: #bbb; }
+i.crgn2 { font-style: normal; }
+i.crgn3 { font-style: normal; font-weight: bold; }
+
+/* DOCUMENTATION PAGES */
+#dpage h3 { margin-top: 20px; }
+#dpage dd { padding-bottom: 5px; margin-left: 70px; }
+
+
+
+#content input.right, #content select.right {
+ float: right;
+}
+#vnlistchange { width: 130px; }
+p.opts {
+ clear: left;
+ text-align: center;
+ width: 100%;
+ background-color: #f0f0f0;
+ margin-bottom: 10px;
+ margin-top: 10px;
+ padding: 1px 0;
+}
+ul#stats {
+ clear: left;
+ list-style-type: none;
+ padding: 0px 0 0 5px;
+}
+ul#stats li {
+ padding: 0;
+ display: block;
+ width: 345px;
+ float: left;
+ margin: 0 0 20px 0;
+}
+ul#stats li.break {
+ clear: left;
+}
+
+acronym {
+ border-bottom: 1px dotted #999;
+}
+acronym.date {
+ border: 0;
+}
+b.future { font-weight: normal; color: #900; }
+
+table td { vertical-align: top; }
+table { width: 100%; border-collapse: collapse; }
+thead tr td { font-weight: bold; }
+thead tr td a { text-decoration: none; }
+
+.plat {
+ background: url(/files/platforms.png) no-repeat;
+ width: 16px;
+ height: 14px;
+ margin: 0 2px 0 0;
+ overflow: hidden;
+ float: right;
+ text-indent: -999px;
+ display: block;
+ padding: 0;
+ border: 0;
+}
+li .plat {
+ float: left;
+}
+.plat.oth { background: none; }
+.plat.dc { background-position: 0px 0px; }
+.plat.lin { background-position: 0px -14px; }
+.plat.nds { background-position: 0px -28px; }
+.plat.ps2 { background-position: 0px -42px; }
+.plat.sfc { background-position: 0px -56px; }
+.plat.gba { background-position: 0px -70px; }
+.plat.wii { background-position: 0px -84px; }
+.plat.dvd { background-position: -16px 0px; }
+.plat.mac { background-position: -16px -14px; }
+.plat.ps { background-position: -16px -28px; }
+.plat.psp { background-position: -16px -42px; }
+.plat.win { background-position: -16px -56px; }
+.plat.ext { background-position: -16px -70px; }
+
+
+
+#content table input {
+ width: 13px;
+ height: 13px;
+ margin: 0;
+ float: right;
+}
+
+/* revisions */
+#tmc { width: 650px; margin: 0 0 30px 30px; border: 1px solid #ddd; background-color: #fbfbfb; clear: both; }
+#tmc thead tr td { font-weight: normal; text-align: center; }
+#tmc .tc1, #tmc .tc2 { border-right: 1px solid #ddd; }
+#tmc .tc1 { font-weight: bold; padding-right: 10px; }
+#tmc .tc1 { width: 100px; }
+#tmc .tc2, #tmc .tc3 { width: 275px; }
+div#tmc { text-align: center; }
+div#revbrowse { margin: 20px 0 0 30px; width: 650px; text-align: center; } /* position: relative; top: 17px; left: 30px; */
+a#revnext { float: right }
+a#revprev { float: left; }
+b.diff_add { font-weight: normal; background-color: #cfc; }
+b.diff_del { font-weight: normal; background-color: #fcc; }
+
+#tvg tr, #tus tr { background-color: #fff!important }
+#tvg .tc1, #tus .tc1 { width: 25px; text-align: right; padding-right: 3px; }
+#tvg .tc2 div, #tus .tc2 div { margin: 0 5px 0 0; padding: 0; float: left; background: url(/files/graph.png); height: 13px; }
+#tus .tc1 { width: 60px }
+
+#tvr .tc3, #tvl .tc5, #tur .tc3, #tul .tc8, #thi .tc6 { text-align: right }
+
+#tvl .tc1, #tvl .tc2, #tvl .tc3 { white-space: nowrap; padding-right: 10px; }
+
+#thi { clear: both }
+#thi .tc1 { width: 35px; }
+#thi .tc2 { width: 110px; }
+
+#tre tr { background-color: #fff!important; }
+#tre tr.lang { background-color: #f5f5f5!important; font-style: italic; }
+
+#tre .tc1 { width: 75px; padding-left: 10px; }
+#tre .tc2 { width: 60px; text-align: center; }
+#tre .tc3 { width: 55px; margin: 0; padding: 0; white-space: nowrap; }
+#tre .tc4 { width: 10px; text-align: right; padding-right:3px; }
+#tre .tc7 { width: 16px; margin: 0; padding: 0; white-space: nowrap; }
+
+#debug {
+ border-top: 1px solid #ffb4b4;
+ background-color: #ffece3;
+ height: 70px;
+ overflow: auto;
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ font-size: 10px;
+ margin: 0;
+ padding: 0;
+}
+
diff --git a/static/files/warning.png b/static/files/warning.png
new file mode 100644
index 00000000..b8af1a53
--- /dev/null
+++ b/static/files/warning.png
Binary files differ
diff --git a/util/cleanimg.pl b/util/cleanimg.pl
new file mode 100644
index 00000000..527fdb3a
--- /dev/null
+++ b/util/cleanimg.pl
@@ -0,0 +1,102 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Time::HiRes 'gettimeofday', 'tv_interval';
+BEGIN {
+ our $ST = [ gettimeofday ];
+}
+use DBI;
+use Image::Magick;
+use Image::MetaData::JPEG;
+use File::Copy 'cp', 'mv';
+use Digest::MD5;
+
+our $ST;
+
+my $sql = DBI->connect('dbi:Pg:dbname=vndb', 'vndb', 'passwd',
+ { RaiseError => 1, PrintError => 0, AutoCommit => 1, pg_enable_utf8 => 1 });
+
+my $imgpath = '/www/vndb/static/img';
+my $tmpimg = '/tmp/vndb-clearimg.jpg';
+
+imgscan();
+
+printf "Finished in %.3f seconds\n", tv_interval($ST);
+
+sub imgscan {
+ print "Scanning images...\n";
+ my $done = 0;
+ for my $c ('0'..'9', 'a'..'f') {
+ opendir(my $D, "$imgpath/$c") || die "$imgpath/$c: $!";
+ for my $f (readdir($D)) {
+ my $cur = "$imgpath/$c/$f";
+ next if !-s $cur || $f !~ /^(.+)\.jpg$/;
+ my $cmd5 = $1;
+
+ # delete unused images
+ if($f =~ /^tmp/ || $f =~ /\.jpg\.jpg$/) {
+ printf "Deleting temp image %s/%s\n", $c, $f;
+ unlink $cur or die $!;
+ next;
+ }
+ my $q = $sql->prepare('SELECT 1 FROM vn_rev WHERE image = DECODE(?, \'hex\')');
+ $q->execute($cmd5);
+ my $d = $q->fetchrow_arrayref();
+ if(!$d || ref($d) ne 'ARRAY' || $d->[0] <= 0) {
+ printf "Deleting %s/%s\n", $c, $f;
+ unlink $cur or die $!;
+ $done++;
+ next;
+ }
+ $q->finish();
+
+ # remove metadata
+ my $i = Image::MetaData::JPEG->new($cur);
+ $i->drop_segments('METADATA');
+ $i->save($tmpimg);
+ if(-s $tmpimg < (-s $cur)-32) {
+ printf "Removed metadata from %s/%s: %.2f to %.2f kB\n", $c, $f, (-s $cur)/1024, (-s $tmpimg)/1024;
+ cp $tmpimg, $cur;
+ }
+
+ # compress large images
+ if(-s $cur > 20*1024) { # > 20 KB
+ $i = Image::Magick->new;
+ $i->Read($cur);
+ $i->Set(quality => 80);
+ $i->Write($tmpimg);
+ undef $i;
+ #if(-s $tmpimg > 35*1024) { # extremely large images get a quality of 65
+ # $i = Image::Magick->new;
+ # $i->Read($cur);
+ # $i->Set(quality => 65);
+ # $i->Write($tmpimg);
+ # undef $i;
+ #}
+ if(-s $tmpimg < (-s $cur)-1024) {
+ printf "Compressed %s/%s from %.2f to %.2f kB\n", $c, $f, (-s $cur)/1024, (-s $tmpimg)/1024;
+ cp $tmpimg, $cur or die $!;
+ $done++;
+ }
+ }
+
+ # rename file if MD5 is different
+ open(my $T, '<:raw:bytes', $cur) || die $!;
+ my $md5 = Digest::MD5->new()->addfile($T)->hexdigest;
+ close($T);
+ if($md5 ne $cmd5) {
+ $sql->do('UPDATE vn_rev SET image = DECODE(?, \'hex\') WHERE image = DECODE(?, \'hex\')', undef, $md5, $cmd5);
+ mv $cur, sprintf "%s/%s/%s.jpg", $imgpath, substr($md5, 0, 1), $md5 or die $!;
+ printf "Renamed %s/%s to %s/%s\n", $c, $cmd5, substr($md5, 0, 1), $md5;
+ }
+ }
+ closedir($D);
+ }
+ unlink $tmpimg;
+ print "Everything seems to be ok\n" if !$done;
+}
+
+
+
+1;
diff --git a/util/cron_daily.sh b/util/cron_daily.sh
new file mode 100755
index 00000000..743a9839
--- /dev/null
+++ b/util/cron_daily.sh
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+# we want to run as user 'yorhel'
+if [ `id -nu` != 'yorhel' ]; then
+ su yorhel -c "$0"
+ exit;
+fi
+
+cd /www/vndb/util
+
+SQL='psql -e vndb -U vndb';
+
+echo '
+
+ =================================================================================
+=================== VNDB cron running at '`date`' ==================
+=== Executing SQL statements'
+echo '\timing
+\i cron_daily.sql' | $SQL
+
+echo '=== Creating/updating sitemap';
+./sitemap.pl
+#echo '=== Cleaning up images';
+#./cleanimg.pl
+#echo '=== Creating relation graphs';
+#./relgraph.pl
+echo '=== VACUUM FULL ANALYZE';
+vacuumdb -U yorhel --full --analyze vndb >/dev/null 2>&1
+
+echo '=== VNDB cron finished at '`date`' ===';
+
diff --git a/util/cron_daily.sql b/util/cron_daily.sql
new file mode 100644
index 00000000..c30f30f1
--- /dev/null
+++ b/util/cron_daily.sql
@@ -0,0 +1,15 @@
+-- update c_* columns in vn
+SELECT update_vncache(0), calculate_rating();
+
+-- update changes.prev columns
+SELECT update_prev('vn', ''), update_prev('releases', ''), update_prev('producers', '');
+
+-- check...
+ SELECT 'r', id FROM releases_rev rr
+ WHERE NOT EXISTS(SELECT 1 FROM releases_vn rv WHERE rr.id = rv.rid)
+UNION
+ SELECT c.type::varchar, id FROM changes c
+ WHERE (c.type = 0 AND NOT EXISTS(SELECT 1 FROM vn_rev vr WHERE vr.id = c.id))
+ OR (c.type = 1 AND NOT EXISTS(SELECT 1 FROM releases_rev rr WHERE rr.id = c.id))
+ OR (c.type = 2 AND NOT EXISTS(SELECT 1 FROM producers_rev pr WHERE pr.id = c.id));
+
diff --git a/util/relgraph.pl b/util/relgraph.pl
new file mode 100755
index 00000000..cdbd022b
--- /dev/null
+++ b/util/relgraph.pl
@@ -0,0 +1,237 @@
+#!/usr/bin/perl
+
+our $S;
+
+open(STDERR, ">&STDOUT"); # warnings and errors can be captured easily this way
+$ENV{PATH} = '/usr/bin'; # required for GraphViz
+
+use strict;
+use warnings;
+use Text::Unidecode;
+#use Time::HiRes 'gettimeofday', 'tv_interval';
+#BEGIN { $S = [ gettimeofday ]; }
+#END { printf "Done in %.2f s\n", tv_interval($S); }
+
+use Digest::MD5 'md5_hex';
+use Time::CTime;
+use GraphViz;
+use DBI;
+use POSIX 'floor';
+
+require '/www/vndb/lib/global.pl';
+
+
+my $font = 's'; #Comic Sans MSssss';
+my @fsize = ( 9, 7, 10 ); # nodes, edges, node_title
+my $tmpfile = '/tmp/vndb_graph.gif';
+my $destdir = '/www/vndb/static/rg';
+my $datdir = '/www/vndb/data/rg';
+my $DEBUG = 0;
+
+
+my %nodes_all = (
+ fontname => $font,
+ shape => 'plaintext',
+ fontsize => $fsize[0],
+ style => "setlinewidth(0.5)",
+);
+
+my %edge_all = (
+ labeldistance => 2.5,
+ labelangle => -20,
+ labeljust => 'l',
+ dir => 'both',
+ minlen => 2,
+ fontname => $font,
+ fontsize => $fsize[1],
+ arrowsize => 0.7,
+ color => '#69a89a',
+# constraint => 0,
+);
+
+my @edge_rel = map {
+ {
+ %edge_all,
+ $VNDB::VRELW->{$_} ? (
+ headlabel => $VNDB::VREL->[$_],
+ taillabel => $VNDB::VREL->[$_-1],
+ ) : $VNDB::VRELW->{$_+1} ? (
+ headlabel => $VNDB::VREL->[$_],
+ taillabel => $VNDB::VREL->[$_+1],
+ ) : (
+ label => ' '.$VNDB::VREL->[$_],
+ ),
+ };
+} 0..$#$VNDB::VREL;
+
+
+
+
+my $sql = DBI->connect('dbi:Pg:dbname=vndb', 'vndb', 'passwd',
+ { RaiseError => 1, PrintError => 0, AutoCommit => 0, pg_enable_utf8 => 1 });
+my %ids; my %nodes;
+my %rels; # "v1-v2" => 1
+my @done;
+
+
+
+sub createGraph { # vid
+ my $id = shift;
+ %ids = ();
+ %nodes = ();
+ %rels = ();
+
+ return 0 if grep { $id == $_ } @done;
+
+ my $g = GraphViz->new(
+# width => 700/96,
+ height => 2000/96,
+ ratio => 'compress',
+ );
+
+ getRel($g, $id);
+ if(!keys %rels) {
+ push @done, $id;
+ $sql->do(q|UPDATE vn SET rgraph = 0 WHERE id = ?|, undef, $id);
+ return 0;
+ }
+
+ # correct order!
+ for (sort { $a->[2] cmp $b->[2] } values %nodes) {
+ $DEBUG && printf "ADD: %d\n", $_->[0];
+ $_->[2] =~ s#^([0-9]{4})([0-9]{2}).+#$1==0?'N/A':$1==9999?'TBA':(($2&&$2>0?($Time::CTime::MoY[$2-1].' '):'').$1)#e;
+ $g->add_node($_->[0], %nodes_all, URL => '/v'.$_->[0], tooltip => $_->[1], label => sprintf
+ qq|<<TABLE CELLSPACING="0" CELLPADDING="1" BORDER="0" CELLBORDER="1" BGCOLOR="#f0f0f0">
+ <TR><TD COLSPAN="2" ALIGN="CENTER" CELLPADDING="3"><FONT POINT-SIZE="$fsize[2]"> %s </FONT></TD></TR>
+ <TR><TD> %s </TD><TD> %s </TD></TR>
+ </TABLE>>|,
+ $_->[1], $_->[2], $_->[3]);
+ }
+
+ # make sure to sort the edges on node release dates
+ my @rel = map { [ split(/-/, $_), $rels{$_} ] } keys %rels;
+ for (sort { ($ids{$a->[0]}gt$ids{$a->[1]}?$ids{$a->[1]}:$ids{$a->[0]})
+ cmp ($ids{$b->[0]}gt$ids{$b->[1]}?$ids{$b->[1]}:$ids{$b->[0]}) } @rel) {
+
+ if($ids{$_->[1]} gt $ids{$_->[0]}) {
+ ($_->[0], $_->[1]) = ($_->[1], $_->[0]);
+ $_->[2] = reverseRel($_->[2]);
+ }
+ $g->add_edge($_->[1] => $_->[0], %{$edge_rel[$_->[2]]});
+ $DEBUG && printf "ADD %d -> %d\n", $_->[1], $_->[0];
+ }
+
+
+ $DEBUG && print "IMAGE\n";
+
+ # get a new number
+ my $gid = $sql->prepare("SELECT nextval('relgraph_seq')");
+ $gid->execute;
+ $gid = $gid->fetchrow_arrayref->[0];
+ my $fn = sprintf '/%02d/%d.', $gid % 50, $gid;
+
+ # save the image & image map
+ my $d = $g->as_gif($destdir.$fn.'gif');
+ chmod 0666, $destdir.$fn.'gif';
+
+ $DEBUG && print "CMAP\n";
+ open my $F, '>', $datdir.$fn.'cmap' or die $!;
+ print $F '<!-- V:'.join(',',keys %nodes)." -->\n";
+ ($d = $g->as_cmapx) =~ s/(id|name)="[^"]+"/$1="rgraph"/g;
+ print $F $d;
+ close $F;
+ chmod 0666, $datdir.$fn.'cmap';
+
+ $DEBUG && print "UPDATE\n";
+ # update the VNs
+ $sql->do(sprintf q|
+ UPDATE vn
+ SET rgraph = %d
+ WHERE id IN(%s)|,
+ $gid, join(',', keys %ids));
+ $DEBUG && print "FIN\n";
+
+ push @done, keys %ids;
+ return 1;
+}
+
+
+sub getRel { # gobj, vid
+ my($g, $id) = @_;
+ $ids{$id} = 0; # false but defined
+ $DEBUG && printf "GET: %d\n", $id;
+ my $s = $sql->prepare(q|
+ SELECT vr1.vid AS vid1, r.vid2, r.relation, vr1.title AS title1, vr2.title AS title2,
+ v1.c_released AS date1, v2.c_released AS date2, v1.c_languages AS lang1, v2.c_languages AS lang2
+ FROM vn_relations r
+ JOIN vn_rev vr1 ON r.vid1 = vr1.id
+ JOIN vn v1 ON v1.id = vr1.vid
+ JOIN vn v2 ON r.vid2 = v2.id
+ JOIN vn_rev vr2 ON v2.id = vr2.vid
+ WHERE (r.vid2 = ? OR vr1.vid = ?) AND v1.latest = vr1.id|
+ );
+ $s->execute($id, $id);
+ for my $r (@{$s->fetchall_arrayref({})}) {
+ if($r->{vid1} < $r->{vid2}) {
+ $rels{$r->{vid1}.'-'.$r->{vid2}} = reverseRel($r->{relation});
+ } else {
+ $rels{$r->{vid2}.'-'.$r->{vid1}} = $r->{relation};
+ }
+
+ for (1,2) {
+ my($cid, $title, $date, $lang) = ($r->{'vid'.$_}, $r->{'title'.$_}, $r->{'date'.$_}, $r->{'lang'.$_});
+ $title = unidecode($title);
+ $title = substr($title, 0, 27).'...' if length($title) > 30;
+ $title =~ s/&/&amp;/g;
+ $date = sprintf('%08d', $date);
+ $nodes{$cid} = [ $cid, $title, $date, $lang ];
+
+ if(!defined $ids{$cid}) {
+ $ids{$cid} = $date;
+ getRel($g, $cid) if $id != $cid;
+ }
+ }
+ }
+}
+
+sub reverseRel { # rel
+ return $VNDB::VRELW->{$_[0]} ? $_[0]-1 : $VNDB::VRELW->{$_[0]+1} ? $_[0]+1 : $_[0];
+}
+
+
+
+if(@ARGV) {
+ #print join('-',@ARGV);
+ createGraph($_) for (@ARGV);
+ $sql->commit;
+} else {
+ require Time::HiRes;
+ my $S = [ Time::HiRes::gettimeofday() ];
+
+ # regenerate all
+ my $s = $sql->prepare(q|SELECT id FROM vn|);
+ $s->execute();
+ my $i = $s->fetchall_arrayref([]);
+ for my $id (@$i) {
+ print "Processed $id->[0]\n" if createGraph($id->[0]);
+ }
+
+ # delete unused
+ # opendir(my $D, $destdir) || die $!;
+ # for (readdir($D)) {
+ # next if !/^([0-9a-fA-F]{32})\.gif$/;
+ # my $s = $sql->prepare(q|SELECT 1 AS yes FROM vn WHERE rgraph = DECODE(?, 'hex')|);
+ # $s->execute($1);
+ # if(!$s->fetchall_arrayref({})->[0]{yes}) {
+ # printf "Deleting %s\n", $1;
+ # unlink "$datdir/$1.cmap" or die $!;
+ # unlink "$destdir/$1.gif" or die $!;
+ # }
+ # }
+ # closedir($D);
+
+ $sql->commit;
+
+ printf "Done in %.3f s\n", Time::HiRes::tv_interval($S);
+}
+
diff --git a/util/sitemap.pl b/util/sitemap.pl
new file mode 100755
index 00000000..64b0957a
--- /dev/null
+++ b/util/sitemap.pl
@@ -0,0 +1,94 @@
+#!/usr/bin/perl
+
+my $sitemapfile = '/www/vndb/www/sitemap.xml.gz';
+my $baseurl = 'http://vndb.org';
+my %chfr = qw( a always h hourly d daily w weekly m monthly y yearly n never );
+
+
+# the code
+use strict;
+use warnings;
+use DBI;
+use POSIX; # for ceil();
+use XML::Writer;
+use PerlIO::gzip;
+use DateTime;
+
+my $sql = DBI->connect('dbi:Pg:dbname=vndb', 'vndb', 'passwd',
+ { RaiseError => 1, PrintError => 0, AutoCommit => 1, pg_enable_utf8 => 1 });
+
+my $urls = 0;
+my $x;
+
+sitemap();
+
+sub sitemap {
+ print "Creating sitemap...\n";
+ # open file and start writing
+ open(my $IO, '>:gzip', $sitemapfile) || die $1;
+ $x = new XML::Writer(OUTPUT => $IO, ENCODING => 'UTF-8', DATA_MODE => 1, DATA_INDENT => 1);
+ $x->xmlDecl();
+ $x->comment(q|NOTE: All URL's that require you to login or that may contain usernames are left out.|);
+ $x->startTag('urlset', xmlns => 'http://www.sitemaps.org/schemas/sitemap/0.9');
+
+ # some default pages
+ _sm_add(@$_) foreach (
+ [ '/', 'd' ],
+ [ '/faq', 'm' ],
+ );
+
+ # some browse pages
+ _sm_add('/v/'.$_, 'w') for ('a'..'z', 'all', 'cat');
+ _sm_add('/p/'.$_, 'w') for ('a'..'z', 'all');
+
+ # visual novels
+ my $q = $sql->prepare(q|
+ SELECT v.id, c.added, v.rgraph
+ FROM vn v
+ JOIN vn_rev vr ON vr.id = v.latest
+ JOIN changes c ON vr.id = c.id
+ |); $q->execute;
+ while($_ = $q->fetchrow_arrayref) {
+ _sm_add('/v'.$_->[0], 'w', $_->[1], 0.7);
+# _sm_add('/v'.$_->[0].'/stats', 'w');
+ _sm_add('/v'.$_->[0].'/rg', 'w', $_->[1]) if $_->[2];
+ }
+
+ # producers
+ $q = $sql->prepare(q|
+ SELECT p.id, c.added
+ FROM producers p
+ JOIN producers_rev pr ON pr.id = p.latest
+ JOIN changes c ON c.id = pr.id
+ |); $q->execute;
+ _sm_add('/p'.$_->[0], 'w', $_->[1]) while $_ = $q->fetchrow_arrayref;
+
+ # releases
+ $q = $sql->prepare(q|
+ SELECT r.id, c.added
+ FROM releases r
+ JOIN releases_rev rr ON rr.id = r.latest
+ JOIN changes c ON c.id = rr.id
+ |); $q->execute;
+ _sm_add('/r'.$_->[0], 'w', $_->[1], 0.3) while $_ = $q->fetchrow_arrayref;
+
+
+ # and stop writing
+ $x->endTag('urlset');
+ $x->end();
+ close($IO);
+ printf "Sitemap created, %d urls added\n", $urls;
+}
+
+
+
+sub _sm_add {
+ my($loc, $cf, $lastmod, $pri) = @_;
+ $x->startTag('url');
+ $x->dataElement('loc', $baseurl . $loc);
+ $x->dataElement('changefreq', $chfr{$cf}?$chfr{$cf}:$cf) if defined $cf;
+ $x->dataElement('lastmod', DateTime->from_epoch(epoch => $lastmod)->ymd) if defined $lastmod;
+ $x->dataElement('priority', $pri) if defined $pri;
+ $x->endTag('url');
+ $urls++;
+}
diff --git a/util/updates/update_1.1.pl b/util/updates/update_1.1.pl
new file mode 100755
index 00000000..c01e6625
--- /dev/null
+++ b/util/updates/update_1.1.pl
@@ -0,0 +1,18 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use DBI;
+
+require '../lib/global.pl';
+
+my $sql = DBI->connect('dbi:Pg:dbname=vndb', 'vndb', 'passwd',
+ { RaiseError => 1, PrintError => 0, AutoCommit => 1, pg_enable_utf8 => 1 });
+
+my $q = $sql->prepare('SELECT id, rel_old, language FROM vnr'); $q->execute;
+for (@{$q->fetchall_arrayref({})}) {
+ my $rel = sprintf !$_->{rel_old} ? 'Original release' :
+ $_->{rel_old} == 1 ? '%s translation' : '%s rerelease', $VNDB::LANG->{$_->{language}};
+ $sql->do('UPDATE vnr SET relation = ? WHERE id = ?', undef, $rel, $_->{id});
+}
+$sql->do('ALTER TABLE vnr DROP COLUMN rel_old');
diff --git a/util/updates/update_1.1.sql b/util/updates/update_1.1.sql
new file mode 100644
index 00000000..0538e3d5
--- /dev/null
+++ b/util/updates/update_1.1.sql
@@ -0,0 +1,13 @@
+ALTER TABLE users ADD COLUMN pvotes smallint NOT NULL DEFAULT 1;
+ALTER TABLE users ADD COLUMN pfind smallint NOT NULL DEFAULT 1;
+
+UPDATE users
+ SET registered = 1191004915
+ WHERE registered = 0;
+UPDATE votes
+ SET date = 1191004915
+ WHERE date = 0;
+
+ALTER TABLE vnr RENAME COLUMN relation TO rel_old;
+ALTER TABLE vnr ADD COLUMN relation varchar(32) NOT NULL DEFAULT 'Original release';
+
diff --git a/util/updates/update_1.10.sql b/util/updates/update_1.10.sql
new file mode 100644
index 00000000..d68b0456
--- /dev/null
+++ b/util/updates/update_1.10.sql
@@ -0,0 +1,92 @@
+
+-- seperate releases_vn table
+CREATE TABLE releases_vn (
+ rid integer DEFAULT 0 NOT NULL,
+ vid integer DEFAULT 0 NOT NULL,
+ PRIMARY KEY(rid, vid)
+) WITHOUT OIDS;
+
+INSERT INTO releases_vn
+ SELECT rr.id AS rid, r.vid AS vid
+ FROM releases_rev rr
+ JOIN releases r ON rr.rid = r.id;
+
+ALTER TABLE releases DROP COLUMN vid;
+
+
+ALTER TABLE releases_rev ALTER COLUMN notes TYPE text;
+UPDATE producers_rev SET "desc" = '' WHERE "desc" = '0';
+
+
+
+
+-- Update rating calculation
+ALTER TABLE vn ALTER COLUMN c_votes TYPE character(10);
+ALTER TABLE vn ALTER COLUMN c_votes SET DEFAULT '00.00|0000';
+
+CREATE OR REPLACE FUNCTION calculate_rating() RETURNS void AS $$
+DECLARE
+ av RECORD;
+BEGIN
+ SELECT INTO av
+ COUNT(vote)::real / COUNT(DISTINCT vid)::real AS num_votes,
+ AVG(vote)::real AS rating
+ FROM votes;
+
+ UPDATE vn
+ SET c_votes = COALESCE((SELECT
+ TO_CHAR(CASE WHEN COUNT(uid) < 2 THEN 0 ELSE
+ ( (av.num_votes * av.rating) + SUM(vote)::real ) / (av.num_votes + COUNT(uid)::real ) END,
+ 'FM00D00'
+ )||'|'||TO_CHAR(
+ COUNT(votes.vote), 'FM0000'
+ )
+ FROM votes
+ WHERE votes.vid = vn.id
+ GROUP BY votes.vid
+ ), '00.00|0000');
+END
+$$ LANGUAGE plpgsql;
+SELECT calculate_rating();
+
+
+-- fix update_vncache
+DROP FUNCTION update_vncache(integer, integer);
+CREATE OR REPLACE FUNCTION update_vncache(id integer) RETURNS void AS $$
+DECLARE
+ w text := '';
+BEGIN
+ IF id > 0 THEN
+ w := ' WHERE id = '||id;
+ END IF;
+ EXECUTE 'UPDATE vn SET
+ c_released = COALESCE((SELECT
+ SUBSTRING(COALESCE(MIN(rr1.released), ''0000-00'') from 1 for 7)
+ FROM releases_rev rr1
+ JOIN releases r1 ON rr1.id = r1.latest
+ JOIN releases_vn rv1 ON rr1.id = rv1.rid
+ WHERE rv1.vid = vn.id
+ AND rr1.type <> 2
+ GROUP BY rv1.vid
+ ), ''0000-00''),
+ c_languages = COALESCE(ARRAY_TO_STRING(ARRAY(
+ SELECT language
+ FROM releases_rev rr2
+ JOIN releases r2 ON rr2.id = r2.latest
+ JOIN releases_vn rv2 ON rr2.id = rv2.rid
+ WHERE rv2.vid = vn.id
+ AND rr2.type <> 2
+ AND rr2.released <= ''today''::date
+ GROUP BY rr2.language
+ ORDER BY rr2.language
+ ), ''/''), '''')
+ '||w;
+END;
+$$ LANGUAGE plpgsql;
+SELECT update_vncache(0);
+
+
+
+-- Add comments field to vnlists
+ALTER TABLE vnlists ADD COLUMN comments character varying(500) NOT NULL DEFAULT '';
+
diff --git a/util/updates/update_1.11.sql b/util/updates/update_1.11.sql
new file mode 100644
index 00000000..63a822a5
--- /dev/null
+++ b/util/updates/update_1.11.sql
@@ -0,0 +1,4 @@
+
+UPDATE vn_rev SET categories = categories << 6;
+
+ALTER TABLE vn_rev ADD COLUMN l_vnn integer NOT NULL DEFAULT 0;
diff --git a/util/updates/update_1.12.sql b/util/updates/update_1.12.sql
new file mode 100644
index 00000000..30238c6d
--- /dev/null
+++ b/util/updates/update_1.12.sql
@@ -0,0 +1,34 @@
+
+UPDATE vn_rev SET categories = categories << 2;
+
+DELETE FROM releases_vn rv WHERE NOT EXISTS(SELECT 1 FROM releases_rev rr WHERE rr.id = rv.rid);
+
+
+-- FOREIGN KEY CHECKING!
+ALTER TABLE releases_rev ADD FOREIGN KEY (id) REFERENCES changes (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_rev ADD FOREIGN KEY (rid) REFERENCES releases (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+--ALTER TABLE releases_rev ADD FOREIGN KEY (id) REFERENCES releases_vn (rid) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases ADD FOREIGN KEY (latest) REFERENCES releases_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_vn ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_vn ADD FOREIGN KEY (vid) REFERENCES vn (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_platforms ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_media ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_producers ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_producers ADD FOREIGN KEY (pid) REFERENCES producers (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE vn_rev ADD FOREIGN KEY (id) REFERENCES changes (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn_rev ADD FOREIGN KEY (vid) REFERENCES vn (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn ADD FOREIGN KEY (latest) REFERENCES vn_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid1) REFERENCES vn_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid2) REFERENCES vn (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE producers_rev ADD FOREIGN KEY (id) REFERENCES changes (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE producers_rev ADD FOREIGN KEY (pid) REFERENCES producers (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE producers ADD FOREIGN KEY (latest) REFERENCES producers_rev (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE changes ADD FOREIGN KEY (requester) REFERENCES users (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE votes ADD FOREIGN KEY (uid) REFERENCES users (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE votes ADD FOREIGN KEY (vid) REFERENCES vn (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vnlists ADD FOREIGN KEY (uid) REFERENCES users (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vnlists ADD FOREIGN KEY (vid) REFERENCES vn (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
+
diff --git a/util/updates/update_1.13.sql b/util/updates/update_1.13.sql
new file mode 100644
index 00000000..48a347e2
--- /dev/null
+++ b/util/updates/update_1.13.sql
@@ -0,0 +1,229 @@
+
+
+
+-- why did we still have this column?
+ALTER TABLE releases_rev DROP COLUMN relation;
+
+
+
+
+-- fix update_prev
+CREATE OR REPLACE FUNCTION update_prev(tbl text, ids text) RETURNS void AS $$
+DECLARE
+ r RECORD;
+ r2 RECORD;
+ i integer;
+ t text;
+ e text;
+BEGIN
+ SELECT INTO t SUBSTRING(tbl, 1, 1);
+ e := '';
+ IF ids <> '' THEN
+ e := ' WHERE id IN('||ids||')';
+ END IF;
+ FOR r IN EXECUTE 'SELECT id FROM '||tbl||e LOOP
+ i := 0;
+ FOR r2 IN EXECUTE 'SELECT id FROM '||tbl||'_rev WHERE '||t||'id = '||r.id||' ORDER BY id ASC' LOOP
+ UPDATE changes SET prev = i WHERE id = r2.id;
+ i := r2.id;
+ END LOOP;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+SELECT update_prev('vn',''), update_prev('releases',''), update_prev('producers','');
+
+
+
+
+-- change votes treshold to 3
+CREATE OR REPLACE FUNCTION calculate_rating() RETURNS void AS $$
+DECLARE
+ av RECORD;
+BEGIN
+ SELECT INTO av
+ COUNT(vote)::real / COUNT(DISTINCT vid)::real AS num_votes,
+ AVG(vote)::real AS rating
+ FROM votes;
+
+ UPDATE vn
+ SET c_votes = COALESCE((SELECT
+ TO_CHAR(CASE WHEN COUNT(uid) < 3 THEN 0 ELSE
+ ( (av.num_votes * av.rating) + SUM(vote)::real ) / (av.num_votes + COUNT(uid)::real ) END,
+ 'FM00D00'
+ )||'|'||TO_CHAR(
+ COUNT(votes.vote), 'FM0000'
+ )
+ FROM votes
+ WHERE votes.vid = vn.id
+ GROUP BY votes.vid
+ ), '00.00|0000');
+END
+$$ LANGUAGE plpgsql;
+SELECT calculate_rating();
+
+
+
+
+-- store release dates as integers
+ALTER TABLE releases_rev ALTER COLUMN released TYPE integer USING REPLACE(released, '-', '')::integer;
+UPDATE releases_rev SET released = 0 WHERE released IS NULL;
+ALTER TABLE releases_rev ALTER COLUMN released SET NOT NULL;
+
+ALTER TABLE vn ALTER COLUMN c_released SET DEFAULT 0;
+ALTER TABLE vn ALTER COLUMN c_released TYPE integer USING 0;
+CREATE OR REPLACE FUNCTION update_vncache(id integer) RETURNS void AS $$
+DECLARE
+ w text := '';
+BEGIN
+ IF id > 0 THEN
+ w := ' WHERE id = '||id;
+ END IF;
+ EXECUTE 'UPDATE vn SET
+ c_released = COALESCE((SELECT
+ MIN(rr1.released)
+ FROM releases_rev rr1
+ JOIN releases r1 ON rr1.id = r1.latest
+ JOIN releases_vn rv1 ON rr1.id = rv1.rid
+ WHERE rv1.vid = vn.id
+ AND rr1.type <> 2
+ AND rr1.released <> 0
+ GROUP BY rv1.vid
+ ), 0),
+ c_languages = COALESCE(ARRAY_TO_STRING(ARRAY(
+ SELECT language
+ FROM releases_rev rr2
+ JOIN releases r2 ON rr2.id = r2.latest
+ JOIN releases_vn rv2 ON rr2.id = rv2.rid
+ WHERE rv2.vid = vn.id
+ AND rr2.type <> 2
+ AND rr2.released <= TO_CHAR(''today''::timestamp, ''YYYYMMDD'')::integer
+ GROUP BY rr2.language
+ ORDER BY rr2.language
+ ), ''/''), '''')
+ '||w;
+END;
+$$ LANGUAGE plpgsql;
+SELECT update_vncache(0);
+
+
+
+
+-- Rewrite category system
+CREATE TABLE vn_categories (
+ vid integer NOT NULL DEFAULT 0,
+ cat char(3) NOT NULL DEFAULT '',
+ lvl smallint NOT NULL DEFAULT 3,
+ PRIMARY KEY(vid, cat)
+) WITHOUT OIDS;
+
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'gaa', 1 FROM vn_rev WHERE (categories & (1<<0)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'gab', 1 FROM vn_rev WHERE (categories & (1<<1)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'gac', 3 FROM vn_rev WHERE (categories & (1<<2)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'grp', 3 FROM vn_rev WHERE (categories & (1<<3)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'gst', 3 FROM vn_rev WHERE (categories & (1<<4)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'gsi', 3 FROM vn_rev WHERE (categories & (1<<5)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'pli', 1 FROM vn_rev WHERE (categories & (1<<6)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'pbr', 1 FROM vn_rev WHERE (categories & (1<<7)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'eac', 3 FROM vn_rev WHERE (categories & (1<<8)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'eco', 3 FROM vn_rev WHERE (categories & (1<<9)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'edr', 3 FROM vn_rev WHERE (categories & (1<<10)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'efa', 3 FROM vn_rev WHERE (categories & (1<<11)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'eho', 3 FROM vn_rev WHERE (categories & (1<<12)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'emy', 3 FROM vn_rev WHERE (categories & (1<<13)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'ero', 3 FROM vn_rev WHERE (categories & (1<<14)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'esf', 3 FROM vn_rev WHERE (categories & (1<<15)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'esj', 3 FROM vn_rev WHERE (categories & (1<<16)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'esn', 3 FROM vn_rev WHERE (categories & (1<<17)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'tfu', 3 FROM vn_rev WHERE (categories & (1<<18)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'tpa', 3 FROM vn_rev WHERE (categories & (1<<19)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'tpr', 3 FROM vn_rev WHERE (categories & (1<<20)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'lea', 3 FROM vn_rev WHERE (categories & (1<<21)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'lfa', 3 FROM vn_rev WHERE (categories & (1<<22)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'lsp', 3 FROM vn_rev WHERE (categories & (1<<23)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'saa', 3 FROM vn_rev WHERE (categories & (1<<24)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'sbe', 3 FROM vn_rev WHERE (categories & (1<<25)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'sin', 3 FROM vn_rev WHERE (categories & (1<<26)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'slo', 3 FROM vn_rev WHERE (categories & (1<<27)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'ssh', 3 FROM vn_rev WHERE (categories & (1<<28)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'sya', 3 FROM vn_rev WHERE (categories & (1<<29)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'syu', 3 FROM vn_rev WHERE (categories & (1<<30)) > 0;
+INSERT INTO vn_categories (vid, cat, lvl) SELECT id, 'sra', 3 FROM vn_rev WHERE (categories & (1<<31)) < 0; -- MSB, mind you!
+ALTER TABLE vn_rev DROP COLUMN categories;
+
+
+
+-- Remove all previously defined constraints
+ALTER TABLE releases_rev DROP CONSTRAINT releases_rev_id_fkey;
+ALTER TABLE releases_rev DROP CONSTRAINT releases_rev_rid_fkey;
+ALTER TABLE releases DROP CONSTRAINT releases_latest_fkey;
+ALTER TABLE releases_vn DROP CONSTRAINT releases_vn_rid_fkey;
+ALTER TABLE releases_vn DROP CONSTRAINT releases_vn_vid_fkey;
+ALTER TABLE releases_platforms DROP CONSTRAINT releases_platforms_rid_fkey;
+ALTER TABLE releases_media DROP CONSTRAINT releases_media_rid_fkey;
+ALTER TABLE releases_producers DROP CONSTRAINT releases_producers_rid_fkey;
+ALTER TABLE releases_producers DROP CONSTRAINT releases_producers_pid_fkey;
+
+ALTER TABLE vn_rev DROP CONSTRAINT vn_rev_id_fkey;
+ALTER TABLE vn_rev DROP CONSTRAINT vn_rev_vid_fkey;
+ALTER TABLE vn DROP CONSTRAINT vn_latest_fkey;
+ALTER TABLE vn_relations DROP CONSTRAINT vn_relations_vid1_fkey;
+ALTER TABLE vn_relations DROP CONSTRAINT vn_relations_vid2_fkey;
+
+ALTER TABLE changes DROP CONSTRAINT changes_requester_fkey;
+ALTER TABLE votes DROP CONSTRAINT votes_uid_fkey;
+ALTER TABLE votes DROP CONSTRAINT votes_vid_fkey;
+ALTER TABLE vnlists DROP CONSTRAINT vnlists_uid_fkey;
+ALTER TABLE vnlists DROP CONSTRAINT vnlists_vid_fkey;
+
+
+-- And re-add them... LOLZ
+ALTER TABLE releases_rev ADD FOREIGN KEY (id) REFERENCES changes (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_rev ADD FOREIGN KEY (rid) REFERENCES releases (id) DEFERRABLE INITIALLY DEFERRED;
+--ALTER TABLE releases_rev ADD FOREIGN KEY (id, NULL) REFERENCES releases_vn (rid, vid) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases ADD FOREIGN KEY (latest) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_vn ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_vn ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_platforms ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_media ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_producers ADD FOREIGN KEY (rid) REFERENCES releases_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE releases_producers ADD FOREIGN KEY (pid) REFERENCES producers (id) DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE vn_rev ADD FOREIGN KEY (id) REFERENCES changes (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn_rev ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn ADD FOREIGN KEY (latest) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn_categories ADD FOREIGN KEY (vid) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid1) REFERENCES vn_rev (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vn_relations ADD FOREIGN KEY (vid2) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE producers_rev ADD FOREIGN KEY (id) REFERENCES changes (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE producers_rev ADD FOREIGN KEY (pid) REFERENCES producers (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE producers ADD FOREIGN KEY (latest) REFERENCES producers_rev (id) DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE changes ADD FOREIGN KEY (requester) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;-- ON DELETE SET DEFAULT
+ALTER TABLE votes ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE votes ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vnlists ADD FOREIGN KEY (uid) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE vnlists ADD FOREIGN KEY (vid) REFERENCES vn (id) DEFERRABLE INITIALLY DEFERRED;
+
+
+--ALTER TABLE releases_rev ADD COLUMN ref_vid_hack integer NULL DEFAULT NULL;
+--ALTER TABLE releases_rev ADD FOREIGN KEY (id, ref_vid_hack) REFERENCES releases_vn (rid, vid) ON DELETE CASCADE;
+
+-- TODO:
+-- - make sure that changes.id should always refer to a row in *_rev
+-- - make sure that there is always at least one row in releases_vn for every releases_rev
+
+-- deletion of items in *_rev should trigger deletion in changes
+--CREATE OR REPLACE FUNCTION changes_reference_del() RETURNS trigger AS $$
+--BEGIN
+-- DELETE FROM changes WHERE id = OLD.id;
+--END
+--$$ LANGUAGE PLPGSQL;
+
+--CREATE TRIGGER vn_rev_cdel AFTER DELETE ON vn_rev FOR EACH ROW EXECUTE PROCEDURE changes_reference_del();
+--CREATE TRIGGER releases_rev_cdel AFTER DELETE ON releases_rev FOR EACH ROW EXECUTE PROCEDURE changes_reference_del();
+--CREATE TRIGGER producers_rev_cdel AFTER DELETE ON producers_rev FOR EACH ROW EXECUTE PROCEDURE changes_reference_del();
+
+
+
+
diff --git a/util/updates/update_1.14.pl b/util/updates/update_1.14.pl
new file mode 100755
index 00000000..1bfc517a
--- /dev/null
+++ b/util/updates/update_1.14.pl
@@ -0,0 +1,57 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use File::Path;
+use DBI;
+
+# script assumes:
+# /static has been created
+# /www/files has already been moved
+chdir '/www/vndb';
+
+
+# run the usual SQL update script
+system('psql -U vndb < util/updates/update_1.14.sql');
+
+# fix directories
+rmtree('data/rg');
+rmtree('www/rg');
+
+mkdir 'data/rg';
+mkdir 'static/cv';
+mkdir 'static/rg';
+chmod 0755, qw|data/rg static/cv static/rg|;
+
+for (0..49) {
+ $_ = sprintf "%02d",$_;
+ mkdir "data/rg/$_";
+ mkdir "static/rg/$_";
+ mkdir "static/cv/$_";
+ chmod 0777, "data/rg/$_", "static/rg/$_", "static/cv/$_";
+}
+
+
+# rename relation graphs
+system('util/relgraph.pl');
+
+
+# rename cover images
+my $sql = DBI->connect('dbi:Pg:dbname=vndb', 'vndb', 'passwd',
+ { RaiseError => 0, PrintError => 1, AutoCommit => 1, pg_enable_utf8 => 1 });
+$sql->do('CREATE SEQUENCE covers_seq');
+$sql->do('ALTER TABLE vn_rev ADD COLUMN image_id integer NOT NULL DEFAULT 0');
+my $q = $sql->prepare('SELECT DISTINCT ENCODE(image,\'hex\') FROM vn_rev WHERE image <> \'\'');
+$q->execute();
+for (@{$q->fetchall_arrayref([])}) {
+ $q = $sql->prepare('SELECT nextval(\'covers_seq\')');
+ $q->execute();
+ my($id) = $q->fetchrow_array();
+ rename
+ sprintf('www/img/%s/%s.jpg', substr($_->[0],0,1), $_->[0]),
+ sprintf('static/cv/%02d/%d.jpg', $id%50, $id);
+ $sql->do('UPDATE vn_rev SET image_id = ? WHERE image = DECODE(\''.$_->[0].'\', \'hex\')', undef, $id);
+}
+$sql->do('ALTER TABLE vn_rev DROP COLUMN image');
+$sql->do('ALTER TABLE vn_rev RENAME COLUMN image_id TO image');
+
diff --git a/util/updates/update_1.14.sql b/util/updates/update_1.14.sql
new file mode 100644
index 00000000..70ae8ef0
--- /dev/null
+++ b/util/updates/update_1.14.sql
@@ -0,0 +1,76 @@
+
+
+-- drop get_new_id()
+CREATE SEQUENCE vn_id_seq OWNED BY vn.id;
+SELECT setval('vn_id_seq', get_new_id('vn')-1);
+ALTER TABLE vn ALTER COLUMN id SET DEFAULT nextval('vn_id_seq');
+
+CREATE SEQUENCE releases_id_seq OWNED BY releases.id;
+SELECT setval('releases_id_seq', get_new_id('releases')-1);
+ALTER TABLE releases ALTER COLUMN id SET DEFAULT nextval('releases_id_seq');
+
+CREATE SEQUENCE producers_id_seq OWNED BY producers.id;
+SELECT setval('producers_id_seq', get_new_id('producers')-1);
+ALTER TABLE producers ALTER COLUMN id SET DEFAULT nextval('producers_id_seq');
+
+DROP FUNCTION get_new_id(text);
+
+
+
+-- relation graphs get ID numbers
+CREATE SEQUENCE relgraph_seq;
+ALTER TABLE vn ALTER COLUMN rgraph DROP NOT NULL;
+ALTER TABLE vn ALTER COLUMN rgraph DROP DEFAULT;
+ALTER TABLE vn ALTER COLUMN rgraph TYPE integer USING 0;
+ALTER TABLE vn ALTER COLUMN rgraph SET DEFAULT 0;
+ALTER TABLE vn ALTER COLUMN rgraph SET NOT NULL;
+
+
+-- cover images get ID numbers as well
+-- (handled in update_1.14.pl)
+
+
+
+-- 'hidden' flag to all items in the DB
+ALTER TABLE vn ADD COLUMN hidden smallint NOT NULL DEFAULT 0;
+ALTER TABLE producers ADD COLUMN hidden smallint NOT NULL DEFAULT 0;
+ALTER TABLE releases ADD COLUMN hidden smallint NOT NULL DEFAULT 0;
+
+
+-- update update_vncache to handle the hidden flag
+CREATE OR REPLACE FUNCTION update_vncache(id integer) RETURNS void AS $$
+DECLARE
+ w text := '';
+BEGIN
+ IF id > 0 THEN
+ w := ' WHERE id = '||id;
+ END IF;
+ EXECUTE 'UPDATE vn SET
+ c_released = COALESCE((SELECT
+ MIN(rr1.released)
+ FROM releases_rev rr1
+ JOIN releases r1 ON rr1.id = r1.latest
+ JOIN releases_vn rv1 ON rr1.id = rv1.rid
+ WHERE rv1.vid = vn.id
+ AND rr1.type <> 2
+ AND r1.hidden = 0
+ AND rr1.released <> 0
+ GROUP BY rv1.vid
+ ), 0),
+ c_languages = COALESCE(ARRAY_TO_STRING(ARRAY(
+ SELECT language
+ FROM releases_rev rr2
+ JOIN releases r2 ON rr2.id = r2.latest
+ JOIN releases_vn rv2 ON rr2.id = rv2.rid
+ WHERE rv2.vid = vn.id
+ AND rr2.type <> 2
+ AND rr2.released <= TO_CHAR(''today''::timestamp, ''YYYYMMDD'')::integer
+ AND r2.hidden = 0
+ GROUP BY rr2.language
+ ORDER BY rr2.language
+ ), ''/''), '''')
+ '||w;
+END;
+$$ LANGUAGE plpgsql;
+SELECT update_vncache(0);
+
diff --git a/util/updates/update_1.2.sql b/util/updates/update_1.2.sql
new file mode 100644
index 00000000..c1b48b84
--- /dev/null
+++ b/util/updates/update_1.2.sql
@@ -0,0 +1,9 @@
+CREATE TABLE vnlists (
+ uid integer NOT NULL DEFAULT 0,
+ vid integer NOT NULL DEFAULT 0,
+ status smallint NOT NULL DEFAULT 0,
+ added bigint NOT NULL DEFAULT 0,
+ PRIMARY KEY(uid, vid)
+) WITHOUT OIDS;
+
+ALTER TABlE users ADD COLUMN plist smallint NOT NULL DEFAULT 1;
diff --git a/util/updates/update_1.4.sql b/util/updates/update_1.4.sql
new file mode 100644
index 00000000..783c0029
--- /dev/null
+++ b/util/updates/update_1.4.sql
@@ -0,0 +1,37 @@
+UPDATE vn_categories SET category = 'aaa' WHERE category = 'ami';
+
+--CREATE TABLE changes (
+-- id SERIAL NOT NULL PRIMARY KEY,
+-- "type" smallint DEFAULT 0 NOT NULL,
+-- rel integer DEFAULT 0 NOT NULL,
+-- vrel integer DEFAULT 0 NOT NULL,
+-- uid integer DEFAULT 0 NOT NULL,
+-- status smallint DEFAULT 0 NOT NULL,
+-- added bigint DEFAULT 0 NOT NULL,
+-- lastmod bigint DEFAULT 0 NOT NULL,
+-- changes bytea DEFAULT ''::bytea NOT NULL,
+-- comments text DEFAULT '' NOT NULL
+--);
+
+
+CREATE LANGUAGE plpgsql;
+CREATE OR REPLACE FUNCTION get_new_id() RETURNS integer AS $$
+DECLARE
+ i integer := 1;
+ r RECORD;
+BEGIN
+ FOR r IN SELECT id FROM vn ORDER BY id ASC LOOP
+ IF i <> r.id THEN
+ EXIT;
+ END IF;
+ i := i+1;
+ END LOOP;
+ RETURN i;
+END;
+$$ LANGUAGE plpgsql;
+
+ALTER TABLE vn ALTER COLUMN id SET DEFAULT get_new_id();
+DROP SEQUENCE vn_id_seq;
+
+
+ALTER TABLE vnr ADD COLUMN notes varchar(250) DEFAULT '';
diff --git a/util/updates/update_1.5.sql b/util/updates/update_1.5.sql
new file mode 100644
index 00000000..970aa0e1
--- /dev/null
+++ b/util/updates/update_1.5.sql
@@ -0,0 +1,10 @@
+CREATE TABLE vn_relations (
+ vid1 integer NOT NULL,
+ vid2 integer NOT NULL,
+ relation smallint NOT NULL,
+ lastmod bigint NOT NULL,
+ PRIMARY KEY(vid1, vid2)
+);
+
+ALTER TABLE vn ADD COLUMN img_nsfw smallint NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN pign_nsfw smallint NOT NULL DEFAULT 0;
diff --git a/util/updates/update_1.6.sql b/util/updates/update_1.6.sql
new file mode 100644
index 00000000..a1c8ea23
--- /dev/null
+++ b/util/updates/update_1.6.sql
@@ -0,0 +1,21 @@
+ALTER TABLE vnr DROP COLUMN rel_old;
+ALTER TABLE vnr ALTER COLUMN released DROP NOT NULL;
+ALTER TABLE vnr ALTER COLUMN released SET DEFAULT NULL;
+UPDATE vnr SET released = NULL WHERE released = '0000-00-00';
+
+ALTER TABLE vn RENAME COLUMN c_years TO c_released;
+UPDATE vn SET c_released = '0000-00';
+ALTER TABLE vn ALTER COLUMN c_released SET DEFAULT '0000-00';
+ALTER TABLE vn ALTER COLUMN c_released TYPE character(7);
+UPDATE vn SET
+ c_released = COALESCE((SELECT
+ SUBSTRING(COALESCE(MIN(released), '0000-00') from 1 for 7)
+ FROM vnr r1
+ WHERE r1.vid = vn.id
+ AND r1.r_rel = 0
+ GROUP BY r1.vid
+ ), '0000-00');
+
+
+ALTER TABLE vn_relations DROP COLUMN lastmod;
+ALTER TABLE vn ADD COLUMN rgraph bytea NOT NULL DEFAULT '';
diff --git a/util/updates/update_1.7.sql b/util/updates/update_1.7.sql
new file mode 100644
index 00000000..3ee6f5a2
--- /dev/null
+++ b/util/updates/update_1.7.sql
@@ -0,0 +1,23 @@
+ALTER TABLE producers ADD COLUMN "desc" text NOT NULL DEFAULT '';
+
+
+--ALTER TABLE users ADD COLUMN flags bit(4) NOT NULL DEFAULT B'1110';
+--UPDATE users SET flags = pvotes::bit || pfind::bit || plist::bit || pign_nsfw::bit;
+ALTER TABLE users ADD COLUMN flags integer NOT NULL DEFAULT 7;
+UPDATE users SET flags = pvotes + pfind*2 + plist*4 + pign_nsfw*8;
+
+--ALTER TABLE users DROP COLUMN pvotes;
+--ALTER TABLE users DROP COLUMN pfind;
+--ALTER TABLE users DROP COLUMN plist;
+--ALTER TABLE users DROP COLUMN pign_nsfw;
+
+
+--ALTER TABLE vn ADD COLUMN categories integer NOT NULL DEFAULT 0;
+--UPDATE vn SET categories =
+-- COALESCE((SELECT 1 FROM vn_categories WHERE vid = vn.id AND category = 'a18'), 0)
+-- +COALESCE((SELECT 2 FROM vn_categories WHERE vid = vn.id AND category = 'aaa'), 0)
+-- +COALESCE((SELECT 4 FROM vn_categories WHERE vid = vn.id AND category = 'ajo'), 0)
+-- +COALESCE((SELECT 8 FROM vn_categories WHERE vid = vn.id AND category = 'ako'), 0)
+-- +COALESCE((SELECT 16 FROM vn_categories WHERE vid = vn.id AND category = 'ase'), 0)
+-- +COALESCE((SELECT 32 FROM vn_categories WHERE vid = vn.id AND category = 'asj'), 0)
+-- +COALESCE((SELECT 64 FROM vn_categories WHERE vid = vn.id AND category = 'asn'), 0);
diff --git a/util/updates/update_1.8.sql b/util/updates/update_1.8.sql
new file mode 100644
index 00000000..b9e58ae6
--- /dev/null
+++ b/util/updates/update_1.8.sql
@@ -0,0 +1,27 @@
+ALTER TABLE vn ADD COLUMN length smallint NOT NULL DEFAULT 0;
+
+DELETE FROM vn_categories WHERE SUBSTR(category, 1, 1) = 'a';
+ALTER TABLE vnr ADD COLUMN minage smallint NOT NULL DEFAULT -1;
+
+ALTER TABLE vn ADD COLUMN l_wp varchar(150) NOT NULL DEFAULT '';
+ALTER TABLE vn ADD COLUMN l_cisv integer NOT NULL DEFAULT 0;
+
+
+UPDATE vn SET
+ c_released = COALESCE((
+ SELECT SUBSTRING(COALESCE(MIN(released), '0000-00') from 1 for 7)
+ FROM vnr r1
+ WHERE r1.vid = vn.id
+ AND r1.r_rel = 0
+ AND r1.relation NOT ILIKE 'trial'
+ GROUP BY r1.vid
+ ), '0000-00'),
+ c_languages = COALESCE(ARRAY_TO_STRING(ARRAY(
+ SELECT language
+ FROM vnr r2
+ WHERE r2.vid = vn.id
+ AND r2.r_rel = 0
+ AND r2.relation NOT ILIKE 'trial'
+ GROUP BY language
+ ORDER BY language
+ ), '/'), '');
diff --git a/util/updates/update_1.9.sql b/util/updates/update_1.9.sql
new file mode 100644
index 00000000..ad36ec2c
--- /dev/null
+++ b/util/updates/update_1.9.sql
@@ -0,0 +1,375 @@
+CREATE TABLE changes (
+ id SERIAL NOT NULL PRIMARY KEY,
+ "type" smallint NOT NULL DEFAULT 0,
+ added bigint NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()),
+ requester integer NOT NULL DEFAULT 0,
+ ip inet NOT NULL DEFAULT '0.0.0.0',
+ comments text NOT NULL DEFAULT '',
+ prev integer NOT NULL DEFAULT 0,
+ causedby integer NOT NULL DEFAULT 0
+) WITHOUT OIDS;
+
+INSERT INTO users (id, username, mail, rank, registered)
+ VALUES (1, 'multi', 'multi@vndb.org', 0, EXTRACT(EPOCH FROM NOW()));
+
+CREATE OR REPLACE FUNCTION get_new_id(tbl text) RETURNS integer AS $$
+DECLARE
+ i integer := 1;
+ r RECORD;
+BEGIN
+ FOR r IN EXECUTE 'SELECT id FROM '||tbl||' ORDER BY id ASC' LOOP
+ IF i <> r.id THEN
+ EXIT;
+ END IF;
+ i := i + 1;
+ END LOOP;
+ RETURN i;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+
+-- V i s u a l N o v e l s
+
+
+ALTER TABLE vn RENAME TO vn_old;
+ALTER TABLE vn_relations RENAME TO vn_relations_old;
+
+CREATE TABLE vn (
+ id integer NOT NULL DEFAULT get_new_id('vn') PRIMARY KEY,
+ latest integer NOT NULL DEFAULT 0,
+ locked smallint NOT NULL DEFAULT 0,
+ rgraph bytea NOT NULL DEFAULT '',
+ c_released character(7) NOT NULL DEFAULT '0000-00',
+ c_languages varchar(32) NOT NULL DEFAULT '',
+ c_votes character(9) NOT NULL DEFAULT '00.0|0000'
+) WITHOUT OIDS;
+
+CREATE TABLE vn_rev (
+ id integer NOT NULL PRIMARY KEY,
+ vid integer NOT NULL DEFAULT 0,
+ title varchar(250) NOT NULL DEFAULT '',
+ alias varchar(500) NOT NULL DEFAULT '',
+ image bytea NOT NULL DEFAULT '',
+ img_nsfw smallint NOT NULL DEFAULT 0,
+ length smallint NOT NULL DEFAULT 0,
+ "desc" text NOT NULL DEFAULT '',
+ categories integer NOT NULL DEFAULT 0,
+ l_wp varchar(150) NOT NULL DEFAULT '',
+ l_cisv integer NOT NULL DEFAULT 0
+) WITHOUT OIDS;
+
+CREATE TABLE vn_relations (
+ vid1 integer NOT NULL DEFAULT 0,
+ vid2 integer NOT NULL DEFAULT 0,
+ relation integer NOT NULL DEFAULT 0,
+ PRIMARY KEY(vid1, vid2)
+) WITHOUT OIDS;
+
+CREATE OR REPLACE FUNCTION fill_vn() RETURNS void AS $$
+DECLARE
+ r RECORD;
+ r2 RECORD;
+ i integer;
+ rel integer;
+BEGIN
+ FOR r IN SELECT * FROM vn_old ORDER BY added LOOP
+ INSERT INTO changes ("type", added, requester, comments)
+ VALUES (0, r.added, 1, 'Automated import from VNDB 1.8');
+
+ SELECT currval('changes_id_seq') INTO i;
+
+ INSERT INTO vn_rev (id, vid, title, alias, image, img_nsfw, length, "desc", l_wp, l_cisv, categories)
+ VALUES (i, r.id, r.title, r.alias, r.image, r.img_nsfw, r.length, r.desc, r.l_wp, r.l_cisv, (
+ -- ZOMFG DENORMALIZATION LOL!
+ COALESCE((SELECT 1 FROM vn_categories WHERE vid = r.id AND category = 'eac'), 0)
+ +COALESCE((SELECT 2 FROM vn_categories WHERE vid = r.id AND category = 'eco'), 0)
+ +COALESCE((SELECT 4 FROM vn_categories WHERE vid = r.id AND category = 'edr'), 0)
+ +COALESCE((SELECT 8 FROM vn_categories WHERE vid = r.id AND category = 'efa'), 0)
+ +COALESCE((SELECT 16 FROM vn_categories WHERE vid = r.id AND category = 'eho'), 0)
+ +COALESCE((SELECT 32 FROM vn_categories WHERE vid = r.id AND category = 'emy'), 0)
+ +COALESCE((SELECT 64 FROM vn_categories WHERE vid = r.id AND category = 'ero'), 0)
+ +COALESCE((SELECT 128 FROM vn_categories WHERE vid = r.id AND category = 'esf'), 0)
+ +COALESCE((SELECT 256 FROM vn_categories WHERE vid = r.id AND category = 'eja'), 0)
+ +COALESCE((SELECT 512 FROM vn_categories WHERE vid = r.id AND category = 'ena'), 0)
+ +COALESCE((SELECT 1024 FROM vn_categories WHERE vid = r.id AND category = 'tfu'), 0)
+ +COALESCE((SELECT 2048 FROM vn_categories WHERE vid = r.id AND category = 'tpa'), 0)
+ +COALESCE((SELECT 4096 FROM vn_categories WHERE vid = r.id AND category = 'tpr'), 0)
+ +COALESCE((SELECT 8192 FROM vn_categories WHERE vid = r.id AND category = 'pea'), 0)
+ +COALESCE((SELECT 16384 FROM vn_categories WHERE vid = r.id AND category = 'pfw'), 0)
+ +COALESCE((SELECT 32768 FROM vn_categories WHERE vid = r.id AND category = 'psp'), 0)
+ +COALESCE((SELECT 65536 FROM vn_categories WHERE vid = r.id AND category = 'spa'), 0)
+ +COALESCE((SELECT 131072 FROM vn_categories WHERE vid = r.id AND category = 'sbe'), 0)
+ +COALESCE((SELECT 262144 FROM vn_categories WHERE vid = r.id AND category = 'sin'), 0)
+ +COALESCE((SELECT 524288 FROM vn_categories WHERE vid = r.id AND category = 'slo'), 0)
+ +COALESCE((SELECT 1048576 FROM vn_categories WHERE vid = r.id AND category = 'scc'), 0)
+ +COALESCE((SELECT 2097152 FROM vn_categories WHERE vid = r.id AND category = 'sya'), 0)
+ +COALESCE((SELECT 4194304 FROM vn_categories WHERE vid = r.id AND category = 'syu'), 0)
+ +COALESCE((SELECT 8388608 FROM vn_categories WHERE vid = r.id AND category = 'sra'), 0)
+ ));
+
+ INSERT INTO vn (id, latest, locked, rgraph, c_released, c_languages, c_votes)
+ VALUES (r.id, i, r.locked, r.rgraph, r.c_released, r.c_languages, r.c_votes);
+
+ FOR r2 IN SELECT * FROM vn_relations_old WHERE vid2 = r.id LOOP
+ INSERT INTO vn_relations (vid1, vid2, relation)
+ VALUES(i, r2.vid1, r2.relation);
+ END LOOP;
+ FOR r2 IN SELECT * FROM vn_relations_old WHERE vid1 = r.id LOOP
+ rel := r2.relation;
+ IF rel = 0 OR rel = 6 OR rel = 8 THEN
+ rel := rel+1;
+ END IF;
+ INSERT INTO vn_relations (vid1, vid2, relation)
+ VALUES(i, r2.vid2, rel);
+ END LOOP;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+SELECT fill_vn();
+DROP FUNCTION fill_vn();
+
+
+
+
+
+-- R e l e a s e s
+
+
+ALTER TABLE vnr RENAME TO vnr_old;
+
+CREATE TABLE releases (
+ id integer NOT NULL DEFAULT get_new_id('releases') PRIMARY KEY,
+ latest integer NOT NULL DEFAULT 0,
+ vid integer NOT NULL DEFAULT 0,
+ locked smallint NOT NULL DEFAULT 0
+) WITHOUT OIDS;
+
+CREATE TABLE releases_rev (
+ id integer NOT NULL PRIMARY KEY,
+ rid integer NOT NULL DEFAULT 0,
+ title varchar(250) NOT NULL DEFAULT '',
+ original varchar(250) NOT NULL DEFAULT '',
+ "type" smallint NOT NULL DEFAULT 0,
+ relation varchar(32) NOT NULL DEFAULT '', -- deprecated
+ language varchar NOT NULL DEFAULT 'ja',
+ website varchar(250) NOT NULL DEFAULT '',
+ released varchar(10),
+ notes varchar(250) NOT NULL DEFAULT '',
+ minage smallint NOT NULL DEFAULT -1
+) WITHOUT OIDS;
+
+ALTER TABLE vnr_media RENAME TO releases_media;
+ALTER TABLE vnr_platforms RENAME TO releases_platforms;
+ALTER TABLE vnr_producers RENAME TO releases_producers;
+ALTER TABLE releases_media RENAME vnrid TO rid;
+ALTER TABLE releases_platforms RENAME vnrid TO rid;
+ALTER TABLE releases_producers RENAME vnrid TO rid;
+ALTER TABLE releases_media ADD COLUMN tmp_upd smallint DEFAULT 0;
+ALTER TABLE releases_platforms ADD COLUMN tmp_upd smallint DEFAULT 0;
+ALTER TABLE releases_producers ADD COLUMN tmp_upd smallint DEFAULT 0;
+ALTER TABLE releases_platforms DROP CONSTRAINT vnv_platforms_pkey;
+ALTER TABLE releases_producers DROP CONSTRAINT vnv_companies_pkey;
+
+
+CREATE OR REPLACE FUNCTION fill_releases() RETURNS void AS $$
+DECLARE
+ r RECORD;
+ i integer;
+ t integer;
+ ti text;
+ tg text;
+BEGIN
+ FOR r IN SELECT * FROM vnr_old ORDER BY added LOOP
+ INSERT INTO changes ("type", added, requester, comments)
+ VALUES (1, r.added, 1, 'Automated import from VNDB 1.8');
+
+ SELECT currval('changes_id_seq') INTO i;
+
+ -- swap titles
+ ti := r.romaji;
+ tg := r.title;
+ IF ti = '' THEN
+ ti := r.title;
+ tg := '';
+ END IF;
+ -- determine type
+ t := 0;
+ IF r.relation ILIKE '%trial%' OR r.relation ILIKE '%demo%' THEN
+ t := 2;
+ END IF;
+
+ INSERT INTO releases_rev (id, rid, title, original, relation, language, website, released, notes, minage, "type")
+ VALUES (i, r.id, ti, tg, r.relation, r.language, r.website, r.released, r.notes, r.minage, t);
+
+ INSERT INTO releases (id, latest, vid)
+ VALUES (r.id, i, r.vid);
+
+ UPDATE releases_media SET rid = i, tmp_upd = 1 WHERE rid = r.id AND tmp_upd = 0;
+ UPDATE releases_producers SET rid = i, tmp_upd = 1 WHERE rid = r.id AND tmp_upd = 0;
+ UPDATE releases_platforms SET rid = i, tmp_upd = 1 WHERE rid = r.id AND tmp_upd = 0;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+SELECT fill_releases();
+DROP FUNCTION fill_releases();
+
+ALTER TABLE releases_media DROP COLUMN tmp_upd;
+ALTER TABLE releases_producers DROP COLUMN tmp_upd;
+ALTER TABLE releases_platforms DROP COLUMN tmp_upd;
+ALTER TABLE releases_producers ADD CONSTRAINT releases_producers_pkey PRIMARY KEY (pid, rid);
+ALTER TABLE releases_media ADD CONSTRAINT releases_media_pkey PRIMARY KEY (rid, medium, qty);
+ALTER TABLE releases_platforms ADD CONSTRAINT releases_platforms_pkey PRIMARY KEY (rid, platform);
+
+
+
+
+
+-- P r o d u c e r s
+
+
+ALTER TABLE producers RENAME TO producers_old;
+
+CREATE TABLE producers (
+ id integer NOT NULL DEFAULT get_new_id('producers') PRIMARY KEY,
+ latest integer NOT NULL DEFAULT 0,
+ locked smallint NOT NULL DEFAULT 0
+) WITHOUT OIDS;
+
+CREATE TABLE producers_rev (
+ id integer NOT NULL PRIMARY KEY,
+ pid integer NOT NULL DEFAULT 0,
+ "type" character(2) NOT NULL DEFAULT 'co',
+ name varchar(200) NOT NULL DEFAULT '',
+ original varchar(200) NOT NULL DEFAULT '',
+ website varchar(250) NOT NULL DEFAULT '',
+ lang varchar NOT NULL DEFAULT 'ja',
+ "desc" text NOT NULL DEFAULT ''
+) WITHOUT OIDS;
+
+CREATE OR REPLACE FUNCTION fill_producers() RETURNS void AS $$
+DECLARE
+ r RECORD;
+ i integer;
+BEGIN
+ FOR r IN SELECT * FROM producers_old ORDER BY added LOOP
+ INSERT INTO changes ("type", added, requester, comments)
+ VALUES (2, r.added, 1, 'Automated import from VNDB 1.8');
+
+ SELECT currval('changes_id_seq') INTO i;
+
+ INSERT INTO producers_rev (id, pid, "type", name, original, website, lang, "desc")
+ VALUES (i, r.id, r.type, r.name, r.original, r.website, r.lang, r.desc);
+
+ INSERT INTO producers (id, latest, locked)
+ VALUES (r.id, i, 0);
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+SELECT fill_producers();
+DROP FUNCTION fill_producers();
+
+
+
+
+
+
+
+DROP TABLE vn_old;
+DROP TABLE vn_relations_old;
+DROP TABLE vn_categories;
+DROP TABLE vnr_old;
+DROP TABLE producers_old;
+DROP FUNCTION get_new_id();
+
+
+UPDATE users SET rank = rank+1;
+ALTER TABLE users ALTER COLUMN rank SET DEFAULT 2;
+
+
+
+
+
+-- F u n c t i o n s
+
+
+-- ids = empty string or comma-seperated list of id's (as a string)
+CREATE OR REPLACE FUNCTION update_prev(tbl text, ids text) RETURNS void AS $$
+DECLARE
+ r RECORD;
+ r2 RECORD;
+ i integer;
+ t text;
+ e text;
+BEGIN
+ SELECT INTO t SUBSTRING(tbl, 0, 1);
+ e := '';
+ IF ids <> '' THEN
+ e := ' WHERE id IN('||ids||')';
+ END IF;
+ FOR r IN EXECUTE 'SELECT id FROM '||tbl||e LOOP
+ i := 0;
+ FOR r2 IN EXECUTE 'SELECT id FROM '||tbl||'_rev WHERE '||t||'id = '||r.id||' ORDER BY id ASC' LOOP
+ UPDATE changes SET prev = i WHERE id = r2.id;
+ i := r2.id;
+ END LOOP;
+ END LOOP;
+END;
+$$ LANGUAGE plpgsql;
+
+-- /what/ bitflags: released, languages, votes
+-- Typical yorhel-code: ugly...
+CREATE OR REPLACE FUNCTION update_vncache(what integer, id integer) RETURNS void AS $$
+DECLARE
+ s text := '';
+ w text := '';
+BEGIN
+ IF what < 1 OR what > 7 THEN
+ RETURN;
+ END IF;
+ IF what & 1 = 1 THEN
+ s := 'c_released = COALESCE((SELECT
+ SUBSTRING(COALESCE(MIN(rr1.released), ''0000-00'') from 1 for 7)
+ FROM releases r1
+ JOIN releases_rev rr1 ON r1.latest = rr1.id
+ WHERE r1.vid = vn.id
+ AND rr1.type <> 2
+ GROUP BY r1.vid
+ ), ''0000-00'')';
+ END IF;
+ IF what & 2 = 2 THEN
+ IF s <> '' THEN
+ s := s||', ';
+ END IF;
+ s := s||'c_languages = COALESCE(ARRAY_TO_STRING(ARRAY(
+ SELECT language
+ FROM releases r2
+ JOIN releases_rev rr2 ON r2.latest = rr2.id
+ WHERE r2.vid = vn.id
+ AND rr2.type <> 2
+ GROUP BY rr2.language
+ ORDER BY rr2.language
+ ), ''/''), '''')';
+ END IF;
+ IF what & 4 = 4 THEN
+ IF s <> '' THEN
+ s := s||', ';
+ END IF;
+ s := s||'c_votes = COALESCE((SELECT
+ TO_CHAR(CASE WHEN COUNT(uid) < 2 THEN 0 ELSE AVG(vote) END, ''FM00D0'')||''|''||TO_CHAR(COUNT(uid), ''FM0000'')
+ FROM votes
+ WHERE vid = vn.id
+ GROUP BY vid
+ ), ''00.0|0000'')';
+ END IF;
+ IF id > 0 THEN
+ w := ' WHERE id = '||id;
+ END IF;
+ EXECUTE 'UPDATE vn SET '||s||w;
+END;
+$$ LANGUAGE plpgsql;
+
+
+
+