diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index e70d9b3dc..733f940e1 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -27,7 +27,8 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # Dropping copies of the files in the .devcontainer folder should also work, but I cba to duplicate my build files for this and symlinks aint working I hate computers RUN wget -P /tools https://raw.githubusercontent.com/Difegue/LANraragi/dev/tools/cpanfile \ && wget -P /tools https://raw.githubusercontent.com/Difegue/LANraragi/dev/tools/install.pl \ - && wget https://raw.githubusercontent.com/Difegue/LANraragi/dev/package.json + && wget https://raw.githubusercontent.com/Difegue/LANraragi/dev/package.json \ + && wget https://raw.githubusercontent.com/Difegue/LANraragi/dev/package-lock.json RUN npm run lanraragi-installer install-full diff --git a/.github/action-run-tests/entrypoint.sh b/.github/action-run-tests/entrypoint.sh index ed9ab8da6..ca656e28b 100644 --- a/.github/action-run-tests/entrypoint.sh +++ b/.github/action-run-tests/entrypoint.sh @@ -2,6 +2,9 @@ echo "🎌 Running LRR Test Suite 🎌" +# Install cpan deps in case some are missing +perl ./tools/install.pl install-back + # Run the perl tests on the repo prove -r -l -v tests/ diff --git a/.github/workflows/push-continous-delivery.yml b/.github/workflows/push-continous-delivery.yml index 7a5911c8b..2adffef01 100644 --- a/.github/workflows/push-continous-delivery.yml +++ b/.github/workflows/push-continous-delivery.yml @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@master - name: Docker Build and export run: | - docker build -t difegue/lanraragi -f ./tools/build/docker/Dockerfile-legacy . + docker build -t difegue/lanraragi -f ./tools/build/docker/Dockerfile --build-arg INSTALL_PARAMETER=-w . docker create --name rootfs difegue/lanraragi docker export --output=package.tar rootfs - name: Upload rootfs diff --git a/.github/workflows/release-delivery.yml b/.github/workflows/release-delivery.yml index 55a14cce5..2e5a2bca2 100644 --- a/.github/workflows/release-delivery.yml +++ b/.github/workflows/release-delivery.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@master - name: Docker Build and export run: | - docker build -t difegue/lanraragi -f ./tools/build/docker/Dockerfile-legacy . + docker build -t difegue/lanraragi -f ./tools/build/docker/Dockerfile --build-arg INSTALL_PARAMETER=-w . docker create --name rootfs difegue/lanraragi docker export --output=package.tar rootfs - name: Upload rootfs diff --git a/.gitignore b/.gitignore index 547271b9d..87f4b2d5d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,12 @@ content .minion.db* npm-debug.log dump.rdb -package-lock.json .vstags .gitbook *qemu-*-static* Dockerfile.* .DS_Store config.log -autobackup.json \ No newline at end of file +autobackup.json +Makefile +oshino diff --git a/lib/LANraragi.pm b/lib/LANraragi.pm index e6b833c41..358cd24de 100644 --- a/lib/LANraragi.pm +++ b/lib/LANraragi.pm @@ -11,9 +11,9 @@ use Storable; use Sys::Hostname; use Config; -use LANraragi::Utils::Generic qw(start_shinobu start_minion); -use LANraragi::Utils::Logging qw(get_logger get_logdir); -use LANraragi::Utils::Plugins qw(get_plugins); +use LANraragi::Utils::Generic qw(start_shinobu start_minion); +use LANraragi::Utils::Logging qw(get_logger get_logdir); +use LANraragi::Utils::Plugins qw(get_plugins); use LANraragi::Utils::TempFolder qw(get_temp); use LANraragi::Utils::Routing; use LANraragi::Utils::Minion; @@ -36,8 +36,19 @@ sub startup { my $vername = $packagejson->{version_name}; my $descstr = $packagejson->{description}; - # Use the hostname and osname for a sorta-unique set of secrets. - $self->secrets( [ hostname(), $Config{"osname"}, 'oshino' ] ); + my $secret = ""; + my $secretfile_path = $ENV{LRR_DATA_DIRECTORY} ? $ENV{LRR_DATA_DIRECTORY} . "/oshino" : "oshino"; + if ( -e $secretfile_path ) { + $secret = Mojo::File->new($secretfile_path)->slurp; + } else { + + # Generate a random string as the secret and store it in a file + $secret .= sprintf( "%x", rand 16 ) for 1 .. 8; + Mojo::File->new($secretfile_path)->spew($secret); + } + + # Use the hostname alongside the random secret + $self->secrets( [ $secret . hostname() ] ); $self->plugin('RenderFile'); # Set Template::Toolkit as default renderer so we can use the LRR templates @@ -70,19 +81,11 @@ sub startup { die; } - # Check old settings and migrate them if needed - if ( $self->LRR_CONF->get_redis->keys('LRR_*') ) { - say "Migrating old settings to new format..."; - migrate_old_settings($self); - } - - my $devmode; - # Catch Redis errors on our first connection. This is useful in case of temporary LOADING errors, # Where Redis lets us send commands but doesn't necessarily reply to them properly. # (https://github.com/redis/redis/issues/4624) while (1) { - eval { $devmode = $self->LRR_CONF->enable_devmode; }; + eval { $self->LRR_CONF->get_redis->keys('*') }; last unless ($@); @@ -91,13 +94,13 @@ sub startup { sleep 2; } - # Enable AOF saving on the Redis server. - # This allows us to start creating an aof file using existing RDB snapshot data. - # Later LRR releases will then be able to set appendonly directly in redis.conf without fearing data loss. - say "Enabling AOF on Redis... This might take a while."; - $self->LRR_CONF->get_redis->config_set( "appendonly", "yes" ); + # Check old settings and migrate them if needed + if ( $self->LRR_CONF->get_redis->keys('LRR_*') ) { + say "Migrating old settings to new format..."; + migrate_old_settings($self); + } - if ($devmode) { + if ( $self->LRR_CONF->enable_devmode ) { $self->mode('development'); $self->LRR_LOGGER->info("LANraragi $version (re-)started. (Debug Mode)"); @@ -111,7 +114,9 @@ sub startup { open( my $fh, '>>', $logpath ) or die "Could not open file '$logpath' $!"; - print $fh "[Mojolicious] " . $lines[0] . " " . $lines[1] . "\n"; + my $l1 = $lines[0] // ""; + my $l2 = $lines[1] // ""; + print $fh "[Mojolicious] $l1 $l2 \n"; close $fh; } ); @@ -143,9 +148,14 @@ sub startup { # Enable Minion capabilities in the app shutdown_from_pid( get_temp . "/minion.pid" ); - my $miniondb = $self->LRR_CONF->get_redisad . "/" . $self->LRR_CONF->get_miniondb; + my $miniondb = $self->LRR_CONF->get_redisad . "/" . $self->LRR_CONF->get_miniondb; + my $redispassword = $self->LRR_CONF->get_redispassword; + + # If the password is non-empty, add the required delimiters + if ($redispassword) { $redispassword = "x:" . $redispassword . "@"; } + say "Minion will use the Redis database at $miniondb"; - $self->plugin( 'Minion' => { Redis => "redis://$miniondb" } ); + $self->plugin( 'Minion' => { Redis => "redis://$redispassword$miniondb" } ); $self->LRR_LOGGER->info("Successfully connected to Minion database."); $self->minion->missing_after(5); # Clean up older workers after 5 seconds of unavailability @@ -199,7 +209,7 @@ sub add_sigint_handler { shutdown_from_pid( get_temp . "/minion.pid" ); \&$old_int; # Calling the old handler to cleanly exit the server - } + } } sub migrate_old_settings { diff --git a/lib/LANraragi/Controller/Api/Archive.pm b/lib/LANraragi/Controller/Api/Archive.pm index 9a8e317a6..d5f4687e2 100644 --- a/lib/LANraragi/Controller/Api/Archive.pm +++ b/lib/LANraragi/Controller/Api/Archive.pm @@ -4,10 +4,10 @@ use Mojo::Base 'Mojolicious::Controller'; use Redis; use Encode; use Storable; -use Mojo::JSON qw(decode_json); +use Mojo::JSON qw(decode_json); use Scalar::Util qw(looks_like_number); -use LANraragi::Utils::Generic qw(render_api_response); +use LANraragi::Utils::Generic qw(render_api_response); use LANraragi::Utils::Database qw(get_archive_json set_isnew); use LANraragi::Model::Archive; @@ -64,7 +64,7 @@ sub serve_metadata { sub get_categories { my $self = shift; - my $id = check_id_parameter( $self, "find_arc_categories" ) || return; + my $id = check_id_parameter( $self, "find_arc_categories" ) || return; my @categories = LANraragi::Model::Category::get_categories_containing_archive($id); @@ -79,13 +79,13 @@ sub get_categories { sub serve_thumbnail { my $self = shift; - my $id = check_id_parameter( $self, "serve_thumbnail" ) || return; + my $id = check_id_parameter( $self, "serve_thumbnail" ) || return; LANraragi::Model::Archive::serve_thumbnail( $self, $id ); } sub update_thumbnail { my $self = shift; - my $id = check_id_parameter( $self, "update_thumbnail" ) || return; + my $id = check_id_parameter( $self, "update_thumbnail" ) || return; LANraragi::Model::Archive::update_thumbnail( $self, $id ); } @@ -105,14 +105,14 @@ sub serve_file { sub serve_page { my $self = shift; my $id = check_id_parameter( $self, "serve_page" ) || return; - my $path = $self->req->param('path') || "404.xyz"; + my $path = $self->req->param('path') || "404.xyz"; LANraragi::Model::Archive::serve_page( $self, $id, $path ); } sub get_file_list { my $self = shift; - my $id = check_id_parameter( $self, "get_file_list" ) || return; + my $id = check_id_parameter( $self, "get_file_list" ) || return; my $force = $self->req->param('force') eq "true" || "0"; my $reader_json; @@ -129,7 +129,7 @@ sub get_file_list { sub clear_new { my $self = shift; - my $id = check_id_parameter( $self, "clear_new" ) || return; + my $id = check_id_parameter( $self, "clear_new" ) || return; set_isnew( $id, "false" ); @@ -144,7 +144,7 @@ sub clear_new { sub delete_archive { my $self = shift; - my $id = check_id_parameter( $self, "delete_archive" ) || return; + my $id = check_id_parameter( $self, "delete_archive" ) || return; my $delStatus = LANraragi::Utils::Database::delete_archive($id); @@ -160,7 +160,7 @@ sub delete_archive { sub update_metadata { my $self = shift; - my $id = check_id_parameter( $self, "update_metadata" ) || return; + my $id = check_id_parameter( $self, "update_metadata" ) || return; my $title = $self->req->param('title'); my $tags = $self->req->param('tags'); @@ -176,9 +176,10 @@ sub update_metadata { sub update_progress { my $self = shift; - my $id = check_id_parameter( $self, "update_progress" ) || return; + my $id = check_id_parameter( $self, "update_progress" ) || return; my $page = $self->stash('page') || 0; + my $time = time(); # Undocumented parameter to force progress update my $force = $self->req->param('force') || 0; @@ -205,7 +206,8 @@ sub update_progress { } # Just set the progress value. - $redis->hset( $id, "progress", $page ); + $redis->hset( $id, "progress", $page ); + $redis->hset( $id, "lastreadtime", $time ); # Update total pages read statistic $redis_cfg->incr("LRR_TOTALPAGESTAT"); @@ -215,10 +217,11 @@ sub update_progress { $self->render( json => { - operation => "update_progress", - id => $id, - page => $page, - success => 1 + operation => "update_progress", + id => $id, + page => $page, + lastreadtime => $time, + success => 1 } ); diff --git a/lib/LANraragi/Controller/Api/Category.pm b/lib/LANraragi/Controller/Api/Category.pm index 6e9704b10..9bb7e3aa6 100644 --- a/lib/LANraragi/Controller/Api/Category.pm +++ b/lib/LANraragi/Controller/Api/Category.pm @@ -102,10 +102,10 @@ sub add_to_category { if ($result) { my $successMessage = "Added $arcid to Category $catid!"; - my %category = LANraragi::Model::Category::get_category($catid); - my $title = LANraragi::Model::Archive::get_title($arcid); + my %category = LANraragi::Model::Category::get_category($catid); + my $title = LANraragi::Model::Archive::get_title($arcid); - if (%category && defined($title)) { + if ( %category && defined($title) ) { $successMessage = "Added \"$title\" to category \"$category{name}\"!"; } @@ -124,7 +124,15 @@ sub remove_from_category { my ( $result, $err ) = LANraragi::Model::Category::remove_from_category( $catid, $arcid ); if ($result) { - render_api_response( $self, "remove_from_category" ); + my $successMessage = "Removed $arcid from Category $catid!"; + my %category = LANraragi::Model::Category::get_category($catid); + my $title = LANraragi::Model::Archive::get_title($arcid); + + if ( %category && defined($title) ) { + $successMessage = "Removed \"$title\" from category \"$category{name}\"!"; + } + + render_api_response( $self, "remove_from_category", undef, $successMessage ); } else { render_api_response( $self, "remove_from_category", $err ); } diff --git a/lib/LANraragi/Controller/Api/Tankoubon.pm b/lib/LANraragi/Controller/Api/Tankoubon.pm new file mode 100644 index 000000000..4a358e51c --- /dev/null +++ b/lib/LANraragi/Controller/Api/Tankoubon.pm @@ -0,0 +1,163 @@ +package LANraragi::Controller::Api::Tankoubon; +use Mojo::Base 'Mojolicious::Controller'; + +use Redis; +use Encode; + +use LANraragi::Model::Tankoubon; +use LANraragi::Utils::Generic qw(render_api_response); + +sub get_tankoubon_list { + + my $self = shift; + my $req = $self->req; + + my $page = $req->param('page'); + + my ( $total, $filtered, @rgs ) = LANraragi::Model::Tankoubon::get_tankoubon_list($page); + $self->render( json => {result => \@rgs, total => $total, filtered => $filtered} ); + +} + +sub get_tankoubon { + + my $self = shift; + my $tank_id = $self->stash('id'); + my $req = $self->req; + + my $fulldata = $req->param('include_full_data'); + my $page = $req->param('page'); + + my ( $total, $filtered, %tankoubon ) = LANraragi::Model::Tankoubon::get_tankoubon($tank_id, $fulldata, $page); + + unless (%tankoubon) { + render_api_response( $self, "get_tankoubon", "The given tankoubon does not exist." ); + return; + } + + $self->render( json => {result => \%tankoubon, total => $total, filtered => $filtered} ); +} + +sub create_tankoubon { + + my $self = shift; + my $name = $self->req->param('name') || ""; + my $tankid = $self->req->param('tankid') || ""; + + if ( $name eq "" ) { + render_api_response( $self, "create_tankoubon", "Tankoubon name not specified." ); + return; + } + + my $created_id = LANraragi::Model::Tankoubon::create_tankoubon( $name, $tankid ); + $self->render( + json => { + operation => "create_tankoubon", + tankoubon_id => $created_id, + success => 1 + } + ); + +} + +sub delete_tankoubon { + + my $self = shift; + my $tankid = $self->stash('id'); + + my $result = LANraragi::Model::Tankoubon::delete_tankoubon($tankid); + + if ($result) { + render_api_response( $self, "delete_tankoubon" ); + } else { + render_api_response( $self, "delete_tankoubon", "The given tankoubon does not exist." ); + } +} + +sub update_archive_list { + + my $self = shift; + my $tankid = $self->stash('id'); + my $data = $self->req->json; + + my ( $result, $err ) = LANraragi::Model::Tankoubon::update_archive_list( $tankid, $data ); + + if ($result) { + my %tankoubon = LANraragi::Model::Tankoubon::get_tankoubon($tankid); + my $successMessage = "Updated archives of tankoubon \"$tankoubon{name}\"!"; + + render_api_response( $self, "update_archive_list", undef, $successMessage ); + } else { + render_api_response( $self, "update_archive_list", $err ); + } +} + +sub add_to_tankoubon { + + my $self = shift; + my $tankid = $self->stash('id'); + my $arcid = $self->stash('archive'); + + my ( $result, $err ) = LANraragi::Model::Tankoubon::add_to_tankoubon( $tankid, $arcid ); + + if ($result) { + my $successMessage = "Added $arcid to tankoubon $tankid!"; + my %tankoubon = LANraragi::Model::Tankoubon::get_tankoubon($tankid); + my $title = LANraragi::Model::Archive::get_title($arcid); + + if ( %tankoubon && defined($title) ) { + $successMessage = "Added \"$title\" to tankoubon \"$tankoubon{name}\"!"; + } + + render_api_response( $self, "add_to_tankoubon", undef, $successMessage ); + } else { + render_api_response( $self, "add_to_tankoubon", $err ); + } +} + +sub remove_from_tankoubon { + + my $self = shift; + my $tankid = $self->stash('id'); + my $arcid = $self->stash('archive'); + + my ( $result, $err ) = LANraragi::Model::Tankoubon::remove_from_tankoubon( $tankid, $arcid ); + + if ($result) { + my $successMessage = "Removed $arcid from tankoubon $tankid!"; + my %tankoubon = LANraragi::Model::Tankoubon::get_tankoubon($tankid); + my $title = LANraragi::Model::Archive::get_title($arcid); + + if ( %tankoubon && defined($title) ) { + $successMessage = "Removed \"$title\" from tankoubon \"$tankoubon{name}\"!"; + } + + render_api_response( $self, "remove_from_tankoubon", undef, $successMessage ); + } else { + render_api_response( $self, "remove_from_tankoubon", $err ); + } +} + +sub get_tankoubons_file { + + my $self = shift; + my $arcid = $self->stash('id'); + + if ( $arcid eq "" ) { + render_api_response( $self, "get_tankoubons_file", "Archive not specified." ); + return; + } + + my @tanks = LANraragi::Model::Tankoubon::get_tankoubons_file( $arcid ); + + $self->render( + json => { + operation => "find_arc_tankoubons", + tankoubons => \@tanks, + success => 1 + } + ); +} + +1; + diff --git a/lib/LANraragi/Controller/Batch.pm b/lib/LANraragi/Controller/Batch.pm index d2fb3b83a..4be7ad727 100644 --- a/lib/LANraragi/Controller/Batch.pm +++ b/lib/LANraragi/Controller/Batch.pm @@ -5,11 +5,11 @@ use Redis; use Encode; use Mojo::JSON qw(decode_json); -use LANraragi::Utils::Generic qw(generate_themes_header); -use LANraragi::Utils::Tags qw(rewrite_tags split_tags_to_array restore_CRLF); -use LANraragi::Utils::Database qw(get_computed_tagrules set_tags set_title set_isnew invalidate_cache); -use LANraragi::Utils::Plugins qw(get_plugins get_plugin get_plugin_parameters); -use LANraragi::Utils::Logging qw(get_logger); +use LANraragi::Utils::Generic qw(generate_themes_header); +use LANraragi::Utils::Tags qw(rewrite_tags split_tags_to_array restore_CRLF); +use LANraragi::Utils::Database qw(redis_decode get_computed_tagrules set_tags set_title set_isnew invalidate_cache); +use LANraragi::Utils::Plugins qw(get_plugins get_plugin get_plugin_parameters); +use LANraragi::Utils::Logging qw(get_logger); # This action will render a template sub index { @@ -52,6 +52,12 @@ sub socket { message => sub { my ( $self, $msg ) = @_; + $logger->debug("Received WS message $msg"); + + # encode message before json-decoding it in case it has UTF8 characters in the argument overrides + $msg = encode( 'UTF-8', $msg ); + $logger->trace("Encoded message $msg"); + # JSON-decode message and perform the requested action my $command = decode_json($msg); my $operation = $command->{'operation'}; @@ -62,7 +68,6 @@ sub socket { $client->finish( 1001 => 'No archives provided.' ); return; } - $logger->debug("Processing $id"); if ( $operation eq "plugin" ) { @@ -80,6 +85,10 @@ sub socket { # Try getting the saved defaults @args = get_plugin_parameters($pluginname); + } else { + + # Decode user overrides + @args = map { redis_decode($_) } @args; } # Send reply message for completed archive @@ -154,7 +163,7 @@ sub socket { id => $id, filename => $delStatus, message => $delStatus ? "Archive deleted." : "Archive not found.", - success => $delStatus ? 1 : 0 + success => $delStatus ? 1 : 0 } } ); diff --git a/lib/LANraragi/Controller/Category.pm b/lib/LANraragi/Controller/Category.pm index 46a8d9b3d..a7fac8a95 100644 --- a/lib/LANraragi/Controller/Category.pm +++ b/lib/LANraragi/Controller/Category.pm @@ -21,25 +21,18 @@ sub index { $redis->quit(); - #Then complete it with the rest from the database. - #40-character long keys only => Archive IDs - my @keys = $redis->keys('????????????????????????????????????????'); - + my @idlist = LANraragi::Model::Archive::generate_archive_list; #Parse the archive list and build
  • elements accordingly. my $arclist = ""; #Only show IDs that still have their files present. - foreach my $id (@keys) { - my $zipfile = $redis->hget( $id, "file" ); - my $title = $redis->hget( $id, "title" ); - $title = redis_decode($title); - $title = xml_escape($title); - - if ( -e $zipfile ) { - $arclist .= - "
  • "; - $arclist .= "
  • "; - } + foreach my $arc (@idlist) { + my $title = xml_escape($arc->{title}); + my $id = xml_escape($arc->{arcid}); + + $arclist .= + "
  • "; + $arclist .= "
  • "; } $redis->quit(); diff --git a/lib/LANraragi/Controller/Config.pm b/lib/LANraragi/Controller/Config.pm index 2cd2c999d..eaccd08e0 100644 --- a/lib/LANraragi/Controller/Config.pm +++ b/lib/LANraragi/Controller/Config.pm @@ -1,11 +1,12 @@ package LANraragi::Controller::Config; use Mojo::Base 'Mojolicious::Controller'; -use LANraragi::Utils::Generic qw(generate_themes_header remove_spaces remove_newlines); -use LANraragi::Utils::Database qw(redis_encode save_computed_tagrules); +use LANraragi::Utils::Generic qw(generate_themes_header); +use LANraragi::Utils::String qw(trim trim_CRLF); +use LANraragi::Utils::Database qw(redis_encode save_computed_tagrules); use LANraragi::Utils::TempFolder qw(get_tempsize); -use LANraragi::Utils::Tags qw(tags_rules_to_array replace_CRLF restore_CRLF); -use Mojo::JSON qw(encode_json); +use LANraragi::Utils::Tags qw(tags_rules_to_array replace_CRLF restore_CRLF); +use Mojo::JSON qw(encode_json); use Authen::Passphrase::BlowfishCrypt; @@ -22,7 +23,7 @@ sub index { motd => $self->LRR_CONF->get_motd, dirname => $self->LRR_CONF->get_userdir, thumbdir => $self->LRR_CONF->get_thumbdir, - forceddirname => ( defined $ENV{LRR_DATA_DIRECTORY} ? 1 : 0 ), + forceddirname => ( defined $ENV{LRR_DATA_DIRECTORY} ? 1 : 0 ), forcedthumbdir => ( defined $ENV{LRR_THUMB_DIRECTORY} ? 1 : 0 ), pagesize => $self->LRR_CONF->get_pagesize, enablepass => $self->LRR_CONF->enable_pass, @@ -44,6 +45,7 @@ sub index { usedatemodified => $self->LRR_CONF->use_lastmodified, enablecryptofs => $self->LRR_CONF->enable_cryptofs, hqthumbpages => $self->LRR_CONF->get_hqthumbpages, + jxlthumbpages => $self->LRR_CONF->get_jxlthumbpages, csshead => generate_themes_header($self), tempsize => get_tempsize, replacedupe => $self->LRR_CONF->get_replacedupe @@ -85,6 +87,7 @@ sub save_config { usedatemodified => ( scalar $self->req->param('usedatemodified') ? '1' : '0' ), enablecryptofs => ( scalar $self->req->param('enablecryptofs') ? '1' : '0' ), hqthumbpages => ( scalar $self->req->param('hqthumbpages') ? '1' : '0' ), + jxlthumbpages => ( scalar $self->req->param('jxlthumbpages') ? '1' : '0' ), replacedupe => ( scalar $self->req->param('replacedupe') ? '1' : '0' ), ); @@ -123,20 +126,29 @@ sub save_config { #Did all the checks pass ? if ($success) { - # Clean up the user's inputs for non-toggle options and encode for redis insertion + $redis->watch("LRR_CONFIG"); + $redis->multi; + foreach my $key ( keys %confhash ) { - remove_spaces( $confhash{$key} ); - remove_newlines( $confhash{$key} ); - $confhash{$key} = redis_encode( $confhash{$key} ); - $self->LRR_LOGGER->debug( "Saving $key with value " . $confhash{$key} ); + my $value = $confhash{$key}; + + if ( $value ne '0' && $value ne '1' ) { + + # Clean up the user's inputs for non-toggle options and encode for redis insertion + $value = trim($value); + $value = trim_CRLF($value); + $value = redis_encode($value); + } + + # For all keys of the hash, add them to the redis config hash with the matching keys. + $self->LRR_LOGGER->debug( "Saving $key with value " . $value ); + $redis->hset( "LRR_CONFIG", $key, $value ); } - #for all keys of the hash, add them to the redis config hash with the matching keys. - $redis->hset( "LRR_CONFIG", $_, $confhash{$_}, sub { } ) for keys %confhash; - $redis->wait_all_responses; + $redis->exec; } - $redis->quit(); + $redis->quit; my @computed_tagrules = tags_rules_to_array( $self->req->param('tagrules') ); $self->LRR_LOGGER->debug( "Saving computed tag rules : " . encode_json( \@computed_tagrules ) ); diff --git a/lib/LANraragi/Controller/Plugins.pm b/lib/LANraragi/Controller/Plugins.pm index 1816a7481..b8140a762 100644 --- a/lib/LANraragi/Controller/Plugins.pm +++ b/lib/LANraragi/Controller/Plugins.pm @@ -23,15 +23,16 @@ sub index { my @downloadplugins = get_plugins("download"); $self->render( - template => "plugins", - title => $self->LRR_CONF->get_htmltitle, - descstr => $self->LRR_DESC, - metadata => craft_plugin_array(@metaplugins), - downloaders => craft_plugin_array(@downloadplugins), - logins => craft_plugin_array(@loginplugins), - scripts => craft_plugin_array(@scriptplugins), - csshead => generate_themes_header($self), - version => $self->LRR_VERSION + template => "plugins", + title => $self->LRR_CONF->get_htmltitle, + descstr => $self->LRR_DESC, + replacetitles => $self->LRR_CONF->can_replacetitles, + metadata => craft_plugin_array(@metaplugins), + downloaders => craft_plugin_array(@downloadplugins), + logins => craft_plugin_array(@loginplugins), + scripts => craft_plugin_array(@scriptplugins), + csshead => generate_themes_header($self), + version => $self->LRR_VERSION ); } @@ -81,6 +82,12 @@ sub save_config { my $errormess = ""; eval { + + # Save title preference first + my $replacetitles = ( scalar $self->req->param('replacetitles') ? '1' : '0' ); + $redis->hset( "LRR_CONFIG", "replacetitles", $replacetitles ); + + # Save each plugin's settings foreach my $pluginfo (@plugins) { my $namespace = $pluginfo->{namespace}; @@ -172,7 +179,7 @@ sub process_upload { return; } - my $dir = getcwd() . ("/lib/LANraragi/Plugin/$plugintype/"); + my $dir = getcwd() . ("/lib/LANraragi/Plugin/$plugintype/"); my $output_file = $dir . $filename; $logger->info("Uploading new plugin $filename to $output_file ..."); diff --git a/lib/LANraragi/Controller/Reader.pm b/lib/LANraragi/Controller/Reader.pm index 28bb84550..a6b9370d9 100644 --- a/lib/LANraragi/Controller/Reader.pm +++ b/lib/LANraragi/Controller/Reader.pm @@ -15,7 +15,8 @@ sub index { if ( $self->req->param('id') ) { # Allow adding to static categories - my @categories = LANraragi::Model::Category->get_static_category_list; + my @categories = LANraragi::Model::Category->get_static_category_list; + my @arc_categories = LANraragi::Model::Category::get_categories_containing_archive( $self->req->param('id') ); # Get query string from referrer URL, if there's one my $referrer = $self->req->headers->referrer; @@ -26,15 +27,16 @@ sub index { } $self->render( - template => "reader", - title => $self->LRR_CONF->get_htmltitle, - use_local => $self->LRR_CONF->enable_localprogress, - id => $self->req->param('id'), - categories => \@categories, - csshead => generate_themes_header($self), - version => $self->LRR_VERSION, - ref_query => $query, - userlogged => $self->LRR_CONF->enable_pass == 0 || $self->session('is_logged') + template => "reader", + title => $self->LRR_CONF->get_htmltitle, + use_local => $self->LRR_CONF->enable_localprogress, + id => $self->req->param('id'), + arc_categories => \@arc_categories, + categories => \@categories, + csshead => generate_themes_header($self), + version => $self->LRR_VERSION, + ref_query => $query, + userlogged => $self->LRR_CONF->enable_pass == 0 || $self->session('is_logged') ); } else { diff --git a/lib/LANraragi/Controller/Tankoubon.pm b/lib/LANraragi/Controller/Tankoubon.pm new file mode 100644 index 000000000..1185f1667 --- /dev/null +++ b/lib/LANraragi/Controller/Tankoubon.pm @@ -0,0 +1,33 @@ +package LANraragi::Controller::Tankoubon; +use Mojo::Base 'Mojolicious::Controller'; + +use utf8; +use URI::Escape; +use Redis; +use Encode; +use Mojo::Util qw(xml_escape); + +use LANraragi::Utils::Generic qw(generate_themes_header); +use LANraragi::Utils::Database qw(redis_decode); + +# Go through the archives in the content directory and build the template at the end. +sub index { + + my $self = shift; + my $redis = $self->LRR_CONF->get_redis; + my $force = 0; + + my $userlogged = $self->LRR_CONF->enable_pass == 0 || $self->session('is_logged'); + + $redis->quit(); + + $self->render( + template => "tankoubon", + title => $self->LRR_CONF->get_htmltitle, + descstr => $self->LRR_DESC, + csshead => generate_themes_header($self), + version => $self->LRR_VERSION + ); +} + +1; \ No newline at end of file diff --git a/lib/LANraragi/Model/Archive.pm b/lib/LANraragi/Model/Archive.pm index 73e824ddd..30e47e480 100644 --- a/lib/LANraragi/Model/Archive.pm +++ b/lib/LANraragi/Model/Archive.pm @@ -14,7 +14,8 @@ use File::Basename; use File::Copy "cp"; use File::Path qw(make_path); -use LANraragi::Utils::Generic qw(remove_spaces remove_newlines render_api_response); +use LANraragi::Utils::Generic qw(render_api_response); +use LANraragi::Utils::String qw(trim trim_CRLF); use LANraragi::Utils::TempFolder qw(get_temp); use LANraragi::Utils::Logging qw(get_logger); use LANraragi::Utils::Archive qw(extract_single_file extract_thumbnail); @@ -34,7 +35,7 @@ sub get_title($id) { return (); } - return $redis->hget( $id, "title" ); + return redis_decode( $redis->hget( $id, "title" ) ); } # Functions used when dealing with archives. @@ -57,11 +58,13 @@ sub update_thumbnail { $page = 1 unless $page; my $thumbdir = LANraragi::Model::Config->get_thumbdir; + my $use_jxl = LANraragi::Model::Config->get_jxlthumbpages; + my $format = $use_jxl ? 'jxl' : 'jpg'; # Thumbnails are stored in the content directory, thumb subfolder. # Another subfolder with the first two characters of the id is used for FS optimization. my $subfolder = substr( $id, 0, 2 ); - my $thumbname = "$thumbdir/$subfolder/$id.jpg"; # Path to main thumbnail + my $thumbname = "$thumbdir/$subfolder/$id.$format"; # Path to main thumbnail my $newthumb = ""; @@ -98,15 +101,23 @@ sub serve_thumbnail { my $no_fallback = $self->req->param('no_fallback'); $no_fallback = ( $no_fallback && $no_fallback eq "true" ) || "0"; # Prevent undef warnings by checking the variable first - my $thumbdir = LANraragi::Model::Config->get_thumbdir; + my $thumbdir = LANraragi::Model::Config->get_thumbdir; + my $use_jxl = LANraragi::Model::Config->get_jxlthumbpages; + my $format = $use_jxl ? 'jxl' : 'jpg'; + my $fallback_format = $format eq 'jxl' ? 'jpg' : 'jxl'; # Thumbnails are stored in the content directory, thumb subfolder. # Another subfolder with the first two characters of the id is used for FS optimization. my $subfolder = substr( $id, 0, 2 ); - my $thumbname = "$thumbdir/$subfolder/$id.jpg"; - if ( $page > 0 ) { - $thumbname = "$thumbdir/$subfolder/$id/$page.jpg"; + # Check for the page and set the appropriate thumbnail name and fallback thumbnail name + my $thumbbase = ( $page - 1 > 0 ) ? "$thumbdir/$subfolder/$id/$page" : "$thumbdir/$subfolder/$id"; + my $thumbname = "$thumbbase.$format"; + my $fallback_thumbname = "$thumbbase.$fallback_format"; + + # Check if the preferred format thumbnail exists, if not, try the alternate format + unless ( -e $thumbname ) { + $thumbname = $fallback_thumbname; } # Queue a minion job to generate the thumbnail. Thumbnail jobs have the lowest priority. @@ -114,7 +125,6 @@ sub serve_thumbnail { my $job_id = $self->minion->enqueue( thumbnail_task => [ $thumbdir, $id, $page ] => { priority => 0, attempts => 3 } ); if ($no_fallback) { - $self->render( json => { operation => "serve_thumbnail", @@ -129,7 +139,6 @@ sub serve_thumbnail { $self->render_file( filepath => "./public/img/noThumb.png" ); } return; - } else { # Simply serve the thumbnail. @@ -170,7 +179,7 @@ sub serve_page { # Extract the file from the parent archive if it doesn't exist $logger->debug("Extracting missing file"); - my $redis = LANraragi::Model::Config->get_redis; + my $redis = LANraragi::Model::Config->get_redis; my $archive = $redis->hget( $id, "file" ); $redis->quit(); @@ -243,8 +252,8 @@ sub update_metadata { } # Clean up the user's inputs and encode them. - ( remove_spaces($_) ) for ( $title, $tags ); - ( remove_newlines($_) ) for ( $title, $tags ); + ( $_ = trim($_) ) for ( $title, $tags ); + ( $_ = trim_CRLF($_) ) for ( $title, $tags ); if ( defined $title ) { set_title( $id, $title ); diff --git a/lib/LANraragi/Model/Backup.pm b/lib/LANraragi/Model/Backup.pm index b9920c222..e186784ad 100644 --- a/lib/LANraragi/Model/Backup.pm +++ b/lib/LANraragi/Model/Backup.pm @@ -9,14 +9,15 @@ use Mojo::JSON qw(decode_json encode_json); use LANraragi::Model::Category; use LANraragi::Utils::Database; -use LANraragi::Utils::Generic qw(remove_newlines); +use LANraragi::Utils::String qw(trim_CRLF); use LANraragi::Utils::Database qw(redis_encode redis_decode invalidate_cache set_title set_tags); use LANraragi::Utils::Logging qw(get_logger); #build_backup_JSON() #Goes through the Redis archive IDs and builds a JSON string containing their metadata. sub build_backup_JSON { - my $redis = LANraragi::Model::Config->get_redis; + my $redis = LANraragi::Model::Config->get_redis; + my $logger = get_logger( "Backup/Restore", "lanraragi" ); # Basic structure of the backup object my %backup = ( @@ -49,6 +50,8 @@ sub build_backup_JSON { push @{ $backup{categories} }, \%category; }; + $logger->trace("Backing up category $key: $@"); + } # Backup archives themselves next @@ -62,7 +65,7 @@ sub build_backup_JSON { my ( $name, $title, $tags, $thumbhash ) = @hash{qw(name title tags thumbhash)}; ( $_ = redis_decode($_) ) for ( $name, $title, $tags ); - ( remove_newlines($_) ) for ( $name, $title, $tags ); + ( $_ = trim_CRLF($_) ) for ( $name, $title, $tags ); # Backup all user-generated metadata, alongside the unique ID. my %arc = ( @@ -75,6 +78,9 @@ sub build_backup_JSON { push @{ $backup{archives} }, \%arc; }; + + $logger->trace("Backing up archive $id: $@"); + } $redis->quit(); diff --git a/lib/LANraragi/Model/Config.pm b/lib/LANraragi/Model/Config.pm index f52056d43..075376101 100644 --- a/lib/LANraragi/Model/Config.pm +++ b/lib/LANraragi/Model/Config.pm @@ -16,6 +16,9 @@ my $home = Mojo::Home->new; $home->detect; my $config = Mojolicious::Plugin::Config->register( Mojolicious->new, { file => $home . '/lrr.conf' } ); +if ( $ENV{LRR_REDIS_ADDRESS} ) { + $config->{redis_address} = $ENV{LRR_REDIS_ADDRESS}; +} # Address and port of your redis instance. sub get_redisad { return $config->{redis_address} } @@ -42,8 +45,8 @@ sub get_minion { my $miniondb = get_redisad . "/" . get_miniondb; my $password = get_redispassword; - # If the password is non-empty, add the required @ - if ($password) { $password = $password . "@"; } + # If the password is non-empty, add the required delimiters + if ($password) { $password = "x:" . $password . "@"; } return Minion->new( Redis => "redis://$password$miniondb" ); } @@ -182,6 +185,8 @@ sub enable_dateadded { return &get_redis_conf( "usedateadded", "1" ) } sub use_lastmodified { return &get_redis_conf( "usedatemodified", "0" ) } sub enable_cryptofs { return &get_redis_conf( "enablecryptofs", "0" ) } sub get_hqthumbpages { return &get_redis_conf( "hqthumbpages", "0" ) } +sub get_jxlthumbpages { return &get_redis_conf( "jxlthumbpages", "0" ) } sub get_replacedupe { return &get_redis_conf( "replacedupe", "0" ) } +sub can_replacetitles { return &get_redis_conf( "replacetitles", "1" ) } 1; diff --git a/lib/LANraragi/Model/Opds.pm b/lib/LANraragi/Model/Opds.pm index f31a5c95c..6e4daec91 100644 --- a/lib/LANraragi/Model/Opds.pm +++ b/lib/LANraragi/Model/Opds.pm @@ -117,6 +117,10 @@ sub get_opds_data { $arcdata->{mimetype} = "application/x-cbz"; } + if ( $arcdata->{lastreadtime} > 0) { + $arcdata->{lastreaddate} = strftime( "%Y-%m-%dT%H:%M:%SZ", gmtime($arcdata->{lastreadtime}) ); + } + for ( values %{$arcdata} ) { $_ = xml_escape($_); } return $arcdata; diff --git a/lib/LANraragi/Model/Plugins.pm b/lib/LANraragi/Model/Plugins.pm index e7220a681..a16e97320 100644 --- a/lib/LANraragi/Model/Plugins.pm +++ b/lib/LANraragi/Model/Plugins.pm @@ -11,16 +11,16 @@ use Mojo::JSON qw(decode_json encode_json); use Mojo::UserAgent; use Data::Dumper; -use LANraragi::Utils::Generic qw(remove_spaces remove_newlines); +use LANraragi::Utils::String qw(trim); use LANraragi::Utils::Database qw(set_tags set_title); -use LANraragi::Utils::Archive qw(extract_thumbnail); -use LANraragi::Utils::Logging qw(get_logger); -use LANraragi::Utils::Tags qw(rewrite_tags split_tags_to_array); +use LANraragi::Utils::Archive qw(extract_thumbnail); +use LANraragi::Utils::Logging qw(get_logger); +use LANraragi::Utils::Tags qw(rewrite_tags split_tags_to_array); # Sub used by Auto-Plugin. sub exec_enabled_plugins_on_file { - my $id = shift; + my $id = shift; my $logger = get_logger( "Auto-Plugin", "lanraragi" ); $logger->info("Executing enabled metadata plugins on archive with id $id."); @@ -33,6 +33,7 @@ sub exec_enabled_plugins_on_file { my @plugins = LANraragi::Utils::Plugins::get_enabled_plugins("metadata"); # If the regex plugin is in the list, make sure it's ran first. + # TODO: Make plugin exec order configurable foreach my $plugin (@plugins) { if ( $plugin->{namespace} eq "regexplugin" ) { my $regex_plugin = $plugin; @@ -80,8 +81,9 @@ sub exec_enabled_plugins_on_file { set_title( $id, $plugin_result{title} ); $newtitle = $plugin_result{title}; - $logger->debug("Changing title to $newtitle."); + $logger->debug("Changing title to $newtitle. (Will do nothing if title is blank)"); } + } } @@ -270,10 +272,10 @@ sub exec_metadata_plugin { my %returnhash = ( new_tags => $newtags ); # Indicate a title change, if the plugin reports one - if ( exists $newmetadata{title} ) { + if ( exists $newmetadata{title} && LANraragi::Model::Config->can_replacetitles ) { my $newtitle = $newmetadata{title}; - remove_spaces($newtitle); + $newtitle = trim($newtitle); $returnhash{title} = $newtitle; } return %returnhash; diff --git a/lib/LANraragi/Model/Search.pm b/lib/LANraragi/Model/Search.pm index 637cbe868..92acfcd2d 100644 --- a/lib/LANraragi/Model/Search.pm +++ b/lib/LANraragi/Model/Search.pm @@ -9,7 +9,8 @@ use Redis; use Storable qw/ nfreeze thaw /; use Sort::Naturally; -use LANraragi::Utils::Generic qw(split_workload_by_cpu remove_spaces); +use LANraragi::Utils::Generic qw(split_workload_by_cpu); +use LANraragi::Utils::String qw(trim); use LANraragi::Utils::Database qw(redis_decode redis_encode); use LANraragi::Utils::Logging qw(get_logger); @@ -22,7 +23,7 @@ sub do_search { my ( $filter, $category_id, $start, $sortkey, $sortorder, $newonly, $untaggedonly ) = @_; - my $redis = LANraragi::Model::Config->get_redis_search; + my $redis = LANraragi::Model::Config->get_redis_search; my $logger = get_logger( "Search Engine", "lanraragi" ); unless ( $redis->exists("LAST_JOB_TIME") ) { @@ -64,7 +65,7 @@ sub do_search { sub check_cache { my ( $cachekey, $cachekey_inv ) = @_; - my $redis = LANraragi::Model::Config->get_redis_search; + my $redis = LANraragi::Model::Config->get_redis_search; my $logger = get_logger( "Search Cache", "lanraragi" ); my @filtered = (); @@ -190,7 +191,7 @@ sub search_uncached { # If the tag has a namespace, We don't add a wildcard at the start of the tag to keep it intact. # Otherwise, we add a wildcard at the start to match all namespaces. my $indexkey = $tag =~ /:/ ? "INDEX_$tag*" : "INDEX_*$tag*"; - my @keys = $redis->keys($indexkey); + my @keys = $redis->keys($indexkey); # Get the list of IDs for each key foreach my $key (@keys) { @@ -202,7 +203,7 @@ sub search_uncached { # Append fuzzy title search my $namesearch = $isexact ? "$tag\x00*" : "*$tag*"; - my $scan = -1; + my $scan = -1; while ( $scan != 0 ) { # First iteration @@ -373,7 +374,7 @@ sub compute_search_filter { # Escape already present regex characters $logger->debug("Pre-escaped tag: $tag"); - remove_spaces($tag); + $tag = trim($tag); # Escape characters according to redis zscan rules $tag =~ s/([\[\]\^\\])/\\$1/g; @@ -405,10 +406,10 @@ sub sort_results { # (If no tag, defaults to "zzzz") my %tmpfilter = map { $_ => ( $redis->hget( $_, "tags" ) =~ m/.*${re}:(.*)(\,.*|$)/ ) ? $1 : "zzzz" } @filtered; - my @sorted = map { $_->[0] } # Map back to only having the ID - sort { ncmp( $a->[1], $b->[1] ) } # Sort by the tag - map { [ $_, lc( $tmpfilter{$_} ) ] } # Map to an array containing the ID and the lowercased tag - keys %tmpfilter; # List of IDs + my @sorted = map { $_->[0] } # Map back to only having the ID + sort { ncmp( $a->[1], $b->[1] ) } # Sort by the tag + map { [ $_, lc( $tmpfilter{$_} ) ] } # Map to an array containing the ID and the lowercased tag + keys %tmpfilter; # List of IDs if ($sortorder) { @sorted = reverse @sorted; diff --git a/lib/LANraragi/Model/Stats.pm b/lib/LANraragi/Model/Stats.pm index b8794fbed..0acdbc2c9 100644 --- a/lib/LANraragi/Model/Stats.pm +++ b/lib/LANraragi/Model/Stats.pm @@ -8,37 +8,20 @@ use Redis; use File::Find; use Mojo::JSON qw(encode_json); -use LANraragi::Utils::Generic qw(remove_spaces remove_newlines is_archive trim_url); +use LANraragi::Utils::Generic qw(is_archive); +use LANraragi::Utils::String qw(trim trim_CRLF trim_url); use LANraragi::Utils::Database qw(redis_decode redis_encode); use LANraragi::Utils::Logging qw(get_logger); sub get_archive_count { - - #We can't trust the DB to contain the exact amount of files, - #As deleted files are still kept in store. - my $dirname = LANraragi::Model::Config->get_userdir; - my $count = 0; - - #Count files the old-fashioned way instead - find( - { wanted => sub { - return if -d $_; #Directories are excluded on the spot - if ( is_archive($_) ) { - $count++; - } - }, - no_chdir => 1, - follow_fast => 1 - }, - $dirname - ); - return $count; + my $redis = LANraragi::Model::Config->get_redis_search; + return $redis->zcard("LRR_TITLES") + 0; # Total number of archives (as int) } sub get_page_stat { my $redis = LANraragi::Model::Config->get_redis_config; - my $stat = $redis->get("LRR_TOTALPAGESTAT") || 0; + my $stat = $redis->get("LRR_TOTALPAGESTAT") || 0; $redis->quit(); return $stat; @@ -74,26 +57,28 @@ sub build_stat_hashes { # Iterate on hashes to get their tags $logger->info("Building stat indexes... ($archive_count archives)"); + + # TODO go through tanks first, and remove their IDs from @keys + foreach my $id (@keys) { if ( $redis->hexists( $id, "tags" ) ) { my $rawtags = $redis->hget( $id, "tags" ); # Split tags by comma - my @tags = split( /,\s?/, redis_decode($rawtags) ); + my @tags = split( /,\s?/, redis_decode($rawtags) ); my $has_tags = 0; foreach my $t (@tags) { - remove_spaces($t); - remove_newlines($t); + $t = trim($t); + $t = trim_CRLF($t); # The following are basic and therefore don't count as "tagged" $has_tags = 1 unless $t =~ /(artist|parody|series|language|event|group|date_added|timestamp):.*/; # If the tag is a source: tag, add it to the URL index if ( $t =~ /source:(.*)/i ) { - my $url = $1; - trim_url($url); + my $url = trim_url($1); $logger->trace("Adding $url as an URL for $id"); $redistx->hset( "LRR_URLMAP", $url, $id ); # No need to encode the value, as URLs are already encoded by design } @@ -120,8 +105,8 @@ sub build_stat_hashes { # Decode and lowercase the title $title = lc( redis_decode($title) ); - remove_spaces($title); - remove_newlines($title); + $title = trim($title); + $title = trim_CRLF($title); $title = redis_encode($title); # The LRR_TITLES lexicographically sorted set contains both the title and the id under the form $title\x00$id. @@ -153,7 +138,7 @@ sub is_url_recorded { $logger->debug("Checking if url $url is in the url map."); # Trim last slash from url if it's present - trim_url($url); + $url = trim_url($url); if ( $redis->hexists( "LRR_URLMAP", $url ) ) { $id = $redis->hget( "LRR_URLMAP", $url ); @@ -166,11 +151,11 @@ sub is_url_recorded { sub build_tag_stats { my $minscore = shift; - my $logger = get_logger( "Tag Stats", "lanraragi" ); + my $logger = get_logger( "Tag Stats", "lanraragi" ); $logger->debug("Serving tag statistics with a minimum weight of $minscore"); # Login to Redis and grab the stats sorted set - my $redis = LANraragi::Model::Config->get_redis_search; + my $redis = LANraragi::Model::Config->get_redis_search; my %tagcloud = $redis->zrangebyscore( "LRR_STATS", $minscore, "+inf", "WITHSCORES" ); $redis->quit(); @@ -196,12 +181,23 @@ sub build_tag_stats { } sub compute_content_size { + my $redis_db = LANraragi::Model::Config->get_redis; - #Get size of archive folder - my $dirname = LANraragi::Model::Config->get_userdir; - my $size = 0; + my @keys = $redis_db->keys('????????????????????????????????????????'); - find( sub { $size += -s if -f }, $dirname ); + $redis_db->multi; + foreach my $id (@keys) { + LANraragi::Utils::Database::get_arcsize($redis_db, $id); + } + my @result = $redis_db->exec; + $redis_db->quit; + + my $size = 0; + foreach my $row (@result) { + if (defined($row)) { + $size = $size + $row; + } + } return int( $size / 1073741824 * 100 ) / 100; } diff --git a/lib/LANraragi/Model/Tankoubon.pm b/lib/LANraragi/Model/Tankoubon.pm new file mode 100644 index 000000000..f5864061b --- /dev/null +++ b/lib/LANraragi/Model/Tankoubon.pm @@ -0,0 +1,342 @@ +package LANraragi::Model::Tankoubon; + +use feature qw(signatures); +no warnings 'experimental::signatures'; + +use strict; +use warnings; +use utf8; + +use Redis; +use Mojo::JSON qw(decode_json encode_json); +use List::Util qw(min); + +use LANraragi::Utils::Database qw(redis_encode redis_decode invalidate_cache get_archive_json_multi get_tankoubons_by_file); +use LANraragi::Utils::Generic qw(array_difference); +use LANraragi::Utils::Logging qw(get_logger); + +# get_tankoubon_list(page) +# Returns a list of all the Tankoubon objects. +sub get_tankoubon_list ( $page = 0 ) { + + my $redis = LANraragi::Model::Config->get_redis; + my $logger = get_logger( "Tankoubon", "lanraragi" ); + + $page //= 0; + + # Tankoubons are represented by TANK_[timestamp] in DB. Can't wait for 2038! + my @tanks = $redis->keys('TANK_??????????'); + + # Jam tanks into an array of hashes + my @result; + foreach my $key ( sort @tanks ) { + my %data = get_tankoubon($key); + push( @result, \%data ); + } + + # # Only get the first X keys + my $keysperpage = LANraragi::Model::Config->get_pagesize; + + # Return total keys and the filtered ones + my $total = $#tanks + 1; + my $start = $page * $keysperpage; + my $end = min( $start + $keysperpage - 1, $#result ); + + if ( $page < 0 ) { + return ( $total, $total, @result ); + } else { + return ( $total, $#result + 1, @result[ $start .. $end ] ); + } + + #return @result; +} + +# create_tankoubon(name, existing_id) +# Create a Tankoubon. +# If an existing Tankoubon ID is supplied, said Tankoubon will be updated with the given parameters. +# Returns the ID of the created/updated Tankoubon. +sub create_tankoubon ( $name, $tank_id ) { + + my $redis = LANraragi::Model::Config->get_redis; + + # Set all fields of the group object + unless ( length($tank_id) ) { + $tank_id = "TANK_" . time(); + + my $isnewkey = 0; + until ($isnewkey) { + + # Check if the group ID exists, move timestamp further if it does + if ( $redis->exists($tank_id) ) { + $tank_id = "TANK_" . ( time() + 1 ); + } else { + $isnewkey = 1; + } + } + } else { + + # Get name + my @old_name = $redis->zrangebyscore( $tank_id, 0, 0, qw{LIMIT 0 1} ); + my $n = redis_decode( $old_name[0] ); + + $redis->zrem( $tank_id, $n ); + } + + # Default values for new group + # Score 0 will be reserved for the name of the tank + $redis->zadd( $tank_id, 0, redis_encode($name) ); + + $redis->quit; + + return $tank_id; +} + +# get_tankoubon(tankoubonid, fulldata, page) +# Returns the Tankoubon matching the given id. +# Returns undef if the id doesn't exist. +sub get_tankoubon ( $tank_id, $fulldata = 0, $page = 0 ) { + + my $logger = get_logger( "Tankoubon", "lanraragi" ); + my $redis = LANraragi::Model::Config->get_redis; + my $keysperpage = LANraragi::Model::Config->get_pagesize; + + $page //= 0; + + if ( $tank_id eq "" ) { + $logger->debug("No Tankoubon ID provided."); + return (); + } + + unless ( length($tank_id) == 15 && $redis->exists($tank_id) ) { + $logger->warn("$tank_id doesn't exist in the database!"); + return (); + } + + # Declare some needed variables + my %tank; + my @archives; + my @limit = split( ' ', "LIMIT " . ( $keysperpage * $page ) . " $keysperpage" ); + + # Get name + my @name = $redis->zrangebyscore( $tank_id, 0, 0, qw{LIMIT 0 1} ); + $tank{name} = redis_decode( $name[0] ); + + my %tankoubon; + + # Grab page + if ( $page < 0 ) { + %tankoubon = $redis->zrangebyscore( $tank_id, 1, "+inf", "WITHSCORES" ); + } else { + %tankoubon = $redis->zrangebyscore( $tank_id, 1, "+inf", "WITHSCORES", @limit ); + } + + # Sort and add IDs to archives array + foreach my $i ( sort { $tankoubon{$a} <=> $tankoubon{$b} } keys %tankoubon ) { + push( @archives, $i ); + } + + # Verify if we require fulldata files or just IDs + if ($fulldata) { + my @data = get_archive_json_multi(@archives); + eval { $tank{archives} = \@archives }; + eval { $tank{full_data} = \@data } + } else { + eval { $tank{archives} = \@archives }; + } + + if ($@) { + $logger->error("Couldn't deserialize contents of Tankoubon $tank_id! $@"); + } + + # Add the key as well + $tank{id} = $tank_id; + + my $total = $redis->zcard($tank_id) - 1; + + return ( $total, $#archives + 1, %tank ); +} + +# delete_tankoubon(tankoubonid) +# Deletes the Tankoubon with the given ID. +# Returns 0 if the given ID isn't a Tankoubon ID, 1 otherwise +sub delete_tankoubon ($tank_id) { + + my $logger = get_logger( "Tankoubon", "lanraragi" ); + my $redis = LANraragi::Model::Config->get_redis; + + if ( length($tank_id) != 15 ) { + + # Probably not a Tankoubon ID + $logger->error("$tank_id is not a Tankoubon ID, doing nothing."); + $redis->quit; + return 0; + } + + if ( $redis->exists($tank_id) ) { + $redis->del($tank_id); + $redis->quit; + return 1; + } else { + $logger->warn("$tank_id doesn't exist in the database!"); + $redis->quit; + return 1; + } +} + +# update_archive_list(tankoubonid, arcid) +# Updates the archives list in a Tankoubon. +# Returns 1 on success, 0 on failure alongside an error message. +sub update_archive_list ( $tank_id, $data ) { + + my $logger = get_logger( "Tankoubon", "lanraragi" ); + my $redis = LANraragi::Model::Config->get_redis; + my $err = ""; + my @tank_archives = @{ $data->{"archives"} }; + + if ( $redis->exists($tank_id) ) { + + foreach my $key (@tank_archives) { + unless ( $redis->exists($key) ) { + $err = "$key does not exist in the database."; + $logger->error($err); + $redis->quit; + return ( 0, $err ); + } + } + + my @origs = $redis->zrangebyscore( $tank_id, 1, "+inf" ); + my @diff = array_difference( \@tank_archives, \@origs ); + my @update; + + # Remove the ones not in the order + if (@diff) { + $redis->zrem( $tank_id, @diff ); + } + + # Prepare zadd array + my $len = @tank_archives; + + for ( my $i = 0; $i < $len; $i = $i + 1 ) { + push @update, $i + 1; + push @update, $tank_archives[$i]; + } + + # Update + $redis->zadd( $tank_id, @update ); + + $redis->quit; + return ( 1, $err ); + } + + $err = "$tank_id doesn't exist in the database!"; + $logger->warn($err); + $redis->quit; + return ( 0, $err ); +} + +# add_to_tankoubon(tankoubonid, arcid) +# Adds the given archive ID to the given Tankoubon. +# Returns 1 on success, 0 on failure alongside an error message. +sub add_to_tankoubon ( $tank_id, $arc_id ) { + + my $logger = get_logger( "Tankoubon", "lanraragi" ); + my $redis = LANraragi::Model::Config->get_redis; + my $err = ""; + + if ( $redis->exists($tank_id) ) { + + unless ( $redis->exists($arc_id) ) { + $err = "$arc_id does not exist in the database."; + $logger->error($err); + $redis->quit; + return ( 0, $err ); + } + + if ( $redis->zscore( $tank_id, $arc_id ) ) { + $err = "$arc_id already present in category $tank_id, doing nothing."; + $logger->warn($err); + $redis->quit; + return ( 1, $err ); + } + + my $score = $redis->zcard($tank_id); + + $redis->zadd( $tank_id, $score, $arc_id ); + + $redis->quit; + return ( 1, $err ); + } + + $err = "$tank_id doesn't exist in the database!"; + $logger->warn($err); + $redis->quit; + return ( 0, $err ); +} + +# remove_from_tankoubon(tankoubonid, arcid) +# Removes the given archive ID from the given Tankoubon. +# Returns 1 on success, 0 on failure alongside an error message. +sub remove_from_tankoubon ( $tank_id, $arcid ) { + + my $logger = get_logger( "Tankoubon", "lanraragi" ); + my $redis = LANraragi::Model::Config->get_redis; + my $err = ""; + + if ( $redis->exists($tank_id) ) { + + unless ( $redis->exists($arcid) ) { + $err = "$arcid does not exist in the database."; + $logger->error($err); + $redis->quit; + return ( 0, $err ); + } + + # Get the score for reference + my $score = $redis->zscore( $tank_id, $arcid ); + + unless ($score) { + $err = "$arcid not in tankoubon $tank_id, doing nothing."; + $logger->warn($err); + $redis->quit; + return ( 1, $err ); + } + + # Get all the elements after the one to remove to update the score + my %toupdate = $redis->zrangebyscore( $tank_id, $score + 1, "+inf", "WITHSCORES" ); + + my @update; + + # Build new scores + foreach my $i ( keys %toupdate ) { + push @update, $toupdate{$i} - 1; + push @update, $i; + } + + # Remove element + $redis->zrem( $tank_id, $arcid ); + + # Update scores + if ( scalar @update ) { + $redis->zadd( $tank_id, @update ); + } + + $redis->quit; + return ( 1, $err ); + } + + $err = "$tank_id doesn't exist in the database!"; + $logger->warn($err); + $redis->quit; + return ( 0, $err ); +} + +# get_tankoubons_file(arcid) +# Gets a list of Tankoubons where archive ID is contained. +# Returns an array of tank IDs. +sub get_tankoubons_file ($arcid) { + + return get_tankoubons_by_file($arcid); + +} + +1; diff --git a/lib/LANraragi/Model/Upload.pm b/lib/LANraragi/Model/Upload.pm index 2a142f0f2..401e881d0 100644 --- a/lib/LANraragi/Model/Upload.pm +++ b/lib/LANraragi/Model/Upload.pm @@ -13,7 +13,8 @@ use File::Copy qw(move); use LANraragi::Utils::Database qw(invalidate_cache compute_id); use LANraragi::Utils::Logging qw(get_logger); use LANraragi::Utils::Database qw(redis_encode); -use LANraragi::Utils::Generic qw(is_archive remove_spaces remove_newlines trim_url get_bytelength); +use LANraragi::Utils::Generic qw(is_archive get_bytelength); +use LANraragi::Utils::String qw(trim trim_CRLF trim_url); use LANraragi::Model::Config; use LANraragi::Model::Plugins; @@ -31,8 +32,8 @@ use LANraragi::Model::Category; # Returns a status value, the ID and title of the file, and a status message. sub handle_incoming_file { - my ( $tempfile, $catid, $tags ) = @_; - my ( $filename, $dirs, $suffix ) = fileparse( $tempfile, qr/\.[^.]*/ ); + my ( $tempfile, $catid, $tags ) = @_; + my ( $filename, $dirs, $suffix ) = fileparse( $tempfile, qr/\.[^.]*/ ); $filename = $filename . $suffix; my $logger = get_logger( "File Upload/Download", "lanraragi" ); @@ -57,7 +58,7 @@ sub handle_incoming_file { my $isdupe = $redis->exists($id) && -e $redis->hget( $id, "file" ); # Stop here if file is a dupe and replacement is turned off. - if ((-e $output_file || $isdupe) && !$replace_dupe) { + if ( ( -e $output_file || $isdupe ) && !$replace_dupe ) { # Trash temporary file unlink $tempfile; @@ -75,7 +76,7 @@ sub handle_incoming_file { # If we are replacing an existing one, just remove the old one first. if ($replace_dupe) { $logger->debug("Delete archive $id before replacing it."); - LANraragi::Utils::Database::delete_archive( $id ); + LANraragi::Utils::Database::delete_archive($id); } # Add the file to the database ourselves so Shinobu doesn't do it @@ -91,8 +92,8 @@ sub handle_incoming_file { my @tags = split( /,\s?/, $tags ); foreach my $t (@tags) { - remove_spaces($t); - remove_newlines($t); + $t = trim($t); + $t = trim_CRLF($t); # If the tag is a source: tag, add it to the URL index if ( $t =~ /source:(.*)/i ) { @@ -122,6 +123,7 @@ sub handle_incoming_file { # (The file being physically present is necessary in case last modified time is used) LANraragi::Utils::Database::add_timestamp_tag( $redis, $id ); LANraragi::Utils::Database::add_pagecount( $redis, $id ); + LANraragi::Utils::Database::add_arcsize( $redis, $id ); $redis->quit(); $redis_search->quit(); diff --git a/lib/LANraragi/Plugin/Metadata/Chaika.pm b/lib/LANraragi/Plugin/Metadata/Chaika.pm index eaa830ce2..6fe2da16f 100644 --- a/lib/LANraragi/Plugin/Metadata/Chaika.pm +++ b/lib/LANraragi/Plugin/Metadata/Chaika.pm @@ -19,15 +19,20 @@ sub plugin_info { type => "metadata", namespace => "trabant", author => "Difegue", - version => "2.3", - description => "Searches chaika.moe for tags matching your archive. This will try to use the thumbnail first, and fallback to a default text search.", + version => "2.3.1", + description => + "Searches chaika.moe for tags matching your archive. This will try to use the thumbnail first, and fallback to a default text search.", icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA\nB3RJTUUH4wYCFQocjU4r+QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUH\nAAAEZElEQVQ4y42T3WtTdxzGn/M7J+fk5SRpTk7TxMZkXU84tTbVNrUT3YxO7HA4pdtQZDe7cgx2\ns8vBRvEPsOwFYTDYGJUpbDI2wV04cGXCGFLonIu1L2ptmtrmxeb1JDkvv121ZKVze66f74eH7/f5\nMmjRwMCAwrt4/9KDpflMJpPHvyiR2DPcJklJ3TRDDa0xk36cvrm8vDwHAAwAqKrqjjwXecPG205w\nHBuqa9rk77/d/qJYLD7cCht5deQIIczbgiAEKLVAKXWUiqVV06Tf35q8dYVJJBJem2A7Kwi2nQzD\nZig1CG93+PO5/KN6tf5NKpVqbsBUVVVFUUxwHJc1TXNBoxojS7IbhrnLMMx9pVJlBqFQKBKPxwcB\nkJYgjKIo3QCE1nSKoghbfJuKRqN2RVXexMaQzWaLezyeEUEQDjscjk78PxFFUYRkMsltJgGA3t7e\nyMLCwie6rr8iCILVbDbvMgwzYRjGxe0o4XC4s1AoHPP5fMP5/NNOyzLKAO6Ew+HrDADBbre/Ryk9\nnzx81FXJNlEpVpF+OqtpWu2MpmnXWmH9/f2umZmZi4cOHXnLbILLzOchhz1YerJAs9m1GwRAg2GY\nh7GYah488BJYzYW+2BD61AFBlmX/1nSNRqN9//792ujoaIPVRMjOKHoie3DytVGmp2fXCAEAjuMm\nu7u7Umosho6gjL/u/QHeEgvJZHJ2K/D+/fuL4+PjXyvPd5ldkShy1UXcmb4DnjgQj/fd5gDA6/XS\nYCAwTwh9oT3QzrS1+VDVi+vd3Tsy26yQVoFF3dAXJVmK96p9EJ0iLNOwKKU3CQCk0+lSOpP5WLDz\nF9Q9kZqyO0SloOs6gMfbHSU5NLRiUOuax2/HyZPHEOsLw2SbP83eu/fLxrkNp9P554XxCzVa16MC\n7+BPnTk9cfmH74KJE8nmga7Xy5JkZ8VKifGIHpoBb1VX8hNTd3/t/7lQ3OeXfFPvf/jBRw8ezD/a\n7M/aWq91cGgnJaZ2VcgSdnV1XRNNd3vAoBVVYusmnEQS65hfgSG6c+zy3Kre7nF/KrukcMW0Zg8O\nD08DoJutDxxOEb5IPUymwrq8ft1gLKfkFojkkRxemERCAQUACPFWRazYLJcrFGwQhyufbQQ7rFpy\nLMkCwGZC34qPIuwp+XPOjBFwazQ/txrdFS2GGS/Xuj+pUKLGk1Kjvlded3s72lyGW+PLbGVcmrAA\ngN0wTk1NWYODg9XOKltGtpazi5GigzroUnHN5nUHG1ylRsG7rDXHmnEpu4CeEtEKkqNc6QqlLc/M\n8uT5lLH5eq0aGxsju1O7GQB498a5s/0x9dRALPaQEDZnYwnhWJtMCCNrjeb0UP34Z6e/PW22zjPP\n+vwXBwfPvbw38XnXjk7GsiwKAIQQhjAMMrlsam45d+zLH6/8o6vkWcBcrXbVKQhf6bpucCwLjmUB\nSmmhXC419eblrbD/TAgAkUjE987xE0c7ZDmk66ajUCnq+cL63fErl25s5/8baQPaWLhx6goAAAAA\nSUVORK5CYII=", - parameters => [ - { type => "bool", desc => "Save archive title" }, + parameters => [ { type => "bool", desc => "Add the following tags if available: download URL, gallery ID, category, timestamp" }, - { type => "bool", desc => "Add tags without a namespace to the 'other:' namespace instead, mirroring E-H's behavior of namespacing everything" }, - { type => "string", desc => "Add a custom 'source:' tag to your archive. Example: chaika. Will NOT add a tag if blank" } + { type => "bool", + desc => + "Add tags without a namespace to the 'other:' namespace instead, mirroring E-H's behavior of namespacing everything" + }, + { type => "string", + desc => "Add a custom 'source:' tag to your archive. Example: chaika. Will NOT add a tag if blank" + } ], oneshot_arg => "Chaika Gallery or Archive URL (Will attach matching tags to your archive)" ); @@ -38,8 +43,8 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ( $savetitle, $addextra, $addother, $addsource ) = @_; # Plugin parameters + my $lrr_info = shift; # Global info hash + my ( $addextra, $addother, $addsource ) = @_; # Plugin parameters my $logger = get_plugin_logger(); my $newtags = ""; @@ -52,13 +57,14 @@ sub get_tags { } else { # Try SHA-1 reverse search first - $logger->info("Using thumbnail hash " . $lrr_info->{thumbnail_hash}); + $logger->info( "Using thumbnail hash " . $lrr_info->{thumbnail_hash} ); ( $newtags, $newtitle ) = tags_from_sha1( $lrr_info->{thumbnail_hash}, $addextra, $addother, $addsource ); # Try text search if it fails if ( $newtags eq "" ) { $logger->info("No results, falling back to text search."); - ( $newtags, $newtitle ) = search_for_archive( $lrr_info->{archive_title}, $lrr_info->{existing_tags}, $addextra, $addother, $addsource ); + ( $newtags, $newtitle ) = + search_for_archive( $lrr_info->{archive_title}, $lrr_info->{existing_tags}, $addextra, $addother, $addsource ); } } @@ -67,9 +73,9 @@ sub get_tags { return ( error => "No matching Chaika Archive Found!" ); } else { $logger->info("Sending the following tags to LRR: $newtags"); + #Return a hash containing the new metadata - if ( $savetitle && $newtags ne "" ) { return ( tags => $newtags, title => $newtitle ); } - else { return ( tags => $newtags ); } + return ( tags => $newtags, title => $newtitle ); } } @@ -145,7 +151,7 @@ sub get_json_from_chaika { my $ua = Mojo::UserAgent->new; my $res = $ua->get($URL)->result; - if ($res->is_error) { + if ( $res->is_error ) { return; } my $textrep = $res->body; @@ -161,38 +167,40 @@ sub parse_chaika_json { my $tags = $json->{"tags"} || (); foreach my $tag (@$tags) { + #Replace underscores with spaces $tag =~ s/_/ /g; - + #Add 'other' namespace if none - if ($addother && index($tag, ":") == -1) { + if ( $addother && index( $tag, ":" ) == -1 ) { $tag = "other:" . $tag; } } - my $category = lc $json->{"category"}; - my $download = $json->{"download"} ? $json->{"download"} : $json->{"archives"}->[0]->{"link"}; - my $gallery = $json->{"gallery"} ? $json->{"gallery"} : $json->{"id"}; + my $category = lc $json->{"category"}; + my $download = $json->{"download"} ? $json->{"download"} : $json->{"archives"}->[0]->{"link"}; + my $gallery = $json->{"gallery"} ? $json->{"gallery"} : $json->{"id"}; my $timestamp = $json->{"posted"}; - if ($tags && $addextra) { - if ($category ne "") { - push(@$tags, "category:" . $category); + if ( $tags && $addextra ) { + if ( $category ne "" ) { + push( @$tags, "category:" . $category ); } - if ($download ne "") { - push(@$tags, "download:" . $download); + if ( $download ne "" ) { + push( @$tags, "download:" . $download ); } - if ($gallery ne "") { - push(@$tags, "gallery:" . $gallery); + if ( $gallery ne "" ) { + push( @$tags, "gallery:" . $gallery ); } - if ($timestamp ne "") { - push(@$tags, "timestamp:" . $timestamp); + if ( $timestamp ne "" ) { + push( @$tags, "timestamp:" . $timestamp ); } } - if ($gallery && $gallery ne "") { + if ( $gallery && $gallery ne "" ) { + # add custom source, but only if having found gallery - if ($addsource && $addsource ne "") { - push(@$tags, "source:" . $addsource); + if ( $addsource && $addsource ne "" ) { + push( @$tags, "source:" . $addsource ); } return ( join( ', ', @$tags ), $json->{"title"} ); } else { diff --git a/lib/LANraragi/Plugin/Metadata/ChaikaFile.pm b/lib/LANraragi/Plugin/Metadata/ChaikaFile.pm index 728026d17..effe94869 100644 --- a/lib/LANraragi/Plugin/Metadata/ChaikaFile.pm +++ b/lib/LANraragi/Plugin/Metadata/ChaikaFile.pm @@ -16,15 +16,19 @@ sub plugin_info { type => "metadata", namespace => "chaikafileplugin", author => "Difegue & Plebs", - version => "0.1", + version => "0.2", description => "Collects metadata embedded into your archives as Chaika-style api.json files", - icon => + icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA\nB3RJTUUH4wYCFQocjU4r+QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUH\nAAAEZElEQVQ4y42T3WtTdxzGn/M7J+fk5SRpTk7TxMZkXU84tTbVNrUT3YxO7HA4pdtQZDe7cgx2\ns8vBRvEPsOwFYTDYGJUpbDI2wV04cGXCGFLonIu1L2ptmtrmxeb1JDkvv121ZKVze66f74eH7/f5\nMmjRwMCAwrt4/9KDpflMJpPHvyiR2DPcJklJ3TRDDa0xk36cvrm8vDwHAAwAqKrqjjwXecPG205w\nHBuqa9rk77/d/qJYLD7cCht5deQIIczbgiAEKLVAKXWUiqVV06Tf35q8dYVJJBJem2A7Kwi2nQzD\nZig1CG93+PO5/KN6tf5NKpVqbsBUVVVFUUxwHJc1TXNBoxojS7IbhrnLMMx9pVJlBqFQKBKPxwcB\nkJYgjKIo3QCE1nSKoghbfJuKRqN2RVXexMaQzWaLezyeEUEQDjscjk78PxFFUYRkMsltJgGA3t7e\nyMLCwie6rr8iCILVbDbvMgwzYRjGxe0o4XC4s1AoHPP5fMP5/NNOyzLKAO6Ew+HrDADBbre/Ryk9\nnzx81FXJNlEpVpF+OqtpWu2MpmnXWmH9/f2umZmZi4cOHXnLbILLzOchhz1YerJAs9m1GwRAg2GY\nh7GYah488BJYzYW+2BD61AFBlmX/1nSNRqN9//792ujoaIPVRMjOKHoie3DytVGmp2fXCAEAjuMm\nu7u7Umosho6gjL/u/QHeEgvJZHJ2K/D+/fuL4+PjXyvPd5ldkShy1UXcmb4DnjgQj/fd5gDA6/XS\nYCAwTwh9oT3QzrS1+VDVi+vd3Tsy26yQVoFF3dAXJVmK96p9EJ0iLNOwKKU3CQCk0+lSOpP5WLDz\nF9Q9kZqyO0SloOs6gMfbHSU5NLRiUOuax2/HyZPHEOsLw2SbP83eu/fLxrkNp9P554XxCzVa16MC\n7+BPnTk9cfmH74KJE8nmga7Xy5JkZ8VKifGIHpoBb1VX8hNTd3/t/7lQ3OeXfFPvf/jBRw8ezD/a\n7M/aWq91cGgnJaZ2VcgSdnV1XRNNd3vAoBVVYusmnEQS65hfgSG6c+zy3Kre7nF/KrukcMW0Zg8O\nD08DoJutDxxOEb5IPUymwrq8ft1gLKfkFojkkRxemERCAQUACPFWRazYLJcrFGwQhyufbQQ7rFpy\nLMkCwGZC34qPIuwp+XPOjBFwazQ/txrdFS2GGS/Xuj+pUKLGk1Kjvlded3s72lyGW+PLbGVcmrAA\ngN0wTk1NWYODg9XOKltGtpazi5GigzroUnHN5nUHG1ylRsG7rDXHmnEpu4CeEtEKkqNc6QqlLc/M\n8uT5lLH5eq0aGxsju1O7GQB498a5s/0x9dRALPaQEDZnYwnhWJtMCCNrjeb0UP34Z6e/PW22zjPP\n+vwXBwfPvbw38XnXjk7GsiwKAIQQhjAMMrlsam45d+zLH6/8o6vkWcBcrXbVKQhf6bpucCwLjmUB\nSmmhXC419eblrbD/TAgAkUjE987xE0c7ZDmk66ajUCnq+cL63fErl25s5/8baQPaWLhx6goAAAAA\nSUVORK5CYII=", - parameters => [ - { type => "bool", desc => "Save archive title" }, + parameters => [ { type => "bool", desc => "Add the following tags if available: download URL, gallery ID, category, timestamp" }, - { type => "bool", desc => "Add tags without a namespace to the 'other:' namespace instead, mirroring E-H's behavior of namespacing everything" }, - { type => "string", desc => "Add a custom 'source:' tag to your archive. Example: chaika. Will NOT add a tag if blank" } + { type => "bool", + desc => + "Add tags without a namespace to the 'other:' namespace instead, mirroring E-H's behavior of namespacing everything" + }, + { type => "string", + desc => "Add a custom 'source:' tag to your archive. Example: chaika. Will NOT add a tag if blank" + } ], ); } @@ -33,8 +37,8 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ($savetitle, $addextra, $addother, $addsource) = @_; # Plugin parameters + my $lrr_info = shift; # Global info hash + my ( $addextra, $addother, $addsource ) = @_; # Plugin parameters my $logger = get_plugin_logger(); my $newtags = ""; @@ -43,6 +47,7 @@ sub get_tags { # Try reading any embedded api.json file my $path_in_archive = is_file_in_archive( $lrr_info->{file_path}, "api.json" ); if ($path_in_archive) { + #Extract api.json my $filepath = extract_file_from_archive( $lrr_info->{file_path}, $path_in_archive ); ( $newtags, $newtitle ) = tags_from_file( $filepath, $addextra, $addother, $addsource ); @@ -54,9 +59,9 @@ sub get_tags { } else { $logger->info("Sending the following tags to LRR: $newtags"); + #Return a hash containing the new metadata - if ( $savetitle && $newtags ne "" ) { return ( tags => $newtags, title => $newtitle ); } - else { return ( tags => $newtags ); } + return ( tags => $newtags, title => $newtitle ); } } @@ -68,7 +73,7 @@ sub get_tags { # tags_from_file sub tags_from_file { - my ( $filepath, $addextra, $addother, $addsource ) = @_; + my ( $filepath, $addextra, $addother, $addsource ) = @_; my $logger = get_plugin_logger(); @@ -76,16 +81,16 @@ sub tags_from_file { my $stringjson = ""; open( my $fh, '<:encoding(UTF-8)', $filepath ) - or return ( error => "Could not open $filepath!" ); + or return ( error => "Could not open $filepath!" ); while ( my $row = <$fh> ) { chomp $row; $stringjson .= $row; } - unlink( $fh ); + unlink($fh); #Use Mojo::JSON to decode the string into a hash - my $hashjson = from_json( $stringjson ); + my $hashjson = from_json($stringjson); return parse_chaika_json( $hashjson, $addextra, $addother, $addsource ); } @@ -99,38 +104,40 @@ sub parse_chaika_json { my $tags = $json->{"tags"} || (); foreach my $tag (@$tags) { + #Replace underscores with spaces $tag =~ s/_/ /g; #Add 'other' namespace if none - if ($addother && index($tag, ":") == -1) { + if ( $addother && index( $tag, ":" ) == -1 ) { $tag = "other:" . $tag; } } - my $category = lc $json->{"category"}; - my $download = $json->{"download"} ? $json->{"download"} : $json->{"archives"}->[0]->{"link"}; - my $gallery = $json->{"gallery"} ? $json->{"gallery"} : $json->{"id"}; + my $category = lc $json->{"category"}; + my $download = $json->{"download"} ? $json->{"download"} : $json->{"archives"}->[0]->{"link"}; + my $gallery = $json->{"gallery"} ? $json->{"gallery"} : $json->{"id"}; my $timestamp = $json->{"posted"}; - if ($tags && $addextra) { - if ($category ne "") { - push(@$tags, "category:" . $category); + if ( $tags && $addextra ) { + if ( $category ne "" ) { + push( @$tags, "category:" . $category ); } - if ($download ne "") { - push(@$tags, "download:" . $download); + if ( $download ne "" ) { + push( @$tags, "download:" . $download ); } - if ($gallery ne "") { - push(@$tags, "gallery:" . $gallery); + if ( $gallery ne "" ) { + push( @$tags, "gallery:" . $gallery ); } - if ($timestamp ne "") { - push(@$tags, "timestamp:" . $timestamp); + if ( $timestamp ne "" ) { + push( @$tags, "timestamp:" . $timestamp ); } } - if ($gallery && $gallery ne "") { + if ( $gallery && $gallery ne "" ) { + # add custom source, but only if having found gallery - if ($addsource && $addsource ne "") { - push(@$tags, "source:" . $addsource); + if ( $addsource && $addsource ne "" ) { + push( @$tags, "source:" . $addsource ); } return ( join( ', ', @$tags ), $json->{"title"} ); } else { @@ -138,5 +145,4 @@ sub parse_chaika_json { } } - 1; diff --git a/lib/LANraragi/Plugin/Metadata/CopyTags.pm b/lib/LANraragi/Plugin/Metadata/CopyTags.pm index 8dcc4d218..796ce24df 100644 --- a/lib/LANraragi/Plugin/Metadata/CopyTags.pm +++ b/lib/LANraragi/Plugin/Metadata/CopyTags.pm @@ -16,8 +16,8 @@ sub plugin_info { namespace => "copytags", author => "Difegue", version => "2.1", - description => "Apply custom tag modifications.", - icon => + description => "Add the specified tags to your metadata. Good for batch operations!", + icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA\nB3RJTUUH4wYCFQ05iQtpeQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUH\nAAAD8ElEQVQ4y4WUW2yURRTHfzPfty2UWrZbqdrdhktowQAJiQ/iDUwkaFJ5IAQfGjWBByMhwYAm\nBH3xqT5ArARS9cUgJC1qjCDxCY1JEy6lLVJrW9lKq6Wlgcqy2W7b/S4zx4fdbstFneRMZjLJb/7n\nf86MOn78+MlEIvE6gIhgRRARECGTmaS0tJT6+vrvo7HKxserq6f4v9Hc3PyxiBh5yBgYGJBUKiW/\n9vZKV/eVb7s6OyN79uxhbGwMKVx8f2gRUUEQEAQhfhDg+wGeHxAaA8DZs2e50tVNZ+flJ3v7B8ra\n29uJx+M0NTXhed6DCg8dPtzs+77xfV883xfP8yVXCM/zxA9CCcNQzv3402+bX96yuK2tjd27dxOP\nx2loaCCdTj+gkIJlICAFLz0vx/RMjmx2kunpaXK5nF5au3RJb29fzdq162ouX+p4ZHh4mO3bt98j\n0DU2n5oUZqUUvudz8cIFEvEEoQkRIBarXLFr584frLVmdHRUj4yNXu3r69tbVVV1u7W1lcbGxjxQ\njMzjKxAw1rJoURlr1q7B9wMEQaFKBerDwMfzPKKLo6t+uXp12Z07d16pq6tL79ixg0gkgjamkCv5\nvItKUfkrFCilmN0orZmayrJgQSlDQ0NPHz3WskhrTXd3NwBaxBR8m007v7aza+a8RQTXcaipTTAx\nMYHn+2SnJ4nFYgwODuaBRmxB3FylEEGsLRKliBZcHSFVkSFVOUWiOk5uxiObzZJKpfJAa8wciPvA\nzIflzxSKW+EtDgx8wKUbXVSYcuM4TtEWba1VSkGkJILruvmIuFibh2gNjtI4SuNqB6XBVQ7jFTdo\nK/+GiWemNmmt59omFouNfXf6dLsIalZZGJrI6lV1zwIcOP8hQ871YsE0mpRJUVFWTnZ5WjpSF09t\n+WhbJdf4DMDdv2/foYMHDx46c+YMW7dupba2lutDw1UrV674G+B3/xr9VT1gVLFMWjk4SoOg/ope\nZyo9/elzq1/QQIs7v8u11oyMjFBTU6OszKas0I4D6t4nG1qDAqwWJnWG0IbrAdz737ZSCmsNUvAw\nmAnxJsxcHyEoV+FUgDiWhXfL2RS89FXdtSfeYss8oDEGz/Ow1mKMQReq9vXmE4SSVyPAAl3KiT9b\n2X/7XZZMPcbz9sWTl94/9+bn63vUXvaKCxCGIclkkmQyCcCxlhZJ/jHI3XR60hqjpdinloiUSJ/0\nlZQuLC/Z0L9B4n2V73zR08ORXUcEyFd2fHycTCZT7KWLHR3q5s3xyslMxlhrlYjklVvLuv6V/tE3\nTr29vGzZxlffe+q1jV82hObRwFZXVxONRvnXn/e/ounCJzU/z5yPPOzsH4cGnEj6mhLzAAAAAElF\nTkSuQmCC", parameters => [ { type => "string", desc => "Tags to copy, separated by commas." } ] ); @@ -28,8 +28,8 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ($tagstocopy) = @_; # Plugin parameters + my $lrr_info = shift; # Global info hash + my ($tagstocopy) = @_; # Plugin parameters my $logger = get_logger( "Tag Copy", "plugins" ); diff --git a/lib/LANraragi/Plugin/Metadata/EHentai.pm b/lib/LANraragi/Plugin/Metadata/EHentai.pm index 464a4ed41..d418da92a 100644 --- a/lib/LANraragi/Plugin/Metadata/EHentai.pm +++ b/lib/LANraragi/Plugin/Metadata/EHentai.pm @@ -20,19 +20,18 @@ sub plugin_info { return ( #Standard metadata - name => "E-Hentai", - type => "metadata", - namespace => "ehplugin", - login_from => "ehlogin", - author => "Difegue and others", - version => "2.5.1", + name => "E-Hentai", + type => "metadata", + namespace => "ehplugin", + login_from => "ehlogin", + author => "Difegue and others", + version => "2.5.2", description => "Searches g.e-hentai for tags matching your archive.
    This plugin will use the source: tag of the archive if it exists.", icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI\nWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wYBFg0JvyFIYgAAAB1pVFh0Q29tbWVudAAAAAAAQ3Jl\nYXRlZCB3aXRoIEdJTVBkLmUHAAAEo0lEQVQ4y02UPWhT7RvGf8/5yMkxMU2NKaYIFtKAHxWloYNU\ncRDeQTsUFPwAFwUHByu4ODq4Oghdiri8UIrooCC0Lx01ONSKfYOioi1WpWmaxtTm5PTkfNzv0H/D\n/9oeePjdPNd13Y8aHR2VR48eEUURpmmiaRqmaXbOAK7r4vs+IsLk5CSTk5P4vo9hGIgIsViMra0t\nCoUCRi6XY8+ePVSrVTRN61yybZuXL1/y7t078vk8mUyGvXv3cuLECWZnZ1lbW6PdbpNIJHAcB8uy\nePr0KYZlWTSbTRKJBLquo5TCMAwmJia4f/8+Sini8Ti1Wo0oikin09i2TbPZJJPJUK/XefDgAefO\nnWNlZQVD0zSUUvi+TxAE6LqOrut8/fqVTCaDbdvkcjk0TSOdTrOysoLrujiOw+bmJmEYMjAwQLVa\nJZVKYXR1ddFut/F9H9M0MU0T3/dZXV3FdV36+/vp7u7m6NGj7Nq1i0qlwuLiIqVSib6+Pubn5wGw\nbZtYLIaxMymVSuH7PpZlEUURSina7TZBEOD7Pp8/fyYMQ3zfZ25ujv3795NOp3n48CE9PT3ouk4Q\nBBi/fv3Ctm0cx6Grq4utrS26u7sREQzDIIoifv78SU9PD5VKhTAMGRoaYnV1leHhYa5evUoQBIRh\niIigiQhRFKHrOs1mE9u2iaKIkydPYhgGAKZp8v79e+LxOPl8Htd1uXbtGrdv3yYMQ3ZyAODFixeb\nrVZLvn//Lq7rSqVSkfX1dREROXz4sBw/flyUUjI6OipXrlyRQ4cOSbPZlCiKxHVdCcNQHMcRz/PE\ndV0BGL53756sra1JrVaT9fV1cRxHRESGhoakr69PUqmUvHr1SsrlsuzI931ptVriuq78+fNHPM+T\nVqslhoikjh075p09e9ba6aKu6/T39zM4OMjS0hIzMzM0Gg12794N0LEIwPd9YrEYrusShiEK4Nmz\nZ41yudyVy+XI5/MMDAyQzWap1+tks1lEhIWFBQqFArZto5QiCAJc1+14t7m5STweRwOo1WoSBAEj\nIyMUi0WSySQiQiqV6lRoYWGhY3673e7sfRAEiAjZbBbHcbaBb9++5cCBA2SzWZLJJLZt43kesViM\nHX379g1d1wnDsNNVEQEgCAIajQZ3797dBi4tLWGaJq7rYpompVKJmZkZ2u12B3j58mWUUmiahoiw\nsbFBEASdD2VsbIwnT55gACil+PHjB7Ozs0xPT/P7929u3ryJZVmEYUgYhhQKBZRSiAie52EYBkop\nLMvi8ePHTE1NUSwWt0OZn5/3hoeHzRs3bqhcLseXL1+YmJjowGzbRtO07RT/F8jO09+8ecP58+dJ\nJBKcPn0abW5uThWLRevOnTv/Li4u8vr1a3p7e9E0jXg8zsePHymVSnz69Kmzr7quY9s2U1NTXLp0\nCc/zOHLkCPv27UPxf6rX63+NjIz8IyKMj48zPT3NwYMHGRwcpLe3FwARodVqcf36dS5evMj4+DhB\nEHDmzBkymQz6DqxSqZDNZr8tLy//DYzdunWL5eVlqtUqHz58IJVKkUwmaTQalMtlLly4gIjw/Plz\nTp06RT6fZ2Njg/8AqMV7tO07rnsAAAAASUVORK5CYII=", parameters => [ { type => "string", desc => "Forced language to use in searches (Japanese won't work due to EH limitations)" }, - { type => "bool", desc => "Save archive title" }, { type => "bool", desc => "Fetch using thumbnail first (falls back to title)" }, { type => "bool", desc => "Search using gID from title (falls back to title)" }, { type => "bool", desc => "Use ExHentai (enable to search for fjorded content without star cookie)" }, @@ -53,9 +52,9 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash + my $lrr_info = shift; # Global info hash my $ua = $lrr_info->{user_agent}; - my ( $lang, $savetitle, $usethumbs, $search_gid, $enablepanda, $jpntitle, $additionaltags, $expunged ) = @_; # Plugin parameters + my ( $lang, $usethumbs, $search_gid, $enablepanda, $jpntitle, $additionaltags, $expunged ) = @_; # Plugin parameters # Use the logger to output status - they'll be passed to a specialized logfile and written to STDOUT. my $logger = get_plugin_logger(); @@ -110,7 +109,7 @@ sub get_tags { if ( $hashdata{tags} ne "" ) { if ( !$hasSrc ) { $hashdata{tags} .= ", source:" . ( split( '://', $domain ) )[1] . "/g/$gID/$gToken"; } - if ($savetitle) { $hashdata{title} = $ehtitle; } + $hashdata{title} = $ehtitle; } #Return a hash containing the new metadata - it will be integrated in LRR. @@ -133,11 +132,7 @@ sub lookup_gallery { $logger->info("Reverse Image Search Enabled, trying now."); #search with image SHA hash - $URL = - $domain - . "?f_shash=" - . $thumbhash - . "&fs_similar=on&fs_covers=on"; + $URL = $domain . "?f_shash=" . $thumbhash . "&fs_similar=on&fs_covers=on"; $logger->debug("Using URL $URL (archive thumbnail hash)"); @@ -149,12 +144,9 @@ sub lookup_gallery { } # Search using gID if present in title name - my ( $title_gid ) = $title =~ /\[([0-9]+)\]/g; + my ($title_gid) = $title =~ /\[([0-9]+)\]/g; if ( $search_gid && $title_gid ) { - $URL = - $domain - . "?f_search=" - . uri_escape_utf8("gid:$title_gid"); + $URL = $domain . "?f_search=" . uri_escape_utf8("gid:$title_gid"); $logger->debug("Found gID: $title_gid, Using URL $URL (gID from archive title)"); @@ -166,18 +158,17 @@ sub lookup_gallery { } # Regular text search (advanced options: Disable default filters for: Language, Uploader, Tags) - $URL = - $domain - . "?advsearch=1&f_sfu=on&f_sft=on&f_sfl=on" - . "&f_search=" - . uri_escape_utf8( qw(") . $title . qw(") ); + $URL = $domain . "?advsearch=1&f_sfu=on&f_sft=on&f_sfl=on" . "&f_search=" . uri_escape_utf8( qw(") . $title . qw(") ); my $has_artist = 0; - # Add artist tag from the OG tags if it exists + # Add artist tag from the OG tags if it exists (and only contains ASCII characters) if ( $tags =~ /.*artist:\s?([^,]*),*.*/gi ) { - $URL = $URL . "+" . uri_escape_utf8("artist:$1"); - $has_artist = 1; + my $artist = $1; + if ( $artist =~ /^[\x00-\x7F]*$/ ) { + $URL = $URL . "+" . uri_escape_utf8("artist:$artist"); + $has_artist = 1; + } } # Add the language override, if it's defined. @@ -186,7 +177,7 @@ sub lookup_gallery { } # Search expunged galleries if the option is enabled. - if ( $expunged ) { + if ($expunged) { $URL = $URL . "&f_sh=on"; } diff --git a/lib/LANraragi/Plugin/Metadata/Eze.pm b/lib/LANraragi/Plugin/Metadata/Eze.pm index d17a1768d..65fed7ff7 100644 --- a/lib/LANraragi/Plugin/Metadata/Eze.pm +++ b/lib/LANraragi/Plugin/Metadata/Eze.pm @@ -13,7 +13,7 @@ use Time::Local qw(timegm_modern); use LANraragi::Model::Plugins; use LANraragi::Utils::Database; use LANraragi::Utils::Logging qw(get_plugin_logger); -use LANraragi::Utils::Generic qw(remove_spaces); +use LANraragi::Utils::String qw(trim); use LANraragi::Utils::Archive qw(is_file_in_archive extract_file_from_archive); #Meta-information about your plugin. @@ -21,19 +21,18 @@ sub plugin_info { return ( #Standard metadata - name => "eze", - type => "metadata", - namespace => "ezeplugin", - author => "Difegue", - version => "2.3", + name => "eze", + type => "metadata", + namespace => "ezeplugin", + author => "Difegue", + version => "2.3.1", description => "Collects metadata from eze-style info.json files ({'gallery_info': {xxx} } syntax), either embedded in your archive or in the same folder with the same name. ({archive_name}.json)", icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA\nB3RJTUUH4wYCFDYBnHlU6AAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUH\nAAAETUlEQVQ4y22UTWhTWRTHf/d9JHmNJLFpShMcKoRIqxXE4sKpjgthYLCLggU/wI1CUWRUxlmU\nWblw20WZMlJc1yKKKCjCdDdYuqgRiygq2mL8aJpmQot5uabv3XdnUftG0bu593AOv3M45/yvGBgY\n4OrVqwRBgG3bGIaBbduhDSClxPM8tNZMTEwwMTGB53lYloXWmkgkwqdPnygUCljZbJbW1lYqlQqG\nYYRBjuNw9+5dHj16RD6fJ51O09bWxt69e5mammJ5eZm1tTXi8Tiu6xKNRrlx4wZWNBqlXq8Tj8cx\nTRMhBJZlMT4+zuXLlxFCEIvFqFarBEFAKpXCcRzq9TrpdJparcbIyAiHDh1icXERyzAMhBB4nofv\n+5imiWmavHr1inQ6jeM4ZLNZDMMglUqxuLiIlBLXdfn48SNKKXp6eqhUKiQSCaxkMsna2hqe52Hb\nNsMdec3n8+Pn2+vpETt37qSlpYVyucz8/DzT09Ns3bqVYrEIgOM4RCIRrI1MiUQCz/P43vE8jxcv\nXqCUwvM8Zmdn2bJlC6lUitHRUdrb2zFNE9/3sd6/f4/jOLiuSzKZDCH1wV/EzMwM3d3dNN69o729\nnXK5jFKKPXv2sLS0RF9fHydOnMD3fZRSaK0xtNYEQYBpmtTr9RC4b98+LMsCwLZtHj9+TCwWI5/P\nI6Xk5MmTXLhwAaUUG3MA4M6dOzQaDd68eYOUkqHIZj0U2ay11mzfvp1du3YhhGBgYIDjx4/T3d1N\nvV4nCAKklCilcF2XZrOJlBIBcOnSJc6ePYsQgj9yBf1l//7OJcXPH1Y1wK/Ff8SfvT995R9d/SA8\nzyMaja5Xq7Xm1q1bLCwssLS09M1Atm3bFr67urq+8W8oRUqJlBJLCMHNmze5d+8e2Ww2DPyrsSxq\ntRqZTAattZibm6PZbHJFVoUQgtOxtAbwfR8A13WJxWIYANVqFd/36e/v/ypzIpEgCAKEEMzNzYXN\n34CN/FsSvu+jtSaTyeC67jrw4cOHdHZ2kslkQmCz2SQSiYT269evMU0zhF2RVaH1ejt932dlZYXh\n4eF14MLCArZtI6UMAb+1/qBPx9L6jNOmAY4dO/b/agBnnDb9e1un3vhQzp8/z/Xr19eBQgjevn3L\n1NTUd5WilKJQKGAYxje+lpYWrl27xuTk5PqKARSLRfr6+hgaGiKbzfLy5UvGx8dRSqGUwnEcDMNA\nKYUQIlRGNBplZmaGw4cPE4/HOXDgAMbs7Cy9vb1cvHiR+fl5Hjx4QC6XwzAMYrEYz549Y3p6mufP\nn4d6NU0Tx3GYnJzk6NGjNJtNduzYQUdHB+LL8mu1Gv39/WitGRsb4/79+3R1dbF7925yuVw4/Uaj\nwalTpzhy5AhjY2P4vs/BgwdJp9OYG7ByuUwmk6FUKgFw7tw5SqUSlUqFp0+fkkgk2LRpEysrKzx5\n8oTBwUG01ty+fZv9+/eTz+dZXV3lP31rAEu+yXjEAAAAAElFTkSuQmCC", parameters => [ - { type => "bool", desc => "Save archive title" }, - { type => "bool", - desc => "Save the original title when available instead of the English or romanised title" + { type => "bool", + desc => "Save the original title when available instead of the English or romanised title" }, { type => "bool", desc => "Fetch additional timestamp (time posted) and uploader metadata" }, ] @@ -45,25 +44,25 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ($save_title, $origin_title, $additional_tags) = @_; # Plugin parameters + my $lrr_info = shift; # Global info hash + my ( $origin_title, $additional_tags ) = @_; # Plugin parameters my $logger = get_plugin_logger(); my $path_in_archive = is_file_in_archive( $lrr_info->{file_path}, "info.json" ); - my ($name, $path, $suffix) = fileparse($lrr_info->{file_path}, qr/\.[^.]*/); + my ( $name, $path, $suffix ) = fileparse( $lrr_info->{file_path}, qr/\.[^.]*/ ); my $path_nearby_json = $path . $name . '.json'; my $filepath; my $delete_after_parse; - + #Extract info.json - if($path_in_archive) { + if ($path_in_archive) { $filepath = extract_file_from_archive( $lrr_info->{file_path}, $path_in_archive ); $logger->debug("Found file in archive at $filepath"); $delete_after_parse = 1; - } elsif (-e $path_nearby_json) { + } elsif ( -e $path_nearby_json ) { $filepath = $path_nearby_json; $logger->debug("Found file nearby at $filepath"); $delete_after_parse = 0; @@ -75,7 +74,7 @@ sub get_tags { my $stringjson = ""; open( my $fh, '<:encoding(UTF-8)', $filepath ) - or return ( error => "Could not open $filepath!" ); + or return ( error => "Could not open $filepath!" ); while ( my $row = <$fh> ) { chomp $row; @@ -88,9 +87,10 @@ sub get_tags { $logger->debug("Loaded the following JSON: $stringjson"); #Parse it - my ( $tags, $title ) = tags_from_eze_json($origin_title, $additional_tags, $hashjson); + my ( $tags, $title ) = tags_from_eze_json( $origin_title, $additional_tags, $hashjson ); + + if ($delete_after_parse) { - if ($delete_after_parse){ #Delete it unlink $filepath; } @@ -98,7 +98,7 @@ sub get_tags { #Return tags $logger->info("Sending the following tags to LRR: $tags"); - if ( $save_title && $title ) { + if ($title) { $logger->info("Parsed title is $title"); return ( tags => $tags, title => $title ); } else { @@ -111,7 +111,7 @@ sub get_tags { #Goes through the JSON hash obtained from an info.json file and return the contained tags. sub tags_from_eze_json { - my ($origin_title, $additional_tags, $hash) = @_; + my ( $origin_title, $additional_tags, $hash ) = @_; my $return = ""; #Tags are in gallery_info -> tags -> one array per namespace @@ -120,11 +120,11 @@ sub tags_from_eze_json { # Titles returned by eze are in complete E-H notation. my $title = $hash->{"gallery_info"}->{"title"}; - if ($origin_title && $hash->{"gallery_info"}->{"title_original"} ) { + if ( $origin_title && $hash->{"gallery_info"}->{"title_original"} ) { $title = $hash->{"gallery_info"}->{"title_original"}; } - remove_spaces($title); + $title = trim($title); foreach my $namespace ( sort keys %$tags ) { @@ -139,23 +139,25 @@ sub tags_from_eze_json { } # Add source tag if possible - my $site = $hash->{"gallery_info"}->{"source"}->{"site"}; - my $gid = $hash->{"gallery_info"}->{"source"}->{"gid"}; - my $gtoken = $hash->{"gallery_info"}->{"source"}->{"token"}; - my $category = $hash->{"gallery_info"}->{"category"}; - my $uploader = $hash->{"gallery_info_full"}->{"uploader"}; + my $site = $hash->{"gallery_info"}->{"source"}->{"site"}; + my $gid = $hash->{"gallery_info"}->{"source"}->{"gid"}; + my $gtoken = $hash->{"gallery_info"}->{"source"}->{"token"}; + my $category = $hash->{"gallery_info"}->{"category"}; + my $uploader = $hash->{"gallery_info_full"}->{"uploader"}; my $timestamp = $hash->{"gallery_info_full"}->{"date_uploaded"}; - if ( $timestamp ) { + if ($timestamp) { + # convert microsecond to second $timestamp = $timestamp / 1000; } else { my $upload_date = $hash->{"gallery_info"}->{"upload_date"}; - my $time = timegm_modern($$upload_date[5],$$upload_date[4],$$upload_date[3],$$upload_date[2],$$upload_date[1]-1,$$upload_date[0]); + my $time = timegm_modern( $$upload_date[5], $$upload_date[4], $$upload_date[3], $$upload_date[2], $$upload_date[1] - 1, + $$upload_date[0] ); $timestamp = $time; } - if ( $category ) { + if ($category) { $return .= ", category:$category"; } diff --git a/lib/LANraragi/Plugin/Metadata/Fakku.pm b/lib/LANraragi/Plugin/Metadata/Fakku.pm index 8879650d8..8df6b513e 100644 --- a/lib/LANraragi/Plugin/Metadata/Fakku.pm +++ b/lib/LANraragi/Plugin/Metadata/Fakku.pm @@ -13,25 +13,25 @@ use Mojo::DOM; #You can also use the LRR Internal API when fitting. use LANraragi::Model::Plugins; use LANraragi::Utils::Logging qw(get_plugin_logger); -use LANraragi::Utils::Generic qw(remove_spaces remove_newlines); +use LANraragi::Utils::String qw(trim trim_CRLF); #Meta-information about your plugin. sub plugin_info { return ( #Standard metadata - name => "FAKKU", - type => "metadata", - namespace => "fakkumetadata", - login_from => "fakkulogin", - author => "Difegue, Nodja", - version => "0.8", + name => "FAKKU", + type => "metadata", + namespace => "fakkumetadata", + login_from => "fakkulogin", + author => "Difegue, Nodja", + version => "0.9", description => "Searches FAKKU for tags matching your archive. If you have an account, don't forget to enter the matching cookie in the login plugin to be able to access controversial content.

    This plugin can and will return invalid results depending on what you're searching for!
    The FAKKU search API isn't very precise and I recommend you use the Chaika.moe plugin when possible.", icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAACZSURBVDhPlY+xDYQwDEWvZgRGYA22Y4frqJDSZhFugiuuo4cqPGT0iTjAYL3C+fGzktc3hEcsQvJq6HtjE2Jdv4viH4a4pWnL8q4A6g+ET9P8YhS2/kqwIZXWnwqChDxPfCFfD76wOzJ2IOR/0DSwnuRKYAKUW3gq2OsJTYM0jr7QVRVwlabJEaw3ARYBcmFXeomxphIeEMIMmh3lOLQR+QQAAAAASUVORK5CYII=", - parameters => [ { type => "bool", desc => "Save archive title" } ], + parameters => [], oneshot_arg => "FAKKU Gallery URL (Will attach tags matching this exact gallery to your archive)" ); @@ -44,8 +44,6 @@ sub get_tags { my $lrr_info = shift; # Global info hash my $ua = $lrr_info->{user_agent}; - my ($savetitle) = @_; # Plugin parameters - my $logger = get_plugin_logger(); # Work your magic here - You can create subs below to organize the code better @@ -79,8 +77,7 @@ sub get_tags { $logger->info("Sending the following tags to LRR: $newtags"); #Return a hash containing the new metadata - it will be integrated in LRR. - if ( $savetitle && $newtags ne "" ) { return ( tags => $newtags, title => $newtitle ); } - else { return ( tags => $newtags ); } + return ( tags => $newtags, title => $newtitle ); } ###### @@ -179,7 +176,7 @@ sub get_tags_from_fakku { my $metadata_parent = $tags_parent->parent->parent; my $title = $metadata_parent->at('h1')->text; - remove_spaces($title); + $title = trim($title); $logger->debug("Parsed title: $title"); my @tags = (); @@ -201,8 +198,8 @@ sub get_tags_from_fakku { ? $row[1]->at('a')->text : $row[1]->text; - remove_spaces($value); - remove_newlines($value); + $value = trim($value); + $value = trim_CRLF($value); $logger->debug("Parsed row: $namespace"); $logger->debug("Matching tag: $value"); @@ -223,8 +220,8 @@ sub get_tags_from_fakku { foreach my $link (@tag_links) { my $tag = $link->text; - remove_spaces($tag); - remove_newlines($tag); + $tag = trim($tag); + $tag = trim_CRLF($tag); unless ( $tag eq "+" || $tag eq "" ) { push( @tags, lc $tag ); } diff --git a/lib/LANraragi/Plugin/Metadata/Hentag.pm b/lib/LANraragi/Plugin/Metadata/Hentag.pm index a4a11634f..51bb619a8 100644 --- a/lib/LANraragi/Plugin/Metadata/Hentag.pm +++ b/lib/LANraragi/Plugin/Metadata/Hentag.pm @@ -20,11 +20,11 @@ sub plugin_info { type => "metadata", namespace => "hentagplugin", author => "siliconfeces", - version => "0.1", + version => "0.2", description => "Parses Hentag info.json files embedded in archives. Achtung, no API calls!", - parameters => [{ type => "bool", desc => "Save archive title" }], - icon => - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAPoAAAD6AG1e1JrAAAEzklEQVR4nO2Xa0xbZRjH6xe/0NKW0p7TQgul9GJpuQ0YbcFgXFx0M+NaoF0gY4mbkwLLxjYoMoiZMVGnDJZt2TT7plnGxmVCgXEdi859MGabH0xqNHFeBsxsETSGnL95314sWyuXrMYYP/zznHN6zvv83uft87zvw4tnzRYho/HGMmrEMmpOxGg5MWuAiNFBzOofE30uN0Ao0yJWkuKXBsL41LWIo5KkQijVeiVKo4XHlyV5GcUmqBKLllWJRYiXmyFg1AgHEbgXxKVAqjJDaS6A0mRDQtpmsMZsyNNyIDeuLoUpd1mqNiM2LsXLy9Xsw+H875eP5N9Fi+Vn7rWcL7hk5RYIZCshAjMXiNVItW7FKxcG0OCZgmtoAq6hcTj7PkRp77sou/QeynojiqMi71w8vmzYvg28g5u/4Tpsi2izLnBuyxz3ZiFQajqPGFki4thnHlsCgUSN8nd60HLjSzSNXcP+8es4MPEZXFfHYb/SA/uVbr+NKM4+2M1VjZxC8Udvczy/Y7gt81yr5R46bEtwZvQhlkmJsP56OM+cx8GpG2gcnfFBjM2iYXQS9oEeVAycWIu4iv4uEBCe2zJPZk8AQAA6bb/BkXE5PACrg0imQ9XJc2ie/RxNnhk0jc5i/9h1NIz8BWAf7A46C173d6Gin1jfNXlefvl9rAtALNdBJNbBuetjNM/cROP0FJqmZ9A0fQ0N0xOo9pwOOg2I3FcOnUT16GmQsFN5TsFx9Qzy6uvAa7P6AIhdFYDVQ8ikotY8gnbHPA7Vf43D+7xorf8OrrqbKDzUAPtQyDL0d1HHzx07AP22l5BWVgxjyQ5qtS9spUvK2xBA1jA6836HO2sBbVn30ZGzCFfqbRhfLEalp8cX5oETNMTOqXPIrnPg6acYCMQp4Mcmgy9IgkBIsky/QYBMDzoKFtFquwe3bQ5Hn30IV8YtpJdUrATo64Jz8ixy9tZSx5JEI+IUBiqS0hEBnBRAQ+vAoxIxWgrQaVui75MMOlrwEC7zLZh3lIcH2FODmBiVz/EjE+KFA6jO6EWMTAkho6EgJBo+q4FAloyazKHoAXTYlugM5Yo8JCRY/bIErSIhH7uzJ0GKVyutH08QwB1MyTlSniPoLv09kLpPFKDNPyixr1t/iajQ96IUgXk6cGTNRzsC99FufRBR5PeoRqDF8iMa8+5E0FdosfwUnQi0+tNwZ+Yg3Yql8nTEU5mDVsKasCtrzJ8FUaoDjoxLNN9J0SEi1S9wTWpCTeZwdAuRI1iKQz/wXf8jldCxhr3gf4DO/8wSuNdzJPv3AAz/PcBAGAB+xCVYWPOxPJiGWR502H4NArQXPIDLfBvpJXbYh7tXHskmziJ3b+3KCDB6zjeWnltnY2IAX6ZCVfoFHCvkghvTG4V/YI/5U2iKttBTb+UnvgaFQOyc/gDpjgrw+UkBAOKcEzFkPB23vtaMNUAgS4JW9TIac+/QPeFI/g9ozv8WWSm7ESNVwtb8Ko1A6cXj9FT8/FvNiE8yQSTTUuciRkedi1jdMrFhmlPTKs0pgUim+wR5n4hRZEPAJNOmRShNBWsINKqb6DdCKXUeOt6ymNFBxOq9vA2156wBQrov0G/oHzO0bNNWnLTg8WQPCY1gIPw6cu8Vs3rLnwIWEm0oy+KXAAAAAElFTkSuQmCC", + parameters => [], + icon => + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAPoAAAD6AG1e1JrAAAEzklEQVR4nO2Xa0xbZRjH6xe/0NKW0p7TQgul9GJpuQ0YbcFgXFx0M+NaoF0gY4mbkwLLxjYoMoiZMVGnDJZt2TT7plnGxmVCgXEdi859MGabH0xqNHFeBsxsETSGnL95314sWyuXrMYYP/zznHN6zvv83uft87zvw4tnzRYho/HGMmrEMmpOxGg5MWuAiNFBzOofE30uN0Ao0yJWkuKXBsL41LWIo5KkQijVeiVKo4XHlyV5GcUmqBKLllWJRYiXmyFg1AgHEbgXxKVAqjJDaS6A0mRDQtpmsMZsyNNyIDeuLoUpd1mqNiM2LsXLy9Xsw+H875eP5N9Fi+Vn7rWcL7hk5RYIZCshAjMXiNVItW7FKxcG0OCZgmtoAq6hcTj7PkRp77sou/QeynojiqMi71w8vmzYvg28g5u/4Tpsi2izLnBuyxz3ZiFQajqPGFki4thnHlsCgUSN8nd60HLjSzSNXcP+8es4MPEZXFfHYb/SA/uVbr+NKM4+2M1VjZxC8Udvczy/Y7gt81yr5R46bEtwZvQhlkmJsP56OM+cx8GpG2gcnfFBjM2iYXQS9oEeVAycWIu4iv4uEBCe2zJPZk8AQAA6bb/BkXE5PACrg0imQ9XJc2ie/RxNnhk0jc5i/9h1NIz8BWAf7A46C173d6Gin1jfNXlefvl9rAtALNdBJNbBuetjNM/cROP0FJqmZ9A0fQ0N0xOo9pwOOg2I3FcOnUT16GmQsFN5TsFx9Qzy6uvAa7P6AIhdFYDVQ8ikotY8gnbHPA7Vf43D+7xorf8OrrqbKDzUAPtQyDL0d1HHzx07AP22l5BWVgxjyQ5qtS9spUvK2xBA1jA6836HO2sBbVn30ZGzCFfqbRhfLEalp8cX5oETNMTOqXPIrnPg6acYCMQp4Mcmgy9IgkBIsky/QYBMDzoKFtFquwe3bQ5Hn30IV8YtpJdUrATo64Jz8ixy9tZSx5JEI+IUBiqS0hEBnBRAQ+vAoxIxWgrQaVui75MMOlrwEC7zLZh3lIcH2FODmBiVz/EjE+KFA6jO6EWMTAkho6EgJBo+q4FAloyazKHoAXTYlugM5Yo8JCRY/bIErSIhH7uzJ0GKVyutH08QwB1MyTlSniPoLv09kLpPFKDNPyixr1t/iajQ96IUgXk6cGTNRzsC99FufRBR5PeoRqDF8iMa8+5E0FdosfwUnQi0+tNwZ+Yg3Yql8nTEU5mDVsKasCtrzJ8FUaoDjoxLNN9J0SEi1S9wTWpCTeZwdAuRI1iKQz/wXf8jldCxhr3gf4DO/8wSuNdzJPv3AAz/PcBAGAB+xCVYWPOxPJiGWR502H4NArQXPIDLfBvpJXbYh7tXHskmziJ3b+3KCDB6zjeWnltnY2IAX6ZCVfoFHCvkghvTG4V/YI/5U2iKttBTb+UnvgaFQOyc/gDpjgrw+UkBAOKcEzFkPB23vtaMNUAgS4JW9TIac+/QPeFI/g9ozv8WWSm7ESNVwtb8Ko1A6cXj9FT8/FvNiE8yQSTTUuciRkedi1jdMrFhmlPTKs0pgUim+wR5n4hRZEPAJNOmRShNBWsINKqb6DdCKXUeOt6ymNFBxOq9vA2156wBQrov0G/oHzO0bNNWnLTg8WQPCY1gIPw6cu8Vs3rLnwIWEm0oy+KXAAAAAElFTkSuQmCC", ); } @@ -33,8 +33,7 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ($save_title) = @_; # Plugin parameter + my $lrr_info = shift; # Global info hash my $logger = get_plugin_logger(); my $file = $lrr_info->{file_path}; @@ -42,6 +41,7 @@ sub get_tags { my $path_in_archive = is_file_in_archive( $file, "info.json" ); if ($path_in_archive) { + #Extract info.json my $filepath = extract_file_from_archive( $file, $path_in_archive ); @@ -69,10 +69,10 @@ sub get_tags { #Return tags $logger->info("Sending the following tags to LRR: $tags"); - if ( $save_title && $title ) { + if ($title) { $logger->info("Parsed title is $title"); return ( tags => $tags, title => $title ); - } elsif ($tags ne "") { + } elsif ( $tags ne "" ) { return ( tags => $tags ); } } @@ -96,18 +96,19 @@ sub tags_from_hentag_json { my $otherTags = $hash->{"otherTags"}; my $language = language_from_hentag_json($hash); my $urls = $hash->{"locations"}; + # not handled yet: category, createdAt # tons of different shit creates different kinds of info.json file, so validate the shit out of the data - @found_tags = try_add_tags(\@found_tags, "series:", $parodies); - @found_tags = try_add_tags(\@found_tags, "group:", $groups); - @found_tags = try_add_tags(\@found_tags, "artist:", $artists); - @found_tags = try_add_tags(\@found_tags, "character:", $characters); - @found_tags = try_add_tags(\@found_tags, "male:", $maleTags); - @found_tags = try_add_tags(\@found_tags, "female:", $femaleTags); - @found_tags = try_add_tags(\@found_tags, "other:", $otherTags); + @found_tags = try_add_tags( \@found_tags, "series:", $parodies ); + @found_tags = try_add_tags( \@found_tags, "group:", $groups ); + @found_tags = try_add_tags( \@found_tags, "artist:", $artists ); + @found_tags = try_add_tags( \@found_tags, "character:", $characters ); + @found_tags = try_add_tags( \@found_tags, "male:", $maleTags ); + @found_tags = try_add_tags( \@found_tags, "female:", $femaleTags ); + @found_tags = try_add_tags( \@found_tags, "other:", $otherTags ); push( @found_tags, "language:" . $language ) unless !defined $language; - @found_tags = try_add_tags(\@found_tags, "source:", $urls); + @found_tags = try_add_tags( \@found_tags, "source:", $urls ); #Done-o my $concat_tags = join( ", ", @found_tags ); @@ -118,19 +119,20 @@ sub tags_from_hentag_json { sub language_from_hentag_json { my ($hash) = @_; - my $language = $hash->{"language"}; + my $language = $hash->{"language"}; return $language; } sub try_add_tags { - my @found_tags = @{$_[0]}; - my $prefix = $_[1]; - my $tags = $_[2]; + my @found_tags = @{ $_[0] }; + my $prefix = $_[1]; + my $tags = $_[2]; my @potential_tags; - if (ref($tags) eq 'ARRAY') { + if ( ref($tags) eq 'ARRAY' ) { foreach my $tag (@$tags) { - if (ref($tag) eq 'HASH' || ref($tag) eq 'ARRAY') { + if ( ref($tag) eq 'HASH' || ref($tag) eq 'ARRAY' ) { + # Weird stuff in here, don't continue parsing to avoid garbage data return @found_tags; } @@ -138,7 +140,7 @@ sub try_add_tags { } } - push(@found_tags, @potential_tags); + push( @found_tags, @potential_tags ); return @found_tags; } diff --git a/lib/LANraragi/Plugin/Metadata/HentagOnline.pm b/lib/LANraragi/Plugin/Metadata/HentagOnline.pm index e973256c4..1198a0d80 100644 --- a/lib/LANraragi/Plugin/Metadata/HentagOnline.pm +++ b/lib/LANraragi/Plugin/Metadata/HentagOnline.pm @@ -13,6 +13,9 @@ use Mojo::UserAgent; use LANraragi::Model::Plugins; use LANraragi::Utils::Logging qw(get_plugin_logger); +use LANraragi::Utils::String; +use String::Similarity; + # Most parsing is reused between the two plugins require LANraragi::Plugin::Metadata::Hentag; @@ -24,17 +27,16 @@ sub plugin_info { type => "metadata", namespace => "hentagonlineplugin", author => "siliconfeces", - version => "0.1", + version => "0.2", description => "Searches hentag.com for tags matching your archive", parameters => [ - { type => "bool", desc => "Save archive title" }, { type => "string", desc => "Comma-separated list of languages to consider. First language = most preferred. Default is \"english, japanese\"" } ], oneshot_arg => "Hentag.com vault URL (Will attach matching tags to your archive)", - icon => + icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAAPoAAAD6AG1e1JrAAAEzklEQVR4nO2Xa0xbZRjH6xe/0NKW0p7TQgul9GJpuQ0YbcFgXFx0M+NaoF0gY4mbkwLLxjYoMoiZMVGnDJZt2TT7plnGxmVCgXEdi859MGabH0xqNHFeBsxsETSGnL95314sWyuXrMYYP/zznHN6zvv83uft87zvw4tnzRYho/HGMmrEMmpOxGg5MWuAiNFBzOofE30uN0Ao0yJWkuKXBsL41LWIo5KkQijVeiVKo4XHlyV5GcUmqBKLllWJRYiXmyFg1AgHEbgXxKVAqjJDaS6A0mRDQtpmsMZsyNNyIDeuLoUpd1mqNiM2LsXLy9Xsw+H875eP5N9Fi+Vn7rWcL7hk5RYIZCshAjMXiNVItW7FKxcG0OCZgmtoAq6hcTj7PkRp77sou/QeynojiqMi71w8vmzYvg28g5u/4Tpsi2izLnBuyxz3ZiFQajqPGFki4thnHlsCgUSN8nd60HLjSzSNXcP+8es4MPEZXFfHYb/SA/uVbr+NKM4+2M1VjZxC8Udvczy/Y7gt81yr5R46bEtwZvQhlkmJsP56OM+cx8GpG2gcnfFBjM2iYXQS9oEeVAycWIu4iv4uEBCe2zJPZk8AQAA6bb/BkXE5PACrg0imQ9XJc2ie/RxNnhk0jc5i/9h1NIz8BWAf7A46C173d6Gin1jfNXlefvl9rAtALNdBJNbBuetjNM/cROP0FJqmZ9A0fQ0N0xOo9pwOOg2I3FcOnUT16GmQsFN5TsFx9Qzy6uvAa7P6AIhdFYDVQ8ikotY8gnbHPA7Vf43D+7xorf8OrrqbKDzUAPtQyDL0d1HHzx07AP22l5BWVgxjyQ5qtS9spUvK2xBA1jA6836HO2sBbVn30ZGzCFfqbRhfLEalp8cX5oETNMTOqXPIrnPg6acYCMQp4Mcmgy9IgkBIsky/QYBMDzoKFtFquwe3bQ5Hn30IV8YtpJdUrATo64Jz8ixy9tZSx5JEI+IUBiqS0hEBnBRAQ+vAoxIxWgrQaVui75MMOlrwEC7zLZh3lIcH2FODmBiVz/EjE+KFA6jO6EWMTAkho6EgJBo+q4FAloyazKHoAXTYlugM5Yo8JCRY/bIErSIhH7uzJ0GKVyutH08QwB1MyTlSniPoLv09kLpPFKDNPyixr1t/iajQ96IUgXk6cGTNRzsC99FufRBR5PeoRqDF8iMa8+5E0FdosfwUnQi0+tNwZ+Yg3Yql8nTEU5mDVsKasCtrzJ8FUaoDjoxLNN9J0SEi1S9wTWpCTeZwdAuRI1iKQz/wXf8jldCxhr3gf4DO/8wSuNdzJPv3AAz/PcBAGAB+xCVYWPOxPJiGWR502H4NArQXPIDLfBvpJXbYh7tXHskmziJ3b+3KCDB6zjeWnltnY2IAX6ZCVfoFHCvkghvTG4V/YI/5U2iKttBTb+UnvgaFQOyc/gDpjgrw+UkBAOKcEzFkPB23vtaMNUAgS4JW9TIac+/QPeFI/g9ozv8WWSm7ESNVwtb8Ko1A6cXj9FT8/FvNiE8yQSTTUuciRkedi1jdMrFhmlPTKs0pgUim+wR5n4hRZEPAJNOmRShNBWsINKqb6DdCKXUeOt6ymNFBxOq9vA2156wBQrov0G/oHzO0bNNWnLTg8WQPCY1gIPw6cu8Vs3rLnwIWEm0oy+KXAAAAAElFTkSuQmCC", ); @@ -45,7 +47,8 @@ sub get_tags { shift; my $lrr_info = shift; - my ( $save_title, $allowed_languages ) = @_; + my ($allowed_languages) = @_; + my $ua = $lrr_info->{user_agent}; my $oneshot_param = $lrr_info->{oneshot_param}; my $logger = get_plugin_logger(); @@ -63,7 +66,8 @@ sub get_tags { } my $vault_id; - my @source_urls = undef; + my @source_urls = undef; + my $archive_title = undef; # First, try running based on a vault ID from a hentag URL if ( defined($oneshot_param) && $oneshot_param ne '' ) { @@ -105,7 +109,7 @@ sub get_tags { # Title lookup $logger->info('Title lookup'); - my $archive_title = $lrr_info->{archive_title}; + $archive_title = $lrr_info->{archive_title}; if ( $string_json eq '' ) { $string_json = get_json_by_title( $ua, $archive_title, $logger ); @@ -117,11 +121,11 @@ sub get_tags { my $json = from_json($string_json); #Parse it - my ( $tags, $title ) = tags_from_hentag_api_json( $json, $allowed_languages ); + my ( $tags, $title ) = tags_from_hentag_api_json( $json, $allowed_languages, $archive_title ); #Return tags IFF data is found $logger->info("Sending the following tags to LRR: $tags"); - if ( $save_title && $title ) { + if ($title) { $logger->info("Parsed title is $title"); return ( tags => $tags, title => $title ); } elsif ( $tags ne "" ) { @@ -133,7 +137,7 @@ sub get_tags { } # Returns the ID from a hentag URL, or undef if invalid. -sub parse_vault_url($url) { +sub parse_vault_url ($url) { if ( !defined $url ) { return; } @@ -191,7 +195,8 @@ sub get_json_by_urls ( $ua, $logger, @urls ) { } # Fetches tags and title, restricted to a language -sub tags_in_language_from_hentag_api_json ( $json, $language ) { +# If $title_hint is set, it attempts to pick the "best" result if multiple hits were returned from Hentag +sub tags_in_language_from_hentag_api_json ( $json, $language, $title_hint = undef ) { $language =~ s/^\s+|\s+$//g; $language = lc($language); @@ -201,17 +206,18 @@ sub tags_in_language_from_hentag_api_json ( $json, $language ) { if (@lang_json_pairs) { # Possible improvement: Look for hits with "better" metadata (more tags, more tags in namespaces, etc). - my ( $tags, $title ) = LANraragi::Plugin::Metadata::Hentag::tags_from_hentag_json( $lang_json_pairs[0] ); + my ( $tags, $title ) = + LANraragi::Plugin::Metadata::Hentag::tags_from_hentag_json( pick_best_hit( $title_hint, @lang_json_pairs ) ); return ( $tags, $title ); } return ( '', '' ); } # Returns (string_with_tags, string_with_title) on success, (empty_string, empty_string) on failure -sub tags_from_hentag_api_json ( $json, $string_prefered_languages ) { +sub tags_from_hentag_api_json ( $json, $string_prefered_languages, $title_hint = undef ) { my @prefered_languages = split( ",", $string_prefered_languages ); foreach my $language (@prefered_languages) { - my ( $tags, $title ) = tags_in_language_from_hentag_api_json( $json, $language ); + my ( $tags, $title ) = tags_in_language_from_hentag_api_json( $json, $language, $title_hint ); if ( $tags ne '' ) { return ( $tags, $title ); } @@ -219,7 +225,7 @@ sub tags_from_hentag_api_json ( $json, $string_prefered_languages ) { return ( '', '' ); } -sub get_existing_hentag_source_url(@tags) { +sub get_existing_hentag_source_url (@tags) { foreach my $tag ( get_source_tags(@tags) ) { if ( parse_vault_url($tag) ) { return $tag; @@ -228,7 +234,7 @@ sub get_existing_hentag_source_url(@tags) { return; } -sub get_source_tags(@tags) { +sub get_source_tags (@tags) { my @found_tags; foreach my $tag (@tags) { if ( $tag =~ /.*source:(.*)/ ) { @@ -238,4 +244,18 @@ sub get_source_tags(@tags) { return @found_tags; } +sub pick_best_hit ( $title_hint, @hits ) { + if ( !defined($title_hint) ) { + return $hits[0]; + } + $title_hint = lc( LANraragi::Utils::String::clean_title($title_hint) ); + + my @titles; + while ( my ( $index, $elem ) = each @hits ) { + my ( $tags, $title ) = LANraragi::Plugin::Metadata::Hentag::tags_from_hentag_json($elem); + $titles[$index] = lc( LANraragi::Utils::String::clean_title($title) ); + } + return $hits[ LANraragi::Utils::String::most_similar( $title_hint, @titles ) ]; +} + 1; diff --git a/lib/LANraragi/Plugin/Metadata/Hitomi.pm b/lib/LANraragi/Plugin/Metadata/Hitomi.pm index 7377dbddc..c79346921 100644 --- a/lib/LANraragi/Plugin/Metadata/Hitomi.pm +++ b/lib/LANraragi/Plugin/Metadata/Hitomi.pm @@ -22,13 +22,13 @@ sub plugin_info { type => "metadata", namespace => "hitomiplugin", author => "doublewelp", - version => "0.1", + version => "0.2", description => "Searches Hitomi.la for tags matching your archive.
    Supports reading the ID from files formatted as \"{Id} Title\" (curly brackets optional)
    This plugin will use the source: tag of the archive if it exists (ex.: source:https://hitomi.la/XXXXX/XXXXX).", - parameters => [{ type => "bool", desc => "Save archive title" }], - icon => - "data:image/png;base64,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", + parameters => [], + icon => + "data:image/png;base64,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", oneshot_arg => "Hitomi Gallery URL (Will attach tags matching this exact gallery to your archive)" ); @@ -38,16 +38,15 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ($savetitle) = @_; # Plugin parameters - my $logger = get_plugin_logger(); + my $lrr_info = shift; # Global info hash + my $logger = get_plugin_logger(); # Work your magic here - You can create subs below to organize the code better my $galleryID = ""; # Quick regex to get the hitomi gallery id from the provided url or source tag. - $logger->debug("Input param is " . $lrr_info->{oneshot_param}); - $logger->debug("Regex is " . "/[.*-|.*\/]([0-9]+)\.html.*/"); + $logger->debug( "Input param is " . $lrr_info->{oneshot_param} ); + $logger->debug( "Regex is " . "/[.*-|.*\/]([0-9]+)\.html.*/" ); if ( $lrr_info->{oneshot_param} =~ /[.*-|.*\/]([0-9]+)\.html.*/ ) { $galleryID = $1; $logger->debug("Skipping search and using gallery $galleryID from oneshot args"); @@ -76,7 +75,7 @@ sub get_tags { return ( error => "No matching Hitomi Gallery Found!" ); } - my %hashdata = get_tags_from_Hitomi( $galleryID, $savetitle); + my %hashdata = get_tags_from_Hitomi($galleryID); $logger->info( "Sending the following tags to LRR: " . $hashdata{tags} ); @@ -94,7 +93,7 @@ sub get_gallery_id_from_title { my $logger = get_plugin_logger(); - $logger->debug("Attempting to parse id from title $title"); + $logger->debug("Attempting to parse id from title $title"); if ( $title =~ /\{?(\d+)\}?/gm ) { $logger->debug("Got $1 from file."); return $1; @@ -107,104 +106,104 @@ sub get_gallery_id_from_title { sub get_js_from_hitomi { my ($gID) = @_; - - my $logger = get_plugin_logger(); - + + my $logger = get_plugin_logger(); + my $gJS = "https://ltn.hitomi.la/galleries/$gID.js"; - - $logger->debug("Hitomi JS: $gJS"); - - my $ua = Mojo::UserAgent->new; - my $res = $ua->get($gJS)->result; - $logger->debug("Hitomi raw JS: ". $res->body); - - if ( $res->is_error ) { + + $logger->debug("Hitomi JS: $gJS"); + + my $ua = Mojo::UserAgent->new; + my $res = $ua->get($gJS)->result; + $logger->debug( "Hitomi raw JS: " . $res->body ); + + if ( $res->is_error ) { return; } - - my $jsonstring = "{}"; - if ( $res->body =~ /var.*galleryinfo.*= (.*)/gmi ) { + + my $jsonstring = "{}"; + if ( $res->body =~ /var.*galleryinfo.*= (.*)/gmi ) { $jsonstring = $1; } $logger->debug("Tentative new JSON: $jsonstring"); - $logger->debug("Beginning JSON decode"); + $logger->debug("Beginning JSON decode"); my $json = decode_json $jsonstring; - $logger->debug("JSON decode successful"); + $logger->debug("JSON decode successful"); return $json; - + } #Extract tags from Hitomi JSON sub get_tags_from_taglist { my ($json) = @_; - - my $logger = get_plugin_logger(); - - my @tags = (); - - if(defined $json->{"tags"}) { - $logger->debug("Extracting tags array"); - my @tags_list = @{ $json->{"tags"} }; - - $logger->debug("Cycling tags array"); - foreach my $tag (@tags_list) { - my $name = $tag->{"tag"}; - my $male = $tag->{"male"}; - my $female = $tag->{"female"}; - - if($male){ - $name = "male:$name"; - } - - if($female){ - $name = "female:$name"; - } - - push( @tags, $name ); - } - } - - if(defined $json->{"parodys"}) { - push (@tags, extract_tags($logger, $json, "parodys", "parody")); - } - - if(defined $json->{"artists"}) { - push (@tags, extract_tags($logger, $json, "artists", "artist")); - } - - if(defined $json->{"groups"}) { - push (@tags, extract_tags($logger, $json, "groups", "group")); - } - - if(defined $json->{"characters"}) { - push (@tags, extract_tags($logger, $json, "characters", "character")); - } - - $logger->debug("Extracting type value"); - push( @tags, "type:" . $json->{"type"}); - - $logger->debug("Extracting language value"); - push( @tags, "language:" . $json->{"language"}); - + + my $logger = get_plugin_logger(); + + my @tags = (); + + if ( defined $json->{"tags"} ) { + $logger->debug("Extracting tags array"); + my @tags_list = @{ $json->{"tags"} }; + + $logger->debug("Cycling tags array"); + foreach my $tag (@tags_list) { + my $name = $tag->{"tag"}; + my $male = $tag->{"male"}; + my $female = $tag->{"female"}; + + if ($male) { + $name = "male:$name"; + } + + if ($female) { + $name = "female:$name"; + } + + push( @tags, $name ); + } + } + + if ( defined $json->{"parodys"} ) { + push( @tags, extract_tags( $logger, $json, "parodys", "parody" ) ); + } + + if ( defined $json->{"artists"} ) { + push( @tags, extract_tags( $logger, $json, "artists", "artist" ) ); + } + + if ( defined $json->{"groups"} ) { + push( @tags, extract_tags( $logger, $json, "groups", "group" ) ); + } + + if ( defined $json->{"characters"} ) { + push( @tags, extract_tags( $logger, $json, "characters", "character" ) ); + } + + $logger->debug("Extracting type value"); + push( @tags, "type:" . $json->{"type"} ); + + $logger->debug("Extracting language value"); + push( @tags, "language:" . $json->{"language"} ); + return @tags; } sub extract_tags { - my ( $logger, $json, $arrayname, $namespace) = @_; - my @tags; - - $logger->debug("Extracting $arrayname array"); - my @list = @{ $json->{$arrayname} }; - $logger->debug("Cycling $arrayname array"); - - foreach my $tag (@list) { - my $name = $tag->{$namespace}; - push( @tags, "$namespace:$name"); - } - return @tags; + my ( $logger, $json, $arrayname, $namespace ) = @_; + my @tags; + + $logger->debug("Extracting $arrayname array"); + my @list = @{ $json->{$arrayname} }; + $logger->debug("Cycling $arrayname array"); + + foreach my $tag (@list) { + my $name = $tag->{$namespace}; + push( @tags, "$namespace:$name" ); + } + return @tags; } sub get_title_from_json { @@ -214,20 +213,20 @@ sub get_title_from_json { sub get_tags_from_Hitomi { - my ( $gID , $savetitle) = @_; + my ($gID) = @_; my %hashdata = ( tags => "" ); - - my $logger = get_plugin_logger(); + + my $logger = get_plugin_logger(); my $json = get_js_from_hitomi($gID); - $logger->debug("Got fully formed JS from Hitomi"); + $logger->debug("Got fully formed JS from Hitomi"); if ($json) { my @tags = get_tags_from_taglist($json); push( @tags, "source:https://hitomi.la/galleries/$gID.html" ) if ( @tags > 0 ); - $hashdata{tags} = join( ', ', @tags ); - $hashdata{title} = get_title_from_json($json) if ($savetitle); + $hashdata{tags} = join( ', ', @tags ); + $hashdata{title} = get_title_from_json($json); } return %hashdata; diff --git a/lib/LANraragi/Plugin/Metadata/Koromo.pm b/lib/LANraragi/Plugin/Metadata/Koromo.pm index 8c10bf227..d25c3d044 100644 --- a/lib/LANraragi/Plugin/Metadata/Koromo.pm +++ b/lib/LANraragi/Plugin/Metadata/Koromo.pm @@ -21,11 +21,11 @@ sub plugin_info { type => "metadata", namespace => "koromoplugin", author => "CirnoT, Difegue", - version => "2.0", + version => "2.1", description => "Collects metadata embedded into your archives as Koromo-style Info.json files. ( {'Tags': [xxx] } syntax)", - icon => + icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TpVoqDmYQcchQnSyIijhKFYtgobQVWnUweekfNDEkKS6OgmvBwZ/FqoOLs64OroIg+APi4uqk6CIl3pcUWsR44fE+zrvn8N59gNCoMs3qGgc03TbTibiUy69IoVeE0QsRAYgys4xkZiEL3/q6pz6quxjP8u/7s/rUgsWAgEQ8ywzTJl4nnt60Dc77xCIryyrxOfGYSRckfuS64vEb55LLAs8UzWx6jlgklkodrHQwK5sa8RRxVNV0yhdyHquctzhr1Rpr3ZO/MFLQlzNcpzWMBBaRRAoSFNRQQRU2YrTrpFhI03ncxz/k+lPkUshVASPHPDagQXb94H/we7ZWcXLCS4rEge4Xx/kYAUK7QLPuON/HjtM8AYLPwJXe9m80gJlP0uttLXoE9G8DF9dtTdkDLneAwSdDNmVXCtISikXg/Yy+KQ8M3ALhVW9urXOcPgBZmtXSDXBwCIyWKHvN5909nXP7t6c1vx8dzXKFeWpUawAAAAZiS0dEAOwAEABqpSa6lwAAAAlwSFlzAAAuIwAALiMBeKU/dgAAAAd0SU1FB+MKCRQBJSKMeg0AAAGVSURBVDjLpZMxa9tQFIXPeaiyhxiZzKFjBme1JFfYgYAe9Bd0yA8JIaQhkJLBP6T/wh3qpZYzm2I8dyilJJMTW7yTIVGRFasE8uAt93K+d+5991IS8Ybj1SVIer24ty8Jk2wyl5S/GkDSi+O4s9PaOYOQh91wSHK2DeLViVut1pmkTwAQtAPUQcz/xCRBEpKOg3ZwEnbDDklvK6AQ+77fds4tSJbBcM7Nm83GbhXiVcXj8fiHpO/WWgfgHAAkXYxGoy8k/UG/nzxDnsqRxF7cO0iS5AhAQxKLm6bpVZqmn8sxAI3kQ2KjKDqQ9GRFEqDNfpQcukrMkDRF3ADAJJvM1+v1n0G/n5D0AcBaew3gFMCFtfbyuVT/cHCYrFarX1mWLQCgsAWSXtgNO81mY/ed7380xpyUn3XOXefr/Ntyufw9vZn+LL7zn21J+fRmOru/f/hrjNmThFLOGWPeV8UvBklSTnIWdsNh0A4g6RiAI/n17vZuWBVvncQNSBAYEK5OvNGDbSMdRdE+AJdl2aJumfjWdX4EIwDvDt7UjSEAAAAASUVORK5CYII=", - parameters => [ { type => "bool", desc => "Save archive title" } ] + parameters => [] ); } @@ -34,8 +34,7 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ($save_title) = @_; # Plugin parameter + my $lrr_info = shift; # Global info hash my $logger = get_plugin_logger(); my $file = $lrr_info->{file_path}; @@ -77,7 +76,7 @@ sub get_tags { #Return tags $logger->info("Sending the following tags to LRR: $tags"); - if ( $save_title && $title ) { + if ($title) { $logger->info("Parsed title is $title"); return ( tags => $tags, title => $title ); } else { @@ -102,6 +101,7 @@ sub tags_from_koromo_json { my $tags = $hash->{"Tags"}; my $characters = $hash->{"Characters"}; my $series = $hash->{"Series"}; + my $magazine = $hash->{"Magazine"}; my $parody = $hash->{"Parody"}; my $groups = $hash->{"Groups"}; my $artist = $hash->{"Artist"}; @@ -143,6 +143,7 @@ sub tags_from_koromo_json { } } + push( @found_tags, "magazine:" . $magazine ) unless !$magazine; push( @found_tags, "language:" . $language ) unless !$language; push( @found_tags, "category:" . $type ) unless !$type; push( @found_tags, "source:" . $url ) unless !$url; diff --git a/lib/LANraragi/Plugin/Metadata/Ksk.pm b/lib/LANraragi/Plugin/Metadata/Ksk.pm new file mode 100644 index 000000000..3c79aed1f --- /dev/null +++ b/lib/LANraragi/Plugin/Metadata/Ksk.pm @@ -0,0 +1,100 @@ +package LANraragi::Plugin::Metadata::Ksk; + +use strict; +use warnings; + +use LANraragi::Model::Plugins; +use LANraragi::Utils::Logging qw(get_plugin_logger); +use LANraragi::Utils::Archive qw(is_file_in_archive extract_file_from_archive); + +use YAML::Syck qw(LoadFile); + +sub plugin_info { + + return ( + name => "Koushoku.yaml", + type => "metadata", + namespace => "kskyamlmeta", + author => "siliconfeces", + version => "0.002", + description => "Collects metadata embedded into your archives as koushoku.yaml/info.yaml files.", + icon => + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAFiUAABYlAUlSJPAAAANkSURBVDhPJZJpU5NXGIbf/9Ev0g+MM7Udp9WWDsVOsRYQKEVZQ4BsZnt9sy9shgShBbTYaTVCKY1B1pBEQggGFOogKEvYdOoXfszVQ/rhmTkz59zXc9/PeaRO12163DZCbgc+8y06HTJ+h5UOp4xLvoXdoOFBf5Auu4LS3obc0oJDp8VhNtLlcyN1uRWcZj13vS5cBi1+mwWPYiLY6cYjG+lxKoR8LgHpw9BQz+OBAbS1tch6DR1uO1Kox4dWVcfdDg9uswGnVSc66wn47QJmwtreTEPFVZxCoKosJ3hbRmlpRt8kNEIrdfscNN+o4tfeHhz6VhHBgqG1nsHeDpxGDV6zDkWjIvxLH25tK2+WUkzcG8JrNdJ/x4803NuJrr4G7Y/X8+UWIl1TDUGfgsfUjl2nwm/WMjrUh72tEXXFNYoKP+b74ks4FQOStuEnVNVlWBtv8kBYcmhVBJwWLOo6vKY2fvbaSD0ZxdnWxKWCj1CVXiEyPIBVuAz6bUiySc0dj0zAbsZtaM1fRH4fwm/RMDYYYCP2lNnfBsn89ZghxcIjMfmxng5GQ92ExIwkj6Kn5UYF6uofhMUG2mvLycYi7GaTnKwvk0vH+XctzXE6weupCFvRCP9MjLMx+Tfdulak4s8KqSr5kppvLmNT3WRQWN5Oz7ObibObnmMnMSXECxwtxdidi7L+Z5jlP0bYnJnEKX5PUpeVshqdINzl475dZnN+kqPsIocrApCa5fVchP3kDAeLc3nQ1vQTNqcjbCZncbQ3It1XZLLhR7wUtTMZZWd2Ugj+f3yYjpFLzbC/OM1BZoHcygJ7KeFEuHu7lsJmViN5G+o4jsd5+fAhKyMjecDJUoK9xDTH4uG753E+bCxxtJpkX5xzmQS5FyniU2MYNCKCsbo8b/84GWf7aZSt2Wi+81kdPU+wPj1OOOAhIHbi3Yu0GGqS07evqCv7llCXA+n6VxcpKTzHwsgwH1bTvBf0g7NOwu7J6jPGQn4iQ4H8XPZErNPNdYIWPZfPn6OvUwDUlVe59vknfHe+gLGAn9PtNQ7XnpHLJjgUdQZ6vy4iCMDxaiq/D8WFBXx9oZCA+DFJI3agougiVV9cyEOqij6l32UkFr6Xz7yfibG3PM/eSoLs1Di2+loaS0uovFIkFlDhPxYUixj0Cgg3AAAAAElFTkSuQmCC", + parameters => [ { type => "bool", desc => "Assume english" } ], + ); +} + +sub get_tags { + shift; + my $lrr_info = shift; + my ($assume_english) = @_; + + my $logger = get_plugin_logger(); + my $file = $lrr_info->{file_path}; + + my $path_in_archive = is_file_in_archive( $file, "koushoku.yaml" ); + + if ( !$path_in_archive ) { + $path_in_archive = is_file_in_archive( $file, "info.yaml" ); + } + + if ( !$path_in_archive ) { + return ( error => "No KSK metadata file found in archive" ); + } + + my $filepath = extract_file_from_archive( $file, $path_in_archive ); + + my $parsed_data = LoadFile($filepath); + + my ( $tags, $title ) = tags_from_ksk_yaml( $parsed_data, $assume_english ); + + unlink $filepath; + + #Return tags + $logger->info("Sending the following tags to LRR: $tags"); + if ($title) { + $logger->info("Parsed title is $title"); + return ( tags => $tags, title => $title ); + } else { + return ( tags => $tags ); + } +} + +sub tags_from_ksk_yaml { + my $hash = $_[0]; + my $assume_english = $_[1]; + my @found_tags; + my $logger = get_plugin_logger(); + + my $title = $hash->{"Title"}; + my $tags = $hash->{"Tags"}; + my $parody = $hash->{"Parody"}; + my $artists = $hash->{"Artist"}; + my $magazine = $hash->{"Magazine"}; + my $url = $hash->{"URL"}; + + foreach my $tag (@$tags) { + push( @found_tags, $tag ); + } + foreach my $tag (@$artists) { + push( @found_tags, "artist:" . $tag ); + } + foreach my $tag (@$parody) { + push( @found_tags, "series:" . $tag ); + } + foreach my $tag (@$magazine) { + push( @found_tags, "magazine:" . $tag ); + } + if ($assume_english) { + push( @found_tags, "language:english" ); + } + + push( @found_tags, "source:" . $url ) unless !$url; + + #Done-o + my $concat_tags = join( ", ", @found_tags ); + return ( $concat_tags, $title ); + +} + +1; diff --git a/lib/LANraragi/Plugin/Metadata/RegexParse.pm b/lib/LANraragi/Plugin/Metadata/RegexParse.pm index 3d02eab6d..a9c551258 100644 --- a/lib/LANraragi/Plugin/Metadata/RegexParse.pm +++ b/lib/LANraragi/Plugin/Metadata/RegexParse.pm @@ -12,24 +12,23 @@ use Scalar::Util qw(looks_like_number); #You can also use the LRR Internal API when fitting. use LANraragi::Model::Plugins; use LANraragi::Utils::Database qw(redis_encode redis_decode); -use LANraragi::Utils::Logging qw(get_logger); -use LANraragi::Utils::Generic qw(remove_spaces); +use LANraragi::Utils::Logging qw(get_logger); #Meta-information about your plugin. sub plugin_info { return ( #Standard metadata - name => "Filename Parsing", - type => "metadata", - namespace => "regexplugin", - author => "Difegue", - version => "1.0", + name => "Filename Parsing", + type => "metadata", + namespace => "regexplugin", + author => "Difegue", + version => "1.0", description => "Derive tags from the filename of the given archive.
    Follows the doujinshi naming standard (Release) [Artist] TITLE (Series) [Language].", icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAAXNSR0IArs4c6QAAAL1JREFUOI1jZMABpNbH/sclx8DAwPAscDEjNnEMQUIGETIYhUOqYdgMhTPINQzdUEZqGIZsKBM1DEIGTOiuexqwCKdidDl0vtT62P9kuZCJEWuKYWBgYGBgRHbh04BFDNIb4jAUbbSrZTARUkURg6lD10OUC/0PNaMYgs1Skgwk1jCSDCQWoBg46dYmhite0+D8pwGLCMY6uotRDOy8toZBkI2HIhcO/pxCm8KBUkOxFl/kGoq3gCXFYFxVAACeoU/8xSNybwAAAABJRU5ErkJggg==", - parameters => [ { type => "bool", desc => "Save archive title", default_value => "1" } ] + parameters => [ ] ); } @@ -39,10 +38,9 @@ sub get_tags { shift; my $lrr_info = shift; # Global info hash - my ($savetitle) = @_; # Plugin parameters my $logger = get_logger( "regexparse", "plugins" ); - my $file = $lrr_info->{file_path}; + my $file = $lrr_info->{file_path}; # lrr_info's file_path is taken straight from the filesystem, which might not be proper UTF-8. # Run a decode to make sure we can derive tags with the proper encoding. @@ -99,12 +97,8 @@ sub get_tags { $logger->info("Sending the following tags to LRR: $tagstring"); - if ($savetitle) { - $logger->info("Parsed title is $title"); - return ( tags => $tagstring, title => $title ); - } else { - return ( tags => $tagstring ); - } + $logger->info("Parsed title is $title"); + return ( tags => $tagstring, title => $title ); } diff --git a/lib/LANraragi/Plugin/Metadata/nHentai.pm b/lib/LANraragi/Plugin/Metadata/nHentai.pm index 65f712a57..d3e902bc0 100644 --- a/lib/LANraragi/Plugin/Metadata/nHentai.pm +++ b/lib/LANraragi/Plugin/Metadata/nHentai.pm @@ -23,13 +23,13 @@ sub plugin_info { namespace => "nhplugin", login_from => "nhentaicfbypass", author => "Difegue and others", - version => "1.7.2", + version => "1.7.3", description => "Searches nHentai for tags matching your archive.
    Supports reading the ID from files formatted as \"{Id} Title\" and if not, tries to search for a matching gallery.
    This plugin will use the source: tag of the archive if it exists.", icon => "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA\nB3RJTUUH4wYCFA8s1yKFJwAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUH\nAAACL0lEQVQ4y6XTz0tUURQH8O+59773nLFcaGWTk4UUVCBFiJs27VxEQRH0AyRo4x8Q/Qtt2rhr\nU6soaCG0KYKSwIhMa9Ah+yEhZM/5oZMG88N59717T4sxM8eZCM/ycD6Xwznn0pWhG34mh/+PA8mk\n8jO5heziP0sFYwfgMDFQJg4IUjmquSFGG+OIlb1G9li5kykgTgvzSoUCaIYlo8/Igcjpj5wOkARp\n8AupP0uzJLijCY4zzoXOxdBLshAgABr8VOp7bpAXDEI7IBrhdksnjNr3WzI4LaIRV9fk2iAaYV/y\nA1dPiYjBAALgpQxnhV2XzTCAGWGeq7ACBvCdzKQyTH+voAm2hGlpcmQt2Bc2K+ymAhWPxTzPDQLt\nOKo1FiNBQaArq9WNRQwEgKl7XQ1duzSRSn/88vX0qf7DPQddx1nI5UfHxt+m0sLYPiP3shRAG8MD\nok1XEEXR/EI2ly94nrNYWG6Nx0/2Hp2b94dv34mlZge1e4hVCJ4jc6tl9ZP803n3/i4lpdyzq2N0\n7M3DkSeF5ZVYS8v1qxcGz5+5eey4nPDbmGdE9FpGeWErVNe2tTabX3r0+Nk3PwOgXFkdfz99+exA\nMtFZITEt9F23mpLG0hYTVQCKpfKPlZ/rqWKpYoAPcTmpginW76QBbb0OBaBaDdjaDbNlJmQE3/d0\nMYoaybU9126oPkrEhpr+U2wjtoVVGBowkslEsVSupRKdu0Mduq7q7kqExjSS3V2dvwDLavx0eczM\neAAAAABJRU5ErkJggg==", - parameters => [ { type => "bool", desc => "Save archive title" } ], + parameters => [], oneshot_arg => "nHentai Gallery URL (Will attach tags matching this exact gallery to your archive)" ); @@ -39,10 +39,9 @@ sub plugin_info { sub get_tags { shift; - my $lrr_info = shift; # Global info hash - my ($savetitle) = @_; # Plugin parameters - my $ua = $lrr_info->{user_agent}; # UserAgent from login plugin - + my $lrr_info = shift; # Global info hash + my $ua = $lrr_info->{user_agent}; # UserAgent from login plugin + my $logger = get_plugin_logger(); # Work your magic here - You can create subs below to organize the code better @@ -78,7 +77,7 @@ sub get_tags { return ( error => "No matching nHentai Gallery Found!" ); } - my %hashdata = get_tags_from_NH( $galleryID, $savetitle, $ua ); + my %hashdata = get_tags_from_NH( $galleryID, $ua ); $logger->info( "Sending the following tags to LRR: " . $hashdata{tags} ); @@ -93,7 +92,7 @@ sub get_tags { #Uses the website's search to find a gallery and returns its content. sub get_gallery_dom_by_title { - my ($title, $ua) = @_; + my ( $title, $ua ) = @_; my $logger = get_plugin_logger(); @@ -116,7 +115,7 @@ sub get_gallery_dom_by_title { sub get_gallery_id_from_title { - my ($title, $ua) = @_; + my ( $title, $ua ) = @_; my $logger = get_plugin_logger(); @@ -125,7 +124,7 @@ sub get_gallery_id_from_title { return $1; } - my $dom = get_gallery_dom_by_title($title, $ua); + my $dom = get_gallery_dom_by_title( $title, $ua ); if ($dom) { @@ -147,14 +146,15 @@ sub get_gallery_id_from_title { # retrieves html page from NH sub get_html_from_NH { - my ($gID, $ua) = @_; - + my ( $gID, $ua ) = @_; + my $URL = "https://nhentai.net/g/$gID/"; my $res = $ua->get($URL)->result; if ( $res->is_error ) { - return; + my $code = $res->code; + return "error ($code)"; } return $res->body; @@ -213,11 +213,17 @@ sub get_title_from_json { sub get_tags_from_NH { - my ( $gID, $savetitle, $ua ) = @_; + my ( $gID, $ua ) = @_; my %hashdata = ( tags => "" ); - my $html = get_html_from_NH($gID, $ua); + my $html = get_html_from_NH( $gID, $ua ); + + # If the string starts with "error", we couldn't retrieve data from NH. + if ( $html =~ /^error/ ) { + return ( error => "Error retrieving gallery from nHentai! ($html)" ); + } + my $json = get_json_from_html($html); if ($json) { @@ -225,8 +231,8 @@ sub get_tags_from_NH { push( @tags, "source:nhentai.net/g/$gID" ) if ( @tags > 0 ); # Use NH's "pretty" names (romaji titles without extraneous data we already have like (Event)[Artist], etc) - $hashdata{tags} = join( ', ', @tags ); - $hashdata{title} = get_title_from_json($json) if ($savetitle); + $hashdata{tags} = join( ', ', @tags ); + $hashdata{title} = get_title_from_json($json); } return %hashdata; diff --git a/lib/LANraragi/Plugin/Scripts/BlacklistMigrate.pm b/lib/LANraragi/Plugin/Scripts/BlacklistMigrate.pm deleted file mode 100644 index 33552de1b..000000000 --- a/lib/LANraragi/Plugin/Scripts/BlacklistMigrate.pm +++ /dev/null @@ -1,74 +0,0 @@ -package LANraragi::Plugin::Scripts::BlacklistMigrate; - -use strict; -use warnings; -no warnings 'uninitialized'; - -use LANraragi::Utils::Logging qw(get_plugin_logger); -use LANraragi::Utils::Database qw(save_computed_tagrules); -use LANraragi::Utils::Tags qw(tags_rules_to_array restore_CRLF); -use LANraragi::Utils::Generic qw(remove_spaces); -use Mojo::JSON qw(encode_json); -use LANraragi::Model::Config; - -#Meta-information about your plugin. -sub plugin_info { - - return ( - #Standard metadata - name => "Blacklist Migration", - type => "script", - namespace => "blist2rule", - author => "Difegue", - version => "1.0", - description => "Migrate your blacklist from LANraragi < 0.8.0 databases to the new Tag Rules system." - ); - -} - -# Mandatory function to be implemented by your script -sub run_script { - shift; - my $lrr_info = shift; # Global info hash - - my $logger = get_plugin_logger(); - my $redis = LANraragi::Model::Config->get_redis_config; - - my $blist = LANraragi::Model::Config::get_redis_conf( "blacklist", undef ); - my $rules = LANraragi::Model::Config::get_redis_conf( "tagrules", - "-already uploaded;-forbidden content;-incomplete;-ongoing;-complete;-various;-digital;-translated;-russian;-chinese;-portuguese;-french;-spanish;-italian;-vietnamese;-german;-indonesian" - ); - - unless ($blist) { - $logger->info("No blacklist in config, nothing to migrate!"); - return ( status => "Nothing to migrate" ); - } - - $logger->debug("Blacklist is $blist"); - $logger->debug("Rules are $rules"); - my @blacklist = split( ',', $blist ); # array-ize the blacklist string - my $migrated = 0; - - # Parse the blacklist and add matching tag rules. - foreach my $tag (@blacklist) { - - remove_spaces($tag); - if ( index( uc($rules), uc($tag) ) == -1 ) { - $logger->debug("Adding rule -$tag"); - $rules = $rules . ";-$tag"; - $migrated++; - } - } - - # Save rules and recompute them - $redis->hset( "LRR_CONFIG", "tagrules", $rules ); - $redis->hdel( "LRR_CONFIG", "blacklist" ); - - my @computed_tagrules = tags_rules_to_array( restore_CRLF($rules) ); - $logger->debug( "Saving computed tag rules : " . encode_json( \@computed_tagrules ) ); - save_computed_tagrules( \@computed_tagrules ); - - return ( migrated_tags => $migrated ); -} - -1; diff --git a/lib/LANraragi/Plugin/Scripts/SourceFinder.pm b/lib/LANraragi/Plugin/Scripts/SourceFinder.pm index 04d9fe406..5f953fc4f 100644 --- a/lib/LANraragi/Plugin/Scripts/SourceFinder.pm +++ b/lib/LANraragi/Plugin/Scripts/SourceFinder.pm @@ -7,7 +7,7 @@ no warnings 'uninitialized'; use Mojo::UserAgent; use LANraragi::Utils::Logging qw(get_plugin_logger); use LANraragi::Model::Stats; -use LANraragi::Utils::Generic qw(trim_url); +use LANraragi::Utils::String qw(trim_url); #Meta-information about your plugin. sub plugin_info { diff --git a/lib/LANraragi/Utils/Archive.pm b/lib/LANraragi/Utils/Archive.pm index fd80dbde4..bb73e4d05 100644 --- a/lib/LANraragi/Utils/Archive.pm +++ b/lib/LANraragi/Utils/Archive.pm @@ -41,29 +41,35 @@ sub is_pdf { # use ImageMagick to make a thumbnail, height = 500px (view in index is 280px tall) # If use_hq is true, the scale algorithm will be used instead of sample. -sub generate_thumbnail ( $orig_path, $thumb_path, $use_hq ) { +# If use_jxl is true, JPEG XL will be used instead of JPEG. +sub generate_thumbnail ( $orig_path, $thumb_path, $use_hq, $use_jxl ) { my $img = Image::Magick->new; + my $format = $use_jxl ? 'jxl' : 'jpg'; + # For JPEG, the size option (or jpeg:size option) provides a hint to the JPEG decoder # that it can reduce the size on-the-fly during decoding. This saves memory because # it never has to allocate memory for the full-sized image - $img->Set( option => 'jpeg:size=500x' ); + if ($format eq 'jpg') { + $img->Set(option => 'jpeg:size=500x'); + } # If the image is a gif, only take the first frame - if ( $orig_path =~ /\.gif$/ ) { - $img->Read( $orig_path . "[0]" ); + if ($orig_path =~ /\.gif$/) { + $img->Read($orig_path . "[0]"); } else { $img->Read($orig_path); } # The "-scale" resize operator is a simplified, faster form of the resize command. if ($use_hq) { - $img->Scale( geometry => '500x1000' ); - } else { # Sample is very fast due to not applying filters. - $img->Sample( geometry => '500x1000' ); + $img->Scale(geometry => '500x1000'); + } else { # Sample is very fast due to not applying filters. + $img->Sample(geometry => '500x1000'); } - $img->Set( quality => "50", magick => "jpg" ); + + $img->Set(quality => "50", magick => $format); $img->Write($thumb_path); undef $img; } @@ -150,9 +156,13 @@ sub extract_thumbnail ( $thumbdir, $id, $page, $use_hq ) { my $logger = get_logger( "Archive", "lanraragi" ); + # JPG is used for thumbnails by default + my $use_jxl = LANraragi::Model::Config->get_jxlthumbpages; + my $format = $use_jxl ? 'jxl' : 'jpg'; + # Another subfolder with the first two characters of the id is used for FS optimization. my $subfolder = substr( $id, 0, 2 ); - my $thumbname = "$thumbdir/$subfolder/$id.jpg"; + my $thumbname = "$thumbdir/$subfolder/$id.$format"; make_path("$thumbdir/$subfolder"); my $redis = LANraragi::Model::Config->get_redis; @@ -167,28 +177,29 @@ sub extract_thumbnail ( $thumbdir, $id, $page, $use_hq ) { my @filelist = @$images; my $requested_image = $filelist[ $page > 0 ? $page - 1 : 0 ]; - die "Requested image not found" unless $requested_image; + die "Requested image not found: $requested_image" unless $requested_image; $logger->debug("Extracting thumbnail for $id page $page from $requested_image"); # Extract first image to temp dir my $arcimg = extract_single_file( $file, $requested_image, $temppath ); - if ( $page > 0 ) { + if ( $page - 1 > 0 ) { # Non-cover thumbnails land in a dedicated folder. - $thumbname = "$thumbdir/$subfolder/$id/$page.jpg"; + $thumbname = "$thumbdir/$subfolder/$id/$page.$format"; make_path("$thumbdir/$subfolder/$id"); } else { # For cover thumbnails, grab the SHA-1 hash for tag research. # That way, no need to repeat a costly extraction later. my $shasum = shasum( $arcimg, 1 ); + $logger->debug("Setting thumbnail hash: $shasum"); $redis->hset( $id, "thumbhash", $shasum ); $redis->quit(); } # Thumbnail generation - generate_thumbnail( $arcimg, $thumbname, $use_hq ); + generate_thumbnail( $arcimg, $thumbname, $use_hq, $use_jxl ); # Clean up safe folder remove_tree($temppath); @@ -340,17 +351,14 @@ sub extract_single_file ( $archive, $filepath, $destination ) { } # Variant for plugins. -# Extracts the file with a timestamp to a folder in /temp/plugin. +# Extracts the file to a folder in /temp/plugin. sub extract_file_from_archive ( $archive, $filename ) { - # Timestamp extractions in microseconds - my ( $seconds, $microseconds ) = gettimeofday; - my $stamp = "$seconds-$microseconds"; - my $path = get_temp . "/plugin/$stamp"; - mkdir get_temp . "/plugin"; + my $path = get_temp . "/plugin"; mkdir $path; - return extract_single_file( $archive, $filename, $path ); + my $tmp = tempdir( DIR => $path, CLEANUP => 1 ); + return extract_single_file( $archive, $filename, $tmp ); } 1; diff --git a/lib/LANraragi/Utils/Database.pm b/lib/LANraragi/Utils/Database.pm index 45276491f..0338ec4d4 100644 --- a/lib/LANraragi/Utils/Database.pm +++ b/lib/LANraragi/Utils/Database.pm @@ -8,22 +8,25 @@ use feature qw(signatures); no warnings 'experimental::signatures'; use Digest::SHA qw(sha256_hex); -use Mojo::JSON qw(decode_json); +use Mojo::JSON qw(decode_json); use Encode; use File::Basename; +use File::Path qw(remove_tree); use Redis; use Cwd; use Unicode::Normalize; +use List::MoreUtils qw(uniq); -use LANraragi::Utils::Generic qw(flat remove_spaces remove_newlines trim_url); -use LANraragi::Utils::Tags qw(unflat_tagrules tags_rules_to_array restore_CRLF); +use LANraragi::Utils::Generic qw(flat); +use LANraragi::Utils::String qw(trim trim_CRLF trim_url); +use LANraragi::Utils::Tags qw(unflat_tagrules tags_rules_to_array restore_CRLF join_tags_to_string split_tags_to_array ); use LANraragi::Utils::Archive qw(get_filelist); use LANraragi::Utils::Logging qw(get_logger); # Functions for interacting with the DB Model. use Exporter 'import'; our @EXPORT_OK = - qw(redis_encode redis_decode invalidate_cache compute_id set_tags set_title set_isnew get_computed_tagrules save_computed_tagrules get_archive_json get_archive_json_multi); + qw(redis_encode redis_decode invalidate_cache compute_id set_tags set_title set_isnew get_computed_tagrules save_computed_tagrules get_archive_json get_archive_json_multi get_tankoubons_by_file); # Creates a DB entry for a file path with the given ID. # This function doesn't actually require the file to exist at its given location. @@ -39,6 +42,9 @@ sub add_archive_to_redis ( $id, $file, $redis ) { $redis->hset( $id, "name", redis_encode($name) ); $redis->hset( $id, "tags", "" ); + if ( defined($file) && -e $file ) { + set_arcsize( $redis, $id, -s $file ); + } # Don't encode filenames. $redis->hset( $id, "file", $file ); @@ -58,13 +64,16 @@ sub add_archive_to_redis ( $id, $file, $redis ) { sub change_archive_id ( $old_id, $new_id ) { my $logger = get_logger( "Archive", "lanraragi" ); - my $redis = LANraragi::Model::Config->get_redis; + my $redis = LANraragi::Model::Config->get_redis; $logger->debug("Changing ID $old_id to $new_id"); if ( $redis->exists($old_id) ) { $redis->rename( $old_id, $new_id ); } + + my $file = $redis->hget( $new_id, "file" ); + set_arcsize( $redis, $new_id, -s $file ); $redis->quit; # We also need to update categories that contain the ID. @@ -132,7 +141,7 @@ sub get_archive_json ( $redis, $id ) { } # Uses Redis' MULTI to get an archive JSON for each ID. -sub get_archive_json_multi(@ids) { +sub get_archive_json_multi (@ids) { my $redis = LANraragi::Model::Config->get_redis; @@ -171,7 +180,8 @@ sub build_json ( $id, %hash ) { # It's not a new archive, but it might have never been clicked on yet, # so grab the value for $isnew stored in redis. - my ( $name, $title, $tags, $file, $isnew, $progress, $pagecount ) = @hash{qw(name title tags file isnew progress pagecount)}; + my ( $name, $title, $tags, $file, $isnew, $progress, $pagecount, $lastreadtime ) = + @hash{qw(name title tags file isnew progress pagecount lastreadtime)}; # Return undef if the file doesn't exist. return unless ( defined($file) && -e $file ); @@ -185,20 +195,21 @@ sub build_json ( $id, %hash ) { } my $arcdata = { - arcid => $id, - title => $title, - tags => $tags, - isnew => $isnew ? $isnew : "false", - extension => lc( ( split( /\./, $file ) )[-1] ), - progress => $progress ? int($progress) : 0, - pagecount => $pagecount ? int($pagecount) : 0 + arcid => $id, + title => $title, + tags => $tags, + isnew => $isnew ? $isnew : "false", + extension => lc( ( split( /\./, $file ) )[-1] ), + progress => $progress ? int($progress) : 0, + pagecount => $pagecount ? int($pagecount) : 0, + lastreadtime => $lastreadtime ? int($lastreadtime) : 0 }; return $arcdata; } # Deletes the archive with the given id from redis, and the matching archive file/thumbnail. -sub delete_archive($id) { +sub delete_archive ($id) { my $redis = LANraragi::Model::Config->get_redis; my $filename = $redis->hget( $id, "file" ); @@ -206,8 +217,8 @@ sub delete_archive($id) { $oldtags = redis_decode($oldtags); my $oldtitle = lc( redis_decode( $redis->hget( $id, "title" ) ) ); - remove_spaces($oldtitle); - remove_newlines($oldtitle); + $oldtitle = trim($oldtitle); + $oldtitle = trim_CRLF($oldtitle); $oldtitle = redis_encode($oldtitle); $redis->del($id); @@ -227,9 +238,15 @@ sub delete_archive($id) { my $thumbdir = LANraragi::Model::Config->get_thumbdir; my $subfolder = substr( $id, 0, 2 ); - my $thumbname = "$thumbdir/$subfolder/$id.jpg"; - unlink $thumbname; + my $jpg_thumbname = "$thumbdir/$subfolder/$id.jpg"; + unlink $jpg_thumbname; + + my $jxl_thumbname = "$thumbdir/$subfolder/$id.jxl"; + unlink $jxl_thumbname; + + # Delete the thumbpages folder + remove_tree("$thumbdir/$subfolder/$id/"); return $filename; } @@ -270,7 +287,7 @@ sub clean_database { # Get the filemap for ID checks later down the line my @filemapids = $redis_config->exists("LRR_FILEMAP") ? $redis_config->hvals("LRR_FILEMAP") : (); - my %filemap = map { $_ => 1 } @filemapids; + my %filemap = map { $_ => 1 } @filemapids; #40-character long keys only => Archive IDs my @keys = $redis->keys('????????????????????????????????????????'); @@ -338,8 +355,8 @@ sub set_title ( $id, $newtitle ) { # Remove old title from search set if ( $redis->hexists( $id, "title" ) ) { my $oldtitle = lc( redis_decode( $redis->hget( $id, "title" ) ) ); - remove_spaces($oldtitle); - remove_newlines($oldtitle); + $oldtitle = trim($oldtitle); + $oldtitle = trim_CRLF($oldtitle); $oldtitle = redis_encode($oldtitle); $redis_search->zrem( "LRR_TITLES", "$oldtitle\0$id" ); } @@ -349,8 +366,8 @@ sub set_title ( $id, $newtitle ) { # Set title/ID key in search set $newtitle = lc($newtitle); - remove_spaces($newtitle); - remove_newlines($newtitle); + $newtitle = trim($newtitle); + $newtitle = trim_CRLF($newtitle); $newtitle = redis_encode($newtitle); $redis_search->zadd( "LRR_TITLES", 0, "$newtitle\0$id" ); } @@ -362,7 +379,7 @@ sub set_title ( $id, $newtitle ) { # Set $append to 1 if you want to append the tags instead of replacing them. sub set_tags ( $id, $newtags, $append = 0 ) { - my $redis = LANraragi::Model::Config->get_redis; + my $redis = LANraragi::Model::Config->get_redis; my $oldtags = $redis->hget( $id, "tags" ); $oldtags = redis_decode($oldtags); @@ -372,7 +389,7 @@ sub set_tags ( $id, $newtags, $append = 0 ) { unless ( length $newtags ) { return; } if ($oldtags) { - remove_spaces($oldtags); + $oldtags = trim($oldtags); if ( $oldtags ne "" ) { $newtags = $oldtags . "," . $newtags; @@ -380,6 +397,8 @@ sub set_tags ( $id, $newtags, $append = 0 ) { } } + $newtags = join_tags_to_string( uniq( split_tags_to_array($newtags) ) ); + # Update sets depending on the added/removed tags update_indexes( $id, $oldtags, $newtags ); @@ -408,6 +427,8 @@ sub set_isnew ( $id, $isnew ) { $redis_search->quit; $redis->quit; + + invalidate_cache(); } # Splits both old and new tags, and: @@ -418,15 +439,14 @@ sub update_indexes ( $id, $oldtags, $newtags ) { my $redis = LANraragi::Model::Config->get_redis_search; $redis->multi; - my @oldtags = split( /,\s?/, $oldtags // "" ); - my @newtags = split( /,\s?/, $newtags // "" ); + my @oldtags = split( /,\s?/, $oldtags // "" ); + my @newtags = split( /,\s?/, $newtags // "" ); my $has_tags = 0; foreach my $tag (@oldtags) { if ( $tag =~ /source:(.*)/i ) { - my $url = $1; - trim_url($url); + my $url = trim_url($1); $redis->hdel( "LRR_URLMAP", $url ); } @@ -441,8 +461,7 @@ sub update_indexes ( $id, $oldtags, $newtags ) { # If the tag is a source: tag, add it to the URL index if ( $tag =~ /source:(.*)/i ) { - my $url = $1; - trim_url($url); + my $url = trim_url($1); $redis->hset( "LRR_URLMAP", $url, $id ); } @@ -462,7 +481,7 @@ sub update_indexes ( $id, $oldtags, $newtags ) { # This function is used for all ID computation in LRR. # Takes the path to the file as an argument. -sub compute_id($file) { +sub compute_id ($file) { #Read the first 512 KBs only (allows for faster disk speeds ) open( my $handle, '<', $file ) or die "Couldn't open $file :" . $!; @@ -484,7 +503,7 @@ sub compute_id($file) { } # Normalize the string to Unicode NFC, then layer on redis_encode for Redis-safe serialization. -sub redis_encode($data) { +sub redis_encode ($data) { my $NFC_data = NFC($data); return encode_utf8($NFC_data); @@ -492,7 +511,7 @@ sub redis_encode($data) { # Final Solution to the Unicode glitches -- Eval'd double-decode for data obtained from Redis. # This should be a one size fits-all function. -sub redis_decode($data) { +sub redis_decode ($data) { # Setting FB_CROAK tells encode to die instantly if it encounters any errors. # Without this setting, it typically tries to replace characters... which might already be valid UTF8! @@ -506,7 +525,7 @@ sub redis_decode($data) { # Bust the current search cache key in Redis. # Add "1" as a parameter to rebuild stat hashes as well. (Use with caution!) -sub invalidate_cache($rebuild_indexes = 0) { +sub invalidate_cache ( $rebuild_indexes = 0 ) { my $redis = LANraragi::Model::Config->get_redis_search; $redis->del("LRR_SEARCHCACHE"); @@ -519,13 +538,13 @@ sub invalidate_cache($rebuild_indexes = 0) { } } -sub save_computed_tagrules($tagrules) { +sub save_computed_tagrules ($tagrules) { my $redis = LANraragi::Model::Config->get_redis_config; $redis->del("LRR_TAGRULES"); if (@$tagrules) { - my @flat = reverse flat(@$tagrules); + my @flat = reverse flat(@$tagrules); my @encoded_flat = map { redis_encode($_) } @flat; $redis->lpush( "LRR_TAGRULES", @encoded_flat ); } @@ -541,7 +560,7 @@ sub get_computed_tagrules { if ( $redis->exists("LRR_TAGRULES") ) { my @flattened_rules = $redis->lrange( "LRR_TAGRULES", 0, -1 ); - my @decoded_rules = map { redis_decode($_) } @flattened_rules; + my @decoded_rules = map { redis_decode($_) } @flattened_rules; @tagrules = unflat_tagrules( \@decoded_rules ); } else { @tagrules = tags_rules_to_array( restore_CRLF( LANraragi::Model::Config->get_tagrules ) ); @@ -552,4 +571,45 @@ sub get_computed_tagrules { return @tagrules; } +sub get_tankoubons_by_file ($arcid) { + my $redis = LANraragi::Model::Config->get_redis; + my @tankoubons; + + my $logger = get_logger( "Tankoubon", "lanraragi" ); + my $err = ""; + + unless ( $redis->exists($arcid) ) { + $err = "$arcid does not exist in the database."; + $logger->error($err); + $redis->quit; + return (); + } + + my @tanks = $redis->keys('TANK_??????????'); + + foreach my $key ( sort @tanks ) { + + if ( $redis->zscore( $key, $arcid ) ) { + push( @tankoubons, $key ); + } + } + + $redis->quit; + return @tankoubons; +} + +sub add_arcsize ( $redis, $id ) { + my $file = $redis->hget( $id, "file" ); + my $arcsize = -s $file; + set_arcsize( $redis, $id, $arcsize ); +} + +sub set_arcsize ( $redis, $id, $arcsize ) { + $redis->hset( $id, "arcsize", $arcsize ); +} + +sub get_arcsize ( $redis, $id ) { + return $redis->hget( $id, "arcsize" ); +} + 1; diff --git a/lib/LANraragi/Utils/Generic.pm b/lib/LANraragi/Utils/Generic.pm index 147368933..ef858564c 100644 --- a/lib/LANraragi/Utils/Generic.pm +++ b/lib/LANraragi/Utils/Generic.pm @@ -15,43 +15,13 @@ use Proc::Simple; use Sys::CpuAffinity; use LANraragi::Utils::TempFolder qw(get_temp); +use LANraragi::Utils::String qw(trim); use LANraragi::Utils::Logging qw(get_logger); # Generic Utility Functions. use Exporter 'import'; -our @EXPORT_OK = - qw(remove_spaces remove_newlines trim_url is_image is_archive render_api_response get_tag_with_namespace shasum start_shinobu - split_workload_by_cpu start_minion get_css_list generate_themes_header flat get_bytelength); - -# Remove spaces before and after a word -sub remove_spaces { - if ( $_[0] ) { - $_[0] =~ s/^\s+|\s+$//g; - } -} - -# Remove all newlines in a string -sub remove_newlines { - if ( $_[0] ) { - $_[0] =~ s/\R//g; - } -} - -# Fixes up a URL string for use in the DL system. -sub trim_url { - - remove_spaces( $_[0] ); - - # Remove scheme, www. and query parameters if present. Other subdomains are not removed - if ( $_[0] =~ /https?:\/\/(www\.)?([^\?]*)\??.*/gm ) { - $_[0] = $2; - } - - my $char = chop $_[0]; - if ( $char ne "/" ) { - $_[0] .= $char; - } -} +our @EXPORT_OK = qw(is_image is_archive render_api_response get_tag_with_namespace shasum start_shinobu + split_workload_by_cpu start_minion get_css_list generate_themes_header flat get_bytelength array_difference); # Checks if the provided file is an image. # Uses non-capturing groups (?:) to avoid modifying the incoming argument. @@ -72,10 +42,10 @@ sub render_api_response { $mojo->render( json => { - operation => $operation, - error => $failed ? $errormessage : "", - success => $failed ? 0 : 1, - successMessage => $failed ? "" : $successMessage, + operation => $operation, + error => $failed ? $errormessage : "", + success => $failed ? 0 : 1, + successMessage => $failed ? "" : $successMessage, }, status => $failed ? 400 : 200 ); @@ -88,8 +58,8 @@ sub get_tag_with_namespace { foreach my $tag (@values) { my ( $namecheck, $value ) = split( ':', $tag ); - remove_spaces($namecheck); - remove_spaces($value); + $namecheck = trim($namecheck); + $value = trim($value); if ( $namecheck eq $namespace ) { return $value; @@ -119,7 +89,7 @@ sub split_workload_by_cpu { # Start a Minion worker if there aren't any available. sub start_minion { - my $mojo = shift; + my $mojo = shift; my $logger = get_logger( "Minion", "minion" ); my $numcpus = Sys::CpuAffinity::getNumCpus(); @@ -150,8 +120,8 @@ sub start_minion { } sub _spawn { - my ( $job, $pid ) = @_; - my ( $id, $task ) = ( $job->id, $job->task ); + my ( $job, $pid ) = @_; + my ( $id, $task ) = ( $job->id, $job->task ); my $logger = get_logger( "Minion Worker", "minion" ); $job->app->log->debug(qq{Process $pid is performing job "$id" with task "$task"}); } @@ -266,4 +236,22 @@ sub get_bytelength { return length shift; } +# Gets right difference between 2 arrays. +sub array_difference { + my ( $array1, $array2 ) = @_; + + my %seen; + my @difference; + + # Add all elements from array1 to the hash + $seen{$_} = 1 for @$array1; + + # Check elements in array2 and add the ones not seen in array1 to the difference array + foreach my $element (@$array2) { + push @difference, $element unless $seen{$element}; + } + + return @difference; +} + 1; diff --git a/lib/LANraragi/Utils/Logging.pm b/lib/LANraragi/Utils/Logging.pm index ee29c507b..21f64ae56 100644 --- a/lib/LANraragi/Utils/Logging.pm +++ b/lib/LANraragi/Utils/Logging.pm @@ -56,7 +56,7 @@ sub get_logger { $log->level('debug'); } - # Step down into trace if we're launched from npm run dev-server + # Step down into trace if we're launched from npm run dev-server-verbose if ( $ENV{LRR_DEVSERVER} ) { $log->level('trace'); } diff --git a/lib/LANraragi/Utils/Minion.pm b/lib/LANraragi/Utils/Minion.pm index dc3b1bec0..b9056cd56 100644 --- a/lib/LANraragi/Utils/Minion.pm +++ b/lib/LANraragi/Utils/Minion.pm @@ -11,7 +11,8 @@ use LANraragi::Utils::Logging qw(get_logger); use LANraragi::Utils::Database qw(redis_decode); use LANraragi::Utils::Archive qw(extract_thumbnail extract_archive); use LANraragi::Utils::Plugins qw(get_downloader_for_url get_plugin get_plugin_parameters use_plugin); -use LANraragi::Utils::Generic qw(trim_url split_workload_by_cpu); +use LANraragi::Utils::Generic qw(split_workload_by_cpu); +use LANraragi::Utils::String qw(trim_url); use LANraragi::Utils::TempFolder qw(get_temp); use LANraragi::Model::Upload; @@ -27,10 +28,21 @@ sub add_tasks { my ( $job, @args ) = @_; my ( $thumbdir, $id, $page ) = @args; + my $logger = get_logger( "Minion", "minion" ); + # Non-cover thumbnails are rendered in low quality by default. - my $use_hq = $page eq 0 || LANraragi::Model::Config->get_hqthumbpages; - my $thumbname = extract_thumbnail( $thumbdir, $id, $page, $use_hq ); - $job->finish($thumbname); + my $use_hq = $page eq 0 || LANraragi::Model::Config->get_hqthumbpages; + my $thumbname = ""; + + eval { $thumbname = extract_thumbnail( $thumbdir, $id, $page, $use_hq ); }; + if ($@) { + my $msg = "Error building thumbnail: $@"; + $logger->error($msg); + $job->fail( { errors => [$msg] } ); + } else { + $job->finish($thumbname); + } + } ); @@ -61,8 +73,10 @@ sub add_tasks { sub { foreach my $id (@$_) { + my $use_jxl = LANraragi::Model::Config->get_jxlthumbpages; + my $format = $use_jxl ? 'jxl' : 'jpg'; my $subfolder = substr( $id, 0, 2 ); - my $thumbname = "$thumbdir/$subfolder/$id.jpg"; + my $thumbname = "$thumbdir/$subfolder/$id.$format"; unless ( $force == 0 && -e $thumbname ) { eval { @@ -137,7 +151,7 @@ sub add_tasks { my ( $job, @args ) = @_; my ( $url, $catid ) = @args; - my $ua = Mojo::UserAgent->new; + my $ua = Mojo::UserAgent->new; my $logger = get_logger( "Minion", "minion" ); $logger->info("Downloading url $url..."); diff --git a/lib/LANraragi/Utils/Routing.pm b/lib/LANraragi/Utils/Routing.pm index 5f8bc533a..44356dba1 100644 --- a/lib/LANraragi/Utils/Routing.pm +++ b/lib/LANraragi/Utils/Routing.pm @@ -86,6 +86,8 @@ sub apply_routes { $logged_in->get('/logs/mojo')->to('logging#print_mojo'); $logged_in->get('/logs/redis')->to('logging#print_redis'); + $logged_in->get('/tankoubons')->to('tankoubon#index'); + # OPDS API $public_api->get('/api/opds')->to('api-other#serve_opds_catalog'); $public_api->get('/api/opds/:id')->to('api-other#serve_opds_item'); @@ -112,6 +114,7 @@ sub apply_routes { $public_api->delete('/api/archives/:id/isnew')->to('api-archive#clear_new'); $public_api->get('/api/archives/:id')->to('api-archive#serve_metadata'); $public_api->get('/api/archives/:id/categories')->to('api-archive#get_categories'); + $public_api->get('/api/archives/:id/tankoubons')->to('api-tankoubon#get_tankoubons_file'); $public_api->get('/api/archives/:id/metadata')->to('api-archive#serve_metadata'); $logged_in_api->put('/api/archives/:id/thumbnail')->to('api-archive#update_thumbnail'); $logged_in_api->put('/api/archives/:id/metadata')->to('api-archive#update_metadata'); @@ -150,6 +153,15 @@ sub apply_routes { $logged_in_api->put('/api/categories/:id/:archive')->to('api-category#add_to_category'); $logged_in_api->delete('/api/categories/:id/:archive')->to('api-category#remove_from_category'); + # Tankoubon API + $public_api->get('/api/tankoubons')->to('api-tankoubon#get_tankoubon_list'); + $public_api->get('/api/tankoubons/:id')->to('api-tankoubon#get_tankoubon'); + $logged_in_api->put('/api/tankoubons')->to('api-tankoubon#create_tankoubon'); + $logged_in_api->delete('/api/tankoubons/:id')->to('api-tankoubon#delete_tankoubon'); + $logged_in_api->put('/api/tankoubons/:id/archive')->to('api-tankoubon#update_archive_list'); + $logged_in_api->put('/api/tankoubons/:id/:archive')->to('api-tankoubon#add_to_tankoubon'); + $logged_in_api->delete('/api/tankoubons/:id/:archive')->to('api-tankoubon#remove_from_tankoubon'); + } 1; diff --git a/lib/LANraragi/Utils/String.pm b/lib/LANraragi/Utils/String.pm new file mode 100644 index 000000000..d9aa4d761 --- /dev/null +++ b/lib/LANraragi/Utils/String.pm @@ -0,0 +1,80 @@ +package LANraragi::Utils::String; + +use strict; +use warnings; +use utf8; +use feature "switch"; +no warnings 'experimental'; +use feature qw(signatures); + +use String::Similarity; + +use Exporter 'import'; +our @EXPORT_OK = qw(clean_title trim trim_CRLF trim_url most_similar); + +# Remove "junk" from titles, turning something like "(c12) [poop (butt)] hardcore handholding [monogolian] [recensored]" into "hardcore handholding" +sub clean_title ($title) { + $title = trim($title); + + # Remove leading "(c12)" + $title =~ s/^\([^)]*\)?\s?//g; + + # Remove leading "[poop (butt)]" + $title =~ s/^\[[^]]*\]?\s?//g; + + # Remove trailing [mongolian] [recensored]" + $title =~ s/\s?\[[^]]*\]$//g; + $title =~ s/\s?\[[^]]*\]$//g; + return $title; +} + +# Remove spaces before and after a word +sub trim ($s) { + $s =~ s/^\s+|\s+$//g; + return $s; +} + +# Remove all newlines in a string +sub trim_CRLF ($s) { + $s =~ s/\R//g; + return $s; +} + +# Fixes up a URL string for use in the DL system. +sub trim_url ($url) { + + $url = trim($url); + + # Remove scheme, www. and query parameters if present. Other subdomains are not removed + if ( $url =~ /https?:\/\/(www\.)?([^\?]*)\??.*/gm ) { + $url = $2; + } + + my $char = chop $url; + if ( $char ne "/" ) { + $url .= $char; + } + return $url; +} + +# Finds the index of the string in @values that is most similar to $tested_string. Returns undef if @values is empty. +# If multiple rows score "first place", the first one is returned +sub most_similar ( $tested_string, @values ) { + if ( !@values ) { + return; + } + + my $best_similarity = 0.0; + my $best_index = undef; + + while ( my ( $index, $elem ) = each @values ) { + my $similarity = similarity( $tested_string, $elem ); + if ( !defined($best_index) || $similarity > $best_similarity ) { + $best_similarity = $similarity; + $best_index = $index; + } + } + return $best_index; +} + +1; diff --git a/lib/LANraragi/Utils/Tags.pm b/lib/LANraragi/Utils/Tags.pm index 8765963cb..9b2a131e4 100644 --- a/lib/LANraragi/Utils/Tags.pm +++ b/lib/LANraragi/Utils/Tags.pm @@ -6,12 +6,11 @@ use utf8; use feature "switch"; no warnings 'experimental'; -use LANraragi::Utils::Generic qw(remove_spaces remove_newlines); +use LANraragi::Utils::String qw(trim trim_CRLF); # Functions related to the Tag system. use Exporter 'import'; -our @EXPORT_OK = - qw( unflat_tagrules replace_CRLF restore_CRLF tags_rules_to_array rewrite_tags split_tags_to_array ); +our @EXPORT_OK = qw( unflat_tagrules replace_CRLF restore_CRLF tags_rules_to_array rewrite_tags split_tags_to_array join_tags_to_string ); sub is_null_or_empty { return !length(shift); @@ -19,63 +18,67 @@ sub is_null_or_empty { sub replace_CRLF { my ($val) = @_; - $val =~ s/\x{d}\x{a}/;/g if ( $val ); + $val =~ s/\x{d}\x{a}/;/g if ($val); return $val; } sub restore_CRLF { my ($val) = @_; - $val =~ s/;/\x{d}\x{a}/g if ( $val ); + $val =~ s/;/\x{d}\x{a}/g if ($val); return $val; } sub unflat_tagrules { - my ( $flattened_rules ) = @_; + my ($flattened_rules) = @_; my @tagrules = (); - while (@{$flattened_rules || []}) { - push(@tagrules, [ splice(@$flattened_rules, 0, 3) ]); + while ( @{ $flattened_rules || [] } ) { + push( @tagrules, [ splice( @$flattened_rules, 0, 3 ) ] ); } return @tagrules; } sub split_tags_to_array { - my ( $tags_string ) = @_; - my @tags = split( ',', $tags_string ); + my ($tags_string) = @_; + my @tags = split( ',', $tags_string ); foreach my $tags (@tags) { - remove_spaces($tags); - remove_newlines($tags); + $tags = trim($tags); + $tags = trim_CRLF($tags); } return @tags; } +sub join_tags_to_string { + return join( ',', @_ ); +} + sub tags_rules_to_array { - my ( $text_rules ) = @_; + my ($text_rules) = @_; my @rules; my @lines = split( '\n', $text_rules ); - foreach my $line ( @lines ) { + foreach my $line (@lines) { my ( $match, $value ) = split( '->', $line ); - remove_spaces($match); - remove_spaces($value); - if (!is_null_or_empty($match)) { + $match = trim($match); + $value = trim($value); + if ( !is_null_or_empty($match) ) { my $rule_type; if ( !$value && $match =~ m/^-.*:\*$/ ) { $rule_type = 'remove_ns'; - $match = substr ($match, 1, length($match)-3); + $match = substr( $match, 1, length($match) - 3 ); } elsif ( !$value && $match =~ m/^-/ ) { $rule_type = 'remove'; - $match = substr ($match, 1); + $match = substr( $match, 1 ); } elsif ( !$value && $match =~ m/^~/ ) { $rule_type = 'strip_ns'; - $match = substr ($match, 1); + $match = substr( $match, 1 ); } elsif ( $match =~ m/:\*$/ && $value =~ m/:\*$/ ) { $rule_type = 'replace_ns'; - $match = substr ($match, 0, length($match)-2); - $value = substr ($value, 0, length($value)-2); + $match = substr( $match, 0, length($match) - 2 ); + $value = substr( $value, 0, length($value) - 2 ); } elsif ( !$value ) { - $rule_type = 'remove'; # blacklist mode + $rule_type = 'remove'; # blacklist mode } else { - $rule_type = 'replace' + $rule_type = 'replace'; } push( @rules, [ $rule_type, lc $match, $value || '' ] ) if ($rule_type); @@ -89,9 +92,9 @@ sub rewrite_tags { return @$tags if ( !@$rules ); my @parsed_tags; - foreach my $tag ( @$tags ) { - my $new_tag = apply_rules($tag, $rules); - push(@parsed_tags, $new_tag) if ($new_tag); + foreach my $tag (@$tags) { + my $new_tag = apply_rules( $tag, $rules ); + push( @parsed_tags, $new_tag ) if ($new_tag); } return @parsed_tags; } @@ -99,10 +102,10 @@ sub rewrite_tags { sub apply_rules { my ( $tag, $rules ) = @_; - foreach my $rule ( @$rules ) { + foreach my $rule (@$rules) { my $match = $rule->[1]; my $value = $rule->[2]; - given($rule->[0]) { + given ( $rule->[0] ) { when ('remove') { return if ( lc $tag eq $match ); } when ('remove_ns') { return if ( $tag =~ m/^$match:/i ); } when ('replace_ns') { $tag =~ s/^\Q$match:/$value\:/i; } diff --git a/lib/LANraragi/Utils/TempFolder.pm b/lib/LANraragi/Utils/TempFolder.pm index 792b1c1bf..2dcf83953 100644 --- a/lib/LANraragi/Utils/TempFolder.pm +++ b/lib/LANraragi/Utils/TempFolder.pm @@ -111,6 +111,11 @@ sub clean_temp_partial { } closedir $dir_fh; + if ( scalar @folder_list <= 1 ) { + $logger->info("Only one folder left in /temp, aborting clean."); + return; + } + @folder_list = sort { my $a_stat = stat($a); my $b_stat = stat($b); diff --git a/lib/Shinobu.pm b/lib/Shinobu.pm index 52aa55237..7d68e4c06 100644 --- a/lib/Shinobu.pm +++ b/lib/Shinobu.pm @@ -260,6 +260,11 @@ sub add_to_filemap ( $redis_cfg, $file ) { invalidate_cache(); } + unless ( LANraragi::Utils::Database::get_arcsize( $redis_arc, $id ) ) { + $logger->debug("arcsize is not set for $id, storing now!"); + LANraragi::Utils::Database::add_arcsize( $redis_arc, $id ); + } + # Set pagecount in case it's not already there unless ( $redis_arc->hget( $id, "pagecount" ) ) { $logger->debug("Pagecount not calculated for $id, doing it now!"); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..d3ef4ca36 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2372 @@ +{ + "name": "lanraragi", + "version": "0.9.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lanraragi", + "version": "0.9.0", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.2.1", + "@jcubic/tagger": "^0.4.2", + "allcollapsible": "^1.1.0", + "awesomplete": "^1.1.5", + "blueimp-file-upload": "^10.32.0", + "clsx": "^1.1.1", + "datatables.net": "^1.11.5", + "fscreen": "^1.2.0", + "inter-ui": "^3.19.3", + "jqcloud2": "^2.0.3", + "jquery": "^3.6.0", + "jquery-contextmenu": "^2.9.2", + "marked": "^4.0.14", + "open-sans-fontface": "^1.4.0", + "preact": "^10.7.1", + "react-toastify": "^9.0.0-rc-2", + "roboto-fontface": "^0.8.0", + "sweetalert2": "^11.6.14", + "swiper": "^8.4.5", + "tippy.js": "^6.3.7" + }, + "devDependencies": { + "eslint": "^7.32.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-plugin-import": "^2.26.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^13.9.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.2.1.tgz", + "integrity": "sha512-viouXhegu/TjkvYQoiRZK3aax69dGXxgEjpvZW81wIJdxm5Fnvp3VVIP4VHKqX4SvFw6qpmkILkD4RJWAdrt7A==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jcubic/tagger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@jcubic/tagger/-/tagger-0.4.2.tgz", + "integrity": "sha512-A0lGN4lJIUcGGl7+SBNM7rh46G/j03DENon+yZ80Dc7IMZ9dvIEFRReLbLTv53O8qDz6/PVM2MbP0+Cs8E/T4Q==" + }, + "node_modules/@popperjs/core": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.5.tgz", + "integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/allcollapsible": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/allcollapsible/-/allcollapsible-1.1.0.tgz", + "integrity": "sha1-tNnWvJ5okKpOs+SXhJN1isASEuM=", + "peerDependencies": { + "jquery": ">=1.4" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-includes": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", + "integrity": "sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/awesomplete": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/awesomplete/-/awesomplete-1.1.5.tgz", + "integrity": "sha512-UFw1mPW8NaSECDSTC36HbAOTpF9JK2wBUJcNn4MSvlNtK7SZ9N72gB+ajHtA6D1abYXRcszZnBA4nHBwvFwzHw==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/blueimp-canvas-to-blob": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", + "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==", + "optional": true + }, + "node_modules/blueimp-file-upload": { + "version": "10.32.0", + "resolved": "https://registry.npmjs.org/blueimp-file-upload/-/blueimp-file-upload-10.32.0.tgz", + "integrity": "sha512-3WMJw5Cbfz94Adl1OeyH+rRpGwHiNHzja+CR6aRWPoAtwrUwvP5gXKo0XdX+sdPE+iCU63Xmba88hoHQmzY8RQ==", + "optionalDependencies": { + "blueimp-canvas-to-blob": "3", + "blueimp-load-image": "5", + "blueimp-tmpl": "3" + }, + "peerDependencies": { + "jquery": ">=1.7" + } + }, + "node_modules/blueimp-load-image": { + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/blueimp-load-image/-/blueimp-load-image-5.16.0.tgz", + "integrity": "sha512-3DUSVdOtlfNRk7moRZuTwDmA3NnG8KIJuLcq3c0J7/BIr6X3Vb/EpX3kUH1joxUhmoVF4uCpDfz7wHkz8pQajA==", + "optional": true + }, + "node_modules/blueimp-tmpl": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/blueimp-tmpl/-/blueimp-tmpl-3.20.0.tgz", + "integrity": "sha512-g6ln9L+VX8ZA4WA8mgKMethYH+5teroJ2uOkCvcthy9Y9d9LrQ42OAMn+r3ECKu9CB+xe9GOChlIUJBSxwkI6g==", + "optional": true, + "bin": { + "tmpl.js": "js/compile.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", + "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/confusing-browser-globals": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", + "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/datatables.net": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.5.tgz", + "integrity": "sha512-nlFst2xfwSWaQgaOg5sXVG3cxYC0tH8E8d65289w9ROgF2TmLULOOpcdMpyxxUim/qEwVSEem42RjkTWEpr3eA==", + "dependencies": { + "jquery": ">=1.7" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom7": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz", + "integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==", + "dependencies": { + "ssr-window": "^4.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/es-abstract": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.5.tgz", + "integrity": "sha512-Aa2G2+Rd3b6kxEUKTF4TaW67czBLyAv3z7VOhYRU50YBx+bbsYZ9xQP4lMNazePuFlybXI0V4MruPos7qUo5fA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-airbnb-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz", + "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==", + "dev": true, + "dependencies": { + "confusing-browser-globals": "^1.0.10", + "object.assign": "^4.1.2", + "object.entries": "^1.1.5", + "semver": "^6.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + }, + "peerDependencies": { + "eslint": "^7.32.0 || ^8.2.0", + "eslint-plugin-import": "^2.25.2" + } + }, + "node_modules/eslint-config-airbnb-base/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz", + "integrity": "sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "dev": true, + "dependencies": { + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/fscreen": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz", + "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/inter-ui": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.19.3.tgz", + "integrity": "sha512-5FG9fjuYOXocIfjzcCBhICL5cpvwEetseL3FU6tP3d6Bn7g8wODhB+I9RNGRTizCT7CUG4GOK54OPxqq3msQgg==" + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/jqcloud2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jqcloud2/-/jqcloud2-2.0.3.tgz", + "integrity": "sha1-mxaR+FUT0pAqXsY+rUkxi4WgxHg=", + "dependencies": { + "jquery": ">= 1.9.0" + } + }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "node_modules/jquery-contextmenu": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/jquery-contextmenu/-/jquery-contextmenu-2.9.2.tgz", + "integrity": "sha512-6S6sH/08owDStC/7zNwcN366yR0ydX6PmMB0RnjLRQOp7Nc/rqwEHglshfHrrw2kdTev97GXwRXrayDUmToIOw==", + "dependencies": { + "jquery": "^3.5.0" + }, + "peerDependencies": { + "jquery": ">=1.8.2" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/marked": { + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.15.tgz", + "integrity": "sha512-esX5lPdTfG4p8LDkv+obbRCyOKzB+820ZZyMOXJZygZBHrH9b3xXR64X4kT3sPe9Nx8qQXbmcz6kFSMt4Nfk6Q==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "node_modules/object-inspect": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", + "integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz", + "integrity": "sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open-sans-fontface": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/open-sans-fontface/-/open-sans-fontface-1.4.0.tgz", + "integrity": "sha1-A8xtG/XmqLW0eRCIhWL3IsXdNCg=" + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/preact": { + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.7.1.tgz", + "integrity": "sha512-MufnRFz39aIhs9AMFisonjzTud1PK1bY+jcJLo6m2T9Uh8AqjD77w11eAAawmjUogoGOnipECq7e/1RClIKsxg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-toastify": { + "version": "9.0.0-rc-2", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.0.0-rc-2.tgz", + "integrity": "sha512-ErWO+aINQ05zkZIqUxwGQjjhdW6649ANYgFiHY4ecZ8n8eYEhZTSKP0rp1lKEkDT9i4ZhMGrwnOWPk4wfkJsOQ==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roboto-fontface": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/roboto-fontface/-/roboto-fontface-0.8.0.tgz", + "integrity": "sha1-AxqDyPeZMoAaV9g790PzclAWNJk=" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/ssr-window": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz", + "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sweetalert2": { + "version": "11.7.32", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.7.32.tgz", + "integrity": "sha512-44tNNe2oLe7T94mT6dus4hc9G7qg6jZU/K5qZzpNS6e5HGPrSF6Kie6oZ7B5puIJydB34V2h/8f5EhIFivYo4A==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/limonte" + } + }, + "node_modules/swiper": { + "version": "8.4.5", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.5.tgz", + "integrity": "sha512-zveyEFBBv4q1sVkbJHnuH4xCtarKieavJ4SxP0QEHvdpPLJRuD7j/Xg38IVVLbp7Db6qrPsLUePvxohYx39Agw==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "hasInstallScript": true, + "dependencies": { + "dom7": "^4.0.4", + "ssr-window": "^4.0.2" + }, + "engines": { + "node": ">= 4.7.0" + } + }, + "node_modules/table": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.0.tgz", + "integrity": "sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 0335fce92..22e30e2ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lanraragi", - "version": "0.8.90", - "version_name": "The Hearts Filthy Lesson", + "version": "0.9.0", + "version_name": "Hallo Spaceboy", "description": "I'm under Japanese influence and my honor's at stake!", "scripts": { "test": "prove -r -l -v tests/", @@ -14,7 +14,7 @@ "docker-build": "docker build -t difegue/lanraragi -f ./tools/build/docker/Dockerfile .", "critic": "perlcritic ./lib/* ./script/* ./tools/install.pl", "backup-db": "perl ./script/backup", - "get-version": "perl -Mojo -E \"my \\$conf = j(f(qw(package.json))->slurp); say %\\$conf{version} .q/ - '/. %\\$conf{version_name} .q/'/ \"" + "get-version": "perl ./script/get_version" }, "repository": { "type": "git", @@ -44,7 +44,7 @@ "preact": "^10.7.1", "react-toastify": "^9.0.0-rc-2", "roboto-fontface": "^0.8.0", - "sweetalert2": "^11.4.10", + "sweetalert2": "^11.6.14", "swiper": "^8.4.5", "tippy.js": "^6.3.7" }, diff --git a/public/js/common.js b/public/js/common.js index e30b8261e..415983535 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -308,6 +308,7 @@ LRR.getImgSize = function (target) { $.ajax({ async: false, url: target, + cache: true, type: "HEAD", success: (data, textStatus, request) => { imgSize = parseInt(request.getResponseHeader("Content-Length") / 1024, 10); diff --git a/public/js/index_datatables.js b/public/js/index_datatables.js index 2ffb6bb95..7e60f909b 100644 --- a/public/js/index_datatables.js +++ b/public/js/index_datatables.js @@ -50,7 +50,10 @@ IndexTable.initializeAll = function () { IndexTable.dataTable = $(".datatables").DataTable({ serverSide: true, processing: true, - ajax: "search", + ajax: { + url: "search", + cache: true, + }, deferRender: true, lengthChange: false, pageLength: Index.pageSize, diff --git a/public/js/reader.js b/public/js/reader.js index 08d2e981a..48646cc1d 100644 --- a/public/js/reader.js +++ b/public/js/reader.js @@ -43,7 +43,39 @@ Reader.initializeAll = function () { window.location.href = `./reader?id=${Reader.id}&force_reload`; }); $(document).on("click.edit-metadata", "#edit-archive", () => LRR.openInNewTab(`./edit?id=${Reader.id}`)); - $(document).on("click.add-category", "#add-category", () => Server.addArchiveToCategory(Reader.id, $("#category").val())); + $(document).on("click.delete-archive", "#delete-archive", () => { + LRR.closeOverlay(); + LRR.showPopUp({ + text: "Are you sure you want to delete this archive?", + icon: "warning", + showCancelButton: true, + focusConfirm: false, + confirmButtonText: "Yes, delete it!", + reverseButtons: true, + confirmButtonColor: "#d33", + }).then((result) => { + if (result.isConfirmed) { + Server.deleteArchive(Reader.id, () => { document.location.href = "./"; }); + } + }); + }); + $(document).on("click.add-category", "#add-category", () => { + if ($("#category").val() === "" || $(`#archive-categories a[data-id="${$("#category").val()}"]`).length !== 0) { return; } + Server.addArchiveToCategory(Reader.id, $("#category").val()); + + const html = `
    + + ${$("#category option:selected").text()} + × + `; + + $("#archive-categories").append(html); + }); + $(document).on("click.remove-category", ".remove-category", (e) => { + Server.removeArchiveFromCategory(Reader.id, $(e.target).attr("data-id")); + $(e.target).parent().remove(); + }); $(document).on("click.set-thumbnail", "#set-thumbnail", () => Server.callAPI(`/api/archives/${Reader.id}/thumbnail?page=${Reader.currentPage + 1}`, "PUT", `Successfully set page ${Reader.currentPage + 1} as the thumbnail!`, "Error updating thumbnail!", null)); @@ -84,6 +116,7 @@ Reader.initializeAll = function () { } $("#archive-title").html(title); + $("#archive-title-overlay").html(title); if (data.pagecount) { $(".max-page").html(data.pagecount); } document.title = title; @@ -239,9 +272,10 @@ Reader.initInfiniteScrollView = function () { Reader.pages.slice(1).forEach((source) => { const img = new Image(); - img.src = source; img.id = `page-${Reader.pages.indexOf(source)}`; - img.loading = "lazy"; + img.height = 800; + img.width = 600; + img.src = source; $(img).addClass("reader-image"); $("#display").append(img); observer.observe(img); @@ -351,7 +385,7 @@ Reader.checkFiletypeSupport = function (extension) { localStorage.epubWarningShown = true; LRR.toast({ heading: "EPUB support in LANraragi is minimal", - text: "EPUB books will only show images in the Web Reader. If you want text support, consider pairing LANraragi with an OPDS reader.", + text: "EPUB books will only show images in the Web Reader, and potentially out of order. If you want text support, consider pairing LANraragi with an OPDS reader.", icon: "warning", hideAfter: 20000, closeOnClick: false, @@ -437,7 +471,7 @@ Reader.goToPage = function (page) { Reader.showingSinglePage = false; if (Reader.infiniteScroll) { - $("#display img").get(page).scrollIntoView({ behavior: "smooth" }); + $("#display img").get(Reader.currentPage).scrollIntoView({ behavior: "smooth" }); } else { $("#img_doublepage").attr("src", ""); $("#display").removeClass("double-mode"); diff --git a/public/js/server.js b/public/js/server.js index 609702c80..fa61dd11a 100644 --- a/public/js/server.js +++ b/public/js/server.js @@ -42,6 +42,33 @@ Server.callAPI = function (endpoint, method, successMessage, errorMessage, succe .catch((error) => LRR.showErrorToast(errorMessage, error)); }; +Server.callAPIBody = function (endpoint, method, body, successMessage, errorMessage, successCallback) { + return fetch(endpoint, { method, body }) + .then((response) => (response.ok ? response.json() : { success: 0, error: "Response was not OK" })) + .then((data) => { + if (Object.prototype.hasOwnProperty.call(data, "success") && !data.success) { + throw new Error(data.error); + } else { + let message = successMessage; + if ("successMessage" in data && data.successMessage !== null) { + message = data.successMessage; + } + if (message !== null) { + LRR.toast({ + heading: message, + icon: "success", + hideAfter: 7000, + }); + } + + if (successCallback !== null) return successCallback(data); + + return null; + } + }) + .catch((error) => LRR.showErrorToast(errorMessage, error)); +}; + /** * Check the status of a Minion job until it's completed. * @param {*} jobId Job ID to check diff --git a/public/themes/ex.css b/public/themes/ex.css index 28f8bfdb4..033cd4bc3 100644 --- a/public/themes/ex.css +++ b/public/themes/ex.css @@ -544,22 +544,26 @@ div.id1 { display: inline-block; margin: 3px 2px 2px 3px; padding-top: 3px; - height: 335px; + vertical-align: top; + min-height: 335px; } div.id2 { height: 30px; margin: auto; - overflow: hidden; text-align: center; - vertical-align: middle; - line-height: 15px; width: 97%; } div.id2 a { font-weight: bold; text-decoration: none; + overflow: hidden; + + /* Multi-line title support */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } div.id3 { diff --git a/public/themes/g.css b/public/themes/g.css index 5afa3f952..653a82498 100644 --- a/public/themes/g.css +++ b/public/themes/g.css @@ -545,22 +545,26 @@ div.id1 { display: inline-block; margin: 3px 2px 2px 3px; padding-top: 3px; - height: 335px; + vertical-align: top; + min-height: 335px; } div.id2 { height: 30px; margin: auto; - overflow: hidden; text-align: center; - vertical-align: middle; - line-height: 15px; width: 97%; } div.id2 a { font-weight: bold; text-decoration: none; + overflow: hidden; + + /* Multi-line title support */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } div.id3 { diff --git a/public/themes/modern.css b/public/themes/modern.css index 930fb5b55..e4ee72cf5 100644 --- a/public/themes/modern.css +++ b/public/themes/modern.css @@ -538,23 +538,26 @@ div.id1 { display: inline-block; margin: 3px 2px 2px 3px; padding-top: 3px; - height: 335px; + vertical-align: top; + min-height: 335px; } div.id2 { - height: 30px; + min-height: 32px; margin: auto; - overflow: hidden; text-align: center; - text-overflow: ellipsis; - vertical-align: middle; - white-space: nowrap; width: 97%; } div.id2 a { font-weight: bold; text-decoration: none; + overflow: hidden; + + /* Multi-line title support */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } div.id3 { diff --git a/public/themes/modern_clear.css b/public/themes/modern_clear.css index eb34388e5..b13c58ccc 100644 --- a/public/themes/modern_clear.css +++ b/public/themes/modern_clear.css @@ -201,6 +201,18 @@ table.itg th { color: #34495E !important; } +.caption-tags>*>*>span { + color: #34495E !important; +} + +#archive-categories .gt a { + color: #E1E7E9; +} + +#archive-categories .gt a:hover { + color: #ed2553; +} + .tippy-arrow { color: #E1E7E9; } @@ -651,20 +663,22 @@ div.id1 { } div.id2 { - height: 20px; - overflow: hidden; + min-height: 32px; text-align: center; - text-overflow: ellipsis; - vertical-align: middle; - white-space: nowrap; width: 97%; } div.id2 a { font-weight: bold; text-decoration: none; + overflow: hidden; color: #FCFCFC; font-size: 12px; + + /* Multi-line title support */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } div.id2 a:hover { diff --git a/public/themes/modern_red.css b/public/themes/modern_red.css index ce6256622..74c908711 100644 --- a/public/themes/modern_red.css +++ b/public/themes/modern_red.css @@ -603,26 +603,29 @@ div.id1 { display: inline-block; margin: 3px 5px 5px 3px; padding-top: 3px; - height: 335px; + vertical-align: top; + min-height: 335px; } div.id2 { border-bottom: 2px dotted #414135; - height: 20px; + min-height: 20px; margin: auto auto 10px; - overflow: hidden; text-align: center; - text-overflow: ellipsis; - vertical-align: middle; - white-space: nowrap; width: 97%; } div.id2 a { font-weight: bold; text-decoration: none; + overflow: hidden; color: #414135; font-size: 12px; + + /* Multi-line title support */ + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; } div.id2 a:hover { diff --git a/script/get_version b/script/get_version new file mode 100644 index 000000000..cbad900f8 --- /dev/null +++ b/script/get_version @@ -0,0 +1,12 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use feature qw(say); + +use Mojo::JSON qw(j); +use Mojo::File; + +my $conf = j( Mojo::File->new(qw(package.json))->slurp ); +say %$conf{version} . " - '" . %$conf{version_name} . "'" + diff --git a/templates/opds.html.tt2 b/templates/opds.html.tt2 index 75c166c35..08840ac25 100644 --- a/templates/opds.html.tt2 +++ b/templates/opds.html.tt2 @@ -62,7 +62,9 @@ + href="/api/opds/[% arc.arcid %]/pse?page={pageNumber}[% api_key_and %]" pse:count="[% arc.pagecount %]" + [% IF arc.progress %] pse:lastRead="[% arc.progress %]" [% END %] + [% IF arc.lastreaddate %] pse:lastReadDate="[% arc.lastreaddate %]" [% END %]/> [% END %] diff --git a/templates/opds_entry.html.tt2 b/templates/opds_entry.html.tt2 index fafb8dbcb..b3ee9f728 100644 --- a/templates/opds_entry.html.tt2 +++ b/templates/opds_entry.html.tt2 @@ -33,7 +33,9 @@ + href="/api/opds/[% arc.arcid %]/pse?page={pageNumber}[% api_key_and %]" pse:count="[% arc.pagecount %]" + [% IF arc.progress %] pse:lastRead="[% arc.progress %]" [% END %] + [% IF arc.lastreaddate %] pse:lastReadDate="[% arc.lastreaddate %]" [% END %] /> \ No newline at end of file diff --git a/templates/plugins.html.tt2 b/templates/plugins.html.tt2 index 463053250..fbca77cb9 100644 --- a/templates/plugins.html.tt2 +++ b/templates/plugins.html.tt2 @@ -21,7 +21,7 @@ - + @@ -83,6 +83,19 @@ Metadata Plugins
    + +

    Allow Plugins to replace archive titles:

    + [% IF replacetitles %] + [% ELSE + %] + [% END %] + +
    +
    [% INCLUDE pluginlist plugins = metadata %]
    diff --git a/templates/reader.html.tt2 b/templates/reader.html.tt2 index de2be9989..76651efbf 100644 --- a/templates/reader.html.tt2 +++ b/templates/reader.html.tt2 @@ -28,8 +28,12 @@ - + @@ -82,7 +86,7 @@