diff --git a/.github/workflows/push-continous-delivery.yml b/.github/workflows/push-continous-delivery.yml index 5faf7bcd8..559409a60 100644 --- a/.github/workflows/push-continous-delivery.yml +++ b/.github/workflows/push-continous-delivery.yml @@ -65,8 +65,11 @@ jobs: - name: Build MSI Installer shell: powershell run: | - Import-Module "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"; - Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" -SkipAutomaticLocation + [array]$installPath = &"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -property installationpath + # Get first line of installPath in case we have multiple VS installs + Import-Module (Join-Path $installPath[0] "Common7\Tools\Microsoft.VisualStudio.DevShell.dll") + # Import the VS shell module + Enter-VsDevShell -VsInstallPath $installPath[0] -SkipAutomaticLocation $ErrorActionPreference = 'Continue' git submodule init git submodule update diff --git a/.github/workflows/release-delivery.yml b/.github/workflows/release-delivery.yml index 09dbdc5cc..fcefa3e20 100644 --- a/.github/workflows/release-delivery.yml +++ b/.github/workflows/release-delivery.yml @@ -32,8 +32,11 @@ jobs: - name: Build MSI Installer shell: powershell run: | - Import-Module "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"; - Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise" -SkipAutomaticLocation + [array]$installPath = &"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -property installationpath + # Get first line of installPath in case we have multiple VS installs + Import-Module (Join-Path $installPath[0] "Common7\Tools\Microsoft.VisualStudio.DevShell.dll") + # Import the VS shell module + Enter-VsDevShell -VsInstallPath $installPath[0] -SkipAutomaticLocation $ErrorActionPreference = 'Continue' git submodule init git submodule update @@ -63,7 +66,7 @@ jobs: env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} with: - args: 'Windows Installer built and available on the Release page! :logo:🪟' + args: 'Windows Installer built and available on the Release page! <:logo:821516019179978772>🪟' buildLatestDocker: name: Build Latest Docker image diff --git a/lib/LANraragi/Controller/Api/Minion.pm b/lib/LANraragi/Controller/Api/Minion.pm new file mode 100644 index 000000000..2be6845c5 --- /dev/null +++ b/lib/LANraragi/Controller/Api/Minion.pm @@ -0,0 +1,69 @@ +package LANraragi::Controller::Api::Minion; +use Mojo::Base 'Mojolicious::Controller'; + +use Mojo::JSON qw(encode_json decode_json); +use Redis; + +use LANraragi::Model::Stats; +use LANraragi::Utils::TempFolder qw(get_tempsize clean_temp_full); +use LANraragi::Utils::Generic qw(render_api_response); +use LANraragi::Utils::Plugins qw(get_plugin get_plugins get_plugin_parameters use_plugin); + +# Returns basic info for the given Minion job id. +sub minion_job_status { + my $self = shift; + my $id = $self->stash('jobid'); + my $job = $self->minion->job($id); + + if ($job) { + + my %info = %{ $job->info }; + + # Render a basic json containing only task, state and error + $self->render( + json => { + task => $info{task}, + state => $info{state}, + error => $info{error} + } + ); + + } else { + render_api_response( $self, "minion_job_status", "No job with this ID." ); + } +} + +# Returns the full info for the given Minion job id. +sub minion_job_detail { + my $self = shift; + my $id = $self->stash('jobid'); + my $job = $self->minion->job($id); + + if ($job) { + $self->render( json => $job->info ); + } else { + render_api_response( $self, "minion_job_detail", "No job with this ID." ); + } +} + +# Queues a job into Minion. +sub queue_minion_job { + + my ($self) = shift; + my $jobname = $self->stash('jobname'); + my @jobargs = decode_json( $self->req->param('args') ); + my $priority = $self->req->param('priority') || 0; + + my $jobid = $self->minion->enqueue( $jobname => @jobargs => { priority => $priority } ); + + $self->render( + json => { + operation => "queue_minion_job", + success => 1, + job => $jobid + } + ); +} + +1; + diff --git a/lib/LANraragi/Controller/Api/Other.pm b/lib/LANraragi/Controller/Api/Other.pm index 0650e44f9..35f7b76aa 100644 --- a/lib/LANraragi/Controller/Api/Other.pm +++ b/lib/LANraragi/Controller/Api/Other.pm @@ -70,20 +70,6 @@ sub list_plugins { $self->render( json => \@plugins ); } -# Returns the info for the given Minion job id. -sub minion_job_status { - my $self = shift; - my $id = $self->stash('jobid'); - - my $job = $self->minion->job($id); - - if ($job) { - $self->render( json => $job->info ); - } else { - render_api_response( $self, "minion_job_status", "No job with this ID." ); - } -} - # Queue the regen_all_thumbnails Minion job. sub regen_thumbnails { my $self = shift; @@ -102,7 +88,6 @@ sub regen_thumbnails { } sub download_url { - my ($self) = shift; my $url = $self->req->param('url'); my $catid = $self->req->param('catid'); @@ -130,8 +115,7 @@ sub download_url { # Uses a plugin, with the standard global arguments and a provided oneshot argument. sub use_plugin_sync { - - my ($self) = shift; + my ($self) = shift; my $id = $self->req->param('id') || 0; my $plugname = $self->req->param('plugin'); my $input = $self->req->param('arg'); @@ -153,7 +137,6 @@ sub use_plugin_sync { # Queues a plugin execution into Minion. sub use_plugin_async { - my ($self) = shift; my $id = $self->req->param('id') || 0; my $priority = $self->req->param('priority') || 0; @@ -171,24 +154,5 @@ sub use_plugin_async { ); } -# Queues a job into Minion. -sub queue_minion_job { - - my ($self) = shift; - my $jobname = $self->stash('jobname'); - my @jobargs = decode_json( $self->req->param('args') ); - my $priority = $self->req->param('priority') || 0; - - my $jobid = $self->minion->enqueue( $jobname => @jobargs => { priority => $priority } ); - - $self->render( - json => { - operation => "queue_minion_job", - success => 1, - job => $jobid - } - ); -} - 1; diff --git a/lib/LANraragi/Controller/Api/Shinobu.pm b/lib/LANraragi/Controller/Api/Shinobu.pm index 9fa6f77fb..0001fb7aa 100644 --- a/lib/LANraragi/Controller/Api/Shinobu.pm +++ b/lib/LANraragi/Controller/Api/Shinobu.pm @@ -19,6 +19,32 @@ sub shinobu_status { ); } +sub reset_filemap { + my $self = shift; + + # This is a shinobu endpoint even though we're deleting stuff in redis + # since we'll have to restart shinobu anyway to proc filemap re-creation. + + my $redis = $self->LRR_CONF->get_redis; + $redis->del("LRR_FILEMAP"); + + my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; + + #commit sudoku + $shinobu->kill(); + + # Create a new Process, automatically stored in TEMP_FOLDER/shinobu.pid + my $proc = start_shinobu($self); + + $self->render( + json => { + operation => "shinobu_rescan", + success => $proc->poll(), + new_pid => $proc->pid + } + ); +} + sub stop_shinobu { my $self = shift; my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; diff --git a/lib/LANraragi/Controller/Upload.pm b/lib/LANraragi/Controller/Upload.pm index c144419c0..30d70975e 100644 --- a/lib/LANraragi/Controller/Upload.pm +++ b/lib/LANraragi/Controller/Upload.pm @@ -2,7 +2,7 @@ package LANraragi::Controller::Upload; use Mojo::Base 'Mojolicious::Controller'; use Redis; -use File::Temp qw/ tempfile tempdir /; +use File::Temp qw(tempdir); use File::Copy; use File::Find; use File::Basename; diff --git a/lib/LANraragi/Model/Archive.pm b/lib/LANraragi/Model/Archive.pm index 52c0ae033..c2f6dfe7c 100644 --- a/lib/LANraragi/Model/Archive.pm +++ b/lib/LANraragi/Model/Archive.pm @@ -8,8 +8,8 @@ use Cwd 'abs_path'; use Redis; use Time::HiRes qw(usleep); use File::Basename; -use File::Temp qw(tempfile); use File::Copy "cp"; +use File::Path qw(make_path); use Mojo::Util qw(xml_escape); use LANraragi::Utils::Generic qw(get_tag_with_namespace remove_spaces remove_newlines render_api_response); @@ -54,11 +54,11 @@ sub generate_opds_catalog { my $tags = $arcdata->{tags}; # Infer a few OPDS-related fields from the tags - $arcdata->{dateadded} = get_tag_with_namespace( "dateadded", $tags, "2010-01-10T10:01:11Z" ); - $arcdata->{author} = get_tag_with_namespace( "artist", $tags, "" ); - $arcdata->{language} = get_tag_with_namespace( "language", $tags, "" ); - $arcdata->{circle} = get_tag_with_namespace( "group", $tags, "" ); - $arcdata->{event} = get_tag_with_namespace( "event", $tags, "" ); + $arcdata->{dateadded} = get_tag_with_namespace( "date_added", $tags, "2010-01-10T10:01:11Z" ); + $arcdata->{author} = get_tag_with_namespace( "artist", $tags, "" ); + $arcdata->{language} = get_tag_with_namespace( "language", $tags, "" ); + $arcdata->{circle} = get_tag_with_namespace( "group", $tags, "" ); + $arcdata->{event} = get_tag_with_namespace( "event", $tags, "" ); # Application/zip is universally hated by all readers so it's better to use x-cbz and x-cbr here. if ( $file =~ /^(.*\/)*.+\.(pdf)$/ ) { @@ -124,7 +124,7 @@ sub find_untagged_archives { remove_newlines($t); # The following are basic and therefore don't count as "tagged" - $nondefaulttags += 1 unless $t =~ /(artist|parody|series|language|event|group|date_added):.*/; + $nondefaulttags += 1 unless $t =~ /(artist|parody|series|language|event|group|date_added|timestamp):.*/; } #If the archive has no tags, or the tags namespaces are only from @@ -287,17 +287,21 @@ sub serve_page { # Apply resizing transformation if set in Settings if ( LANraragi::Model::Config->enable_resize ) { - # Use File::Temp to copy the extracted file and resize it - my ( $fh, $filename ) = tempfile(); - cp( $file, $fh ); + # Store resized files in a subfolder of the ID's temp folder + my $resized_file = "$tempfldr/$id/resized/$path"; + my ( $n, $resized_folder, $e ) = fileparse( $resized_file, qr/\.[^.]*/ ); + make_path($resized_folder); + + $logger->debug("Copying file to $resized_folder for resize transformation"); + cp( $file, $resized_file ); my $threshold = LANraragi::Model::Config->get_threshold; my $quality = LANraragi::Model::Config->get_readquality; - LANraragi::Model::Reader::resize_image( $filename, $quality, $threshold ); + LANraragi::Model::Reader::resize_image( $resized_file, $quality, $threshold ); # resize_image always converts the image to jpg $self->render_file( - filepath => $filename, + filepath => $resized_file, content_disposition => "inline", format => "jpg" ); diff --git a/lib/LANraragi/Model/Upload.pm b/lib/LANraragi/Model/Upload.pm index c00c4c63f..f63474792 100644 --- a/lib/LANraragi/Model/Upload.pm +++ b/lib/LANraragi/Model/Upload.pm @@ -6,7 +6,7 @@ use warnings; use Redis; use URI::Escape; use File::Basename; -use File::Temp qw/ tempfile tempdir /; +use File::Temp qw(tempdir); use File::Find qw(find); use File::Copy qw(move); @@ -107,9 +107,10 @@ sub handle_incoming_file { return ( 0, $id, $name, "The file couldn't be moved to your content folder!" ); } - # Now that the file has been copied, we can add the timestamp tag. + # Now that the file has been copied, we can add the timestamp tag and calculate pagecount. # (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 ); $redis->quit(); $logger->debug("Running autoplugin on newly uploaded file $id..."); diff --git a/lib/LANraragi/Plugin/Metadata/EHentai.pm b/lib/LANraragi/Plugin/Metadata/EHentai.pm index e9282c54c..77d2dba2e 100644 --- a/lib/LANraragi/Plugin/Metadata/EHentai.pm +++ b/lib/LANraragi/Plugin/Metadata/EHentai.pm @@ -35,7 +35,7 @@ sub plugin_info { { type => "bool", desc => "Fetch using thumbnail first (falls back to title)" }, { type => "bool", desc => "Use ExHentai (enable to search for fjorded content without star cookie)" }, { type => "bool", - desc => "Save the original Japanese title when available instead of the English or " . "romanised title" + 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" }, { type => "bool", desc => "Search expunged galleries as well" }, @@ -69,7 +69,7 @@ sub get_tags { $gID = $1; $gToken = $2; $logger->debug("Skipping search and using gallery $gID / $gToken from oneshot args"); - } elsif ( $lrr_info->{existing_tags} =~ /.*source:e(?:x|-)hentai\.org\/g\/([0-9]*)\/([0-z]*)\/*.*/gi ) { + } elsif ( $lrr_info->{existing_tags} =~ /.*source:\s*e(?:x|-)hentai\.org\/g\/([0-9]*)\/([0-z]*)\/*.*/gi ) { $gID = $1; $gToken = $2; $hasSrc = 1; diff --git a/lib/LANraragi/Plugin/Metadata/Eze.pm b/lib/LANraragi/Plugin/Metadata/Eze.pm index bb78c373d..d17a1768d 100644 --- a/lib/LANraragi/Plugin/Metadata/Eze.pm +++ b/lib/LANraragi/Plugin/Metadata/Eze.pm @@ -6,6 +6,8 @@ use warnings; #Plugins can freely use all Perl packages already installed on the system #Try however to restrain yourself to the ones already installed for LRR (see tools/cpanfile) to avoid extra installations by the end-user. use Mojo::JSON qw(from_json); +use File::Basename; +use Time::Local qw(timegm_modern); #You can also use the LRR Internal API when fitting. use LANraragi::Model::Plugins; @@ -23,12 +25,18 @@ sub plugin_info { type => "metadata", namespace => "ezeplugin", author => "Difegue", - version => "2.2", + version => "2.3", description => - "Collects metadata embedded into your archives as eze-style info.json files. ({'gallery_info': {xxx} } syntax)", + "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" } ] + 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 => "Fetch additional timestamp (time posted) and uploader metadata" }, + ] ); } @@ -38,49 +46,63 @@ sub get_tags { shift; my $lrr_info = shift; # Global info hash - my ($save_title) = @_; # Plugin parameters + my ($save_title, $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" ); - if ($path_in_archive) { - #Extract info.json - my $filepath = extract_file_from_archive( $lrr_info->{file_path}, $path_in_archive ); + 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) { + $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) { + $filepath = $path_nearby_json; + $logger->debug("Found file nearby at $filepath"); + $delete_after_parse = 0; + } else { + return ( error => "No in-archive info.json or {archive_name}.json file found!" ); + } - #Open it - my $stringjson = ""; + #Open it + my $stringjson = ""; - open( my $fh, '<:encoding(UTF-8)', $filepath ) - or return ( error => "Could not open $filepath!" ); + open( my $fh, '<:encoding(UTF-8)', $filepath ) + or return ( error => "Could not open $filepath!" ); - while ( my $row = <$fh> ) { - chomp $row; - $stringjson .= $row; - } + while ( my $row = <$fh> ) { + chomp $row; + $stringjson .= $row; + } - #Use Mojo::JSON to decode the string into a hash - my $hashjson = from_json $stringjson; + #Use Mojo::JSON to decode the string into a hash + my $hashjson = from_json $stringjson; - $logger->debug("Found and loaded the following JSON: $stringjson"); + $logger->debug("Loaded the following JSON: $stringjson"); - #Parse it - my ( $tags, $title ) = tags_from_eze_json($hashjson); + #Parse it + my ( $tags, $title ) = tags_from_eze_json($origin_title, $additional_tags, $hashjson); + if ($delete_after_parse){ #Delete it unlink $filepath; + } - #Return tags - $logger->info("Sending the following tags to LRR: $tags"); - - if ( $save_title && $title ) { - $logger->info("Parsed title is $title"); - return ( tags => $tags, title => $title ); - } else { - return ( tags => $tags ); - } + #Return tags + $logger->info("Sending the following tags to LRR: $tags"); + if ( $save_title && $title ) { + $logger->info("Parsed title is $title"); + return ( tags => $tags, title => $title ); } else { - return ( error => "No eze info.json file found in this archive!" ); + return ( tags => $tags ); } } @@ -89,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 $hash = $_[0]; + my ($origin_title, $additional_tags, $hash) = @_; my $return = ""; #Tags are in gallery_info -> tags -> one array per namespace @@ -97,6 +119,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"} ) { + $title = $hash->{"gallery_info"}->{"title_original"}; + } + remove_spaces($title); foreach my $namespace ( sort keys %$tags ) { @@ -115,9 +142,33 @@ sub tags_from_eze_json { 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 ) { + # 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]); + $timestamp = $time; + } + + if ( $category ) { + $return .= ", category:$category"; + } + + if ( $additional_tags && $uploader ) { + $return .= ", uploader:$uploader"; + } + + if ( $additional_tags && $timestamp ) { + $return .= ", timestamp:$timestamp"; + } if ( $site && $gid && $gtoken ) { - $return .= ", source: $site.org/g/$gid/$gtoken"; + $return .= ", source:$site.org/g/$gid/$gtoken"; } #Done-o diff --git a/lib/LANraragi/Plugin/Metadata/Koromo.pm b/lib/LANraragi/Plugin/Metadata/Koromo.pm index a46c369f8..edb9a68cb 100644 --- a/lib/LANraragi/Plugin/Metadata/Koromo.pm +++ b/lib/LANraragi/Plugin/Metadata/Koromo.pm @@ -41,6 +41,13 @@ sub get_tags { my $file = $lrr_info->{file_path}; my $path_in_archive = is_file_in_archive( $file, "Info.json" ); + + unless ($path_in_archive) { + + # Try for the lowercase variant as well + $path_in_archive = is_file_in_archive( $file, "info.json" ); + } + if ($path_in_archive) { #Extract info.json diff --git a/lib/LANraragi/Plugin/Metadata/nHentai.pm b/lib/LANraragi/Plugin/Metadata/nHentai.pm index 40776e177..861cd37be 100644 --- a/lib/LANraragi/Plugin/Metadata/nHentai.pm +++ b/lib/LANraragi/Plugin/Metadata/nHentai.pm @@ -44,11 +44,15 @@ sub get_tags { # Work your magic here - You can create subs below to organize the code better my $galleryID = ""; - # Quick regex to get the nh gallery id from the provided url. + # Quick regex to get the nh gallery id from the provided url or source tag. if ( $lrr_info->{oneshot_param} =~ /.*\/g\/([0-9]+).*/ ) { $galleryID = $1; + $logger->debug("Skipping search and using gallery $galleryID from oneshot args"); + } elsif ( $lrr_info->{existing_tags} =~ /.*source:\s*(?:https?:\/\/)?nhentai\.net\/g\/([0-9]*).*/gi ) { + # Matching URL Scheme like 'https://' is only for backward compatible purpose. + $galleryID = $1; + $logger->debug("Skipping search and using gallery $galleryID from source tag") } else { - #Get Gallery ID by hand if the user didn't specify a URL $galleryID = get_gallery_id_from_title( $lrr_info->{archive_title} ); } @@ -210,7 +214,7 @@ sub get_tags_from_NH { if ( $json ) { my @tags = get_tags_from_json($json); - push( @tags, "source:https://nhentai.net/g/$gID" ) if ( @tags > 0 ); + 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); diff --git a/lib/LANraragi/Plugin/Scripts/FolderToCat.pm b/lib/LANraragi/Plugin/Scripts/FolderToCat.pm index 7817890ae..b4c45faa5 100644 --- a/lib/LANraragi/Plugin/Scripts/FolderToCat.pm +++ b/lib/LANraragi/Plugin/Scripts/FolderToCat.pm @@ -82,8 +82,10 @@ sub run_script { push @created_categories, $catID; for my $file ( @{ $subfolders{$folder} } ) { - my $id = compute_id($file) || next; - LANraragi::Model::Category::add_to_category( $catID, $id ); + eval { + my $id = compute_id($file) || next; + LANraragi::Model::Category::add_to_category( $catID, $id ); + }; } } diff --git a/lib/LANraragi/Plugin/Scripts/nHentaiSourceConverter.pm b/lib/LANraragi/Plugin/Scripts/nHentaiSourceConverter.pm index fcb66e446..a11db37c7 100644 --- a/lib/LANraragi/Plugin/Scripts/nHentaiSourceConverter.pm +++ b/lib/LANraragi/Plugin/Scripts/nHentaiSourceConverter.pm @@ -19,7 +19,7 @@ sub plugin_info { author => "Guerra24", version => "1.0", description => - "Converts \"source:id\" tags with 6 or less digits into \"source:https://nhentai.net/g/id\"" + "Converts \"source:{id}\" tags with 6 or less digits into \"source:nhentai.net/g/{id}\"" ); } @@ -41,7 +41,7 @@ sub run_script { my %hash = $redis->hgetall($id); my ( $tags ) = @hash{qw(tags)}; - if ( $tags =~ s/source:(\d{1,6})/source:https:\/\/nhentai\.net\/g\/$1/igm ) { + if ( $tags =~ s/source:(\d{1,6})/source:nhentai\.net\/g\/$1/igm ) { $count++; } diff --git a/lib/LANraragi/Utils/Archive.pm b/lib/LANraragi/Utils/Archive.pm index ed2a905f3..9aa9d6ea9 100644 --- a/lib/LANraragi/Utils/Archive.pm +++ b/lib/LANraragi/Utils/Archive.pm @@ -19,7 +19,7 @@ use Image::Magick; use Archive::Libarchive qw( ARCHIVE_OK ); use Archive::Libarchive::Extract; use Archive::Libarchive::Peek; -use File::Temp qw(tempfile tempdir); +use File::Temp qw(tempdir); use LANraragi::Utils::TempFolder qw(get_temp); use LANraragi::Utils::Logging qw(get_logger); diff --git a/lib/LANraragi/Utils/Database.pm b/lib/LANraragi/Utils/Database.pm index 7c70cbc65..544d66987 100644 --- a/lib/LANraragi/Utils/Database.pm +++ b/lib/LANraragi/Utils/Database.pm @@ -15,6 +15,7 @@ use Unicode::Normalize; use LANraragi::Model::Plugins; use LANraragi::Utils::Generic qw(flat remove_spaces); use LANraragi::Utils::Tags qw(unflat_tagrules tags_rules_to_array restore_CRLF); +use LANraragi::Utils::Archive qw(get_filelist); use LANraragi::Utils::Logging qw(get_logger); # Functions for interacting with the DB Model. @@ -59,18 +60,32 @@ sub add_timestamp_tag { if ( LANraragi::Model::Config->enable_dateadded eq "1" ) { $logger->debug("Adding timestamp tag..."); + my $date; if ( LANraragi::Model::Config->use_lastmodified eq "1" ) { $logger->info("Using file date"); - my $date = ( stat( $redis->hget( $id, "file" ) ) )[9]; #9 is the unix time stamp for date modified. - $redis->hset( $id, "tags", "date_added:$date" ); + $date = ( stat( $redis->hget( $id, "file" ) ) )[9]; #9 is the unix time stamp for date modified. } else { $logger->info("Using current date"); - $redis->hset( $id, "tags", "date_added:" . time() ); + $date = time(); } + + add_tags( $id, "date_added:$date" ); } } +# add_pagecount(redis,id) +# Calculates and adds pagecount to the given ID. +sub add_pagecount { + my ( $redis, $id ) = @_; + my $logger = get_logger( "Archive", "lanraragi" ); + + my $file = $redis->hget( $id, "file" ); + my ( $images, $sizes ) = get_filelist($file); + my @images = @$images; + $redis->hset( $id, "pagecount", scalar @images ); +} + # get_archive_json(redis, id) # Builds a JSON object for an archive registered in the database and returns it. # If you need to get many JSONs at once, use the multi variant. diff --git a/lib/LANraragi/Utils/Routing.pm b/lib/LANraragi/Utils/Routing.pm index c6d1b28a9..4d078fbcc 100644 --- a/lib/LANraragi/Utils/Routing.pm +++ b/lib/LANraragi/Utils/Routing.pm @@ -93,8 +93,6 @@ sub apply_routes { $logged_in_api->post('/api/plugins/use')->to('api-other#use_plugin_sync'); $logged_in_api->post('/api/plugins/queue')->to('api-other#use_plugin_async'); $logged_in_api->delete('/api/tempfolder')->to('api-other#clean_tempfolder'); - $logged_in_api->get('/api/minion/:jobid')->to('api-other#minion_job_status'); - $logged_in_api->post('/api/minion/:jobname/queue')->to('api-other#queue_minion_job'); $logged_in_api->post('/api/download_url')->to('api-other#download_url'); $logged_in_api->post('/api/regen_thumbs')->to('api-other#regen_thumbnails'); @@ -132,6 +130,12 @@ sub apply_routes { $logged_in_api->get('/api/shinobu')->to('api-shinobu#shinobu_status'); $logged_in_api->post('/api/shinobu/stop')->to('api-shinobu#stop_shinobu'); $logged_in_api->post('/api/shinobu/restart')->to('api-shinobu#restart_shinobu'); + $logged_in_api->post('/api/shinobu/rescan')->to('api-shinobu#reset_filemap'); + + # Minion API + $public_api->get('/api/minion/:jobid')->to('api-minion#minion_job_status'); + $logged_in_api->get('/api/minion/:jobid/detail')->to('api-minion#minion_job_detail'); + $logged_in_api->post('/api/minion/:jobname/queue')->to('api-minion#queue_minion_job'); # unused for now # Category API $public_api->get('/api/categories')->to('api-category#get_category_list'); diff --git a/lib/Shinobu.pm b/lib/Shinobu.pm index a8aba5e3e..34d9c3e07 100644 --- a/lib/Shinobu.pm +++ b/lib/Shinobu.pm @@ -246,6 +246,13 @@ sub add_to_filemap { $redis->wait_all_responses; invalidate_cache(); } + + # Set pagecount in case it's not already there + unless ( $redis->hget( $id, "pagecount" ) ) { + $logger->debug("Pagecount not calculated for $id, doing it now!"); + LANraragi::Utils::Database::add_pagecount( $redis, $id ); + } + } else { # Add to Redis if not present beforehand @@ -302,6 +309,7 @@ sub add_new_file { eval { LANraragi::Utils::Database::add_archive_to_redis( $id, $file, $redis ); LANraragi::Utils::Database::add_timestamp_tag( $redis, $id ); + LANraragi::Utils::Database::add_pagecount( $redis, $id ); #AutoTagging using enabled plugins goes here! LANraragi::Model::Plugins::exec_enabled_plugins_on_file($id); diff --git a/package.json b/package.json index 74baf9dab..cfc6ade76 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lanraragi", - "version": "0.8.4", - "version_name": "Real Cool World", + "version": "0.8.5", + "version_name": "Sex and the Church", "description": "I'm under Japanese influence and my honor's at stake!", "scripts": { "test": "prove -r -l -v tests/", @@ -34,7 +34,7 @@ "jquery": "^3.6.0", "jquery-contextmenu": "^2.9.2", "jquery-toast-plugin": "^1.3.2", - "marked": "^3.0.4", + "marked": "^4.0.10", "open-sans-fontface": "^1.4.0", "roboto-fontface": "^0.8.0", "swiper": "^7.2.0", diff --git a/public/css/lrr.css b/public/css/lrr.css index e6851ec27..4ef206ca5 100644 --- a/public/css/lrr.css +++ b/public/css/lrr.css @@ -36,14 +36,14 @@ td.tags { span.tags { display: block; - overflow:hidden; - text-overflow:ellipsis; - min-height:20px; + overflow: hidden; + text-overflow: ellipsis; + min-height: 20px; } .tagger { - min-height:125px; - cursor:text; + min-height: 125px; + cursor: text; } @@ -75,7 +75,7 @@ div.id4 { } table.itg { - table-layout:fixed; + table-layout: fixed; } /*Archive list Sorting*/ @@ -148,7 +148,7 @@ table thead .sorting_disabled a:after { margin-bottom: 36px; } -.index-carousel > .option-flyout { +.index-carousel>.option-flyout { margin: 0 4%; } @@ -184,9 +184,9 @@ p#nb { .caption-namespace { width: 100px !important; - font-size:10pt; - padding: 3px 0px 7px; - vertical-align:top; + font-size: 10pt; + padding: 3px 0px 7px; + vertical-align: top; } .caption-tags { @@ -275,28 +275,30 @@ p#nb { color: lightskyblue; } -.quick-thumbnail:hover > .page-number { +.quick-thumbnail:hover>.page-number { z-index: 300; background-color: #000000; } .reader-image { - max-width:100%; + max-width: 100%; + min-width: 0; height: auto; width: auto; - min-height:300px; user-select: none; + align-self: center; + cursor: pointer; } .caption-reader { - margin-left: auto; - margin-right: auto; - padding:16px; + margin-left: auto; + margin-right: auto; + padding: 16px; min-width: 50% !important; } .reader-thumbnail { - display: inline-block; + display: inline-block; vertical-align: middle; margin-right: 48px !important; } @@ -318,7 +320,7 @@ p#nb { } .collapsible-right { - float: right; + float: right; padding: 1.2rem 1.2rem 0 0; } @@ -326,12 +328,12 @@ p#nb { border: none !important; } -.option-flyout > .collapsible-title { +.option-flyout>.collapsible-title { font-size: 18px; font-weight: bold; } -.option-flyout > .collapsible-body { +.option-flyout>.collapsible-body { padding: 10px !important; } @@ -367,7 +369,7 @@ li { font-size: 9pt; } -.checklist > li { +.checklist>li { text-align: left; text-overflow: ellipsis; white-space: nowrap; @@ -406,22 +408,24 @@ li { background-color: transparent !important; } -.artist-tag{ +.artist-tag { color: #22a7f0; } -.series-tag, .parody-tag { +.series-tag, +.parody-tag { color: #d2527f; } -.circle-tag, .group-tag { +.circle-tag, +.group-tag { color: #36d7b7; } .theme-preview { - height: 64px; - margin-top:4px; - margin-right:8px; + height: 64px; + margin-top: 4px; + margin-right: 8px; border-radius: 4px; } @@ -439,6 +443,7 @@ li { margin: auto; padding-bottom: 6px; } + .indeterminate .bar-container { position: absolute; margin: 0; @@ -459,27 +464,36 @@ li { 0% { -moz-transform: translateX(100%); } + 100% { -moz-transform: translateX(-100%); } } + @-webkit-keyframes scroll-left { - 0% { + 0% { -webkit-transform: translateX(100%); } + 100% { -webkit-transform: translateX(-100%); } } + @keyframes scroll-left { - 0% { - -moz-transform: translateX(100%); /* Browser bug fix */ - -webkit-transform: translateX(100%); /* Browser bug fix */ + 0% { + -moz-transform: translateX(100%); + /* Browser bug fix */ + -webkit-transform: translateX(100%); + /* Browser bug fix */ transform: translateX(100%); } + 100% { - -moz-transform: translateX(-100%); /* Browser bug fix */ - -webkit-transform: translateX(-100%); /* Browser bug fix */ + -moz-transform: translateX(-100%); + /* Browser bug fix */ + -webkit-transform: translateX(-100%); + /* Browser bug fix */ transform: translateX(-100%); } } @@ -488,11 +502,12 @@ li { .jq-toast-single a { padding-bottom: 0 !important; } + .jq-toast-single h2 { margin: 0 !important; } -.awesomplete > ul { +.awesomplete>ul { z-index: 99 !important; } @@ -525,6 +540,7 @@ li { .ie7 .page-overlay { min-height: 100% !important; } + .left-column, .right-column { width: 100% !important; @@ -554,7 +570,7 @@ li { @media all and (max-width: 560px) { div.id1 { - width:164px; + width: 164px; height: 256px !important; } @@ -598,6 +614,7 @@ li { white-space: nowrap; text-overflow: ellipsis; } + .option-td { width: 150px !important; } @@ -643,9 +660,9 @@ li { height: unset; } -#settingsOverlay > div { - margin: auto; - font-size: 8pt; +#settingsOverlay>div { + margin: auto; + font-size: 8pt; } #changelog { @@ -653,7 +670,7 @@ li { text-align: left; } -#changelog > * > img { +#changelog>*>img { max-width: 400px; } @@ -690,4 +707,29 @@ body.infinite-scroll #display img { body.infinite-scroll .sb { margin-top: 10px; +} + +/* Double Page & Fullscreen */ + +div.sni img[src=""] { + display: none; +} + +div.fullscreen img { + height: unset !important; + max-height: 100vh !important; + margin: 0px !important; +} + +#display { + display: flex; + justify-content: center; +} + +.fullscreen-infinite { + overflow-y: scroll; +} + +body.infinite-scroll .fullscreen-infinite img { + margin: 0 auto !important; } \ No newline at end of file diff --git a/public/js/common.js b/public/js/common.js index d5238f968..bdb3cf497 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -38,7 +38,13 @@ LRR.isNullOrWhitespace = function (input) { */ LRR.getTagSearchURL = function (namespace, tag) { const namespacedTag = this.buildNamespacedTag(namespace, tag); - return (namespace !== "source") ? `/?q=${encodeURIComponent(namespacedTag)}` : `http://${tag}`; + if (namespace !== "source"){ + return `/?q=${encodeURIComponent(namespacedTag)}`; + } else if (/https?:\/\//.test(tag)) { + return `${tag}`; + } else { + return `https://${tag}`; + } }; /** @@ -259,3 +265,21 @@ LRR.showErrorToast = function (header, error) { icon: "error", }); }; + +/** + * Fires a HEAD request to get filesize of a given URL. + * return target img size. + * @param {*} target Target URL String + */ +LRR.getImgSize = function (target) { + let imgSize = 0; + $.ajax({ + async: false, + url: target, + type: "HEAD", + success: (data, textStatus, request) => { + imgSize = parseInt(request.getResponseHeader("Content-Length") / 1024, 10); + }, + }); + return imgSize; +}; diff --git a/public/js/index.js b/public/js/index.js index 4bb66a90c..2d5f547e4 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -383,7 +383,7 @@ Index.fetchChangelog = function () { throw new Error(data.result); } - marked(data.body, { + marked.parse(data.body, { gfm: true, breaks: true, sanitize: true, diff --git a/public/js/reader.js b/public/js/reader.js index 7d0f0f69e..bcbcf4695 100644 --- a/public/js/reader.js +++ b/public/js/reader.js @@ -9,6 +9,7 @@ Reader.force = false; Reader.previousPage = -1; Reader.currentPage = -1; Reader.showingSinglePage = true; +Reader.isFullscreen = false; Reader.preloadedImg = {}; Reader.preloadedSizes = {}; @@ -19,6 +20,7 @@ Reader.initializeAll = function () { // Bind events to DOM $(document).on("keyup", Reader.handleShortcuts); + $(document).on("wheel", Reader.handleWheel); $(document).on("click.toggle_fit_mode", "#fit-mode input", Reader.toggleFitMode); $(document).on("click.toggle_double_mode", "#toggle-double-mode input", Reader.toggleDoublePageMode); @@ -34,6 +36,7 @@ Reader.initializeAll = function () { $(document).on("click.pagination_change_pages", ".page-link", Reader.handlePaginator); $(document).on("click.close_overlay", "#overlay-shade", LRR.closeOverlay); + $(document).on("click.toggle_full_screen", "#toggle-full-screen", Reader.toggleFullScreen); $(document).on("click.toggle_archive_overlay", "#toggle-archive-overlay", Reader.toggleArchiveOverlay); $(document).on("click.toggle_settings_overlay", "#toggle-settings-overlay", Reader.toggleSettingsOverlay); $(document).on("click.toggle_help", "#toggle-help", Reader.toggleHelp); @@ -109,8 +112,18 @@ Reader.loadImages = function () { if (Reader.infiniteScroll) { Reader.initInfiniteScrollView(); } else { - $(document).on("click.imagemap_change_pages", "#Map area", Reader.handlePaginator); - $(window).on("resize", Reader.updateImagemap); + // when click left or right img area change page + $(document).on("click", (event) => { + // check click Y position is in img Y area + if ($(event.target).closest("#i3").length && !$("#overlay-shade").is(":visible")) { + // is click X position is left on screen or right + if (event.pageX < $(window).width() / 2) { + Reader.changePage(-1); + } else { + Reader.changePage(1); + } + } + }); // when there's no parameter, null is coerced to 0 so it becomes -1 Reader.currentPage = Reader.currentPage || ( @@ -131,7 +144,7 @@ Reader.loadImages = function () { if (Reader.showOverlayByDefault) { Reader.toggleArchiveOverlay(); } // Wait for the extraction job to conclude before getting thumbnails - Server.checkJobStatus(data.job, + Server.checkJobStatus(data.job, false, () => Reader.initializeArchiveOverlay(), () => LRR.showErrorToast("The extraction job didn't conclude properly. Your archive might be corrupted.")); }).finally(() => { @@ -234,6 +247,9 @@ Reader.handleShortcuts = function (e) { case 68: // d Reader.changePage(1); break; + case 70: // f + Reader.toggleFullScreen(); + break; case 72: // h Reader.toggleHelp(); break; @@ -258,6 +274,17 @@ Reader.handleShortcuts = function (e) { } }; +Reader.handleWheel = function (e) { + if (Reader.isFullscreen && !Reader.infiniteScroll) { + let changePage = 1; + if (e.originalEvent.deltaY > 0) changePage = -1; + // In Manga mode, reverse the changePage variable + // so that we always move forward + if (!Reader.mangaMode) changePage *= -1; + Reader.changePage(changePage); + } +}; + Reader.checkFiletypeSupport = function (extension) { if ((extension === "rar" || extension === "cbr") && !localStorage.rarWarningShown) { localStorage.rarWarningShown = true; @@ -308,6 +335,11 @@ Reader.updateMetadata = function () { const img = $("#img")[0]; const imageUrl = new URL(img.src); const filename = imageUrl.searchParams.get("path"); + + const imgDoublePage = $("#img_doublepage")[0]; + const imageUrlDoublePage = new URL(imgDoublePage.src); + const filenameDoublePage = imageUrlDoublePage.searchParams.get("path"); + if (!filename && Reader.showingSinglePage) { Reader.currentPageLoaded = true; $("#i3").removeClass("loading"); @@ -316,22 +348,36 @@ Reader.updateMetadata = function () { const width = img.naturalWidth; const height = img.naturalHeight; + const widthDoublePage = imgDoublePage.naturalWidth; + const heightDoublePage = imgDoublePage.naturalHeight; + const widthView = width + widthDoublePage; if (Reader.showingSinglePage) { - // HEAD request to get filesize let size = Reader.preloadedSizes[Reader.currentPage]; if (!size) { - $.ajax({ - url: Reader.pages[Reader.currentPage], - type: "HEAD", - success: (data, textStatus, request) => { - size = parseInt(request.getResponseHeader("Content-Length") / 1024, 10); - Reader.preloadedSizes[Reader.currentPage] = size; - $(".file-info").text(`${filename} :: ${width} x ${height} :: ${size} KB`); - }, - }); - } else { $(".file-info").text(`${filename} :: ${width} x ${height} :: ${size} KB`); } - } else { $(".file-info").text(`Double-Page View :: ${width} x ${height}`); } + size = LRR.getImgSize(Reader.pages[Reader.currentPage]); + Reader.preloadedSizes[Reader.currentPage] = size; + $(".file-info").text(`${filename} :: ${width} x ${height} :: ${size} KB`); + $(".file-info").attr("title", `${filename} :: ${width} x ${height} :: ${size} KB`); + } else { + $(".file-info").text(`${filename} :: ${width} x ${height} :: ${size} KB`); + $(".file-info").attr("title", `${filename} :: ${width} x ${height} :: ${size} KB`); + } + } else { + let size = Reader.preloadedSizes[Reader.currentPage]; + let sizePre = Reader.preloadedSizes[Reader.currentPage + 1]; + + if (!size || !sizePre) { + size = LRR.getImgSize(Reader.pages[Reader.currentPage]); + sizePre = LRR.getImgSize(Reader.pages[Reader.currentPage + 1]); + Reader.preloadedSizes[Reader.currentPage] = size; + Reader.preloadedSizes[Reader.currentPage + 1] = sizePre; + } + + const sizeView = size + sizePre; + $(".file-info").text(`${filename} - ${filenameDoublePage} :: ${widthView} x ${height} :: ${sizeView} KB`); + $(".file-info").attr("title", `${filename} :: ${width} x ${height} :: ${size} KB - ${filenameDoublePage} :: ${widthDoublePage} x ${heightDoublePage} :: ${sizePre} KB`); + } // Update page numbers in the paginator const newVal = Reader.showingSinglePage @@ -339,37 +385,37 @@ Reader.updateMetadata = function () { : `${Reader.currentPage + 1} + ${Reader.currentPage + 2}`; $(".current-page").each((_i, el) => $(el).html(newVal)); - Reader.updateImageMap(); Reader.currentPageLoaded = true; $("#i3").removeClass("loading"); }; -Reader.updateImageMap = function () { - // update imagemap with the w/h parameters we obtained - const img = $("#img")[0]; - const mapWidth = img.width / 2; - const mapHeight = img.height; - - $("#leftmap").attr("coords", `0,0,${mapWidth},${mapHeight}`); - $("#rightmap").attr("coords", `${mapWidth + 1},0,${img.width},${mapHeight}`); -}; - Reader.goToPage = function (page) { Reader.previousPage = Reader.currentPage; Reader.currentPage = Math.min(Reader.maxPage, Math.max(0, +page)); Reader.showingSinglePage = false; + $("#img_doublepage").attr("src", ""); + $("#display").removeClass("double-mode"); if (Reader.doublePageMode && Reader.currentPage > 0 && Reader.currentPage < Reader.maxPage) { - // composite an image and use that as the source + // Composite an image and use that as the source const img1 = Reader.loadImage(Reader.currentPage); const img2 = Reader.loadImage(Reader.currentPage + 1); - let imagesLoaded = 0; - const loadHandler = () => { (imagesLoaded += 1) === 2 && Reader.drawCanvas(img1, img2); }; - $([img1, img2]).each((_i, img) => { - img.onload = loadHandler; - // If the image is preloaded it does not trigger onload, so we have to call it manually - if (img.complete) { loadHandler(); } - }); + // If w > h on one of the images(widespread), set canvasdata to the first image only + if (img1.naturalWidth > img1.naturalHeight || img2.naturalWidth > img2.naturalHeight) { + // Depending on whether we were going forward or backward, display img1 or img2 + const wideSrc = Reader.previousPage > Reader.currentPage ? img2.src : img1.src; + $("#img").attr("src", wideSrc); + Reader.showingSinglePage = true; + } else { + if (Reader.mangaMode) { + $("#img").attr("src", img2.src); + $("#img_doublepage").attr("src", img1.src); + } else { + $("#img").attr("src", img1.src); + $("#img_doublepage").attr("src", img2.src); + } + $("#display").addClass("double-mode"); + } } else { const img = Reader.loadImage(Reader.currentPage); $("#img").attr("src", img.src); @@ -515,6 +561,7 @@ Reader.toggleHeader = function () { $("#toggle-header input").toggleClass("toggled"); $("#i2").toggle(); Reader.applyContainerWidth(); + return false; }; Reader.toggleProgressTracking = function () { @@ -541,6 +588,50 @@ Reader.toggleArchiveOverlay = function () { return LRR.toggleOverlay("#archivePagesOverlay"); }; +Reader.toggleFullScreen = function () { + // if already full screen; exit + // else go fullscreen + if ( + document.fullscreenElement + || document.webkitFullscreenElement + || document.mozFullScreenElement + || document.msFullscreenElement + ) { + if ($("body").hasClass("infinite-scroll")) { + $("div#i3").removeClass("fullscreen-infinite"); + } else { + $("div#i3").removeClass("fullscreen"); + } + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + Reader.isFullscreen = false; + } else { + if ($("body").hasClass("infinite-scroll")) { + $("div#i3").addClass("fullscreen-infinite"); + } else { + $("div#i3").addClass("fullscreen"); + } + const element = $("div#i3").get(0); + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } + Reader.isFullscreen = true; + } +}; + Reader.initializeArchiveOverlay = function () { if ($("#archivePagesOverlay").attr("loaded") === "true") { return; @@ -581,7 +672,7 @@ Reader.initializeArchiveOverlay = function () { thumbSuccess(); } else if (response.status === 202) { // Wait for Minion job to finish - response.json().then((data) => Server.checkJobStatus(data.job, + response.json().then((data) => Server.checkJobStatus(data.job, false, () => thumbSuccess(), () => thumbFail())); } else { @@ -595,33 +686,6 @@ Reader.initializeArchiveOverlay = function () { $("#archivePagesOverlay").attr("loaded", "true"); }; -Reader.drawCanvas = function (img1, img2) { - // If w > h on one of the images(widespread), set canvasdata to the first image only - if (img1.naturalWidth > img1.naturalHeight || img2.naturalWidth > img2.naturalHeight) { - // Depending on whether we were going forward or backward, display img1 or img2 - $("#img").attr("src", Reader.previousPage > Reader.currentPage ? img2.src : img1.src); - Reader.showingSinglePage = true; - return; - } - - // Create an adequately-sized canvas - const canvas = $("#dpcanvas")[0]; - canvas.width = img1.naturalWidth + img2.naturalWidth; - canvas.height = Math.max(img1.naturalHeight, img2.naturalHeight); - - // Draw both images on it - const ctx = canvas.getContext("2d"); - if (Reader.mangaMode) { - ctx.drawImage(img2, 0, 0); - ctx.drawImage(img1, img2.naturalWidth + 1, 0); - } else { - ctx.drawImage(img1, 0, 0); - ctx.drawImage(img2, img1.naturalWidth + 1, 0); - } - - $("#img").attr("src", canvas.toDataURL("image/jpeg")); -}; - Reader.changePage = function (targetPage) { let destination; if (Reader.infiniteScroll) { diff --git a/public/js/server.js b/public/js/server.js index 2c2549632..efc9c09c6 100644 --- a/public/js/server.js +++ b/public/js/server.js @@ -43,11 +43,13 @@ Server.callAPI = function (endpoint, method, successMessage, errorMessage, succe /** * Check the status of a Minion job until it's completed. * @param {*} jobId Job ID to check + * @param {*} useDetail Whether to get full details or the job or not. + * This requires the user to be logged in. * @param {*} callback Execute a callback on successful job completion. * @param {*} failureCallback Execute a callback on unsuccessful job completion. */ -Server.checkJobStatus = function (jobId, callback, failureCallback) { - fetch(`/api/minion/${jobId}`, { method: "GET" }) +Server.checkJobStatus = function (jobId, useDetail, callback, failureCallback) { + fetch(useDetail ? `/api/minion/${jobId}/detail` : `/api/minion/${jobId}`, { method: "GET" }) .then((response) => (response.ok ? response.json() : { success: 0, error: "Response was not OK" })) .then((data) => { if (data.error) throw new Error(data.error); @@ -59,7 +61,7 @@ Server.checkJobStatus = function (jobId, callback, failureCallback) { if (data.state !== "finished") { // Wait and retry, job isn't done yet setTimeout(() => { - Server.checkJobStatus(jobId, callback, failureCallback); + Server.checkJobStatus(jobId, useDetail, callback, failureCallback); }, 1000); } else { // Update UI with info @@ -113,7 +115,7 @@ Server.triggerScript = function (namespace) { .then(Server.callAPI(`/api/plugins/queue?plugin=${namespace}&arg=${scriptArg}`, "POST", null, "Error while executing Script :", (data) => { // Check minion job state periodically while we're on this page - Server.checkJobStatus(data.job, + Server.checkJobStatus(data.job, true, (d) => { Server.isScriptRunning = false; $(".script-running").hide(); @@ -197,7 +199,7 @@ Server.regenerateThumbnails = function (force) { $("#forcethumb-button").prop("disabled", true); // Check minion job state periodically while we're on this page - Server.checkJobStatus(data.job, + Server.checkJobStatus(data.job, true, (d) => { $("#genthumb-button").prop("disabled", false); $("#forcethumb-button").prop("disabled", false); diff --git a/public/js/upload.js b/public/js/upload.js index 6ec7767a8..94ac2eefd 100644 --- a/public/js/upload.js +++ b/public/js/upload.js @@ -1,29 +1,26 @@ // Scripting for the Upload page. -var processingArchives = 0; -var completedArchives = 0; -var failedArchives = 0; -var totalUploads = 0; +let processingArchives = 0; +let completedArchives = 0; +let failedArchives = 0; +let totalUploads = 0; // Handle updating the upload counters. function updateUploadCounters() { - $("#progressCount").html(`🤔 Processing: ${processingArchives} 🙌 Completed: ${completedArchives} 👹 Failed: ${failedArchives}`); - var icon = (completedArchives == totalUploads) ? "fas fa-check-circle" : - failedArchives > 0 ? "fas fa-exclamation-circle" : - "fa fa-spinner fa-spin"; + const icon = (completedArchives == totalUploads) ? "fas fa-check-circle" + : failedArchives > 0 ? "fas fa-exclamation-circle" + : "fa fa-spinner fa-spin"; $("#progressTotal").html(` Total:${completedArchives + failedArchives}/${totalUploads}`); // At the end of the upload job, dump the search cache! - if (processingArchives === 0) - Server.invalidateCache(); + if (processingArchives === 0) { Server.invalidateCache(); } } // Handle a completed job from minion. Update the line in upload results with the title, ID, message. function handleCompletedUpload(jobID, d) { - $(`#${jobID}-name`).html(d.result.title); if (d.result.id) { @@ -32,11 +29,11 @@ function handleCompletedUpload(jobID, d) { } if (d.result.success) { - $(`#${jobID}-link`).html("Click here to edit metadata.
(" + d.result.message + ")") + $(`#${jobID}-link`).html(`Click here to edit metadata.
(${d.result.message})`); $(`#${jobID}-icon`).attr("class", "fa fa-check-circle"); completedArchives++; } else { - $(`#${jobID}-link`).html("Error while processing archive.
(" + d.result.message + ")"); + $(`#${jobID}-link`).html(`Error while processing archive.
(${d.result.message})`); $(`#${jobID}-icon`).attr("class", "fa fa-exclamation-circle"); failedArchives++; } @@ -46,8 +43,7 @@ function handleCompletedUpload(jobID, d) { } function handleFailedUpload(jobID, d) { - - $(`#${jobID}-link`).html("Error while processing file.
(" + d + ")"); + $(`#${jobID}-link`).html(`Error while processing file.
(${d})`); $(`#${jobID}-icon`).attr("class", "fa fa-exclamation-circle"); failedArchives++; @@ -57,26 +53,24 @@ function handleFailedUpload(jobID, d) { // Send URLs to the Download API and add a Server.checkJobStatus to track its progress. function downloadUrl() { - const categoryID = document.getElementById("category").value; // One fetch job per non-empty line of the form - $('#urlForm').val().split(/\r|\n/).forEach(url => { - + $("#urlForm").val().split(/\r|\n/).forEach((url) => { if (url === "") return; - let formData = new FormData(); - formData.append('url', url); + const formData = new FormData(); + formData.append("url", url); if (categoryID !== "") { - formData.append('catid', categoryID); + formData.append("catid", categoryID); } fetch("/api/download_url", { method: "POST", - body: formData + body: formData, }) - .then(response => response.json()) + .then((response) => response.json()) .then((data) => { if (data.success) { result = ` @@ -87,41 +81,38 @@ function downloadUrl() { `; - $('#files').append(result); + $("#files").append(result); totalUploads++; processingArchives++; updateUploadCounters(); - // Check minion job state periodically to update the result - Server.checkJobStatus(data.job, + // Check minion job state periodically to update the result + Server.checkJobStatus(data.job, true, (d) => handleCompletedUpload(data.job, d), (error) => handleFailedUpload(data.job, error)); } else { throw new Error(data.message); } }) - .catch(error => LRR.showErrorToast("Error while adding download job", error)); - + .catch((error) => LRR.showErrorToast("Error while adding download job", error)); }); } // Set up jqueryfileupload. function initUpload() { - - $('#fileupload').fileupload({ - dataType: 'json', - formData: function (form) { - let array = [{ name: 'catid', value: document.getElementById("category").value }]; + $("#fileupload").fileupload({ + dataType: "json", + formData(form) { + const array = [{ name: "catid", value: document.getElementById("category").value }]; return array; }, - done: function (e, data) { - - if (data.result.success == 0) + done(e, data) { + if (data.result.success == 0) { result = `${data.result.name} ${data.result.error} `; - else + } else { result = ` ${data.result.name} @@ -129,37 +120,37 @@ function initUpload() { Processing file... (Job #${data.result.job}) `; + } - $('#progress .bar').css('width', '0%'); - $('#files').append(result); + $("#progress .bar").css("width", "0%"); + $("#files").append(result); totalUploads++; processingArchives++; updateUploadCounters(); - // Check minion job state periodically to update the result - Server.checkJobStatus(data.result.job, + // Check minion job state periodically to update the result + Server.checkJobStatus(data.result.job, true, (d) => handleCompletedUpload(data.result.job, d), (error) => handleFailedUpload(data.result.job, error)); }, - fail: function (e, data) { + fail(e, data) { result = `${data.result.name} ${data.errorThrown} `; - $('#progress .bar').css('width', '0%'); - $('#files').append(result); + $("#progress .bar").css("width", "0%"); + $("#files").append(result); totalUploads++; failedArchives++; updateUploadCounters(); }, - progressall: function (e, data) { - var progress = parseInt(data.loaded / data.total * 100, 10); - $('#progress .bar').css('width', progress + '%'); - } + progressall(e, data) { + const progress = parseInt(data.loaded / data.total * 100, 10); + $("#progress .bar").css("width", `${progress}%`); + }, }); - -} \ No newline at end of file +} diff --git a/templates/config.html.tt2 b/templates/config.html.tt2 index f83bbbce8..189a17af2 100644 --- a/templates/config.html.tt2 +++ b/templates/config.html.tt2 @@ -148,6 +148,15 @@ }); } + function rescanContentFolder() { + $("#rescan-button").prop("disabled", true); + Server.callAPI("/api/shinobu/rescan", "POST", "Content folder rescan started!", "Error while restarting Worker:", + () => { + $("#rescan-button").prop("disabled", false); + shinobuStatus(); + }); + } + // Update the status of the background worker. function shinobuStatus() { Server.callAPI("/api/shinobu", "GET", null, "Error while querying Shinobu status:", diff --git a/templates/reader.html.tt2 b/templates/reader.html.tt2 index 1314ff32b..06e05fbde 100644 --- a/templates/reader.html.tt2 +++ b/templates/reader.html.tt2 @@ -40,11 +40,7 @@ - - - - - + @@ -148,6 +144,7 @@
  • P: toggle double page mode
  • Q: bring up the thumbnail index and archive options.
  • R: open a random archive.
  • +
  • F: toggle fullscreen mode

  • To return to the archive index, touch the arrow pointing down or use Backspace. @@ -253,12 +250,13 @@ [% BLOCK pagesel %]
    - +
    +
    [% END %] diff --git a/templates/templates_config/config_files.html.tt2 b/templates/templates_config/config_files.html.tt2 index 00376a72e..35ac9c51e 100644 --- a/templates/templates_config/config_files.html.tt2 +++ b/templates/templates_config/config_files.html.tt2 @@ -11,6 +11,20 @@ + + + + + + + + Click this button to trigger a rescan of the Archive Directory in case you're missing files,
    + or some data such as total page counts.
    +
    + + +

    Maximum
    Temporary Folder Size

    diff --git a/tests/LANraragi/Plugin/Metadata/Eze.t b/tests/LANraragi/Plugin/Metadata/Eze.t index 32566d10a..7e20fe9da 100644 --- a/tests/LANraragi/Plugin/Metadata/Eze.t +++ b/tests/LANraragi/Plugin/Metadata/Eze.t @@ -18,11 +18,13 @@ require "$cwd/tests/mocks.pl"; use_ok('LANraragi::Plugin::Metadata::Eze'); -note("eze Tests"); -{ +sub eve_test { + + my ($jsonpath, $save_title, $origin_title, $additional_tags) = @_; + # Copy the eze sample json to a temporary directory as it's deleted once parsed my ( $fh, $filename ) = tempfile(); - cp( $SAMPLES . "/eze/eze_sample.json", $fh ); + cp( $SAMPLES . $jsonpath , $fh ); # Mock LANraragi::Utils::Archive's subs to return the temporary sample JSON # Since we're using exports, the methods are under the plugin's namespace. @@ -31,19 +33,105 @@ note("eze Tests"); local *LANraragi::Plugin::Metadata::Eze::extract_file_from_archive = sub { $filename }; local *LANraragi::Plugin::Metadata::Eze::is_file_in_archive = sub { 1 }; - my %dummyhash = ( something => 22, file_path => "test" ); + my %dummyhash = ( something => 42, file_path => "dummy" ); # Since this is calling the sub directly and not in an object context, # we pass a dummy string as first parameter to replace the object. - my %ezetags = trap { LANraragi::Plugin::Metadata::Eze::get_tags( "", \%dummyhash, 1 ); }; + my %ezetags = trap { LANraragi::Plugin::Metadata::Eze::get_tags( "", \%dummyhash, $save_title, $origin_title, $additional_tags ); }; + + return %ezetags; + +} + +note("eze-lite Tests, save_title on, origin_title off, additional_tags off"); +{ + my $save_title = 1; + my $origin_title = 0; + my $additional_tags = 0; + + my %ezetags = eve_test("/eze/eze_lite_sample.json", $save_title, $origin_title, $additional_tags); - my $ezetags = - "artist:mitarashi kousei, character:akiko minase, character:yuuichi aizawa, female:aunt, female:lingerie, female:sole female, group:mitarashi club, language:english, language:translated, male:sole male, misc:multi-work series, parody:kanon, source: website.org/g/1179590/7c5815c77b"; is( $ezetags{title}, "(C72) [Mitarashi Club (Mitarashi Kousei)] Akiko-san to Issho (Kanon) [English] [Belldandy100] [Decensored]", - "eze parsing test 1/2" + "title parsing test 1/2" + ); + is( $ezetags{tags}, + "artist:mitarashi kousei, character:akiko minase, character:yuuichi aizawa, female:aunt, female:lingerie, female:sole female, group:mitarashi club, language:english, language:translated, male:sole male, misc:multi-work series, parody:kanon, source:website.org/g/1179590/7c5815c77b", + "tags parsing test 2/2" + ); +} + + +note("eze-lite Tests, save_title on, origin_title on, additional_tags on"); +{ + my $save_title = 1; + my $origin_title = 1; + my $additional_tags = 1; + + my %ezetags = eve_test("/eze/eze_lite_sample.json", $save_title, $origin_title, $additional_tags); + + is( $ezetags{title}, + "(C72) [Mitarashi Club (Mitarashi Kousei)] Akiko-san to Issho (Kanon) [English] [Belldandy100] [Decensored]", + "title parsing test 1/2" + ); + is( $ezetags{tags}, + "artist:mitarashi kousei, character:akiko minase, character:yuuichi aizawa, female:aunt, female:lingerie, female:sole female, group:mitarashi club, language:english, language:translated, male:sole male, misc:multi-work series, parody:kanon, timestamp:1517540580, source:website.org/g/1179590/7c5815c77b", + "tags parsing test 2/2" + ); +} + +note("eze-full Tests, save_title off, origin_title off, additional_tags off"); +{ + my $save_title = 0; + my $origin_title = 0; + my $additional_tags = 0; + + my %ezetags = eve_test("/eze/eze_full_sample.json", $save_title, $origin_title, $additional_tags); + + is( $ezetags{title}, + undef, + "title parsing test 1/2" + ); + is( $ezetags{tags}, + "artist:hiten, female:defloration, female:pantyhose, female:sole female, group:hitenkei, language:chinese, language:translated, male:sole male, parody:original, category:doujinshi, source:exhentai.org/g/1017975/49b3c275a1", + "tags parsing test 2/2" + ); +} + +note("eze-full Tests, save_title on, origin_title off, additional_tags on"); +{ + my $save_title = 1; + my $origin_title = 0; + my $additional_tags = 1; + + my %ezetags = eve_test("/eze/eze_full_sample.json", $save_title, $origin_title, $additional_tags); + + is( $ezetags{title}, + "(C91) [HitenKei (Hiten)] R.E.I.N.A [Chinese] [無邪気漢化組]", + "title parsing test 1/2" + ); + is( $ezetags{tags}, + "artist:hiten, female:defloration, female:pantyhose, female:sole female, group:hitenkei, language:chinese, language:translated, male:sole male, parody:original, category:doujinshi, uploader:cocy, timestamp:1484412360, source:exhentai.org/g/1017975/49b3c275a1", + "tags parsing test 2/2" + ); +} + +note("eze-full Tests, save_title on, origin_title on, additional_tags on"); +{ + my $save_title = 1; + my $origin_title = 1; + my $additional_tags = 1; + + my %ezetags = eve_test("/eze/eze_full_sample.json", $save_title, $origin_title, $additional_tags); + + is( $ezetags{title}, + "(C91) [HitenKei (Hiten)] R.E.I.N.A [中国翻訳]", + "title parsing test 1/2" + ); + is( $ezetags{tags}, + "artist:hiten, female:defloration, female:pantyhose, female:sole female, group:hitenkei, language:chinese, language:translated, male:sole male, parody:original, category:doujinshi, uploader:cocy, timestamp:1484412360, source:exhentai.org/g/1017975/49b3c275a1", + "tags parsing test 2/2" ); - is( $ezetags{tags}, $ezetags, "eze parsing test 2/2" ); } -done_testing(); +done_testing(); \ No newline at end of file diff --git a/tests/modules.t b/tests/modules.t index f1d9a5e00..1c660671f 100644 --- a/tests/modules.t +++ b/tests/modules.t @@ -4,7 +4,7 @@ use utf8; use Cwd; use Mojo::Base 'Mojolicious'; -use Test::More tests => 57; +use Test::More; use Test::Mojo; use Test::MockObject; @@ -26,35 +26,35 @@ require $cwd . "/tests/mocks.pl"; setup_redis_mock(); my @modules = ( - "Shinobu", "LANraragi", - "LANraragi::Utils::Archive", "LANraragi::Utils::Database", - "LANraragi::Utils::Generic", "LANraragi::Utils::Plugins", - "LANraragi::Utils::Routing", "LANraragi::Utils::TempFolder", - "LANraragi::Utils::Logging", "LANraragi::Utils::Minion", - "LANraragi::Utils::Tags", "LANraragi::Controller::Api::Archive", - "LANraragi::Controller::Api::Search", "LANraragi::Controller::Api::Category", - "LANraragi::Controller::Api::Database", "LANraragi::Controller::Api::Shinobu", - "LANraragi::Controller::Api::Other", "LANraragi::Controller::Backup", - "LANraragi::Controller::Batch", "LANraragi::Controller::Config", - "LANraragi::Controller::Edit", "LANraragi::Controller::Index", - "LANraragi::Controller::Logging", "LANraragi::Controller::Login", - "LANraragi::Controller::Plugins", "LANraragi::Controller::Reader", - "LANraragi::Controller::Stats", "LANraragi::Controller::Upload", - "LANraragi::Controller::Category", "LANraragi::Model::Archive", - "LANraragi::Model::Backup", "LANraragi::Model::Config", - "LANraragi::Model::Plugins", "LANraragi::Model::Reader", - "LANraragi::Model::Search", "LANraragi::Model::Stats", - "LANraragi::Model::Category", "LANraragi::Model::Upload", - "LANraragi::Plugin::Metadata::Chaika", "LANraragi::Plugin::Metadata::CopyTags", - "LANraragi::Plugin::Metadata::DateAdded", "LANraragi::Plugin::Metadata::EHentai", - "LANraragi::Plugin::Metadata::Eze", "LANraragi::Plugin::Metadata::Hdoujin", - "LANraragi::Plugin::Metadata::Koromo", "LANraragi::Plugin::Metadata::MEMS", - "LANraragi::Plugin::Metadata::nHentai", "LANraragi::Plugin::Metadata::RegexParse", - "LANraragi::Plugin::Metadata::Fakku", "LANraragi::Plugin::Login::EHentai", - "LANraragi::Plugin::Login::Fakku", "LANraragi::Plugin::Scripts::SourceFinder", - "LANraragi::Plugin::Scripts::FolderToCat", "LANraragi::Plugin::Download::EHentai", - "LANraragi::Plugin::Download::Chaika", "LANraragi::Plugin::Scripts::nHentaiSourceConverter", - "LANraragi::Plugin::Scripts::BlacklistMigrate" + "Shinobu", "LANraragi", + "LANraragi::Utils::Archive", "LANraragi::Utils::Database", + "LANraragi::Utils::Generic", "LANraragi::Utils::Plugins", + "LANraragi::Utils::Routing", "LANraragi::Utils::TempFolder", + "LANraragi::Utils::Logging", "LANraragi::Utils::Minion", + "LANraragi::Utils::Tags", "LANraragi::Controller::Api::Archive", + "LANraragi::Controller::Api::Search", "LANraragi::Controller::Api::Category", + "LANraragi::Controller::Api::Database", "LANraragi::Controller::Api::Shinobu", + "LANraragi::Controller::Api::Minion", "LANraragi::Controller::Api::Other", + "LANraragi::Controller::Backup", "LANraragi::Controller::Batch", + "LANraragi::Controller::Config", "LANraragi::Controller::Edit", + "LANraragi::Controller::Index", "LANraragi::Controller::Logging", + "LANraragi::Controller::Login", "LANraragi::Controller::Plugins", + "LANraragi::Controller::Reader", "LANraragi::Controller::Stats", + "LANraragi::Controller::Upload", "LANraragi::Controller::Category", + "LANraragi::Model::Archive", "LANraragi::Model::Backup", + "LANraragi::Model::Config", "LANraragi::Model::Plugins", + "LANraragi::Model::Reader", "LANraragi::Model::Search", + "LANraragi::Model::Stats", "LANraragi::Model::Category", + "LANraragi::Model::Upload", "LANraragi::Plugin::Metadata::Chaika", + "LANraragi::Plugin::Metadata::CopyTags", "LANraragi::Plugin::Metadata::DateAdded", + "LANraragi::Plugin::Metadata::EHentai", "LANraragi::Plugin::Metadata::Eze", + "LANraragi::Plugin::Metadata::Hdoujin", "LANraragi::Plugin::Metadata::Koromo", + "LANraragi::Plugin::Metadata::MEMS", "LANraragi::Plugin::Metadata::nHentai", + "LANraragi::Plugin::Metadata::RegexParse", "LANraragi::Plugin::Metadata::Fakku", + "LANraragi::Plugin::Login::EHentai", "LANraragi::Plugin::Login::Fakku", + "LANraragi::Plugin::Scripts::SourceFinder", "LANraragi::Plugin::Scripts::FolderToCat", + "LANraragi::Plugin::Download::EHentai", "LANraragi::Plugin::Download::Chaika", + "LANraragi::Plugin::Scripts::nHentaiSourceConverter", "LANraragi::Plugin::Scripts::BlacklistMigrate" ); # Test all modules load properly diff --git a/tests/samples/eze/eze_full_sample.json b/tests/samples/eze/eze_full_sample.json new file mode 100644 index 000000000..7edf4c86a --- /dev/null +++ b/tests/samples/eze/eze_full_sample.json @@ -0,0 +1,109 @@ +{ + "gallery_info": { + "title": "(C91) [HitenKei (Hiten)] R.E.I.N.A [Chinese] [無邪気漢化組]", + "title_original": "(C91) [HitenKei (Hiten)] R.E.I.N.A [中国翻訳]", + "category": "doujinshi", + "tags": { + "language": [ + "chinese", + "translated" + ], + "parody": [ + "original" + ], + "group": [ + "hitenkei" + ], + "artist": [ + "hiten" + ], + "male": [ + "sole male" + ], + "female": [ + "defloration", + "pantyhose", + "sole female" + ] + }, + "language": "Chinese", + "translated": true, + "favorite_category": null, + "upload_date": [ + 2017, + 1, + 14, + 16, + 46, + 0 + ], + "source": { + "site": "exhentai", + "gid": 1017975, + "token": "49b3c275a1", + "parent_gallery": null, + "newer_versions": [] + } + }, + "gallery_info_full": { + "gallery": { + "gid": 1017975, + "token": "49b3c275a1" + }, + "title": "(C91) [HitenKei (Hiten)] R.E.I.N.A [Chinese] [無邪気漢化組]", + "title_original": "(C91) [HitenKei (Hiten)] R.E.I.N.A [中国翻訳]", + "date_uploaded": 1484412360000, + "category": "doujinshi", + "uploader": "cocy", + "rating": { + "average": 4.85, + "count": 557 + }, + "favorites": { + "category": -1, + "category_title": "", + "count": 5166 + }, + "parent": null, + "newer_versions": [], + "thumbnail": "https://exhentai.org/t/03/a6/03a6180e6de784661608a2c7416588dae530110a-4062963-2116-3000-png_250.jpg", + "thumbnail_size": "large", + "thumbnail_rows": 4, + "image_count": 23, + "images_resized": false, + "total_file_size_approx": 56675532, + "visible": true, + "visible_reason": "", + "language": "Chinese", + "translated": true, + "tags": { + "language": [ + "chinese", + "translated" + ], + "parody": [ + "original" + ], + "group": [ + "hitenkei" + ], + "artist": [ + "hiten" + ], + "male": [ + "sole male" + ], + "female": [ + "defloration", + "pantyhose", + "sole female" + ] + }, + "tags_have_namespace": true, + "torrent_count": 1, + "archiver_key": "458297--6d569f8eddc13d17401566b8786598987906ee0b", + "source": "html", + "source_site": "exhentai", + "date_generated": 1649871435117 + } +} \ No newline at end of file diff --git a/tests/samples/eze/eze_sample.json b/tests/samples/eze/eze_lite_sample.json similarity index 86% rename from tests/samples/eze/eze_sample.json rename to tests/samples/eze/eze_lite_sample.json index e70a63871..a9e6fdb73 100644 --- a/tests/samples/eze/eze_sample.json +++ b/tests/samples/eze/eze_lite_sample.json @@ -1,7 +1,7 @@ { "gallery_info": { "title": "(C72) [Mitarashi Club (Mitarashi Kousei)] Akiko-san to Issho (Kanon) [English] [Belldandy100] [Decensored]", - "title_original": "(C72) [みたらし倶楽部 (みたらし侯成)] 秋子さんといっしょ (カノン) [英訳] [無修正]", + "title_original": "", "category": "", "tags": { "language": [ diff --git a/tests/search.t b/tests/search.t index 0f23fe5e2..5981fd5f1 100644 --- a/tests/search.t +++ b/tests/search.t @@ -5,7 +5,7 @@ use Cwd; use Mojo::Base 'Mojolicious'; -use Test::More tests => 19; +use Test::More; use Test::Mojo; use Test::MockObject; use Mojo::JSON qw (decode_json); diff --git a/tools/Documentation/SUMMARY.md b/tools/Documentation/SUMMARY.md index efd07dbd5..f10d7a9cd 100644 --- a/tools/Documentation/SUMMARY.md +++ b/tools/Documentation/SUMMARY.md @@ -45,6 +45,7 @@ * [Database API](api-documentation/database-api.md) * [Category API](api-documentation/category-api.md) * [Shinobu API](api-documentation/shinobu-api.md) +* [Minion API](api-documentation/minion-api.md) * [Miscellaneous other API](api-documentation/miscellaneous-other-api.md) ## Writing Plugins diff --git a/tools/Documentation/api-documentation/database-api.md b/tools/Documentation/api-documentation/database-api.md index aeb5c09a9..d3b56535d 100644 --- a/tools/Documentation/api-documentation/database-api.md +++ b/tools/Documentation/api-documentation/database-api.md @@ -10,11 +10,7 @@ Get tags from the database, with a value symbolizing their prevalence. {% endswagger-description %} {% swagger-parameter name="minweight" type="int" required="false" in="query" %} -Add this parameter if you want to only get tags whose weight is at least the given minimum. - -\ - - +Add this parameter if you want to only get tags whose weight is at least the given minimum. Default is 1 if not specified, to get all tags. {% endswagger-parameter %} @@ -51,11 +47,7 @@ Cleans the Database, removing entries for files that are no longer on the filesy {% swagger baseUrl="http://lrr.tvc-16.science" path="/api/database/drop" method="post" summary="🔑Drop the Database" %} {% swagger-description %} -Delete the entire database, including user preferences. - -\ - - +Delete the entire database, including user preferences. This is a rather dangerous endpoint, invoking it might lock you out of the server as a client! {% endswagger-description %} @@ -71,11 +63,7 @@ This is a rather dangerous endpoint, invoking it might lock you out of the serve {% swagger baseUrl="http://lrr.tvc-16.science" path="/api/database/backup" method="get" summary="🔑Get a backup JSON" %} {% swagger-description %} -Scans the entire database and returns a backup in JSON form. - -\ - - +Scans the entire database and returns a backup in JSON form. This backup can be reimported manually through the Backup and Restore feature. {% endswagger-description %} diff --git a/tools/Documentation/api-documentation/minion-api.md b/tools/Documentation/api-documentation/minion-api.md new file mode 100644 index 000000000..01c2dae71 --- /dev/null +++ b/tools/Documentation/api-documentation/minion-api.md @@ -0,0 +1,77 @@ +--- +description: Control the built-in Minion Job Queue. +--- + +# Minion API + +{% swagger baseUrl="http://lrr.tvc-16.science" path="/api/minion/:jobid" method="get" summary="Get the basic status of a Minion Job" %} +{% swagger-description %} +For a given Minion job ID, check whether it succeeded or failed. +Minion jobs are ran for various occasions like thumbnails, cache warmup and handling incoming files. +{% endswagger-description %} + +{% swagger-parameter name="id" type="string" required="true" in="path" %} +ID of the Job. +{% endswagger-parameter %} + +{% swagger-response status="200" description="You get job data." %} +```javascript +{ + "state": "finished", + "task": "handle_upload", + "error": null +} + +{ + "state": "failed", + "task": "thumbnail_task", + "error": "oh no" +} +``` +{% endswagger-response %} +{% endswagger %} + +{% swagger baseUrl="http://lrr.tvc-16.science" path="/api/minion/:jobid/detail" method="get" summary="🔑Get the full status of a Minion Job" %} +{% swagger-description %} +Get the status of a Minion Job. +This API is there for internal usage mostly, but you can use it to get detailed status for jobs like plugin runs or URL downloads. +{% endswagger-description %} + +{% swagger-parameter name="id" type="string" required="true" in="path" %} +ID of the Job. +{% endswagger-parameter %} + +{% swagger-response status="200" description="You get detailed job data." %} +```javascript +{ + "args": ["\/tmp\/QF3UCnKdMr\/myfile.zip"], + "attempts": 1, + "children": [], + "created": "1601145004", + "delayed": "1601145004", + "expires": null, + "finished": "1601145004", + "id": 7, + "lax": 0, + "notes": {}, + "parents": [], + "priority": 0, + "queue": "default", + "result": { + "id": "75d18ce470dc99f83dc355bdad66319d1f33c82b", + "message": "This file already exists in the Library.", + "success": 0 + }, + "retried": null, + "retries": 0, + "started": "1601145004", + "state": "finished", + "task": "handle_upload", + "time": "1601145005", + "worker": 1 +} +``` +{% endswagger-response %} +{% endswagger %} + + diff --git a/tools/Documentation/api-documentation/miscellaneous-other-api.md b/tools/Documentation/api-documentation/miscellaneous-other-api.md index 53582a27a..29b5b0cf1 100644 --- a/tools/Documentation/api-documentation/miscellaneous-other-api.md +++ b/tools/Documentation/api-documentation/miscellaneous-other-api.md @@ -283,10 +283,18 @@ You can either use `login`, `metadata`, `script`, or `all` to get all previous t { "desc": "Save archive 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", + "type": "bool" } ], "type": "metadata", - "version": "2.2" + "version": "2.3" }, { "author": "Pao", @@ -456,47 +464,4 @@ Whether to generate all thumbnails, or only the missing ones. } ``` {% endswagger-response %} -{% endswagger %} - -{% swagger baseUrl="http://lrr.tvc-16.science" path="/api/minion/:jobid" method="get" summary="🔑Get the status of a Minion Job" %} -{% swagger-description %} -Get the status of a Minion Job. Minions jobs are ran for various occasions like thumbnails, cache warmup and handling incoming files. -Usually stuff you don't need to care about as a client, but the API is there for internal usage mostly. -{% endswagger-description %} - -{% swagger-parameter name="id" type="string" required="true" in="path" %} -ID of the Job. -{% endswagger-parameter %} - -{% swagger-response status="200" description="" %} -```javascript -{ - "args": ["\/tmp\/QF3UCnKdMr\/myfile.zip"], - "attempts": 1, - "children": [], - "created": "1601145004", - "delayed": "1601145004", - "expires": null, - "finished": "1601145004", - "id": 7, - "lax": 0, - "notes": {}, - "parents": [], - "priority": 0, - "queue": "default", - "result": { - "id": "75d18ce470dc99f83dc355bdad66319d1f33c82b", - "message": "This file already exists in the Library.", - "success": 0 - }, - "retried": null, - "retries": 0, - "started": "1601145004", - "state": "finished", - "task": "handle_upload", - "time": "1601145005", - "worker": 1 -} -``` -{% endswagger-response %} -{% endswagger %} +{% endswagger %} \ No newline at end of file diff --git a/tools/Documentation/installing-lanraragi/source.md b/tools/Documentation/installing-lanraragi/source.md index 9234edb76..c89d82d6f 100644 --- a/tools/Documentation/installing-lanraragi/source.md +++ b/tools/Documentation/installing-lanraragi/source.md @@ -27,7 +27,7 @@ perlmagick ghostscript npm _Base software dependencies._ {% hint style="info" %} -If your package manager requires you to specify which ImageMagick version to install you need to choose version 7. +If your package manager requires you to specify which ImageMagick version to install, choose version 7. {% endhint %} {% hint style="info" %} @@ -44,7 +44,10 @@ git clone -b master http://github.com/Difegue/LANraragi /home/koyomi/lanraragi cd /home/koyomi/lanraragi && sudo npm run lanraragi-installer install-full ``` -Note: Do not use `sudo` in the above command if you are using `perlbrew`. +{% hint style="info"} +Do not use `sudo` in the above command if you are using `perlbrew`. +Arch users might need to install `perl-config-autoconf` and use env variable `export PERL5LIB=~/perl5/lib/perl5` before running the installer. +{% endhint %} Once this is done, you can get started by running `npm start` and opening [http://localhost:3000](http://localhost:3000). diff --git a/tools/Documentation/installing-lanraragi/windows.md b/tools/Documentation/installing-lanraragi/windows.md index b3775e5a8..920490af5 100644 --- a/tools/Documentation/installing-lanraragi/windows.md +++ b/tools/Documentation/installing-lanraragi/windows.md @@ -2,12 +2,21 @@ ## Download a Release -You can directly install LANraragi from the [Microsoft Store](https://cutt.ly/9TJIMC6). This will install the latest release. +You can directly install LANraragi from the Microsoft Store, using either this link: (paste in a browser window) +ms-windows-store://pdp/?productid=XP9K4NMNPDMH6L -[](https://cutt.ly/9TJIMC6) +Or through winget: -ms-windows-store://pdp/?productid=XP9K4NMNPDMH6L +``` +winget install lanraragi +``` + +{% hint style="warning" %} +The installer will tell you about this anyways, but LRR for Windows **requires** the Windows Subsystem for Linux to function properly. +Read the tutorial [here](https://docs.microsoft.com/en-us/windows/wsl/install) to see how to enable WSL on your Windows 10 machine. +You don't need to install a distribution through the Windows Store, as that is handled by the LRR installer package. +{% endhint %} As an alternative, you can always download the latest Windows MSI Installer on the [Release Page](https://github.com/Difegue/LANraragi/releases). @@ -23,16 +32,7 @@ You might get a SmartScreen prompt from Windows (doesn't seem to happen with the (If you're wondering why I don't sign installers, [this](https://gaby.dev/posts/code-signing) article is a good read.) {% hint style="info" %} -MS Store installs will be installed to the default location. If you don't want the app to install in _%AppData%_, consider downloading the installer and running it manually. -{% endhint %} - - - -{% hint style="warning" %} -The installer will tell you about this anyways, but LRR for Windows **requires** the Windows Subsystem for Linux to function properly. -Read the tutorial [here](https://docs.microsoft.com/en-us/windows/wsl/install) to see how to enable WSL on your Windows 10 machine. - -You don't need to install a distribution through the Windows Store, as that is handled by the LRR installer package. +MS Store/winget installs will be installed to the default location. If you don't want the app to install in _%AppData%_, consider downloading the installer and running it manually. {% endhint %} Once the install completes properly, you'll be able to launch the GUI from the shortcut in your Start Menu: diff --git a/tools/Documentation/plugin-docs/index.md b/tools/Documentation/plugin-docs/index.md index 863da1d2e..d5fc23e31 100644 --- a/tools/Documentation/plugin-docs/index.md +++ b/tools/Documentation/plugin-docs/index.md @@ -69,7 +69,7 @@ The `type` field can be either: * `login` for [Login Plugins](login.md) * `metadata` for [Metadata Plugins](metadata.md) -* `download` for [Downloader Plugins](downloaders.md) +* `download` for [Downloader Plugins](download.md) * `script` for [Script Plugins](scripts.md) The `parameters` array can contain as many arguments as you need. They can be set by the user in Plugin Configuration, and are transmitted every time. diff --git a/tools/build/docker/Dockerfile b/tools/build/docker/Dockerfile index fb786b2e4..2d4d652fc 100644 --- a/tools/build/docker/Dockerfile +++ b/tools/build/docker/Dockerfile @@ -1,8 +1,7 @@ # DOCKER-VERSION 0.3.4 -FROM alpine:3.12 +FROM alpine:3.14 LABEL git="https://github.com/Difegue/LANraragi" -ENV S6_OVERLAY_RELEASE v2.0.0.1 ENV S6_KEEP_ENV 1 # warn if we can't run stage2 (fix-attrs/cont-init) @@ -11,8 +10,9 @@ ENV S6_BEHAVIOUR_IF_STAGE2_FAILS 1 # wait 10s before KILLing ENV S6_KILL_GRACETIME 10000 -# s6 -ENTRYPOINT ["/init"] +# s6: The init is provided by alpine's s6-overlay package, hence the double slash. +# See https://pkgs.alpinelinux.org/contents?branch=v3.14&name=s6-overlay&arch=x86&repo=community +ENTRYPOINT ["//init"] # Check application health HEALTHCHECK --interval=1m --timeout=10s --retries=3 \ @@ -39,14 +39,6 @@ RUN \ adduser -D -u ${LRR_UID} -G koyomi koyomi; \ fi - -# we use s6-overlay-nobin to just pull in the s6-overlay arch agnostic (shell) -# components, since we apk install the binaries of s6 later which are arch specific -# /!\ While the s6 version here is fixed by an envvar, the apk install is not pinned and takes whatever's in alpine:latest! This certainly needs a fix. -ADD https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_RELEASE}/s6-overlay-nobin.tar.gz /tmp/s6-overlay-nobin.tar.gz -RUN tar -C / -xzf /tmp/s6-overlay-nobin.tar.gz && rm -f /tmp/s6-overlay-nobin.tar.gz - - WORKDIR /home/koyomi/lanraragi #Copy cpanfile and install script before copying the entire context diff --git a/tools/build/docker/install-everything.sh b/tools/build/docker/install-everything.sh index 6d4598d28..597a77faf 100755 --- a/tools/build/docker/install-everything.sh +++ b/tools/build/docker/install-everything.sh @@ -4,8 +4,8 @@ apk update apk add perl perl-io-socket-ssl perl-dev redis libarchive-dev libbz2 openssl-dev zlib-dev apk add imagemagick imagemagick-perlmagick libwebp-tools libheif -apk add g++ make pkgconf gnupg wget curl nodejs nodejs-npm file -apk add shadow s6 s6-portable-utils +apk add g++ make pkgconf gnupg wget curl nodejs npm file +apk add shadow s6 s6-portable-utils s6-overlay s6-overlay-preinit #Hey it's cpanm curl -L https://cpanmin.us | perl - App::cpanminus @@ -28,5 +28,5 @@ cd tools && cpanm --notest --installdeps . -M https://cpan.metacpan.org && cd .. npm run lanraragi-installer install-full #Cleanup to lighten the image -apk del perl-dev g++ make gnupg wget curl nodejs nodejs-npm openssl-dev file +apk del perl-dev g++ make gnupg wget curl nodejs npm openssl-dev file rm -rf /root/.cpanm/* /root/.npm/ /usr/local/share/man/* node_modules /var/cache/apk/* diff --git a/tools/build/homebrew/Lanraragi.rb b/tools/build/homebrew/Lanraragi.rb index 51df2fde3..a03884c4b 100644 --- a/tools/build/homebrew/Lanraragi.rb +++ b/tools/build/homebrew/Lanraragi.rb @@ -12,6 +12,7 @@ class Lanraragi < Formula revision 1 head "https://github.com/Difegue/LANraragi.git" + depends_on "nettle" => :build depends_on "pkg-config" => :build depends_on "cpanminus" depends_on "ghostscript" @@ -24,8 +25,13 @@ class Lanraragi < Formula depends_on "perl" depends_on "redis" depends_on "zstd" + uses_from_macos "libarchive" + on_linux do + depends_on "libarchive" + end + resource "Image::Magick" do url "https://cpan.metacpan.org/authors/id/J/JC/JCRISTY/PerlMagick-7.0.10.tar.gz" sha256 "1d5272d71b5cb44c30cd84b09b4dc5735b850de164a192ba191a9b35568305f4" @@ -40,8 +46,10 @@ def install ENV.prepend_create_path "PERL5LIB", "#{libexec}/lib/perl5" ENV.prepend_path "PERL5LIB", "#{libexec}/lib" ENV["CFLAGS"] = "-I#{libexec}/include" + # https://stackoverflow.com/questions/60521205/how-can-i-install-netssleay-with-perlbrew-in-macos-catalina - ENV["OPENSSL_PREFIX"] = "#{Formula["openssl@1.1"]}/1.1.1g" + ENV["OPENSSL_PREFIX"] = Formula["openssl@1.1"].opt_prefix # for Net::SSLeay + system "cpanm", "-v", "Net::SSLeay", "-l", libexec imagemagick = Formula["imagemagick"] resource("Image::Magick").stage do @@ -61,8 +69,8 @@ def install end end - system "npm", "install", *Language::Node.local_npm_install_args system "cpanm", "Config::AutoConf", "--notest", "-l", libexec + system "npm", "install", *Language::Node.local_npm_install_args system "perl", "./tools/install.pl", "install-full" prefix.install "README.md" diff --git a/tools/build/windows/Karen b/tools/build/windows/Karen index 41dc19b52..3d50b496d 160000 --- a/tools/build/windows/Karen +++ b/tools/build/windows/Karen @@ -1 +1 @@ -Subproject commit 41dc19b52d78024cfc2601c47ab8bd26dd003864 +Subproject commit 3d50b496decd9ff95b59690103837d77f393c3e2 diff --git a/tools/build/windows/build.ps1 b/tools/build/windows/build.ps1 index fb6f42f29..8d41dafa0 100644 --- a/tools/build/windows/build.ps1 +++ b/tools/build/windows/build.ps1 @@ -17,6 +17,6 @@ echo (Resolve-Path .\).Path nuget restore # Build Karen and Setup -msbuild /p:Configuration=Release /p:Platform=x64 +msbuild /p:Configuration=Release Get-FileHash .\Setup\bin\LANraragi.msi | Format-List \ No newline at end of file diff --git a/tools/cpanfile b/tools/cpanfile index cd2cd3643..68f27c294 100755 --- a/tools/cpanfile +++ b/tools/cpanfile @@ -52,3 +52,6 @@ requires 'File::ChangeNotify', 0.31; # Plugin system requires 'Module::Pluggable', 5.2; + +# Eze plugin +requires 'Time::Local', 1.30; \ No newline at end of file