├── .docker └── config │ ├── app │ └── cron │ │ ├── 15min │ │ └── archive.sh │ │ └── hourly │ │ ├── audit.sh │ │ └── thumbnails.sh │ ├── nginx │ └── vhost.conf │ └── php │ └── php.ini ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── TaskRunner.php ├── Vagrantfile ├── apache.conf.linux ├── apache.conf.win32 ├── archives └── .gitignore ├── bootstrap.sh ├── classes ├── ApiHandler.php ├── Auth.php ├── Cache.php ├── Config.php ├── ExArchiver.php ├── ExClient.php ├── ExPage │ ├── Abstract.php │ ├── Archiver.php │ ├── Gallery.php │ └── Index.php ├── Log.php ├── Model │ ├── Abstract.php │ ├── Gallery.php │ └── Image.php ├── QueryHelper.php ├── SphinxQL.php ├── Suggested.php └── Task │ ├── Abstract.php │ ├── Addgallery.php │ ├── Archive.php │ ├── Audit.php │ ├── Cleanup.php │ ├── EditGallery.php │ ├── Extract.php │ ├── ForceAudit.php │ └── Thumbnails.php ├── common.php ├── config.json.linux ├── config.json.win32 ├── db.sql ├── docker-compose.override.yml.dist ├── docker-compose.yml ├── docker-entrypoint-init.d ├── 10-config.sh └── 11-cron.sh ├── init.d.sh ├── lib ├── PHPImageWorkshop │ ├── Core │ │ ├── Exception │ │ │ ├── ImageWorkshopLayerException.php │ │ │ └── ImageWorkshopLibException.php │ │ ├── ImageWorkshopLayer.php │ │ └── ImageWorkshopLib.php │ ├── Exception │ │ ├── ImageWorkshopBaseException.php │ │ └── ImageWorkshopException.php │ ├── Exif │ │ └── ExifOrientations.php │ └── ImageWorkshop.php ├── phpQuery.php ├── phpQuery │ ├── Callback.php │ ├── DOMDocumentWrapper.php │ ├── DOMEvent.php │ ├── bootstrap.example.php │ ├── compat │ │ └── mbstring.php │ ├── phpQueryEvents.php │ ├── phpQueryObject.php │ └── plugins │ │ ├── Scripts.php │ │ ├── Scripts │ │ ├── __config.example.php │ │ ├── example.php │ │ ├── fix_webroot.php │ │ ├── google_login.php │ │ ├── print_source.php │ │ └── print_websafe.php │ │ ├── WebBrowser.php │ │ └── example.php └── rb.php ├── setup ├── Docker-setup.md ├── Linux-Setup.md ├── Userscript-Setup.md ├── Vagrant-Setup.md └── Windows-Setup.md ├── sphinx.conf.linux ├── sphinx.conf.win32 ├── ssl ├── server.crt ├── server.csr └── server.key ├── userscript.user.js └── www ├── api.php ├── css ├── exhen.css └── foundation-icons.css ├── font ├── OpenSans.eot ├── OpenSans.svg ├── OpenSans.ttf ├── OpenSans.woff ├── OpenSans.woff2 ├── foundation-icons.eot ├── foundation-icons.svg ├── foundation-icons.ttf └── foundation-icons.woff ├── img ├── arrow-next.png ├── arrow-prev.png ├── icon.png └── input-clear.png ├── index.html └── js ├── exhen.js ├── jquery.min.js └── jquery.mousewheel.min.js /.docker/config/app/cron/15min/archive.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd /var/www/ && php TaskRunner.php Archive 3 | -------------------------------------------------------------------------------- /.docker/config/app/cron/hourly/audit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd /var/www/ && php TaskRunner.php Audit 3 | -------------------------------------------------------------------------------- /.docker/config/app/cron/hourly/thumbnails.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | cd /var/www/ && php TaskRunner.php Thumbnails 3 | -------------------------------------------------------------------------------- /.docker/config/nginx/vhost.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | root /var/www/www; 5 | index index.php index.html; 6 | 7 | location / { 8 | try_files $uri /index.html$is_args$args; 9 | 10 | if ($request_method = 'OPTIONS') { 11 | add_header 'Access-Control-Allow-Origin' '*'; 12 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 13 | # 14 | # Custom headers and headers various browsers *should* be OK with but aren't 15 | # 16 | add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; 17 | # 18 | # Tell client that this pre-flight info is valid for 20 days 19 | # 20 | add_header 'Access-Control-Max-Age' 1728000; 21 | add_header 'Content-Type' 'text/plain; charset=utf-8'; 22 | add_header 'Content-Length' 0; 23 | return 204; 24 | } 25 | if ($request_method = 'POST') { 26 | add_header 'Access-Control-Allow-Origin' '*'; 27 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 28 | add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; 29 | add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; 30 | } 31 | if ($request_method = 'GET') { 32 | add_header 'Access-Control-Allow-Origin' '*'; 33 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 34 | add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; 35 | add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; 36 | } 37 | } 38 | 39 | location /images { 40 | root /var/www/images; 41 | expires 30d; 42 | access_log off; 43 | } 44 | 45 | location ~* \.(jpg|jpeg|gif|css|png|js|ico|html|eof|woff|ttf)$ { 46 | if (-f $request_filename) { 47 | expires 30d; 48 | access_log off; 49 | } 50 | } 51 | 52 | location ~ \.php$ { 53 | fastcgi_pass app:9000; 54 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 55 | include fastcgi_params; 56 | } 57 | 58 | error_log /var/log/nginx/sf4_error.log; 59 | access_log /var/log/nginx/sf4_access.log; 60 | } 61 | -------------------------------------------------------------------------------- /.docker/config/php/php.ini: -------------------------------------------------------------------------------- 1 | ; General settings 2 | 3 | date.timezone = UTC 4 | xdebug.max_nesting_level=500 5 | short_open_tag = off 6 | memory_limit="512M" 7 | upload_max_filesize="20M" 8 | post_max_size="20M" 9 | 10 | 11 | ; Error reporting optimized for production (http://www.phptherightway.com/#error_reporting) 12 | 13 | display_errors = Off 14 | display_startup_errors = Off 15 | error_reporting = E_ALL 16 | log_errors = On 17 | error_log = /var/log/php-app/error.log 18 | 19 | ; Opcache settings (best on https://www.scalingphpbook.com/best-zend-opcache-settings-tuning-config/) 20 | 21 | opcache.revalidate_freq = 0 22 | opcache.max_accelerated_files = 7963 23 | opcache.memory_consumption = 96 24 | opcache.interned_strings_buffer = 16 25 | opcache.fast_shutdown = 1 26 | 27 | ; Extensions 28 | 29 | #extension=imagick.so 30 | #extension = apcu.so 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | log.txt 2 | images/* 3 | archive/ 4 | temp/* 5 | io/* 6 | .vagrant/ 7 | *.box 8 | .bashrc 9 | config.json 10 | www/config.json 11 | sphinx.conf 12 | .env 13 | .env.example 14 | phpMyAdmin/ 15 | .idea/ 16 | docker-compose.override.yml 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:fpm-alpine 2 | 3 | MAINTAINER development@oguzhanuysal.eu 4 | 5 | ENV CONF_MEMBERID=null 6 | ENV CONF_PASSHASH=null 7 | ENV CONF_ACCESSKEY=ChangeMeIAmNotSecure 8 | ENV DB_HOST=mariadb 9 | ENV DB_USER=dbuser 10 | ENV DB_PASS=pass 11 | ENV DB_NAME=exhen 12 | ENV CONF_TEMPDIR="./tmp" 13 | ENV CONF_ARCHDIR="./archive" 14 | ENV CONF_IMGDIR="./images" 15 | ENV CONF_SQLDSN="mysql:host=$DB_HOST;dbname=$DB_NAME" 16 | ENV CONF_SPHINXDSN="msql:host=sphinx;port=9306;dbname=exhen" 17 | ENV MEMCACHED_DEPS zlib-dev libmemcached-dev cyrus-sasl-dev 18 | ENV GIT_BRANCH=dev 19 | 20 | RUN apk add --no-cache --update libmemcached-libs zlib 21 | RUN set -xe \ 22 | && apk add --no-cache --update --virtual .phpize-deps $PHPIZE_DEPS \ 23 | && apk add --no-cache --update --virtual .memcached-deps $MEMCACHED_DEPS \ 24 | && pecl install memcached \ 25 | && echo "extension=memcached.so" > /usr/local/etc/php/conf.d/20_memcached.ini \ 26 | && rm -rf /usr/share/php7 \ 27 | && rm -rf /tmp/* \ 28 | && apk del .memcached-deps .phpize-deps 29 | 30 | RUN apk add --no-cache --update curl-dev jpeg-dev freetype-dev mysql-client \ 31 | && docker-php-ext-install -j$(nproc) mysqli \ 32 | && docker-php-ext-install -j$(nproc) pdo_mysql \ 33 | && docker-php-ext-install -j$(nproc) curl \ 34 | && docker-php-ext-install -j$(nproc) zip \ 35 | && docker-php-ext-install -j$(nproc) exif \ 36 | && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \ 37 | && docker-php-ext-install -j$(nproc) gd 38 | 39 | # install jq for manilupating config from bash easily 40 | RUN apk add --update jq openssh-client\ 41 | && rm -rf /var/cache/apk/* 42 | 43 | COPY init.d.sh /usr/local/bin/ 44 | COPY . /var/www 45 | WORKDIR /var/www 46 | 47 | RUN chmod +x /var/www/init.d.sh 48 | 49 | CMD ["init.d.sh"] 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ExHentai-Archive 2 | ================ 3 | 4 | A system for crawling, downloading and viewing ExHentai galleries. 5 | 6 | Screenshots 7 | --- 8 | 9 | 10 | What is ExHentai-Archive? 11 | --- 12 | 13 | ExHentai-Archive allows the user to mark galleries on E(x)Hentai for download, the downloading is then done through the built in Gallery Archive function of E(x)Hentai. 14 | 15 | Then all downloaded galleries can be viewed and searched through in the same way you'd navigate E(x)Hentai. 16 | 17 | As long as your local web server is running you can queue any gallery for download with the included userscript adding a button to every gallery thumbnail. 18 | 19 | You can even setup scheduled times you'd like to download these galleries through a little scripting. (see setup guides for more information) 20 | 21 | Setup 22 | --- 23 | 24 | List of setup guides: 25 | 26 | * [Vagrant Setup](https://github.com/Sn0wCrack/ExHentai-Archive/blob/master/setup/Vagrant-Setup.md) - EASIEST SETUP FOR NEW PEOPLE 27 | * [Linux Setup](https://github.com/Sn0wCrack/ExHentai-Archive/blob/master/setup/Linux-Setup.md) - kimoi's guide 28 | * [Windows Setup](https://github.com/Sn0wCrack/ExHentai-Archive/blob/master/setup/Windows-Setup.md) 29 | * [Userscript Setup](https://github.com/Sn0wCrack/ExHentai-Archive/blob/master/setup/Userscript-Setup.md) - Required for all setups 30 | 31 | TODO 32 | --- 33 | 34 | Major - Takes a long amount time. 35 | 36 | Minor - Takes a short amount of time. 37 | 38 | **Major:** 39 | 40 | * Make key navigation better on the Multi Page Viewer. (better page detection) 41 | * Fix Multi Page Viewer on Mobile. 42 | 43 | **Minor:** 44 | 45 | * OSX setup guide. 46 | * Consider reimplementing Touch controls. -------------------------------------------------------------------------------- /TaskRunner.php: -------------------------------------------------------------------------------- 1 | \n"); 22 | printf("List of avaliable Tasks: \n"); 23 | $tasks = array_diff(scandir("./classes/Task"), array("..", ".")); 24 | for ($i = 2; $i < count($tasks) + 2; $i++) { 25 | printf(" * " . $tasks[$i] . "\n"); 26 | } 27 | exit; 28 | } 29 | 30 | if ($web) { 31 | $name = 'Task_'.$task; 32 | } else { 33 | $name = 'Task_'.$argv[1]; 34 | } 35 | if(!class_exists($name) && $task != "Full") { 36 | Log::error(LOG_TAG, 'Failed to load class: %s', $name); 37 | return; 38 | } 39 | 40 | $pidFile = Config::get()->tempDir.DS.$name.'.pid'; 41 | 42 | if (!file_exists(Config::get()->tempDir)) { 43 | mkdir(Config::get()->tempDir); 44 | } 45 | 46 | if(file_exists($pidFile)) { 47 | $pid = file_get_contents($pidFile); 48 | if(processExists($pid)) { 49 | Log::error(LOG_TAG, 'Task already running: %s', $name); 50 | exit; 51 | } 52 | } 53 | 54 | file_put_contents($pidFile, getmypid()); 55 | 56 | if (!$web) { 57 | $opts = $argv; 58 | $opts = array_slice($opts, 2); 59 | } else { 60 | $opts = $web_opts; 61 | } 62 | 63 | if ($web) { 64 | echo "Running Task: " . $name . "
"; 65 | } else { 66 | Log::debug(LOG_TAG, 'Running task %s', $name); 67 | } 68 | 69 | if ($task == "Full") { 70 | $archive = new Task_Archive(); 71 | $archive->run($opts); 72 | 73 | echo "
"; 74 | 75 | $thumbnails = new Task_Thumbnails(); 76 | $thumbnails->run($opts); 77 | 78 | echo "
"; 79 | 80 | $audit = new Task_Audit(); 81 | $audit->run($opts); 82 | 83 | echo "
"; 84 | 85 | $command = Config::get()->indexer->full; 86 | $output = ""; 87 | exec($command . " 2>&1", $output, $ret); 88 | print_r($output); 89 | 90 | echo "
"; 91 | } else { 92 | $task = new $name(); 93 | $task->run($opts); 94 | } 95 | 96 | if ($web) { 97 | echo "Finished Task ". $name . "
"; 98 | } else { 99 | Log::debug(LOG_TAG, 'Finished running task %s', $name); 100 | } 101 | 102 | unlink($pidFile); 103 | 104 | if ($web) { 105 | echo "Finished running task."; 106 | } 107 | 108 | function processExists($pid) { 109 | if(function_exists('posix_kill')) { 110 | return posix_kill($pid, 0); 111 | } 112 | else { 113 | $output = array(); 114 | exec('tasklist /FI "PID eq ' . $pid . '"', $output); 115 | return empty($output); 116 | } 117 | } 118 | 119 | ?> -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure(2) do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://atlas.hashicorp.com/search. 15 | config.vm.box = "debian/jessie64" 16 | 17 | # Disable automatic box update checking. If you disable this, then 18 | # boxes will only be checked for updates when the user runs 19 | # `vagrant box outdated`. This is not recommended. 20 | # config.vm.box_check_update = false 21 | 22 | # Create a forwarded port mapping which allows access to a specific port 23 | # within the machine from a port on the host machine. In the example below, 24 | # accessing "localhost:8080" will access port 80 on the guest machine. 25 | config.vm.network "forwarded_port", guest: 80, host: 8080 26 | config.vm.network "forwarded_port", guest: 443, host: 443 27 | 28 | # Create a private network, which allows host-only access to the machine 29 | # using a specific IP. 30 | # config.vm.network "private_network", ip: "192.168.33.10" 31 | 32 | # Create a public network, which generally matched to bridged network. 33 | # Bridged networks make the machine appear as another physical device on 34 | # your network. 35 | # config.vm.network "public_network" 36 | 37 | # Share an additional folder to the guest VM. The first argument is 38 | # the path on the host to the actual folder. The second argument is 39 | # the path on the guest to mount the folder. And the optional third 40 | # argument is a set of non-required options. 41 | config.vm.synced_folder ".", "/vagrant", type: "virtualbox", :mount_options => [ 'dmode=775', 'fmode=775' ] 42 | 43 | 44 | # Provider-specific configuration so you can fine-tune various 45 | # backing providers for Vagrant. These expose provider-specific options. 46 | # Example for VirtualBox: 47 | # 48 | config.vm.provider "virtualbox" do |vb| 49 | # Display the VirtualBox GUI when booting the machine 50 | # vb.gui = true 51 | 52 | # Customize the amount of memory on the VM: 53 | #vb.memory = "1024" 54 | 55 | config.vbguest.auto_update = true 56 | end 57 | # 58 | # View the documentation for the provider you are using for more 59 | # information on available options. 60 | 61 | # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies 62 | # such as FTP and Heroku are also available. See the documentation at 63 | # https://docs.vagrantup.com/v2/push/atlas.html for more information. 64 | # config.push.define "atlas" do |push| 65 | # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" 66 | # end 67 | 68 | # Enable provisioning with a shell script. Additional provisioners such as 69 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 70 | # documentation for more information about their specific syntax and use. 71 | config.vm.provision "shell" do |s| 72 | s.path = "bootstrap.sh" 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /apache.conf.linux: -------------------------------------------------------------------------------- 1 | 2 | ServerName exhen.localhost 3 | ServerAlias exhen.localhost 4 | ServerAlias exhen.debian 5 | 6 | DocumentRoot /var/www/vhosts/exhen/www 7 | 8 | Options FollowSymLinks ExecCGI 9 | AllowOverride None 10 | Order allow,deny 11 | Require all granted 12 | 13 | 14 | Alias /images /var/www/vhosts/exhen/images 15 | 16 | AllowOverride None 17 | Order allow,deny 18 | Require all granted 19 | 20 | 21 | ErrorLog ${APACHE_LOG_DIR}/exhen-error.log 22 | CustomLog ${APACHE_LOG_DIR}/exhen-access.log combined 23 | 24 | -------------------------------------------------------------------------------- /apache.conf.win32: -------------------------------------------------------------------------------- 1 | 2 | ServerName exhen.localhost 3 | ServerAlias exhen.localhost 4 | ServerAlias exhen.windows 5 | 6 | DocumentRoot C:\path\to\apache\www 7 | 8 | Options FollowSymLinks ExecCGI 9 | AllowOverride None 10 | Order allow,deny 11 | Require all granted 12 | 13 | 14 | Alias /images C:\path\to\apache\www\images 15 | 16 | AllowOverride None 17 | Order allow,deny 18 | Require all granted 19 | 20 | 21 | ErrorLog ${APACHE_LOG_DIR}/exhen-error.log 22 | CustomLog ${APACHE_LOG_DIR}/exhen-access.log combined 23 | -------------------------------------------------------------------------------- /archives/.gitignore: -------------------------------------------------------------------------------- 1 | galleries/ 2 | pages/ 3 | -------------------------------------------------------------------------------- /bootstrap.sh: -------------------------------------------------------------------------------- 1 | # Prevent apt-get from using stdin at all during setup 2 | export DEBIAN_FRONTEND=noninteractive 3 | # Project-specific variables 4 | NEW_HOSTNAME="exhen" 5 | MYSQL_DB="exhen" 6 | MYSQL_HOST="127.0.0.1" 7 | MYSQL_USER="root" # Should be "root" if using localhost mysql host 8 | MYSQL_PASSWORD="changeme" 9 | 10 | # End of configuration --- edit below with caution! 11 | 12 | # Update apt cache 13 | apt-get update 14 | 15 | # Set mysql root password upfront to disable prompt from asking while installing mysql-server package 16 | debconf-set-selections <<< "mysql-server mysql-server/root_password password $MYSQL_PASSWORD" 17 | debconf-set-selections <<< "mysql-server mysql-server/root_password_again password $MYSQL_PASSWORD" 18 | apt-get -y install mysql-server 19 | 20 | # Install the rest of the dependencies 21 | apt-get -y install \ 22 | apache2 \ 23 | php5 \ 24 | php5-mysql \ 25 | php5-mcrypt \ 26 | php5-curl \ 27 | php5-imagick \ 28 | php5-gd \ 29 | php5-memcache \ 30 | mysql-client \ 31 | unixodbc \ 32 | libpq5 \ 33 | memcached \ 34 | p7zip-full 35 | 36 | # Update hostname 37 | echo "127.0.1.1 $NEW_HOSTNAME" >> /etc/hosts 38 | echo "$NEW_HOSTNAME" > /etc/hostname 39 | hostname -F /etc/hostname 40 | 41 | # Make required directories 42 | if [ -d /vagrant/images ] || [ -d /vagrant/temp ]; 43 | then 44 | echo "required directories already exist" 45 | else 46 | mkdir /vagrant/images 47 | mkdir /vagrant/temp 48 | fi 49 | 50 | 51 | # Set ServerName directive on apache globally to suppress warnings on start 52 | if [ -f /etc/apache2/sites-available/server-name.conf ]; 53 | then 54 | echo "server-name.conf exists" 55 | else 56 | mkdir -p /etc/apache2/conf-available && touch /etc/apache2/conf-available/server-name.conf 57 | fi 58 | echo "ServerName $(hostname)" > /etc/apache2/conf-available/server-name.conf 59 | a2enconf server-name 60 | 61 | # Enable apache modules 62 | a2enmod rewrite 63 | a2enmod cgi 64 | a2enmod ssl 65 | a2enmod headers 66 | sudo mkdir -p /etc/apache2/ssl 67 | sudo cp /vagrant/ssl/{server.crt,server.csr,server.key} /etc/apache2/ssl 68 | 69 | # Add apache vhost config for application 70 | cat << 'EOF' > /etc/apache2/sites-available/vagrant.conf 71 | 72 | Header set Access-Control-Allow-Origin "*" 73 | DocumentRoot /vagrant/www 74 | 75 | Options FollowSymLinks ExecCGI 76 | AllowOverride None 77 | Order deny,allow 78 | Require all granted 79 | 80 | 81 | Alias /images /vagrant/images 82 | 83 | Options +Indexes 84 | AllowOverride None 85 | Order deny,allow 86 | Require all granted 87 | 88 | 89 | Alias /vagrant /vagrant 90 | 91 | Options +Indexes +FollowSymLinks +ExecCGI 92 | AllowOverride None 93 | Order deny,allow 94 | Require all granted 95 | 96 | ErrorLog ${APACHE_LOG_DIR}/exhen-error.log 97 | CustomLog ${APACHE_LOG_DIR}/exhen-access.log combined 98 | 99 | 100 | Header set Access-Control-Allow-Origin "*" 101 | 102 | SSLEngine On 103 | SSLCipherSuite ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM:+SSLv3 104 | 105 | SSLCertificateFile /etc/apache2/ssl/server.crt 106 | SSLCertificateKeyFile /etc/apache2/ssl/server.key 107 | 108 | DocumentRoot /vagrant/www 109 | 110 | Options FollowSymLinks ExecCGI 111 | AllowOverride None 112 | Order deny,allow 113 | Require all granted 114 | 115 | 116 | Alias /images /vagrant/images 117 | 118 | Options +Indexes 119 | AllowOverride None 120 | Order deny,allow 121 | Require all granted 122 | 123 | 124 | Alias /vagrant /vagrant 125 | 126 | Options +Indexes +FollowSymLinks +ExecCGI 127 | AllowOverride None 128 | Order deny,allow 129 | Require all granted 130 | 131 | ErrorLog ${APACHE_LOG_DIR}/exhen-error.log 132 | CustomLog ${APACHE_LOG_DIR}/exhen-access.log combined 133 | 134 | EOF 135 | 136 | sudo cat << 'EOF' > /etc/sudoers.d/www-data 137 | www-data ALL=(ALL) NOPASSWD:ALL 138 | EOF 139 | 140 | 141 | 142 | # Disable the default apache vhost and enable our new one 143 | a2dissite 000-default 144 | a2ensite vagrant 145 | 146 | # Add www-data user to the vagrant group 147 | # Allows access to /vagrant shared mount 148 | usermod --append --groups vagrant www-data 149 | 150 | # Reload changes 151 | apache2ctl -k restart 152 | service apache2 restart 153 | service apache2 reload 154 | 155 | # Setup for Sphinx 156 | wget -q http://sphinxsearch.com/files/sphinxsearch_2.2.11-release-1~jessie_amd64.deb 157 | sudo dpkg -i sphinxsearch_2.2.11-release-1~jessie_amd64.deb 158 | rm sphinxsearch_2.2.11-release-1~jessie_amd64.deb 159 | cp /vagrant/sphinx.conf.linux /etc/sphinxsearch/sphinx.conf 160 | sudo mkdir -p /etc/sphinxsearch/log 161 | sudo mkdir -p /var/lib/sphinxsearch/data/exhen/ 162 | 163 | # Kill searchd process 164 | sudo killall -w searchd 165 | 166 | # Setup searchd init daemon 167 | sudo cat << 'EOF' > /etc/init.d/searchd 168 | #!/bin/bash 169 | 170 | case "${1:-''}" in 171 | 'start') 172 | sudo mkdir -p /var/run/sphinxsearch/ 173 | sudo searchd 174 | ;; 175 | 'stop') 176 | sudo searchd --stop 177 | ;; 178 | 'restart') 179 | sudo searchd --stop 180 | sudo mkdir -p /var/run/sphinxsearch/ 181 | sudo searchd 182 | ;; 183 | 'status') 184 | sudo search --status | echo 185 | ;; 186 | *) 187 | echo "Usage: $SELF start|stop|restart" 188 | exit 1 189 | ;; 190 | esac 191 | EOF 192 | 193 | sudo chmod +x /etc/init.d/searchd 194 | sudo update-rc.d searchd defaults 195 | 196 | wget -q https://files.phpmyadmin.net/phpMyAdmin/4.6.3/phpMyAdmin-4.6.3-all-languages.7z 197 | mkdir -p /vagrant/phpMyAdmin/ 198 | 7z x phpMyAdmin-4.6.3-all-languages.7z > /dev/null 199 | cp -a phpMyAdmin-4.6.3-all-languages/. /vagrant/phpMyAdmin 200 | 201 | # Set mysql client creds for automatic login 202 | tee > ~vagrant/.my.cnf <accessKey)) { 22 | if ($setCookie) { 23 | setcookie('baka', $hash, strtotime('+1 year')); 24 | } 25 | } else { 26 | http_response_code(401); 27 | exit; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /classes/Cache.php: -------------------------------------------------------------------------------- 1 | connected = false; 12 | 13 | if (class_exists('Memcache') || class_exists('MemcachePool')) { 14 | $config = Config::get(); 15 | if ($config->memcache) { 16 | if (class_exists('MemcachePool')) { 17 | $memcache = new MemcachePool(); 18 | } else { 19 | $memcache = new Memcache(); 20 | } 21 | 22 | $this->connected = $memcache->addServer($config->memcache->host, $config->memcache->port); 23 | 24 | if ($this->connected) { 25 | $this->memcache = $memcache; 26 | } 27 | } 28 | } 29 | } 30 | 31 | public static function getInstance() 32 | { 33 | if (!(self::$instance instanceof self)) { 34 | self::$instance = new self(); 35 | } 36 | 37 | return self::$instance; 38 | } 39 | 40 | public function cacheConnected() 41 | { 42 | return $this->connected; 43 | } 44 | 45 | public function getObject($objectType, $objectId) 46 | { 47 | if ($this->connected) { 48 | $key = $this->createObjectKey($objectType, $objectId); 49 | $data = $this->memcache->get($key); 50 | if ($data) { 51 | return $data; 52 | } else { 53 | return false; 54 | } 55 | } 56 | } 57 | 58 | public function setObject($objectType, $objectId, $data) 59 | { 60 | if ($this->connected) { 61 | $key = $this->createObjectKey($objectType, $objectId); 62 | $this->memcache->set($key, $data, MEMCACHE_COMPRESSED, 0); 63 | } 64 | } 65 | 66 | public function deleteObject($objectType, $objectId) 67 | { 68 | if ($this->connected) { 69 | $key = $this->createObjectKey($objectType, $objectId); 70 | $this->memcache->delete($key); 71 | } 72 | } 73 | 74 | public function flush() 75 | { 76 | if ($this->connected) { 77 | return $this->memcache->flush(); 78 | } 79 | } 80 | 81 | public function createObjectKey($objectType, $objectId) 82 | { 83 | return sprintf("%s_%d", $objectType, $objectId); 84 | } 85 | 86 | public function __destruct() 87 | { 88 | if ($this->connected) { 89 | $this->memcache->close(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /classes/Config.php: -------------------------------------------------------------------------------- 1 | base; 17 | 18 | $host = gethostname(); 19 | if (!self::processEntry($config, $data, $host)) { 20 | self::processEntry($config, $data, 'default'); 21 | } 22 | 23 | self::$config = $config; 24 | } 25 | 26 | return self::$config; 27 | } 28 | 29 | protected static function processEntry(&$config, $data, $key) 30 | { 31 | if (property_exists($data, $key)) { 32 | if (property_exists($data->$key, 'inherits')) { 33 | self::processEntry($config, $data, $data->$key->inherits); 34 | } 35 | 36 | foreach ($data->$key as $key => $value) { 37 | $config->$key = $value; 38 | } 39 | 40 | return true; 41 | } else { 42 | return false; 43 | } 44 | } 45 | 46 | public static function buildCookie() 47 | { 48 | $cookie = array(); 49 | foreach (self::$config->cookie as $var => $value) { 50 | $cookie[] = $var.'='.$value; 51 | } 52 | 53 | return implode('; ', $cookie); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /classes/ExArchiver.php: -------------------------------------------------------------------------------- 1 | client = new ExClient(); 15 | 16 | $this->config = Config::get(); 17 | $this->cache = Cache::getInstance(); 18 | 19 | $this->archiveDir = $this->config->archiveDir.'/galleries'; 20 | if (!is_dir($this->archiveDir)) { 21 | mkdir($this->archiveDir, 0777, true); 22 | } 23 | 24 | $pagesDir = $this->config->archiveDir.'/pages'; 25 | if (!is_dir($pagesDir)) { 26 | mkdir($pagesDir, 0777, true); 27 | } 28 | } 29 | 30 | public function start($forced_feed = false) 31 | { 32 | $archivedCount = 0; 33 | 34 | //archive unarchived galleries 35 | $unarchived = R::find('gallery', '((archived = 0 and download = 1) or hasmeta = 0) and deleted = 0 and source = 0'); 36 | foreach ($unarchived as $gallery) { 37 | $this->archiveGallery($gallery); 38 | 39 | if ($gallery->archived) { 40 | $archivedCount++; 41 | } 42 | } 43 | 44 | //run through feeds and add new galleries 45 | if ($forced_feed == false) { 46 | $feeds = R::find('feed', 'disabled = 0'); 47 | } else { 48 | $feeds = R::find('feed', 'disabled = 0 and id = :id', [':id' => $forced_feed]); 49 | } 50 | 51 | foreach ($feeds as $feed) { 52 | $page = 0; 53 | 54 | while (true) { 55 | Log::debug(self::LOG_TAG, 'Crawling feed "%s", page %d', $feed->term, $page); 56 | 57 | $params = array(); 58 | 59 | if ($feed->expunged) { 60 | $params['f_sh'] = 1; 61 | } 62 | 63 | $indexHtml = $this->client->index($feed->term, $page, $params); 64 | $indexPage = new ExPage_Index($indexHtml); 65 | 66 | $isLastPage = $indexPage->isLastPage(); 67 | $hasGallery = false; 68 | 69 | $galleries = $indexPage->getGalleries(); 70 | if (count($galleries) == 0) { 71 | break; 72 | } else { 73 | foreach ($galleries as $exGallery) { 74 | $gallery = R::findOne('gallery', 'exhenid = ?', array($exGallery->exhenid)); 75 | if (!$gallery) { 76 | $gallery = R::dispense('gallery'); 77 | $gallery->exhenid = $exGallery->exhenid; 78 | $gallery->hash = $exGallery->hash; 79 | $gallery->name = $exGallery->name; 80 | $gallery->archived = false; 81 | $gallery->feed = $feed; 82 | $gallery->download = $feed->download; 83 | R::store($gallery); 84 | } else { 85 | if ($gallery->feed == $feed && $feed->archived) { 86 | $hasGallery = true; 87 | } 88 | 89 | if ($feed->download && $gallery->deleted == 0) { 90 | $gallery->download = true; 91 | R::store($gallery); 92 | } 93 | } 94 | 95 | if ((!$gallery->archived && $gallery->download) || !$gallery->hasmeta) { 96 | $this->archiveGallery($gallery); 97 | 98 | if ($gallery->archived) { 99 | $archivedCount++; 100 | } 101 | } 102 | } 103 | } 104 | 105 | if ($isLastPage) { 106 | $feed->archived = true; 107 | R::store($feed); 108 | } 109 | 110 | if ($isLastPage || $hasGallery) { 111 | break; 112 | } 113 | 114 | $page++; 115 | } 116 | } 117 | 118 | Log::debug(self::LOG_TAG, 'Archive complete. Archived %d galleries.', $archivedCount); 119 | 120 | if ($archivedCount > 0) { 121 | if (isset(Config::get()->indexer->full)) { 122 | $command = Config::get()->indexer->full; 123 | system($command); 124 | } 125 | } 126 | } 127 | 128 | protected function archiveGallery($gallery) 129 | { 130 | Log::debug(self::LOG_TAG, 'Archiving gallery: #%d', $gallery->exhenid); 131 | 132 | $this->cache->deleteObject('gallery', $gallery->id); 133 | 134 | $galleryHtml = $this->client->gallery($gallery->exhenid, $gallery->hash); 135 | if (!$galleryHtml) { 136 | Log::error(self::LOG_TAG, 'Failed to retrieve page from server'); 137 | return; 138 | } 139 | 140 | $galleryPage = new ExPage_Gallery($galleryHtml); 141 | 142 | // save html 143 | $pagesFile = sprintf('%s/pages/%d.html', $this->config->archiveDir, $gallery->exhenid); 144 | file_put_contents($pagesFile, $galleryHtml); 145 | 146 | //name, jap name, type 147 | $gallery->name = $galleryPage->getName(); 148 | $gallery->origtitle = $galleryPage->getOriginalName(); 149 | $gallery->type = $galleryPage->getType(); 150 | 151 | //tags 152 | $gallery->ownGalleryTag = array(); //remove tags 153 | $tags = $galleryPage->getTags(); 154 | $gallery->addTags($tags); 155 | 156 | //properties 157 | $gallery->ownGalleryproperty = array(); //remove properties 158 | $props = $galleryPage->getProperties(); 159 | $gallery->addProperties($props); 160 | 161 | $gallery->hasmeta = true; 162 | 163 | if ($gallery->download) { 164 | //delete if gallery zip exists (it shouldn't) 165 | $targetFile = $gallery->getArchiveFilepath(); 166 | if (file_exists($targetFile)) { 167 | unlink($targetFile); 168 | } 169 | 170 | Log::debug(self::LOG_TAG, 'Downloading gallery archive'); 171 | 172 | //download archive 173 | $archiverUrl = $galleryPage->getArchiverUrl(); 174 | if (!$archiverUrl) { 175 | Log::error(self::LOG_TAG, 'Failed to find archiver link for gallery: %s (#%d)', $gallery->name, $gallery->exhenid); 176 | return; 177 | } 178 | 179 | $buttonPress = $this->client->buttonPress($archiverUrl); 180 | 181 | if (strpos($buttonPress, 'Insufficient Credits.') !== false) { 182 | Log::error(self::LOG_TAG, 'Insufficient Credits'); 183 | exit; 184 | } 185 | 186 | $archiverPage = new ExPage_Archiver($buttonPress); 187 | 188 | $continueUrl = $archiverPage->getContinueUrl(); 189 | 190 | if ($continueUrl) { 191 | if (preg_match("/\?.*/", $continueUrl)) { 192 | $continueUrl = preg_replace('/\?.*/', '', $continueUrl); 193 | } 194 | $archiveDownloadUrl = $continueUrl.'?start=1'; 195 | 196 | $ret = @copy($archiveDownloadUrl, $targetFile); 197 | 198 | if ($ret) { 199 | $archive = new ZipArchive(); 200 | $ret = $archive->open($targetFile); 201 | if ($ret === true && $archive->status == ZipArchive::ER_OK) { 202 | $gallery->numfiles = $archive->numFiles; 203 | $archive->close(); 204 | 205 | $gallery->filesize = filesize($targetFile); 206 | $gallery->archived = true; 207 | } else { 208 | Log::error(self::LOG_TAG, 'Downloaded file is not an archive for gallery: %s (#%d)', $gallery->name, $gallery->exhenid); 209 | } 210 | } else { 211 | Log::error(self::LOG_TAG, 'Failed to download archive for gallery: %s (#%d)', $gallery->name, $gallery->exhenid); 212 | } 213 | } else { 214 | Log::error(self::LOG_TAG, 'Failed to find archive link for gallery: %s (#%d) - low GP?', $gallery->name, $gallery->exhenid); 215 | } 216 | } 217 | 218 | R::store($gallery); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /classes/ExClient.php: -------------------------------------------------------------------------------- 1 | $page); 12 | 13 | if (is_array($extraParams)) { 14 | $params = array_merge($params, $extraParams); 15 | } 16 | 17 | if ($search) { 18 | $params = array_merge($params, array( //todo - move to config 19 | 'f_doujinshi' => 1, 20 | 'f_manga' => 1, 21 | 'f_artistcg' => 0, 22 | 'f_gamecg' => 0, 23 | 'f_non-h' => 0, 24 | 'f_search' => $search 25 | )); 26 | } 27 | 28 | $url = self::BASE_URL . "/?" . http_build_query($params); 29 | return $this->exec($url); 30 | } 31 | 32 | public function tagSearch($search = '', $page = 0) 33 | { 34 | $url = sprinf("%s/tag/%s/%d", self::BASE_URL, $search, $page); 35 | return $this->exec($url); 36 | } 37 | 38 | public function gallery($id, $hash, $thumbPage = 0) 39 | { 40 | $url = sprintf('%s/g/%d/%s/?p=%d', self::BASE_URL, $id, $hash, $thumbPage); 41 | return $this->exec($url); 42 | } 43 | 44 | public function exec($url) 45 | { 46 | $this->ctr++; 47 | if ($this->ctr > 4) { 48 | sleep(3); 49 | $this->ctr = 0; 50 | } 51 | 52 | $ch = curl_init($url); 53 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 54 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 55 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 56 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 57 | curl_setopt($ch, CURLOPT_MAXREDIRS, 300); 58 | 59 | $cookie = Config::buildCookie(); 60 | curl_setopt($ch, CURLOPT_COOKIE, $cookie); 61 | curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36'); 62 | 63 | $ret = curl_exec($ch); 64 | curl_close($ch); 65 | 66 | if (strpos($ret, 'Your IP address has been temporarily banned for using automated mirroring/harvesting software and/or failing to heed the overload warning.') !== false) { 67 | printf("Banned. Waiting a minute before retrying.\n"); 68 | sleep(60*60); 69 | return $this->exec($url); 70 | } 71 | 72 | if (strpos($ret, 'You are opening pages too fast') !== false) { 73 | printf("Warned. Waiting 10 seconds before retying.\n"); 74 | sleep(60*10); 75 | return $this->exec($url); 76 | } 77 | 78 | return $ret; 79 | } 80 | 81 | public function buttonPress($url) 82 | { 83 | if (strpos($this->exec($url), "dlcheck") !== false) { 84 | $this->ctr++; 85 | if ($this->ctr > 4) { 86 | sleep(3); 87 | $this->ctr = 0; 88 | } 89 | 90 | $ch = curl_init($url); 91 | curl_setopt($ch, CURLOPT_POST, true); 92 | $post = array("dlcheck" => "true", 93 | "dltype" => "org"); 94 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post)); 95 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 96 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 97 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 98 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 99 | 100 | $cookie = Config::buildCookie(); 101 | curl_setopt($ch, CURLOPT_COOKIE, $cookie); 102 | curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36'); 103 | 104 | $ret = curl_exec($ch); 105 | curl_close($ch); 106 | 107 | return $ret; 108 | } else { 109 | Log::debug("ExClient", "dlcheck bypassed already"); 110 | } 111 | return ""; 112 | } 113 | 114 | public function invalidateForm($url) 115 | { 116 | $ch = curl_init($url); 117 | curl_setopt($ch, CURLOPT_POST, true); 118 | curl_setopt($ch, CURLOPT_POSTFIELDS, "invalidate_sessions=1"); 119 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 120 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 121 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 122 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 123 | 124 | $cookie = Config::buildCookie(); 125 | curl_setopt($ch, CURLOPT_COOKIE, $cookie); 126 | curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36'); 127 | 128 | $ret = curl_exec($ch); 129 | curl_close($ch); 130 | } 131 | 132 | public function getArchiveFileSize($url) 133 | { 134 | $ch = curl_init($url); 135 | curl_setopt($ch, CURLOPT_NOBODY, true); 136 | curl_setopt($ch, CURLOPT_HEADER, true); 137 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 138 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); 139 | curl_setopt($ch, CURLOPT_FAILONERROR, true); 140 | 141 | $cookie = Config::buildCookie(); 142 | curl_setopt($ch, CURLOPT_COOKIE, $cookie); 143 | curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36'); 144 | 145 | $ret = curl_exec($ch); 146 | curl_close($ch); 147 | 148 | $result = "unknown"; 149 | 150 | if ($ret) { 151 | $content_length = "unknown"; 152 | $status = "unknown"; 153 | 154 | if (preg_match( "/^HTTP\/1\.[01] (\d\d\d)/", $ret, $matches )) { 155 | $status = (int)$matches[1]; 156 | } 157 | 158 | if (preg_match( "/Content-Length: (\d+)/", $ret, $matches )) { 159 | $content_length = (int)$matches[1]; 160 | } 161 | 162 | if ($status == 200 || ($status > 300 && $status <= 308)) { 163 | $result = $content_length; 164 | } 165 | } 166 | 167 | return $result; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /classes/ExPage/Abstract.php: -------------------------------------------------------------------------------- 1 | doc = phpQuery::newDocumentHTML($html); 11 | } 12 | 13 | public function getDocument() 14 | { 15 | return $this->doc; 16 | } 17 | 18 | public function find($selector) 19 | { 20 | return $this->doc->find($selector); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /classes/ExPage/Archiver.php: -------------------------------------------------------------------------------- 1 | find('#continue a'); 8 | if (count($elem) >= 1) { 9 | return $elem->attr('href'); 10 | } else { 11 | return false; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /classes/ExPage/Gallery.php: -------------------------------------------------------------------------------- 1 | find('h1#gn, div#taglist, div#gdd'); 8 | return count($result) >= 3; 9 | } 10 | 11 | public function getName() 12 | { 13 | return $this->find('h1#gn')->text(); 14 | } 15 | 16 | public function getOriginalName() 17 | { 18 | return $this->find('h1#gj')->text(); 19 | } 20 | 21 | public function getType() 22 | { 23 | return $this->find('div#gdc .cs')->text(); 24 | } 25 | 26 | public function getThumbnailUrl() 27 | { 28 | $style = $this->find('div#gd1 div')->attr('style'); 29 | 30 | preg_match('/url\((.*?)\)/', $style, $matches); 31 | 32 | return $matches[1]; 33 | } 34 | 35 | public function getTags() 36 | { 37 | $ret = array(); 38 | 39 | $tagRows = $this->find('#taglist tr'); 40 | foreach ($tagRows as $i => $tagRowElem) { 41 | $tagRow = $tagRows->eq($i); 42 | 43 | $tagNamespace = $tagRow->find('td:first-child')->text(); 44 | $tagNamespace = trim($tagNamespace, ':'); 45 | 46 | $tags = array(); 47 | $tagLinks = $tagRow->find('a'); 48 | foreach ($tagLinks as $x => $tagLinkElem) { 49 | $tagLink = $tagLinks->eq($x); 50 | $tags[] = $tagLink->text(); 51 | } 52 | 53 | $ret[$tagNamespace] = $tags; 54 | } 55 | 56 | return $ret; 57 | } 58 | 59 | public function getProperties() 60 | { 61 | $ret = array(); 62 | 63 | $attrs = $this->find('td.gdt1'); 64 | foreach ($attrs as $i => $attrElem) { 65 | $attr = $attrs->eq($i); 66 | $propName = trim($attr->text(), ':'); 67 | $propValue = $attr->next()->text(); 68 | 69 | $ret[$propName] = $propValue; 70 | } 71 | 72 | return $ret; 73 | } 74 | 75 | public function getArchiverUrl() 76 | { 77 | $elem = $this->find('a[onclick*="archiver.php"]'); 78 | if (count($elem) > 0) { 79 | preg_match("~(https://exhentai.org/archiver.php.*)'~", $elem->attr('onclick'), $matches); 80 | if (count($matches) >= 2) { 81 | return $matches[1]; 82 | } 83 | } 84 | 85 | return false; 86 | } 87 | 88 | public function getNewestVersion() 89 | { 90 | $elem = $this->find('div#gnd a:last'); 91 | if (count($elem) === 1) { 92 | preg_match("~https://exhentai.org/g/(\d*)/(\w*)/~", $elem->attr('href'), $matches); 93 | 94 | $ret =new stdClass(); 95 | $ret->exhenid = $matches[1]; 96 | $ret->hash = $matches[2]; 97 | 98 | return $ret; 99 | } 100 | 101 | return false; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /classes/ExPage/Index.php: -------------------------------------------------------------------------------- 1 | find('td.ptds + td.ptdd')) > 0); 8 | } 9 | 10 | public function getGalleries() 11 | { 12 | $ret = array(); 13 | 14 | $links = $this->find('.itg.gld .gl1t a'); 15 | foreach ($links as $linkElem) { 16 | $gallery = new stdClass(); 17 | 18 | $link = pq($linkElem); 19 | $gallery->name = $link->text(); 20 | 21 | preg_match("~https://exhentai.org/g/(\d*)/(\w*)/~", $link->attr('href'), $matches); 22 | 23 | if (isset($matches[1]) && isset($matches[2])) { 24 | $gallery->exhenid = $matches[1]; 25 | $gallery->hash = $matches[2]; 26 | 27 | $ret[] = $gallery; 28 | } 29 | } 30 | 31 | return $ret; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /classes/Log.php: -------------------------------------------------------------------------------- 1 | 0) { 12 | $params = array_merge(array($message), $vargs); 13 | $message = call_user_func_array('sprintf', $params); 14 | } 15 | 16 | $fmt = "%s: [%s][%s] %s\n"; 17 | 18 | if (substr(PHP_SAPI, 0, 3) != 'cli') 19 | $fmt = nl2br($fmt); 20 | 21 | printf($fmt, strtoupper($level), date('Y-m-d H:i:s'), $tag, $message); 22 | } 23 | 24 | public static function debug($tag, $message) 25 | { 26 | $args = array_merge(array(self::LOG_DEBUG), func_get_args()); 27 | call_user_func_array('Log::add', $args); 28 | } 29 | 30 | public static function error($tag, $message) 31 | { 32 | $args = array_merge(array(self::LOG_ERROR), func_get_args()); 33 | call_user_func_array('Log::add', $args); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /classes/Model/Abstract.php: -------------------------------------------------------------------------------- 1 | sql('select *, crc32(to_string(id + :seed)) as rnd from galleries'); 22 | $query->addParams(array('seed' => $randomSeed)); 23 | } elseif ($order === 'weight') { 24 | $query->sql('select *, weight() as ranked_weight from galleries'); 25 | } else { 26 | $query->sql('select * from galleries'); 27 | } 28 | 29 | $query->sql('where deleted = 0'); 30 | 31 | if (!$unarchived) { 32 | $query->sql('and archived = 1'); 33 | } 34 | 35 | if ($read !== false) { 36 | $query->sql('and read = :read') 37 | ->addParams(array('read' => $read)); 38 | } 39 | 40 | if ($color !== false) { 41 | $query->sql('and color = :color') 42 | ->addParams(array('color' => $color)); 43 | } 44 | 45 | if ($search) { 46 | $search = SphinxQL::halfEscapeMatch($search); 47 | 48 | $query->sql('and match(:match)') 49 | ->addParams(array('match' => $search)); 50 | } 51 | 52 | if ($order === 'added') { 53 | $query->sql('order by added desc'); 54 | } elseif ($order === 'random') { 55 | $query->sql('order by rnd asc'); 56 | } elseif ($order === 'weight') { 57 | $query->sql('order by ranked_weight desc, posted desc'); 58 | } else { 59 | $query->sql('order by posted desc'); 60 | } 61 | 62 | if ($page >= 0 && $pagesize) { 63 | $query->sql('limit :offset, :limit') 64 | ->addParams(array( 65 | 'offset' => (int)($page * $pagesize), 66 | 'limit' => (int)$pagesize 67 | )); 68 | } 69 | 70 | if ($order === 'weight') { 71 | $query->sql('option ranker = wordcount,'); 72 | } else { 73 | $query->sql('option ranker = none,'); 74 | } 75 | 76 | $query->sql('max_matches = 500000'); 77 | 78 | $result = SphinxQL::query($query->getSql(), $query->getParams()); 79 | $meta = SphinxQL::getMeta(); 80 | 81 | return array('result' => $result, 'meta' => $meta); 82 | } 83 | 84 | // this really needs redoing.. 85 | public function getArchiveFilepath() 86 | { 87 | if ($this->source == self::SOURCE_EXHENTAI) { 88 | return sprintf('%s/galleries/%d.zip', Config::get()->archiveDir, $this->exhenid); 89 | } else { 90 | return sprintf('%s/galleries/%d-%d.zip', Config::get()->archiveDir, $this->source, $this->id); 91 | } 92 | } 93 | 94 | public function getImageFilepath($index) 95 | { 96 | $zipPath = $this->getArchiveFilepath(); 97 | if (file_exists($zipPath)) { 98 | $files = array(); 99 | 100 | $zip = new PharData($zipPath); 101 | if ($zip) { 102 | foreach ($zip as $file) { 103 | $files[] = basename($file); 104 | } 105 | 106 | natcasesort($files); 107 | $files = array_values($files); //strip keys 108 | 109 | if (array_key_exists($index, $files)) { 110 | return sprintf('phar://%s/%s', $zipPath, $files[$index]); 111 | } 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | public function getImageBean($index) 119 | { 120 | return R::findOne('galleryimage', 'gallery_id = ? and galleryimage.index = ?', array($this->id, $index)); 121 | } 122 | 123 | public function getThumbnail($index, $type, $create = true) 124 | { 125 | if ($this->archived) { 126 | $link = $this->unbox()->withCondition('gallery_thumb.index = ? and gallery_thumb.type = ?', array($index, $type))->via('gallery_thumb')->ownGalleryThumb; 127 | 128 | if (count($link) > 0) { 129 | $link = array_pop($link); 130 | 131 | return $link->image; 132 | } elseif ($create) { 133 | $inFile = $this->getImageFilepath($index); 134 | 135 | if (file_exists($inFile)) { 136 | $resizedFilename = sprintf('resized_gallery_%d_%d.jpg', $this->id, $index); 137 | 138 | $layer = ImageWorkshop::initFromPath($inFile); 139 | 140 | if ($type == self::THUMB_LARGE) { 141 | $layer->resizeInPixel(350, null, true); 142 | } elseif ($type == self::THUMB_SMALL) { 143 | $layer->resizeInPixel(140, null, true); 144 | } else { 145 | return false; 146 | } 147 | 148 | $tempDir = Config::get()->tempDir; 149 | $layer->save($tempDir, $resizedFilename, true, null, 95); 150 | 151 | $outFile = $tempDir.DS.$resizedFilename; 152 | $image = Model_Image::importFromFile($outFile); 153 | 154 | unlink($outFile); 155 | 156 | $link = $image->unbox()->link('gallery_thumb', array('index' => $index, 'type' => $type))->gallery = $this->unbox(); 157 | R::store($image); 158 | 159 | return $image; 160 | } 161 | } 162 | } 163 | 164 | return false; 165 | } 166 | 167 | public function dispense() 168 | { 169 | $this->added = date('Y-m-d H:i:s'); 170 | } 171 | 172 | public function update() 173 | { 174 | $this->updated = date('Y-m-d H:i:s'); 175 | } 176 | 177 | public function getRoundedId() 178 | { 179 | return round(($this->exhenid - 500), -4); 180 | } 181 | 182 | public static function getStats() 183 | { 184 | $sql = 185 | 'select 186 | count(id) as count_total, 187 | sum(filesize) as filesize, 188 | round(sum(filesize) / pow(1024, 3), 2) as filesize_gb, 189 | (select count(id) from gallery where archived = 1) as count_archived, 190 | (select count(id) from gallery where deleted >= 1) as count_deleted, 191 | round((sum(filesize) / (select count(id) from gallery where archived = 1)) / pow(1024, 2), 2) as filesize_average_mb, 192 | sum(round(round(filesize / pow(1024, 2), 2) * 41.9435018158)) as gp_total, 193 | round(sum(round(filesize / pow(1024, 2), 2) * 41.9435018158) / (select count(id) from gallery where archived = 1)) as gp_average 194 | from gallery 195 | limit 1'; 196 | 197 | $stats = R::getRow($sql); 198 | 199 | return $stats; 200 | } 201 | 202 | public function hasTag($ns, $tag) 203 | { 204 | $tagBean = R::findOne('tag', 'name = ?', array($tag)); 205 | $nsBean = R::findOne('tagnamespace', 'name = ?', array($ns)); 206 | 207 | if (!$tagBean || !$nsBean) { 208 | return false; 209 | } 210 | 211 | $result = $this->unbox()->withCondition('tag_id = ? and namespace_id = ?', array($tagBean->id, $nsBean->id))->ownGalleryTag; 212 | return (count($result) > 0); 213 | } 214 | 215 | public function addTags($tagList) 216 | { 217 | foreach ($tagList as $tagNamespace => $tags) { 218 | $ns = R::findOne('tagnamespace', 'name = ?', array($tagNamespace)); 219 | if (!$ns) { 220 | $ns = R::dispense('tagnamespace'); 221 | $ns->name = $tagNamespace; 222 | R::store($ns); 223 | } 224 | 225 | foreach ($tags as $tagName) { 226 | $tag = R::findOne('tag', 'name = ?', array($tagName)); 227 | if (!$tag) { 228 | $tag = R::dispense('tag'); 229 | $tag->name = $tagName; 230 | R::store($tag); 231 | } 232 | 233 | $this->unbox()->link('gallery_tag', array('namespace' => $ns))->tag = $tag; 234 | } 235 | } 236 | } 237 | 238 | public function addProperties($props) 239 | { 240 | foreach ($props as $name => $value) { 241 | $galleryAttr = R::dispense('galleryproperty'); 242 | $galleryAttr->name = $name; 243 | $galleryAttr->value = $value; 244 | $galleryAttr->gallery = $this->unbox(); 245 | R::store($galleryAttr); 246 | 247 | if ($name === 'Posted') { 248 | $this->unbox()->posted = $value; 249 | } 250 | } 251 | } 252 | 253 | public static function addGallery($gid, $hash) 254 | { 255 | $gallery = R::findOne('gallery', 'exhenid = ?', array($gid)); 256 | if (!$gallery) { 257 | $gallery = R::dispense('gallery'); 258 | $gallery->exhenid = $gid; 259 | $gallery->hash = $hash; 260 | R::store($gallery); 261 | } elseif (!$gallery->download) { 262 | $gallery->download = true; 263 | $gallery->added = date('Y-m-d H:i:s'); 264 | R::store($gallery); 265 | } 266 | 267 | return $gallery; 268 | } 269 | 270 | public function exportTags() 271 | { 272 | $tagLinks = $this->ownGalleryTag; 273 | 274 | R::preload($tagLinks, array('namespace' => 'tagnamespace')); 275 | 276 | $export = array(); 277 | foreach ($tagLinks as $link) { 278 | if (!array_key_exists($link->namespace->name, $export)) { 279 | $export[$link->namespace->name] = array(); 280 | } 281 | 282 | $export[$link->namespace->name][] = $link->tag->name; 283 | } 284 | 285 | return $export; 286 | } 287 | 288 | public function exportSource() 289 | { 290 | return $this->source; 291 | } 292 | 293 | public function exportFeed() 294 | { 295 | return $this->feed_id; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /classes/Model/Image.php: -------------------------------------------------------------------------------- 1 | getFilepath(); 8 | if (file_exists($filepath)) { 9 | unlink($filepath); 10 | } 11 | } 12 | 13 | public function getFilepath() 14 | { 15 | if ($this->id) { 16 | return realpath(Config::get()->imagesDir).DS.str_pad($this->id, 6, '0', STR_PAD_LEFT).'.'.$this->type; 17 | } 18 | } 19 | 20 | public function getUrl() 21 | { 22 | if ($this->id) { 23 | return '/images/'.str_pad($this->id, 6, '0', STR_PAD_LEFT).'.'.$this->type; 24 | } 25 | } 26 | 27 | public static function importFromFile($filepath) 28 | { 29 | if (!file_exists($filepath)) { 30 | throw new Exception('Input file does not exist'); 31 | } 32 | 33 | $ret = getimagesize($filepath); 34 | if (!$ret) { 35 | throw new Exception('Input file is invalid'); 36 | } 37 | 38 | list($width, $height, $type, $attr) = $ret; 39 | 40 | $image = R::dispense('image'); 41 | R::store($image); 42 | $image->width = $width; 43 | $image->height = $height; 44 | $image->type = image_type_to_extension($type, false); 45 | $image->filename = pathinfo($filepath, PATHINFO_BASENAME); 46 | R::store($image); 47 | 48 | $ret = copy($filepath, $image->getFilepath()); 49 | if (!$ret) { 50 | R::trash($image); 51 | throw new Exception('Failed to copy image'); 52 | } 53 | 54 | return $image; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /classes/QueryHelper.php: -------------------------------------------------------------------------------- 1 | sql[] = $sql; 11 | 12 | return $this; 13 | } 14 | 15 | public function addParams($params) 16 | { 17 | $this->params = array_merge($this->params, $params); 18 | 19 | return $this; 20 | } 21 | 22 | public function getSql() 23 | { 24 | return implode(' ', $this->sql); 25 | } 26 | 27 | public function getParams() 28 | { 29 | return $this->params; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /classes/SphinxQL.php: -------------------------------------------------------------------------------- 1 | sphinxql->dsn, $config->sphinxql->user, $config->sphinxql->pass, true); 13 | } 14 | 15 | public static function getInstance() 16 | { 17 | if (!self::$instance) { 18 | self::$instance = new self(); 19 | } 20 | 21 | return self::$instance; 22 | } 23 | 24 | public static function query($sql, $params = array()) 25 | { 26 | self::getInstance(); 27 | 28 | R::selectDatabase('sphinx'); 29 | 30 | $result = R::getAll($sql, $params); 31 | 32 | R::selectDatabase('default'); 33 | 34 | return $result; 35 | } 36 | 37 | public static function getMeta() 38 | { 39 | self::getInstance(); 40 | 41 | R::selectDatabase('sphinx'); 42 | 43 | $result = R::getAll('show meta'); 44 | 45 | $obj = new stdClass(); 46 | foreach ($result as $param) { 47 | $name = $param['Variable_name']; 48 | $value = $param['Value']; 49 | 50 | if (preg_match('/^(.*)\[\d*\]$/', $name, $matches) === 1) { 51 | $name = $matches[1]; 52 | 53 | if (!isset($obj->$name) || !is_array($obj->$name)) { 54 | $obj->$name = array(); 55 | } 56 | 57 | array_push($obj->$name, $value); 58 | } else { 59 | $obj->$name = $value; 60 | } 61 | } 62 | 63 | R::selectDatabase('default'); 64 | 65 | return $obj; 66 | } 67 | 68 | public static function getIds($result) 69 | { 70 | $ids = array(); 71 | foreach ($result as $row) { 72 | $ids[] = $row['id']; 73 | } 74 | 75 | return $ids; 76 | } 77 | 78 | /** 79 | * Escapes the query for the MATCH() function 80 | * Allows some of the control characters to pass through for use with a search field: -, |, " 81 | * It also does some tricks to wrap/unwrap within " the string and prevents errors 82 | * 83 | * @param string $string The string to escape for the MATCH 84 | * 85 | * @return string The escaped string 86 | */ 87 | public static function halfEscapeMatch($string) 88 | { 89 | $from_to = array( 90 | '\\' => '\\\\', 91 | '(' => '\(', 92 | ')' => '\)', 93 | '!' => '\!', 94 | '@' => '\@', 95 | '~' => '\~', 96 | '&' => '\&', 97 | '/' => '\/', 98 | '^' => '\^', 99 | '$' => '\$', 100 | '=' => '\=', 101 | ); 102 | 103 | $string = str_replace(array_keys($from_to), array_values($from_to), $string); 104 | 105 | // this manages to lower the error rate by a lot 106 | if (substr_count($string, '"') % 2 !== 0) { 107 | $string .= '"'; 108 | } 109 | 110 | $from_to_preg = array( 111 | "'\"([^\s]+)-([^\s]*)\"'" => "\\1\-\\2", 112 | "'([^\s]+)-([^\s]*)'" => "\"\\1\-\\2\"" 113 | ); 114 | 115 | $string = mb_strtolower(preg_replace(array_keys($from_to_preg), array_values($from_to_preg), $string)); 116 | 117 | return $string; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /classes/Suggested.php: -------------------------------------------------------------------------------- 1 | $match)); 23 | 24 | $data = array(); 25 | foreach ($result as $row) { 26 | $data[] = $row['keyword']; 27 | } 28 | 29 | return $data; 30 | } else { 31 | return array(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /classes/Task/Abstract.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /classes/Task/Addgallery.php: -------------------------------------------------------------------------------- 1 | name = $galleryName; 24 | $gallery->archived = 1; 25 | $gallery->added = date('Y-m-d H:i:s'); 26 | $gallery->posted = date('Y-m-d H:i:s'); 27 | $gallery->source = Model_Gallery::SOURCE_MANUAL; 28 | 29 | $archive = new ZipArchive(); 30 | $ret = $archive->open($inputFile); 31 | if($ret === true && $archive->status == ZipArchive::ER_OK) { 32 | $gallery->numfiles = $archive->numFiles; 33 | $archive->close(); 34 | 35 | $gallery->filesize = filesize($inputFile); 36 | $gallery->archived = true; 37 | } 38 | else { 39 | Log::error(self::LOG_TAG, 'Input file is not a valid zip'); 40 | } 41 | 42 | R::store($gallery); 43 | 44 | $dest = $gallery->getArchiveFilepath(); 45 | $ret = copy($inputFile, $dest); 46 | 47 | if(!$ret) { 48 | Log::error(self::LOG_TAG, 'Failed to copy file to dest: %s', $dest); 49 | R::trash($gallery); 50 | return; 51 | } 52 | 53 | if(isset(Config::get()->indexer->full)) { 54 | $command = Config::get()->indexer->full; 55 | system($command); 56 | } 57 | } 58 | 59 | } 60 | 61 | 62 | ?> 63 | -------------------------------------------------------------------------------- /classes/Task/Archive.php: -------------------------------------------------------------------------------- 1 | start((int)$options[1]); 12 | } else { 13 | $archiver->start(); 14 | } 15 | } else { 16 | $archiver->start(); 17 | } 18 | 19 | } 20 | 21 | } 22 | 23 | 24 | ?> 25 | -------------------------------------------------------------------------------- /classes/Task/Audit.php: -------------------------------------------------------------------------------- 1 | client = new ExClient(); 11 | 12 | while(true) { 13 | 14 | $interval = isset($options[0]) ? $options[0] : "1 year"; 15 | 16 | $galleries = R::find('gallery', 17 | 'archived = 1 and deleted = 0 and source = 0 and'. 18 | '((added <= date_sub(date(now()), interval 3 day) and'. // if added more than 3 days ago... 19 | '(posted >= date_sub(date(now()), interval ' . $interval . ') or (lastaudit is null)) and'. // and if the gallery is more than input old but NOT if it's unaudited 20 | '(lastaudit is null or lastaudit <= date_sub(date(now()), interval 7 day))) or'. // ...and not yet audited, or audited more than 7 days ago 21 | '((added >= date_sub(date(now()), interval 7 day) and added <= date_sub(date(now()), interval 1 day)) and'. // OR, added less than 7 days ago (but more than 24 hours ago)... 22 | '(lastaudit is null or lastaudit <= date_sub(date(now()), interval 1 day))))'. // ...and not yet audited, or audited more than 1 day ago 23 | 'order by posted desc limit 100'); 24 | 25 | if(count($galleries) === 0) { 26 | break; 27 | } 28 | 29 | foreach($galleries as $gallery) { 30 | $this->audit($gallery); 31 | } 32 | } 33 | 34 | if(isset(Config::get()->indexer->full)) { 35 | $command = Config::get()->indexer->full; 36 | system($command); 37 | } 38 | } 39 | 40 | protected function audit($gallery) { 41 | Log::debug(self::LOG_TAG, 'Auditing gallery: #%d - %s', $gallery->exhenid, $gallery->name); 42 | 43 | $galleryHtml = $this->client->gallery($gallery->exhenid, $gallery->hash); 44 | 45 | // Galleries that are removed now return a 404, so we have to leave them as audited completely for now. 46 | if(!$galleryHtml) { 47 | $gallery->lastaudit = date('Y-m-d H:i:s'); //gallery was probably deleted, so mark it as audited for now 48 | R::store($gallery); 49 | 50 | Log::error(self::LOG_TAG, 'Gallery was either removed or was invalid.'); 51 | return; 52 | } 53 | 54 | $galleryPage = new ExPage_Gallery($galleryHtml); 55 | if(!$galleryPage->isValid()) { 56 | $gallery->lastaudit = date('Y-m-d H:i:s'); //gallery was probably deleted, so mark it as audited for now 57 | R::store($gallery); 58 | 59 | Log::error(self::LOG_TAG, 'Gallery was either removed or was invalid.'); 60 | return; 61 | } 62 | 63 | $childGallery = $galleryPage->getNewestVersion(); 64 | if($childGallery) { 65 | 66 | $childHtml = $this->client->gallery($childGallery->exhenid, $childGallery->hash); 67 | $childPage = new ExPage_Gallery($childHtml); 68 | 69 | if ($childPage->isValid()) { 70 | Log::debug(self::LOG_TAG, 'New gallery found for gallery (%d): #%d - %s', $childGallery->exhenid, $gallery->exhenid, $gallery->name); 71 | 72 | Model_Gallery::addGallery($childGallery->exhenid, $childGallery->hash); 73 | 74 | $gallery->deleted = 1; 75 | } 76 | 77 | } 78 | else { 79 | $newTags = $galleryPage->getTags(); 80 | $oldTags = $gallery->exportTags(); 81 | 82 | if(count($newTags) > 0) { 83 | $diff = self::tagsDiff($oldTags, $newTags); 84 | 85 | if(count($diff) > 0) { 86 | $humanDiff = array(); 87 | foreach($diff as $ns => $tags) { 88 | foreach($tags as $tag) { 89 | $humanDiff[] = $ns.':'.$tag; 90 | } 91 | } 92 | 93 | $humanDiff = implode(', ', $humanDiff); 94 | 95 | Log::debug(self::LOG_TAG, 'Different tags found for gallery: #%d - %s (%s)', $gallery->exhenid, $gallery->name, $humanDiff); 96 | $gallery->ownGalleryTag = array(); 97 | $gallery->addTags($newTags); 98 | 99 | $cache = Cache::getInstance(); 100 | $cache->deleteObject('gallery', $gallery->id); 101 | } 102 | } 103 | } 104 | 105 | $gallery->lastaudit = date('Y-m-d H:i:s'); 106 | 107 | if ($gallery->deleted == 1) { 108 | $gallery->lastaudit = null; 109 | } 110 | 111 | R::store($gallery); 112 | } 113 | 114 | static function tagsDiff($tags1, $tags2){ 115 | $ret = array(); 116 | 117 | foreach ($tags1 as $ns => $tags) { 118 | if(!array_key_exists($ns, $tags2)) { 119 | $ret[$ns] = $tags; 120 | continue; 121 | } 122 | 123 | foreach($tags as $index => $tag) { 124 | if(!in_array($tag, $tags2[$ns])) { 125 | if(!array_key_exists($ns, $ret) || !is_array($ret[$ns])) { 126 | $ret[$ns] = array(); 127 | } 128 | 129 | $ret[$ns][] = $tag; 130 | } 131 | else { 132 | $pos = array_search($tag, $tags2[$ns]); 133 | unset($tags2[$ns][$pos]); 134 | } 135 | } 136 | 137 | if(count($tags2[$ns]) > 0) { 138 | if(!array_key_exists($ns, $ret) || !is_array($ret[$ns])) { 139 | $ret[$ns] = array(); 140 | } 141 | 142 | $ret[$ns] = array_merge($ret[$ns], $tags2[$ns]); 143 | } 144 | 145 | unset($tags2[$ns]); 146 | } 147 | 148 | $ret = array_merge($ret, $tags2); 149 | 150 | return $ret; 151 | } 152 | 153 | } 154 | 155 | 156 | ?> 157 | -------------------------------------------------------------------------------- /classes/Task/Cleanup.php: -------------------------------------------------------------------------------- 1 | 0) { 15 | $count = 0; 16 | Log::debug(self::LOG_TAG, 'NULL entries found, deleting them now.'); 17 | foreach($entries as $entry) { 18 | $book = R::load('galleryproperty', $entry["id"]); 19 | R::trash($book); 20 | $count++; 21 | } 22 | Log::debug(self::LOG_TAG, 'All NULL entries deleted. Total deleted was %d', $count); 23 | } 24 | else { 25 | Log::debug(self::LOG_TAG, 'No NULL galleryproperty entries found.'); 26 | } 27 | 28 | Log::debug(self::LOG_TAG, 'Checking for NULL gallery_tag entries'); 29 | $entries = R::getAll('SELECT * FROM gallery_tag WHERE gallery_id IS NULL'); 30 | if (count($entries) > 0) { 31 | $count = 0; 32 | Log::debug(self::LOG_TAG, 'NULL entries found, deleting them now.'); 33 | foreach($entries as $entry) { 34 | $book = R::load('gallery_tag', $entry["id"]); 35 | R::trash($book); 36 | $count++; 37 | } 38 | Log::debug(self::LOG_TAG, 'All NULL entries deleted. Total deleted was %d', $count); 39 | } else { 40 | Log::debug(self::LOG_TAG, 'No NULL gallery_tag entries found.'); 41 | } 42 | 43 | Log::debug(self::LOG_TAG, 'Checking for any galleries marked as deleted.'); 44 | 45 | $entries = R::getAll('SELECT * FROM gallery WHERE deleted = 1'); 46 | if (count($entries) > 0) { 47 | $count = 0; 48 | Log::debug(Self::LOG_TAG, 'Deleting galleries now. This may take a while.'); 49 | foreach ($entries as $entry) { 50 | $gallery = R::load('gallery', $entry["id"]); 51 | 52 | Log::debug(Self::LOG_TAG, 'Deleting Gallery ' . $entry["name"] . ' (' . $entry["id"] . ')'); 53 | $galleryproperty = R::getAll('SELECT * FROM galleryproperty WHERE gallery_id = ?', array($entry["id"])); 54 | $gallery_tag = R::getAll('SELECT * FROM gallery_tag WHERE gallery_id = ?', array($entry["id"])); 55 | $gallery_thumb = R::getAll('SELECT * FROM gallery_thumb WHERE gallery_id = ?', array($entry["id"])); 56 | 57 | // Get all image entries associated with the gallery_thumb then delete those, then delete the gallery_thumb entry 58 | foreach ($gallery_thumb as $thumb) { 59 | $images = R::getAll('SELECT * FROM image WHERE id = ?', array($thumb["image_id"])); 60 | // Delete all associated images and the thumbnails 61 | foreach ($images as $image) { 62 | $book = R::load('image', $image["id"]); 63 | $imageName = str_pad($book["id"], 6, '0', STR_PAD_LEFT) . '.jpeg'; 64 | unlink($config->imagesDir . '/' . $imageName); 65 | R::trash($book); 66 | } 67 | $book = R::load('gallery_thumb', $thumb["id"]); 68 | R::trash($book); 69 | } 70 | 71 | // Delete all associated tags (just the references, not the actual tag entries) 72 | foreach ($gallery_tag as $tag) { 73 | $book = R::load("gallery_tag", $tag["id"]); 74 | R::trash($book); 75 | } 76 | 77 | // Delete all associated properties. 78 | foreach ($galleryproperty as $prop) { 79 | $book = R::load('galleryproperty', $prop["id"]); 80 | R::trash($book); 81 | } 82 | 83 | // Delete archive 84 | $archiveName = $gallery["exhenid"] . '.zip'; 85 | unlink($config->archiveDir . '/galleries/' . $archiveName); 86 | 87 | // Delete html file 88 | $htmlName = $gallery["exhenid"] . '.html'; 89 | unlink($config->archiveDir . '/pages/' . $htmlName); 90 | 91 | if ($gallery["feed_id"] == null) { 92 | R::trash($gallery); 93 | } else { 94 | $gallery["deleted"] = 2; 95 | R::store($gallery); 96 | } 97 | 98 | $cache->deleteObject('gallery', $gallery->id); 99 | $count++; 100 | } 101 | Log::debug(self::LOG_TAG, 'All galleries marked as deleted were deleted. Total deleted was %d', $count); 102 | if(isset($config->indexer->full)) { 103 | $command = Config::get()->indexer->full; 104 | system($command); 105 | } 106 | } 107 | else { 108 | Log::debug(self::LOG_TAG, 'No galleries found that were marked as deleted.'); 109 | } 110 | } 111 | 112 | } 113 | 114 | ?> -------------------------------------------------------------------------------- /classes/Task/EditGallery.php: -------------------------------------------------------------------------------- 1 | id) { 21 | Log::error(self::LOG_TAG, 'Failed to load gallery with id: %s', $galleryId); 22 | return; 23 | } 24 | 25 | $action = $options[1]; 26 | $params = array_slice($options, 2); 27 | 28 | if($action === 'addtag') { 29 | if(count($params) < 1) { 30 | Log::error(self::LOG_TAG, 'Missing tag to add'); 31 | return; 32 | } 33 | 34 | foreach($params as $tagParam) { 35 | $bits = explode(':', $tagParam); 36 | if(count($bits) !== 2) { 37 | Log::error(self::LOG_TAG, 'Missing tag namespace: %s', $tagParam); 38 | continue; 39 | } 40 | 41 | list($ns, $tag) = $bits; 42 | 43 | if($gallery->hasTag($ns, $tag)) { 44 | Log::error(self::LOG_TAG, 'Gallery already has tag: %s:%s', $ns, $tag); 45 | continue; 46 | } 47 | else { 48 | $gallery->addTags(array($ns => array($tag))); 49 | } 50 | } 51 | } 52 | else { 53 | Log::error(self::LOG_TAG, 'Invalid action: %s', $action); 54 | return; 55 | } 56 | 57 | R::store($gallery); 58 | 59 | $cache = Cache::getInstance(); 60 | $cache->deleteObject('gallery', $gallery->id); 61 | 62 | if(isset(Config::get()->indexer->full)) { 63 | $command = Config::get()->indexer->full; 64 | system($command); 65 | } 66 | } 67 | 68 | } 69 | 70 | 71 | ?> 72 | -------------------------------------------------------------------------------- /classes/Task/Extract.php: -------------------------------------------------------------------------------- 1 | getArchiveFilepath(); 22 | if(!file_exists($zipPath)) { 23 | Log::error(self::LOG_TAG, 'Zip file doesn\'t exist: %s', $zipPath); 24 | } 25 | 26 | $convertedPath = preg_replace('/.zip$/', '.tar', $zipPath); 27 | 28 | if(file_exists($convertedPath)) { 29 | unlink($convertedPath); 30 | } 31 | 32 | $zip = new PharData($zipPath); 33 | $tar = $zip->convertToData(Phar::TAR); 34 | unset($tar); 35 | 36 | $tarFolder = sprintf('%s/archives/%s', Config::get()->archiveDir, $gallery->getRoundedId()); 37 | if(!file_exists($tarFolder)) { 38 | mkdir($tarFolder); 39 | } 40 | 41 | $tarPath = sprintf('%s/%d.tar', $tarFolder, $gallery->exhenid); 42 | rename($convertedPath, $tarPath); 43 | 44 | $index++; 45 | 46 | exit; 47 | } 48 | 49 | $page++; 50 | } 51 | } 52 | } 53 | 54 | ?> 55 | -------------------------------------------------------------------------------- /classes/Task/ForceAudit.php: -------------------------------------------------------------------------------- 1 | client = new ExClient(); 11 | 12 | if (isset($options[0])) { 13 | $mode = strtolower($options[0]); 14 | } else { 15 | exit; 16 | } 17 | 18 | if ($mode == "all") { 19 | $galleries = R::findAll('gallery', 20 | 'archived = 1 and deleted = 0 and source = 0'); 21 | } 22 | 23 | if ($mode == "id") { 24 | if (isset($options[1])) { 25 | $id = (int)$options[1]; 26 | $galleries = R::findAll('gallery', 27 | 'archived = 1 and deleted = 0 and source = 0 and id = ?', 28 | [$id]); 29 | } else { 30 | exit; 31 | } 32 | } 33 | 34 | if ($mode == "before") { 35 | if (isset($options[1])) { 36 | $date = strtotime($options[1]); 37 | $date = date("Y-m-d H:i:s", $date); 38 | $galleries = R::findAll('gallery', 39 | 'archived = 1 and deleted = 0 and source = 0 ' . 40 | 'and added <= ?', [$date]); 41 | } else { 42 | exit; 43 | } 44 | } 45 | 46 | if ($mode == "after") { 47 | if (isset($options[1])) { 48 | $date = strtotime($options[1]); 49 | $date = date("Y-m-d H:i:s", $date); 50 | $galleries = R::findAll('gallery', 51 | 'archived = 1 and deleted = 0 and source = 0 ' . 52 | 'and added >= ?', [$date]); 53 | } else { 54 | exit; 55 | } 56 | } 57 | 58 | if ($mode == "range" || $mode == "between") { 59 | if (isset($options[1]) && isset($options[2])) { 60 | $startDate = strtotime($options[1]); 61 | $startDate = date("Y-m-d H:i:s", $startDate); 62 | 63 | $endDate = strtotime($options[2]); 64 | $endDate = date("Y-m-d H:i:s", $endDate); 65 | 66 | $galleries = R::findAll('gallery', 67 | 'archived = 1 and deleted = 0 and source = 0 ' . 68 | 'and added between :start and :end', 69 | array(":start" => $startDate, ":end" => $endDate)); 70 | } else { 71 | exit; 72 | } 73 | } 74 | 75 | if (count($galleries) == 0) { 76 | exit; 77 | } 78 | 79 | foreach($galleries as $gallery) { 80 | $this->audit($gallery); 81 | } 82 | 83 | if(isset(Config::get()->indexer->full)) { 84 | $command = Config::get()->indexer->full; 85 | system($command); 86 | } 87 | } 88 | 89 | protected function audit($gallery) { 90 | Log::debug(self::LOG_TAG, 'Auditing gallery: #%d - %s', $gallery->exhenid, $gallery->name); 91 | 92 | $galleryHtml = $this->client->gallery($gallery->exhenid, $gallery->hash); 93 | 94 | // Galleries that are removed now return a 404, so we have to leave them as audited completely for now. 95 | if(!$galleryHtml) { 96 | $gallery->lastaudit = date('Y-m-d H:i:s'); //gallery was probably deleted, so mark it as audited for now 97 | R::store($gallery); 98 | 99 | Log::error(self::LOG_TAG, 'Gallery was either removed or was invalid.'); 100 | return; 101 | } 102 | 103 | $galleryPage = new ExPage_Gallery($galleryHtml); 104 | if(!$galleryPage->isValid()) { 105 | $gallery->lastaudit = date('Y-m-d H:i:s'); //gallery was probably deleted, so mark it as audited for now 106 | R::store($gallery); 107 | 108 | Log::error(self::LOG_TAG, 'Gallery was either removed or was invalid.'); 109 | return; 110 | } 111 | 112 | $childGallery = $galleryPage->getNewestVersion(); 113 | if($childGallery) { 114 | 115 | $childHtml = $this->client->gallery($childGallery->exhenid, $childGallery->hash); 116 | $childPage = new ExPage_Gallery($childHtml); 117 | 118 | if ($childPage->isValid()) { 119 | Log::debug(self::LOG_TAG, 'New gallery found for gallery (%d): #%d - %s', $childGallery->exhenid, $gallery->exhenid, $gallery->name); 120 | 121 | Model_Gallery::addGallery($childGallery->exhenid, $childGallery->hash); 122 | 123 | $gallery->deleted = 1; 124 | } 125 | 126 | } 127 | else { 128 | $newTags = $galleryPage->getTags(); 129 | $oldTags = $gallery->exportTags(); 130 | 131 | if(count($newTags) > 0) { 132 | $diff = self::tagsDiff($oldTags, $newTags); 133 | 134 | if(count($diff) > 0) { 135 | $humanDiff = array(); 136 | foreach($diff as $ns => $tags) { 137 | foreach($tags as $tag) { 138 | $humanDiff[] = $ns.':'.$tag; 139 | } 140 | } 141 | 142 | $humanDiff = implode(', ', $humanDiff); 143 | 144 | Log::debug(self::LOG_TAG, 'Different tags found for gallery: #%d - %s (%s)', $gallery->exhenid, $gallery->name, $humanDiff); 145 | $gallery->ownGalleryTag = array(); 146 | $gallery->addTags($newTags); 147 | 148 | $cache = Cache::getInstance(); 149 | $cache->deleteObject('gallery', $gallery->id); 150 | } 151 | } 152 | } 153 | 154 | $gallery->lastaudit = date('Y-m-d H:i:s'); 155 | 156 | if ($gallery->deleted == 1) { 157 | $gallery->lastaudit = null; 158 | } 159 | 160 | R::store($gallery); 161 | } 162 | 163 | static function tagsDiff($tags1, $tags2){ 164 | $ret = array(); 165 | 166 | foreach ($tags1 as $ns => $tags) { 167 | if(!array_key_exists($ns, $tags2)) { 168 | $ret[$ns] = $tags; 169 | continue; 170 | } 171 | 172 | foreach($tags as $index => $tag) { 173 | if(!in_array($tag, $tags2[$ns])) { 174 | if(!array_key_exists($ns, $ret) || !is_array($ret[$ns])) { 175 | $ret[$ns] = array(); 176 | } 177 | 178 | $ret[$ns][] = $tag; 179 | } 180 | else { 181 | $pos = array_search($tag, $tags2[$ns]); 182 | unset($tags2[$ns][$pos]); 183 | } 184 | } 185 | 186 | if(count($tags2[$ns]) > 0) { 187 | if(!array_key_exists($ns, $ret) || !is_array($ret[$ns])) { 188 | $ret[$ns] = array(); 189 | } 190 | 191 | $ret[$ns] = array_merge($ret[$ns], $tags2[$ns]); 192 | } 193 | 194 | unset($tags2[$ns]); 195 | } 196 | 197 | $ret = array_merge($ret, $tags2); 198 | 199 | return $ret; 200 | } 201 | 202 | } 203 | 204 | 205 | ?> 206 | -------------------------------------------------------------------------------- /classes/Task/Thumbnails.php: -------------------------------------------------------------------------------- 1 | numfiles; $i++) { 16 | try { 17 | $gallery->getThumbnail($i, 2, true); 18 | } 19 | catch(Exception $e) { 20 | $success = false; 21 | } 22 | } 23 | 24 | if($success) { 25 | $count++; 26 | } 27 | } 28 | 29 | Log::debug(self::LOG_TAG, 'Processed thumbnails for %d galleries', $count); 30 | } 31 | 32 | } 33 | 34 | 35 | ?> 36 | -------------------------------------------------------------------------------- /common.php: -------------------------------------------------------------------------------- 1 | db->dsn, $config->db->user, $config->db->pass); 26 | R::freeze(true); 27 | 28 | 29 | ?> 30 | -------------------------------------------------------------------------------- /config.json.linux: -------------------------------------------------------------------------------- 1 | { 2 | "base": { 3 | "accessKey": "changeme", 4 | "autoReadPercentage": 80, 5 | "viewType": "mpv", 6 | "cookie": { 7 | "ipb_member_id": "changeme", 8 | "ipb_pass_hash": "changeme", 9 | "hath_perks": "m1.m2.m3.tf.t1.t2.t3.p1.p2.s-210aa44613", 10 | "sp": "changeme", 11 | "sk": "changeme" 12 | }, 13 | "tempDir": "./temp", 14 | "archiveDir": "./archive", 15 | "imagesDir": "./images" 16 | }, 17 | "default": { 18 | "db": { 19 | "dsn": "mysql:host=localhost;dbname=exhen", 20 | "user": "root", 21 | "pass": "changeme" 22 | }, 23 | "sphinxql": { 24 | "dsn": "mysql:host=127.0.0.1;port=9306;dbname=exhen", 25 | "user": "root", 26 | "pass": "" 27 | }, 28 | "memcache": { 29 | "host": "localhost", 30 | "port": 11211 31 | }, 32 | "indexer": { 33 | "full": "sudo indexer --rotate galleries suggested" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config.json.win32: -------------------------------------------------------------------------------- 1 | { 2 | "base": { 3 | "accessKey": "changeme", 4 | "autoReadPercentage": 80, 5 | "viewType": "mpv", 6 | "cookie": { 7 | "ipb_member_id": "changeme", 8 | "ipb_pass_hash": "changeme", 9 | "hath_perks": "m1.m2.m3.tf.t1.t2.t3.p1.p2.s-210aa44613", 10 | "sp": "changeme", 11 | "sk": "changeme" 12 | }, 13 | "tempDir": "./temp", 14 | "archiveDir": "./archive", 15 | "imagesDir": "./images" 16 | }, 17 | "default": { 18 | "db": { 19 | "dsn": "mysql:host=localhost;dbname=exhen", 20 | "user": "root", 21 | "pass": "changeme" 22 | }, 23 | "sphinxql": { 24 | "dsn": "mysql:host=127.0.0.1;port=9306;dbname=exhen", 25 | "user": "root", 26 | "pass": "" 27 | }, 28 | "memcache": { 29 | "host": "localhost", 30 | "port": 11211 31 | }, 32 | "indexer": { 33 | "full": "C:\path\to\sphinx\bin\indexer.exe --rotate galleries suggested" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /db.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS `exhen` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */; 2 | USE `exhen`; 3 | -- MySQL dump 10.13 Distrib 5.5.16, for Win32 (x86) 4 | -- 5 | -- Host: localhost Database: exhen 6 | -- ------------------------------------------------------ 7 | -- Server version 5.5.37-0+wheezy1 8 | 9 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 10 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 11 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 12 | /*!40101 SET NAMES utf8 */; 13 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 14 | /*!40103 SET TIME_ZONE='+00:00' */; 15 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 16 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 17 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 18 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 19 | 20 | -- 21 | -- Table structure for table `feed` 22 | -- 23 | 24 | DROP TABLE IF EXISTS `feed`; 25 | /*!40101 SET @saved_cs_client = @@character_set_client */; 26 | /*!40101 SET character_set_client = utf8 */; 27 | CREATE TABLE `feed` ( 28 | `id` int(11) NOT NULL AUTO_INCREMENT, 29 | `term` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 30 | `archived` tinyint(4) NOT NULL DEFAULT '0', 31 | `disabled` tinyint(4) NOT NULL DEFAULT '0', 32 | `download` tinyint(4) NOT NULL DEFAULT '1', 33 | PRIMARY KEY (`id`) 34 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 35 | /*!40101 SET character_set_client = @saved_cs_client */; 36 | 37 | -- 38 | -- Table structure for table `gallery` 39 | -- 40 | 41 | DROP TABLE IF EXISTS `gallery`; 42 | /*!40101 SET @saved_cs_client = @@character_set_client */; 43 | /*!40101 SET character_set_client = utf8 */; 44 | CREATE TABLE `gallery` ( 45 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 46 | `exhenid` int(11) unsigned DEFAULT NULL, 47 | `hash` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 48 | `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 49 | `added` datetime DEFAULT NULL, 50 | `archived` tinyint(3) unsigned DEFAULT '0', 51 | `origtitle` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 52 | `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 53 | `posted` datetime DEFAULT NULL, 54 | `numfiles` int(11) DEFAULT NULL, 55 | `filesize` int(10) unsigned DEFAULT NULL, 56 | `deleted` tinyint(4) DEFAULT '0', 57 | `lastaudit` datetime DEFAULT NULL, 58 | `updated` datetime DEFAULT NULL, 59 | `feed_id` int(11) unsigned DEFAULT NULL, 60 | `hasmeta` tinyint(4) DEFAULT '0', 61 | `download` tinyint(4) DEFAULT '1', 62 | `source` tinyint(4) DEFAULT '0', 63 | `read` tinyint(4) DEFAULT '0', 64 | `color` varchar(255) DEFAULT 'black', 65 | PRIMARY KEY (`id`), 66 | UNIQUE KEY `exhenid_UNIQUE` (`exhenid`), 67 | KEY `index_foreignkey_gallery_feed` (`feed_id`) 68 | ) ENGINE=InnoDB AUTO_INCREMENT=178734 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 69 | /*!40101 SET character_set_client = @saved_cs_client */; 70 | 71 | -- 72 | -- Table structure for table `gallery_tag` 73 | -- 74 | 75 | DROP TABLE IF EXISTS `gallery_tag`; 76 | /*!40101 SET @saved_cs_client = @@character_set_client */; 77 | /*!40101 SET character_set_client = utf8 */; 78 | CREATE TABLE `gallery_tag` ( 79 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 80 | `gallery_id` int(11) unsigned DEFAULT NULL, 81 | `namespace_id` int(11) unsigned DEFAULT NULL, 82 | `tag_id` int(11) unsigned DEFAULT NULL, 83 | PRIMARY KEY (`id`), 84 | KEY `index_foreignkey_gallery_tag_tagnamespace` (`namespace_id`), 85 | KEY `index_foreignkey_gallery_tag_tag` (`tag_id`), 86 | KEY `index_foreignkey_gallery_tag_gallery` (`gallery_id`), 87 | CONSTRAINT `cons_fk_gallery_tag_gallery_id_id` FOREIGN KEY (`gallery_id`) REFERENCES `gallery` (`id`) ON DELETE SET NULL ON UPDATE SET NULL, 88 | CONSTRAINT `cons_fk_gallery_tag_namespace_id_id` FOREIGN KEY (`namespace_id`) REFERENCES `tagnamespace` (`id`) ON DELETE SET NULL ON UPDATE SET NULL, 89 | CONSTRAINT `cons_fk_gallery_tag_tag_id_id` FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`) ON DELETE SET NULL ON UPDATE SET NULL 90 | ) ENGINE=InnoDB AUTO_INCREMENT=1717865 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 91 | /*!40101 SET character_set_client = @saved_cs_client */; 92 | 93 | -- 94 | -- Table structure for table `gallery_thumb` 95 | -- 96 | 97 | DROP TABLE IF EXISTS `gallery_thumb`; 98 | /*!40101 SET @saved_cs_client = @@character_set_client */; 99 | /*!40101 SET character_set_client = utf8 */; 100 | CREATE TABLE `gallery_thumb` ( 101 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 102 | `index` int(10) unsigned DEFAULT NULL, 103 | `type` tinyint(4) DEFAULT NULL, 104 | `gallery_id` int(11) unsigned DEFAULT NULL, 105 | `image_id` int(11) unsigned DEFAULT NULL, 106 | PRIMARY KEY (`id`), 107 | KEY `index_foreignkey_gallery_thumb_image` (`image_id`), 108 | KEY `index_foreignkey_gallery_thumb_gallery` (`gallery_id`), 109 | CONSTRAINT `cons_fk_gallery_thumb_gallery_id_id` FOREIGN KEY (`gallery_id`) REFERENCES `gallery` (`id`) ON DELETE SET NULL ON UPDATE SET NULL, 110 | CONSTRAINT `cons_fk_gallery_thumb_image_id_id` FOREIGN KEY (`image_id`) REFERENCES `image` (`id`) ON DELETE SET NULL ON UPDATE SET NULL 111 | ) ENGINE=InnoDB AUTO_INCREMENT=69208 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 112 | /*!40101 SET character_set_client = @saved_cs_client */; 113 | 114 | -- 115 | -- Table structure for table `galleryproperty` 116 | -- 117 | 118 | DROP TABLE IF EXISTS `galleryproperty`; 119 | /*!40101 SET @saved_cs_client = @@character_set_client */; 120 | /*!40101 SET character_set_client = utf8 */; 121 | CREATE TABLE `galleryproperty` ( 122 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 123 | `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 124 | `value` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 125 | `gallery_id` int(11) unsigned DEFAULT NULL, 126 | PRIMARY KEY (`id`), 127 | KEY `index_foreignkey_galleryproperty_gallery` (`gallery_id`), 128 | CONSTRAINT `cons_fk_galleryproperty_gallery_id_id` FOREIGN KEY (`gallery_id`) REFERENCES `gallery` (`id`) ON DELETE SET NULL ON UPDATE SET NULL 129 | ) ENGINE=InnoDB AUTO_INCREMENT=1072699 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 130 | /*!40101 SET character_set_client = @saved_cs_client */; 131 | 132 | -- 133 | -- Table structure for table `image` 134 | -- 135 | 136 | DROP TABLE IF EXISTS `image`; 137 | /*!40101 SET @saved_cs_client = @@character_set_client */; 138 | /*!40101 SET character_set_client = utf8 */; 139 | CREATE TABLE `image` ( 140 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 141 | `width` int(11) unsigned DEFAULT NULL, 142 | `height` int(11) unsigned DEFAULT NULL, 143 | `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 144 | `filename` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 145 | PRIMARY KEY (`id`) 146 | ) ENGINE=InnoDB AUTO_INCREMENT=69208 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 147 | /*!40101 SET character_set_client = @saved_cs_client */; 148 | 149 | -- 150 | -- Table structure for table `sph_counter` 151 | -- 152 | 153 | DROP TABLE IF EXISTS `sph_counter`; 154 | /*!40101 SET @saved_cs_client = @@character_set_client */; 155 | /*!40101 SET character_set_client = utf8 */; 156 | CREATE TABLE `sph_counter` ( 157 | `counter_id` int(11) NOT NULL, 158 | `last_updated` datetime NOT NULL, 159 | PRIMARY KEY (`counter_id`) 160 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 161 | /*!40101 SET character_set_client = @saved_cs_client */; 162 | 163 | -- 164 | -- Table structure for table `tag` 165 | -- 166 | 167 | DROP TABLE IF EXISTS `tag`; 168 | /*!40101 SET @saved_cs_client = @@character_set_client */; 169 | /*!40101 SET character_set_client = utf8 */; 170 | CREATE TABLE `tag` ( 171 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 172 | `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 173 | PRIMARY KEY (`id`) 174 | ) ENGINE=InnoDB AUTO_INCREMENT=65845 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 175 | /*!40101 SET character_set_client = @saved_cs_client */; 176 | 177 | -- 178 | -- Table structure for table `tagnamespace` 179 | -- 180 | 181 | DROP TABLE IF EXISTS `tagnamespace`; 182 | /*!40101 SET @saved_cs_client = @@character_set_client */; 183 | /*!40101 SET character_set_client = utf8 */; 184 | CREATE TABLE `tagnamespace` ( 185 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT, 186 | `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 187 | `similar_weight` tinyint(4) DEFAULT '0', 188 | PRIMARY KEY (`id`) 189 | ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 190 | /*!40101 SET character_set_client = @saved_cs_client */; 191 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 192 | 193 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 194 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 195 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 196 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 197 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 198 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 199 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 200 | 201 | -- Dump completed on 2014-07-11 20:44:00 -------------------------------------------------------------------------------- /docker-compose.override.yml.dist: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | web: 4 | ports: 5 | - "80:80" 6 | app: 7 | environment: 8 | CONF_MEMBERID: null 9 | CONF_PASSHASH: null 10 | CONF_ACCESSKEY: ChangeMeIAmNotSecure 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | web: 4 | image: nginx 5 | links: 6 | - app 7 | volumes: 8 | - ".:/var/www" 9 | - "./.docker/config/nginx/vhost.conf:/etc/nginx/conf.d/default.conf:ro" 10 | - "./images:/var/www/www/images" 11 | 12 | app: 13 | build: . 14 | links: 15 | - mariadb 16 | - sphinx 17 | - memcache 18 | depends_on: 19 | - mariadb 20 | volumes: 21 | - "./.docker/config/php/php.ini:/usr/local/etc/php/conf.d/030-custom.ini:ro" 22 | - ".:/var/www" 23 | - "./archives:/archive" 24 | - "./images:/images" 25 | environment: 26 | TZ: UTC 27 | CONF_MEMBERID: null 28 | CONF_PASSHASH: null 29 | CONF_ACCESSKEY: ChangeMeIAmNotSecure 30 | CONF_TEMPDIR: "/tmp" 31 | CONF_ARCHDIR: "/archive" 32 | CONF_IMGDIR: "/images" 33 | CONF_SPHINXDSN: "mysql:host=sphinx;port=9306;dbname=exhen" 34 | DB_HOST: mariadb 35 | DB_USER: appuser 36 | DB_PASS: userPass 37 | DB_NAME: exhen 38 | 39 | mariadb: 40 | image: mariadb 41 | command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci 42 | environment: 43 | - "MYSQL_ROOT_PASSWORD=rootPass" 44 | - "MYSQL_USER=appuser" 45 | - "MYSQL_PASSWORD=userPass" 46 | - "MYSQL_DATABASE=exhen" 47 | volumes: 48 | - "./db.sql:/docker-entrypoint-initdb.d/db.sql" 49 | 50 | sphinx: 51 | image: stefobark/sphinxdocker 52 | links: 53 | - mariadb 54 | depends_on: 55 | - mariadb 56 | volumes: 57 | - "./.docker/config/sphinx/sphinx.conf:/etc/sphinxsearch/sphinx.conf" 58 | command: > 59 | bash -c "curl https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh > $HOME/wait-for-it.sh 60 | && chmod +x $HOME/wait-for-it.sh 61 | && $HOME/wait-for-it.sh --timeout=30 mariadb:3306 62 | && mkdir -p /var/lib/sphinxsearch/data/exhen/ 63 | && mkdir -p /etc/sphinxsearch/log/ 64 | && (crontab -l ; echo \"*/10 * * * * indexer -c /etc/sphinxsearch/sphinx.conf --all --rotate > /proc/1/fd/1 2>/proc/1/fd/2\") | sort - | uniq - | crontab - 65 | && apt-get install -y cron && cron 66 | && indexer -c /etc/sphinxsearch/sphinx.conf --all --rotate 67 | && searchd -c /etc/sphinxsearch/sphinx.conf --nodetach --pidfile" 68 | expose: 69 | - 9306 70 | 71 | memcache: 72 | image: memcached:alpine 73 | -------------------------------------------------------------------------------- /docker-entrypoint-init.d/10-config.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | if [ -f "config.json.linux" ]; then 3 | echo "Updating config.json" 4 | 5 | jq '.base.cookie.ipb_member_id=env.CONF_MEMBERID | .base.cookie.ipb_pass_hash=env.CONF_PASSHASH | .base.cookie.ipb_accesskey=env.CONF_ACCESSKEY | .base.tempDir=env.CONF_TEMPDIR | .base.archiveDir=env.CONF_ARCHDIR | .base.imagesDir=env.CONF_IMGDIR | .default.db.dsn=env.CONF_SQLDSN | .default.db.user=env.DB_USER | .default.db.pass=env.DB_PASS | .default.sphinxql.dsn=env.CONF_SPHINXDSN | .default.memcache.host="memcache"' config.json.linux > config.json 6 | cp config.json.linux www/config.json 7 | echo "Updated config" 8 | fi 9 | -------------------------------------------------------------------------------- /docker-entrypoint-init.d/11-cron.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | if [ ! -f "/etc/periodic/15min/archive.sh" ]; then 3 | cp /var/www/.docker/config/app/cron/15min/archive.sh /etc/periodic/15min/archive.sh 4 | chmod +x /etc/periodic/15min/archive.sh 5 | (crontab -l ; echo "*/10 * * * * /etc/periodic/15min/archive.sh > /proc/1/fd/1 2>/proc/1/fd/2") | sort - | uniq - | crontab - 6 | fi 7 | 8 | if [ ! -f "/etc/periodic/hourly/audit.sh" ]; then 9 | cp /var/www/.docker/config/app/cron/hourly/audit.sh /etc/periodic/hourly/audit.sh 10 | chmod +x /etc/periodic/hourly/audit.sh 11 | (crontab -l ; echo "0 4 * * * /etc/periodic/hourly/audit.sh > /proc/1/fd/1 2>/proc/1/fd/2") | sort - | uniq - | crontab - 12 | fi 13 | 14 | if [ ! -f "/etc/periodic/hourly/thumbnails.sh" ]; then 15 | cp /var/www/.docker/config/app/cron/hourly/thumbnails.sh /etc/periodic/hourly/thumbnails.sh 16 | chmod +x /etc/periodic/hourly/thumbnails.sh 17 | (crontab -l ; echo "0 5 * * * /etc/periodic/hourly/thumbnails.sh > /proc/1/fd/1 2>/proc/1/fd/2") | sort - | uniq - | crontab - 18 | fi 19 | 20 | 21 | crond 22 | -------------------------------------------------------------------------------- /init.d.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | echo 3 | for f in docker-entrypoint-init.d/*; do 4 | case "$f" in 5 | *.sh) 6 | # https://github.com/docker-library/postgres/issues/450#issuecomment-393167936 7 | # https://github.com/docker-library/postgres/pull/452 8 | if [ -x "$f" ]; then 9 | echo "$0: running $f" 10 | "$f" 11 | else 12 | echo "$0: sourcing $f" 13 | . "$f" 14 | fi 15 | ;; 16 | *) echo "$0: ignoring $f" ;; 17 | esac 18 | echo 19 | done 20 | 21 | echo 22 | echo 'init process complete; ready for start up.' 23 | echo 24 | 25 | exec php-fpm 26 | -------------------------------------------------------------------------------- /lib/PHPImageWorkshop/Core/Exception/ImageWorkshopLayerException.php: -------------------------------------------------------------------------------- 1 | $layerPositionX, 84 | 'y' => $layerPositionY, 85 | ); 86 | } 87 | 88 | /** 89 | * Convert Hex color to RGB color format 90 | * 91 | * @param string $hex 92 | * 93 | * @return array 94 | */ 95 | public static function convertHexToRGB($hex) 96 | { 97 | return array( 98 | 'R' => (int) base_convert(substr($hex, 0, 2), 16, 10), 99 | 'G' => (int) base_convert(substr($hex, 2, 2), 16, 10), 100 | 'B' => (int) base_convert(substr($hex, 4, 2), 16, 10), 101 | ); 102 | } 103 | 104 | /** 105 | * Generate a new image resource var 106 | * 107 | * @param integer $width 108 | * @param integer $height 109 | * @param string $color 110 | * @param integer $opacity 111 | * 112 | * @return resource 113 | */ 114 | public static function generateImage($width = 100, $height = 100, $color = 'ffffff', $opacity = 127) 115 | { 116 | $RGBColors = ImageWorkshopLib::convertHexToRGB($color); 117 | 118 | $image = imagecreatetruecolor($width, $height); 119 | imagesavealpha($image, true); 120 | $color = imagecolorallocatealpha($image, $RGBColors['R'], $RGBColors['G'], $RGBColors['B'], $opacity); 121 | imagefill($image, 0, 0, $color); 122 | 123 | return $image; 124 | } 125 | 126 | /** 127 | * Return dimension of a text 128 | * 129 | * @param $fontSize 130 | * @param $fontAngle 131 | * @param $fontFile 132 | * @param $text 133 | * 134 | * @return array or boolean 135 | */ 136 | public static function getTextBoxDimension($fontSize, $fontAngle, $fontFile, $text) 137 | { 138 | if (!file_exists($fontFile)) { 139 | throw new ImageWorkshopLibException('Can\'t find a font file at this path : "'.$fontFile.'".', static::ERROR_FONT_NOT_FOUND); 140 | } 141 | 142 | $box = imagettfbbox($fontSize, $fontAngle, $fontFile, $text); 143 | 144 | if (!$box) { 145 | 146 | return false; 147 | } 148 | 149 | $minX = min(array($box[0], $box[2], $box[4], $box[6])); 150 | $maxX = max(array($box[0], $box[2], $box[4], $box[6])); 151 | $minY = min(array($box[1], $box[3], $box[5], $box[7])); 152 | $maxY = max(array($box[1], $box[3], $box[5], $box[7])); 153 | $width = ($maxX - $minX); 154 | $height = ($maxY - $minY); 155 | $left = abs($minX) + $width; 156 | $top = abs($minY) + $height; 157 | 158 | // to calculate the exact bounding box, we write the text in a large image 159 | $img = @imagecreatetruecolor($width << 2, $height << 2); 160 | $white = imagecolorallocate($img, 255, 255, 255); 161 | $black = imagecolorallocate($img, 0, 0, 0); 162 | imagefilledrectangle($img, 0, 0, imagesx($img), imagesy($img), $black); 163 | 164 | // for ensure that the text is completely in the image 165 | imagettftext($img, $fontSize, $fontAngle, $left, $top, $white, $fontFile, $text); 166 | 167 | // start scanning (0=> black => empty) 168 | $rleft = $w4 = $width<<2; 169 | $rright = 0; 170 | $rbottom = 0; 171 | $rtop = $h4 = $height<<2; 172 | 173 | for ($x = 0; $x < $w4; $x++) { 174 | 175 | for ($y = 0; $y < $h4; $y++) { 176 | 177 | if (imagecolorat($img, $x, $y)) { 178 | 179 | $rleft = min($rleft, $x); 180 | $rright = max($rright, $x); 181 | $rtop = min($rtop, $y); 182 | $rbottom = max($rbottom, $y); 183 | } 184 | } 185 | } 186 | 187 | imagedestroy($img); 188 | 189 | return array( 190 | 'left' => $left - $rleft, 191 | 'top' => $top - $rtop, 192 | 'width' => $rright - $rleft + 1, 193 | 'height' => $rbottom - $rtop + 1, 194 | ); 195 | } 196 | 197 | /** 198 | * Copy an image on another one and converse transparency 199 | * 200 | * @param resource $destImg 201 | * @param resource $srcImg 202 | * @param integer $destX 203 | * @param integer $destY 204 | * @param integer $srcX 205 | * @param integer $srcY 206 | * @param integer $srcW 207 | * @param integer $srcH 208 | * @param integer $pct 209 | */ 210 | public static function imageCopyMergeAlpha(&$destImg, &$srcImg, $destX, $destY, $srcX, $srcY, $srcW, $srcH, $pct = 0) 211 | { 212 | $destX = (int) $destX; 213 | $destY = (int) $destY; 214 | $srcX = (int) $srcX; 215 | $srcY = (int) $srcY; 216 | $srcW = (int) $srcW; 217 | $srcH = (int) $srcH; 218 | $pct = (int) $pct; 219 | $destW = imageSX($destImg); 220 | $destH = imageSY($destImg); 221 | 222 | for ($y = 0; $y < $srcH + $srcY; $y++) { 223 | 224 | for ($x = 0; $x < $srcW + $srcX; $x++) { 225 | 226 | if ($x + $destX >= 0 && $x + $destX < $destW && $x + $srcX >= 0 && $x + $srcX < $srcW && $y + $destY >= 0 && $y + $destY < $destH && $y + $srcY >= 0 && $y + $srcY < $srcH) { 227 | 228 | $destPixel = imageColorsForIndex($destImg, imageColorat($destImg, $x + $destX, $y + $destY)); 229 | $srcImgColorat = imageColorat($srcImg, $x + $srcX, $y + $srcY); 230 | 231 | if ($srcImgColorat >= 0) { 232 | 233 | $srcPixel = imageColorsForIndex($srcImg, $srcImgColorat); 234 | 235 | $srcAlpha = 1 - ($srcPixel['alpha'] / 127); 236 | $destAlpha = 1 - ($destPixel['alpha'] / 127); 237 | $opacity = $srcAlpha * $pct / 100; 238 | 239 | if ($destAlpha >= $opacity) { 240 | $alpha = $destAlpha; 241 | } 242 | 243 | if ($destAlpha < $opacity) { 244 | $alpha = $opacity; 245 | } 246 | 247 | if ($alpha > 1) { 248 | $alpha = 1; 249 | } 250 | 251 | if ($opacity > 0) { 252 | 253 | $destRed = round((($destPixel['red'] * $destAlpha * (1 - $opacity)))); 254 | $destGreen = round((($destPixel['green'] * $destAlpha * (1 - $opacity)))); 255 | $destBlue = round((($destPixel['blue'] * $destAlpha * (1 - $opacity)))); 256 | $srcRed = round((($srcPixel['red'] * $opacity))); 257 | $srcGreen = round((($srcPixel['green'] * $opacity))); 258 | $srcBlue = round((($srcPixel['blue'] * $opacity))); 259 | $red = round(($destRed + $srcRed ) / ($destAlpha * (1 - $opacity) + $opacity)); 260 | $green = round(($destGreen + $srcGreen) / ($destAlpha * (1 - $opacity) + $opacity)); 261 | $blue = round(($destBlue + $srcBlue ) / ($destAlpha * (1 - $opacity) + $opacity)); 262 | 263 | if ($red > 255) { 264 | $red = 255; 265 | } 266 | 267 | if ($green > 255) { 268 | $green = 255; 269 | } 270 | 271 | if ($blue > 255) { 272 | $blue = 255; 273 | } 274 | 275 | $alpha = round((1 - $alpha) * 127); 276 | $color = imageColorAllocateAlpha($destImg, $red, $green, $blue, $alpha); 277 | imageSetPixel($destImg, $x + $destX, $y + $destY, $color); 278 | } 279 | } 280 | } 281 | } 282 | } 283 | } 284 | 285 | /** 286 | * Merge two image var 287 | * 288 | * @param resource $destinationImage 289 | * @param resource $sourceImage 290 | * @param integer $destinationPosX 291 | * @param integer $destinationPosY 292 | * @param integer $sourcePosX 293 | * @param integer $sourcePosY 294 | */ 295 | public static function mergeTwoImages(&$destinationImage, $sourceImage, $destinationPosX = 0, $destinationPosY = 0, $sourcePosX = 0, $sourcePosY = 0) 296 | { 297 | imageCopy($destinationImage, $sourceImage, $destinationPosX, $destinationPosY, $sourcePosX, $sourcePosY, imageSX($sourceImage), imageSY($sourceImage)); 298 | } 299 | } -------------------------------------------------------------------------------- /lib/PHPImageWorkshop/Exception/ImageWorkshopBaseException.php: -------------------------------------------------------------------------------- 1 | code}]: {$this->message}\n"; 37 | } 38 | } -------------------------------------------------------------------------------- /lib/PHPImageWorkshop/Exception/ImageWorkshopException.php: -------------------------------------------------------------------------------- 1 | fixOrientation(); 102 | } 103 | 104 | return $layer; 105 | } 106 | 107 | /** 108 | * Initialize a text layer 109 | * 110 | * @param string $text 111 | * @param string $fontPath 112 | * @param integer $fontSize 113 | * @param string $fontColor 114 | * @param integer $textRotation 115 | * @param integer $backgroundColor 116 | * 117 | * @return ImageWorkshopLayer 118 | */ 119 | public static function initTextLayer($text, $fontPath, $fontSize = 13, $fontColor = 'ffffff', $textRotation = 0, $backgroundColor = null) 120 | { 121 | $textDimensions = ImageWorkshopLib::getTextBoxDimension($fontSize, $textRotation, $fontPath, $text); 122 | 123 | $layer = static::initVirginLayer($textDimensions['width'], $textDimensions['height'], $backgroundColor); 124 | $layer->write($text, $fontPath, $fontSize, $fontColor, $textDimensions['left'], $textDimensions['top'], $textRotation); 125 | 126 | return $layer; 127 | } 128 | 129 | /** 130 | * Initialize a new virgin layer 131 | * 132 | * @param integer $width 133 | * @param integer $height 134 | * @param string $backgroundColor 135 | * 136 | * @return ImageWorkshopLayer 137 | */ 138 | public static function initVirginLayer($width = 100, $height = 100, $backgroundColor = null) 139 | { 140 | $opacity = 0; 141 | 142 | if (null === $backgroundColor || $backgroundColor == 'transparent') { 143 | $opacity = 127; 144 | $backgroundColor = 'ffffff'; 145 | } 146 | 147 | return new ImageWorkshopLayer(ImageWorkshopLib::generateImage($width, $height, $backgroundColor, $opacity)); 148 | } 149 | 150 | /** 151 | * Initialize a layer from a resource image var 152 | * 153 | * @param \resource $image 154 | * 155 | * @return ImageWorkshopLayer 156 | */ 157 | public static function initFromResourceVar($image) 158 | { 159 | return new ImageWorkshopLayer($image); 160 | } 161 | 162 | /** 163 | * Initialize a layer from a string (obtains with file_get_contents, cURL...) 164 | * 165 | * This not recommanded to initialize JPEG string with this method, GD displays bugs ! 166 | * 167 | * @param string $imageString 168 | * 169 | * @return ImageWorkshopLayer 170 | */ 171 | public static function initFromString($imageString) 172 | { 173 | if (!$image = @imageCreateFromString($imageString)) { 174 | throw new ImageWorkshopException('Can\'t generate an image from the given string.', static::ERROR_CREATE_IMAGE_FROM_STRING); 175 | } 176 | 177 | return new ImageWorkshopLayer($image); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /lib/phpQuery/Callback.php: -------------------------------------------------------------------------------- 1 | 25 | * 26 | * @TODO??? return fake forwarding function created via create_function 27 | * @TODO honor paramStructure 28 | */ 29 | class Callback 30 | implements ICallbackNamed { 31 | public $callback = null; 32 | public $params = null; 33 | protected $name; 34 | public function __construct($callback, $param1 = null, $param2 = null, 35 | $param3 = null) { 36 | $params = func_get_args(); 37 | $params = array_slice($params, 1); 38 | if ($callback instanceof Callback) { 39 | // TODO implement recurention 40 | } else { 41 | $this->callback = $callback; 42 | $this->params = $params; 43 | } 44 | } 45 | public function getName() { 46 | return 'Callback: '.$this->name; 47 | } 48 | public function hasName() { 49 | return isset($this->name) && $this->name; 50 | } 51 | public function setName($name) { 52 | $this->name = $name; 53 | return $this; 54 | } 55 | // TODO test me 56 | // public function addParams() { 57 | // $params = func_get_args(); 58 | // return new Callback($this->callback, $this->params+$params); 59 | // } 60 | } 61 | /** 62 | * Shorthand for new Callback(create_function(...), ...); 63 | * 64 | * @author Tobiasz Cudnik 65 | */ 66 | class CallbackBody extends Callback { 67 | public function __construct($paramList, $code, $param1 = null, $param2 = null, 68 | $param3 = null) { 69 | $params = func_get_args(); 70 | $params = array_slice($params, 2); 71 | $this->callback = create_function($paramList, $code); 72 | $this->params = $params; 73 | } 74 | } 75 | /** 76 | * Callback type which on execution returns reference passed during creation. 77 | * 78 | * @author Tobiasz Cudnik 79 | */ 80 | class CallbackReturnReference extends Callback 81 | implements ICallbackNamed { 82 | protected $reference; 83 | public function __construct(&$reference, $name = null){ 84 | $this->reference =& $reference; 85 | $this->callback = array($this, 'callback'); 86 | } 87 | public function callback() { 88 | return $this->reference; 89 | } 90 | public function getName() { 91 | return 'Callback: '.$this->name; 92 | } 93 | public function hasName() { 94 | return isset($this->name) && $this->name; 95 | } 96 | } 97 | /** 98 | * Callback type which on execution returns value passed during creation. 99 | * 100 | * @author Tobiasz Cudnik 101 | */ 102 | class CallbackReturnValue extends Callback 103 | implements ICallbackNamed { 104 | protected $value; 105 | protected $name; 106 | public function __construct($value, $name = null){ 107 | $this->value =& $value; 108 | $this->name = $name; 109 | $this->callback = array($this, 'callback'); 110 | } 111 | public function callback() { 112 | return $this->value; 113 | } 114 | public function __toString() { 115 | return $this->getName(); 116 | } 117 | public function getName() { 118 | return 'Callback: '.$this->name; 119 | } 120 | public function hasName() { 121 | return isset($this->name) && $this->name; 122 | } 123 | } 124 | /** 125 | * CallbackParameterToReference can be used when we don't really want a callback, 126 | * only parameter passed to it. CallbackParameterToReference takes first 127 | * parameter's value and passes it to reference. 128 | * 129 | * @author Tobiasz Cudnik 130 | */ 131 | class CallbackParameterToReference extends Callback { 132 | /** 133 | * @param $reference 134 | * @TODO implement $paramIndex; 135 | * param index choose which callback param will be passed to reference 136 | */ 137 | public function __construct(&$reference){ 138 | $this->callback =& $reference; 139 | } 140 | } 141 | //class CallbackReference extends Callback { 142 | // /** 143 | // * 144 | // * @param $reference 145 | // * @param $paramIndex 146 | // * @todo implement $paramIndex; param index choose which callback param will be passed to reference 147 | // */ 148 | // public function __construct(&$reference, $name = null){ 149 | // $this->callback =& $reference; 150 | // } 151 | //} 152 | class CallbackParam {} -------------------------------------------------------------------------------- /lib/phpQuery/DOMEvent.php: -------------------------------------------------------------------------------- 1 | 8 | * @package phpQuery 9 | * @todo implement ArrayAccess ? 10 | */ 11 | class DOMEvent { 12 | /** 13 | * Returns a boolean indicating whether the event bubbles up through the DOM or not. 14 | * 15 | * @var unknown_type 16 | */ 17 | public $bubbles = true; 18 | /** 19 | * Returns a boolean indicating whether the event is cancelable. 20 | * 21 | * @var unknown_type 22 | */ 23 | public $cancelable = true; 24 | /** 25 | * Returns a reference to the currently registered target for the event. 26 | * 27 | * @var unknown_type 28 | */ 29 | public $currentTarget; 30 | /** 31 | * Returns detail about the event, depending on the type of event. 32 | * 33 | * @var unknown_type 34 | * @link http://developer.mozilla.org/en/DOM/event.detail 35 | */ 36 | public $detail; // ??? 37 | /** 38 | * Used to indicate which phase of the event flow is currently being evaluated. 39 | * 40 | * NOT IMPLEMENTED 41 | * 42 | * @var unknown_type 43 | * @link http://developer.mozilla.org/en/DOM/event.eventPhase 44 | */ 45 | public $eventPhase; // ??? 46 | /** 47 | * The explicit original target of the event (Mozilla-specific). 48 | * 49 | * NOT IMPLEMENTED 50 | * 51 | * @var unknown_type 52 | */ 53 | public $explicitOriginalTarget; // moz only 54 | /** 55 | * The original target of the event, before any retargetings (Mozilla-specific). 56 | * 57 | * NOT IMPLEMENTED 58 | * 59 | * @var unknown_type 60 | */ 61 | public $originalTarget; // moz only 62 | /** 63 | * Identifies a secondary target for the event. 64 | * 65 | * @var unknown_type 66 | */ 67 | public $relatedTarget; 68 | /** 69 | * Returns a reference to the target to which the event was originally dispatched. 70 | * 71 | * @var unknown_type 72 | */ 73 | public $target; 74 | /** 75 | * Returns the time that the event was created. 76 | * 77 | * @var unknown_type 78 | */ 79 | public $timeStamp; 80 | /** 81 | * Returns the name of the event (case-insensitive). 82 | */ 83 | public $type; 84 | public $runDefault = true; 85 | public $data = null; 86 | public function __construct($data) { 87 | foreach($data as $k => $v) { 88 | $this->$k = $v; 89 | } 90 | if (! $this->timeStamp) 91 | $this->timeStamp = time(); 92 | } 93 | /** 94 | * Cancels the event (if it is cancelable). 95 | * 96 | */ 97 | public function preventDefault() { 98 | $this->runDefault = false; 99 | } 100 | /** 101 | * Stops the propagation of events further along in the DOM. 102 | * 103 | */ 104 | public function stopPropagation() { 105 | $this->bubbles = false; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/phpQuery/bootstrap.example.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/phpQuery/compat/mbstring.php: -------------------------------------------------------------------------------- 1 | document) 33 | $pq->find('*')->add($pq->document) 34 | ->trigger($type, $data); 35 | } 36 | } else { 37 | if (isset($data[0]) && $data[0] instanceof DOMEvent) { 38 | $event = $data[0]; 39 | $event->relatedTarget = $event->target; 40 | $event->target = $node; 41 | $data = array_slice($data, 1); 42 | } else { 43 | $event = new DOMEvent(array( 44 | 'type' => $type, 45 | 'target' => $node, 46 | 'timeStamp' => time(), 47 | )); 48 | } 49 | $i = 0; 50 | while($node) { 51 | // TODO whois 52 | phpQuery::debug("Triggering ".($i?"bubbled ":'')."event '{$type}' on " 53 | ."node \n");//.phpQueryObject::whois($node)."\n"); 54 | $event->currentTarget = $node; 55 | $eventNode = self::getNode($documentID, $node); 56 | if (isset($eventNode->eventHandlers)) { 57 | foreach($eventNode->eventHandlers as $eventType => $handlers) { 58 | $eventNamespace = null; 59 | if (strpos($type, '.') !== false) 60 | list($eventName, $eventNamespace) = explode('.', $eventType); 61 | else 62 | $eventName = $eventType; 63 | if ($name != $eventName) 64 | continue; 65 | if ($namespace && $eventNamespace && $namespace != $eventNamespace) 66 | continue; 67 | foreach($handlers as $handler) { 68 | phpQuery::debug("Calling event handler\n"); 69 | $event->data = $handler['data'] 70 | ? $handler['data'] 71 | : null; 72 | $params = array_merge(array($event), $data); 73 | $return = phpQuery::callbackRun($handler['callback'], $params); 74 | if ($return === false) { 75 | $event->bubbles = false; 76 | } 77 | } 78 | } 79 | } 80 | // to bubble or not to bubble... 81 | if (! $event->bubbles) 82 | break; 83 | $node = $node->parentNode; 84 | $i++; 85 | } 86 | } 87 | } 88 | /** 89 | * Binds a handler to one or more events (like click) for each matched element. 90 | * Can also bind custom events. 91 | * 92 | * @param DOMNode|phpQueryObject|string $document 93 | * @param unknown_type $type 94 | * @param unknown_type $data Optional 95 | * @param unknown_type $callback 96 | * 97 | * @TODO support '!' (exclusive) events 98 | * @TODO support more than event in $type (space-separated) 99 | * @TODO support binding to global events 100 | */ 101 | public static function add($document, $node, $type, $data, $callback = null) { 102 | phpQuery::debug("Binding '$type' event"); 103 | $documentID = phpQuery::getDocumentID($document); 104 | // if (is_null($callback) && is_callable($data)) { 105 | // $callback = $data; 106 | // $data = null; 107 | // } 108 | $eventNode = self::getNode($documentID, $node); 109 | if (! $eventNode) 110 | $eventNode = self::setNode($documentID, $node); 111 | if (!isset($eventNode->eventHandlers[$type])) 112 | $eventNode->eventHandlers[$type] = array(); 113 | $eventNode->eventHandlers[$type][] = array( 114 | 'callback' => $callback, 115 | 'data' => $data, 116 | ); 117 | } 118 | /** 119 | * Enter description here... 120 | * 121 | * @param DOMNode|phpQueryObject|string $document 122 | * @param unknown_type $type 123 | * @param unknown_type $callback 124 | * 125 | * @TODO namespace events 126 | * @TODO support more than event in $type (space-separated) 127 | */ 128 | public static function remove($document, $node, $type = null, $callback = null) { 129 | $documentID = phpQuery::getDocumentID($document); 130 | $eventNode = self::getNode($documentID, $node); 131 | if (is_object($eventNode) && isset($eventNode->eventHandlers[$type])) { 132 | if ($callback) { 133 | foreach($eventNode->eventHandlers[$type] as $k => $handler) 134 | if ($handler['callback'] == $callback) 135 | unset($eventNode->eventHandlers[$type][$k]); 136 | } else { 137 | unset($eventNode->eventHandlers[$type]); 138 | } 139 | } 140 | } 141 | protected static function getNode($documentID, $node) { 142 | foreach(phpQuery::$documents[$documentID]->eventsNodes as $eventNode) { 143 | if ($node->isSameNode($eventNode)) 144 | return $eventNode; 145 | } 146 | } 147 | protected static function setNode($documentID, $node) { 148 | phpQuery::$documents[$documentID]->eventsNodes[] = $node; 149 | return phpQuery::$documents[$documentID]->eventsNodes[ 150 | count(phpQuery::$documents[$documentID]->eventsNodes)-1 151 | ]; 152 | } 153 | protected static function issetGlobal($documentID, $type) { 154 | return isset(phpQuery::$documents[$documentID]) 155 | ? in_array($type, phpQuery::$documents[$documentID]->eventsGlobal) 156 | : false; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/phpQuery/plugins/Scripts.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/phpQuery/plugins/Scripts/__config.example.php: -------------------------------------------------------------------------------- 1 | array('login@mail', 'password'), 9 | ); 10 | ?> -------------------------------------------------------------------------------- /lib/phpQuery/plugins/Scripts/example.php: -------------------------------------------------------------------------------- 1 | find($params[0]); 14 | ?> -------------------------------------------------------------------------------- /lib/phpQuery/plugins/Scripts/fix_webroot.php: -------------------------------------------------------------------------------- 1 | filter($filter) as $el) { 9 | $el = pq($el, $self->getDocumentID()); 10 | // imgs and scripts 11 | if ( $el->is('img') || $el->is('script') ) 12 | $el->attr('src', $params[0].$el->attr('src')); 13 | // css 14 | if ( $el->is('link') ) 15 | $el->attr('href', $params[0].$el->attr('href')); 16 | } -------------------------------------------------------------------------------- /lib/phpQuery/plugins/Scripts/google_login.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | phpQuery::ajaxAllowHost( 10 | 'code.google.com', 11 | 'google.com', 'www.google.com', 12 | 'mail.google.com', 13 | 'docs.google.com', 14 | 'reader.google.com' 15 | ); 16 | if (! function_exists('ndfasui8923')) { 17 | function ndfasui8923($browser, $scope) { 18 | extract($scope); 19 | $browser 20 | ->WebBrowser() 21 | ->find('#Email') 22 | ->val($config['google_login'][0])->end() 23 | ->find('#Passwd') 24 | ->val($config['google_login'][1]) 25 | ->parents('form') 26 | ->submit(); 27 | } 28 | $ndfasui8923 = new Callback('ndfasui8923', new CallbackParam, compact( 29 | 'config', 'self', 'return', 'params' 30 | )); 31 | } 32 | phpQuery::plugin('WebBrowser'); 33 | $self->document->xhr = phpQuery::$plugins->browserGet( 34 | 'https://www.google.com/accounts/Login', 35 | $ndfasui8923 36 | ); 37 | //$self->document->xhr = phpQuery::$plugins->browserGet('https://www.google.com/accounts/Login', create_function('$browser', " 38 | // \$browser 39 | // ->WebBrowser() 40 | // ->find('#Email') 41 | // ->val('{$config['google_login'][0]}')->end() 42 | // ->find('#Passwd') 43 | // ->val('".str_replace("'", "\\'", $config['google_login'][1])."') 44 | // ->parents('form') 45 | // ->submit();" 46 | //)); 47 | ?> -------------------------------------------------------------------------------- /lib/phpQuery/plugins/Scripts/print_source.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | /** @var phpQueryObject */ 8 | $self = $self; 9 | $return = htmlspecialchars($self); -------------------------------------------------------------------------------- /lib/phpQuery/plugins/Scripts/print_websafe.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | /** @var phpQueryObject */ 8 | $self = $self; 9 | $self 10 | ->find('script') 11 | ->add('meta[http-equiv=refresh]') 12 | ->add('meta[http-equiv=Refresh]') 13 | ->remove(); -------------------------------------------------------------------------------- /lib/phpQuery/plugins/example.php: -------------------------------------------------------------------------------- 1 | plugin('example') 9 | * pq('ul')->plugin('example', 'example.php') 10 | * 11 | * Plugin classes are never intialized, just method calls are forwarded 12 | * in static way from phpQuery. 13 | * 14 | * Have fun writing plugins :) 15 | */ 16 | 17 | /** 18 | * phpQuery plugin class extending phpQuery object. 19 | * Methods from this class are callable on every phpQuery object. 20 | * 21 | * Class name prefix 'phpQueryObjectPlugin_' must be preserved. 22 | */ 23 | abstract class phpQueryObjectPlugin_example { 24 | /** 25 | * Limit binded methods. 26 | * 27 | * null means all public. 28 | * array means only specified ones. 29 | * 30 | * @var array|null 31 | */ 32 | public static $phpQueryMethods = null; 33 | /** 34 | * Enter description here... 35 | * 36 | * @param phpQueryObject $self 37 | */ 38 | public static function example($self, $arg1) { 39 | // this method can be called on any phpQuery object, like this: 40 | // pq('div')->example('$arg1 Value') 41 | 42 | // do something 43 | $self->append('Im just an example !'); 44 | // change stack of result object 45 | return $self->find('div'); 46 | } 47 | protected static function helperFunction() { 48 | // this method WONT be avaible as phpQuery method, 49 | // because it isn't publicly callable 50 | } 51 | } 52 | 53 | /** 54 | * phpQuery plugin class extending phpQuery static namespace. 55 | * Methods from this class are callable as follows: 56 | * phpQuery::$plugins->staticMethod() 57 | * 58 | * Class name prefix 'phpQueryPlugin_' must be preserved. 59 | */ 60 | abstract class phpQueryPlugin_example { 61 | /** 62 | * Limit binded methods. 63 | * 64 | * null means all public. 65 | * array means only specified ones. 66 | * 67 | * @var array|null 68 | */ 69 | public static $phpQueryMethods = null; 70 | public static function staticMethod() { 71 | // this method can be called within phpQuery class namespace, like this: 72 | // phpQuery::$plugins->staticMethod() 73 | } 74 | } 75 | ?> -------------------------------------------------------------------------------- /setup/Docker-setup.md: -------------------------------------------------------------------------------- 1 | Docker Setup 2 | --- 3 | 4 | ### 5 | * Docker 6 | * Docker-compose 7 | * e-hentai account with ExHentai access 8 | * GP to download galleries 9 | 10 | Docker is generally the easiest way to get a compatible environment started as it contains all the required packages and configurations to get started. 11 | 12 | #### Getting started 13 | The easiest way to get a local (dev/test) environment up and running is by copying the `docker-compose.override.yml` to `docker-compose.override.yml`. 14 | This file will override the default values for configuration. 15 | 16 | After copying the override YAML, make sure to replace the `environment` parameters under the `app` section. 17 | 18 | * The `CONF_ACCESSKEY` option is a password used for deleting and adding galleries. 19 | * The `CONF_MEMBERID` and `CONF_PASSHASH` should be changed to match your ExHentai cookie. 20 | * The `db` block should stay as it is in most cases, with the exception of `pass`, which you should input whatever password you used for the MySQL setup step. 21 | * On first run add `INIT_DB: 1` to the `environment:` list for `web`. This will run the sql script to create the database 22 | * SphinxQL and memcache should stay as-is. 23 | 24 | Run: `docker-compose up` 25 | Use `-d` flag to detach console and continue running in the background. Note, you will not get the log output by default if you run in detached mode. You'll have to use `docker logs` to check logs in that case, the instructions for that are out of scope for this guide. 26 | 27 | Visit `http://localhost` to see the webpage. 28 | 29 | #### Adding galleries 30 | 31 | Adding galleries is done via a userscript. Open the provided `userscript.js` file in a text editor and change the two values at the top of the code. The `baseUrl` in this example would be http://exhen.localhost and `key` would be the `accessKey` value in the config.json file. 32 | 33 | Once edited, add to your browser via Greasemonkey/Tampermonkey. 34 | 35 | The script will add a "Send to archive" link to the search results page (in thumbnails view) and on the gallery detail page. Clicking it will send it to the archive to be marked for download. Galleries added to the database will be marked in green on the search results page. 36 | 37 | After you have added a few galleries, you can now run the `Archive` task that will download them. 38 | 39 | cd /var/www/vhosts/exhen 40 | php TaskRunner.php Archive 41 | 42 | The task should download the galleries and reindex Sphinx. 43 | 44 | Once completed, reload http://exhen.localhost and you should see your galleries appear! 45 | 46 | #### Cron 47 | 48 | For ideal automation, tasks should be setup in the crontab of your system. Below is an example I use. 49 | 50 | */10 * * * * cd /var/www/vhosts/exhen/ && php TaskRunner.php Archive >> log.txt 2>&1 51 | 0 4 * * * cd /var/www/vhosts/exhen/ && php TaskRunner.php Thumbnails >> log.txt 2>&1 52 | 0 5 * * * cd /var/www/vhosts/exhen/ && php TaskRunner.php Audit >> log.txt 2>&1 53 | 54 | There are 3 main tasks in use. 55 | 56 | * **Archive** - downloads pending galleries in the database; 57 | * **Audit** - updates meta data for added galleries. Will typically update each gallery periodically for new tags, or add newer versions of that gallery to the database. 58 | * **Thumbnails** - generates thumbs for galleries instead of doing it on-the-fly on the frontend. 59 | 60 | #### Extras 61 | 62 | If you find the search results are out of sync with the database, you may need to connect to the sphinx container and reindex Sphinx: 63 | 64 | docker-compose run --rm sphinx "sudo -u sphinxsearch indexer --rotate --all" 65 | 66 | ##### Feeds 67 | 68 | Feeds are ways of adding galleries to the database automatically through search terms. The Archive task will run through all feeds in the database, adding new galleries, and optionally downloading these galleries. Support for managing these in a friendly way does not exist. 69 | 70 | If you wish to play around with feeds, get a MySQL database admin tool (MySQL Workbench) and take a look at the feeds table. Create a new row with the `term` matching the search term you want (i.e "comic x-ero"), then `download` should be 1 or 0, depending on if you want the archiver to also download the gallery zips. 71 | -------------------------------------------------------------------------------- /setup/Linux-Setup.md: -------------------------------------------------------------------------------- 1 | Linux Setup 2 | --- 3 | 4 | ### Requirements 5 | 6 | 7 | * Apache or nginx (other servers are untested, but should work) 8 | * PHP 5.4+ 9 | * MySQL 5.5+ or MariaDB 5.5+ 10 | * memcached 11 | * Sphinx 12 | * e-hentai account with ExHentai access 13 | * GP to download galleries 14 | 15 | This "guide" is a copy and paste-tier walkthough of setup on a vanilla Debian Wheezy system. This site uses standard software, and should run on almost anything. If you have experience in the technologies involved then you will not need to follow this word-for-word. 16 | 17 | Windows is generally fine (minus memcache support), though it is outside the scope of this guide. 18 | 19 | Most of the following commands will require running as root. 20 | 21 | #### Inital setup 22 | 23 | First, install dependencies. 24 | 25 | apt-get install apache2 php5 mysql-server php5-memcached php5-mysql php5-curl php5-gd git 26 | 27 | If you don't already have MySQL setup, the configurator will ask you to create a password. Note this password for later. 28 | 29 | Create the path to hold the site files. 30 | 31 | mkdir -p /var/www/vhosts/exhen 32 | cd /var/www/vhosts 33 | 34 | Clone the directory from git. 35 | 36 | git clone https://github.com/kimoi/ExHentai-Archive.git exhen 37 | 38 | Enter the directory and clone the example configuration. 39 | 40 | cd exhen 41 | cp config.json.example config.json 42 | 43 | Open the `config.json.linux` in a text editor an edit the following values. 44 | 45 | * The `accessKey` option is a password used for deleting and adding galleries. 46 | * The `ipb_member_id` and `ipb_pass_hash` in the `cookie` block should be changed to match your ExHentai cookie. 47 | * The `tempDir`, `archiveDir` and `imagesDir` all need to point to server-writable folders on your system. 48 | * The `db` block should stay as it is in most cases, with the exception of `pass`, which you should input whatever password you used for the MySQL setup step. 49 | * SphinxQL and memcache should stay as-is. 50 | 51 | Afterwards rename `config.json.linux` to `config.json` and copy it into the `www` folder, making sure to keep both copies. 52 | 53 | #### MySQL 54 | 55 | Open up the MySQL console. 56 | 57 | mysql -u root -h localhost -p 58 | 59 | You should be prompted for your MySQL password, enter it and you will be dropped into the console. 60 | 61 | Create the database. 62 | 63 | create database exhen; 64 | exit; 65 | 66 | Import the structure into the created database. 67 | 68 | mysql -u root -h localhost -p exhen < /var/www/vhosts/exhen/db.sql 69 | 70 | #### Sphinx 71 | 72 | On Debian there are two ways of doing this. You can use the repo version, which doesn't support random ordering, or you can compile the latest release from source. 73 | 74 | If you can live without random ordering, grab the repo version. 75 | 76 | apt-get install sphinxsearch 77 | 78 | Enable Sphinx to start on boot, edit `/etc/default/sphinxsearch` and change `START=yes`. 79 | 80 | Copy the example Sphinx configuration. 81 | 82 | cp sphinx.conf.example /etc/sphinxsearch/sphinx.conf 83 | cd /etc/sphinxsearch 84 | 85 | Open `sphinx.conf` in an editor and change the `sql_pass` parameter at the top to match your MySQL password. 86 | 87 | Now create the directories to hold the generated indexes. 88 | 89 | mkdir -p /var/lib/sphinxsearch/data/exhen 90 | chown sphinxsearch:sphinxsearch /var/lib/sphinxsearch/data/exhen 91 | 92 | Build the initial indexes. 93 | 94 | sudo -u sphinxsearch indexer --all --rotate 95 | 96 | #### Apache 97 | 98 | Enter `/etc/apache2/sites-enabled` and create a `exhen.localhost` config file. 99 | 100 | Copy the following, you may need to change the /images alias and directive to match the location of your images dir. 101 | 102 | 103 | ServerName exhen.localhost 104 | ServerAlias exhen.localhost 105 | ServerAlias exhen.debian 106 | 107 | DocumentRoot /var/www/vhosts/exhen/www 108 | 109 | Options FollowSymLinks ExecCGI 110 | AllowOverride None 111 | Order allow,deny 112 | allow from all 113 | 114 | 115 | Alias /images /var/www/vhosts/exhen/images 116 | 117 | AllowOverride None 118 | Order allow,deny 119 | allow from all 120 | 121 | 122 | ErrorLog ${APACHE_LOG_DIR}/exhen-error.log 123 | CustomLog ${APACHE_LOG_DIR}/exhen-access.log combined 124 | 125 | 126 | 127 | Restart apache. 128 | 129 | sudo apache2ctl -k graceful 130 | 131 | Add the appropriate hostname to your `/etc/hosts` file. 132 | 133 | sudo sh -c 'echo "127.0.0.1 exhen.localhost" >> /etc/hosts' 134 | 135 | Open up http://exhen.localhost/ in a browser and you should see the default layout with "Displaying 0 of 0 results". 136 | 137 | #### Adding galleries 138 | 139 | Adding galleries is done via a userscript. Open the provided `userscript.js` file in a text editor and change the two values at the top of the code. The `baseUrl` in this example would be http://exhen.localhost and `key` would be the `accessKey` value in the config.json file. 140 | 141 | Once edited, add to your browser via Greasemonkey/Tampermonkey. 142 | 143 | The script will add a "Send to archive" link to the search results page (in thumbnails view) and on the gallery detail page. Clicking it will send it to the archive to be marked for download. Galleries added to the database will be marked in green on the search results page. 144 | 145 | After you have added a few galleries, you can now run the `Archive` task that will download them. 146 | 147 | cd /var/www/vhosts/exhen 148 | php TaskRunner.php Archive 149 | 150 | The task should download the galleries and reindex Sphinx. 151 | 152 | Once completed, reload http://exhen.localhost and you should see your galleries appear! 153 | 154 | #### Cron 155 | 156 | For ideal automation, tasks should be setup in the crontab of your system. Below is an example I use. 157 | 158 | */10 * * * * cd /var/www/vhosts/exhen/ && php TaskRunner.php Archive >> log.txt 2>&1 159 | 0 4 * * * cd /var/www/vhosts/exhen/ && php TaskRunner.php Thumbnails >> log.txt 2>&1 160 | 0 5 * * * cd /var/www/vhosts/exhen/ && php TaskRunner.php Audit >> log.txt 2>&1 161 | 162 | There are 3 main tasks in use. 163 | 164 | * **Archive** - downloads pending galleries in the database; 165 | * **Audit** - updates meta data for added galleries. Will typically update each gallery periodically for new tags, or add newer versions of that gallery to the database. 166 | * **Thumbnails** - generates thumbs for galleries instead of doing it on-the-fly on the frontend. 167 | 168 | #### Extras 169 | 170 | If you find the search results are out of sync with the database, you may need to reindex Sphinx: 171 | 172 | sudo -u sphinxsearch indexer --rotate --all 173 | 174 | ##### Feeds 175 | 176 | Feeds are ways of adding galleries to the database automatically through search terms. The Archive task will run through all feeds in the database, adding new galleries, and optionally downloading these galleries. Support for managing these in a friendly way does not exist. 177 | 178 | If you wish to play around with feeds, get a MySQL database admin tool (MySQL Workbench) and take a look at the feeds table. Create a new row with the `term` matching the search term you want (i.e "comic x-ero"), then `download` should be 1 or 0, depending on if you want the archiver to also download the gallery zips. 179 | -------------------------------------------------------------------------------- /setup/Userscript-Setup.md: -------------------------------------------------------------------------------- 1 | Windows Setup 2 | --- 3 | 4 | ### Editing 5 | 6 | Opening up the `userscript.user.js` file from the root of the repository you'll be presented with the code for the userscript, the main two lines you need to edit are: 7 | 8 | var baseUrl = 'http://your.archive.url.com/'; 9 | var key = 'changeme'; 10 | 11 | For `baseUrl` you'll want to change it to whatever URL your local server is addressed to, the case of the Windows and Linux guides it's `https://exhen.localhost/`. 12 | The Vagrant guide it is meerly just `https://localhost/` 13 | 14 | For `key` this is the value you configured in your `config.json` file. 15 | 16 | 17 | ### Installing 18 | 19 | Now to install the userscript just drag and drop it into your browser, this will open it prompting what ever userscript engine you have to ask if you want to install it. 20 | 21 | Or you can click File -> Open and then open the file, either way the file will be open. 22 | -------------------------------------------------------------------------------- /setup/Vagrant-Setup.md: -------------------------------------------------------------------------------- 1 | Windows Setup 2 | --- 3 | 4 | ### Requirements 5 | 6 | * VirtualBox ( [https://www.virtualbox.org/wiki/Downloads](https://www.virtualbox.org/wiki/Downloads) ) 7 | * Vagrant ( [https://www.vagrantup.com/downloads.html](https://www.vagrantup.com/downloads.html) ) 8 | * ExHentai-Archive ( [https://github.com/Sn0wCrack/ExHentai-Archive/archive/master.zip](https://github.com/Sn0wCrack/ExHentai-Archive/archive/master.zip) ) 9 | 10 | 11 | ### How-To 12 | 13 | Extract the contents of ExHentai-Archive in a safe place, this will be the program's home and will stay there and where it will download all your archives. So I reccomend a drive with a large storage space. 14 | 15 | First we're going to edit some configuration files, the ones we want to focus on have the ```.linux``` extension, mainly the config.json.linux file. 16 | 17 | ``` 18 | { 19 | "base": { 20 | "accessKey": "changeme", 21 | "autoReadPercentage": 80, 22 | "viewType": "mpv", 23 | "cookie": { 24 | "ipb_member_id": "changeme", 25 | "ipb_pass_hash": "changeme", 26 | "hath_perks": "m1.m2.m3.tf.t1.t2.t3.p1.p2.s-210aa44613", 27 | "sp": "changeme", 28 | "sk": "changeme" 29 | }, 30 | "tempDir": "./temp", 31 | "archiveDir": "./archive", 32 | "imagesDir": "./images" 33 | }, 34 | "default": { 35 | "db": { 36 | "dsn": "mysql:host=localhost;dbname=exhen", 37 | "user": "root", 38 | "pass": "changeme" 39 | }, 40 | "sphinxql": { 41 | "dsn": "mysql:host=127.0.0.1;port=9306;dbname=exhen", 42 | "user": "root", 43 | "pass": "" 44 | }, 45 | "memcache": { 46 | "host": "localhost", 47 | "port": 11211 48 | }, 49 | "indexer": { 50 | "full": "sudo indexer --rotate galleries suggested" 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | Inside the configuration file you'll see this, the three major things you need to edit are ```"accessKey"```, ```"ipb_member_id"```, ```"ipb_pass_hash"``` and ```sk```. 57 | accessKey will be password used to confirm you wish to delete archives as well as a passphrase between the browser and server to make sure you're sending a gallery to be archived. 58 | So you can really change this to anything you want, or keep it as changeme, all other changeme instances, can remain the same if you wish. (This is extra setup, chaning all configuration files to make this password.) 59 | 60 | To get ipb_member_id and ipb_pass_hash you have to find your exhentai.org cookie, to find this you have to browse through your browsers cookies. 61 | 62 | Under the ExHetai settings page you need to create a second profile that must use the "List View" settings for the "Front Page Settings" section, this is needed if you wish to view feeds correctly. Now set the "sp" in the "cookie" section to "2". If you don't wish to use feeds in any capacity, please change the cookie to be either blank, or remove it entirely. 63 | 64 | * On firefox you can access this under the options menu under the Privacy tab then clicking "remove individual cookies" and searching for e-hentai and copy pasting the "Content" section into the sections of the configuration file needed 65 | 66 | After that, install Vagrant to your system, and make sure to have restarted your computer after installing it (this may or may not be an issue, just do it just in case.) 67 | 68 | Open a command prompt window and type in ```vagrant plugin install vagrant-vbguest``` and then close the command prompt window after it has finished downloading the plugin. 69 | 70 | If you've changed any other passwords in the config.json.linux file please change the following to the be the same: 71 | * sphinx.conf.linux under ```source connect``` option called ```sql_pass``` 72 | * bootstrap.sh option called ```MYSQL_PASSWORD``` 73 | 74 | Open a new command prompt winodw in the directory where your extracted ExHentai-Archive to and type in ```vagrant up``` this will begin a lengthy process of installing and downloading updates so please give it some time. 75 | 76 | After ```vagrant up``` finishes running, you'll need to run ```vagrant halt``` and then ```vagrant up``` once more, this fixes an issues with certain services that may not be running correctly, etc. 77 | 78 | Afterwards everything should be running smoothly and your vagrant box should be up and running, you can check this by going to ```https://localhost``` in your web browser of choice. **The https is highly important.** 79 | 80 | After this you can proceed to [userscript setup](https://github.com/Sn0wCrack/ExHentai-Archive/blob/master/setup/Userscript-Setup.md) using the baseUrl as "https://localhost/" and key as whatever you set previously. 81 | 82 | If you're on Windows (I assume most using this guide would be) then you can schedule a task to run to open the vagrant box at start up so you don't have to it manually by making a batch file called ```startup.bat``` in the ExHentai-Archive folder similar to the following: 83 | ``` 84 | @echo off 85 | vagrant up 86 | exit 87 | ``` 88 | Then opening "Task Scheduler" and pressing "Action" and "Create Basic Action" setting a name for it, then the rest is self explanatory, when it ask for a program file the .bat file you just made.\ 89 | 90 | Now you can access https://localhost without doing a thing. 91 | 92 | 93 | ### Archiving 94 | 95 | After the initial setup is complete and you've setup your userscript in your browser of choice, to download your archives you'll want to goto ```https://localhost/vagrant/TaskRunner.php?task=Full``` in your browser, this will invoke the Archive, Thumbnails and Auit actions, however due to some limitations you won't be able to get amazingly formatted output. 96 | 97 | After sending galleries to archive, you **NEED** to do this, this is what actually downloads and archives the files to your computer. 98 | 99 | Enjoy it. 100 | -------------------------------------------------------------------------------- /setup/Windows-Setup.md: -------------------------------------------------------------------------------- 1 | Windows Setup 2 | --- 3 | 4 | ### Requirements 5 | 6 | 7 | * Apache or nginx (other servers are untested, but should work) 8 | * PHP 5.4+ 9 | * MySQL 5.5+ or MariaDB 5.5+ 10 | * memcached 11 | * Sphinx 12 | * phpMyAdmin 13 | * Either Firefox or Chrome with Greasemonkey or Tampermonkey installed. 14 | * e-hentai account with ExHentai access 15 | * GP to download galleries 16 | 17 | Guide is still a work in progress, use the Linux one as a skeleton if you know what you're doing. 18 | 19 | #### Inital setup 20 | 21 | First, install dependencies: 22 | 23 | * Apache: [http://www.apachelounge.com/download/](http://www.apachelounge.com/download/) - Get the **win32 or win64 version** 24 | * PHP: [http://windows.php.net/download/](http://windows.php.net/download/) - Get the **Thread Safe x86 or x64 version** 25 | * If using PHP7 then PHP7-memcache.dll [https://github.com/nono303/PHP7-memcache-dll](https://github.com/nono303/PHP7-memcache-dll) - Get the version which matches yours (At the moment that would be VC15/x64/ts) 26 | * MariaDB / MySQL: [https://downloads.mariadb.org/mariadb/](https://downloads.mariadb.org/mariadb/) - Get the **win32 or win64 zip verson** 27 | * memcached: [https://commaster.net/content/installing-memcached-windows](https://commaster.net/content/installing-memcached-windows) - Get the **version 1.4.5 win32 or win64 zip version** 28 | * Sphinx: [http://sphinxsearch.com/downloads/release/](http://sphinxsearch.com/downloads/release/) 29 | * phpMyAdmin: [https://www.phpmyadmin.net/downloads/](https://www.phpmyadmin.net/downloads/) 30 | 31 | Create a folder somewhere and call it whatever you want, for example `server` or `exhen`. 32 | Extract Apache, PHP, MariaDB, memcached and Sphinx into their own folders with the same names. 33 | 34 | Then extract the contents of this repo into the folder in the Apache folder `htdocs` 35 | 36 | Open the `config.json.win32` in a text editor an edit the following values. 37 | 38 | * The `accessKey` option is a password used for deleting and adding galleries. 39 | * The `ipb_member_id`, `ipb_pass_hash` and `sk` in the `cookie` block should be changed to match your ExHentai cookie. 40 | * Create a new Profile in the ExHentai settings page, make sure this profile set to use "List View" under the "Front Page Setttings" section. 41 | * Change the `sp` in the `cookie` block to "2" 42 | * The `tempDir`, `archiveDir` and `imagesDir` all need to point to server-writable folders on your system. 43 | * The `db` block should stay as it is in most cases, with the exception of `pass`, which you should input whatever password you used for the phpMyAdmin setup step. 44 | * SphinxQL change the `full` option to point to where ever you installed Sphinx too. 45 | * memcached should stay the same. 46 | 47 | Afterwards, rename `config.json.win32` to `config.json` and then copy it into the `www` directory as well, making sure to keep both copies. 48 | 49 | #### Apache 50 | 51 | Enter `apache\conf\` and open the `httpd.conf` file in your text editor. 52 | 53 | Change any references of `c:/Apache##/` with the location of your Apache server's location. 54 | 55 | Copy the following, you may need to change the /images alias and directive to match the location of your images directory. 56 | 57 | DocumentRoot C:\path\to\apache\htdocs\exhen 58 | 59 | Options FollowSymLinks ExecCGI 60 | AllowOverride None 61 | Order allow,deny 62 | allow from all 63 | 64 | 65 | Alias /images C:\path\to\apache\htdocs\exhen\images 66 | 67 | AllowOverride None 68 | Order allow,deny 69 | allow from all 70 | 71 | 72 | ErrorLog ${APACHE_LOG_DIR}/exhen-error.log 73 | CustomLog ${APACHE_LOG_DIR}/exhen-access.log combined 74 | 75 | Restart Apache. 76 | 77 | Add the appropriate hostname to your `C:\Windows\system32\drivers\etc\hosts` file. 78 | 79 | Open it in the text editor of choice and add the line 80 | 81 | 127.0.0.1 exhen.localhost 82 | 83 | Change exhen.localhost with whatever your server's name is. You don't have to do this if you just want to keep localhost or use 127.0.0.1. 84 | 85 | Open up http://exhen.localhost/ in a browser and you should see the default layout with "Displaying 0 of 0 results". 86 | 87 | 88 | #### phpMyAdmin and MariaDB / MySQL 89 | 90 | Extract the phpmyAdmin-version#-all.7z file into the Apache folder `htdocs` under a folder named `phpMyAdmin` (so `apache\htdocs\phpMyAdmin\...`) 91 | 92 | Run `httpd.exe` and `mysqld.exe`. and go to `http://localhost/phpMyAdmin` in your browser. 93 | 94 | Follow the prompts given my phpMyAdmin and it should all be setup. 95 | 96 | Go to the import tab and click the Browse button, and find the `db.sql` file from the repo files and press go. 97 | 98 | This will setup the database for you. 99 | 100 | 101 | #### Sphinx 102 | 103 | To setup Sphinx you must first rename the file `sphinx.conf.win32` to `sphinx.conf` and then open it. 104 | Under the `source connect` structure change the `sql_user`, `sql_pass` and any other value to suit your MySQL / MariaDB setup. 105 | 106 | On Windows you have the option to install Sphinx as a service, this way it will start with your computer whenever you turn it on. 107 | 108 | This can be done by opening a command window in the `bin` directory of the Sphinx root folder and running this command: 109 | 110 | searchd.exe --install --config "C:\path\to\sphinx.conf" 111 | 112 | Of course you can just open Sphinx manually every time you want to run it with this command: 113 | 114 | searchd.exe --config "C:\path\to\sphinx.conf" 115 | 116 | After you have either setup a script to open Sphinx using the previous command or have installed it as a service, close it down / end the service and run this command from the `bin` directory 117 | 118 | indexer.exe --config "C:\path\to\sphinx.conf --all --rotate 119 | 120 | This will setup all the needed indexes for Sphinx syncing with the data from the database. 121 | 122 | #### memcahced 123 | 124 | Open a command window in the memcached folder and type in the command: 125 | 126 | memcached.exe -d start 127 | 128 | This will install memcached as a service on Windows and will start with your computer from now on. 129 | 130 | You can change the memory pool size (currently defaults to 64MB, which seems fine for me) with the command: 131 | 132 | memcached.exe -m SIZE_IN_MBS 133 | 134 | #### Adding galleries 135 | 136 | Adding galleries is done via a userscript. Open the provided `userscript.js` file in a text editor and change the two values at the top of the code. The `baseUrl` in this example would be http://exhen.localhost and `key` would be the `accessKey` value in the config.json file. 137 | 138 | Once edited, add to your browser via Greasemonkey/Tampermonkey. 139 | 140 | The script will add a "Send to archive" link to the search results page (in thumbnails view) and on the gallery detail page. Clicking it will send it to the archive to be marked for download. Galleries added to the database will be marked in green on the search results page. 141 | 142 | After you have added a few galleries, you can now run the `Archive` task that will download them. 143 | 144 | cd C:\path\to\apache\htdocs\exhen\ 145 | php TaskRunner.php Archive 146 | 147 | The task should download the galleries and reindex Sphinx. 148 | 149 | Once completed, reload http://exhen.localhost and you should see your galleries appear! 150 | 151 | #### Task Scheduler 152 | 153 | You can setup a Task through Windows built in Task Scheduler. 154 | 155 | First create a simple script using Batch or any language of your choice that will automate the download process 156 | 157 | Below is an example: 158 | 159 | @echo off 160 | E:\NPMDB\php\php.exe TaskRunner.php Archive 161 | E:\NPMDB\php\php.exe TaskRunner.php Thumbnails 162 | E:\NPMDB\php\php.exe TaskRunner.php Audit 163 | exit 164 | 165 | Then opening up the Task Scheduler and right click and go to `Create Basic Task...` and follow the prompts. When asked for a script or program open the script you've created. 166 | 167 | There are 3 main tasks in use. 168 | 169 | * **Archive** - downloads pending galleries in the database; 170 | * **Audit** - updates meta data for added galleries. Will typically update each gallery periodically for new tags, or add newer versions of that gallery to the database. 171 | * **Thumbnails** - generates thumbs for galleries instead of doing it on-the-fly on the frontend. 172 | 173 | #### Extras 174 | 175 | If you find the search results are out of sync with the database, you may need to reindex Sphinx: 176 | 177 | indexer --config "C:\path\to\sphinx.conf --rotate --all 178 | 179 | ##### Feeds 180 | 181 | **NOTE**: I've never used this personally, so this is the same as the old guide. 182 | 183 | Feeds are ways of adding galleries to the database automatically through search terms. The Archive task will run through all feeds in the database, adding new galleries, and optionally downloading these galleries. Support for managing these in a friendly way does not exist. 184 | 185 | If you wish to play around with feeds, get a MySQL database admin tool (MySQL Workbench or phpMyAdmin) and take a look at the feeds table. Create a new row with the `term` matching the search term you want (i.e "comic x-ero"), then `download` should be 1 or 0, depending on if you want the archiver to also download the gallery zips. 186 | -------------------------------------------------------------------------------- /sphinx.conf.linux: -------------------------------------------------------------------------------- 1 | source connect { 2 | type = mysql 3 | sql_host = localhost 4 | sql_port = 3306 5 | sql_sock = /var/run/mysqld/mysqld.sock 6 | sql_user = root 7 | sql_pass = changeme 8 | sql_db = exhen 9 | } 10 | 11 | source galleries : connect { 12 | sql_attr_uint = posted 13 | sql_attr_uint = added 14 | sql_attr_bool = archived 15 | sql_attr_uint = deleted 16 | sql_attr_string = read 17 | sql_attr_string = color 18 | 19 | sql_query_pre = set names utf8 20 | sql_query_pre = set session group_concat_max_len = 10240 21 | sql_query_pre = replace into sph_counter select 1, NOW() 22 | sql_query = select gallery.id, gallery.archived, gallery.deleted, gallery.read, gallery.color, UNIX_TIMESTAMP(gallery.posted) as posted, UNIX_TIMESTAMP(gallery.added) as added, gallery.id, gallery.exhenid, gallery.name, gallery.origtitle, gallery.type, group_concat(tag.name, ',', tagnamespace.name, ':', tag.name) as tags from exhen.gallery \ 23 | left join gallery_tag on gallery_tag.gallery_id = gallery.id \ 24 | left join tag on tag.id = gallery_tag.tag_id and gallery_tag.id is not null \ 25 | left join tagnamespace on tagnamespace.id = gallery_tag.namespace_id and gallery_tag.id is not null \ 26 | where updated <= (select last_updated from sph_counter where counter_id = 1) \ 27 | group by gallery.id \ 28 | limit 0, 100000000 29 | } 30 | 31 | source galleries_delta : galleries { 32 | sql_query_pre = set names utf8 33 | sql_query_pre = set session group_concat_max_len = 10240 34 | sql_query_pre = set @starttime = NOW() 35 | sql_query = select gallery.id, gallery.archived, gallery.deleted, UNIX_TIMESTAMP(gallery.posted) as posted, UNIX_TIMESTAMP(gallery.added) as added, gallery.id, gallery.exhenid, gallery.name, gallery.origtitle, gallery.type, group_concat(tag.name, ',', tagnamespace.name, ':', tag.name) as tags from exhen.gallery \ 36 | inner join gallery_tag on gallery_tag.gallery_id = gallery.id \ 37 | inner join tag on tag.id = gallery_tag.tag_id \ 38 | inner join tagnamespace on tagnamespace.id = gallery_tag.namespace_id \ 39 | where updated > (select last_updated from sph_counter where counter_id = 1) and updated <= @starttime \ 40 | group by gallery.id \ 41 | limit 0, 1000000 42 | sql_query_post = replace into sph_counter select 1, @starttime 43 | } 44 | 45 | source suggested_tags : connect { 46 | sql_attr_string = keyword 47 | sql_attr_uint = freq 48 | 49 | sql_query_pre = set names utf8 50 | sql_query = select tag.id, tag.name as text, tag.name as keyword, count(*) as freq from exhen.tag \ 51 | left join gallery_tag on gallery_tag.tag_id = tag.id \ 52 | group by tag.id 53 | } 54 | 55 | index galleries { 56 | source = galleries 57 | path = /var/lib/sphinxsearch/data/exhen/galleries 58 | min_word_len = 2 59 | 60 | blend_chars = :, @ 61 | blend_mode = trim_none 62 | 63 | ngram_len = 1 64 | ngram_chars = U+4E00..U+9FBF, U+3040..U+309F, U+30A0..U+30FF # kanji, hiragana, katakana 65 | } 66 | 67 | index suggested { 68 | source = suggested_tags 69 | path = /var/lib/sphinxsearch/data/exhen/suggested 70 | min_prefix_len = 1 71 | } 72 | 73 | indexer { 74 | mem_limit = 64M 75 | } 76 | 77 | searchd { 78 | listen = localhost:9312 79 | listen = 9306:mysql41 80 | log = /etc/sphinxsearch/log/searchd_exhen.log 81 | query_log = /etc/sphinxsearch/log/query_exhen.log 82 | pid_file = /etc/sphinxsearch/searchd.pid 83 | } 84 | -------------------------------------------------------------------------------- /sphinx.conf.win32: -------------------------------------------------------------------------------- 1 | source connect { 2 | type = mysql 3 | sql_host = localhost 4 | sql_port = 3306 5 | sql_user = root 6 | sql_pass = changeme 7 | sql_db = exhen 8 | } 9 | 10 | source galleries : connect { 11 | sql_attr_uint = posted 12 | sql_attr_uint = added 13 | sql_attr_bool = archived 14 | sql_attr_uint = deleted 15 | sql_attr_string = read 16 | sql_attr_string = color 17 | 18 | sql_query_pre = set names utf8 19 | sql_query_pre = set session group_concat_max_len = 10240 20 | sql_query_pre = replace into sph_counter select 1, NOW() 21 | sql_query = select gallery.id, gallery.archived, gallery.deleted, gallery.read, gallery.color, UNIX_TIMESTAMP(gallery.posted) as posted, UNIX_TIMESTAMP(gallery.added) as added, gallery.id, gallery.exhenid, gallery.name, gallery.origtitle, gallery.type, group_concat(tag.name, ',', tagnamespace.name, ':', tag.name) as tags from exhen.gallery \ 22 | left join gallery_tag on gallery_tag.gallery_id = gallery.id \ 23 | left join tag on tag.id = gallery_tag.tag_id and gallery_tag.id is not null \ 24 | left join tagnamespace on tagnamespace.id = gallery_tag.namespace_id and gallery_tag.id is not null \ 25 | where updated <= (select last_updated from sph_counter where counter_id = 1) \ 26 | group by gallery.id \ 27 | limit 0, 100000000 28 | } 29 | 30 | source galleries_delta : galleries { 31 | sql_query_pre = set names utf8 32 | sql_query_pre = set session group_concat_max_len = 10240 33 | sql_query_pre = set @starttime = NOW() 34 | sql_query = select gallery.id, gallery.archived, gallery.deleted, UNIX_TIMESTAMP(gallery.posted) as posted, UNIX_TIMESTAMP(gallery.added) as added, gallery.id, gallery.exhenid, gallery.name, gallery.origtitle, gallery.type, group_concat(tag.name, ',', tagnamespace.name, ':', tag.name) as tags from exhen.gallery \ 35 | inner join gallery_tag on gallery_tag.gallery_id = gallery.id \ 36 | inner join tag on tag.id = gallery_tag.tag_id \ 37 | inner join tagnamespace on tagnamespace.id = gallery_tag.namespace_id \ 38 | where updated > (select last_updated from sph_counter where counter_id = 1) and updated <= @starttime \ 39 | group by gallery.id \ 40 | limit 0, 1000000 41 | sql_query_post = replace into sph_counter select 1, @starttime 42 | } 43 | 44 | source suggested_tags : connect { 45 | sql_attr_string = keyword 46 | sql_attr_uint = freq 47 | 48 | sql_query_pre = set names utf8 49 | sql_query = select tag.id, tag.name as text, tag.name as keyword, count(*) as freq from exhen.tag \ 50 | left join gallery_tag on gallery_tag.tag_id = tag.id \ 51 | group by tag.id 52 | } 53 | 54 | index galleries { 55 | source = galleries 56 | path = C:\path\to\sphinx\data\exhen\galleries 57 | min_word_len = 2 58 | 59 | blend_chars = :, @ 60 | blend_mode = trim_none 61 | 62 | ngram_len = 1 63 | ngram_chars = U+4E00..U+9FBF, U+3040..U+309F, U+30A0..U+30FF # kanji, hiragana, katakana 64 | } 65 | 66 | index suggested { 67 | source = suggested_tags 68 | path = C:\path\to\sphinx\data\exhen\suggested 69 | min_prefix_len = 1 70 | enable_star = 1 71 | } 72 | 73 | indexer { 74 | mem_limit = 64M 75 | } 76 | 77 | searchd { 78 | listen = localhost:9312 79 | listen = 9306:mysql41 80 | log = C:\path\to\sphinx\searchd_exhen.log 81 | query_log = C:\path\to\sphinx\query_exhen.log 82 | pid_file = C:\path\to\sphinx\searchd_exhen.pid 83 | } 84 | -------------------------------------------------------------------------------- /ssl/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE7jCCAtYCCQDPYNtTpFg0ATANBgkqhkiG9w0BAQsFADA5MQswCQYDVQQGEwJK 3 | UDEPMA0GA1UECAwGSGVudGFpMRkwFwYDVQQKDBBFeEhlbnRhaS1BcmNoaXZlMB4X 4 | DTE2MDgyMjEwNDQxM1oXDTQ0MDEwNzEwNDQxM1owOTELMAkGA1UEBhMCSlAxDzAN 5 | BgNVBAgMBkhlbnRhaTEZMBcGA1UECgwQRXhIZW50YWktQXJjaGl2ZTCCAiIwDQYJ 6 | KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJyje6vrG9vtovH44kSyEPyjJ/Rhe2qT 7 | mhPwE4abtpm/DQGXxle/64dGJds8jjZYH/uywMx9FsN+NBYNuLSIilhjO+KMoqYe 8 | 5M73K+EXWZAexXIBzCes7jZkK6CccmgMsgdzbvoxgu0C4G6iQW/+SPoIs4Tc9HQ0 9 | D+y/AeRIZWQxGa7FPcFM/+WoALQBWAYTjQonUwyMEM1579m4imVvzAvniiO86vMO 10 | 7J1HiDT/2P0JiRKV71srO1kb5GtaxRSmHUC9O/B8rZJMY839VZIxYe1OlopzuRGP 11 | V5yKp/H3tcxw/2b5X44NX4wdc9+CccDBmOaKc372jDWYvaXwj7X4gU9w7/oRuIX1 12 | /v0wDcotF9H9v6teFRyheOjuYi9x3qQc8LIiZxWRQfqen1X6qCUdOj+MVa6mmnlm 13 | XU+80+JQt/W//TT3e5Ez4ksW2G7kg0MyZMT/lZJrTgQAFioEkqBqnt4VeWEJXqKZ 14 | U4gFpJmHxeQF0YAqvO+vrvv1PlJ79vRRWfoS/tA7D7PEd+EJpAhNe+CY3hzqK2C4 15 | EoEpk4HUhES3EzDLY9ypZPOXTSkaoX+1ieRCATBPDfbagjJtmUqk0Pg2gRHa5RhV 16 | lgDPHnazrNu4HOQTNPX75w7QobCdOjYScQoBWhXyuxPWc/GYLFnYWklM/PODuWSl 17 | N+vEmfwd+NF/AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAD7YY4tT+4xime9sy9a6 18 | RmtGuJwQF1p6g386RF28F7LmraBwQ8qaAPq/hAWad5mqklRpsaSaLBQC8f3aIGzd 19 | xfCBALBjv6KCT3UZD1HGhhzyBK/apU6+rpY39C/jfdjJUctPzPQ6J+v32ajfqENn 20 | HsVcSTbCuJnfyY7nrYZ3RrP84liKap0B8TILCGo4sRaDPRWLEvJ3Gm6BBLO0eq1N 21 | JBnnaObg0v1utHbx0Od+kr9inCod6oDzQbZ3QFqm73wKtty4VjRd17z17tWIR48i 22 | kH2xH2hqjgc06tdVDgOK7ejIVNBB8ASCEYQngi4l0dOgyd5Udk+fG7x6p28WYl70 23 | NRRfBFt/vxfHkCmFC+8fU9qjGJRf8mnoToz5iUZkAHXHbgQNjK1tgdz/zPrlPzkj 24 | dHdg7i9UqFFPvpG3k5erN4ctmyGAxb/Wz57AGmtnduMu+SXfOhp1mKW/rJAXMjbG 25 | CnT/PbW/4zhAkcpYHvTaRFdL4DquefM00RdwJiU6Ep4yCSwXRFiSwNuxeVsaG07o 26 | 7XrhfkkNIGjFKadmyhRll64MJuBJrSrUgcWvcUc+CjAw+4vhn71vSaPCvQ9WKSZJ 27 | PqxXrCULo0A8xnqoaRBgbuL8qMVdz+Vem1/6fyaKzLf8anC/BFW48TuRWReKaWPs 28 | bfgowXzZ0rlslapcddFQaxTR 29 | -----END CERTIFICATE----- 30 | -------------------------------------------------------------------------------- /ssl/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEfjCCAmYCAQAwOTELMAkGA1UEBhMCSlAxDzANBgNVBAgMBkhlbnRhaTEZMBcG 3 | A1UECgwQRXhIZW50YWktQXJjaGl2ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC 4 | AgoCggIBAJyje6vrG9vtovH44kSyEPyjJ/Rhe2qTmhPwE4abtpm/DQGXxle/64dG 5 | Jds8jjZYH/uywMx9FsN+NBYNuLSIilhjO+KMoqYe5M73K+EXWZAexXIBzCes7jZk 6 | K6CccmgMsgdzbvoxgu0C4G6iQW/+SPoIs4Tc9HQ0D+y/AeRIZWQxGa7FPcFM/+Wo 7 | ALQBWAYTjQonUwyMEM1579m4imVvzAvniiO86vMO7J1HiDT/2P0JiRKV71srO1kb 8 | 5GtaxRSmHUC9O/B8rZJMY839VZIxYe1OlopzuRGPV5yKp/H3tcxw/2b5X44NX4wd 9 | c9+CccDBmOaKc372jDWYvaXwj7X4gU9w7/oRuIX1/v0wDcotF9H9v6teFRyheOju 10 | Yi9x3qQc8LIiZxWRQfqen1X6qCUdOj+MVa6mmnlmXU+80+JQt/W//TT3e5Ez4ksW 11 | 2G7kg0MyZMT/lZJrTgQAFioEkqBqnt4VeWEJXqKZU4gFpJmHxeQF0YAqvO+vrvv1 12 | PlJ79vRRWfoS/tA7D7PEd+EJpAhNe+CY3hzqK2C4EoEpk4HUhES3EzDLY9ypZPOX 13 | TSkaoX+1ieRCATBPDfbagjJtmUqk0Pg2gRHa5RhVlgDPHnazrNu4HOQTNPX75w7Q 14 | obCdOjYScQoBWhXyuxPWc/GYLFnYWklM/PODuWSlN+vEmfwd+NF/AgMBAAGgADAN 15 | BgkqhkiG9w0BAQsFAAOCAgEACwCSOuebsoOysypivrEuhUzAZdR7I9mc0tIvJpGs 16 | jM+h05vajRLiyuHlCyINnAn33x4kb/wSYnAcx7yvulP3VwkH620IH7n5A7OYxeav 17 | BEOXHRI6HeEsh5yaQoF8qbi/eIXNwLKyitMPdXg1VBF9HX3rwBaRVQqf2P0WxUDu 18 | UXHO0tBHvdqfL6/U3H7XKjW7n0d0cliQM3P3xFOg/tfArGxwKqWq0VSWQ/+B35qk 19 | u6ICw+Sg9jTe3jUn9A0zQ5MgFqCs9NlN3LnQTSnSftntoFJwU44FWTURk5z1ekRi 20 | hbXdDU9QR2OfTLRaCHonQq7kPS6iLOGd8J0NWaabgJ+XKjLHWi5HEb5PiR2nq7K2 21 | EhiOT0cGwW/IiOlMbD4oAQMr99BIBrmJpjdErT+gW2E2Y/3rS5QrNNKge3ldl5bF 22 | K/oc+F+VHLbxxImUBTrPQfSHSF2JDDiCCA1oQaaiOX0KRr2CD0qVbqcOYHBPQFpJ 23 | 1MzobjZkaKor6ZrYQor/fqSe+8EYKC2I8e0lLZrdh0uHR1IFC7+S/DgQVkmJh1YL 24 | I7gpu3v80w/vUjKBe+CduxPba1n5BKypN9/KymtztN99f+SLnZ6TAKpVUmu3ZPOy 25 | K4VO6/Fu+JTsH05SNZDUHHJR35a/8iqL2ngFfNfeN5exm/dhkiEfa+Cloq4sc4T4 26 | fOw= 27 | -----END CERTIFICATE REQUEST----- 28 | -------------------------------------------------------------------------------- /ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCco3ur6xvb7aLx 3 | +OJEshD8oyf0YXtqk5oT8BOGm7aZvw0Bl8ZXv+uHRiXbPI42WB/7ssDMfRbDfjQW 4 | Dbi0iIpYYzvijKKmHuTO9yvhF1mQHsVyAcwnrO42ZCugnHJoDLIHc276MYLtAuBu 5 | okFv/kj6CLOE3PR0NA/svwHkSGVkMRmuxT3BTP/lqAC0AVgGE40KJ1MMjBDNee/Z 6 | uIplb8wL54ojvOrzDuydR4g0/9j9CYkSle9bKztZG+RrWsUUph1AvTvwfK2STGPN 7 | /VWSMWHtTpaKc7kRj1eciqfx97XMcP9m+V+ODV+MHXPfgnHAwZjminN+9ow1mL2l 8 | 8I+1+IFPcO/6EbiF9f79MA3KLRfR/b+rXhUcoXjo7mIvcd6kHPCyImcVkUH6np9V 9 | +qglHTo/jFWuppp5Zl1PvNPiULf1v/0093uRM+JLFthu5INDMmTE/5WSa04EABYq 10 | BJKgap7eFXlhCV6imVOIBaSZh8XkBdGAKrzvr6779T5Se/b0UVn6Ev7QOw+zxHfh 11 | CaQITXvgmN4c6itguBKBKZOB1IREtxMwy2PcqWTzl00pGqF/tYnkQgEwTw322oIy 12 | bZlKpND4NoER2uUYVZYAzx52s6zbuBzkEzT1++cO0KGwnTo2EnEKAVoV8rsT1nPx 13 | mCxZ2FpJTPzzg7lkpTfrxJn8HfjRfwIDAQABAoICAH4P9JA//4PYlLM5IqE2zgUF 14 | Kjq72Z/EetRg+tXyq9rAr68Af1hP1TZhdkYjTjDea58eFZx9b7yV7UVydZyV5wGL 15 | m76QgLZBVtRaiK33JNWgbjd6ytuDiZOsuo/gWRL0ZLMqa75f6oblMcrYOuHvPLw+ 16 | FHxxdyuuWsKmVtdqjG8+NPU7OKBBYBFsTGSSUE1Tnsb1LxmCAGPeJvKKWpeYihdq 17 | mPp6KHzFHhjWYQCGiBPdgLu8f45YFMN6dzMPMl2T8ycPJRY+wVJkuE4n/J5TsARU 18 | o0N6Lw+61T/fN5q9SUroBiTRvd2NVcBmU8MmMHZ5WJzyAFHGCsF30+mg2YgSH76V 19 | LftMJ3JFNV4zlPtdfsTVx4eYu0kRD4XVbTPXZYYTfqv4NFcmvPVHXCHW9Aq/K/1y 20 | AbHG43eBLWkOwoMen2t/qHqeSl8icTk4TkrjUYj50QJxQcfZh7v4YZqW9ZWmXSu6 21 | MYa26JCmyO/20yih8w3LB1eyxjYrChKNVcl/GsOueFzXudPxQOxAs0oNo2dyzjls 22 | H+39qQbuQuJtM6X+KAjJDEybBsUJgnUEb99ds7A6mnCSnTfMC9mbw1FlBSKZl8y7 23 | 0Bp1gM2LYp29pMXPRb9VkCKuKsh4VnOTnwBEyXg7Aq8nT/Y3sK8NSHC+I3l/zWIB 24 | x0nXKXpAWQhH8MSoiWABAoIBAQDNpIgFEVoYAKXHTqSURo7LDp3GYimKh7rSIQog 25 | PuYxiVGkq6xIYB2nJZETO2y50fwMI07vxx/p6/caEojrAAyn1NTVDj5r3GorN/VO 26 | /o8vYVmcr94w/gDJ0n0N0fNY9igtYizHrO9xfi4XJf4PXbmvk0k58AGAyZTnMWXZ 27 | viJr6TTfYOa0HQ14QJwnnb1ySmtjA6vQB4uXZcfvhVkBr8Rg0VsFWq27t6ai40o6 28 | f7x4DiGyukBbMJozVPwaYmCIsLyyU33c+y/DkhHnSQ6IRag5MhWBHjL6CdRBJMw5 29 | /7Q/h9uXPlWuPNsrWJlEy1JU5TQgP4SPZzGFN1jbbZxQr1oBAoIBAQDC/vMvPIP8 30 | jcJq3eeTLUarqWqgMEfy5+fEVKuvFDPWpWO4KxY24m0UR6yBHtInu69Wu0RMSsG4 31 | NzVR5VWqC5OsxqupuNu/NP/SKEiS7ybQIz0XUMAX6XolEynpvnqwQxWzc6bWzR2C 32 | 9R6aGpYWTooZQag7yiFQoyv/rp0+S0n9EVnqn+nclMXljdgyJCqFMrf8JZDWT3RA 33 | qTnAJDLpDMUPIvBCHf7RX36LeURs31YPpvYZosGfloKhNjv+Ecu2f6TDrczPdVQv 34 | J+IrHLgZVJHpd3vBqrUDgA8P2B8eGrh5M5sqPSfxXYiMtMSqac51LHatuUzK1eap 35 | ZwB7UiHw3St/AoIBAQCTgJCQEgItdUlzzFUAhhumSrWgtohVdUXrswcwWe2DWpvV 36 | Ic5Rm5+pZKjKwrUaFCRWEH1uP8YEY9y6NtE+vqpVN7PxnmXnuBHvLhQmtTC/K3S7 37 | juow+gBWw0QvxYhnJylqZCD6mHCnV3p34Ch7nR6zcVH03NI3LmA/9I65wEewX3Yt 38 | Q7Q3PR+MQcvAHsTkVbzxKJD0hnT6A2eFxoAxVYHmcER5crFrFC/SZnW3wCfPdVN2 39 | f2HqBtKUP0EVK7tSUHMI9hoxh3qbLQrqetyRomUnoWULWD34W1SD5YEXmPIRH8HC 40 | mlOaxsvuK37EeZJ0knLxLp5Gvh4nD6wMuHKG7UABAoIBAH8QBMUmCNd8RFDA0pKr 41 | 8R4Q4mMJswiMpxDfH5SRdIoM8aLjqLEQ8IVDgxwMSmMLwOCiXIneOMdiakM4To7k 42 | xlDSkK+ivsaksYJvL4NXhRIhtEBKJTIvuKEKEMi4j1fmDvEFjpTvyag5M7y1UFGu 43 | 0fxNNWPofbb+7L/KN7qM8uSN9uqVU52h8CZ6PIPH31E2UH9ktzF/SsCLxQ74R5hm 44 | 8s2/NZHP2+jw8hPObJEJIxpF8J2Z2dO1DuAf6A3R6M299U5xJUGWMcockhsHtssC 45 | uaXJbwIuy49BVg88BGLohIat4xEuEqYMDduqO3DUS209EQR495pAsfJ3JPA2/9jl 46 | Nd8CggEAKfu486YTaXxdJCrr1545zQpWyu2jn1T9IbToHGKNMmvva4YFOcRCkV9D 47 | LX6Sd4xvMYeoTC2na2+o6JkqsUKUZJUGC7pWQHoRZjPhrQwetH4mZBjWzU4MBuo6 48 | /J5evtIZRLUBi+T8l4epYecWYSSehG57/bWBbwtF9sT3GTOT2NDzuVX1Nvy1cinK 49 | yQjFN+cziPFRvcAXlVmv874cT5mLUFPs0BdkrS1jGABk/JQ36zK/S+HaVQoIqokD 50 | E+jV94PVqRrdRE9GrX64BhMTC+9/jcNsa57+KuQcMYCWNALyXfENYoguKQ53/qun 51 | imtovH9OEB2lHcddZEHt5clnI/xoBg== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /userscript.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name ExHentai Archive 3 | // @match *://exhentai.org/* 4 | // @match *://e-hentai.org/* 5 | // @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js 6 | // ==/UserScript== 7 | 8 | var baseUrl = '//your.archive.url.com/'; 9 | var key = 'changeme'; 10 | 11 | function createArchiveLink(gid, token) { 12 | var link = $(''); 13 | link.data('gid', gid); 14 | link.data('token', token); 15 | 16 | link.on('click', function() { 17 | $.getJSON(baseUrl + 'api.php', { action: 'addgallery', gid: link.data('gid'), token: link.data('token'), key: key }, function(data, result) { 18 | if(data.ret === true && result === 'success') { 19 | $(link).css({ 20 | color: '#777', 21 | pointerEvents: 'none' 22 | }); 23 | } 24 | else { 25 | alert('An error occured while adding to archive'); 26 | } 27 | }); 28 | 29 | return false; 30 | }); 31 | 32 | return link; 33 | } 34 | 35 | $('div#gd5').each(function() { //archive button on gallery detail 36 | var container = $(this); 37 | 38 | $.getJSON(baseUrl + 'api.php', { action: 'hasgallery', gid: gid, key: key }, function(data, result) { 39 | if(data.data.exists) { 40 | var p = $('

'); 41 | var link = ""; 42 | if (data.data.deleted == 0) { 43 | link = $('Archived'); 44 | } else if (data.data.deleted >= 1) { 45 | link = $('Deleted'); 46 | } 47 | 48 | if(data.data.archived && data.data.deleted == 0) { 49 | link.prop('href', baseUrl + '?' + $.param({ action: 'gallery', id: data.data.id })); 50 | } 51 | else if (!data.data.archived) { 52 | link.on('click', function() { 53 | alert('Not yet downloaded'); 54 | return false; 55 | }); 56 | } 57 | 58 | link.appendTo(p); 59 | $('.g2', container).last().after(p); 60 | } 61 | else { 62 | var p = $('

'); 63 | var link = createArchiveLink(gid, token); 64 | link.appendTo(p); 65 | 66 | $('.g2', container).last().after(p); 67 | } 68 | }); 69 | }); 70 | 71 | $('div.itg').each(function() { //gallery search 72 | var container = $(this); 73 | var galleries = $('div.gl1t', container); 74 | var gids = [ ]; 75 | 76 | galleries.each(function() { 77 | var galleryContainer = $(this); 78 | var link = $('a', galleryContainer).prop('href'); 79 | 80 | var bits = link.split("/"); 81 | 82 | var gid = bits[4]; 83 | var token = bits[5]; 84 | 85 | gids.push(gid); 86 | 87 | galleryContainer.data('gid', gid); 88 | 89 | $.getJSON(baseUrl + 'api.php', { action: 'hasgallery', gid: gid, key: key }, function(data, result) { 90 | if (!data.data.exists) { 91 | var link = createArchiveLink(gid, token); 92 | link.css({ 'text-align': 'center', 'font-size': '12px' }); 93 | link.on('click', function() { 94 | galleryContainer.css({background: 'green'}); 95 | }); 96 | 97 | link.appendTo(galleryContainer); 98 | } else { 99 | var res = ''; 100 | if (data.data.archived && data.data.deleted == 0) { 101 | res = $('

Archived

'); 102 | galleryContainer.css({background: 'green'}); 103 | } 104 | 105 | if (data.data.deleted >= 1) { 106 | res = $('

Deleted

'); 107 | galleryContainer.css({background: '#AA0000'}); 108 | } 109 | 110 | res.appendTo(galleryContainer); 111 | } 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /www/api.php: -------------------------------------------------------------------------------- 1 | handle($_REQUEST); 8 | 9 | ?> -------------------------------------------------------------------------------- /www/font/OpenSans.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/font/OpenSans.eot -------------------------------------------------------------------------------- /www/font/OpenSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/font/OpenSans.ttf -------------------------------------------------------------------------------- /www/font/OpenSans.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/font/OpenSans.woff -------------------------------------------------------------------------------- /www/font/OpenSans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/font/OpenSans.woff2 -------------------------------------------------------------------------------- /www/font/foundation-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/font/foundation-icons.eot -------------------------------------------------------------------------------- /www/font/foundation-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/font/foundation-icons.ttf -------------------------------------------------------------------------------- /www/font/foundation-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/font/foundation-icons.woff -------------------------------------------------------------------------------- /www/img/arrow-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/img/arrow-next.png -------------------------------------------------------------------------------- /www/img/arrow-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/img/arrow-prev.png -------------------------------------------------------------------------------- /www/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/img/icon.png -------------------------------------------------------------------------------- /www/img/input-clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sn0wCrack/ExHen-Archive/ded46525e953f8f1053edd743a9207ad1ea4b189/www/img/input-clear.png -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ExHentai Archive 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 | 24 |
    25 |
    26 |
    27 |
    28 | 39 | 40 |
    41 |
    42 |
    43 | 44 |
    45 | 46 |
    47 | 55 |
    56 |
    57 |
    58 | 59 |
    60 |
    61 |
    62 | 69 | 77 | 90 |
    91 | 92 |
    93 |
    94 | 95 |
    96 |
    97 |
    98 |
    99 |
    100 |
    101 |
    102 |
    103 | Click to load previous 104 |
    105 | 108 |
    109 | Click to load next 110 |
    111 |
    112 |
    113 |
    114 |
    115 |
    116 |
    117 |
    118 |
    119 |
    120 |
    121 |
    122 | 123 |
    124 | 129 | 130 | 137 | 138 | 155 |
    End
    156 |
    157 |
    158 | 159 |
    160 |
    161 |
    162 | 171 |
    172 | 173 | 174 |
    175 |
    176 |
    177 | 178 | 179 |
    180 |
    181 |
    182 | 183 |
    184 | 185 | 186 |
    187 | 188 |
    189 |
    190 |
    191 |
    192 |
    193 |
    194 |
    195 |
    196 | 197 | -------------------------------------------------------------------------------- /www/js/jquery.mousewheel.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Mousewheel 3.1.13 3 | * 4 | * Copyright 2015 jQuery Foundation and other contributors 5 | * Released under the MIT license. 6 | * http://jquery.org/license 7 | */ 8 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.12",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b),d=c["offsetParent"in a.fn?"offsetParent":"parent"]();return d.length||(d=a("body")),parseInt(d.css("fontSize"),10)||parseInt(c.css("fontSize"),10)||16},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})}); --------------------------------------------------------------------------------