diff options
Diffstat (limited to 'lib/VNDB')
-rw-r--r-- | lib/VNDB/BBCode.pm | 14 | ||||
-rw-r--r-- | lib/VNDB/Config.pm | 22 | ||||
-rw-r--r-- | lib/VNDB/ExtLinks.pm | 272 | ||||
-rw-r--r-- | lib/VNDB/Func.pm | 163 | ||||
-rw-r--r-- | lib/VNDB/Schema.pm | 28 | ||||
-rw-r--r-- | lib/VNDB/Types.pm | 123 |
6 files changed, 405 insertions, 217 deletions
diff --git a/lib/VNDB/BBCode.pm b/lib/VNDB/BBCode.pm index 340201bf..950dcb8b 100644 --- a/lib/VNDB/BBCode.pm +++ b/lib/VNDB/BBCode.pm @@ -186,7 +186,7 @@ FINAL: # delspoil => 0/1 - delete [spoiler] tags and its contents # replacespoil => 0/1 - replace [spoiler] tags with a "hidden by spoiler settings" message # keepsoil => 0/1 - keep the contents of spoiler tags without any special formatting -# default: format as <b class="spoiler">.. +# default: format as <span class="spoiler">.. sub bb_format { my($input, %opt) = @_; $opt{delspoil} = 1 if $opt{text} && !$opt{keepspoil}; @@ -235,8 +235,8 @@ sub bb_format { } elsif($opt{idonly}) { $ret .= e $raw; - } elsif($tag eq 'b_start') { $ret .= $opt{text} ? e '*' : '<b>' - } elsif($tag eq 'b_end') { $ret .= $opt{text} ? e '*' : '</b>' + } elsif($tag eq 'b_start') { $ret .= $opt{text} ? e '*' : '<strong>' + } elsif($tag eq 'b_end') { $ret .= $opt{text} ? e '*' : '</strong>' } elsif($tag eq 'i_start') { $ret .= $opt{text} ? e '/' : '<em>' } elsif($tag eq 'i_end') { $ret .= $opt{text} ? e '/' : '</em>' } elsif($tag eq 'u_start') { $ret .= $opt{text} ? e '_' : '<span class="underline">' @@ -262,11 +262,11 @@ sub bb_format { } elsif($tag eq 'spoiler_start') { $inspoil = 1; $ret .= $opt{delspoil} || $opt{keepspoil} ? '' - : $opt{replacespoil} ? '<b class="grayedout"><hidden by spoiler settings></b>' - : '<b class="spoiler">'; + : $opt{replacespoil} ? '<small><hidden by spoiler settings></small>' + : '<span class="spoiler">'; } elsif($tag eq 'spoiler_end') { $inspoil = 0; - $ret .= $opt{delspoil} || $opt{keepspoil} || $opt{replacespoil} ? '' : '</b>'; + $ret .= $opt{delspoil} || $opt{keepspoil} || $opt{replacespoil} ? '' : '</span>'; } elsif($tag eq 'url_start') { $ret .= $opt{text} ? '' : sprintf '<a href="%s" rel="nofollow">', xml_escape($arg[0]); @@ -302,7 +302,7 @@ sub bb_subst_links { my $first = 0; my %links = map +($_->{id}, $_->{title}), $TUWF::OBJ->dbAlli( - 'SELECT id, title FROM (VALUES', (map +($first++ ? ',(' : '(', \"$_", '::vndbid)'), sort keys %lookup), ') n(id), item_info(n.id, NULL::int)' + 'SELECT id, title[1+1] FROM (VALUES', (map +($first++ ? ',(' : '(', \"$_", '::vndbid)'), sort keys %lookup), ') n(id), item_info(NULL, n.id, NULL)' )->@*; return $msg unless %links; diff --git a/lib/VNDB/Config.pm b/lib/VNDB/Config.pm index d360c258..050a0124 100644 --- a/lib/VNDB/Config.pm +++ b/lib/VNDB/Config.pm @@ -3,13 +3,19 @@ package VNDB::Config; use strict; use warnings; use Exporter 'import'; +use Cwd 'abs_path'; our @EXPORT = ('config'); my $ROOT = $INC{'VNDB/Config.pm'} =~ s{/lib/VNDB/Config\.pm$}{}r; +my $GEN = abs_path($ENV{VNDB_GEN} // "$ROOT/gen"); +my $VAR = abs_path($ENV{VNDB_VAR} // "$ROOT/var"); # Default config options my $config = { - url => 'http://localhost:3000', + gen_path => $GEN, + var_path => $VAR, + + url => 'http://localhost:3000', tuwf => { db_login => [ 'dbi:Pg:dbname=vndb', 'vndb_site', undef ], @@ -24,16 +30,22 @@ my $config = { source_url => 'https://code.blicky.net/yorhel/vndb', admin_email => 'contact@vndb.org', login_throttle => [ 24*3600/10, 24*3600 ], # interval between attempts, max burst (10 a day) + reset_throttle => [ 24*3600/2, 24*3600 ], # interval between attempts, max burst (2 a day) board_edit_time => 7*24*3600, # Time after which posts become immutable graphviz_path => '/usr/bin/dot', - convert_path => '/usr/bin/convert', - identify_path => '/usr/bin/identify', + imgproc_path => "$GEN/imgproc", trace_log => 0, + # Put the site in full read-only mode; Login is disabled and nothing is written to the DB. Handy for migrations. + read_only => 0, + + location_db => undef, # Optional path to a libloc database for IP geolocation scr_size => [ 136, 102 ], # w*h of screenshot thumbnails ch_size => [ 256, 300 ], # max. w*h of char images cv_size => [ 256, 400 ], # max. w*h of cover images + api_throttle => [ 60, 5 ], # execution time multiplier, allowed burst + Multi => { Core => {}, Maintenance => {}, @@ -41,7 +53,7 @@ my $config = { }; -my $config_file = do $ROOT.'/data/conf.pl'; +my $config_file = -e "$VAR/conf.pl" ? do("$VAR/conf.pl") || die $! : {}; my $config_merged; sub config { @@ -55,7 +67,7 @@ sub config { $c->{version} ||= `git -C "$ROOT" describe` =~ s/\-g[0-9a-f]+$//rg =~ s/\r?\n//rg; $c->{root} = $ROOT; $c->{Multi}{Core}{log_level} ||= 'debug'; - $c->{Multi}{Core}{log_dir} ||= $ROOT.'/data/log'; + $c->{Multi}{Core}{log_dir} ||= $VAR.'/log'; $c }; $config_merged diff --git a/lib/VNDB/ExtLinks.pm b/lib/VNDB/ExtLinks.pm index ddc1cf5d..7d22ec32 100644 --- a/lib/VNDB/ExtLinks.pm +++ b/lib/VNDB/ExtLinks.pm @@ -6,7 +6,12 @@ use VNDB::Config; use VNDB::Schema; use Exporter 'import'; -our @EXPORT = ('sql_extlinks', 'enrich_extlinks', 'revision_extlinks', 'validate_extlinks'); +our @EXPORT = qw/ + sql_extlinks + enrich_extlinks + revision_extlinks + validate_extlinks +/; # column name in wikidata table => \%info @@ -40,7 +45,7 @@ our %WIKIDATA = ( crunchyroll => { type => 'text[]', property => 'P4110', label => undef, fmt => undef }, igdb_game => { type => 'text[]', property => 'P5794', label => 'IGDB', fmt => 'https://www.igdb.com/games/%s' }, giantbomb => { type => 'text[]', property => 'P5247', label => undef, fmt => undef }, - pcgamingwiki => { type => 'text[]', property => 'P6337', label => undef, fmt => undef }, + pcgamingwiki => { type => 'text[]', property => 'P6337', label => 'PCGamingWiki', fmt => 'https://www.pcgamingwiki.com/wiki/%s' }, steam => { type => 'integer[]', property => 'P1733', label => undef, fmt => undef }, gog => { type => 'text[]', property => 'P2725', label => 'GOG', fmt => 'https://www.gog.com/game/%s' }, pixiv_user => { type => 'integer[]', property => 'P5435', label => 'Pixiv', fmt => 'https://www.pixiv.net/member.php?id=%d' }, @@ -48,11 +53,16 @@ our %WIKIDATA = ( soundcloud => { type => 'text[]', property => 'P3040', label => 'Soundcloud', fmt => 'https://soundcloud.com/%s' }, humblestore => { type => 'text[]', property => 'P4477', label => undef, fmt => undef }, itchio => { type => 'text[]', property => 'P7294', label => undef, fmt => undef }, - + playstation_jp => { type => 'text[]', property => 'P5999', label => undef, fmt => undef }, + playstation_na => { type => 'text[]', property => 'P5944', label => undef, fmt => undef }, + playstation_eu => { type => 'text[]', property => 'P5971', label => undef, fmt => undef }, + lutris => { type => 'text[]', property => 'P7597', label => 'Lutris', fmt => 'https://lutris.net/games/%s' }, + wine => { type => 'integer[]', property => 'P600', label => 'Wine AppDB', fmt => 'https://appdb.winehq.org/appview.php?iAppId=%d' }, ); # dbentry_type => column name => \%info +# Column names are also used for AdvSearch filters, so they should be stable. # info keys: # label Name of the link # fmt How to generate a url (basic version, printf-style only) @@ -77,9 +87,6 @@ our %LINKS = ( l_egs => { label => 'ErogameScape' , fmt => 'https://erogamescape.dyndns.org/~ap2/ero/toukei_kaiseki/game.php?game=%d' , regex => qr{erogamescape\.dyndns\.org/~ap2/ero/toukei_kaiseki/(?:before_)?game\.php\?(?:.*&)?game=([0-9]+)(?:&.*)?} }, - l_erotrail => { label => 'ErogeTrailers' - , fmt => 'http://erogetrailers.com/soft/%d' - , regex => qr{(?:www\.)?erogetrailers\.com/soft/([0-9]+)} }, l_steam => { label => 'Steam' , fmt => 'https://store.steampowered.com/app/%d/' , fmt2 => 'https://store.steampowered.com/app/%d/?utm_source=vndb' @@ -87,31 +94,38 @@ our %LINKS = ( l_dlsite => { label => 'DLsite' , fmt => 'https://www.dlsite.com/home/work/=/product_id/%s.html' , fmt2 => sub { config->{dlsite_url} && sprintf config->{dlsite_url}, shift->{l_dlsite_shop}||'home' } - , regex => qr{(?:www\.)?dlsite\.com/.*/(?:dlaf/=/link/work/aid/.*/id|work/=/product_id)/([VR]J[0-9]{6}).*} + , regex => qr{(?:www\.)?dlsite\.com/.*/(?:dlaf/=/link/work/aid/.*/id|work/=/product_id)/([VR]J[0-9]{6,8}).*} , patt => 'https://www.dlsite.com/<store>/work/=/product_id/<VJ or RJ-code>' }, - l_dlsiteen => { label => 'DLsite (eng)' - , fmt => 'https://www.dlsite.com/eng/work/=/product_id/%s.html' - , fmt2 => sub { config->{dlsite_url} && sprintf config->{dlsite_url}, shift->{l_dlsiteen_shop}||'eng' } - , regex => qr{(?:www\.)?dlsite\.com/.*/(?:dlaf/=/link/work/aid/.*/id|work/=/product_id)/([VR]E[0-9]{6}).*} - , patt => 'https://www.dlsite.com/<store>/work/=/product_id/<VE or RE-code>' }, l_gog => { label => 'GOG' , fmt => 'https://www.gog.com/game/%s' - , regex => qr{(?:www\.)?gog\.com/game/([a-z0-9_]+).*} }, + , regex => qr{(?:www\.)?gog\.com/(?:[a-z]{2}/)?game/([a-z0-9_]+).*} }, l_itch => { label => 'Itch.io' , fmt => 'https://%s' , regex => qr{([a-z0-9_-]+\.itch\.io/[a-z0-9_-]+)} , patt => 'https://<artist>.itch.io/<product>' }, + l_patreonp => { label => 'Patreon post' + , fmt => 'https://www.patreon.com/posts/%d' + , regex => qr{(?:www\.)?patreon\.com/posts/(?:[^/?]+-)?([0-9]+).*} }, + l_patreon => { label => 'Patreon' + , fmt => 'https://www.patreon.com/%s' + , regex => qr{(?:www\.)?patreon\.com/(?!user[\?/]|posts[\?/]|join[\?/])([^/?]+).*} }, + l_substar => { label => 'SubscribeStar' + , fmt => 'https://subscribestar.%s' + , regex => qr{(?:www\.)?subscribestar\.((?:adult|com)/[^/?]+).*} + , patt => 'https://subscribestar.<adult or com>/<name>' }, l_denpa => { label => 'Denpasoft' , fmt => 'https://denpasoft.com/product/%s/' , fmt2 => config->{denpa_url} - , regex => qr{(?:www\.)?denpasoft\.com/products?/([a-z0-9-]+).*} }, + , regex => qr{(?:www\.)?denpasoft\.com/products?/([^/&#?:]+).*} }, l_jlist => { label => 'J-List' - , fmt => 'https://www.jlist.com/%s' - , fmt2 => sub { config->{ shift->{l_jlist_jbox} ? 'jbox_url' : 'jlist_url' } } - , regex => qr{(?:www\.)?(?:jlist|jbox)\.com/(?:.+/)?([a-z0-9-]*[0-9][a-z0-9-]*)} }, + , fmt => 'https://www.jlist.com/shop/product/%s' + , fmt2 => config->{jlist_url}, + , regex => qr{(?:www\.)?(?:jlist|jbox)\.com/shop/product/([^/#?]+).*} }, l_jastusa => { label => 'JAST USA' - , fmt => 'https://jastusa.com/games/%s' - , regex => qr{(?:www\.)?jastusa\.com/games/([a-z0-9_-]+)} }, + , fmt => 'https://jastusa.com/games/%s/vndb' + , fmt2 => sub { config->{jastusa_url} && sprintf config->{jastusa_url}, shift->{l_jast_slug}||'vndb' }, + , regex => qr{(?:www\.)?jastusa\.com/games/([a-z0-9_-]+)/[^/]+} + , patt => 'https://jastusa.com/games/<code>/<title>' }, l_fakku => { label => 'Fakku' , fmt => 'https://www.fakku.net/games/%s' , regex => qr{(?:www\.)?fakku\.(?:net|com)/games/([^/]+)(?:[/\?].*)?} }, @@ -127,6 +141,10 @@ our %LINKS = ( l_freem => { label => 'Freem!' , fmt => 'https://www.freem.ne.jp/win/game/%d' , regex => qr{(?:www\.)?freem\.ne\.jp/win/game/([0-9]+)} }, + l_freegame => { label => 'Freegame Mugen' + , fmt => 'https://freegame-mugen.jp/%s.html' + , regex => qr{(?:www\.)?freegame-mugen\.jp/([^/]+/game_[0-9]+)\.html} + , patt => 'https://freegame-mugen.jp/<genre>/game_<id>.html' }, l_novelgam => { label => 'NovelGame' , fmt => 'https://novelgame.jp/games/show/%d' , regex => qr{(?:www\.)?novelgame\.jp/games/show/([0-9]+)} }, @@ -154,7 +172,7 @@ our %LINKS = ( , regex => qr{(?:dl|order)\.getchu\.com/(?:i/item|(?:r|index).php.*[?&]gcd=D?0*)([0-9]+).*} }, l_dmm => { label => 'DMM' , fmt => 'https://%s' - , regex => qr{((?:www\.|dlsoft\.)?dmm\.(?:com|co\.jp)/[^\s]+)} + , regex => qr{((?:www\.|dlsoft\.)?dmm\.(?:com|co\.jp)/[^\s?]+)(?:\?.*)?} , patt => 'https://<any link to dmm.com or dmm.co.jp>' }, l_toranoana=> { label => 'Toranoana' # ec.* is for 18+, ecs.toranoana.jp is for non-18+. @@ -162,19 +180,79 @@ our %LINKS = ( , fmt => 'https://ec.toranoana.shop/tora/ec/item/%012d/' , regex => qr{(?:www\.)?ecs?\.toranoana\.(?:shop|jp)/(?:aqua/ec|(?:tora|joshi)(?:/ec|_r/ec|_d/digi|_rd/digi)?)/item/([0-9]{12}).*} , patt => 'https://ec.toranoana.<shop or jp>/<shop>/item/<number>/' }, + l_booth => { label => 'BOOTH' + , fmt => 'https://booth.pm/en/items/%d' + , regex => qw{(?:[a-z0-9_-]+\.)?booth\.pm/(?:[a-z-]+\/)?items/([0-9]+).*} + , patt => 'https://booth.pm/<language>/items/<id> OR https://<publisher>.booth.pm/items/<id>' }, l_gamejolt => { label => 'Game Jolt' , fmt => 'https://gamejolt.com/games/vn/%d', # /vn/ should be the game title, but it doesn't matter , regex => qr{(?:www\.)?gamejolt\.com/games/(?:[^/]+)/([0-9]+)(?:/.*)?} }, l_nutaku => { label => 'Nutaku' , fmt => 'https://www.nutaku.net/games/%s/' , regex => qr{(?:www\.)?nutaku\.net/games/(?:mobile/|download/|app/)?([a-z0-9-]+)/?} }, # The section part does sometimes link to different pages, but it's the same game and the non-section link always works. + l_playstation_jp => { label => 'PlayStation Store (JP)' + , fmt => 'https://store.playstation.com/ja-jp/product/%s' + , regex => qr{store\.playstation\.com/(?:[-a-z]+\/)?product\/(JP\d{4}-[A-Z]{4}\d{5}_00-[\dA-Z_]{16})} }, + l_playstation_na => { label => 'PlayStation Store (NA)' + , fmt => 'https://store.playstation.com/en-us/product/%s' + , regex => qr{store\.playstation\.com/(?:[-a-z]+\/)?product\/(UP\d{4}-[A-Z]{4}\d{5}_00-[\dA-Z_]{16})} }, + l_playstation_eu => { label => 'PlayStation Store (EU)' + , fmt => 'https://store.playstation.com/en-gb/product/%s' + , regex => qr{store\.playstation\.com/(?:[-a-z]+\/)?product\/(EP\d{4}-[A-Z]{4}\d{5}_00-[\dA-Z_]{16})} }, + l_playstation_hk => { label => 'PlayStation Store (HK)' + , fmt => 'https://store.playstation.com/en-hk/product/%s' + , regex => qr{store\.playstation\.com/(?:[-a-z]+\/)?product\/(HP\d{4}-[A-Z]{4}\d{5}_00-[\dA-Z_]{16})} }, + l_nintendo => { label => 'Nintendo' + , fmt => 'https://www.nintendo.com/store/products/%s/' + , regex => qr{www\.nintendo\.com\/store\/products\/([-a-z0-9]+-(?:switch|wii-u|3ds))\/} }, + l_nintendo_jp => { label => 'Nintendo (JP)' + , fmt => 'https://store-jp.nintendo.com/list/software/%d.html' + , regex => qr{store-jp\.nintendo\.com/list/software/([0-9]+).html} }, + l_nintendo_hk => { label => 'Nintendo (HK)' + , fmt => 'https://store.nintendo.com.hk/%d' + , regex => qr{store\.nintendo\.com\.hk/([0-9]+)} }, + # deprecated + l_dlsiteen => { label => 'DLsite (eng)', fmt => 'https://www.dlsite.com/eng/work/=/product_id/%s.html' }, + l_erotrail => { label => 'ErogeTrailers', fmt => 'http://erogetrailers.com/soft/%d' }, }, s => { l_site => { label => 'Official website', fmt => '%s' }, - l_wikidata => { label => 'Wikidata', fmt => 'https://www.wikidata.org/wiki/Q%d' }, - l_twitter => { label => 'Twitter', fmt => 'https://twitter.com/%s' }, - l_anidb => { label => 'AniDB', fmt => 'https://anidb.net/cr%s' }, - l_pixiv => { label => 'Pixiv', fmt => 'https://www.pixiv.net/member.php?id=%d' }, + l_wikidata => { label => 'Wikidata' + , fmt => 'https://www.wikidata.org/wiki/Q%d' + , regex => qr{www\.wikidata\.org/wiki/Q([1-9][0-9]*)} }, + l_twitter => { label => 'Xitter' + , fmt => 'https://twitter.com/%s' + , regex => qr{(?:(?:www\.)?twitter\.com|nitter\.[^/]+)/([^?\/ ]{1,16})(?:[?/].*)?} }, + l_anidb => { label => 'AniDB' + , fmt => 'https://anidb.net/cr%s' + , regex => qr{anidb\.net/(?:cr|creator/)([1-9][0-9]*)} }, + l_pixiv => { label => 'Pixiv' + , fmt => 'https://www.pixiv.net/member.php?id=%d' + , regex => qr{www\.pixiv\.net/(?:member\.php\?id=|en/users/|users/)([0-9]+)} }, + l_vgmdb => { label => 'VGMdb' + , fmt => 'https://vgmdb.net/artist/%d' + , regex => qr{vgmdb\.net/artist/([0-9]+)} }, + l_discogs => { label => 'Discogs' + , fmt => 'https://www.discogs.com/artist/%d' + , regex => qr{(?:www\.)?discogs\.com/artist/([0-9]+)(?:[?/-].*)?} }, + l_mobygames=> { label => 'MobyGames' + , fmt => 'https://www.mobygames.com/person/%d' + , regex => qr{(?:www\.)?mobygames\.com/person/([0-9]+)(?:[?/].*)?} }, + l_bgmtv => { label => 'Bangumi' + , fmt => 'https://bgm.tv/person/%d' + , regex => qr{(?:www\.)?(?:bgm|bangumi)\.tv/person/([0-9]+)(?:[?/].*)?} }, + l_imdb => { label => 'IMDb' + , fmt => 'https://www.imdb.com/name/nm%07d' + , regex => qr{(?:www\.)?imdb\.com/name/nm([0-9]{7,8})(?:[?/].*)?} }, + l_mbrainz => { label => 'MusicBrainz' + , fmt => 'https://musicbrainz.org/artist/%s' + , regex => qr{musicbrainz\.org/artist/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})} }, + l_scloud => { label => 'SoundCloud' + , fmt => 'https://soundcloud.com/%s' + , regex => qr{soundcloud\.com/([a-z0-9-]+)} }, + l_vndb => { label => 'VNDB user' + , fmt => 'https://vndb.org/%s' + , regex => qr{vndb\.org/(u[1-9][0-9]*)} }, # deprecated l_wp => { label => 'Wikipedia', fmt => 'https://en.wikipedia.org/wiki/%s' }, }, @@ -192,7 +270,7 @@ sub sql_extlinks { my($type, $prefix) = @_; $prefix ||= ''; my $l = $LINKS{$type} || die "DB entry type $type has no links"; - VNWeb::DB::sql_comma(map $prefix.$_, sort keys %$l) + join ',', map $prefix.$_, sort keys %$l } @@ -200,13 +278,14 @@ sub sql_extlinks { # following field to each object: # # extlinks => [ -# [ $title, $url, $price ], +# { name, label, id, url, url2, price }, # depending on which fields are $enabled # .. # ] # -# (It also adds a few other fields in some cases, but you can ignore those) +# Assumes the columns returned by sql_extlinks() are already available. sub enrich_extlinks { - my($type, @obj) = @_; + my($type, $enabled, @obj) = @_; + $enabled ||= { label => 1, url2 => 1, price => 1 }; @obj = map ref $_ eq 'ARRAY' ? @$_ : ($_), @obj; my $l = $LINKS{$type} || die "DB entry type $type has no links"; @@ -215,25 +294,38 @@ sub enrich_extlinks { my $w = @w_ids ? { map +($_->{id}, $_), $TUWF::OBJ->dbAlli('SELECT * FROM wikidata WHERE id IN', \@w_ids)->@* } : {}; # Fetch shop info for releases + my @cleanup; if($type eq 'r') { VNWeb::DB::enrich_merge(id => q{ SELECT r.id , smg.price AS l_mg_price, smg.r18 AS l_mg_r18 , sdenpa.price AS l_denpa_price - , sjlist.price AS l_jlist_price, sjlist.jbox AS l_jlist_jbox + , sjast.price AS l_jast_price, sjast.slug AS l_jast_slug + , sjlist.price AS l_jlist_price , sdlsite.price AS l_dlsite_price, sdlsite.shop AS l_dlsite_shop FROM releases r LEFT JOIN shop_denpa sdenpa ON sdenpa.id = r.l_denpa AND sdenpa.lastfetch IS NOT NULL AND sdenpa.deadsince IS NULL LEFT JOIN shop_dlsite sdlsite ON sdlsite.id = r.l_dlsite AND sdlsite.lastfetch IS NOT NULL AND sdlsite.deadsince IS NULL + LEFT JOIN shop_jastusa sjast ON sjast.id = r.l_jastusa AND sjast.lastfetch IS NOT NULL AND sjast.deadsince IS NULL LEFT JOIN shop_jlist sjlist ON sjlist.id = r.l_jlist AND sjlist.lastfetch IS NOT NULL AND sjlist.deadsince IS NULL LEFT JOIN shop_mg smg ON smg.id = r.l_mg AND smg.lastfetch IS NOT NULL AND smg.deadsince IS NULL WHERE r.id IN}, - grep $_->{l_mg}||$_->{l_denpa}||$_->{l_jlist}||$_->{l_dlsite}, @obj - ); - VNWeb::DB::enrich(l_playasia => gtin => gtin => - "SELECT gtin, price, url FROM shop_playasia WHERE price <> '' AND gtin IN", - grep $_->{gtin}, @obj - ); + grep $_->{l_mg}||$_->{l_denpa}||$_->{l_jastusa}||$_->{l_jlist}||$_->{l_dlsite}, @obj + ) if $enabled->{price} || $enabled->{url2}; + + if(grep exists $_->{gtin}, @obj) { + VNWeb::DB::enrich(l_playasia => gtin => gtin => + "SELECT gtin, price, url FROM shop_playasia WHERE price <> '' AND gtin IN", + grep $_->{gtin}, @obj + ); + } else { + VNWeb::DB::enrich(l_playasia => id => id => + "SELECT r.id, s.gtin, s.price, s.url FROM releases r JOIN shop_playasia s ON s.gtin = r.gtin WHERE s.price <> '' AND r.id IN", + @obj + ); + } + + @cleanup = qw{l_mg_price l_mg_r18 l_denpa_price l_jast_price l_jast_slug l_jlist_price l_dlsite_price l_dlsite_shop l_playasia}; } for my $obj (@obj) { @@ -241,12 +333,36 @@ sub enrich_extlinks { my sub w { return if !$obj->{l_wikidata}; my($v, $fmt, $label) = ($w->{$obj->{l_wikidata}}{$_[0]}, @{$WIKIDATA{$_[0]}}{'fmt', 'label'}); - push @links, map [ $label, ref $fmt ? $fmt->($_) : sprintf($fmt, $_), undef ], ref $v ? @$v : $v ? $v : () + push @links, map +{ + $enabled->{name} ? (name => $_[0]) : (), + $enabled->{label} ? (label => $label) : (), + $enabled->{id} ? (id => $_) : (), + $enabled->{url} ? (url => ref $fmt ? $fmt->($_) : sprintf $fmt, $_) : (), + $enabled->{url2} ? (url2 => ref $fmt ? $fmt->($_) : sprintf $fmt, $_) : (), + }, ref $v ? @$v : $v ? $v : () } my sub l { my($f, $price) = @_; my($v, $fmt, $fmt2, $label) = ($obj->{$f}, $l->{$f} ? @{$l->{$f}}{'fmt', 'fmt2', 'label'} : ()); - push @links, map [ $label, sprintf((ref $fmt2 ? $fmt2->($obj) : $fmt2) || $fmt, $_), $price ], ref $v ? @$v : $v ? $v : () + push @links, map +{ + $enabled->{name} ? (name => $_[0] =~ s/^l_//r) : (), + $enabled->{label} ? (label => $label) : (), + $enabled->{id} ? (id => $_) : (), + $enabled->{url} ? (url => sprintf($fmt, $_)) : (), + $enabled->{url2} ? (url2 => sprintf((ref $fmt2 ? $fmt2->($obj) : $fmt2) || $fmt, $_)) : (), + $enabled->{price} && length $price ? (price => $price) : (), + }, ref $v ? @$v : $v ? $v : () + } + my sub c { + my($name, $label, $fmt, $id, $price) = @_; + push @links, { + $enabled->{name} ? (name => $name) : (), + $enabled->{label} ? (label => $label) : (), + $enabled->{id} ? (id => $id) : (), + $enabled->{url} ? (url => sprintf($fmt, $id)) : (), + $enabled->{url2} ? (url2 => sprintf($fmt, $id)) : (), + $enabled->{price} && length $price ? (price => $price) : (), + } } l 'l_site'; @@ -264,28 +380,34 @@ sub enrich_extlinks { w 'indiedb_game'; w 'howlongtobeat'; w 'igdb_game'; + w 'pcgamingwiki'; + w 'lutris'; + w 'wine'; l 'l_renai'; - push @links, [ 'VNStat', sprintf('https://vnstat.net/novel/%d', $obj->{id} =~ s/^.//r), undef ] if $obj->{c_votecount}>=20; + c 'vnstat', 'VNStat', 'https://vnstat.net/novel/%d', $obj->{id} =~ s/^.//r if $obj->{c_votecount}>=20; } # Release links if($type eq 'r') { l 'l_egs'; - l 'l_erotrail'; l 'l_steam'; - push @links, [ 'SteamDB', sprintf('https://steamdb.info/app/%d/info', $obj->{l_steam}), undef ] if $obj->{l_steam}; + c 'steamdb', 'SteamDB', 'https://steamdb.info/app/%d/info', $obj->{l_steam} if $obj->{l_steam}; l 'l_dlsite', $obj->{l_dlsite_price}; l 'l_gog'; l 'l_itch'; + l 'l_patreonp'; + l 'l_patreon'; + l 'l_substar'; l 'l_gamejolt'; l 'l_denpa', $obj->{l_denpa_price}; l 'l_jlist', $obj->{l_jlist_price}; - l 'l_jastusa'; + l 'l_jastusa', $obj->{l_jast_price}; l 'l_fakku'; l 'l_appstore'; l 'l_googplay'; l 'l_animateg'; l 'l_freem'; + l 'l_freegame'; l 'l_novelgam'; l 'l_gyutto'; l 'l_digiket'; @@ -297,7 +419,15 @@ sub enrich_extlinks { l 'l_getchudl'; l 'l_dmm'; l 'l_toranoana'; - push @links, map [ 'PlayAsia', $_->{url}, $_->{price} ], @{$obj->{l_playasia}} if $obj->{l_playasia}; + l 'l_booth'; + l 'l_playstation_jp'; + l 'l_playstation_na'; + l 'l_playstation_eu'; + l 'l_playstation_hk'; + l 'l_nintendo'; + l 'l_nintendo_jp'; + l 'l_nintendo_hk'; + c 'playasia', 'PlayAsia', '%s', $_->{url}, $_->{price} for $obj->{l_playasia}->@*; } # Staff links @@ -305,11 +435,15 @@ sub enrich_extlinks { l 'l_twitter'; w 'twitter' if !$obj->{l_twitter}; l 'l_anidb'; w 'anidb_person' if !$obj->{l_anidb}; l 'l_pixiv'; w 'pixiv_user' if !$obj->{l_pixiv}; - w 'musicbrainz_artist'; - w 'vgmdb_artist'; - w 'discogs_artist'; - w 'doujinshi_author'; - w 'soundcloud'; + l 'l_mbrainz'; w 'musicbrainz_artist' if !$obj->{l_mbrainz}; + l 'l_vgmdb'; w 'vgmdb_artist' if !$obj->{l_vgmdb}; + l 'l_discogs'; w 'discogs_artist' if !$obj->{l_discogs}; + l 'l_scloud'; w 'soundcloud' if !$obj->{l_scloud}; + l 'l_mobygames'; + l 'l_bgmtv'; + l 'l_imdb'; + l 'l_vndb'; + #w 'doujinshi_author'; } # Producer links @@ -317,12 +451,13 @@ sub enrich_extlinks { w 'twitter'; w 'mobygames_company'; w 'gamefaqs_company'; - w 'doujinshi_author'; + #w 'doujinshi_author'; w 'soundcloud'; - push @links, [ 'VNStat', sprintf('https://vnstat.net/developer/%d', $obj->{id} =~ s/^.//r), undef ]; + c 'vnstat', 'VNStat', 'https://vnstat.net/developer/%d', $obj->{id} =~ s/^.//r; } - $obj->{extlinks} = \@links + $obj->{extlinks} = \@links; + delete @{$obj}{ @cleanup }; } } @@ -341,31 +476,30 @@ sub revision_extlinks { sub full_regex { qr{^(?:https?://)?$_[0](?:\#.*)?$} } -# Returns a TUWF::Validate schema for a hash with links for the given entry type. +# Returns a list of keys for inclusion into a TUWF::Validate schema. # Only includes links for which a 'regex' has been set. sub validate_extlinks { my($type) = @_; my($schema) = grep +($_->{dbentry_type}||'') eq $type, values VNDB::Schema::schema->%*; - +{ type => 'hash', keys => { - map { - my($f, $p) = ($_, $LINKS{$type}{$_}); - my($s) = grep $_->{name} eq $f, $schema->{cols}->@*; - - my %val; - $val{int} = 1 if $s->{type} =~ /^(big)?int/; - $val{func} = sub { $val{int} && !$_[0] ? 1 : sprintf($p->{fmt}, $_[0]) =~ full_regex $p->{regex} }; - ($f, $s->{type} =~ /\[\]/ - ? { type => 'array', values => \%val } - : { required => 0, default => $val{int} ? 0 : '', %val } - ) - } sort grep $LINKS{$type}{$_}{regex}, keys $LINKS{$type}->%* - } } + map { + my($f, $p) = ($_, $LINKS{$type}{$_}); + my($s) = grep $_->{name} eq $f, $schema->{cols}->@*; + + my %val; + $val{int} = 1 if $s->{type} =~ /^(big)?int/; + $val{maxlength} = 512 if !$val{int}; + $val{func} = sub { $val{int} && !$_[0] ? 1 : sprintf($p->{fmt}, $_[0]) =~ full_regex $p->{regex} }; + ($f, $s->{type} =~ /\[\]/ + ? { type => 'array', values => \%val } + : { default => $s->{decl} !~ /not\s+null/i ? undef : $val{int} ? 0 : '', %val } + ) + } sort grep $LINKS{$type}{$_}{regex}, keys $LINKS{$type}->%* } -# Returns a list of sites for use in VNWeb::Elm: -# { id => $id, name => $label, fmt => $label, regex => $regex, int => $bool, multi => $bool, default => 0||'""'||'[]', pattern => [..] } +# Returns a list of sites for use in VNWeb::Elm and util/jsgen.pl: +# { id => $id, name => $label, fmt => $label, regex => $regex, int => $bool, default => undef||0||''||[], pattern => [..] } sub extlinks_sites { my($type) = @_; my($schema) = grep +($_->{dbentry_type}||'') eq $type, values VNDB::Schema::schema->%*; @@ -374,8 +508,8 @@ sub extlinks_sites { my($s) = grep $_->{name} eq $f, $schema->{cols}->@*; my $patt = $p->{patt} || ($p->{fmt} =~ s/%s/<code>/rg =~ s/%[0-9]*d/<number>/rg); +{ id => $f, name => $p->{label}, fmt => $p->{fmt}, regex => full_regex($p->{regex}) - , int => $s->{type} =~ /^(big)?int/?1:0, multi => $s->{type} =~ /\[\]/?1:0 - , default => $s->{type} =~ /\[\]/ ? '[]' : $s->{type} =~ /^(big)?int/ ? 0 : '""' + , int => $s->{type} =~ /^(big)?int/ ? 1 : 0, + , default => $s->{type} =~ /\[\]/ ? [] : $s->{decl} !~ /not\s+null/i ? undef : $s->{type} =~ /^(big)?int/ ? 0 : '' , pattern => [ split /(<[^>]+>)/, $patt ] } } sort grep $LINKS{$type}{$_}{regex}, keys $LINKS{$type}->%* } diff --git a/lib/VNDB/Func.pm b/lib/VNDB/Func.pm index 68b76f36..8c448ad8 100644 --- a/lib/VNDB/Func.pm +++ b/lib/VNDB/Func.pm @@ -4,10 +4,9 @@ use strict; use warnings; use TUWF::Misc 'uri_escape'; use Exporter 'import'; -use POSIX 'strftime'; -use Encode 'encode_utf8'; -use Unicode::Normalize 'NFKD', 'compose'; +use POSIX 'strftime', 'floor'; use Socket 'inet_pton', 'inet_ntop', 'AF_INET', 'AF_INET6'; +use Digest::SHA 'sha1'; use VNDB::Config; use VNDB::Types; use VNDB::BBCode; @@ -17,15 +16,16 @@ our @EXPORT = ('bb_format', qw| shorten resolution gtintype - normalize_titles normalize_query imgsize norm_ip minage - fmtvote fmtmedia fmtage fmtdate fmtrating fmtspoil + fmtvote fmtmedia fmtage fmtdate fmtrating fmtspoil fmtanimation + rdate imgpath imgurl - lang_attr + tlang tattr query_encode md2html + is_insecurepass |); @@ -64,7 +64,7 @@ sub resolution { # GTIN code as argument, -# Returns 'JAN', 'EAN', 'UPC' or undef, +# Returns 'JAN', 'EAN', 'UPC', 'ISBN' or undef, # Also 'normalizes' the first argument in place sub gtintype { $_[0] =~ s/[^\d]+//g; @@ -88,60 +88,12 @@ sub gtintype { local $_ = $c; return 'JAN' if /^4[59]/; # prefix code 450-459 & 490-499 return 'UPC' if /^(?:0[01]|0[6-9]|13|75[45])/; # prefix code 000-019 & 060-139 & 754-755 - return undef if /^(?:0[2-5]|2|97[789]|9[6-9])/; # some codes we don't want: 020–059 & 200-299 & 977-999 + return 'ISBN' if /^97[89]/; + return undef if /^(?:0[2-5]|2|9[6-9])/; # some codes we don't want: 020–059 & 200-299 & non-ISBN 977-999 return 'EAN'; # let's just call everything else EAN :) } -# a rather aggressive normalization -sub normalize { - local $_ = lc shift; - use utf8; - # Remove combining markings, except for kana. - # This effectively removes all accents from the characters (e.g. é -> e) - $_ = compose(NFKD($_) =~ s/(?<=[^ア-ンあ-ん])\pM//rg); - # remove some characters that have no significance when searching - tr/\r\n\t,_\-.~~〜∼ー῀:[]()%+!?#$"'`♥★☆♪†「」『』【】・‟“”‛’‘‚„«‹»›//d; - tr/@/a/; - tr/ı/i/; # Turkish lowercase i - tr/×/x/; - s/&/and/; - # Remove spaces. We're doing substring search, so let it cross word boundary to find more stuff - tr/ //d; - # remove commonly used release titles ("x Edition" and "x Version") - # this saves some space and speeds up the search - s/(?: - first|firstpress|firstpresslimited|limited|regular|standard - |package|boxed|download|complete|popular - |lowprice|best|cheap|budget - |special|trial|allages|fullvoice - |cd|cdr|cdrom|dvdrom|dvd|dvdpack|dvdpg|windows - |初回限定|初回|限定|通常|廉価|パッケージ|ダウンロード - )(?:edition|version|版|生産)//xg; - # other common things - s/fandisk/fandisc/g; - s/sempai/senpai/g; - no utf8; - return $_; -} - - -# normalizes each title and returns a concatenated string of unique titles -sub normalize_titles { - my %t = map +(normalize($_), 1), @_; - return join ' ', grep length $_, sort keys %t; -} - - -sub normalize_query { - my $q = shift; - # remove spaces within quotes, so that it's considered as one search word - $q =~ s/"([^"]+)"/(my $s=$1)=~y{ }{}d;$s/ge; - # split into search words and normalize - return map quotemeta($_), grep length $_, map normalize($_), split / /, $q; -} - - # arguments: <image size>, <max dimensions> # returns the size of the thumbnail with the same aspect ratio as the full-size # image, but fits within the specified maximum dimensions @@ -155,7 +107,7 @@ sub imgsize { $ow *= $sh/$oh; $oh = $sh; } - return (int $ow, int $oh); + return (int ($ow+0.5), int ($oh+0.5)); } @@ -193,14 +145,15 @@ sub minage { sub _path { my($t, $id) = $_[1] =~ /([a-z]+)([0-9]+)/; - $t = 'st' if $t eq 'sf' && $_[2]; - sprintf '%s/%s/%02d/%d.jpg', $_[0], $t, $id%100, $id; + sprintf '%s/%s%s/%02d/%d.%s', $_[0], $t, $_[2] ? ".$_[2]" : '', $id%100, $id, $_[3]||'jpg'; } -# imgpath($image_id, $thumb) -sub imgpath { _path config->{root}.'/static', @_ } +# imgpath($image_id, $dir, $format) +# $dir = empty || 't' || 'orig' +# $format = empty || $file_ext +sub imgpath { _path config->{var_path}.'/static', @_ } -# imgurl($image_id, $thumb) +# imgurl($image_id, $dir, $format) sub imgurl { _path config->{url_static}, @_ } @@ -237,8 +190,8 @@ sub fmtage { # argument: unix timestamp and optional format (compact/full) sub fmtdate { my($t, $f) = @_; - return strftime '%Y-%m-%d', gmtime $t if !$f || $f eq 'compact'; - return strftime '%Y-%m-%d at %R', gmtime $t; + return strftime '%Y-%m-%d', localtime $t if !$f || $f eq 'compact'; + return strftime '%Y-%m-%d at %R', localtime $t; } # Turn a (natural number) vote into a rating indication @@ -264,16 +217,46 @@ sub fmtspoil { } -# Generates a HTML 'lang' attribute given a list of possible languages. -# This is used for the 'original language' field, which we can safely assume is not used for latin-alphabet languages. -sub lang_attr { - my @l = map ref($_) eq 'HASH' ? $_->{lang} : $_, ref $_[0] ? $_[0]->@* : @_; - # Choose Japanese, Chinese or Korean (in order of likelyness) if those are in the list. - return (lang => 'ja') if grep $_ eq 'ja', @l; - return (lang => 'zh') if grep $_ eq 'zh', @l; - return (lang => 'ko') if grep $_ eq 'ko', @l; - return (lang => $l[0]) if @l == 1; - () +sub fmtanimation { + my($a, $cat) = @_; + return if !defined $a; + return $cat ? ucfirst "$cat not animated" : 'Not animated' if !$a; + return $cat ? "No $cat" : 'Not applicable' if $a == 1; + ($a & 256 ? 'Some scenes ' : $a & 512 ? 'All scenes ' : '').join('/', + $a & 4 ? 'Hand drawn' : (), + $a & 8 ? 'Vectorial' : (), + $a & 16 ? '3D' : (), + $a & 32 ? 'Live action' : () + ).($cat ? " $cat" : ''); +} + + +# Format a release date as a string. +sub rdate { + my($y, $m, $d) = ($1, $2, $3) if sprintf('%08d', shift||0) =~ /^([0-9]{4})([0-9]{2})([0-9]{2})$/; + $y == 0 ? 'unknown' : + $y == 9999 ? 'TBA' : + $m == 99 ? sprintf('%04d', $y) : + $d == 99 ? sprintf('%04d-%02d', $y, $m) : + sprintf('%04d-%02d-%02d', $y, $m, $d); +} + + +# Given a language code & title, returns a (lang => $x) html property. +sub tlang { + my($lang, $title) = @_; + # TODO: The -Latn suffix is redundant for languages that use the Latin script by default, need to check with a list. + # English is the site's default, so no need to specify that. + $lang && $lang ne 'en' + ? (lang => $lang . ($title =~ /[\x{0400}-\x{04ff}\x{0600}-\x{06ff}\x{0e00}-\x{0e7f}\x{1100}-\x{11ff}\x{1400}-\x{167f}\x{3040}-\x{3099}\x{30a1}-\x{30fa}\x{3100}-\x{9fff}\x{ac00}-\x{d7af}\x{ff66}-\x{ffdc}\x{20000}-\x{323af}]/ ? '' : '-Latn')) + : (); +} + + +# Given an SQL titles array, returns element attributes & content. +sub tattr { + my $title = ref $_[0] eq 'HASH' ? $_[0]{title} : $_[0]; + (tlang($title->[0],$title->[1]), title => $title->[3], $title->[1]) } @@ -318,4 +301,34 @@ sub md2html { $html } + +sub is_insecurepass { + utf8::encode(local $_ = shift); + my $hash = sha1 $_; + my $dir = config->{var_path}.'/hibp'; + return 0 if !-d $dir; + + my $prefix = uc unpack 'H4', $hash; + my $data = substr $hash, 2, 10; + my $F; + if(!open $F, '<', "$dir/$prefix") { + warn "Unable to lookup password prefix $prefix: $!"; + return 0; + } + + # Plain old binary search. + # Would be nicer to search through an mmap'ed view of the file, or at least + # use pread(), but alas, neither are easily available in Perl. + my($left, $right) = (0, -10 + -s $F); + while($left <= $right) { + my $off = floor(($left+$right)/20)*10; + sysseek $F, $off, 0 or die $!; + 10 == sysread $F, my $buf, 10 or die $!; + return 1 if $buf eq $data; + if($buf lt $data) { $left = $off + 10; } + else { $right = $off - 10; } + } + 0; +} + 1; diff --git a/lib/VNDB/Schema.pm b/lib/VNDB/Schema.pm index 63c0f258..ffc80e77 100644 --- a/lib/VNDB/Schema.pm +++ b/lib/VNDB/Schema.pm @@ -23,9 +23,11 @@ my $ROOT = $INC{'VNDB/Schema.pm'} =~ s{/lib/VNDB/Schema\.pm}{}r; # type => 'serial', # decl => 'id SERIAL', # full declaration, exluding comments and PRIMARY KEY marker # pub => 1, +# comment => '', # }, ... # ], # primary => ['id'], +# comment => '', # } # } sub schema { @@ -35,17 +37,18 @@ sub schema { while(<$F>) { chomp; next if /^\s*--/ || /^\s*$/; - next if /^\s*CREATE\s+TYPE/; - next if /^\s*CREATE\s+SEQUENCE/; + next if /^\s*CREATE\s+(?:TYPE|SEQUENCE|FUNCTION|DOMAIN|VIEW)/; if(/^\s*CREATE\s+TABLE\s+([^ ]+)/) { die "Unexpected 'CREATE TABLE $1'\n" if $table; + next if /PARTITION OF/; $table = $1; $schema{$table}{name} = $table; - $schema{$table}{dbentry_type} = $1 if /--.*\s+dbentry_type=(.)/; + $schema{$table}{comment} = /--\s*(.*)\s*/ ? $1 : ''; + $schema{$table}{dbentry_type} = $1 if $schema{$table}{comment} =~ s/\s*dbentry_type=(.)\s*//; $schema{$table}{cols} = []; - } elsif(/^\s*\);/) { + } elsif(/^\s*\)(?: PARTITION .+)?;/) { $table = undef; } elsif(/^\s+(?:CHECK|CONSTRAINT)/) { @@ -55,22 +58,19 @@ sub schema { die "Double primary key for '$table'?\n" if $schema{$table}{primary}; $schema{$table}{primary} = [ map s/\s*"?([^\s"]+)"?\s*/$1/r, split /,/, $1 ]; - } elsif($table && s/^\s+"?([^"\( ]+)"?\s+//) { + } elsif($table && s/^\s+([^"\( ]+)\s+//) { my $col = { name => $1 }; push @{$schema{$table}{cols}}, $col; - $col->{pub} = /--.*\[pub\]/; - s/,?\s*(?:--.*)?$//; + $col->{comment} = (s/,?\s*(?:--(.*))?$// && $1) || ''; + $col->{pub} = $col->{comment} =~ s/\s*\[pub\]\s*//; if(s/\s+PRIMARY\s+KEY//i) { die "Double primary key for '$table'?\n" if $schema{$table}{primary}; $schema{$table}{primary} = [ $col->{name} ]; } - $col->{decl} = "\"$col->{name}\" $_"; + $col->{decl} = "$col->{name} $_"; $col->{type} = lc s/^([^ ]+)\s.+/$1/r; - - } else { - die "Unrecognized line in schema.sql: $_\n"; } } @@ -89,7 +89,7 @@ sub types { open my $F, '<', "$ROOT/sql/schema.sql" or die "schema.sql: $!"; while(<$F>) { chomp; - if(/^CREATE TYPE ([^ ]+)/) { + if(/^CREATE (?:TYPE|DOMAIN) ([^ ]+)/) { $types{$1} = { decl => $_ }; } } @@ -118,9 +118,9 @@ sub references { decl => $_, from_table => $1, name => $2, - from_cols => [ map s/"//r, split /\s*,\s*/, $3 ], + from_cols => [ split /\s*,\s*/, $3 ], to_table => $4, - to_cols => [ map s/"//r, split /\s*,\s*/, $5 ] + to_cols => [ split /\s*,\s*/, $5 ] }; } \@ref diff --git a/lib/VNDB/Types.pm b/lib/VNDB/Types.pm index e13f8e33..16f730c5 100644 --- a/lib/VNDB/Types.pm +++ b/lib/VNDB/Types.pm @@ -15,52 +15,61 @@ sub hash { # SQL: ENUM language +# 'latin' indicates whether the language is primarily written in a latin-ish script. +# 'rank' is for quick selection of commonly used languages. hash LANGUAGE => - ar => 'Arabic', - bg => 'Bulgarian', - ca => 'Catalan', - cs => 'Czech', - da => 'Danish', - de => 'German', - el => 'Greek', - en => 'English', - eo => 'Esperanto', - es => 'Spanish', - fa => 'Persian', - fi => 'Finnish', - fr => 'French', - ga => 'Irish', - gd => 'Scottish Gaelic', - he => 'Hebrew', - hi => 'Hindi', - hr => 'Croatian', - hu => 'Hungarian', - id => 'Indonesian', - it => 'Italian', - ja => 'Japanese', - ko => 'Korean', - mk => 'Macedonian', - ms => 'Malay', - la => 'Latin', - lt => 'Lithuanian', - lv => 'Latvian', - nl => 'Dutch', - no => 'Norwegian', - pl => 'Polish', - 'pt-br' => 'Portuguese (Brazil)', - 'pt-pt' => 'Portuguese (Portugal)', - ro => 'Romanian', - ru => 'Russian', - sk => 'Slovak', - sl => 'Slovene', - sv => 'Swedish', - ta => 'Tagalog', - th => 'Thai', - tr => 'Turkish', - uk => 'Ukrainian', - ur => 'Urdu', - vi => 'Vietnamese', - zh => 'Chinese'; + ar => { latin => 0, rank => 0, txt => 'Arabic' }, + eu => { latin => 1, rank => 0, txt => 'Basque' }, + be => { latin => 0, rank => 0, txt => 'Belarusian' }, + bg => { latin => 1, rank => 0, txt => 'Bulgarian' }, + ca => { latin => 1, rank => 0, txt => 'Catalan' }, + ck => { latin => 0, rank => 0, txt => 'Cherokee' }, # 'chr' in ISO 639-2 but not present in ISO 639-1, let's just use an unassigned code + zh => { latin => 0, rank => 2, txt => 'Chinese' }, + 'zh-Hans'=> { latin => 0, rank => 2, txt => 'Chinese (simplified)' }, + 'zh-Hant'=> { latin => 0, rank => 2, txt => 'Chinese (traditional)' }, + hr => { latin => 1, rank => 0, txt => 'Croatian' }, + cs => { latin => 1, rank => 0, txt => 'Czech' }, + da => { latin => 1, rank => 0, txt => 'Danish' }, + nl => { latin => 1, rank => 0, txt => 'Dutch' }, + en => { latin => 1, rank => 3, txt => 'English' }, + eo => { latin => 1, rank => 0, txt => 'Esperanto' }, + fi => { latin => 1, rank => 0, txt => 'Finnish' }, + fr => { latin => 1, rank => 1, txt => 'French' }, + de => { latin => 1, rank => 1, txt => 'German' }, + el => { latin => 0, rank => 0, txt => 'Greek' }, + he => { latin => 0, rank => 0, txt => 'Hebrew' }, + hi => { latin => 0, rank => 0, txt => 'Hindi' }, + hu => { latin => 1, rank => 0, txt => 'Hungarian' }, + ga => { latin => 1, rank => 0, txt => 'Irish' }, + id => { latin => 1, rank => 0, txt => 'Indonesian' }, + it => { latin => 1, rank => 0, txt => 'Italian' }, + iu => { latin => 1, rank => 0, txt => 'Inuktitut' }, + ja => { latin => 0, rank => 4, txt => 'Japanese' }, + ko => { latin => 0, rank => 1, txt => 'Korean' }, + la => { latin => 1, rank => 0, txt => 'Latin' }, + lv => { latin => 1, rank => 0, txt => 'Latvian' }, + lt => { latin => 1, rank => 0, txt => 'Lithuanian' }, + mk => { latin => 1, rank => 0, txt => 'Macedonian' }, + ms => { latin => 1, rank => 0, txt => 'Malay' }, + no => { latin => 1, rank => 0, txt => 'Norwegian' }, + fa => { latin => 0, rank => 0, txt => 'Persian' }, + pl => { latin => 1, rank => 0, txt => 'Polish' }, + 'pt-br' => { latin => 1, rank => 1, txt => 'Portuguese (Brazil)' }, + 'pt-pt' => { latin => 1, rank => 1, txt => 'Portuguese (Portugal)' }, + ro => { latin => 1, rank => 0, txt => 'Romanian' }, + ru => { latin => 0, rank => 2, txt => 'Russian' }, + gd => { latin => 1, rank => 0, txt => 'Scottish Gaelic' }, + sr => { latin => 1, rank => 0, txt => 'Serbian' }, + sk => { latin => 0, rank => 0, txt => 'Slovak' }, + sl => { latin => 1, rank => 0, txt => 'Slovene' }, + es => { latin => 1, rank => 1, txt => 'Spanish' }, + sv => { latin => 1, rank => 0, txt => 'Swedish' }, + ta => { latin => 1, rank => 0, txt => 'Tagalog' }, + th => { latin => 0, rank => 0, txt => 'Thai' }, + tr => { latin => 1, rank => 0, txt => 'Turkish' }, + uk => { latin => 0, rank => 1, txt => 'Ukrainian' }, + ur => { latin => 0, rank => 0, txt => 'Urdu' }, + vi => { latin => 1, rank => 1, txt => 'Vietnamese' }; @@ -131,6 +140,22 @@ hash VN_RELATION => orig => { reverse => 'fan', pref => 0, txt => 'Original game' }; +hash DEVSTATUS => + 0 => 'Finished', + 1 => 'In development', + 2 => 'Cancelled'; + + +hash DRM_PROPERTY => # No DRM: https://lucide.dev/icons/unlock (needs circle?) + disc => 'Disc check', # https://lucide.dev/icons/disc-3 + cdkey => 'CD-key', # https://lucide.dev/icons/key-round (needs circle?) + activate => 'Online activation', # https://lucide.dev/icons/wifi (needs circle?) + alimit => 'Activation limit', + account => 'Account-based', # https://lucide.dev/icons/link (needs circle?) + online => 'Always online', + cloud => 'Cloud gaming', + physical => 'Physical'; # XXX: How does this relate to cdkey? + # SQL: ENUM producer_relation # "Pref" relations are considered the "preferred" relation to show (as opposed to their reverse) @@ -157,11 +182,14 @@ hash PRODUCER_TYPE => # SQL: ENUM credit_type hash CREDIT_TYPE => scenario => 'Scenario', + director => 'Director', chardesign => 'Character design', art => 'Artist', music => 'Composer', songs => 'Vocals', - director => 'Director', + translator => 'Translator', + editor => 'Editor', + qa => 'Quality assurance', staff => 'Staff'; @@ -198,7 +226,7 @@ hash TAG_CATEGORY => hash ANIMATED => 0 => { txt => 'Unknown' }, - 1 => { txt => 'No animations' }, + 1 => { txt => 'Not animated' }, 2 => { txt => 'Simple animations' }, 3 => { txt => 'Some fully animated scenes' }, 4 => { txt => 'All scenes fully animated' }; @@ -216,6 +244,7 @@ hash VOICED => hash AGE_RATING => 0 => { txt => 'All ages', ex => 'CERO A' }, + 3 => { txt => '3+', ex => '' }, 6 => { txt => '6+', ex => '' }, 7 => { txt => '7+', ex => '' }, 8 => { txt => '8+', ex => '' }, |