Compare commits

..
48 Commits
Author SHA1 Message Date
death-claw 6269ed6f6e v2.8.0 2019-12-24 19:47:17 +01:00
death-claw 43b8dedbb1 Use pointer cursor on gallery items 2019-12-24 19:42:45 +01:00
death-claw 978be7bd49 Add view photos settings
Some improvements
2019-12-23 22:46:46 +01:00
death-claw 1b7f3aef62 Add missing css and script includes 2019-12-23 19:36:07 +01:00
death-claw 7d24ee4867 Use photoswipe for image gallery 2019-12-23 00:02:13 +01:00
death-claw 269a3475b0 Generate thumbnails on the fly and cache them 2019-12-21 19:17:51 +01:00
death-claw 18e2bc6afc Add Photo Viewer 2019-12-21 17:23:22 +01:00
death-claw c6ef54e115 v2.7.1 2019-12-21 11:24:03 +01:00
death-claw 441b2831fc Add version to title 2019-12-21 11:16:53 +01:00
death-claw 3cf4d60eca v2.7.0 2019-12-21 09:59:17 +01:00
death-claw d228c89a1c Add support for pixxxels
Add missing icon
2019-12-21 09:56:57 +01:00
death-claw f3b592e81b v2.6.0 2019-12-21 08:39:04 +01:00
death-claw a5ff659fb4 Better quality app icon
Implement context menu on download tab
Add stop remove and start button to context menu
Link will not be removed automatically from link collector tab
Some UI fixes
2019-12-21 08:34:56 +01:00
death-claw ab80e2f3b3 v2.5.1 2019-12-09 20:26:52 +01:00
death-claw 2ef082f87e Add hosts column in Link collector grab menu
Disable download button when nothing selected in Link collector grab menu
2019-12-09 19:56:26 +01:00
death-claw a1b97e76a7 v2.5.0 2019-12-08 20:13:58 +01:00
death-claw 185a078b4d Missing change 2019-12-08 20:08:42 +01:00
death-claw 28a4c944ee Add support for multiple links at a time
Add the Link collector feature
Fix bug with previews
2019-12-08 20:07:13 +01:00
death-claw 23d3553d99 Include startup icon
Fix UI layout
2019-12-06 23:09:57 +01:00
death-claw fb09e4c46b v2.4.1 2019-11-26 20:03:10 +01:00
death-claw 4feb6f5fe1 Increase attempts for establishing ws connection between front and back end
Change default download folder to Home folder (installer), App folder (portable)
2019-11-26 20:01:08 +01:00
death-claw df1b9efbbb v2.4.0 2019-11-23 14:50:16 +01:00
death-claw 553924fa1d Pimpandhost Host support 2019-11-23 14:48:05 +01:00
death-claw a90dbd568f v2.3.2 2019-11-23 12:15:42 +01:00
death-claw ca7ca1ce8e Fix a bug with the scan component 2019-11-23 12:14:07 +01:00
death-claw b3ab239a1e v2.3.1 2019-11-22 20:07:41 +01:00
death-claw 6e212c791c Fix connexion reset issue
Fix Post Status when autostart option is selected
Add stop all button
2019-11-22 20:05:14 +01:00
death-claw c6067ef147 v2.3.0 2019-11-21 21:17:25 +01:00
death-claw 8333f0c34e Fix Restoring post status on App startup
Use local sockjs and material icons font
2019-11-21 21:16:24 +01:00
death-claw 75becd8bb4 UI enhancements and overhaul
Stability fixes with HTTP connections
MacOS official support
2019-11-21 20:54:44 +01:00
death-claw 667376301a v2.2.5 2019-11-09 13:26:54 +01:00
death-claw 128e6b68a3 More robust parsing
Fix pixhost and imagezilla to exclude thumbs
2019-11-09 13:16:09 +01:00
death-claw a7c2844ab4 Change Retry duration 2019-11-09 10:13:12 +01:00
death-claw bd0b7d274b Fix macOs paths 2019-10-06 13:33:29 +01:00
death-claw bc57fad52b Remove failing rule 2019-10-06 12:00:01 +01:00
death-claw b81ef53cb9 Add support for macOs 2019-10-06 11:22:48 +01:00
death-claw c182bf0a08 v2.2.4 2019-10-05 18:06:40 +01:00
death-claw 07802b4b13 Add AppImage target 2019-10-05 18:03:32 +01:00
death-claw 50c4d947ed v2.2.3 2019-10-01 23:28:12 +01:00
death-claw b9b20e86dd Strip libjvm.so for debian 2019-10-01 23:27:01 +01:00
death-claw 64b93ce7e1 v2.2.2 2019-10-01 22:59:39 +01:00
death-claw 0bde4399e3 Update product name 2019-10-01 22:57:13 +01:00
death-claw 9b7818c5a7 v2.2.1 2019-10-01 22:44:19 +01:00
death-claw 1f2dcff353 Update dependency list for deb package 2019-10-01 22:42:28 +01:00
death-claw 4d82622fdb v2.2.0 2019-10-01 21:26:27 +01:00
death-claw e7393caf01 Fix 'Start' on pending items
Upgrade to Java 11
Embedd JRE within the app
2019-10-01 21:24:02 +01:00
death-claw 8e5cb958ea v2.1.0 2019-09-30 21:12:56 +01:00
death-claw bc2cf02ec6 Add a create a subfolder per thread option 2019-09-30 21:06:40 +01:00
151 changed files with 9252 additions and 4516 deletions
+1
View File
@@ -2,4 +2,5 @@
/**/*/node_modules
/**/*/target
/**/*/build-dir
/**/*/java-runtime
.idea
+96
View File
@@ -1,5 +1,101 @@
# Changelog
## [2.8.0] - 2019-12-24
### Added
- Photo gallery feature
## [2.7.1] - 2019-12-21
### Added
- Add version to title bar
## [2.7.0] - 2019-12-21
### Changed
- Add missing icon
### Added
- Add support for pixxxels
## [2.6.0] - 2019-12-21
### Changed
- Disable download button when nothing selected in Link collector grab menu
- Better quality app icon
- Implement context menu on download tab
- Add stop remove and start button to context menu
- Link will not be removed automatically from link collector tab
- Some UI fixes
## [2.5.1] - 2019-12-09
### Changed
- Disable download button when nothing selected in Link collector grab menu
### Added
- Add hosts column in Link collector grab menu
## [2.5.0] - 2019-12-08
### Changed
- Change loading screen
- UI Fixes
- Fix bug with previews
### Added
- Add link collector feature
- Add support for multiple links at a time
## [2.4.1] - 2019-11-26
### Changed
- Increase attempts for establishing ws connection between front and back end
- Change default download folder to Home folder (installer), App folder (portable)
## [2.4.0] - 2019-11-23
### Changed
- Pimpandhost Host support
## [2.3.2] - 2019-11-23
### Changed
- Fix a bug with the scan component
## [2.3.1] - 2019-11-22
### Changed
- Fix connexion reset issue
- Fix Post Status when autostart option is selected
- Add stop all button
## [2.3.0] - 2019-11-21
### Changed
- UI enhancements and overhaul
- Stability fixes with HTTP connections
- MacOS official support
- Use local sockjs and material icons font
## [2.2.5] - 2019-11-09
### Changed
- Exclude thumbs download for imagezilla and pixhost
- Change retry on fail logic (may help avoid having download errors)
## [2.2.4] - 2019-10-05
### Added
- AppImage target for linux
## [2.2.3] - 2019-10-01
### Changed
- Strip libjvm.so for debian
## [2.2.2] - 2019-10-01
### Changed
- Update product name
## [2.2.1] - 2019-10-01
### Changed
- Update dependency list for deb package
## [2.2.0] - 2019-10-01
### Added
- Embedd JRE within the app
### Changed
- Fix 'Start' on pending items
- Upgrade to Java 11
## [2.1.0] - 2019-09-30
### Added
- Add a create a subfolder per thread option
## [2.0.4] - 2019-09-27
### Changed
- Fix some issues with ImxHost
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.0.4</version>
<version>2.8.0</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+23 -13
View File
@@ -4,7 +4,6 @@ const url = require("url");
const getPort = require("get-port");
const { spawn } = require("child_process");
const { ipcMain } = require("electron");
const commandExists = require("command-exists").sync;
const { dialog } = require("electron");
const axios = require('axios');
const appDir = process.env.APPDIR;
@@ -23,14 +22,6 @@ process.on("uncaughtException", err => {
process.exit(1);
});
if (!commandExists("java")) {
dialog.showErrorBox(
"Java command is missing",
"Java cannot be found on your PATH, make sure to install java before running Viper Ripper"
);
process.exit(1);
}
function createWindow() {
let icon;
if(process.platform === "win32") {
@@ -73,13 +64,31 @@ getPort().then(port => {
ipcMain.on("get-port", event => {
event.reply("port", port);
});
vripperServer = spawn("java", [
let javaBinPath;
if(appDir !== undefined) {
javaBinPath = path.join(appDir, "java-runtime/bin/java");
} else {
if(process.platform === 'darwin') {
javaBinPath = path.join(app.getPath('exe'), "../../java-runtime/bin/java");
} else {
javaBinPath = path.join(app.getPath('exe'), "../java-runtime/bin/java");
}
}
let jarPath;
if(appDir !== undefined) {
jarPath = path.join(appDir, "bin/vripper-server.jar");
} else {
if(process.platform === 'darwin') {
jarPath = path.join(app.getPath('exe'), "../../bin/vripper-server.jar");
} else {
jarPath = path.join(app.getPath('exe'), "../bin/vripper-server.jar");
}
}
vripperServer = spawn(javaBinPath, [
"-Xms256m",
"-Xmx1024m",
"-Dvripper.server.port=" + port,
"-jar",
appDir !== undefined ? path.join(appDir, "bin/vripper-server.jar") :
path.join(app.getPath('exe'), "../bin/vripper-server.jar")
jarPath
], {
stdio: 'ignore'
});
@@ -126,6 +135,7 @@ if (!gotTheLock) {
})
.catch((error) => {
// Terminate immediately
console.log(error);
vripperServer.kill('SIGKILL');
terminated = true;
app.quit();
+60 -40
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.0.4",
"version": "2.8.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -35,6 +35,11 @@
"defer-to-connect": "^1.0.1"
}
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
},
"@types/debug": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz",
@@ -117,6 +122,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -216,9 +222,9 @@
"dev": true
},
"astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
},
"async-exit-hook": {
"version": "2.0.1",
@@ -519,18 +525,18 @@
"dev": true
},
"cli-truncate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.0.0.tgz",
"integrity": "sha512-C4hp+8GCIFVsUUiXcw+ce+7wexVWImw8rQrgMBFsqerx9LvvcGlwm6sMjQYAEmV/Xb87xc1b5Ttx505MSpZVqg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"requires": {
"slice-ansi": "^2.1.0",
"string-width": "^4.1.0"
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"emoji-regex": {
"version": "8.0.0",
@@ -543,21 +549,21 @@
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"string-width": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz",
"integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^5.2.0"
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^4.1.0"
"ansi-regex": "^5.0.0"
}
}
}
@@ -626,6 +632,7 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
@@ -633,7 +640,8 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"combined-stream": {
"version": "1.0.8",
@@ -644,11 +652,6 @@
"delayed-stream": "~1.0.0"
}
},
"command-exists": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.8.tgz",
"integrity": "sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -765,11 +768,6 @@
"array-find-index": "^1.0.1"
}
},
"custom-electron-titlebar": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/custom-electron-titlebar/-/custom-electron-titlebar-3.0.10.tgz",
"integrity": "sha512-FVKy7P0NpMTkm5rEpETXkJWQ7w9ufdU8TA5xhfiR1Jgnhy6fBaCNUMmXWLrNFrhG69MVvDXM/lLN21Rxaz78Ig=="
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -2431,19 +2429,41 @@
}
},
"slice-ansi": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
"integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"requires": {
"ansi-styles": "^3.2.0",
"astral-regex": "^1.0.0",
"is-fullwidth-code-point": "^2.0.0"
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.0.tgz",
"integrity": "sha512-7kFQgnEaMdRtwf6uSfUnVr9gSGC7faurn+J/Mv90/W+iTtN0405/nLdopfMWwchyxhbGYl6TC4Sccn9TUkGAgg==",
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
}
}
},
+14 -16
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.0.4",
"version": "2.8.0",
"description": "A ripper for vipergirls.to built using web technolgies",
"main": "main.js",
"author": "death-claw <53543762+death-claw@users.noreply.github.com>",
@@ -8,7 +8,7 @@
"license": "ISC",
"build": {
"appId": "tn.mnlr.vripper",
"productName": "V Ripper",
"productName": "VRipper",
"files": [
"**/*",
"!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}",
@@ -31,10 +31,14 @@
{
"from": "../vripper-server/target/vripper-server-${version}-electron.jar",
"to": "bin/vripper-server.jar"
},
{
"from": "java-runtime",
"to": "java-runtime"
}
],
"win": {
"icon": "icon.png",
"icon": "icon.ico",
"target": [
"nsis"
]
@@ -50,19 +54,15 @@
"packageCategory": "Utility",
"icon": "icons",
"target": [
"deb"
"AppImage"
]
},
"deb": {
"depends": [
"gconf2",
"gconf-service",
"libnotify4",
"libappindicator1",
"libxtst6",
"libnss3",
"openjdk-8-jre"
]
"mac": {
"category": "public.app-category.utilities",
"target": [
"dmg"
],
"icon": "icon.icns"
}
},
"scripts": {
@@ -76,9 +76,7 @@
"dependencies": {
"axios": "^0.19.0",
"cheerio": "^1.0.0-rc.3",
"command-exists": "^1.2.8",
"copy-dir": "^1.1.0",
"custom-electron-titlebar": "^3.0.10",
"electron-context-menu": "^0.13.0",
"get-port": "^5.0.0",
"rimraf": "^2.6.3"
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.0.4</version>
<version>2.8.0</version>
</parent>
<artifactId>vripper-electron</artifactId>
<name>vripper-electron</name>
+8 -1
View File
@@ -2,7 +2,7 @@ const cheerio = require('cheerio');
const fs = require('fs');
const rimraf = require("rimraf");
const copydir = require("copy-dir");
const { execSync } = require("child_process");
rimraf.sync('./build/vripper-ui');
copydir.sync('../vripper-ui/dist/vripper-ui', './build/vripper-ui', {});
@@ -16,3 +16,10 @@ $('body').prepend(`<script>require('../renderer.js')</script>`);
fs.writeFileSync('./build/vripper-ui/index.html', $.html());
console.log('Building runtime environment');
rimraf.sync('java-runtime');
execSync('jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.desktop,java.instrument,java.management,java.naming,java.prefs,java.rmi,java.scripting,java.security.jgss,java.sql,jdk.httpserver,jdk.unsupported,jdk.crypto.ec --output java-runtime');
if(process.platform === 'linux') {
console.log('Stripping libjvm.so');
execSync('strip -p --strip-unneeded java-runtime/lib/server/libjvm.so');
}
+12 -2
View File
@@ -5,14 +5,14 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.0.4</version>
<version>2.8.0</version>
</parent>
<artifactId>vripper-server</artifactId>
<name>vripper-server</name>
<description>vripper-server</description>
<properties>
<java.version>1.8</java.version>
<java.version>11</java.version>
</properties>
<dependencies>
@@ -67,6 +67,16 @@
<artifactId>failsafe</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
<dependency>
<groupId>org.imgscalr</groupId>
<artifactId>imgscalr-lib</artifactId>
<version>4.2</version>
</dependency>
<dependency>
<groupId>tn.mnlr</groupId>
<artifactId>vripper-ui</artifactId>
@@ -17,18 +17,11 @@ public class SpringContext implements ApplicationContextAware {
private static ConfigurableApplicationContext context;
/**
* Returns the Spring managed bean instance of the given class type (if it exists).
* Returns null otherwise.
*
* @param beanClass
* @return
*/
public static <T extends Object> T getBean(Class<T> beanClass) {
public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
public static <T extends Object> Map<String, T> getBeansOfType(Class<T> beanClass) {
public static <T> Map<String, T> getBeansOfType(Class<T> beanClass) {
return context.getBeansOfType(beanClass);
}
@@ -23,13 +23,9 @@ import java.util.concurrent.Executors;
public class VripperApplication {
private static final Logger logger = LoggerFactory.getLogger(VripperApplication.class);
// public static final String dataPath = System.getProperty("user.home", ".") + File.separator + ".vripper" + File.separator + "data.json";
public static final ExecutorService commonExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
public static void main(String[] args) {
try {
SpringApplication.run(VripperApplication.class, args);
} catch (Exception e) {
@@ -48,7 +48,7 @@ public class Image {
this.postName = postName;
this.host = host;
this.index = index;
status = Status.PENDING;
status = Status.STOPPED;
imageStateProcessor = BehaviorProcessor.create();
}
@@ -80,7 +80,7 @@ public class Image {
.subscribe();
current.set(0);
status = Status.PENDING;
status = Status.STOPPED;
imageStateProcessor.onNext(this);
}
@@ -26,6 +26,8 @@ public class Post {
private String postId;
private String threadTitle;
private String threadId;
private String title;
@@ -46,7 +48,7 @@ public class Post {
private String forum;
public Post(String title, String url, List<Image> images, Map<String, Object> metadata, String postId, String threadId, String forum) {
public Post(String title, String url, List<Image> images, Map<String, Object> metadata, String postId, String threadId, String threadTitle, String forum) {
this.title = title;
this.url = url;
this.images = images;
@@ -54,20 +56,10 @@ public class Post {
this.postId = postId;
this.forum = forum;
this.threadId = threadId;
if (this.images.contains(null)) {
System.out.println("Oops");
}
this.images.stream().map(e -> {
if (e == null) {
System.out.println("Am the cause");
}
return e.getHost();
}).collect(Collectors.toSet());
this.images.stream().map(e -> e.getHost()).map(Host::getHost).collect(Collectors.toSet());
this.hosts = this.images.stream().map(e -> e.getHost()).map(Host::getHost).collect(Collectors.toSet());
this.threadTitle = threadTitle;
this.hosts = this.images.stream().map(Image::getHost).map(Host::getHost).collect(Collectors.toSet());
total = images.size();
status = Status.PENDING;
status = Status.STOPPED;
}
public void setAppStateService(AppStateService appStateService) {
@@ -5,10 +5,14 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.reactivex.disposables.Disposable;
import io.reactivex.processors.BehaviorProcessor;
import lombok.Getter;
import lombok.Setter;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.host.Host;
import tn.mnlr.vripper.services.AppStateService;
@Getter
@Setter
public abstract class ImagePersistanceMixin {
@JsonSerialize(converter = HostToString.class)
@@ -1,8 +1,12 @@
package tn.mnlr.vripper.entities.mixin.persistance;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import tn.mnlr.vripper.services.AppStateService;
@Getter
@Setter
public abstract class PostPersistanceMixin {
@JsonIgnore
@@ -3,10 +3,14 @@ package tn.mnlr.vripper.entities.mixin.ui;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.reactivex.disposables.Disposable;
import io.reactivex.processors.BehaviorProcessor;
import lombok.Getter;
import lombok.Setter;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.host.Host;
import tn.mnlr.vripper.services.AppStateService;
@Getter
@Setter
public abstract class ImageUIMixin {
@JsonIgnore
@@ -1,11 +1,15 @@
package tn.mnlr.vripper.entities.mixin.ui;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.services.AppStateService;
import java.util.List;
@Getter
@Setter
public abstract class PostUIMixin {
@JsonIgnore
@@ -6,7 +6,7 @@ public class DownloadException extends Exception {
super(message);
}
public DownloadException(Exception e) {
public DownloadException(Throwable e) {
super(e);
}
}
@@ -1,7 +1,7 @@
package tn.mnlr.vripper.exception;
public class HostException extends Exception {
public HostException(Exception e) {
public HostException(Throwable e) {
super(e);
}
@@ -9,7 +9,7 @@ public class HostException extends Exception {
super(message);
}
public HostException(String message, Exception e) {
public HostException(String message, Throwable e) {
super(message,e);
}
}
@@ -2,7 +2,7 @@ package tn.mnlr.vripper.exception;
public class HtmlProcessorException extends Exception {
public HtmlProcessorException(Exception e) {
public HtmlProcessorException(Throwable e) {
super(e);
}
}
@@ -6,11 +6,11 @@ public class PostParseException extends Exception {
super(message);
}
public PostParseException(String message, Exception e) {
public PostParseException(String message, Throwable e) {
super(message, e);
}
public PostParseException(Exception e) {
public PostParseException(Throwable e) {
super(e);
}
}
@@ -5,7 +5,7 @@ public class VripperException extends Exception {
super(message);
}
public VripperException(Exception e) {
public VripperException(Throwable e) {
super(e);
}
}
@@ -1,7 +1,7 @@
package tn.mnlr.vripper.exception;
public class XpathException extends Exception {
public XpathException(Exception e) {
public XpathException(Throwable e) {
super(e);
}
}
@@ -5,6 +5,7 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
@@ -28,7 +29,7 @@ public class AcidimgHost extends Host {
private static final String host = "acidimg.cc";
private static final String CONTINUE_BUTTON_XPATH = "//input[@id='continuebutton']";
public static final String IMG_XPATH = "//img[@class='centred']";
private static final String IMG_XPATH = "//img[@class='centred']";
@Autowired
private ConnectionManager cm;
@@ -39,20 +40,25 @@ public class AcidimgHost extends Host {
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return host;
}
Document doc = getResponse(url).getDocument();
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
Document doc = getResponse(url, context).getDocument();
Node contDiv;
try {
logger.info(String.format("Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url));
contDiv = xpathService.getAsNode(doc, CONTINUE_BUTTON_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
if (contDiv != null) {
logger.info(String.format("Click button found for %s", url));
logger.debug(String.format("Click button found for %s", url));
HttpClient client = cm.getClient().build();
HttpPost httpPost = cm.buildHttpPost(url);
httpPost.addHeader("Referer", url);
@@ -64,9 +70,9 @@ public class AcidimgHost extends Host {
throw new HostException(e);
}
logger.info(String.format("Requesting %s", httpPost));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpPost)) {
logger.info(String.format("Cleaning response for %s", httpPost));
logger.debug(String.format("Requesting %s", httpPost));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpPost, context)) {
logger.debug(String.format("Cleaning response for %s", httpPost));
doc = htmlProcessorService.clean(EntityUtils.toString(response.getEntity()));
EntityUtils.consumeQuietly(response.getEntity());
} catch (Exception e) {
@@ -76,7 +82,7 @@ public class AcidimgHost extends Host {
Node imgNode;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
@@ -87,7 +93,7 @@ public class AcidimgHost extends Host {
}
try {
logger.info(String.format("Resolving name and image url for %s", url));
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
@@ -5,6 +5,8 @@ import org.apache.http.Header;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -25,9 +27,11 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Iterator;
import java.util.Random;
@Service
abstract public class Host {
@@ -59,59 +63,63 @@ abstract public class Host {
abstract public String getHost();
abstract public String getLookup();
public boolean isSupported(String url) {
return url.contains(getHost());
return url.contains(getLookup());
}
public void download(Image image, ImageFileData imageFileData) throws DownloadException, InterruptedException {
HttpClientContext context = HttpClientContext.create();
context.setCookieStore(new BasicCookieStore());
try {
appStateService.postDownloadingUpdate(image.getPostId());
imageFileData.setPageUrl(image.getUrl());
/**
/*
* HOST SPECIFIC
*/
logger.info(String.format("Getting image url and name from %s using %s", image.getUrl(), this.getHost()));
setNameAndUrl(image.getUrl(), imageFileData);
logger.info(String.format("Resolved name for %s: %s", image.getUrl(), imageFileData.getImageName()));
logger.info(String.format("Resolved image url for %s: %s", image.getUrl(), imageFileData.getImageUrl()));
logger.debug(String.format("Getting image url and name from %s using %s", image.getUrl(), this.getHost()));
setNameAndUrl(image.getUrl(), imageFileData, context);
logger.debug(String.format("Resolved name for %s: %s", image.getUrl(), imageFileData.getImageName()));
logger.debug(String.format("Resolved image url for %s: %s", image.getUrl(), imageFileData.getImageUrl()));
logger.info(String.format("Building image request for %s", image.getUrl()));
logger.debug(String.format("Building image request for %s", image.getUrl()));
setImageRequest(imageFileData);
/**
/*
* END HOST SPECIFIC
*/
String formatImageFileName = pathService.formatImageFileName(imageFileData.getImageName());
logger.info(String.format("Sanitizing image name from %s to %s", imageFileData.getImageName(), formatImageFileName));
logger.debug(String.format("Sanitizing image name from %s to %s", imageFileData.getImageName(), formatImageFileName));
imageFileData.setImageName(formatImageFileName);
File destinationFolder = pathService.getDownloadDestinationFolder(image.getPostId());
logger.info(String.format("Saving to %s", destinationFolder.getPath()));
logger.debug(String.format("Saving to %s", destinationFolder.getPath()));
if (!destinationFolder.exists()) {
logger.info(String.format("Creating %s", destinationFolder.getPath()));
destinationFolder.mkdirs();
logger.debug(String.format("Creating %s", destinationFolder.getPath()));
if (destinationFolder.mkdirs()) {
logger.debug(String.format("Folder %s is created", destinationFolder.toString()));
}
}
HttpClient client = cm.getClient().build();
logger.info(String.format("Downloading %s", imageFileData.getImageUrl()));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(imageFileData.getImageRequest())) {
logger.debug(String.format("Downloading %s", imageFileData.getImageUrl()));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(imageFileData.getImageRequest(), context)) {
if(response.getStatusLine().getStatusCode() / 100 != 2) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
EntityUtils.consumeQuietly(response.getEntity());
throw new DownloadException(String.format("Server returned code %d", response.getStatusLine().getStatusCode()));
}
File outputFile = new File(destinationFolder.getPath() + File.separator + imageFileData.getImageName() + ".tmp");
InputStream downloadStream = response.getEntity().getContent();
FileOutputStream fos = new FileOutputStream(outputFile);
try {
try (InputStream downloadStream = response.getEntity().getContent(); FileOutputStream fos = new FileOutputStream(outputFile)) {
image.setTotal(response.getEntity().getContentLength());
logger.info(String.format("%s length is %d", imageFileData.getImageUrl(), image.getTotal()));
logger.info(String.format("Starting data transfer for %s", imageFileData.getImageUrl()));
logger.debug(String.format("%s length is %d", imageFileData.getImageUrl(), image.getTotal()));
logger.debug(String.format("Starting data transfer for %s", imageFileData.getImageUrl()));
byte[] buffer = new byte[READ_BUFFER_SIZE];
int read;
@@ -120,77 +128,71 @@ abstract public class Host {
image.increase(read);
downloadSpeedService.increase(read);
}
fos.flush();
EntityUtils.consumeQuietly(response.getEntity());
} finally {
if (downloadStream != null) {
downloadStream.close();
}
if (fos != null) {
fos.close();
}
}
checkImageTypeAndRename(outputFile, imageFileData.getImageName(), image.getIndex());
File finalName = checkImageTypeAndRename(outputFile, imageFileData.getImageName(), image.getIndex());
imageFileData.setFileName(finalName.getName());
}
} catch (Exception e) {
if(Thread.interrupted()) {
if (Thread.interrupted()) {
throw new InterruptedException("Download was interrupted");
}
throw new DownloadException(e);
}
}
private void checkImageTypeAndRename(File outputFile, String imageName, int index) throws HostException {
String formatName;
private File checkImageTypeAndRename(File outputFile, String imageName, int index) throws HostException {
try (ImageInputStream iis = ImageIO.createImageInputStream(outputFile)) {
Iterator<ImageReader> it = ImageIO.getImageReaders(iis);
if (!it.hasNext()) {
throw new HostException("Image file is not recognized!");
}
ImageReader reader = it.next();
formatName = reader.getFormatName();
if (formatName.toUpperCase().equals("JPEG")) {
formatName = "jpg";
if (reader.getFormatName().toUpperCase().equals("JPEG")) {
String imageNameLC = imageName.toLowerCase();
if (!imageNameLC.endsWith("_jpg") && !imageNameLC.endsWith("_jpeg")) {
imageName += ".jpg";
} else {
String toReplace = null;
if (imageNameLC.endsWith("_jpg")) {
toReplace = "_jpg";
} else if (imageNameLC.endsWith("_jpeg")) {
toReplace = "_jpeg";
}
if (toReplace != null) {
imageName = imageName.substring(0, imageName.length() - toReplace.length()) + ".jpg";
}
}
}
} catch (Exception e) {
throw new HostException("Failed to guess image format", e);
}
try {
File outImage = new File(outputFile.getParent(), (appSettingsService.isForceOrder() ? String.format("%03d_", index) : "") + imageName + "." + formatName.toLowerCase());
if (outImage.exists()) {
outImage.delete();
File outImage = new File(outputFile.getParent(), (appSettingsService.isForceOrder() ? String.format("%03d_", index) : "") + imageName);
if (outImage.exists() && outImage.delete()) {
logger.debug(String.format("%s is deleted", outImage.toString()));
}
Files.move(outputFile.toPath(), outImage.toPath());
return Files.move(outputFile.toPath(), outImage.toPath(), StandardCopyOption.ATOMIC_MOVE).toFile();
} catch (Exception e) {
throw new HostException("Failed to rename the image", e);
}
}
/**
* Just for testing, you may ignore
* @throws Exception
*/
private void randomFail() throws Exception {
Random random = new Random();
int i = random.nextInt(100);
if (i < 50) {
throw new Exception("Purpose error");
}
}
protected final String getDefaultImageName(final String imgUrl) {
String imageTitle = imgUrl.substring(imgUrl.lastIndexOf('/') + 1);
final String getDefaultImageName(final String imgUrl) {
String imageTitle = imgUrl.substring(imgUrl.lastIndexOf('/') + 1);
logger.debug(String.format("Extracting name from url %s: %s", imgUrl, imageTitle));
return imgUrl;
}
protected final Response getResponse(final String url) throws HostException {
final Response getResponse(final String url, final HttpClientContext context) throws HostException {
String basePage;
HttpClient client = cm.getClient().build();
HttpGet httpGet = cm.buildHttpGet(url);
Header[] headers;
logger.info(String.format("Requesting %s", url));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpGet)) {
logger.debug(String.format("Requesting %s", url));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpGet, context)) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new HostException(String.format("Unexpected response code: %d", response.getStatusLine().getStatusCode()));
}
@@ -203,20 +205,34 @@ abstract public class Host {
}
try {
logger.info(String.format("Cleaning %s response", url));
logger.debug(String.format("Cleaning %s response", url));
return new Response(htmlProcessorService.clean(basePage), headers);
} catch (HtmlProcessorException e) {
throw new HostException(e);
}
}
protected void setImageRequest(final ImageFileData imageFileData) {
protected String appendUri(String uri, String appendQuery) throws URISyntaxException {
URI oldUri = new URI(uri);
String newQuery = oldUri.getQuery();
if (newQuery == null) {
newQuery = appendQuery;
} else {
newQuery += "&" + appendQuery;
}
return new URI(oldUri.getScheme(), oldUri.getAuthority(),
oldUri.getPath(), newQuery, oldUri.getFragment()).toString();
}
private void setImageRequest(final ImageFileData imageFileData) {
HttpGet httpGet = cm.buildHttpGet(imageFileData.getImageUrl());
httpGet.addHeader("Referer", imageFileData.getPageUrl());
imageFileData.setImageRequest(httpGet);
}
protected abstract void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException;
protected abstract void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException;
@Getter
public static class Response {
@@ -224,6 +240,7 @@ abstract public class Host {
this.document = document;
this.headers = headers;
}
private Document document;
private Header[] headers;
}
@@ -1,9 +1,9 @@
package tn.mnlr.vripper.host;
import org.apache.http.Header;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -18,8 +18,6 @@ import tn.mnlr.vripper.q.ImageFileData;
import tn.mnlr.vripper.services.ConnectionManager;
import java.io.IOException;
import java.util.Arrays;
import java.util.stream.Collectors;
@Service
public class ImageBamHost extends Host {
@@ -27,8 +25,8 @@ public class ImageBamHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(ImageBamHost.class);
private static final String host = "imagebam.com";
public static final String CONTINUE_BUTTON_XPATH = "//a[@title='Continue to your image']";
public static final String IMG_XPATH = "//img[@class='image']";
private static final String CONTINUE_BUTTON_XPATH = "//a[@title='Continue to your image']";
private static final String IMG_XPATH = "//img[@class='image']";
@Autowired
private ConnectionManager cm;
@@ -39,34 +37,31 @@ public class ImageBamHost extends Host {
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return host;
}
Response response = getResponse(url);
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
Response response = getResponse(url, context);
Document doc = response.getDocument();
Header[] headers = response.getHeaders();
String cookies = Arrays.asList(headers)
.stream()
.filter(e -> e.getName().toLowerCase().contains("Set-Cookie".toLowerCase()))
.map(e -> e.getValue().split(";")[0].trim())
.collect(Collectors.joining("; "));
Node contDiv;
try {
logger.info(String.format("Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url));
contDiv = xpathService.getAsNode(doc, CONTINUE_BUTTON_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
if (contDiv != null) {
logger.info(String.format("Getting cookies to use for %s", url));
logger.info(String.format("Click button found for %s", url));
logger.debug(String.format("Click button found for %s", url));
HttpClient client = cm.getClient().build();
HttpGet httpGet = cm.buildHttpGet(url);
httpGet.addHeader("Referer", url);
httpGet.addHeader("Cookie", cookies);
logger.info(String.format("Requesting %s", httpGet));
try (CloseableHttpResponse res = (CloseableHttpResponse) client.execute(httpGet)) {
logger.debug(String.format("Requesting %s", httpGet));
try (CloseableHttpResponse res = (CloseableHttpResponse) client.execute(httpGet, context)) {
String s = EntityUtils.toString(res.getEntity());
logger.debug(String.format("%s response is:%n%s", httpGet, s));
logger.debug(String.format("Cleaning response for %s", httpGet));
@@ -79,14 +74,14 @@ public class ImageBamHost extends Host {
Node imgNode;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.info(String.format("Resolving name and image url for %s", url));
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("id").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
@@ -1,5 +1,6 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -16,7 +17,7 @@ import java.util.Optional;
@Service
public class ImageTwistHost extends Host {
public static final String IMG_XPATH = "//img[contains(@class, 'img')]";
private static final String IMG_XPATH = "//img[contains(@class, 'img')]";
private static final Logger logger = LoggerFactory.getLogger(ImageTwistHost.class);
private static final String host = "imagetwist.com";
@Autowired
@@ -28,20 +29,25 @@ public class ImageTwistHost extends Host {
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return host;
}
Document doc = getResponse(url).getDocument();
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
Document doc = getResponse(url, context).getDocument();
Node imgNode;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.info(String.format("Resolving name and image url for %s", url));
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = Optional.ofNullable(imgNode.getAttributes().getNamedItem("alt")).map(Node::getTextContent).map(String::trim).orElse(null);
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
@@ -1,5 +1,6 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,7 +18,8 @@ public class ImageZillaHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(ImageZillaHost.class);
private static final String host = "imagezilla.net";
public static final String IMG_XPATH = "//img[@id='photo']";
private static final String lookup = "imagezilla.net/show";
private static final String IMG_XPATH = "//img[@id='photo']";
@Autowired
private ConnectionManager cm;
@@ -28,15 +30,20 @@ public class ImageZillaHost extends Host {
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return lookup;
}
Document doc = getResponse(url).getDocument();
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
Document doc = getResponse(url, context).getDocument();
String title;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
Node titleNode = xpathService.getAsNode(doc, IMG_XPATH).getAttributes().getNamedItem("title");
logger.info(String.format("Resolving name for %s", url));
logger.debug(String.format("Resolving name for %s", url));
if(titleNode != null) {
title = titleNode.getTextContent().trim();
} else {
@@ -1,5 +1,6 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,7 +18,7 @@ public class ImgboxHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(ImgboxHost.class);
private static final String host = "imgbox.com";
public static final String IMG_XPATH = "//img[@id='img']";
private static final String IMG_XPATH = "//img[@id='img']";
@Autowired
private ConnectionManager cm;
@@ -28,20 +29,25 @@ public class ImgboxHost extends Host {
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return host;
}
Document doc = getResponse(url).getDocument();
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
Document doc = getResponse(url, context).getDocument();
Node imgNode;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.info(String.format("Resolving name and image url for %s", url));
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("title").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
@@ -5,6 +5,7 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
@@ -29,8 +30,8 @@ public class ImxHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(ImxHost.class);
private static final String host = "imx.to";
public static final String CONTINUE_BUTTON_XPATH = "//*[@name='imgContinue']";
public static final String IMG_XPATH = "//img[@class='centred']";
private static final String CONTINUE_BUTTON_XPATH = "//*[@name='imgContinue']";
private static final String IMG_XPATH = "//img[@class='centred']";
@Autowired
private ConnectionManager cm;
@@ -41,16 +42,21 @@ public class ImxHost extends Host {
}
@Override
protected void setNameAndUrl(final String _url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return host;
}
@Override
protected void setNameAndUrl(final String _url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
String url = _url.replace("http://", "https://");
Response resp = getResponse(url);
Response resp = getResponse(url, context);
Document doc = resp.getDocument();
Node contDiv;
String value = null;
try {
logger.info(String.format("Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", CONTINUE_BUTTON_XPATH, url));
contDiv = xpathService.getAsNode(doc, CONTINUE_BUTTON_XPATH);
Node node = contDiv.getAttributes().getNamedItem("value");
if (node != null) {
@@ -60,40 +66,38 @@ public class ImxHost extends Host {
throw new HostException(e);
}
if (contDiv != null) {
if (value == null) {
throw new HostException("Failed to obtain value attribute from continue input");
}
logger.info(String.format("Click button found for %s", url));
HttpClient client = cm.getClient().build();
HttpPost httpPost = cm.buildHttpPost(url);
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("imgContinue", value));
try {
httpPost.setEntity(new UrlEncodedFormEntity(params));
} catch (Exception e) {
throw new HostException(e);
}
logger.info(String.format("Requesting %s", httpPost));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpPost)) {
logger.debug(String.format("Cleaning response for %s", httpPost));
doc = htmlProcessorService.clean(EntityUtils.toString(response.getEntity()));
EntityUtils.consumeQuietly(response.getEntity());
} catch (IOException | HtmlProcessorException e) {
throw new HostException(e);
}
if (value == null) {
throw new HostException("Failed to obtain value attribute from continue input");
}
logger.debug(String.format("Click button found for %s", url));
HttpClient client = cm.getClient().build();
HttpPost httpPost = cm.buildHttpPost(url);
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("imgContinue", value));
try {
httpPost.setEntity(new UrlEncodedFormEntity(params));
} catch (Exception e) {
throw new HostException(e);
}
logger.debug(String.format("Requesting %s", httpPost));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpPost, context)) {
logger.debug(String.format("Cleaning response for %s", httpPost));
doc = htmlProcessorService.clean(EntityUtils.toString(response.getEntity()));
EntityUtils.consumeQuietly(response.getEntity());
} catch (IOException | HtmlProcessorException e) {
throw new HostException(e);
}
Node imgNode;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.info(String.format("Resolving name and image url for %s", url));
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
@@ -0,0 +1,68 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import tn.mnlr.vripper.exception.HostException;
import tn.mnlr.vripper.exception.XpathException;
import tn.mnlr.vripper.q.ImageFileData;
import tn.mnlr.vripper.services.ConnectionManager;
@Service
public class PimpandhostHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(PimpandhostHost.class);
private static final String host = "pimpandhost.com";
private static final String IMG_XPATH = "//img[contains(@class, 'original')]";
@Autowired
private ConnectionManager cm;
@Override
public String getHost() {
return host;
}
@Override
public String getLookup() {
return host;
}
@Override
protected void setNameAndUrl(final String _url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
String url;
try {
url = appendUri(_url.replace("http://", "https://"), "size=original");
} catch (Exception e) {
throw new HostException(e);
}
Response resp = getResponse(url, context);
Document doc = resp.getDocument();
Node imgNode;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = "https:" + imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
imageFileData.setImageUrl(imgUrl);
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -1,5 +1,6 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,7 +18,8 @@ public class PixhostHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(PixhostHost.class);
private static final String host = "pixhost.to";
public static final String IMG_XPATH = "//img[@id='image']";
private static final String lookup = "pixhost.to/show";
private static final String IMG_XPATH = "//img[@id='image']";
@Autowired
private ConnectionManager cm;
@@ -28,20 +30,25 @@ public class PixhostHost extends Host {
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return lookup;
}
Document doc = getResponse(url).getDocument();
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
Document doc = getResponse(url, context).getDocument();
Node imgNode;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.info(String.format("Resolving name and image url for %s", url));
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
@@ -0,0 +1,67 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import tn.mnlr.vripper.exception.HostException;
import tn.mnlr.vripper.exception.XpathException;
import tn.mnlr.vripper.q.ImageFileData;
import tn.mnlr.vripper.services.ConnectionManager;
@Service
public class PixxxelsHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(PixxxelsHost.class);
private static final String host = "pixxxels.cc";
private static final String IMG_XPATH = "//*[@id='download']";
private static final String TITLE_XPATH = "//*[contains(@class,'imagename')]";
@Autowired
private ConnectionManager cm;
@Override
public String getHost() {
return host;
}
@Override
public String getLookup() {
return host;
}
@Override
protected void setNameAndUrl(final String _url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
String url = _url.replace("http://", "https://");
Response resp = getResponse(url, context);
Document doc = resp.getDocument();
Node imgNode, titleNode;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
logger.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url));
titleNode = xpathService.getAsNode(doc, TITLE_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = titleNode.getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("href").getTextContent().trim();
imageFileData.setImageUrl(imgUrl);
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -1,5 +1,6 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -17,8 +18,8 @@ public class TurboImageHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(TurboImageHost.class);
private static final String host = "turboimagehost.com";
public static final String TITLE_XPATH = "//div[contains(@class,'titleFullS')]/h1";
public static final String IMG_XPATH = "//img[@id='uImage']";
private static final String TITLE_XPATH = "//div[contains(@class,'titleFullS')]/h1";
private static final String IMG_XPATH = "//img[@id='uImage']";
@Autowired
private ConnectionManager cm;
@@ -29,15 +30,20 @@ public class TurboImageHost extends Host {
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
public String getLookup() {
return host;
}
Document doc = getResponse(url).getDocument();
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
Document doc = getResponse(url, context).getDocument();
String title;
try {
logger.info(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url));
logger.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url));
Node titleNode = xpathService.getAsNode(doc, TITLE_XPATH);
logger.info(String.format("Resolving name for %s", url));
logger.debug(String.format("Resolving name for %s", url));
if(titleNode != null) {
title = titleNode.getTextContent().trim();
} else {
@@ -18,14 +18,14 @@ public class DownloadJob implements Callable<Image> {
@Getter
private final ImageFileData imageFileData = new ImageFileData();
public DownloadJob(Image image) {
DownloadJob(Image image) {
this.image = image;
}
@Override
public Image call() throws Exception {
logger.info(String.format("Starting downloading %s", image.getUrl()));
logger.debug(String.format("Starting downloading %s", image.getUrl()));
image.setStatus(Image.Status.DOWNLOADING);
image.setCurrent(0);
image.getHost().download(image, imageFileData);
@@ -36,7 +36,7 @@ public class DownloadQ {
public void put(Image image) throws InterruptedException {
synchronized (appStateService) {
logger.info(String.format("Enqueuing a job for %s", image.getUrl()));
logger.debug(String.format("Enqueuing a job for %s", image.getUrl()));
image.init();
DownloadJob downloadJob = new DownloadJob(image);
downloadQ.put(downloadJob);
@@ -44,9 +44,9 @@ public class DownloadQ {
}
}
public DownloadJob take() throws InterruptedException {
DownloadJob take() throws InterruptedException {
DownloadJob downloadJob = downloadQ.take();
logger.info(String.format("Retrieving a job for %s", downloadJob.getImage().getUrl()));
logger.debug(String.format("Retrieving a job for %s", downloadJob.getImage().getUrl()));
return downloadJob;
}
@@ -71,17 +71,17 @@ public class DownloadQ {
return;
}
appStateService.getPost(postId).setStatus(Post.Status.PENDING);
logger.info(String.format("Restarting %d jobs for post id %s", images.size(), postId));
logger.debug(String.format("Restarting %d jobs for post id %s", images.size(), postId));
for (Image image : images) {
put(image);
}
}
}
public void removeScheduled(Image image) {
private void removeScheduled(Image image) {
synchronized (appStateService) {
image.setStatus(Image.Status.STOPPED);
logger.info(String.format("Removing scheduled job for %s", image.getUrl()));
logger.debug(String.format("Removing scheduled job for %s", image.getUrl()));
Iterator<DownloadJob> iterator = downloadQ.iterator();
boolean removed = false;
@@ -90,22 +90,22 @@ public class DownloadQ {
if (next.getImage().getPostId().equals(image.getPostId())) {
iterator.remove();
appStateService.doneDownloadJob(image);
logger.info(String.format("Scheduled job for %s is removed", image.getUrl()));
logger.debug(String.format("Scheduled job for %s is removed", image.getUrl()));
removed = true;
break;
}
}
if (!removed) {
logger.warn(String.format("Job for %s does not exist", image.getUrl()));
logger.debug(String.format("Job for %s does not exist", image.getUrl()));
}
image.cleanup();
}
}
public void removeRunning(String postId) {
logger.info(String.format("Interrupting running jobs for post id %s", postId));
private void removeRunning(String postId) {
logger.debug(String.format("Interrupting running jobs for post id %s", postId));
executionService.stop(postId);
}
@@ -126,8 +126,8 @@ public class DownloadQ {
if (images.isEmpty()) {
return;
}
logger.info(String.format("Stopping %d jobs for post id %s", images.size(), postId));
images.forEach(image -> removeScheduled(image));
logger.debug(String.format("Stopping %d jobs for post id %s", images.size(), postId));
images.forEach(this::removeScheduled);
removeRunning(postId);
}
} finally {
@@ -7,13 +7,15 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.AppStateService;
import tn.mnlr.vripper.services.ThumbnailGenerator;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -36,7 +38,13 @@ public class ExecutionService {
@Autowired
private AppStateService appStateService;
private AtomicInteger threadCount = new AtomicInteger();
@Autowired
private ThumbnailGenerator thumbnailGenerator;
@Autowired
private AppSettingsService appSettingsService;
private final AtomicInteger threadCount = new AtomicInteger();
private ExecutorService executor = Executors.newFixedThreadPool(10);
@@ -53,8 +61,8 @@ public class ExecutionService {
retryPolicy = new RetryPolicy<>()
.handleIf(e -> !(e instanceof InterruptedException))
.withDelay(Duration.ofSeconds(5))
.withMaxRetries(2)
.withBackoff(10, 60, ChronoUnit.SECONDS)
.withMaxRetries(4)
.abortOn(InterruptedException.class)
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
@@ -68,7 +76,7 @@ public class ExecutionService {
executionThread.interrupt();
executor.shutdown();
appStateService.getCurrentPosts().keySet().forEach(p -> {
logger.info(String.format("Stopping download jobs for %s", p));
logger.debug(String.format("Stopping download jobs for %s", p));
this.stop(p);
});
executor.awaitTermination(10, TimeUnit.SECONDS);
@@ -80,7 +88,7 @@ public class ExecutionService {
.filter(e -> e.getImage().getPostId().equals(postId))
.peek(e -> e.getImage().setStatus(Image.Status.STOPPED))
.collect(Collectors.toList());
logger.warn(String.format("Interrupting %d jobs for post id %s", data.size(), postId));
logger.debug(String.format("Interrupting %d jobs for post id %s", data.size(), postId));
data.forEach(e -> {
futures.get(e.getImage().getUrl()).cancel(true);
@@ -91,7 +99,7 @@ public class ExecutionService {
});
}
boolean canRun() {
private boolean canRun() {
boolean canRun = threadCount.get() < settings.getMaxThreads();
if (canRun && downloadQ.isNotPauseQ()) {
threadCount.incrementAndGet();
@@ -103,7 +111,7 @@ public class ExecutionService {
public void start() {
while (!Thread.interrupted()) {
if (canRun()) {
DownloadJob take = null;
DownloadJob take;
try {
take = downloadQ.take();
if (take == null) {
@@ -121,7 +129,7 @@ public class ExecutionService {
Failsafe.with(retryPolicy)
.onFailure(e -> {
if (e.getFailure() instanceof InterruptedException || (e.getFailure() instanceof FailsafeException && e.getFailure().getCause() instanceof InterruptedException)) {
logger.info("Job successfully interrupted");
logger.debug("Job successfully interrupted");
return;
}
logger.error(String.format("Failed to download %s after %d tries", finalTake.getImage().getUrl(), e.getAttemptCount()), e.getFailure());
@@ -129,7 +137,12 @@ public class ExecutionService {
})
.onComplete(e -> {
appStateService.doneDownloadJob(finalTake.getImage());
logger.info(String.format("Finished downloading %s", finalTake.getImage().getUrl()));
logger.debug(String.format("Finished downloading %s", finalTake.getImage().getUrl()));
if (appSettingsService.isViewPhotos()) {
VripperApplication.commonExecutor.submit(
() -> thumbnailGenerator.getThumbnails()
.get(new ThumbnailGenerator.CacheKey(finalTake.getImage().getPostId(), finalTake.getImageFileData().getFileName())));
}
synchronized (threadCount) {
threadCount.decrementAndGet();
running.remove(finalTake);
@@ -139,7 +152,7 @@ public class ExecutionService {
})
.get(finalTake::call);
};
logger.info(String.format("Scheduling a job for %s", finalTake.getImage().getUrl()));
logger.debug(String.format("Scheduling a job for %s", finalTake.getImage().getUrl()));
futures.put(finalTake.getImage().getUrl(), executor.submit(task));
} else {
synchronized (threadCount) {
@@ -10,6 +10,7 @@ public class ImageFileData {
private String pageUrl;
private String imageName;
private String fileName;
private String imageUrl;
private HttpUriRequest imageRequest;
}
@@ -7,6 +7,7 @@ import lombok.Setter;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.exception.ValidationException;
@@ -23,6 +24,9 @@ import java.util.prefs.Preferences;
@Setter
public class AppSettingsService {
@Value("${base.dir}")
private String defaultDownloadPath;
private static final Logger logger = LoggerFactory.getLogger(AppSettingsService.class);
private Preferences prefs = Preferences.userNodeForPackage(AppSettingsService.class);
@@ -37,8 +41,10 @@ public class AppSettingsService {
private final String DESKTOP_CLIPBOARD = "DESKTOP_CLIPBOARD";
private final String FORCE_ORDER = "FORCE_ORDER";
private final String SUBFOLDER = "SUBFOLDER";
private final String THREADSUBFOLDER = "THREADSUBFOLDER";
private final String CLEAR = "CLEAR";
private final String DARK_THEME = "DARK_THEME";
private final String VIEW_PHOTOS = "VIEW_PHOTOS";
private String downloadPath;
private int maxThreads;
@@ -49,9 +55,11 @@ public class AppSettingsService {
private boolean vThanks;
private boolean desktopClipboard;
private boolean subLocation;
private boolean threadSubLocation;
private boolean forceOrder;
private boolean clearCompleted;
private boolean darkTheme;
private boolean viewPhotos;
public void setVPassword(String vPassword) {
if(vPassword.isEmpty()) {
@@ -63,7 +71,7 @@ public class AppSettingsService {
public void restore() {
downloadPath = prefs.get(DOWNLOAD_PATH, System.getProperty("user.dir"));
downloadPath = prefs.get(DOWNLOAD_PATH, defaultDownloadPath);
maxThreads = prefs.getInt(MAX_THREADS, 4);
autoStart = prefs.getBoolean(AUTO_START, true);
vLogin = prefs.getBoolean(V_LOGIN, false);
@@ -73,8 +81,10 @@ public class AppSettingsService {
desktopClipboard = prefs.getBoolean(DESKTOP_CLIPBOARD, false);
forceOrder = prefs.getBoolean(FORCE_ORDER, false);
subLocation = prefs.getBoolean(SUBFOLDER, false);
threadSubLocation = prefs.getBoolean(THREADSUBFOLDER, false);
clearCompleted = prefs.getBoolean(CLEAR, false);
darkTheme = prefs.getBoolean(DARK_THEME, false);
viewPhotos = prefs.getBoolean(VIEW_PHOTOS, false);
}
@PreDestroy
@@ -90,8 +100,10 @@ public class AppSettingsService {
prefs.putBoolean(DESKTOP_CLIPBOARD, desktopClipboard);
prefs.putBoolean(FORCE_ORDER, forceOrder);
prefs.putBoolean(SUBFOLDER, subLocation);
prefs.putBoolean(THREADSUBFOLDER, threadSubLocation);
prefs.putBoolean(CLEAR, clearCompleted);
prefs.putBoolean(DARK_THEME, darkTheme);
prefs.putBoolean(VIEW_PHOTOS, viewPhotos);
try {
prefs.sync();
@@ -114,8 +126,8 @@ public class AppSettingsService {
throw new ValidationException(String.format("%s is not a directory", settings.getDownloadPath()));
}
if (settings.getMaxThreads() < 1 || settings.getMaxThreads() > 8) {
throw new ValidationException(String.format("Invalid max concurrent download settings, values must be in [%d,%d]", 1, 8));
if (settings.getMaxThreads() < 1 || settings.getMaxThreads() > 4) {
throw new ValidationException(String.format("Invalid max concurrent download settings, values must be in [%d,%d]", 1, 4));
}
}
@@ -135,7 +147,7 @@ public class AppSettingsService {
@JsonProperty("darkTheme")
private boolean darkTheme;
public Theme(boolean darkTheme) {
Theme(boolean darkTheme) {
this.darkTheme = darkTheme;
}
}
@@ -163,10 +175,14 @@ public class AppSettingsService {
private boolean forceOrder;
@JsonProperty("subLocation")
private boolean subLocation;
@JsonProperty("threadSubLocation")
private boolean threadSubLocation;
@JsonProperty("clearCompleted")
private boolean clearCompleted;
@JsonProperty("viewPhotos")
private boolean viewPhotos;
public Settings(String downloadPath, int maxThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder, boolean subLocation, boolean clearCompleted) {
public Settings(String downloadPath, int maxThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder, boolean subLocation, boolean threadSubLocation, boolean clearCompleted, boolean viewPhotos) {
this.downloadPath = downloadPath;
this.maxThreads = maxThreads;
this.autoStart = autoStart;
@@ -177,7 +193,9 @@ public class AppSettingsService {
this.desktopClipboard = desktopClipboard;
this.forceOrder = forceOrder;
this.subLocation = subLocation;
this.threadSubLocation = threadSubLocation;
this.clearCompleted = clearCompleted;
this.viewPhotos = viewPhotos;
}
}
}
@@ -1,6 +1,5 @@
package tn.mnlr.vripper.services;
import io.reactivex.disposables.Disposable;
import io.reactivex.processors.PublishProcessor;
import lombok.Getter;
import lombok.Setter;
@@ -12,7 +11,6 @@ import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.q.DownloadJob;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -31,6 +29,10 @@ public class AppStateService {
private Map<String, AtomicInteger> runningPosts = new ConcurrentHashMap<>();
private Map<String, QueuedVGLink> grabQueue = new ConcurrentHashMap<>();
private PublishProcessor<QueuedVGLink> liveGrabQueue = PublishProcessor.create();
private PublishProcessor<Image> liveImageUpdates = PublishProcessor.create();
private PublishProcessor<Post> livePostsState = PublishProcessor.create();
@@ -41,8 +43,6 @@ public class AppStateService {
@Autowired
private AppSettingsService appSettingsService;
private Disposable subscription;
public void onImageUpdate(Image imageState) {
persistenceService.getProcessor().onNext(currentPosts);
@@ -81,7 +81,7 @@ public class AppStateService {
post.setStatus(Post.Status.PARTIAL);
}
if (i == 0) {
if (post.getImages().stream().map(Image::getStatus).filter(e -> e.equals(Image.Status.ERROR)).count() > 0) {
if (post.getImages().stream().map(Image::getStatus).anyMatch(e -> e.equals(Image.Status.ERROR))) {
post.setStatus(Post.Status.ERROR);
} else {
if (!Post.Status.STOPPED.equals(post.getStatus())) {
@@ -104,13 +104,7 @@ public class AppStateService {
currentPosts.get(postId).setRemoved(true);
runningPosts.remove(postId);
currentPosts.remove(postId);
Iterator<Map.Entry<String, Image>> iterator = currentImages.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Image> entry = iterator.next();
if(entry.getValue().getPostId().equals(postId)) {
iterator.remove();
}
}
currentImages.entrySet().removeIf(entry -> entry.getValue().getPostId().equals(postId));
persistenceService.getProcessor().onNext(currentPosts);
}
@@ -4,16 +4,19 @@ import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.concurrent.TimeUnit;
@Service
@EnableScheduling
public class ConnectionManager {
private ConnectionManager() {
@@ -32,15 +35,21 @@ public class ConnectionManager {
private void buildConnectionPool() {
pcm = new PoolingHttpClientConnectionManager();
pcm.setMaxTotal(200);
pcm.setDefaultMaxPerRoute(10);
pcm.setMaxTotal(50);
pcm.setDefaultMaxPerRoute(4);
}
@Scheduled(fixedDelay = 1000)
private void idleConnectionMonitoring() {
pcm.closeExpiredConnections();
pcm.closeIdleConnections(30, TimeUnit.SECONDS);
}
public HttpClientBuilder getClient() {
return HttpClients.custom()
.setConnectionManager(pcm)
.setRedirectStrategy(new LaxRedirectStrategy())
.setRetryHandler(new DefaultHttpRequestRetryHandler(5, true))
.disableAutomaticRetries()
.setDefaultRequestConfig(rc);
}
@@ -13,7 +13,7 @@ public class GlobalState {
private long remaining;
private long error;
public GlobalState(long running, long queued, long remaining, long error) {
GlobalState(long running, long queued, long remaining, long error) {
this.running = running;
this.queued = queued;
this.remaining = remaining;
@@ -25,11 +25,7 @@ public class GlobalState {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GlobalState that = (GlobalState) o;
return running == that.running &&
queued == that.queued &&
remaining == that.remaining &&
error == that.error &&
Objects.equals(type, that.type);
return running == that.running && queued == that.queued && remaining == that.remaining && error == that.error;
}
@Override
@@ -29,6 +29,9 @@ public class GlobalStateService {
@Getter
private PublishProcessor<GlobalState> liveGlobalState = PublishProcessor.create();
public GlobalStateService() {
}
@Scheduled(fixedDelay = 3000)
private void interval() {
GlobalState newGlobalState = new GlobalState(
@@ -11,10 +11,6 @@ import tn.mnlr.vripper.exception.HtmlProcessorException;
@Service
public class HtmlProcessorService {
private HtmlProcessorService() {
}
public Document clean(String htmlContent) throws HtmlProcessorException {
try {
TagNode clean = new HtmlCleaner().clean(htmlContent);
@@ -22,7 +22,9 @@ public class PathService {
public final File getDownloadDestinationFolder(String postId) {
Post post = appStateService.getCurrentPosts().get(postId);
String postTitle = post.getTitle();
String threadTitle = post.getThreadTitle();
File sourceFolder = appSettingsService.isSubLocation() ? new File(appSettingsService.getDownloadPath(), sanitize(post.getForum())) : new File(appSettingsService.getDownloadPath());
sourceFolder = appSettingsService.isThreadSubLocation() ? new File(sourceFolder, threadTitle) : sourceFolder;
return new File(sourceFolder, sanitize(postTitle + "_" + postId));
}
@@ -35,17 +37,10 @@ public class PathService {
/**
* Will sanitize the image name and remove extension
*
* @param imageName
* @return
* @param imageName path string
* @return Sanitized local path string
*/
public final String formatImageFileName(String imageName) {
int extensionIndex = imageName.lastIndexOf('.');
String fileName;
if (extensionIndex != -1) {
fileName = imageName.substring(0, extensionIndex);
} else {
fileName = imageName;
}
return sanitize(fileName);
return sanitize(imageName);
}
}
@@ -29,7 +29,6 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
public class PersistenceService {
@@ -55,9 +54,7 @@ public class PersistenceService {
om.addMixIn(Post.class, PostPersistanceMixin.class);
subscription = processor
.onBackpressureLatest()
.buffer(10, TimeUnit.SECONDS)
.filter(e -> !e.isEmpty())
.map(e -> e.get(0))
.throttleLatest(1, TimeUnit.SECONDS)
.doOnNext(this::persist)
.subscribe();
}
@@ -67,19 +64,21 @@ public class PersistenceService {
File dataFile = new File(appCommandRunner.getDataPath());
if (!dataFile.exists()) {
try {
dataFile.getParentFile().mkdirs();
if (dataFile.getParentFile().mkdirs()) {
logger.debug(String.format("%s is created", dataFile.getParentFile().toString()));
}
if (!dataFile.getParentFile().isDirectory() || !dataFile.getParentFile().canWrite()) {
logger.error(String.format("Unable to write in %s", dataFile.getParent()));
SpringContext.close();
}
if (dataFile.createNewFile()) {
logger.info("Data file successfully created");
logger.debug("Data file successfully created");
try (FileWriter fw = new FileWriter(dataFile)) {
fw.write("{}");
}
} else {
logger.info("Data file already exists");
logger.warn("Data file already exists");
}
} catch (IOException e) {
logger.error("Unable to create data file", e);
@@ -96,9 +95,9 @@ public class PersistenceService {
this.persist(stateService.getCurrentPosts());
}
public void persist(Map<String, Post> currentPosts) {
private void persist(Map<String, Post> currentPosts) {
try (PrintWriter out = new PrintWriter(appCommandRunner.getDataPath(), "UTF-8")) {
try (PrintWriter out = new PrintWriter(appCommandRunner.getDataPath(), StandardCharsets.UTF_8)) {
out.print(om.writeValueAsString(currentPosts));
} catch (IOException e) {
logger.error("Failed to persist app state", e);
@@ -128,7 +127,7 @@ public class PersistenceService {
String jsonContent = null;
try {
jsonContent = Files.readAllLines(Paths.get(appCommandRunner.getDataPath()), StandardCharsets.UTF_8).stream().collect(Collectors.joining());
jsonContent = String.join("", Files.readAllLines(Paths.get(appCommandRunner.getDataPath()), StandardCharsets.UTF_8));
} catch (Exception e) {
logger.error("data file cannot be read, previous state cannot be restored", e);
SpringContext.close();
@@ -139,7 +138,7 @@ public class PersistenceService {
stateService.getCurrentPosts().clear();
stateService.getCurrentPosts().putAll(read);
stateService.getCurrentPosts().values().forEach(p -> {
if(Arrays.asList(Post.Status.DOWNLOADING, Post.Status.PARTIAL).contains(p.getStatus())) {
if (Arrays.asList(Post.Status.DOWNLOADING, Post.Status.PARTIAL, Post.Status.PENDING).contains(p.getStatus())) {
p.setStatus(Post.Status.STOPPED);
}
});
@@ -150,8 +149,6 @@ public class PersistenceService {
stateService.getCurrentImages().put(e.getUrl(), e);
});
read.values().forEach(e -> {
e.setAppStateService(stateService);
});
read.values().forEach(e -> e.setAppStateService(stateService));
}
}
@@ -1,50 +1,27 @@
package tn.mnlr.vripper.services;
import io.reactivex.processors.PublishProcessor;
import lombok.Getter;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.exception.DownloadException;
import tn.mnlr.vripper.exception.PostParseException;
import tn.mnlr.vripper.host.Host;
import tn.mnlr.vripper.q.DownloadQ;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
@Service
public class PostParser {
private static final Logger logger = LoggerFactory.getLogger(PostParser.class);
private static final String VR_API = "https://vipergirls.to/vr.php";
static final String VR_API = "https://vipergirls.to/vr.php";
@Autowired
private ConnectionManager cm;
SAXParserFactory factory = SAXParserFactory.newInstance();
@Autowired
private List<Host> supportedHosts;
@Autowired
private AppStateService appStateService;
@@ -60,25 +37,31 @@ public class PostParser {
@Autowired
private VipergirlsAuthService vipergirlsAuthService;
public PostParser() throws ParserConfigurationException, SAXException {
@Autowired
private List<Host> supportedHosts;
@PostConstruct
private void init() {
}
public void addPost(String postId, String threadId) throws PostParseException {
if (appStateService.getCurrentPosts().containsKey(postId)) {
logger.info(String.format("skipping %s, already loaded", postId));
logger.warn(String.format("skipping %s, already loaded", postId));
return;
}
VRPostParser vrPostParser = new VRPostParser(threadId, postId);
VRPostParser vrPostParser = new VRPostParser(threadId, postId, cm, vipergirlsAuthService, supportedHosts);
Post post = vrPostParser.parse();
post.setAppStateService(appStateService);
post.getImages().forEach(e -> e.setAppStateService(appStateService));
authService.leaveThanks(post.getUrl(), post.getPostId());
if (appSettingsService.isAutoStart()) {
logger.info("Auto start downloads option is enabled");
logger.info(String.format("Starting to enqueue %d jobs for %s", post.getImages().size(), post.getUrl()));
logger.debug("Auto start downloads option is enabled");
logger.debug(String.format("Starting to enqueue %d jobs for %s", post.getImages().size(), post.getUrl()));
post.setStatus(Post.Status.PENDING);
try {
downloadQ.enqueue(post);
} catch (InterruptedException e) {
@@ -86,306 +69,14 @@ public class PostParser {
Thread.currentThread().interrupt();
return;
}
logger.info(String.format("Done enqueuing jobs for %s", post.getUrl()));
logger.debug(String.format("Done enqueuing jobs for %s", post.getUrl()));
} else {
logger.info("Auto start downloads option is disabled");
post.setStatus(Post.Status.STOPPED);
logger.debug("Auto start downloads option is disabled");
}
}
@Getter
public static abstract class VRPostState {
private final String threadId;
protected VRPostState(String threadId) {
this.threadId = threadId;
}
}
@Getter
public static class VRPostParse extends VRPostState {
private final String type = "postParse";
private String postId;
private int number;
private String title;
private int imageCount;
private String url;
private List<String> previews;
public VRPostParse(String threadId, String postId, int number, String title, int imageCount, String url, List<String> previews) {
super(threadId);
this.postId = postId;
this.number = number;
this.title = title;
this.imageCount = imageCount;
this.previews = previews;
this.url = url;
}
}
@Getter
public static class VRThreadParseState extends VRPostState {
private final String type = "threadParseState";
private final String state;
public VRThreadParseState(String threadId, String state) {
super(threadId);
this.state = state;
}
}
public class VRThreadParser {
@Getter
private final PublishProcessor<VRPostState> postPublishProcessor = PublishProcessor.create();
private String threadId;
public VRThreadParser(String threadId) {
this.threadId = threadId;
}
public Void parse() throws PostParseException {
logger.info(String.format("Parsing thread %s", threadId));
HttpGet httpGet;
try {
URIBuilder uriBuilder = new URIBuilder(VR_API);
uriBuilder.setParameter("t", threadId);
httpGet = cm.buildHttpGet(uriBuilder.build());
if (vipergirlsAuthService.getCookies() != null) {
httpGet.addHeader("Cookie", vipergirlsAuthService.getCookies());
}
} catch (URISyntaxException e) {
throw new PostParseException(e);
}
VRThreadHandler handler = new VRThreadHandler(threadId, postPublishProcessor);
HttpClient connection = cm.getClient().build();
logger.info(String.format("Requesting %s", httpGet));
try (CloseableHttpResponse response = (CloseableHttpResponse) connection.execute(httpGet)) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new DownloadException(String.format("Unexpected response code '%d' for %s", response.getStatusLine().getStatusCode(), httpGet));
}
factory.newSAXParser().parse(new BufferedInputStream(response.getEntity().getContent()), handler);
EntityUtils.consumeQuietly(response.getEntity());
} catch (Exception e) {
logger.error("parsing failed", e);
throw new PostParseException(e);
}
return null;
}
}
private class VRThreadHandler extends DefaultHandler {
private final String threadId;
private final PublishProcessor<VRPostState> vrPostPublishProcessor;
private String threadTitle;
private String postId;
private String postTitle;
private int imageCount;
private int postCounter;
private int previewCounter = 0;
private List<String> previews = new ArrayList<>();
public VRThreadHandler(String threadId, PublishProcessor<VRPostState> vrPostPublishProcessor) {
this.vrPostPublishProcessor = vrPostPublishProcessor;
this.threadId = threadId;
}
@Override
public void startDocument() {
vrPostPublishProcessor.onNext(new VRThreadParseState(threadId, "START"));
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (qName.toLowerCase()) {
case "thread":
threadTitle = attributes.getValue("title").trim();
break;
case "post":
imageCount = Integer.parseInt(attributes.getValue("imagecount").trim());
postId = attributes.getValue("id").trim();
postCounter = Integer.parseInt(attributes.getValue("number").trim());
postTitle = Optional.ofNullable(attributes.getValue("title")).map(e -> e.trim().isEmpty() ? null : e.trim()).orElse(threadTitle);
break;
case "image":
if (previewCounter++ < 4) {
String thumbUrl = Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).orElse(null);
if (thumbUrl != null) {
previews.add(thumbUrl);
}
}
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
switch (qName.toLowerCase()) {
case "post":
if (imageCount != 0) {
vrPostPublishProcessor.onNext(new VRPostParse(
threadId,
postId,
postCounter,
postTitle,
imageCount,
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId),
previews
));
}
previewCounter = 0;
previews = new ArrayList<>();
break;
}
}
@Override
public void endDocument() {
vrPostPublishProcessor.onNext(new VRThreadParseState(threadId, "END"));
vrPostPublishProcessor.onComplete();
}
}
public class VRPostParser {
private String threadId;
private String postId;
public VRPostParser(String threadId, String postId) {
this.threadId = threadId;
this.postId = postId;
}
public Post parse() throws PostParseException {
Post post;
logger.info(String.format("Parsing post %s", postId));
HttpGet httpGet;
try {
URIBuilder uriBuilder = new URIBuilder(VR_API);
uriBuilder.setParameter("p", postId);
httpGet = cm.buildHttpGet(uriBuilder.build());
if (vipergirlsAuthService.getCookies() != null) {
httpGet.addHeader("Cookie", vipergirlsAuthService.getCookies());
}
} catch (URISyntaxException e) {
throw new PostParseException(e);
}
VRPostHandler handler = new VRPostHandler(threadId, postId);
HttpClient connection = cm.getClient().build();
logger.info(String.format("Requesting %s", httpGet));
try (CloseableHttpResponse response = (CloseableHttpResponse) connection.execute(httpGet)) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new DownloadException(String.format("Unexpected response code '%d' for %s", response.getStatusLine().getStatusCode(), httpGet));
}
factory.newSAXParser().parse(new BufferedInputStream(response.getEntity().getContent()), handler);
post = handler.getParsedPost();
EntityUtils.consumeQuietly(response.getEntity());
} catch (Exception e) {
logger.error("parsing failed", e);
throw new PostParseException(e);
}
return post;
}
}
private class VRPostHandler extends DefaultHandler {
private final String threadId;
private final String postId;
private String threadTitle;
private int previewCounter = 0;
private int index = 0;
private String postTitle;
private int imageCount;
private String forum;
private List<String> previews = new ArrayList<>();
private List<Image> images = new ArrayList<>();
@Getter
private Post parsedPost;
public VRPostHandler(String threadId, String postId) {
this.threadId = threadId;
this.postId = postId;
}
@Override
public void startDocument() {
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (qName.toLowerCase()) {
case "forum":
forum = attributes.getValue("title").trim();
break;
case "thread":
threadTitle = attributes.getValue("title").trim();
break;
case "post":
imageCount = Integer.parseInt(attributes.getValue("imagecount").trim());
postTitle = Optional.ofNullable(attributes.getValue("title")).map(e -> e.trim().isEmpty() ? null : e.trim()).orElse(threadTitle);
break;
case "image":
index++;
if (previewCounter++ < 4) {
String thumbUrl = Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).orElse(null);
if (thumbUrl != null) {
previews.add(thumbUrl);
}
}
String mainUrl = Optional.ofNullable(attributes.getValue("main_url")).map(String::trim).orElse(null);
if (mainUrl != null) {
Host foundHost = supportedHosts.stream().filter(host -> host.isSupported(mainUrl)).findFirst().orElse(null);
if (foundHost != null) {
logger.info(String.format("Found supported host %s for %s", foundHost.getClass().getSimpleName(), mainUrl));
images.add(new Image(mainUrl, postId, postTitle, foundHost, index));
} else {
logger.warn(String.format("unsupported host for %s, skipping", mainUrl));
}
}
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
switch (qName.toLowerCase()) {
case "post":
if (imageCount != 0) {
HashMap<String, Object> metadata = new HashMap<>();
metadata.put("PREVIEWS", previews);
parsedPost = new Post(
postTitle,
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId),
images,
metadata,
postId,
threadId,
forum
);
}
index = 0;
previewCounter = 0;
previews = new ArrayList<>();
images = new ArrayList<>();
break;
}
}
public VRThreadParser createVRThreadParser(String threadId) {
return new VRThreadParser(threadId, cm, vipergirlsAuthService, supportedHosts);
}
}
@@ -0,0 +1,38 @@
package tn.mnlr.vripper.services;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.Objects;
@Getter
@ToString
public class QueuedVGLink {
private final String type = "grabQueue";
private final String link;
private final String threadId;
private final String postId;
@Setter
boolean removed;
public QueuedVGLink(String link, String threadId, String postId) {
this.link = link;
this.threadId = threadId;
this.postId = postId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QueuedVGLink that = (QueuedVGLink) o;
return Objects.equals(link, that.link);
}
@Override
public int hashCode() {
return Objects.hash(link);
}
}
@@ -0,0 +1,105 @@
package tn.mnlr.vripper.services;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.imgscalr.Scalr;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.Files;
import java.util.Objects;
@Service
public class ThumbnailGenerator {
@Getter
private LoadingCache<CacheKey, byte[]> thumbnails;
@Value("${base.dir}")
private String baseDir;
@Autowired
private PathService pathService;
private File cacheFolder;
CacheLoader<CacheKey, byte[]> loader = new CacheLoader<>() {
@Override
public byte[] load(CacheKey key) throws Exception {
File destinationFolder = pathService.getDownloadDestinationFolder(key.getPostId());
if (!destinationFolder.exists() || !destinationFolder.isDirectory()) {
return null;
}
return Files.readAllBytes(generateThumbnail(new File(destinationFolder, key.getImgName()), key.getPostId()).toPath());
}
};
@PostConstruct
private void init() throws Exception {
cacheFolder = new File(baseDir + File.separator + ".vripper" + File.separator + "cache");
if (!cacheFolder.exists()) {
if (!cacheFolder.mkdirs()) {
throw new Exception(String.format("%s could not be created", cacheFolder.toString()));
}
}
thumbnails = CacheBuilder.newBuilder()
.maximumSize(20000)
.build(loader);
}
private File generateThumbnail(File inputFile, String postId) throws Exception {
if (!inputFile.exists()) {
throw new Exception(String.format("Input file %s does not exist", inputFile.toString()));
}
File postsCacheFolder = new File(cacheFolder, postId);
if (!postsCacheFolder.exists()) {
if (!postsCacheFolder.mkdirs()) {
throw new Exception(String.format("%s could not be created", postsCacheFolder.toString()));
}
}
File thumbFile = new File(postsCacheFolder, inputFile.getName());
if (thumbFile.exists()) {
return thumbFile;
}
BufferedImage image = ImageIO.read(inputFile);
BufferedImage resize = Scalr.resize(image, 350, 350);
ImageIO.write(resize, "jpg", thumbFile);
image.flush();
resize.flush();
return thumbFile;
}
@Getter
@Setter
@NoArgsConstructor
public static class CacheKey {
private String postId;
private String imgName;
public CacheKey(String postId, String imgName) {
this.postId = postId;
this.imgName = imgName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return Objects.equals(postId, cacheKey.postId) &&
Objects.equals(imgName, cacheKey.imgName);
}
@Override
public int hashCode() {
return Objects.hash(postId, imgName);
}
}
}
@@ -0,0 +1,86 @@
package tn.mnlr.vripper.services;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.VripperApplication;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
@Service
public class VGHandler {
private static final Logger logger = LoggerFactory.getLogger(VGHandler.class);
@Autowired
private AppStateService appStateService;
@Autowired
private PostParser postParser;
@Getter
private LoadingCache<String, List<VRPostState>> cache;
@PostConstruct
private void init() {
CacheLoader<String, List<VRPostState>> loader = new CacheLoader<>() {
@Override
public List<VRPostState> load(String threadId) throws Exception {
VRThreadParser vrThreadParser = postParser.createVRThreadParser(threadId);
List<VRPostState> posts = new ArrayList<>();
vrThreadParser.parse();
vrThreadParser.getPostPublishProcessor()
.onBackpressureBuffer()
.blockingSubscribe(posts::add);
return posts;
}
};
cache = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.build(loader);
}
public void handle(List<QueuedVGLink> queuedVGLinks) throws Exception {
for (QueuedVGLink queuedVGLink : queuedVGLinks) {
if (queuedVGLink.getPostId() != null) {
postParser.addPost(queuedVGLink.getPostId(), queuedVGLink.getThreadId());
} else {
appStateService.getGrabQueue().put(queuedVGLink.getLink(), queuedVGLink);
appStateService.getLiveGrabQueue().onNext(queuedVGLink);
Callable<Void> cl = () -> {
List<VRPostState> vrPostStates = cache.get(queuedVGLink.getThreadId());
logger.debug(String.format("%d found for %s", vrPostStates.size(), queuedVGLink.getLink()));
if (vrPostStates.size() == 1) {
postParser.addPost(vrPostStates.get(0).getPostId(), vrPostStates.get(0).getThreadId());
remove(queuedVGLink.getLink());
logger.debug(String.format("threadId %s, postId %s is added automatically for download", queuedVGLink.getThreadId(), queuedVGLink.getPostId()));
}
return null;
};
VripperApplication.commonExecutor.submit(cl);
}
}
}
public void remove(String url) {
Optional.ofNullable(appStateService.getGrabQueue().remove(url)).ifPresent(e -> {
e.setRemoved(true);
appStateService.getLiveGrabQueue().onNext(e);
});
}
}
@@ -0,0 +1,188 @@
package tn.mnlr.vripper.services;
import lombok.Getter;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.exception.DownloadException;
import tn.mnlr.vripper.exception.PostParseException;
import tn.mnlr.vripper.host.Host;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import static tn.mnlr.vripper.services.PostParser.VR_API;
class VRPostParser {
private static final Logger logger = LoggerFactory.getLogger(VRPostParser.class);
private static SAXParserFactory factory = SAXParserFactory.newInstance();
private static RetryPolicy<Object> retryPolicy = new RetryPolicy<>()
.handleIf(e -> e instanceof IOException)
.withBackoff(5, 30, ChronoUnit.SECONDS)
.withMaxRetries(4)
.abortOn(InterruptedException.class)
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
private final String threadId;
private final String postId;
private final ConnectionManager cm;
private final VipergirlsAuthService vipergirlsAuthService;
private final List<Host> supportedHosts;
VRPostParser(String threadId, String postId, ConnectionManager cm, VipergirlsAuthService vipergirlsAuthService, List<Host> supportedHosts) {
this.threadId = threadId;
this.postId = postId;
this.cm = cm;
this.vipergirlsAuthService = vipergirlsAuthService;
this.supportedHosts = supportedHosts;
}
public Post parse() throws PostParseException {
logger.debug(String.format("Parsing post %s", postId));
HttpGet httpGet;
try {
URIBuilder uriBuilder = new URIBuilder(VR_API);
uriBuilder.setParameter("p", postId);
httpGet = cm.buildHttpGet(uriBuilder.build());
} catch (URISyntaxException e) {
throw new PostParseException(e);
}
VRPostHandler handler = new VRPostHandler(threadId, postId, supportedHosts);
AtomicReference<Throwable> thr = new AtomicReference<>();
logger.debug(String.format("Requesting %s", httpGet));
Post post = Failsafe.with(retryPolicy)
.onFailure(e -> thr.set(e.getFailure()))
.get(() -> {
HttpClient connection = cm.getClient().build();
try (CloseableHttpResponse response = (CloseableHttpResponse) connection.execute(httpGet, vipergirlsAuthService.getContext())) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new DownloadException(String.format("Unexpected response code '%d' for %s", response.getStatusLine().getStatusCode(), httpGet));
}
try {
factory.newSAXParser().parse(new BufferedInputStream(response.getEntity().getContent()), handler);
return handler.getParsedPost();
} catch (Exception e) {
throw new PostParseException(String.format("Failed to parse thread %s, post %s", threadId, postId), e);
} finally {
EntityUtils.consumeQuietly(response.getEntity());
}
}
});
if (thr.get() != null || post == null) {
logger.error(String.format("parsing failed for thread %s, post %s", threadId, postId), thr.get());
throw new PostParseException(thr.get());
}
return post;
}
}
class VRPostHandler extends DefaultHandler {
private static final Logger logger = LoggerFactory.getLogger(VRPostHandler.class);
private final List<Host> supportedHosts;
private final String threadId;
private final String postId;
private List<Image> images = new ArrayList<>();
private List<String> previews = new ArrayList<>();
private String threadTitle;
private String postTitle;
private String forum;
private int previewCounter = 0;
private int index = 0;
private int imageCount;
@Getter
private Post parsedPost;
VRPostHandler(String threadId, String postId, List<Host> supportedHosts) {
this.threadId = threadId;
this.postId = postId;
this.supportedHosts = supportedHosts;
}
@Override
public void startDocument() {
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (qName.toLowerCase()) {
case "forum":
forum = attributes.getValue("title").trim();
break;
case "thread":
threadTitle = attributes.getValue("title").trim();
break;
case "post":
imageCount = Integer.parseInt(attributes.getValue("imagecount").trim());
postTitle = Optional.ofNullable(attributes.getValue("title")).map(e -> e.trim().isEmpty() ? null : e.trim()).orElse(threadTitle);
break;
case "image":
index++;
if (previewCounter++ < 4) {
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(previews::add);
}
String mainUrl = Optional.ofNullable(attributes.getValue("main_url")).map(String::trim).orElse(null);
if (mainUrl != null) {
Host foundHost = supportedHosts.stream().filter(host -> host.isSupported(mainUrl)).findFirst().orElse(null);
if (foundHost != null) {
logger.debug(String.format("Found supported host %s for %s", foundHost.getClass().getSimpleName(), mainUrl));
images.add(new Image(mainUrl, postId, postTitle, foundHost, index));
} else {
logger.warn(String.format("unsupported host for %s, skipping", mainUrl));
}
}
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("post".equals(qName.toLowerCase())) {
if (imageCount != 0) {
HashMap<String, Object> metadata = new HashMap<>();
metadata.put("PREVIEWS", previews);
parsedPost = new Post(
postTitle,
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId),
images,
metadata,
postId,
threadId,
threadTitle,
forum
);
}
index = 0;
previewCounter = 0;
previews = new ArrayList<>();
images = new ArrayList<>();
}
}
}
@@ -0,0 +1,29 @@
package tn.mnlr.vripper.services;
import lombok.Getter;
import java.util.List;
@Getter
public class VRPostState {
private final String type = "postParse";
private final String threadId;
private String postId;
private int number;
private String title;
private int imageCount;
private String url;
private List<String> previews;
private String hosts;
VRPostState(String threadId, String postId, int number, String title, int imageCount, String url, List<String> previews, String hosts) {
this.threadId = threadId;
this.postId = postId;
this.number = number;
this.title = title;
this.imageCount = imageCount;
this.previews = previews;
this.url = url;
this.hosts = hosts;
}
}
@@ -0,0 +1,174 @@
package tn.mnlr.vripper.services;
import io.reactivex.processors.ReplayProcessor;
import lombok.Getter;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import tn.mnlr.vripper.exception.DownloadException;
import tn.mnlr.vripper.exception.PostParseException;
import tn.mnlr.vripper.host.Host;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static tn.mnlr.vripper.services.PostParser.VR_API;
public class VRThreadParser {
private static final Logger logger = LoggerFactory.getLogger(VRThreadParser.class);
private static RetryPolicy<Object> retryPolicy = new RetryPolicy<>()
.handleIf(e -> e instanceof IOException)
.withBackoff(5, 30, ChronoUnit.SECONDS)
.withMaxRetries(4)
.abortOn(InterruptedException.class)
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
private static SAXParserFactory factory = SAXParserFactory.newInstance();
@Getter
private final ReplayProcessor<VRPostState> postPublishProcessor = ReplayProcessor.create();
private final String threadId;
private final ConnectionManager cm;
private final VipergirlsAuthService vipergirlsAuthService;
private final List<Host> hosts;
VRThreadParser(String threadId, ConnectionManager cm, VipergirlsAuthService vipergirlsAuthService, List<Host> hosts) {
this.threadId = threadId;
this.cm = cm;
this.vipergirlsAuthService = vipergirlsAuthService;
this.hosts = hosts;
}
public Void parse() throws PostParseException {
logger.debug(String.format("Parsing thread %s", threadId));
HttpGet httpGet;
try {
URIBuilder uriBuilder = new URIBuilder(VR_API);
uriBuilder.setParameter("t", threadId);
httpGet = cm.buildHttpGet(uriBuilder.build());
} catch (URISyntaxException e) {
throw new PostParseException(e);
}
VRThreadHandler handler = new VRThreadHandler(threadId, postPublishProcessor, hosts);
AtomicReference<Throwable> thr = new AtomicReference<>();
logger.debug(String.format("Requesting %s", httpGet));
Failsafe.with(retryPolicy)
.onFailure(e -> thr.set(e.getFailure()))
.get(() -> {
HttpClient connection = cm.getClient().build();
try (CloseableHttpResponse response = (CloseableHttpResponse) connection.execute(httpGet, vipergirlsAuthService.getContext())) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new DownloadException(String.format("Unexpected response code '%d' for %s", response.getStatusLine().getStatusCode(), httpGet));
}
try {
factory.newSAXParser().parse(new BufferedInputStream(response.getEntity().getContent()), handler);
} catch (Exception e) {
throw new PostParseException(String.format("Failed to parse thread %s", threadId), e);
} finally {
EntityUtils.consumeQuietly(response.getEntity());
}
}
return null;
});
if (thr.get() != null) {
logger.error(String.format("parsing failed for thread %s", threadId), thr.get());
throw new PostParseException(thr.get());
}
return null;
}
}
class VRThreadHandler extends DefaultHandler {
private final String threadId;
private final ReplayProcessor<VRPostState> vrPostPublishProcessor;
private final List<Host> supportedHosts;
private final Map<Host, AtomicInteger> hostMap = new HashMap<>();
private List<String> previews = new ArrayList<>();
private String threadTitle;
private String postId;
private String postTitle;
private int imageCount;
private int postCounter;
private int previewCounter = 0;
VRThreadHandler(String threadId, ReplayProcessor<VRPostState> vrPostPublishProcessor, List<Host> supportedHosts) {
this.vrPostPublishProcessor = vrPostPublishProcessor;
this.threadId = threadId;
this.supportedHosts = supportedHosts;
}
@Override
public void startDocument() {
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
switch (qName.toLowerCase()) {
case "thread":
threadTitle = attributes.getValue("title").trim();
break;
case "post":
imageCount = Integer.parseInt(attributes.getValue("imagecount").trim());
postId = attributes.getValue("id").trim();
postCounter = Integer.parseInt(attributes.getValue("number").trim());
postTitle = Optional.ofNullable(attributes.getValue("title")).map(e -> e.trim().isEmpty() ? null : e.trim()).orElse(threadTitle);
break;
case "image":
Optional.ofNullable(attributes.getValue("main_url"))
.map(String::trim)
.flatMap(mainUrl -> supportedHosts.stream().filter(host -> host.isSupported(mainUrl)).findFirst())
.ifPresent(host -> Optional.ofNullable(hostMap.get(host)).ifPresentOrElse(AtomicInteger::incrementAndGet, () -> hostMap.put(host, new AtomicInteger(0))));
if (previewCounter++ < 4) {
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(previews::add);
}
break;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("post".equals(qName.toLowerCase())) {
if (imageCount != 0) {
vrPostPublishProcessor.onNext(new VRPostState(
threadId,
postId,
postCounter,
postTitle,
imageCount,
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId),
previews,
hostMap.entrySet().stream().filter(v -> v.getValue().get() > 0).map(e -> e.getKey().getHost() + " (" + e.getValue().get() + ")").collect(Collectors.joining(", "))
));
}
previewCounter = 0;
previews = new ArrayList<>();
hostMap.clear();
}
}
@Override
public void endDocument() {
vrPostPublishProcessor.onComplete();
}
}
@@ -7,6 +7,9 @@ import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
@@ -18,6 +21,7 @@ import org.w3c.dom.Document;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.exception.VripperException;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
@@ -42,7 +46,7 @@ public class VipergirlsAuthService {
private AppSettingsService appSettingsService;
@Getter
private String cookies;
private HttpClientContext context = HttpClientContext.create();
private boolean authenticated = false;
@@ -52,6 +56,11 @@ public class VipergirlsAuthService {
@Getter
private PublishProcessor<String> loggedInUser = PublishProcessor.create();
@PostConstruct
private void init() {
context.setCookieStore(new BasicCookieStore());
}
@PreDestroy
private void destroy() throws InterruptedException {
logger.info("Shutting down VipergirlsAuthService");
@@ -65,8 +74,8 @@ public class VipergirlsAuthService {
authenticated = false;
if(!appSettingsService.isVLogin()) {
logger.warn("Authentication option is disabled");
cookies = null;
logger.debug("Authentication option is disabled");
context.getCookieStore().clear();
loggedUser = "";
loggedInUser.onNext(loggedUser);
return;
@@ -77,7 +86,7 @@ public class VipergirlsAuthService {
if (username == null || password == null || username.isEmpty() || password.isEmpty()) {
logger.error("Cannot authenticate with ViperGirls credentials, username or password is empty");
cookies = null;
context.getCookieStore().clear();
loggedUser = "";
loggedInUser.onNext(loggedUser);
return;
@@ -97,7 +106,7 @@ public class VipergirlsAuthService {
try {
postAuth.setEntity(new UrlEncodedFormEntity(params));
} catch (Exception e) {
cookies = null;
context.getCookieStore().clear();
loggedUser = "";
loggedInUser.onNext(loggedUser);
throw new VripperException(e);
@@ -108,28 +117,19 @@ public class VipergirlsAuthService {
CloseableHttpClient client = cm.getClient().build();
try (CloseableHttpResponse response = client.execute(postAuth)) {
try (CloseableHttpResponse response = client.execute(postAuth, context)) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new VripperException(String.format("Unexpected response code returned %s", response.getStatusLine().getStatusCode()));
}
StringBuilder sb = new StringBuilder();
Arrays.asList(response.getHeaders("set-cookie"))
.stream()
.map(e -> e.getValue())
.map(e -> e.split(";")[0])
.filter(e -> e.startsWith("vg_lastactivity") || e.startsWith("vg_userid") || e.startsWith("vg_password"))
.forEach(e -> sb.append(e).append(";"));
String cookies = sb.toString();
String responseBody = EntityUtils.toString(response.getEntity());
logger.debug(String.format("Authentication with ViperGirls response body:%n%s", responseBody));
EntityUtils.consumeQuietly(response.getEntity());
if(!cookies.contains("vg_userid")) {
if (context.getCookieStore().getCookies().stream().map(Cookie::getName).noneMatch(e -> e.equals("vg_userid"))) {
throw new VripperException("Failed to authenticate user with ViperRipper");
}
this.cookies = cookies;
} catch (Exception e) {
cookies = null;
context.getCookieStore().clear();
loggedUser = "";
loggedInUser.onNext(loggedUser);
if (e instanceof VripperException) {
@@ -144,14 +144,14 @@ public class VipergirlsAuthService {
loggedInUser.onNext(loggedUser);
}
public void leaveThanks(String postUrl, String postId) {
void leaveThanks(String postUrl, String postId) {
VripperApplication.commonExecutor.submit(() -> {
if (!appSettingsService.isVLogin()) {
logger.warn("Authentication with ViperGirls option is disabled");
logger.debug("Authentication with ViperGirls option is disabled");
return;
}
if (!appSettingsService.isVThanks()) {
logger.warn("Leave thanks option is disabled");
logger.debug("Leave thanks option is disabled");
return;
}
if (!authenticated) {
@@ -168,16 +168,15 @@ public class VipergirlsAuthService {
private String getSecurityToken(String url) throws VripperException {
String securityToken = "";
String securityToken;
HttpGet httpGet = cm.buildHttpGet(url);
httpGet.addHeader("Referer", "https://vipergirls.to/");
httpGet.addHeader("Host", "vipergirls.to");
httpGet.addHeader("Cookie", cookies);
CloseableHttpClient client = cm.getClient().build();
try (CloseableHttpResponse response = client.execute(httpGet)) {
try (CloseableHttpResponse response = client.execute(httpGet, context)) {
String postPage = EntityUtils.toString(response.getEntity());
Document document = htmlProcessorService.clean(postPage);
@@ -189,7 +188,7 @@ public class VipergirlsAuthService {
.getTextContent()
.trim();
securityToken = Arrays.asList(thanksUrl.split("&amp;")).stream()
securityToken = Arrays.stream(thanksUrl.split("&amp;"))
.filter(v -> v.startsWith("securitytoken"))
.findAny()
.orElse("")
@@ -218,11 +217,10 @@ public class VipergirlsAuthService {
postThanks.addHeader("Referer", "https://vipergirls.to/");
postThanks.addHeader("Host", "vipergirls.to");
postThanks.addHeader("Cookie", cookies);
CloseableHttpClient client = cm.getClient().build();
try (CloseableHttpResponse response = client.execute(postThanks)) {
try (CloseableHttpResponse response = client.execute(postThanks, context)) {
EntityUtils.consumeQuietly(response.getEntity());
} catch (Exception e) {
throw new VripperException(e);
@@ -2,7 +2,6 @@ package tn.mnlr.vripper.services;
import org.springframework.stereotype.Service;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import tn.mnlr.vripper.exception.XpathException;
import javax.xml.xpath.XPath;
@@ -12,20 +11,8 @@ import javax.xml.xpath.XPathFactory;
@Service
public class XpathService {
private XpathService() {
}
private XPath xpath = XPathFactory.newInstance().newXPath();
public NodeList getAsNodeList(Node source, String xpathExpression) throws XpathException {
try {
return (NodeList) xpath.compile(xpathExpression).evaluate(source, XPathConstants.NODESET);
} catch (Exception e) {
throw new XpathException(e);
}
}
public Node getAsNode(Node source, String xpathExpression) throws XpathException {
try {
return (Node) xpath.compile(xpathExpression).evaluate(source, XPathConstants.NODE);
@@ -16,6 +16,6 @@ public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*").withSockJS();
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*").withSockJS().setClientLibraryUrl("../../assets/sockjs.min.js");
}
}
@@ -0,0 +1,143 @@
package tn.mnlr.vripper.web.restendpoints;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.PathService;
import tn.mnlr.vripper.services.ThumbnailGenerator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@RestController
@CrossOrigin(value = "*")
public class GalleryEndpoint {
private static final Logger logger = LoggerFactory.getLogger(GalleryEndpoint.class);
@Autowired
private PathService pathService;
@Autowired
private ThumbnailGenerator thumbnailGenerator;
private static final String cacheControl = CacheControl.maxAge(365, TimeUnit.DAYS).getHeaderValue();
@Autowired
private AppSettingsService appSettingsService;
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception e) {
logger.error("Error when process request", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(e.getMessage());
}
@GetMapping(value = "/image/{postId}/{imgName}", produces = {MediaType.IMAGE_JPEG_VALUE})
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<byte[]> getImage(@PathVariable("postId") @NonNull String postId, @PathVariable("imgName") @NonNull String imgName, HttpServletResponse response) throws Exception {
if (!appSettingsService.isViewPhotos()) {
return new ResponseEntity("Gallery option is disabled", HttpStatus.BAD_REQUEST);
}
response.addHeader("Cache-Control", cacheControl);
File destinationFolder = pathService.getDownloadDestinationFolder(postId);
return ResponseEntity.ok(Files.readAllBytes(Paths.get(destinationFolder.toPath().toString(), imgName)));
}
@GetMapping(value = "/image/thumb/{postId}/{imgName}", produces = {MediaType.IMAGE_JPEG_VALUE})
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<byte[]> getImageThumb(@PathVariable("postId") @NonNull String postId, @PathVariable("imgName") @NonNull String imgName, HttpServletResponse response) throws Exception {
if (!appSettingsService.isViewPhotos()) {
return new ResponseEntity("Gallery option is disabled", HttpStatus.BAD_REQUEST);
}
response.addHeader("Cache-Control", cacheControl);
File destinationFolder = pathService.getDownloadDestinationFolder(postId);
if (destinationFolder.exists() && destinationFolder.isDirectory()) {
return ResponseEntity.ok(thumbnailGenerator.getThumbnails().get(new ThumbnailGenerator.CacheKey(postId, imgName)));
}
return new ResponseEntity("Gallery does not exist in download location, you probably removed it", HttpStatus.BAD_REQUEST);
}
@GetMapping("/gallery/{postId}")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<List<GalleryImage>> getGallery(@PathVariable("postId") @NonNull String postId) {
if (!appSettingsService.isViewPhotos()) {
return new ResponseEntity("Gallery option is disabled", HttpStatus.BAD_REQUEST);
}
File destinationFolder = pathService.getDownloadDestinationFolder(postId);
if (destinationFolder.exists() && destinationFolder.isDirectory()) {
return ResponseEntity.ok(
Arrays.stream(Objects.requireNonNull(destinationFolder.listFiles()))
.filter(f -> !f.getName().endsWith("tmp"))
.sorted(Comparator.comparing(File::getName))
.map(GalleryImage::fromFile)
.filter(Objects::nonNull)
.collect(Collectors.toList())
);
}
return new ResponseEntity("Gallery does not exist in download location, you probably removed it", HttpStatus.BAD_REQUEST);
}
}
@Getter
@Setter
@NoArgsConstructor
class GalleryImage {
private static final Logger logger = LoggerFactory.getLogger(GalleryImage.class);
private String src;
private String msrc;
private double w;
private double h;
public GalleryImage(String src, String msrc, double w, double h) {
this.src = src;
this.msrc = msrc;
this.w = w;
this.h = h;
}
public static GalleryImage fromFile(File file) {
Dimension dimension;
try (ImageInputStream in = ImageIO.createImageInputStream(file)) {
final Iterator<ImageReader> readers = ImageIO.getImageReaders(in);
if (readers.hasNext()) {
ImageReader reader = readers.next();
try {
reader.setInput(in);
dimension = new Dimension(reader.getWidth(0), reader.getHeight(0));
} finally {
reader.dispose();
}
} else {
logger.error(String.format("No reader found for image %s", file.toString()));
return null;
}
} catch (Exception e) {
logger.error(String.format("Failed to create image object for %s", file.toString()), e);
return null;
}
return new GalleryImage(file.getName(), file.getName(), dimension.getWidth(), dimension.getHeight());
}
}
@@ -1,7 +1,6 @@
package tn.mnlr.vripper.web.restendpoints;
import lombok.Getter;
import lombok.ToString;
import lombok.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -11,15 +10,13 @@ import org.springframework.web.bind.annotation.*;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.exception.PostParseException;
import tn.mnlr.vripper.q.DownloadQ;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.AppStateService;
import tn.mnlr.vripper.services.PathService;
import tn.mnlr.vripper.services.PostParser;
import tn.mnlr.vripper.services.*;
import java.io.File;
import java.util.List;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@RestController
@CrossOrigin(value = "*")
@@ -27,8 +24,7 @@ public class PostRestEndpoint {
private static final Logger logger = LoggerFactory.getLogger(PostRestEndpoint.class);
private static final Pattern VG_URL_PATTERN = Pattern.compile("https:\\/\\/vipergirls\\.to\\/threads\\/(\\d+)((.*p=)(\\d+))?");
private static final Pattern VG_URL_PATTERN = Pattern.compile("https://vipergirls\\.to/threads/(\\d+)((.*p=)(\\d+))?");
@Autowired
private AppStateService appStateService;
@@ -39,8 +35,11 @@ public class PostRestEndpoint {
@Autowired
private PathService pathService;
@Autowired
private VGHandler vgHandler;
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception e) {
public ResponseEntity<String> handleException(Exception e) {
logger.error("Error when process request", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
@@ -55,33 +54,41 @@ public class PostRestEndpoint {
@PostMapping("/post")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity processPost(@RequestBody ThreadUrl url) throws Exception {
logger.info(String.format("Starting to process thread: %s", url.url));
if (url.url == null || url.url.isEmpty()) {
return new ResponseEntity<>("Failed to process empty request", HttpStatus.BAD_REQUEST);
} else if (!url.url.startsWith("https://vipergirls.to")) {
return new ResponseEntity<>("ViperGirls only links are supported", HttpStatus.BAD_REQUEST);
public ResponseEntity processPost(@RequestBody ThreadUrl _url) throws Exception {
if (_url.getUrl() == null || _url.getUrl().isEmpty()) {
return new ResponseEntity("Failed to process empty request", HttpStatus.BAD_REQUEST);
}
String threadId, postId;
try {
Matcher m = VG_URL_PATTERN.matcher(url.url);
if (m.find()) {
threadId = m.group(1);
postId = m.group(4);
} else {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url));
List<String> urls = Arrays.stream(_url.getUrl().split("\\r?\\n")).map(String::trim).filter(e -> !e.isEmpty()).collect(Collectors.toList());
for (String url : urls) {
logger.debug(String.format("Starting to process thread: %s", url));
if (!url.startsWith("https://vipergirls.to")) {
logger.error(String.format("Unsupported link %s", url));
continue;
}
} catch (Exception e) {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url), e);
String threadId, postId;
try {
Matcher m = VG_URL_PATTERN.matcher(url);
if (m.find()) {
threadId = m.group(1);
postId = m.group(4);
} else {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url));
}
} catch (Exception e) {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url), e);
}
vgHandler.handle(Collections.singletonList(new QueuedVGLink(url, threadId, postId)));
}
return ResponseEntity.ok(new PairThreadIdPostId(threadId, postId));
return ResponseEntity.ok().build();
}
@PostMapping("/post/restart")
@ResponseStatus(value = HttpStatus.OK)
public void restartPost(@RequestBody PostId postId) throws Exception {
downloadQ.restart(postId.getPostId());
public synchronized void restartPost(@RequestBody @NonNull List<PostId> postIds) throws Exception {
for (PostId postId : postIds) {
downloadQ.restart(postId.getPostId());
}
}
@PostMapping("/post/add")
@@ -90,9 +97,9 @@ public class PostRestEndpoint {
for (PostToAdd post : posts) {
VripperApplication.commonExecutor.submit(() -> {
try {
postParser.addPost(post.postId, post.threadId);
postParser.addPost(post.getPostId(), post.getThreadId());
} catch (PostParseException e) {
logger.error(String.format("Failed to add post %s", post.postId), e);
logger.error(String.format("Failed to add post %s", post.getPostId()), e);
}
});
}
@@ -113,8 +120,10 @@ public class PostRestEndpoint {
@PostMapping("/post/stop")
@ResponseStatus(value = HttpStatus.OK)
public void stop(@RequestBody PostId postId) {
downloadQ.stop(postId.getPostId());
public synchronized void stop(@RequestBody @NonNull List<PostId> postIds) {
for (PostId postId : postIds) {
downloadQ.stop(postId.getPostId());
}
}
@PostMapping("/post/stop/all")
@@ -125,110 +134,146 @@ public class PostRestEndpoint {
@PostMapping("/post/remove")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity remove(@RequestBody PostId postId) {
downloadQ.stop(postId.getPostId());
appStateService.remove(postId.getPostId());
return ResponseEntity.ok(new RemoveResult(postId.getPostId()));
public synchronized ResponseEntity<List<RemoveResult>> remove(@RequestBody @NonNull List<PostId> postIds) {
List<RemoveResult> result = new ArrayList<>();
for (PostId postId : postIds) {
downloadQ.stop(postId.getPostId());
appStateService.remove(postId.getPostId());
result.add(new RemoveResult(postId.getPostId()));
}
return ResponseEntity.ok(result);
}
@PostMapping("/post/clear/all")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity clearAll() {
public ResponseEntity<RemoveAllResult> clearAll() {
return ResponseEntity.ok(new RemoveAllResult(appStateService.clearAll()));
}
@PostMapping("/post/remove/all")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity removeAll() {
public ResponseEntity<RemoveAllResult> removeAll() {
return ResponseEntity.ok(new RemoveAllResult(appStateService.removeAll()));
}
@Getter
@ToString
private static class ThreadUrl {
private String url;
@GetMapping("/grab/{threadId}")
@ResponseStatus(value = HttpStatus.OK)
public synchronized ResponseEntity<List<VRPostState>> grab(@PathVariable("threadId") @NonNull ThreadId threadId) throws Exception {
return ResponseEntity.ok(vgHandler.getCache().get(threadId.getThreadId()));
}
@Getter
private static class PostId {
private String postId;
}
@Getter
private static class PairThreadIdPostId {
private String threadId;
private String postId;
public PairThreadIdPostId(String threadId, String postId) {
this.threadId = threadId;
this.postId = postId;
}
}
@Getter
private static class PostToAdd {
private String threadId;
private String postId;
}
@Getter
private static class ParseResult {
private List<PostResult> posts;
private int count;
private String threadId;
public ParseResult(List<PostResult> posts, int count, String threadId) {
this.posts = posts;
this.count = count;
this.threadId = threadId;
}
@Getter
public static class PostResult {
private String title;
private int counter;
private String url;
private String postId;
private List<String> previews;
public PostResult(String title, int counter, String url, String postId, List<String> previews) {
this.title = title;
this.counter = counter;
this.url = url;
this.postId = postId;
this.previews = previews;
}
}
}
@Getter
private static class RemoveAllResult {
private List<String> postIds;
private int removed;
RemoveAllResult(List<String> postIds) {
this.removed = postIds.size();
this.postIds = postIds;
}
}
@Getter
private static class RemoveResult {
private String postId;
RemoveResult(String postId) {
this.postId = postId;
}
}
@Getter
private static class DownloadPath {
private String path;
DownloadPath(String path) {
this.path = path;
}
@PostMapping("/grab/remove")
@ResponseStatus(value = HttpStatus.OK)
public synchronized void grabRemove(@RequestBody @NonNull ThreadUrl threadUrl) {
vgHandler.remove(threadUrl.getUrl());
}
}
@Getter
@Setter
@NoArgsConstructor
class ThreadUrl {
private String url;
public ThreadUrl(String url) {
this.url = url;
}
}
@Getter
@Setter
@NoArgsConstructor
class ThreadId {
private String threadId;
public ThreadId(String threadId) {
this.threadId = threadId;
}
}
@Getter
@Setter
@NoArgsConstructor
class PostId {
private String postId;
public PostId(String postId) {
this.postId = postId;
}
}
@Getter
@Setter
@NoArgsConstructor
class PostToAdd {
private String threadId;
private String postId;
public PostToAdd(String threadId, String postId) {
this.threadId = threadId;
this.postId = postId;
}
}
@Getter
@Setter
@NoArgsConstructor
class RemoveAllResult {
private List<String> postIds;
private int removed;
RemoveAllResult(List<String> postIds) {
this.removed = postIds.size();
this.postIds = postIds;
}
}
@Getter
@Setter
@NoArgsConstructor
class RemoveResult {
private String postId;
RemoveResult(String postId) {
this.postId = postId;
}
}
@Getter
@Setter
@NoArgsConstructor
class DownloadPath {
private String path;
DownloadPath(String path) {
this.path = path;
}
}
@Getter
@Setter
@NoArgsConstructor
@ToString
class PairThreadIdPostId {
private String threadId;
private String postId;
public PairThreadIdPostId(String threadId, String postId) {
this.threadId = threadId;
this.postId = postId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PairThreadIdPostId that = (PairThreadIdPostId) o;
return Objects.equals(threadId, that.threadId) &&
Objects.equals(postId, that.postId);
}
@Override
public int hashCode() {
return Objects.hash(threadId, postId);
}
}
@@ -54,7 +54,9 @@ public class SettingsRestEndpoint {
try {
this.settings.check(settings);
} catch (ValidationException e) {
return new ResponseEntity(new Response(e.getMessage()), HttpStatus.BAD_REQUEST);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
this.settings.setDownloadPath(settings.getDownloadPath());
this.settings.setMaxThreads(settings.getMaxThreads());
@@ -75,7 +77,9 @@ public class SettingsRestEndpoint {
this.settings.setDesktopClipboard(settings.isDesktopClipboard());
this.settings.setForceOrder(settings.isForceOrder());
this.settings.setSubLocation(settings.isSubLocation());
this.settings.setThreadSubLocation(settings.isThreadSubLocation());
this.settings.setClearCompleted(settings.isClearCompleted());
this.settings.setViewPhotos(settings.isViewPhotos());
this.settings.save();
@@ -99,15 +103,17 @@ public class SettingsRestEndpoint {
settings.isDesktopClipboard(),
settings.isForceOrder(),
settings.isSubLocation(),
settings.isClearCompleted()
settings.isThreadSubLocation(),
settings.isClearCompleted(),
settings.isViewPhotos()
);
}
@Getter
class Response {
static class Response {
String message;
public Response(String message) {
Response(String message) {
this.message = message;
}
}
@@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -12,7 +14,6 @@ import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.entities.mixin.ui.ImageUIMixin;
@@ -20,7 +21,7 @@ import tn.mnlr.vripper.entities.mixin.ui.PostUIMixin;
import tn.mnlr.vripper.services.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@@ -54,10 +55,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
private Map<String, Disposable> postsSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> postDetailsSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> vrPostParserSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> stateSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> downloadSpeedSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> userSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> grabQueueSubscriptions = new ConcurrentHashMap<>();
private Map<String, Future<Void>> threadParseRequests = new ConcurrentHashMap<>();
@@ -69,6 +70,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
WSMessage wsMessage = om.readValue(message.getPayload(), WSMessage.class);
WSMessage.CMD cmd = WSMessage.CMD.valueOf(wsMessage.getCmd());
switch (cmd) {
case GRAB_QUEUE_SUB:
subscribeForGrabQueue(session);
break;
case GLOBAL_STATE_SUB:
subscribeForGlobalState(session);
break;
@@ -81,49 +85,45 @@ public class WebSocketHandler extends TextWebSocketHandler {
case POSTS_SUB:
subscribeForPosts(session);
break;
case THREAD_PARSING_SUB:
subscribeForThreadParsing(session, wsMessage.getPayload());
break;
case POST_DETAILS_SUB:
subscribeForPostDetails(session, wsMessage.getPayload());
break;
case POST_DETAILS_UNSUB:
logger.info(String.format("Client %s unsubscribed from post details", session.getId()));
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
break;
case THREAD_PARSING_UNSUB:
logger.info(String.format("Client %s unsubscribed from thread parsing", session.getId()));
Optional.ofNullable(vrPostParserSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(threadParseRequests.remove(session.getId())).ifPresent(d -> d.cancel(true));
logger.debug(String.format("Client %s unsubscribed from post details", session.getId()));
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
case POSTS_UNSUB:
logger.info(String.format("Client %s unsubscribed from posts", session.getId()));
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
logger.debug(String.format("Client %s unsubscribed from posts", session.getId()));
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
case GLOBAL_STATE_UNSUB:
logger.info(String.format("Client %s unsubscribed from global state", session.getId()));
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
logger.debug(String.format("Client %s unsubscribed from global state", session.getId()));
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
case SPEED_UNSUB:
logger.info(String.format("Client %s unsubscribed from download speed info", session.getId()));
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
logger.debug(String.format("Client %s unsubscribed from download speed info", session.getId()));
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
case USER_UNSUB:
logger.info(String.format("Client %s unsubscribed from user info", session.getId()));
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
logger.debug(String.format("Client %s unsubscribed from user info", session.getId()));
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
case GRAB_QUEUE_UNSUB:
logger.debug(String.format("Client %s unsubscribed from grab queue", session.getId()));
Optional.ofNullable(grabQueueSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
}
}
private void subscribeForGlobalState(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for global state", session.getId()));
logger.debug(String.format("Client %s subscribed for global state", session.getId()));
if (stateSubscriptions.containsKey(session.getId())) {
stateSubscriptions.get(session.getId()).dispose();
}
try {
send(session, new TextMessage(om.writeValueAsString(Arrays.asList(globalStateService.getCurrentState()))));
send(session, new TextMessage(om.writeValueAsString(Collections.singletonList(globalStateService.getCurrentState()))));
} catch (Exception e) {
logger.error("Unexpected error occurred", e);
}
@@ -143,13 +143,13 @@ public class WebSocketHandler extends TextWebSocketHandler {
private void subscribeForSpeed(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for download speed info", session.getId()));
logger.debug(String.format("Client %s subscribed for download speed info", session.getId()));
if (downloadSpeedSubscriptions.containsKey(session.getId())) {
downloadSpeedSubscriptions.get(session.getId()).dispose();
}
try {
send(session, new TextMessage(om.writeValueAsString(Arrays.asList(new DownloadSpeed(0)))));
send(session, new TextMessage(om.writeValueAsString(Collections.singletonList(new DownloadSpeed(0)))));
} catch (Exception e) {
logger.error("Unexpected error occurred", e);
}
@@ -170,13 +170,13 @@ public class WebSocketHandler extends TextWebSocketHandler {
private void subscribeForUser(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for user info", session.getId()));
logger.debug(String.format("Client %s subscribed for user info", session.getId()));
if (userSubscriptions.containsKey(session.getId())) {
userSubscriptions.get(session.getId()).dispose();
}
try {
send(session, new TextMessage(om.writeValueAsString(Arrays.asList(new LoggedUser(vipergirlsAuthService.getLoggedUser())))));
send(session, new TextMessage(om.writeValueAsString(Collections.singletonList(new LoggedUser(vipergirlsAuthService.getLoggedUser())))));
} catch (Exception e) {
logger.error("Unexpected error occurred", e);
}
@@ -185,7 +185,7 @@ public class WebSocketHandler extends TextWebSocketHandler {
vipergirlsAuthService.getLoggedInUser()
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.map(e -> Arrays.asList(new LoggedUser(e)))
.map(e -> Collections.singletonList(new LoggedUser(e)))
.map(om::writeValueAsString)
.map(TextMessage::new)
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
@@ -194,7 +194,7 @@ public class WebSocketHandler extends TextWebSocketHandler {
private void subscribeForPosts(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for posts", session.getId()));
logger.debug(String.format("Client %s subscribed for posts", session.getId()));
if (postsSubscriptions.containsKey(session.getId())) {
postsSubscriptions.get(session.getId()).dispose();
}
@@ -218,35 +218,35 @@ public class WebSocketHandler extends TextWebSocketHandler {
);
}
private void subscribeForThreadParsing(WebSocketSession session, String threadId) {
private void subscribeForGrabQueue(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for thread parsing with threadId = %s", session.getId(), threadId));
if (vrPostParserSubscriptions.containsKey(session.getId())) {
vrPostParserSubscriptions.get(session.getId()).dispose();
logger.debug(String.format("Client %s subscribed for grab queue", session.getId()));
if (grabQueueSubscriptions.containsKey(session.getId())) {
grabQueueSubscriptions.get(session.getId()).dispose();
}
if (threadParseRequests.containsKey(session.getId())) {
threadParseRequests.get(session.getId()).cancel(true);
try {
send(session, new TextMessage(om.writeValueAsString(appStateService.getGrabQueue().values())));
} catch (Exception e) {
logger.error("Unexpected error occurred", e);
}
PostParser.VRThreadParser vrThreadParser = postParser.new VRThreadParser(threadId);
vrPostParserSubscriptions.put(session.getId(), vrThreadParser.getPostPublishProcessor()
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.buffer(200)
.filter(e -> !e.isEmpty())
.map(om::writeValueAsString)
.map(TextMessage::new)
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
grabQueueSubscriptions.put(session.getId(),
appStateService.getLiveGrabQueue()
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.buffer(2000, TimeUnit.MILLISECONDS, 200)
.filter(e -> !e.isEmpty())
.map(e -> e.stream().distinct().collect(Collectors.toList()))
.map(om::writeValueAsString)
.map(TextMessage::new)
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
);
threadParseRequests.put(session.getId(), VripperApplication.commonExecutor.submit(vrThreadParser::parse));
}
private void subscribeForPostDetails(WebSocketSession session, String postId) {
logger.info(String.format("Client %s subscribed for post details with id = %s", session.getId(), postId));
logger.debug(String.format("Client %s subscribed for post details with id = %s", session.getId(), postId));
if (postDetailsSubscriptions.containsKey(session.getId())) {
postDetailsSubscriptions.get(session.getId()).dispose();
}
@@ -282,24 +282,26 @@ public class WebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) {
logger.info(String.format("Connection open for client id: %s", session.getId()));
logger.debug(String.format("Connection open for client id: %s", session.getId()));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(vrPostParserSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(grabQueueSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(threadParseRequests.remove(session.getId())).ifPresent(d -> d.cancel(true));
logger.info(String.format("Connection closed for client id: %s", session.getId()));
logger.debug(String.format("Connection closed for client id: %s", session.getId()));
}
@Getter
@Setter
@NoArgsConstructor
private static class WSMessage {
private String cmd;
@@ -314,10 +316,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
GLOBAL_STATE_UNSUB,
SPEED_SUB,
SPEED_UNSUB,
THREAD_PARSING_SUB,
THREAD_PARSING_UNSUB,
USER_SUB,
USER_UNSUB
USER_UNSUB,
GRAB_QUEUE_SUB,
GRAB_QUEUE_UNSUB
}
}
@@ -327,7 +329,7 @@ public class WebSocketHandler extends TextWebSocketHandler {
private final String type = "user";
private String user;
public LoggedUser(String user) {
LoggedUser(String user) {
this.user = user;
}
}
@@ -6,4 +6,5 @@ server.port=${vripper.server.port:8080}
management.endpoints.web.exposure.include=shutdown
management.endpoint.shutdown.enabled=true
endpoints.shutdown.enabled=true
spring.profiles.active=portable
spring.profiles.active=portable
#spring.profiles.active=installer
+14 -4
View File
@@ -27,9 +27,14 @@
"src/assets"
],
"styles": [
"src/styles.scss"
"src/styles.scss",
"./node_modules/photoswipe/dist/photoswipe.css",
"./node_modules/photoswipe/dist/default-skin/default-skin.css"
],
"scripts": []
"scripts": [
"./node_modules/photoswipe/dist/photoswipe.min.js",
"./node_modules/photoswipe/dist/photoswipe-ui-default.min.js"
]
},
"configurations": {
"production": {
@@ -83,9 +88,14 @@
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.scss"
"src/styles.scss",
"./node_modules/photoswipe/dist/photoswipe.css",
"./node_modules/photoswipe/dist/default-skin/default-skin.css"
],
"scripts": [
"./node_modules/photoswipe/dist/photoswipe.min.js",
"./node_modules/photoswipe/dist/photoswipe-ui-default.min.js"
],
"scripts": [],
"assets": [
"src/favicon.ico",
"src/assets"
+5434 -2397
View File
File diff suppressed because it is too large Load Diff
+22 -21
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "2.0.4",
"version": "2.8.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -13,31 +13,32 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.3",
"@angular/cdk": "^7.3.1",
"@angular/common": "~7.2.0",
"@angular/compiler": "~7.2.0",
"@angular/core": "~7.2.0",
"@angular/flex-layout": "^7.0.0-beta.23",
"@angular/forms": "~7.2.0",
"@angular/material": "^7.3.0",
"@angular/platform-browser": "~7.2.0",
"@angular/platform-browser-dynamic": "~7.2.0",
"@angular/router": "~7.2.0",
"ag-grid-angular": "^21.2.1",
"ag-grid-community": "^21.2.1",
"@angular/animations": "^8.2.14",
"@angular/cdk": "^8.2.3",
"@angular/common": "~8.2.14",
"@angular/compiler": "~8.2.14",
"@angular/core": "~8.2.14",
"@angular/flex-layout": "^8.0.0-beta.27",
"@angular/forms": "~8.2.14",
"@angular/material": "^8.2.3",
"@angular/platform-browser": "~8.2.14",
"@angular/platform-browser-dynamic": "~8.2.14",
"@angular/router": "~8.2.14",
"ag-grid-angular": "^22.1.0",
"ag-grid-community": "^22.1.0",
"core-js": "^2.5.4",
"hammerjs": "^2.0.8",
"ngx-electron": "^2.1.1",
"rxjs": "~6.3.3",
"ngx-electron": "^2.2.0",
"photoswipe": "^4.1.3",
"rxjs": "~6.5.3",
"tslib": "^1.9.0",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.13.9",
"@angular/cli": "~7.2.2",
"@angular/compiler-cli": "^7.2.15",
"@angular/language-service": "~7.2.0",
"@angular-devkit/build-angular": "^0.803.21",
"@angular/cli": "~8.3.20",
"@angular/compiler-cli": "^8.2.14",
"@angular/language-service": "~8.2.14",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
@@ -53,6 +54,6 @@
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.2.2"
"typescript": "~3.5.0"
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.0.4</version>
<version>2.8.0</version>
</parent>
<artifactId>vripper-ui</artifactId>
<name>vripper-ui</name>
+22 -42
View File
@@ -1,45 +1,25 @@
<div fxLayout="column" *ngIf="connecting()" class="overlay loading" fxLayoutAlign="center center">
<mat-spinner></mat-spinner>
<h2>Loading...</h2>
</div>
<div fxLayout="column" *ngIf="noConnectionState()" class="overlay no-connection" fxLayoutAlign="center center">
<mat-icon class="overlay-icon">error</mat-icon>
<h2>The app seems to be shutdown</h2>
</div>
<div [ngClass]="{'electron': electronService.isElectronApp}" id="app-container" fxLayout="column" fxLayoutGap="20px">
<div id="app-header" fxFlex="nogrow">
<mat-toolbar class="mat-elevation-z8" color="accent">
<mat-toolbar-row fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="10px">
<div id="left-controls">
<button (click)="scan()" class="add-button" color="primary" mat-icon-button>
<mat-icon>add</mat-icon>
</button>
<button (click)="restartAll()" aria-label="Start" mat-icon-button title="Start">
<mat-icon>play_arrow</mat-icon>
</button>
<button (click)="stopAll()" aria-label="Stop" mat-icon-button title="Stop">
<mat-icon>stop</mat-icon>
</button>
<button (click)="clear()" aria-label="Clear completed" mat-icon-button title="Clear completed">
<mat-icon>clear_all</mat-icon>
</button>
<button (click)="remove()" aria-label="Remove All" mat-icon-button title="Remove All">
<mat-icon>delete</mat-icon>
</button>
</div>
<span fxFlex="grow" fxLayoutAlign="end center">
<p style="font-size: 16px">Logged in as: {{loggedUser.user == null || loggedUser.user === '' ? 'guest': loggedUser.user }}</p>
</span>
<div id="right-controls">
<button (click)="openSettings()" aria-label="settings" mat-icon-button>
<mat-icon>menu</mat-icon>
</button>
</div>
</mat-toolbar-row>
</mat-toolbar>
<ng-container *ngIf="appState | async as state">
<div *ngIf="state === 'CONNECTING'" class="overlay loading" fxLayout="column" fxLayoutAlign="center center">
<div class="sk-chase">
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
</div>
</div>
<div id="app-body" fxFlex="grow">
<router-outlet></router-outlet>
<div *ngIf="state === 'DISCONNECTED'" class="overlay no-connection" fxLayout="column" fxLayoutAlign="center center">
<mat-icon class="overlay-icon">error</mat-icon>
<h2>Error connecting to daemon, try restarting the application</h2>
</div>
<div *ngIf="state === 'CONNECTED'" [ngClass]="{ electron: electron | async }" fxLayout="column" id="app-container">
<div fxFlex="nogrow" id="app-header">
<app-toolbar></app-toolbar>
</div>
<div fxFlex="grow" id="app-body" style="margin-bottom: 20px;">
<router-outlet></router-outlet>
</div>
<app-status-bar style="width: 100%"></app-status-bar>
</div>
</div>
</ng-container>
+72 -17
View File
@@ -1,21 +1,76 @@
#logo {
height: 48px;
margin-right: 10px;
background-image: url('../assets/logo.png');
background-repeat: no-repeat;
background-size: 48px 48px;
}
#title {
user-select: none
}
.overlay-icon {
font-size: 128px;
width: 128px;
height: 128px;
font-size: 64px;
width: 64px;
height: 64px;
}
.no-connection {
color: #eb6060;
.no-connection, .loading {
background-color: #2c3e50;
}
.overlay {
position: absolute;
display: block;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
}
.sk-chase {
width: 60px;
height: 60px;
position: relative;
animation: sk-chase 2.5s infinite linear both;
}
.sk-chase-dot {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
animation: sk-chase-dot 2.0s infinite ease-in-out both;
}
.sk-chase-dot:before {
content: '';
display: block;
width: 25%;
height: 25%;
background-color: #fff;
border-radius: 100%;
animation: sk-chase-dot-before 2.0s infinite ease-in-out both;
}
.sk-chase-dot:nth-child(1) { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2) { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3) { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4) { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5) { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6) { animation-delay: -0.6s; }
.sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; }
@keyframes sk-chase {
100% { transform: rotate(360deg); }
}
@keyframes sk-chase-dot {
80%, 100% { transform: rotate(360deg); }
}
@keyframes sk-chase-dot-before {
50% {
transform: scale(0.4);
} 100%, 0% {
transform: scale(1.0);
}
}
+23 -160
View File
@@ -1,185 +1,48 @@
import { AppService } from './app.service';
import { ClipboardService } from './clipboard.service';
import { ElectronService } from 'ngx-electron';
import { SettingsComponent } from './settings/settings.component';
import { Component, OnInit, OnDestroy, NgZone, Renderer2, AfterViewInit } from '@angular/core';
import { MatDialog, MatSnackBar } from '@angular/material';
import { BreakpointObserver, BreakpointState, Breakpoints } from '@angular/cdk/layout';
import { Observable, Subscription } from 'rxjs';
import { Component, OnDestroy, AfterViewInit, Renderer2, ChangeDetectionStrategy, NgZone } from '@angular/core';
import { MatDialog } from '@angular/material';
import { Subscription, BehaviorSubject, Subject, merge } from 'rxjs';
import { WsConnectionService, WSState } from './ws-connection.service';
import { LoggedUser } from './common/logged-user.model';
import { WsHandler } from './ws-handler';
import { CMD } from './common/cmd.enum';
import { WSMessage } from './common/ws-message.model';
import { HttpClient } from '@angular/common/http';
import { RemoveAllResponse } from './common/remove-all-response.model';
import { ServerService } from './server-service';
import { ConfirmDialogComponent } from './common/confirmation-component/confirmation-dialog';
import { filter, flatMap } from 'rxjs/operators';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
export class AppComponent implements OnDestroy, AfterViewInit {
constructor(
public dialog: MatDialog,
private breakpointObserver: BreakpointObserver,
private dialog: MatDialog,
private ws: WsConnectionService,
public electronService: ElectronService,
private clipboardService: ClipboardService,
private ngZone: NgZone,
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar,
private appService: AppService,
private renderer: Renderer2,
private appService: AppService
private ngZone: NgZone
) {
this.websocketHandlerPromise = this.ws.getConnection();
this.electron = new BehaviorSubject(electronService.isElectronApp);
}
subscriptions: Subscription[] = [];
websocketHandlerPromise: Promise<WsHandler>;
currentState: WSState;
themeLoaded = false;
loggedUser: LoggedUser = new LoggedUser(null);
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
private subscriptions: Subscription[] = [];
appState: Subject<string> = new BehaviorSubject('CONNECTING');
electron: Subject<boolean>;
ngAfterViewInit() {
this.appService.renderer = this.renderer;
}
scan() {
this.appService.scan();
}
openSettings(): void {
const dialogRef = this.dialog.open(SettingsComponent, {
width: '70%',
height: '70%',
maxWidth: '100vw',
maxHeight: '100vh'
});
const smallDialogSubscription = this.isExtraSmall.subscribe(result => {
if (result.matches) {
dialogRef.updateSize('100%', '100%');
} else {
dialogRef.updateSize('70%', '70%');
}
});
dialogRef.afterClosed().subscribe(result => {
smallDialogSubscription.unsubscribe();
});
}
noConnectionState(): boolean {
return this.currentState === WSState.CLOSE || this.currentState === WSState.ERROR;
}
connecting(): boolean {
return (this.currentState === WSState.INIT || this.currentState === WSState.CONNECTING) && !this.themeLoaded ;
}
ngOnInit() {
this.websocketHandlerPromise.then((handler: WsHandler) => {
console.log('Connecting to user stream');
this.subscriptions.push(
handler.subscribeForUser((e: LoggedUser[]) => {
this.ngZone.run(() => {
this.loggedUser = e[0];
});
})
);
handler.send(new WSMessage(CMD.USER_SUB.toString()));
});
this.currentState = WSState.INIT;
this.ws.state.subscribe(e => {
this.currentState = e;
if (this.currentState === WSState.CLOSE || this.currentState === WSState.ERROR) {
this.dialog.closeAll();
} else if (this.currentState === WSState.OPEN) {
this.subscriptions.push(this.ws.state.subscribe(wsState => {
if (wsState === WSState.CLOSE || wsState === WSState.ERROR) {
setTimeout(() => this.ngZone.run(() => {
this.dialog.closeAll();
this.appState.next('DISCONNECTED');
}), 500);
} else if (wsState === WSState.OPEN) {
this.clipboardService.init();
this.appService
.loadTheme()
.subscribe(() => this.ngZone.run(() => this.themeLoaded = true));
merge(this.appService.loadTheme(), this.appService.loadSettings())
.subscribe(() => this.ngZone.run(() => this.appState.next('CONNECTED')));
}
});
}
clear() {
this.ngZone.run(() => {
this.httpClient.post<RemoveAllResponse>(this.serverService.baseUrl + '/post/clear/all', {}).subscribe(
data => {
this._snackBar.open(`${data.removed} items cleared`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}
remove() {
this.ngZone.run(() => {
this.dialog
.open(ConfirmDialogComponent, {
maxHeight: '100vh',
maxWidth: '100vw',
height: '200px',
width: '60%',
data: { header: 'Confirmation', content: 'Are you sure you want to remove all items ?' }
})
.afterClosed()
.pipe(
filter(e => e === 'yes'),
flatMap(e => this.httpClient.post<RemoveAllResponse>(this.serverService.baseUrl + '/post/remove/all', {}))
)
.subscribe(
data => {
this._snackBar.open(`${data.removed} items removed`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}
stopAll() {
this.ngZone.run(() => {
this.httpClient.post(this.serverService.baseUrl + '/post/stop/all', {}).subscribe(
() => {
this._snackBar.open(`Download stopped`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}
restartAll() {
this.ngZone.run(() => {
this.httpClient.post(this.serverService.baseUrl + '/post/restart/all', {}).subscribe(
() => {
this._snackBar.open(`Download started`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}));
}
ngOnDestroy() {
+26 -5
View File
@@ -1,3 +1,6 @@
import { ContextMenuService } from './ctxt-menu.service';
import { PostContextMenuComponent } from './posts/post-context-menu.component';
import { PostsDataService } from './posts-data.service';
import { AppPreviewDirective } from './common/preview-tooltip.directive';
import { WsConnectionService } from './ws-connection.service';
import { AppService } from './app.service';
@@ -31,6 +34,12 @@ import { UrlRendererComponent } from './multi-post/url-renderer.component';
import { FilterComponent } from './filter/filter.component';
import { ScanComponent } from './scan/scan.component';
import { StatusBarComponent } from './status-bar/status-bar.component';
import { SelectionService } from './selection-service';
import { ToolbarComponent } from './toolbar/toolbar.component';
import { GrabQueueComponent } from './grab-queue/grab-queue.component';
import { UrlGrabRendererComponent } from './grab-queue/url-renderer.component';
import { GalleryComponent } from './gallery/gallery.component';
import { PhotoSwipeComponent } from './photo-swipe/photo-swipe.component';
@NgModule({
declarations: [
@@ -47,17 +56,25 @@ import { StatusBarComponent } from './status-bar/status-bar.component';
AppPreviewComponent,
AppPreviewDirective,
UrlRendererComponent,
UrlGrabRendererComponent,
FilterComponent,
ScanComponent,
StatusBarComponent
StatusBarComponent,
ToolbarComponent,
GrabQueueComponent,
PostContextMenuComponent,
GalleryComponent,
PhotoSwipeComponent
],
entryComponents: [
PostDetailComponent,
SettingsComponent,
ConfirmDialogComponent,
// MultiPostComponent,
AppPreviewComponent,
ScanComponent
ScanComponent,
MultiPostComponent,
PostContextMenuComponent,
GalleryComponent
],
imports: [
BrowserAnimationsModule,
@@ -71,7 +88,8 @@ import { StatusBarComponent } from './status-bar/status-bar.component';
AgGridModule.withComponents([
PostProgressRendererComponent,
PostDetailsProgressRendererComponent,
UrlRendererComponent
UrlRendererComponent,
UrlGrabRendererComponent
]),
OverlayModule
],
@@ -80,7 +98,10 @@ import { StatusBarComponent } from './status-bar/status-bar.component';
WsConnectionService,
{ provide: HTTP_INTERCEPTORS, useClass: XhrInterceptorService, multi: true },
ServerService,
SharedService
SharedService,
SelectionService,
PostsDataService,
ContextMenuService
],
bootstrap: [AppComponent]
})
+36 -12
View File
@@ -1,11 +1,14 @@
import { Settings } from './common/settings.model';
import { environment } from 'src/environments/environment';
import { MatDialog } from '@angular/material';
import { HttpClient } from '@angular/common/http';
import { Injectable, Renderer2, NgZone } from '@angular/core';
import { Injectable, Renderer2 } from '@angular/core';
import { ServerService } from './server-service';
import { tap } from 'rxjs/operators';
import { ScanComponent } from './scan/scan.component';
import { Observable } from 'rxjs';
import { Observable, Subject } from 'rxjs';
import { BreakpointState, Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { Title } from '@angular/platform-browser';
@Injectable()
export class AppService {
@@ -14,20 +17,34 @@ export class AppService {
private serverService: ServerService,
public dialog: MatDialog,
private breakpointObserver: BreakpointObserver,
) {}
private titleService: Title
) {
this.titleService.setTitle('VRipper ' + environment.version);
}
private _darkTheme = false;
private _settings: Settings;
private _renderer: Renderer2;
// private settings$: Subject<Settings> = new Subject();
get settings(): Settings {
return { ...this._settings };
}
get darkTheme(): boolean {
return this._darkTheme;
}
darkTheme = false;
_renderer: Renderer2;
set renderer(renderer: Renderer2) {
this._renderer = renderer;
}
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
isScanOpen = false;
updateTheme(darkTheme: boolean) {
this.darkTheme = darkTheme;
if (this.darkTheme) {
this._darkTheme = darkTheme;
if (this._darkTheme) {
this._renderer.addClass(document.body, 'dark-theme');
this._renderer.removeClass(document.body, 'light-theme');
} else {
@@ -36,17 +53,27 @@ export class AppService {
}
this.httpClient
.post<Theme>(this.serverService.baseUrl + '/settings/theme', {
darkTheme: this.darkTheme
darkTheme: this._darkTheme
})
.subscribe();
}
updateSettings(settings: Settings) {
this._settings = settings;
}
loadTheme() {
return this.httpClient
.get<Theme>(this.serverService.baseUrl + '/settings/theme')
.pipe(tap(theme => this.updateTheme(theme.darkTheme)));
}
loadSettings() {
return this.httpClient
.get<Settings>(this.serverService.baseUrl + '/settings')
.pipe(tap(settings => this.updateSettings(settings)));
}
scan(url?: string) {
const scanDialog = this.dialog.open(ScanComponent, {
@@ -65,11 +92,8 @@ export class AppService {
}
});
scanDialog.afterOpened().subscribe(() => this.isScanOpen = true);
scanDialog.afterClosed().subscribe(() => {
smallDialogSubscription.unsubscribe();
this.isScanOpen = false;
});
}
}
+3 -1
View File
@@ -5,10 +5,12 @@ export enum CMD {
SPEED_SUB = 'SPEED_SUB',
USER_SUB = 'USER_SUB',
THREAD_PARSING_SUB = 'THREAD_PARSING_SUB',
GRAB_QUEUE_SUB = 'GRAB_QUEUE_SUB',
POSTS_UNSUB = 'POSTS_UNSUB',
POST_DETAILS_UNSUB = 'POST_DETAILS_UNSUB',
GLOBAL_STATE_UNSUB = 'GLOBAL_STATE_UNSUB',
SPEED_UNSUB = 'SPEED_UNSUB',
USER_UNSUB = 'USER_UNSUB',
THREAD_PARSING_UNSUB = 'THREAD_PARSING_UNSUB'
THREAD_PARSING_UNSUB = 'THREAD_PARSING_UNSUB',
GRAB_QUEUE_UNSUB = 'GRAB_QUEUE_UNSUB'
}
@@ -4,11 +4,10 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
@Component({
selector: 'app-preview',
template: `
<div class="previews" style="display: inline-block; white-space: nowrap;">
<div class="previews" style="display: inline-block; white-space: nowrap; height: 200px; max-width: 400px">
<ng-container *ngFor="let link of links">
<img
[@simpleFadeAnimation]="'in'"
class="mat-elevation-z8"
[src]="link"
/>
</ng-container>
@@ -18,9 +17,8 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
`
img {
max-height: 200px;
max-width: 200px;
max-width: 400px;
margin-left: 5px;
border: 1px solid #900000;
}
`
],
@@ -1,30 +1,48 @@
import { AppPreviewComponent } from './preview-tooltip.component';
import { Directive, Input, HostListener, ComponentRef, OnInit, ElementRef, NgZone } from '@angular/core';
import { Directive, Input, HostListener, ComponentRef, OnInit, ElementRef, NgZone, OnDestroy } from '@angular/core';
import { OverlayRef, Overlay, OverlayPositionBuilder } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
@Directive({ selector: '[appPreview]' })
export class AppPreviewDirective implements OnInit {
export class AppPreviewDirective implements OnInit, OnDestroy {
private overlayRef: OverlayRef;
tooltipPortal = new ComponentPortal(AppPreviewComponent);
@Input() appPreview: string[];
constructor(private overlayPositionBuilder: OverlayPositionBuilder,
constructor(
private overlayPositionBuilder: OverlayPositionBuilder,
private elementRef: ElementRef,
private overlay: Overlay,
private ngZone: NgZone) {}
private ngZone: NgZone
) {}
ngOnInit() {
const positionStrategy = this.overlayPositionBuilder
.flexibleConnectedTo(this.elementRef)
.withPositions([{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom',
}]);
this.overlayRef = this.overlay.create({ positionStrategy });
.flexibleConnectedTo(this.elementRef)
.withPush(true)
.withGrowAfterOpen(true)
.withPositions([
{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
},
{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom'
}
]);
this.overlayRef = this.overlay.create({ positionStrategy, scrollStrategy: this.overlay.scrollStrategies.close() });
}
ngOnDestroy() {
this.ngZone.run(() => {
this.overlayRef.dispose();
});
}
@HostListener('mouseenter')
@@ -7,4 +7,5 @@ export interface Settings {
vPassword: string;
vThanks: boolean;
desktopClipboard: boolean;
viewPhotos: boolean;
}
@@ -6,10 +6,7 @@ export class VRPostParse {
public title: string,
public imageCount: number,
public url: string,
public previews: string[]
public previews: string[],
public hosts: string
) {}
}
export class VRThreadParseState {
constructor(public threadId: string, public state: string) {}
}
+68
View File
@@ -0,0 +1,68 @@
import { PostState } from './posts/post-state.model';
import { Injectable, ComponentRef } from '@angular/core';
import { filter, take } from 'rxjs/operators';
import { fromEvent, Subscription } from 'rxjs';
import { OverlayPositionBuilder, Overlay, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { PostContextMenuComponent } from './posts/post-context-menu.component';
@Injectable()
export class ContextMenuService {
constructor(private overlayPositionBuilder: OverlayPositionBuilder, private overlay: Overlay) {}
postCtxtMenuPortal = new ComponentPortal(PostContextMenuComponent);
private postCtxtMenuOverlayRef: OverlayRef;
sub: Subscription;
openPostCtxtMenu(mouseEvent: MouseEvent, postState: PostState) {
mouseEvent.preventDefault();
this.closePostCtxtMenu();
const positionStrategy = this.overlayPositionBuilder
.flexibleConnectedTo({ x: mouseEvent.x, y: mouseEvent.y })
.withPush(true)
.withGrowAfterOpen(true)
.withPositions([
{
originX: 'start',
originY: 'bottom',
overlayX: 'start',
overlayY: 'top'
},
{
originX: 'start',
originY: 'top',
overlayX: 'start',
overlayY: 'bottom'
}
]);
this.postCtxtMenuOverlayRef = this.overlay.create({
positionStrategy
});
const ref: ComponentRef<PostContextMenuComponent> = this.postCtxtMenuOverlayRef.attach(this.postCtxtMenuPortal);
ref.instance.postState = postState;
ref.instance.contextMenuService = this;
this.sub = fromEvent<MouseEvent>(document, 'click')
.pipe(
filter(event => {
const clickTarget = event.target as HTMLElement;
return !!this.postCtxtMenuOverlayRef && !this.postCtxtMenuOverlayRef.overlayElement.contains(clickTarget);
}),
take(1)
)
.subscribe(() => this.closePostCtxtMenu());
}
closePostCtxtMenu() {
if (this.sub != null) {
this.sub.unsubscribe();
}
if (this.postCtxtMenuOverlayRef) {
this.postCtxtMenuOverlayRef.dispose();
this.postCtxtMenuOverlayRef = null;
}
}
}
export interface Theme {
darkTheme: boolean;
}
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterComponent } from './filter.component';
describe('FilterComponent', () => {
let component: FilterComponent;
let fixture: ComponentFixture<FilterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FilterComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FilterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,26 @@
<div class="dialog-container" fxLayout="column" style="height: 100%;">
<div fxFlex="nogrow">
<h2 class="no-wrap" mat-dialog-title>{{ dialogData.title }}</h2>
</div>
<mat-dialog-content fxFlex="grow">
<div class="container">
<div class="masonry ">
<ng-container *ngFor="let img of images | async; let i=index">
<figure class="masonry-brick " fxLayout="row" fxLayoutAlign="center center"
style="position: relative; cursor: pointer;">
<mat-progress-spinner *ngIf="!img._initialized" mode="indeterminate"
style="position: absolute;"></mat-progress-spinner>
<img (click)="openSlideshow(i)" (load)="loaded(img)" [src]="img.msrc"/>
</figure>
</ng-container>
<app-photo-swipe #photoSwipe></app-photo-swipe>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" fxFlex="nogrow">
<button (click)="refresh()" color="primary" mat-raised-button>
<mat-icon>refresh</mat-icon>
<span>Refresh</span></button>
<button mat-dialog-close mat-raised-button>Close</button>
</mat-dialog-actions>
</div>
@@ -0,0 +1,25 @@
.container {
height: 100%;
}
.masonry {
display: flex;
flex-flow: row wrap;
margin-left: -8px;
width: 100%;
}
.masonry-brick {
flex: auto;
height: 250px;
min-width: 150px;
margin: 0 8px 8px 0;
}
img {
height: 250px;
width: auto;
}
figure {
}
@@ -0,0 +1,87 @@
import { IImage, PhotoSwipeComponent } from './../photo-swipe/photo-swipe.component';
import { Component, OnInit, ChangeDetectionStrategy, Inject, NgZone, OnDestroy, ViewChild } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { MatDialogRef, MAT_DIALOG_DATA, MatSnackBar } from '@angular/material';
import { ServerService } from '../server-service';
import { PostState } from '../posts/post-state.model';
import { BehaviorSubject, Subject } from 'rxjs';
class Image extends IImage {
constructor(public src: string, public msrc: string, public w: number, public h: number, public _initialized: boolean) {
super(src, msrc, w, h);
}
}
@Component({
selector: 'app-gallery',
templateUrl: './gallery.component.html',
styleUrls: ['./gallery.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GalleryComponent implements OnInit, OnDestroy {
constructor(
public dialogRef: MatDialogRef<GalleryComponent>,
@Inject(MAT_DIALOG_DATA) public dialogData: PostState,
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar,
private ngZone: NgZone
) {}
images: Subject<Image[]> = new BehaviorSubject([]);
_images: Image[] = [];
@ViewChild('photoSwipe', { static: true }) photoSwipe: PhotoSwipeComponent;
loaded(img: Image) {
img._initialized = true;
}
ngOnInit() {
this.refresh();
}
ngOnDestroy() {}
openSlideshow(index: number) {
this.photoSwipe.openGallery(this._images, {
index: index,
getThumbBoundsFn: this.getThumbBoundsFn,
showHideOpacity: true,
preload: [1, 8]
});
}
getThumbBoundsFn(index: number) {
// See Options -> getThumbBoundsFn section of documentation for more info
const thumbnail = document.querySelectorAll('.masonry-brick img')[index],
pageYScroll = window.pageYOffset || document.documentElement.scrollTop,
rect = thumbnail.getBoundingClientRect();
return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
}
refresh() {
this.httpClient.get<Image[]>(this.serverService.baseUrl + '/gallery/' + this.dialogData.postId).subscribe(
response => {
this.ngZone.run(() => {
response.forEach(i => {
i.src = this.serverService.baseUrl + '/image/' + this.dialogData.postId + '/' + i.src;
i.msrc = this.serverService.baseUrl + '/image/thumb/' + this.dialogData.postId + '/' + i.msrc;
i._initialized = false;
});
this._images = response;
this.images.next(this._images);
});
},
error => {
this.ngZone.run(() => {
this.dialogRef.close();
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
});
}
);
}
}
@@ -0,0 +1,2 @@
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
</ag-grid-angular>
@@ -0,0 +1,48 @@
import { UrlGrabRendererComponent } from './url-renderer.component';
import { GrabQueueDataSource } from './grab-queue.datasource';
import { Component, OnInit, NgZone } from '@angular/core';
import { GridOptions } from 'ag-grid-community';
import { WsConnectionService } from '../ws-connection.service';
@Component({
selector: 'app-grab-queue',
templateUrl: './grab-queue.component.html',
styleUrls: ['./grab-queue.component.scss']
})
export class GrabQueueComponent implements OnInit {
constructor(private wsConnection: WsConnectionService, private zone: NgZone) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
headerName: 'Url',
field: 'link',
sortable: true,
sort: 'asc',
suppressMovable: true,
cellRenderer: 'urlCellRenderer'
}
],
rowHeight: 48,
animateRows: true,
rowData: [],
frameworkComponents: {
urlCellRenderer: UrlGrabRendererComponent
},
overlayLoadingTemplate: '<span></span>',
overlayNoRowsTemplate: '<span></span>',
getRowNodeId: data => data['link'],
onGridReady: () => {
this.gridOptions.api.sizeColumnsToFit();
this.dataSource = new GrabQueueDataSource(this.wsConnection, this.gridOptions, this.zone);
this.dataSource.connect();
},
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit()
};
}
gridOptions: GridOptions;
dataSource: GrabQueueDataSource;
ngOnInit() {}
}
@@ -0,0 +1,60 @@
import { GrabQueueState } from './grab-queue.model';
import { CMD } from './../common/cmd.enum';
import { WSMessage } from './../common/ws-message.model';
import { Subscription } from 'rxjs';
import { WsConnectionService } from '../ws-connection.service';
import { GridOptions } from 'ag-grid-community';
import { WsHandler } from '../ws-handler';
import { NgZone } from '@angular/core';
export class GrabQueueDataSource {
constructor(
private wsConnectionService: WsConnectionService,
private gridOptions: GridOptions,
private zone: NgZone
) {
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
}
websocketHandlerPromise: Promise<WsHandler>;
subscriptions: Subscription[] = [];
connect() {
this.websocketHandlerPromise.then((handler: WsHandler) => {
console.log('Connecting to grab queue datasource');
this.subscriptions.push(
handler.subscribeForGrabQueue((e: GrabQueueState[]) => {
this.zone.run(() => {
const toAdd = [];
const toUpdate = [];
const toRemove = [];
e.forEach(v => {
if (v.removed) {
if (this.gridOptions.api.getRowNode(v.link) != null) {
toRemove.push(this.gridOptions.api.getRowNode(v.link).data);
}
return;
}
if (this.gridOptions.api.getRowNode(v.link) == null) {
toAdd.push(v);
} else {
toUpdate.push(v);
}
});
this.gridOptions.api.updateRowData({ update: toUpdate, add: toAdd, remove: toRemove });
});
})
);
handler.send(new WSMessage(CMD.GRAB_QUEUE_SUB.toString()));
});
}
disconnect() {
console.log('Disconnecting from grab queue datasource');
this.subscriptions.forEach(e => e.unsubscribe());
this.websocketHandlerPromise.then((handler: WsHandler) => {
handler.send(new WSMessage(CMD.GRAB_QUEUE_UNSUB.toString()));
});
}
}
@@ -0,0 +1,9 @@
export class GrabQueueState {
constructor(
public type: string,
public link: string,
public threadId: string,
public postId: string,
public removed: boolean
) {}
}
@@ -0,0 +1,98 @@
import { ElectronService } from 'ngx-electron';
import { MultiPostComponent } from './../multi-post/multi-post.component';
import { OnInit, OnDestroy, Component, ChangeDetectionStrategy } from '@angular/core';
import { AgRendererComponent } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
import { GrabQueueState } from './grab-queue.model';
import { MatDialog, MatSnackBar } from '@angular/material';
import { BreakpointObserver, BreakpointState, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { ServerService } from '../server-service';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-url-cell-grab',
template: `
<div fxLayout="row" fxLayoutAlign="space-between center">
<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"
><a (click)="goTo()" href="javascript:void(0)">{{ grabQueue.link }}</a></span
>
<span>
<button style="margin-right: 5px" (click)="grab()" color="primary" mat-mini-fab>
<mat-icon>get_app</mat-icon>
</button>
<button (click)="remove()" color="primary" mat-mini-fab><mat-icon>delete</mat-icon></button>
</span>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UrlGrabRendererComponent implements OnInit, OnDestroy, AgRendererComponent {
constructor(
public dialog: MatDialog,
private breakpointObserver: BreakpointObserver,
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar,
private electronService: ElectronService
) {}
grabQueue: GrabQueueState;
params: ICellRendererParams;
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
ngOnInit(): void {}
ngOnDestroy(): void {}
agInit(params: ICellRendererParams): void {
this.grabQueue = params.data;
}
refresh(params: ICellRendererParams): boolean {
return false;
}
grab() {
const dialogRef = this.dialog.open(MultiPostComponent, {
width: '90%',
height: '90%',
maxWidth: '100vw',
maxHeight: '100vh',
data: this.grabQueue
});
const smallDialogSubscription = this.isExtraSmall.subscribe(result => {
if (result.matches) {
dialogRef.updateSize('100%', '100%');
} else {
dialogRef.updateSize('90%', '90%');
}
});
dialogRef.afterClosed().subscribe(() => {
if (smallDialogSubscription != null) {
smallDialogSubscription.unsubscribe();
}
});
}
goTo() {
if (this.electronService.isElectronApp) {
this.electronService.shell.openExternal(this.grabQueue.link);
} else {
window.open(this.grabQueue.link, '_blank');
}
}
remove() {
this.httpClient.post(this.serverService.baseUrl + '/grab/remove', { url: this.grabQueue.link }).subscribe(
() => {},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
}
+8 -10
View File
@@ -1,10 +1,8 @@
<div *ngIf="loading" class="overlay loading" fxLayout="row" fxLayoutAlign="center center">
<mat-progress-spinner [mode]="spinnerMode" [value]="spinnerValue" style="position: absolute;"></mat-progress-spinner>
</div>
<div fxLayout="column" fxLayoutGap="10px" style="height: 100%">
<div fxFlex="0 0 calc(100% - 90px)">
<app-posts
style="width: 100%; height: 100%;"
></app-posts>
</div>
</div>
<mat-tab-group dynamicHeight style="height: 100%; margin-bottom: 20px; overflow: hidden;">
<mat-tab label="Downloads">
<app-posts style="width: 100%; height: 100%;"></app-posts>
</mat-tab>
<mat-tab label="Link Collector">
<app-grab-queue style="width: 100%; height: 100%;"></app-grab-queue>
</mat-tab>
</mat-tab-group>
@@ -0,0 +1,49 @@
table {
width: 100%;
}
:host ::ng-deep .mat-tab-body-wrapper {
height: 100%;
}
.app-custom-plain-container-with-desc {
align-items: center;
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
figure {
margin: 0;
position: relative;
img {
max-width: 100%;
height: auto;
cursor: pointer;
}
figcaption {
background: rgba(0, 0, 0, .5);
color: #fff;
font-size: 85%;
padding: 5px;
position: absolute;
bottom: 3px;
left: 0;
right: 0;
}
}
.description {
font-weight: 400;
text-align: center;
}
.app-custom-image-with-desc {
height: auto;
margin-right: 2px;
width: 200px;
align-self: start;
}
}
@@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomeComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

Some files were not shown because too many files have changed in this diff Show More