EHentai.pm 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. package LANraragi::Plugin::Metadata::EHentai;
  2. use strict;
  3. use warnings;
  4. no warnings 'uninitialized';
  5. use utf8;
  6. #Plugins can freely use all Perl packages already installed on the system
  7. #Try however to restrain yourself to the ones already installed for LRR (see tools/cpanfile) to avoid extra installations by the end-user.
  8. use URI::Escape;
  9. use Mojo::JSON qw(decode_json encode_json);
  10. use Mojo::Util qw(html_unescape);
  11. use Mojo::UserAgent;
  12. #You can also use the LRR Internal API when fitting.
  13. use LANraragi::Model::Plugins;
  14. use LANraragi::Utils::Logging qw(get_plugin_logger);
  15. #Meta-information about your plugin.
  16. sub plugin_info {
  17. return (
  18. #Standard metadata
  19. name => "E-Hentai",
  20. type => "metadata",
  21. namespace => "ehplugin",
  22. login_from => "ehlogin",
  23. author => "Difegue and others",
  24. version => "2.5.1",
  25. description =>
  26. "搜索 g.e-hentai 以查找与您的存档匹配的标签. <br/><i class='fa fa-exclamation-circle'></i> 此插件将使用存档的 source: tag (如果存在)",
  27. icon =>
  28. "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=",
  29. parameters => [
  30. { type => "string", desc => "在搜索中强制使用语言(由于 EH 限制,日语无法使用)" },
  31. { type => "bool", desc => "保存档案名称" },
  32. { type => "bool", desc => "首先使用缩略图获取(否则使用标题)" },
  33. { type => "bool", desc => "使用 ExHentai(可以在没有星形 cookie 的情况下搜索fjorded内容)" },
  34. { type => "bool",
  35. desc => "如果可用,请保存原始标题,而不是英文或罗马拼音标题"
  36. },
  37. { type => "bool", desc => "获取额外的时间戳(发布时间)和上传者元数据" },
  38. { type => "bool", desc => "搜索已删除的图库" },
  39. ],
  40. oneshot_arg => "E-H Gallery URL (Will attach tags matching this exact gallery to your archive)",
  41. cooldown => 4
  42. );
  43. }
  44. #Mandatory function to be implemented by your plugin
  45. sub get_tags {
  46. shift;
  47. my $lrr_info = shift; # Global info hash
  48. my $ua = $lrr_info->{user_agent};
  49. my ( $lang, $savetitle, $usethumbs, $enablepanda, $jpntitle, $additionaltags, $expunged ) = @_; # Plugin parameters
  50. # Use the logger to output status - they'll be passed to a specialized logfile and written to STDOUT.
  51. my $logger = get_plugin_logger();
  52. # Work your magic here - You can create subroutines below to organize the code better
  53. my $gID = "";
  54. my $gToken = "";
  55. my $domain = ( $enablepanda ? 'https://exhentai.org' : 'https://e-hentai.org' );
  56. my $hasSrc = 0;
  57. # Quick regex to get the E-H archive ids from the provided url or source tag
  58. if ( $lrr_info->{oneshot_param} =~ /.*\/g\/([0-9]*)\/([0-z]*)\/*.*/ ) {
  59. $gID = $1;
  60. $gToken = $2;
  61. $logger->debug("Skipping search and using gallery $gID / $gToken from oneshot args");
  62. } elsif ( $lrr_info->{existing_tags} =~ /.*source:\s*e(?:x|-)hentai\.org\/g\/([0-9]*)\/([0-z]*)\/*.*/gi ) {
  63. $gID = $1;
  64. $gToken = $2;
  65. $hasSrc = 1;
  66. $logger->debug("Skipping search and using gallery $gID / $gToken from source tag");
  67. } else {
  68. # Craft URL for Text Search on EH if there's no user argument
  69. ( $gID, $gToken ) = &lookup_gallery(
  70. $lrr_info->{archive_title},
  71. $lrr_info->{existing_tags},
  72. $lrr_info->{thumbnail_hash},
  73. $ua, $domain, $lang, $usethumbs, $expunged
  74. );
  75. }
  76. # If an error occured, return a hash containing an error message.
  77. # LRR will display that error to the client.
  78. # Using the GToken to store error codes - not the cleanest but it's convenient
  79. if ( $gID eq "" ) {
  80. if ( $gToken ne "" ) {
  81. $logger->error($gToken);
  82. return ( error => $gToken );
  83. }
  84. $logger->info("No matching EH Gallery Found!");
  85. return ( error => "No matching EH Gallery Found!" );
  86. } else {
  87. $logger->debug("EH API Tokens are $gID / $gToken");
  88. }
  89. my ( $ehtags, $ehtitle ) = &get_tags_from_EH( $ua, $gID, $gToken, $jpntitle, $additionaltags );
  90. my %hashdata = ( tags => $ehtags );
  91. # Add source URL and title if possible/applicable
  92. if ( $hashdata{tags} ne "" ) {
  93. if ( !$hasSrc ) { $hashdata{tags} .= ", source:" . ( split( '://', $domain ) )[1] . "/g/$gID/$gToken"; }
  94. if ($savetitle) { $hashdata{title} = $ehtitle; }
  95. }
  96. #Return a hash containing the new metadata - it will be integrated in LRR.
  97. return %hashdata;
  98. }
  99. ######
  100. ## EH Specific Methods
  101. ######
  102. sub lookup_gallery {
  103. my ( $title, $tags, $thumbhash, $ua, $domain, $defaultlanguage, $usethumbs, $expunged ) = @_;
  104. my $logger = get_plugin_logger();
  105. my $URL = "";
  106. #Thumbnail reverse image search
  107. if ( $thumbhash ne "" && $usethumbs ) {
  108. $logger->info("Reverse Image Search Enabled, trying now.");
  109. #search with image SHA hash
  110. $URL =
  111. $domain
  112. . "?advsearch=1&f_sname=on&f_sdt2=on&f_spf=&f_spt=&f_sfu=on&f_sft=on&f_sfl=on&f_shash="
  113. . $thumbhash
  114. . "&fs_covers=1&fs_similar=1";
  115. #Include expunged galleries in the search if the option is enabled.
  116. if ($expunged) {
  117. $URL = $URL . "&fs_exp=1";
  118. }
  119. # Add the language override, if it's defined.
  120. if ( $defaultlanguage ne "" ) {
  121. # Add f_stags to search in tags for language
  122. $URL = $URL . "&f_stags=on&f_search=" . uri_escape_utf8("language:$defaultlanguage");
  123. }
  124. $logger->debug("Using URL $URL (archive thumbnail hash)");
  125. my ( $gId, $gToken ) = &ehentai_parse( $URL, $ua );
  126. if ( $gId ne "" && $gToken ne "" ) {
  127. return ( $gId, $gToken );
  128. }
  129. }
  130. # Regular text search
  131. $URL =
  132. $domain
  133. . "?advsearch=1&f_sname=on&f_sdt2=on&f_spf=&f_spt=&f_sfu=on&f_sft=on&f_sfl=on"
  134. . "&f_search="
  135. . uri_escape_utf8( qw(") . $title . qw(") );
  136. my $has_artist = 0;
  137. # Add artist tag from the OG tags if it exists
  138. if ( $tags =~ /.*artist:\s?([^,]*),*.*/gi ) {
  139. $URL = $URL . "+" . uri_escape_utf8("artist:$1");
  140. $has_artist = 1;
  141. }
  142. # Add the language override, if it's defined.
  143. if ( $defaultlanguage ne "" ) {
  144. $URL = $URL . "+" . uri_escape_utf8("language:$defaultlanguage");
  145. }
  146. # Add f_stags to search in tags if we added a tag (or two) in the search
  147. if ( $has_artist || $defaultlanguage ne "" ) {
  148. $URL = $URL . "&f_stags=on";
  149. }
  150. # Include expunged galleries in the search if the option is enabled.
  151. if ($expunged) {
  152. $URL = $URL . "&f_sh=on";
  153. }
  154. $logger->debug("Using URL $URL (archive title)");
  155. return &ehentai_parse( $URL, $ua );
  156. }
  157. # ehentai_parse(URL, UA)
  158. # Performs a remote search on e- or exhentai, and returns the ID/token matching the found gallery.
  159. sub ehentai_parse() {
  160. my ( $url, $ua ) = @_;
  161. my $logger = get_plugin_logger();
  162. my ( $dom, $error ) = search_gallery( $url, $ua );
  163. if ($error) {
  164. return ( "", $error );
  165. }
  166. my $gID = "";
  167. my $gToken = "";
  168. eval {
  169. # Get the first row of the search results
  170. # The "glink" class is parented by a <a> tag containing the gallery link in href.
  171. # This works in Minimal, Minimal+ and Compact modes, which should be enough.
  172. my $firstgal = $dom->at(".glink")->parent->attr('href');
  173. # A EH link looks like xhentai.org/g/{gallery id}/{gallery token}
  174. my $url = ( split( 'hentai.org/g/', $firstgal ) )[1];
  175. my @values = ( split( '/', $url ) );
  176. $gID = $values[0];
  177. $gToken = $values[1];
  178. };
  179. if ( index( $dom->to_string, "You are opening" ) != -1 ) {
  180. my $rand = 15 + int( rand( 51 - 15 ) );
  181. $logger->info("Sleeping for $rand seconds due to EH excessive requests warning");
  182. sleep($rand);
  183. }
  184. #Returning shit yo
  185. return ( $gID, $gToken );
  186. }
  187. sub search_gallery {
  188. my ( $url, $ua ) = @_;
  189. my $logger = get_plugin_logger();
  190. my $res = $ua->max_redirects(5)->get($url)->result;
  191. if ( index( $res->body, "Your IP address has been" ) != -1 ) {
  192. return ( "", "Temporarily banned from EH for excessive pageloads." );
  193. }
  194. return ( $res->dom, undef );
  195. }
  196. # get_tags_from_EH(userAgent, gID, gToken, jpntitle, additionaltags)
  197. # Executes an e-hentai API request with the given JSON and returns tags and title.
  198. sub get_tags_from_EH {
  199. my ( $ua, $gID, $gToken, $jpntitle, $additionaltags ) = @_;
  200. my $uri = 'https://api.e-hentai.org/api.php';
  201. my $logger = get_plugin_logger();
  202. my $jsonresponse = get_json_from_EH( $ua, $gID, $gToken );
  203. #if an error occurs(no response) return empty strings.
  204. if ( !$jsonresponse ) {
  205. return ( "", "" );
  206. }
  207. my $data = $jsonresponse->{"gmetadata"};
  208. my @tags = @{ @$data[0]->{"tags"} };
  209. my $ehtitle = @$data[0]->{ ( $jpntitle ? "title_jpn" : "title" ) };
  210. if ( $ehtitle eq "" && $jpntitle ) {
  211. $ehtitle = @$data[0]->{"title"};
  212. }
  213. my $ehcat = lc @$data[0]->{"category"};
  214. push( @tags, "category:$ehcat" );
  215. if ($additionaltags) {
  216. my $ehuploader = @$data[0]->{"uploader"};
  217. my $ehtimestamp = @$data[0]->{"posted"};
  218. push( @tags, "uploader:$ehuploader" );
  219. push( @tags, "timestamp:$ehtimestamp" );
  220. }
  221. # Unescape title received from the API as it might contain some HTML characters
  222. $ehtitle = html_unescape($ehtitle);
  223. my $ehtags = join( ', ', @tags );
  224. $logger->info("Sending the following tags to LRR: $ehtags");
  225. return ( $ehtags, $ehtitle );
  226. }
  227. sub get_json_from_EH {
  228. my ( $ua, $gID, $gToken ) = @_;
  229. my $uri = 'https://api.e-hentai.org/api.php';
  230. my $logger = get_plugin_logger();
  231. #Execute the request
  232. my $rep = $ua->post(
  233. $uri => json => {
  234. method => "gdata",
  235. gidlist => [ [ $gID, $gToken ] ],
  236. namespace => 1
  237. }
  238. )->result;
  239. my $textrep = $rep->body;
  240. $logger->debug("E-H API returned this JSON: $textrep");
  241. my $jsonresponse = $rep->json;
  242. if ( exists $jsonresponse->{"error"} ) {
  243. return;
  244. }
  245. return $jsonresponse;
  246. }
  247. 1;