diff options
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/&/&/g;s/</</g;s/>/>/g;s/"/"/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/&/&/g;s/</</g;s/>/>/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"><- previous</a>', $_[0].($d{page}-2 ? $ng.'p='.($d{page}-1) : '') if $d{page} > 1; + push @br, sprintf '<a href="%s">next -></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 -></a>| : ''). + ($x ? qq|<a href="/$type$$y{id}?rev=$$x{cid}" id="revprev"><- earlier revision</a>| : ''). + qq|<a href="/$type$$y{id}" id="revmain">$type$$y{id}</a> </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"> </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> </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/\&/&/g; + s/>/>/g; + s/</</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">< '.join(' - ', map { sprintf $_, $t.$$o{id} } @act).' ></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 & 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}} ? '▸' : '▾', $_->{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">< <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> ></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. <=><i>Prequel</i>. + </dd><dt>Prequel</dt><dd> + The story happens before the original story.<=><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. <=><i>Parent story</i> + </dd><dt>Parent story</dt><dd> + .. <=><i>Side story</i>. + </dd><dt>Summary</dt><dd> + Summarizes full story, may contain additional stuff. <=><i>Full story</i>. + </dd><dt>Full story</dt><dd> + Full version of the summarized story. <=><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 & 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&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> </li> + <li><a href="/v/new">Add visual novel</a></li> + <li><a href="/p/add">Add producer</a></li> + [[ } ]] + <li> </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&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&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"> </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 => ' ', short => 'l_vnn', pre => 'http://visual-novels.net/vn/index.php?option=com_content&task=view&id=', class => 'shortopts' }, + { type => 'input', name => ' ', 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 ]]&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"> </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&task=view&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">< 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> +></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&task=view&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(' & ', 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 & releases</b>' : '<a href="/v'.$d{vn}{id}.'">description & 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"> </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"> </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/&/&/g;s/</</g;s/>/>/g;s/"/"/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] ? '▸' : '▾') + 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 + '&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,"&").replace(/</,"<").replace(/>/,">").replace(/'/g,/*'*/ "\\'").replace(/"/g,/*"*/'"'); +} + +// 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 Binary files differnew file mode 100644 index 00000000..c87fb121 --- /dev/null +++ b/static/files/footer.gif diff --git a/static/files/graph.png b/static/files/graph.png Binary files differnew file mode 100644 index 00000000..bb56f758 --- /dev/null +++ b/static/files/graph.png diff --git a/static/files/headerbg.jpg b/static/files/headerbg.jpg Binary files differnew file mode 100644 index 00000000..81f4dd75 --- /dev/null +++ b/static/files/headerbg.jpg diff --git a/static/files/headerbot.png b/static/files/headerbot.png Binary files differnew file mode 100644 index 00000000..6e04ab05 --- /dev/null +++ b/static/files/headerbot.png diff --git a/static/files/platforms.png b/static/files/platforms.png Binary files differnew file mode 100644 index 00000000..66951fe2 --- /dev/null +++ b/static/files/platforms.png diff --git a/static/files/rss.png b/static/files/rss.png Binary files differnew file mode 100644 index 00000000..923c3822 --- /dev/null +++ b/static/files/rss.png diff --git a/static/files/select.png b/static/files/select.png Binary files differnew file mode 100644 index 00000000..ac219e05 --- /dev/null +++ b/static/files/select.png diff --git a/static/files/sidebarbg.jpg b/static/files/sidebarbg.jpg Binary files differnew file mode 100644 index 00000000..00eb5697 --- /dev/null +++ b/static/files/sidebarbg.jpg diff --git a/static/files/sidebarbot.jpg b/static/files/sidebarbot.jpg Binary files differnew file mode 100644 index 00000000..49884ded --- /dev/null +++ b/static/files/sidebarbot.jpg diff --git a/static/files/sidebg.jpg b/static/files/sidebg.jpg Binary files differnew file mode 100644 index 00000000..65fd3306 --- /dev/null +++ b/static/files/sidebg.jpg 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 Binary files differnew file mode 100644 index 00000000..b8af1a53 --- /dev/null +++ b/static/files/warning.png 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/&/&/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; + + + + |