mirror of
https://github.com/dev-claw/vripper-project.git
synced 2026-08-19 08:35:41 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67c605e640 | ||
|
|
8d4b878656 | ||
|
|
bf532dd1d0 | ||
|
|
eb1a125e59 | ||
|
|
256659a72d | ||
|
|
dbf3aa7e7d | ||
|
|
a75ce181ff | ||
|
|
37ae6b4ebf | ||
|
|
6631a6a95b | ||
|
|
918f8578ad | ||
|
|
10fa9f68ae | ||
|
|
6b5c9ccb0f | ||
|
|
652e8f1a9d | ||
|
|
fe53ae733d | ||
|
|
690dab2ae8 | ||
|
|
530542163e | ||
|
|
cf7f4c75c0 | ||
|
|
c6b395cf4c | ||
|
|
e72edeed62 | ||
|
|
3f22d53f89 | ||
|
|
08c0fa62b1 | ||
|
|
4c79cca079 | ||
|
|
67096242a2 | ||
|
|
f66112375d | ||
|
|
c5a082e248 | ||
|
|
c2a1b0aa33 | ||
|
|
a89dcc96d8 | ||
|
|
b2a10eafd9 | ||
|
|
f027aff89d | ||
|
|
b4eaf0d1c4 | ||
|
|
9d2975faaa | ||
|
|
0c0fdf9d78 | ||
|
|
e17afa0ef3 |
+70
-1
@@ -1,5 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
## [2.12.1] - 2020-05-22
|
||||
### Changed
|
||||
- Fix alternative name bug
|
||||
|
||||
## [2.12.0] - 2020-05-21
|
||||
### Changed
|
||||
- Use new user hash from the api
|
||||
|
||||
## [2.11.7] - 2020-05-08
|
||||
### Changed
|
||||
- Fix bug with paths creation
|
||||
- Move Open Link button
|
||||
- Rename rename all title
|
||||
|
||||
## [2.11.6] - 2020-05-04
|
||||
### Changed
|
||||
- PNG filenames are now correctly named
|
||||
- Fix issue with special characters in post titles
|
||||
|
||||
## [2.11.5] - 2020-05-03
|
||||
### Changed
|
||||
- Reduce rows buffer size
|
||||
|
||||
## [2.11.4] - 2020-05-03
|
||||
### Changed
|
||||
- Code cleanup
|
||||
- Dependencies update
|
||||
|
||||
## [2.11.3] - 2020-04-30
|
||||
### Changed
|
||||
- Source code restructure
|
||||
- UI fixes and enhancements
|
||||
### Added
|
||||
- Add menu entry to rename a gallery to first alternative title
|
||||
- Add menu on toolbar for massive rename to first alternative title
|
||||
|
||||
## [2.11.2] - 2020-04-26
|
||||
### Changed
|
||||
- Bug fixes
|
||||
|
||||
## [2.11.1] - 2020-04-25
|
||||
### Changed
|
||||
- Code source maintenance
|
||||
### Added
|
||||
- Added option to rename gallery
|
||||
|
||||
## [2.11.0] - 2020-03-29
|
||||
### Changed
|
||||
- Upgrade Electron to v8
|
||||
- Upgrade Angular to v9
|
||||
- App settings are now portable in a json file
|
||||
- General code source maintenance
|
||||
### Added
|
||||
- Added Support for pixroute.com
|
||||
- Added grab post title from thread option
|
||||
|
||||
## [2.10.8] - 2020-01-25
|
||||
### Changed
|
||||
- Fix app url
|
||||
|
||||
## [2.10.7] - 2020-01-25
|
||||
### Changed
|
||||
- Drop SockJs and switch to WebSocket
|
||||
- Autoreconnect front to back
|
||||
|
||||
## [2.10.6] - 2020-01-19
|
||||
### Changed
|
||||
- Limit cache size
|
||||
|
||||
## [2.10.5] - 2020-01-18
|
||||
### Changed
|
||||
- Bug fixes
|
||||
@@ -304,4 +373,4 @@
|
||||
- Add real time communication for progress updates via websocket (SockJS)
|
||||
- Add settings for download path, max concurrent download, enable/disable integration with vipergirls
|
||||
- Add integration with vipergitls.to (possibility to auto leave thanks after grab post)
|
||||
- Add support for acidimg.cc, imagezilla.net, imgbox.com, imx.to, pixhost.to, turboimagehost.com
|
||||
- Add support for acidimg.cc, imagezilla.net, imgbox.com, imx.to, pixhost.to, turboimagehost.com
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
# Viper Ripper!
|
||||
|
||||
This is my spin for a gallery ripper app for [vipergirls](https://vipergirls.to) website.
|
||||
This is my spin for a cross platform gallery ripper app for [vipergirls](https://vipergirls.to) website.
|
||||
The purpose of this project is to build a robust and clean application to conveniently download photo galleries using modern web technologies, Java + Spring boot for the back end and angular + electron for the front end.
|
||||
|
||||
## How to build
|
||||
You need a recent version of maven 3.6.1+.
|
||||
|
||||
The application support 2 build modes:
|
||||
- Desktop app (*Partially cross platform): Self contained app, built with electron, depends on Java runtimes.
|
||||
- Server app (Fully cross platform): Depends on Java runtimes, you need a web browser to access the app UI.
|
||||
- Desktop app: Self contained app, built with electron includes Java runtimes.
|
||||
- Server app: Depends on Java runtimes, you need a web browser to access the app UI.
|
||||
|
||||
Most people will be interested only on the desktop app, however if you have a nas or a server, the server app is there for you.
|
||||
|
||||
To build the server app:
|
||||
|
||||
mvn clean install
|
||||
mvn clean install -DskipTests
|
||||
To build the desktop app:
|
||||
|
||||
mvn clean install -Pelectron
|
||||
mvn clean install -Pelectron -DskipTests
|
||||
|
||||
Maven will automatically handle front end compilation. However, for development, you will need to install a recent version of nodejs on your system.
|
||||
|
||||
*Partially cross platform:
|
||||
Technically cross platform, but the Desktop app relies on electron builder to make appropriate binaries for each platform. mac binaries can only be generated on a mac, and i don't own one. So i can only provide binaries for windows and linux.
|
||||
The Server app however depends only on Java, so it will work just fine on a mac, you only need a browser to access it.
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SC7M5LNWZ528Q)
|
||||
@@ -4,7 +4,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.10.5</version>
|
||||
<version>2.12.1</version>
|
||||
<packaging>pom</packaging>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
+119
-118
@@ -1,3 +1,4 @@
|
||||
require('v8-compile-cache');
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const path = require("path");
|
||||
const url = require("url");
|
||||
@@ -18,139 +19,139 @@ let terminationInteval;
|
||||
let terminated = false;
|
||||
|
||||
process.on("uncaughtException", err => {
|
||||
dialog.showErrorBox(err.message, err.stack);
|
||||
process.exit(1);
|
||||
dialog.showErrorBox(err.message, err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function createWindow() {
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
app.setAppUserModelId("tn.mnlr.vripper");
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
app.setAppUserModelId("tn.mnlr.vripper");
|
||||
}
|
||||
|
||||
let icon;
|
||||
if(process.platform === "win32") {
|
||||
icon = __dirname + '/icon.ico';
|
||||
} else {
|
||||
icon = __dirname + '/icon.png';
|
||||
}
|
||||
win = new BrowserWindow({
|
||||
width: 1024,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
},
|
||||
icon: icon
|
||||
});
|
||||
let icon;
|
||||
if(process.platform === "win32") {
|
||||
icon = __dirname + '/icon.ico';
|
||||
} else {
|
||||
icon = __dirname + '/icon.png';
|
||||
}
|
||||
win = new BrowserWindow({
|
||||
width: 1024,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
webPreferences: {
|
||||
nodeIntegration: true
|
||||
},
|
||||
icon: icon
|
||||
});
|
||||
|
||||
win.removeMenu();
|
||||
win.setMenu(null);
|
||||
win.setMenuBarVisibility(false);
|
||||
win.removeMenu();
|
||||
win.setMenu(null);
|
||||
win.setMenuBarVisibility(false);
|
||||
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, `vripper-ui/index.html`),
|
||||
protocol: "file:",
|
||||
slashes: true
|
||||
})
|
||||
);
|
||||
win.loadURL(
|
||||
url.format({
|
||||
pathname: path.join(__dirname, `vripper-ui/index.html`),
|
||||
protocol: "file:",
|
||||
slashes: true
|
||||
})
|
||||
);
|
||||
|
||||
// win.webContents.openDevTools();
|
||||
// win.webContents.openDevTools();
|
||||
|
||||
win.on("closed", () => {
|
||||
win = null;
|
||||
});
|
||||
win.on("closed", () => {
|
||||
win = null;
|
||||
});
|
||||
}
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
app.quit();
|
||||
} else {
|
||||
getPort().then(port => {
|
||||
serverPort = port;
|
||||
ipcMain.on("get-port", event => {
|
||||
event.reply("port", port);
|
||||
});
|
||||
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",
|
||||
"-Dvripper.server.port=" + port,
|
||||
"-jar",
|
||||
jarPath
|
||||
], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
vripperServer.on('exit', (code, signal) => {
|
||||
console.log(`vripper server terminated, code = ${code}, signal = ${signal}`);
|
||||
terminated = true;
|
||||
app.quit();
|
||||
});
|
||||
});
|
||||
|
||||
app.on("second-instance", (event, commandLine, workingDirectory) => {
|
||||
if (win) {
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.focus();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("ready", createWindow);
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
axios.post('http://localhost:' + serverPort + '/actuator/shutdown', {}, {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}).then((response) => {
|
||||
terminationInteval= setInterval(() => {
|
||||
terminationAttemps++;
|
||||
if(terminated) {
|
||||
console.log('viper server terminated');
|
||||
clearInterval(terminationInteval);
|
||||
getPort().then(port => {
|
||||
serverPort = port;
|
||||
ipcMain.on("get-port", event => {
|
||||
event.reply("port", port);
|
||||
});
|
||||
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",
|
||||
"-Dvripper.server.port=" + port,
|
||||
"-jar",
|
||||
jarPath
|
||||
], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
vripperServer.on('exit', (code, signal) => {
|
||||
console.log(`vripper server terminated, code = ${code}, signal = ${signal}`);
|
||||
terminated = true;
|
||||
app.quit();
|
||||
} else if(terminationAttemps > maxTerminationAttemps) {
|
||||
console.log('viper server is not terminated');
|
||||
console.log('Proceed to kill');
|
||||
vripperServer.kill('SIGKILL');
|
||||
clearInterval(terminationInteval);
|
||||
app.quit();
|
||||
}
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
// Terminate immediately
|
||||
console.log(error);
|
||||
vripperServer.kill('SIGKILL');
|
||||
terminated = true;
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
app.on("second-instance", (event, commandLine, workingDirectory) => {
|
||||
if (win) {
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.focus();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("ready", createWindow);
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
axios.post('http://localhost:' + serverPort + '/actuator/shutdown', {}, {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
}).then((response) => {
|
||||
terminationInteval= setInterval(() => {
|
||||
terminationAttemps++;
|
||||
if(terminated) {
|
||||
console.log('viper server terminated');
|
||||
clearInterval(terminationInteval);
|
||||
app.quit();
|
||||
} else if(terminationAttemps > maxTerminationAttemps) {
|
||||
console.log('viper server is not terminated');
|
||||
console.log('Proceed to kill');
|
||||
vripperServer.kill('SIGKILL');
|
||||
clearInterval(terminationInteval);
|
||||
app.quit();
|
||||
}
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error) => {
|
||||
// Terminate immediately
|
||||
console.log(error);
|
||||
vripperServer.kill('SIGKILL');
|
||||
terminated = true;
|
||||
app.quit();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Generated
+652
-794
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vripper-electron",
|
||||
"version": "2.10.5",
|
||||
"version": "2.12.1",
|
||||
"description": "A ripper for vipergirls.to built using web technolgies",
|
||||
"main": "main.js",
|
||||
"author": "death-claw <53543762+death-claw@users.noreply.github.com>",
|
||||
@@ -70,8 +70,8 @@
|
||||
"dist": "node pre-build.js && electron-builder"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^7.1.9",
|
||||
"electron-builder": "^22.2.0"
|
||||
"electron": "8.2.4",
|
||||
"electron-builder": "22.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.0",
|
||||
@@ -79,6 +79,7 @@
|
||||
"copy-dir": "^1.1.0",
|
||||
"electron-context-menu": "^0.13.0",
|
||||
"get-port": "^5.0.0",
|
||||
"rimraf": "^2.6.3"
|
||||
"rimraf": "^2.6.3",
|
||||
"v8-compile-cache": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.10.5</version>
|
||||
<version>2.12.1</version>
|
||||
</parent>
|
||||
<artifactId>vripper-electron</artifactId>
|
||||
<name>vripper-electron</name>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.10.5</version>
|
||||
<version>2.12.1</version>
|
||||
</parent>
|
||||
<artifactId>vripper-server</artifactId>
|
||||
<name>vripper-server</name>
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
package tn.mnlr.vripper;
|
||||
|
||||
import net.jodah.failsafe.RetryPolicy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
@SpringBootApplication
|
||||
public class VripperApplication {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VripperApplication.class);
|
||||
public static final ExecutorService commonExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
public static final RetryPolicy<Object> retryPolicy = new RetryPolicy<>()
|
||||
.handleIf(e -> !(e instanceof InterruptedException))
|
||||
.withDelay(1, 3, ChronoUnit.SECONDS)
|
||||
.withMaxAttempts(5)
|
||||
.abortOn(InterruptedException.class)
|
||||
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
|
||||
@@ -3,10 +3,11 @@ package tn.mnlr.vripper.entities;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.processors.BehaviorProcessor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tn.mnlr.vripper.SpringContext;
|
||||
import tn.mnlr.vripper.exception.PostParseException;
|
||||
import tn.mnlr.vripper.host.Host;
|
||||
import tn.mnlr.vripper.services.AppStateService;
|
||||
|
||||
@@ -14,48 +15,41 @@ import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
public class Image {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(Image.class);
|
||||
|
||||
private AppStateService appStateService;
|
||||
|
||||
private Disposable subscription;
|
||||
private final AppStateService appStateService;
|
||||
|
||||
private final String type = "img";
|
||||
|
||||
private String postId;
|
||||
|
||||
private String postName;
|
||||
|
||||
private Host host;
|
||||
|
||||
private String url;
|
||||
|
||||
private int index;
|
||||
|
||||
private AtomicLong current = new AtomicLong(0);
|
||||
private Status status;
|
||||
private BehaviorProcessor<Image> imageStateProcessor;
|
||||
|
||||
@Setter
|
||||
private long total = 0;
|
||||
private Status status;
|
||||
private BehaviorProcessor<Image> imageStateProcessor;
|
||||
private Disposable subscription;
|
||||
|
||||
public Image(String url, String postId, String postName, Host host, int index) {
|
||||
private Image() {
|
||||
this.appStateService = SpringContext.getBean(AppStateService.class);
|
||||
}
|
||||
|
||||
public Image(String url, String postId, String postName, Host host, int index) throws PostParseException {
|
||||
this();
|
||||
this.url = url;
|
||||
this.postId = postId;
|
||||
this.postName = postName;
|
||||
this.host = host;
|
||||
this.index = index;
|
||||
status = Status.STOPPED;
|
||||
imageStateProcessor = BehaviorProcessor.create();
|
||||
}
|
||||
|
||||
public void setAppStateService(AppStateService appStateService) {
|
||||
this.appStateService = appStateService;
|
||||
appStateService.getCurrentImages().put(this.url, this);
|
||||
appStateService.getLiveImageUpdates().onNext(this);
|
||||
if (!appStateService.newImage(this)) {
|
||||
throw new PostParseException("Image already loaded");
|
||||
}
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
@@ -68,15 +62,11 @@ public class Image {
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (appStateService == null) {
|
||||
logger.warn("Attempting to init the image whilst the App State is null, unexpected behaviour could occur from this");
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
imageStateProcessor = BehaviorProcessor.create();
|
||||
subscription = imageStateProcessor
|
||||
.onBackpressureLatest()
|
||||
.doOnNext(appStateService::onImageUpdate)
|
||||
.onBackpressureBuffer()
|
||||
.doOnNext(appStateService::imageUpdated)
|
||||
.subscribe();
|
||||
|
||||
current.set(0);
|
||||
@@ -104,7 +94,7 @@ public class Image {
|
||||
}
|
||||
|
||||
private void update() {
|
||||
if(imageStateProcessor == null) {
|
||||
if (imageStateProcessor == null) {
|
||||
return;
|
||||
}
|
||||
imageStateProcessor.onNext(this);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package tn.mnlr.vripper.entities;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import tn.mnlr.vripper.SpringContext;
|
||||
import tn.mnlr.vripper.exception.PostParseException;
|
||||
import tn.mnlr.vripper.host.Host;
|
||||
import tn.mnlr.vripper.services.AppStateService;
|
||||
|
||||
@@ -15,10 +17,13 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@NoArgsConstructor
|
||||
public class Post {
|
||||
|
||||
private AppStateService appStateService;
|
||||
public enum METADATA {
|
||||
PREVIEWS, RESOLVED_NAME, POSTED_BY, THANKED
|
||||
}
|
||||
|
||||
private final AppStateService appStateService;
|
||||
|
||||
private Status status;
|
||||
|
||||
@@ -38,7 +43,7 @@ public class Post {
|
||||
|
||||
private Map<String, Object> metadata;
|
||||
|
||||
private AtomicInteger done = new AtomicInteger(0);
|
||||
private final AtomicInteger done = new AtomicInteger(0);
|
||||
|
||||
private int total;
|
||||
|
||||
@@ -48,7 +53,17 @@ public class Post {
|
||||
|
||||
private String forum;
|
||||
|
||||
public Post(String title, String url, List<Image> images, Map<String, Object> metadata, String postId, String threadId, String threadTitle, String forum) {
|
||||
private String securityToken;
|
||||
|
||||
@Setter
|
||||
private String postFolderName;
|
||||
|
||||
private Post() {
|
||||
this.appStateService = SpringContext.getBean(AppStateService.class);
|
||||
}
|
||||
|
||||
public Post(String title, String url, List<Image> images, Map<String, Object> metadata, String postId, String threadId, String threadTitle, String forum, String securityToken) throws PostParseException {
|
||||
this();
|
||||
this.title = title;
|
||||
this.url = url;
|
||||
this.images = images;
|
||||
@@ -57,15 +72,19 @@ public class Post {
|
||||
this.forum = forum;
|
||||
this.threadId = threadId;
|
||||
this.threadTitle = threadTitle;
|
||||
this.securityToken = securityToken;
|
||||
this.hosts = this.images.stream().map(Image::getHost).map(Host::getHost).collect(Collectors.toSet());
|
||||
total = images.size();
|
||||
status = Status.STOPPED;
|
||||
|
||||
if (!this.appStateService.newPost(this)) {
|
||||
throw new PostParseException("Post already loaded");
|
||||
}
|
||||
}
|
||||
|
||||
public void setAppStateService(AppStateService appStateService) {
|
||||
this.appStateService = appStateService;
|
||||
this.appStateService.getCurrentPosts().put(postId, this);
|
||||
this.appStateService.getLivePostsState().onNext(this);
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
public void setRemoved(boolean removed) {
|
||||
@@ -85,7 +104,7 @@ public class Post {
|
||||
|
||||
private void updateNotification() {
|
||||
if (appStateService != null) {
|
||||
appStateService.getLivePostsState().onNext(this);
|
||||
appStateService.postUpdated(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ public class AcidimgHost extends Host {
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
|
||||
public AcidimgHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return host;
|
||||
@@ -88,7 +92,7 @@ public class AcidimgHost extends Host {
|
||||
throw new HostException(e);
|
||||
}
|
||||
|
||||
if(imgNode == null) {
|
||||
if (imgNode == null) {
|
||||
throw new HostException("Cannot find the image node");
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.w3c.dom.Document;
|
||||
import tn.mnlr.vripper.entities.Image;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
import tn.mnlr.vripper.exception.DownloadException;
|
||||
import tn.mnlr.vripper.exception.HostException;
|
||||
import tn.mnlr.vripper.exception.HtmlProcessorException;
|
||||
@@ -62,25 +63,39 @@ abstract public class Host {
|
||||
@Autowired
|
||||
private PathService pathService;
|
||||
|
||||
@Autowired
|
||||
private AppStateExchange appStateExchange;
|
||||
|
||||
@Autowired
|
||||
private VipergirlsAuthService authService;
|
||||
|
||||
protected Host() {
|
||||
|
||||
}
|
||||
|
||||
abstract public String getHost();
|
||||
|
||||
abstract public String getLookup();
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(pathService);
|
||||
}
|
||||
|
||||
public boolean isSupported(String url) {
|
||||
return url.contains(getLookup());
|
||||
}
|
||||
|
||||
public void download(Image image, ImageFileData imageFileData) throws DownloadException, InterruptedException {
|
||||
public void download(final Post post, final Image image, final ImageFileData imageFileData) throws DownloadException, InterruptedException {
|
||||
|
||||
HttpClientContext context = HttpClientContext.create();
|
||||
context.setCookieStore(new BasicCookieStore());
|
||||
try {
|
||||
|
||||
File destinationFolder;
|
||||
synchronized (appStateExchange.getPost(post.getPostId())) {
|
||||
if (post.getPostFolderName() == null) {
|
||||
pathService.createDefaultPostFolder(post);
|
||||
}
|
||||
destinationFolder = pathService.getDownloadDestinationFolder(post);
|
||||
authService.leaveThanks(post);
|
||||
}
|
||||
|
||||
appStateService.postDownloadingUpdate(image.getPostId());
|
||||
|
||||
imageFileData.setPageUrl(image.getUrl());
|
||||
@@ -102,13 +117,7 @@ abstract public class Host {
|
||||
String formatImageFileName = pathService.formatImageFileName(imageFileData.getImageName());
|
||||
logger.debug(String.format("Sanitizing image name from %s to %s", imageFileData.getImageName(), formatImageFileName));
|
||||
imageFileData.setImageName(formatImageFileName);
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(image.getPostId());
|
||||
logger.debug(String.format("Saving to %s", destinationFolder.getPath()));
|
||||
logger.debug(String.format("Creating %s", destinationFolder.getPath()));
|
||||
destinationFolder.mkdirs();
|
||||
if (!destinationFolder.exists()) {
|
||||
logger.debug(String.format("Folder %s is created", destinationFolder.toString()));
|
||||
}
|
||||
|
||||
HttpClient client = cm.getClient().build();
|
||||
|
||||
@@ -136,7 +145,7 @@ abstract public class Host {
|
||||
fos.flush();
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
}
|
||||
File finalName = checkImageTypeAndRename(outputFile, imageFileData.getImageName(), image.getIndex());
|
||||
File finalName = checkImageTypeAndRename(post, outputFile, imageFileData.getImageName(), image.getIndex());
|
||||
imageFileData.setFileName(finalName.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -147,7 +156,7 @@ abstract public class Host {
|
||||
}
|
||||
}
|
||||
|
||||
private File checkImageTypeAndRename(File outputFile, String imageName, int index) throws HostException {
|
||||
private File checkImageTypeAndRename(Post post, File outputFile, String imageName, int index) throws HostException {
|
||||
try (ImageInputStream iis = ImageIO.createImageInputStream(outputFile)) {
|
||||
Iterator<ImageReader> it = ImageIO.getImageReaders(iis);
|
||||
if (!it.hasNext()) {
|
||||
@@ -169,12 +178,26 @@ abstract public class Host {
|
||||
imageName = imageName.substring(0, imageName.length() - toReplace.length()) + ".jpg";
|
||||
}
|
||||
}
|
||||
} else if (reader.getFormatName().toUpperCase().equals("PNG")) {
|
||||
String imageNameLC = imageName.toLowerCase();
|
||||
if (!imageNameLC.endsWith("_png")) {
|
||||
imageName += ".png";
|
||||
} else {
|
||||
String toReplace = null;
|
||||
if (imageNameLC.endsWith("_png")) {
|
||||
toReplace = "_png";
|
||||
}
|
||||
if (toReplace != null) {
|
||||
imageName = imageName.substring(0, imageName.length() - toReplace.length()) + ".png";
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
File downloadDestinationFolder = pathService.getDownloadDestinationFolder(post);
|
||||
File outImage = new File(downloadDestinationFolder, (appSettingsService.getSettings().getForceOrder() ? String.format("%03d_", index) : "") + imageName);
|
||||
if (outImage.exists() && outImage.delete()) {
|
||||
logger.debug(String.format("%s is deleted", outImage.toString()));
|
||||
}
|
||||
@@ -247,6 +270,11 @@ abstract public class Host {
|
||||
return Objects.equals(getHost(), host.getHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getHost());
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class Response {
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ public class ImageBamHost extends Host {
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
|
||||
public ImageBamHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return host;
|
||||
@@ -87,7 +91,7 @@ public class ImageBamHost extends Host {
|
||||
|
||||
imageFileData.setImageUrl(imgUrl);
|
||||
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
throw new HostException("Unexpected error occurred", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,12 @@ 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;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -20,8 +18,10 @@ public class ImageTwistHost extends Host {
|
||||
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
|
||||
private ConnectionManager cm;
|
||||
|
||||
public ImageTwistHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
|
||||
@@ -21,6 +21,10 @@ public class ImageVenueHost extends Host {
|
||||
private static final String CONTINUE_BUTTON_XPATH = "//a[@title='Continue to your image']";
|
||||
private static final String IMG_XPATH = "//img[@id='thepic']";
|
||||
|
||||
public ImageVenueHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return host;
|
||||
|
||||
@@ -3,14 +3,12 @@ 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 ImageZillaHost extends Host {
|
||||
@@ -21,8 +19,9 @@ public class ImageZillaHost extends Host {
|
||||
private static final String lookup = "imagezilla.net/show";
|
||||
private static final String IMG_XPATH = "//img[@id='photo']";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
public ImageZillaHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
@@ -44,7 +43,7 @@ public class ImageZillaHost extends Host {
|
||||
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.debug(String.format("Resolving name for %s", url));
|
||||
if(titleNode != null) {
|
||||
if (titleNode != null) {
|
||||
title = titleNode.getTextContent().trim();
|
||||
} else {
|
||||
title = null;
|
||||
@@ -53,7 +52,7 @@ public class ImageZillaHost extends Host {
|
||||
throw new HostException(e);
|
||||
}
|
||||
|
||||
if(title == null || title.isEmpty()) {
|
||||
if (title == null || title.isEmpty()) {
|
||||
title = getDefaultImageName(url);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ 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 ImgSpiceHost extends Host {
|
||||
@@ -20,8 +18,9 @@ public class ImgSpiceHost extends Host {
|
||||
private static final String host = "imgspice.com";
|
||||
private static final String IMG_XPATH = "//img[@id='imgpreview']";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
public ImgSpiceHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
|
||||
@@ -23,6 +23,10 @@ public class ImgboxHost extends Host {
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
|
||||
public ImgboxHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return host;
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ImxHost extends Host {
|
||||
|
||||
imageFileData.setImageUrl(imgUrl);
|
||||
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
throw new HostException("Unexpected error occurred", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,12 @@ 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 {
|
||||
@@ -20,8 +18,9 @@ public class PimpandhostHost extends Host {
|
||||
private static final String host = "pimpandhost.com";
|
||||
private static final String IMG_XPATH = "//img[contains(@class, 'original')]";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
public PimpandhostHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package tn.mnlr.vripper.host;
|
||||
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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;
|
||||
|
||||
@Service
|
||||
public class PixRouteHost extends Host {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PixRouteHost.class);
|
||||
|
||||
private static final String host = "pixroute.com";
|
||||
private static final String IMG_XPATH = "//img[@id='imgpreview']";
|
||||
|
||||
@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://");
|
||||
Document doc = getResponse(url, context).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);
|
||||
}
|
||||
|
||||
if (imgNode == null) {
|
||||
throw new HostException("Failed to locate image");
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(String.format("Resolving name and image url for %s", url));
|
||||
|
||||
imageFileData.setImageUrl(imgNode.getAttributes().getNamedItem("src").getTextContent().trim());
|
||||
imageFileData.setImageName(imgNode.getAttributes().getNamedItem("alt").getTextContent().trim());
|
||||
} catch (Exception e) {
|
||||
throw new HostException("Unexpected error occurred", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,12 @@ 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 PixhostHost extends Host {
|
||||
@@ -21,8 +19,9 @@ public class PixhostHost extends Host {
|
||||
private static final String lookup = "pixhost.to/show";
|
||||
private static final String IMG_XPATH = "//img[@id='image']";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
public PixhostHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
|
||||
@@ -3,14 +3,12 @@ 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 {
|
||||
@@ -21,8 +19,9 @@ public class PixxxelsHost extends Host {
|
||||
private static final String IMG_XPATH = "//*[@id='download']";
|
||||
private static final String TITLE_XPATH = "//*[contains(@class,'imagename')]";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
public PixxxelsHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
|
||||
@@ -21,6 +21,10 @@ public class PostImgHost extends Host {
|
||||
private static final String TITLE_XPATH = "//span[contains(@class,'imagename')]";
|
||||
private static final String IMG_XPATH = "//a[@id='download']";
|
||||
|
||||
public PostImgHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return host;
|
||||
|
||||
@@ -3,14 +3,12 @@ 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 TurboImageHost extends Host {
|
||||
@@ -21,8 +19,9 @@ public class TurboImageHost extends Host {
|
||||
private static final String TITLE_XPATH = "//div[contains(@class,'titleFullS')]/h1";
|
||||
private static final String IMG_XPATH = "//img[@id='uImage']";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
public TurboImageHost() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
@@ -44,7 +43,7 @@ public class TurboImageHost extends Host {
|
||||
logger.debug(String.format("Looking for xpath expression %s in %s", TITLE_XPATH, url));
|
||||
Node titleNode = xpathService.getAsNode(doc, TITLE_XPATH);
|
||||
logger.debug(String.format("Resolving name for %s", url));
|
||||
if(titleNode != null) {
|
||||
if (titleNode != null) {
|
||||
title = titleNode.getTextContent().trim();
|
||||
} else {
|
||||
title = null;
|
||||
@@ -53,7 +52,7 @@ public class TurboImageHost extends Host {
|
||||
throw new HostException(e);
|
||||
}
|
||||
|
||||
if(title == null || title.isEmpty()) {
|
||||
if (title == null || title.isEmpty()) {
|
||||
title = getDefaultImageName(url);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tn.mnlr.vripper.entities.Image;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
@@ -14,12 +15,14 @@ public class DownloadJob implements Callable<Image> {
|
||||
|
||||
@Getter
|
||||
private final Image image;
|
||||
private final Post post;
|
||||
|
||||
@Getter
|
||||
private final ImageFileData imageFileData = new ImageFileData();
|
||||
|
||||
DownloadJob(Image image) {
|
||||
DownloadJob(Post post, Image image) {
|
||||
this.image = image;
|
||||
this.post = post;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -28,7 +31,7 @@ public class DownloadJob implements Callable<Image> {
|
||||
logger.debug(String.format("Starting downloading %s", image.getUrl()));
|
||||
image.setStatus(Image.Status.DOWNLOADING);
|
||||
image.setCurrent(0);
|
||||
image.getHost().download(image, imageFileData);
|
||||
image.getHost().download(post, image, imageFileData);
|
||||
image.setStatus(Image.Status.COMPLETE);
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -24,24 +24,28 @@ public class DownloadQ {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DownloadQ.class);
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
private final AppStateService appStateService;
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final List<Host> hosts;
|
||||
|
||||
private final ConcurrentHashMap<Host, BlockingDeque<DownloadJob>> downloadQ = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
@Autowired
|
||||
private List<Host> hosts;
|
||||
public DownloadQ(AppStateService appStateService, AppSettingsService appSettingsService, List<Host> hosts) {
|
||||
this.appStateService = appStateService;
|
||||
this.appSettingsService = appSettingsService;
|
||||
this.hosts = hosts;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
hosts.forEach(host -> downloadQ.put(host, new LinkedBlockingDeque<>()));
|
||||
}
|
||||
|
||||
public void put(Image image) throws InterruptedException {
|
||||
public void put(Post post, Image image) throws InterruptedException {
|
||||
logger.debug(String.format("Enqueuing a job for %s", image.getUrl()));
|
||||
image.init();
|
||||
DownloadJob downloadJob = new DownloadJob(image);
|
||||
DownloadJob downloadJob = new DownloadJob(post, image);
|
||||
downloadQ.get(downloadJob.getImage().getHost()).putLast(downloadJob);
|
||||
appStateService.newDownloadJob(downloadJob);
|
||||
}
|
||||
@@ -57,7 +61,7 @@ public class DownloadQ {
|
||||
}
|
||||
for (Host host : hosts) {
|
||||
Iterator<DownloadJob> it = downloadQ.get(host).iterator();
|
||||
for (int i = 0; i < appSettingsService.getMaxThreads(); i++) {
|
||||
for (int i = 0; i < appSettingsService.getSettings().getMaxThreads(); i++) {
|
||||
DownloadJob downloadJob = it.hasNext() ? it.next() : null;
|
||||
if (downloadJob != null) {
|
||||
downloadJobs.add(downloadJob);
|
||||
@@ -69,7 +73,7 @@ public class DownloadQ {
|
||||
|
||||
public void enqueue(Post post) throws InterruptedException {
|
||||
for (Image image : post.getImages()) {
|
||||
put(image);
|
||||
put(post, image);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package tn.mnlr.vripper.q;
|
||||
|
||||
import lombok.NonNull;
|
||||
import net.jodah.failsafe.Failsafe;
|
||||
import net.jodah.failsafe.FailsafeException;
|
||||
import net.jodah.failsafe.RetryPolicy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -11,14 +11,10 @@ import tn.mnlr.vripper.VripperApplication;
|
||||
import tn.mnlr.vripper.entities.Image;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
import tn.mnlr.vripper.host.Host;
|
||||
import tn.mnlr.vripper.services.AppSettingsService;
|
||||
import tn.mnlr.vripper.services.AppStateService;
|
||||
import tn.mnlr.vripper.services.ThumbnailGenerator;
|
||||
import tn.mnlr.vripper.services.*;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
@@ -28,49 +24,45 @@ import java.util.stream.Collectors;
|
||||
public class ExecutionService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExecutionService.class);
|
||||
|
||||
private static final List<Post.Status> FINISHED = Arrays.asList(Post.Status.ERROR, Post.Status.COMPLETE, Post.Status.STOPPED);
|
||||
|
||||
@Autowired
|
||||
private DownloadQ downloadQ;
|
||||
private final DownloadQ downloadQ;
|
||||
private final AppSettingsService settings;
|
||||
private final AppStateService appStateService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
private final ThumbnailGenerator thumbnailGenerator;
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final CommonExecutor commonExecutor;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService settings;
|
||||
private final int MAX_POOL_SIZE = 12;
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
|
||||
@Autowired
|
||||
private ThumbnailGenerator thumbnailGenerator;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
private final ConcurrentHashMap<Host, AtomicInteger> threadCount = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean notPauseQ = true;
|
||||
private ExecutorService executor = Executors.newFixedThreadPool(20);
|
||||
private ExecutorService executor = Executors.newFixedThreadPool(MAX_POOL_SIZE);
|
||||
|
||||
private Thread executionThread;
|
||||
|
||||
BlockingQueue<DownloadJob> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
private RetryPolicy<Object> retryPolicy;
|
||||
private BlockingQueue<DownloadJob> queue = new LinkedBlockingQueue<>();
|
||||
|
||||
private List<DownloadJob> running = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private Map<String, Future<?>> futures = new ConcurrentHashMap<>();
|
||||
private Thread pollThread;
|
||||
|
||||
@Autowired
|
||||
public ExecutionService(DownloadQ downloadQ, AppSettingsService settings, AppStateService appStateService, AppStateExchange appStateExchange, ThumbnailGenerator thumbnailGenerator, AppSettingsService appSettingsService, CommonExecutor commonExecutor) {
|
||||
this.downloadQ = downloadQ;
|
||||
this.settings = settings;
|
||||
this.appStateService = appStateService;
|
||||
this.appStateExchange = appStateExchange;
|
||||
this.thumbnailGenerator = thumbnailGenerator;
|
||||
this.appSettingsService = appSettingsService;
|
||||
this.commonExecutor = commonExecutor;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
|
||||
retryPolicy = new RetryPolicy<>()
|
||||
.handleIf(e -> !(e instanceof InterruptedException))
|
||||
.withBackoff(1, 10, ChronoUnit.SECONDS)
|
||||
.withMaxDuration(Duration.of(10, ChronoUnit.SECONDS))
|
||||
.withMaxAttempts(3)
|
||||
.abortOn(InterruptedException.class)
|
||||
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
|
||||
|
||||
executionThread = new Thread(this::start, "Executor thread");
|
||||
pollThread = new Thread(this::poll, "Polling thread");
|
||||
pollThread.start();
|
||||
@@ -81,15 +73,16 @@ public class ExecutionService {
|
||||
private void destroy() throws InterruptedException {
|
||||
logger.info("Shutting down ExecutionService");
|
||||
executionThread.interrupt();
|
||||
pollThread.interrupt();
|
||||
executor.shutdown();
|
||||
appStateService.getCurrentPosts().keySet().forEach(p -> {
|
||||
appStateExchange.getPosts().keySet().forEach(p -> {
|
||||
logger.debug(String.format("Stopping download jobs for %s", p));
|
||||
this.stopRunning(p);
|
||||
});
|
||||
executor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void stopRunning(String postId) {
|
||||
private void stopRunning(@NonNull String postId) {
|
||||
List<DownloadJob> data = running
|
||||
.stream()
|
||||
.filter(e -> e.getImage().getPostId().equals(postId))
|
||||
@@ -107,23 +100,29 @@ public class ExecutionService {
|
||||
}
|
||||
|
||||
|
||||
public synchronized void stopAll() {
|
||||
appStateService.getCurrentPosts().values().stream().map(Post::getPostId).forEach(this::stop);
|
||||
}
|
||||
|
||||
public synchronized void restartAll() throws InterruptedException {
|
||||
for (Post post : appStateService.getCurrentPosts().values()) {
|
||||
String postId = post.getPostId();
|
||||
restart(postId);
|
||||
public synchronized void stopAll(List<String> posIds) {
|
||||
if (posIds != null) {
|
||||
posIds.forEach(this::stop);
|
||||
} else {
|
||||
appStateExchange.getPosts().values().forEach(p -> this.stop(p.getPostId()));
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void restart(String postId) throws InterruptedException {
|
||||
if (appStateService.getRunningPosts().get(postId) != null && appStateService.getRunningPosts().get(postId).get() > 0) {
|
||||
public synchronized void restartAll(List<String> posIds) {
|
||||
if (posIds != null) {
|
||||
posIds.forEach(this::restart);
|
||||
} else {
|
||||
appStateExchange.getPosts().values().forEach(p -> this.restart(p.getPostId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void restart(@NonNull String postId) {
|
||||
if (appStateService.isRunning(postId)) {
|
||||
logger.warn(String.format("Cannot restart, jobs are currently running for post id %s", postId));
|
||||
return;
|
||||
}
|
||||
List<Image> images = appStateService.getPost(postId)
|
||||
Post post = appStateExchange.getPost(postId);
|
||||
List<Image> images = post
|
||||
.getImages()
|
||||
.stream()
|
||||
.filter(e -> !e.getStatus().equals(Image.Status.COMPLETE))
|
||||
@@ -131,14 +130,20 @@ public class ExecutionService {
|
||||
if (images.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
appStateService.getPost(postId).setStatus(Post.Status.PENDING);
|
||||
|
||||
post.setStatus(Post.Status.PENDING);
|
||||
logger.debug(String.format("Restarting %d jobs for post id %s", images.size(), postId));
|
||||
for (Image image : images) {
|
||||
downloadQ.put(image);
|
||||
try {
|
||||
downloadQ.put(post, image);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Thread was interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void removeScheduled(Image image) {
|
||||
private void removeScheduled(Image image) {
|
||||
image.setStatus(Image.Status.STOPPED);
|
||||
logger.debug(String.format("Removing scheduled job for %s", image.getUrl()));
|
||||
|
||||
@@ -165,19 +170,23 @@ public class ExecutionService {
|
||||
image.cleanup();
|
||||
}
|
||||
|
||||
private synchronized void removeRunning(String postId) {
|
||||
private void removeRunning(String postId) {
|
||||
logger.debug(String.format("Interrupting running jobs for post id %s", postId));
|
||||
stopRunning(postId);
|
||||
}
|
||||
|
||||
public synchronized void stop(String postId) {
|
||||
private void stop(String postId) {
|
||||
try {
|
||||
if (FINISHED.contains(appStateService.getPost(postId).getStatus())) {
|
||||
final Post post = appStateExchange.getPost(postId);
|
||||
if (post == null) {
|
||||
return;
|
||||
}
|
||||
if (FINISHED.contains(post.getStatus())) {
|
||||
return;
|
||||
}
|
||||
notPauseQ = false;
|
||||
appStateService.getPost(postId).setStatus(Post.Status.STOPPED);
|
||||
List<Image> images = appStateService.getPost(postId)
|
||||
post.setStatus(Post.Status.STOPPED);
|
||||
List<Image> images = post
|
||||
.getImages()
|
||||
.stream()
|
||||
.filter(e -> !e.getStatus().equals(Image.Status.COMPLETE))
|
||||
@@ -199,7 +208,7 @@ public class ExecutionService {
|
||||
if (count == null) {
|
||||
threadCount.put(host, new AtomicInteger(0));
|
||||
}
|
||||
canRun = threadCount.get(host).get() < settings.getMaxThreads() && (settings.getMaxTotalThreads() == 0 || threadCount.values().stream().mapToInt(AtomicInteger::get).sum() < settings.getMaxTotalThreads());
|
||||
canRun = threadCount.get(host).get() < settings.getSettings().getMaxThreads() && (settings.getSettings().getMaxTotalThreads() == 0 ? threadCount.values().stream().mapToInt(AtomicInteger::get).sum() < MAX_POOL_SIZE : threadCount.values().stream().mapToInt(AtomicInteger::get).sum() < settings.getSettings().getMaxTotalThreads());
|
||||
if (canRun && notPauseQ) {
|
||||
threadCount.get(host).incrementAndGet();
|
||||
return true;
|
||||
@@ -207,7 +216,7 @@ public class ExecutionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void poll() {
|
||||
private void poll() {
|
||||
while (!Thread.interrupted()) {
|
||||
try {
|
||||
List<DownloadJob> peek = downloadQ.peek();
|
||||
@@ -227,7 +236,7 @@ public class ExecutionService {
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
private void start() {
|
||||
while (!Thread.interrupted()) {
|
||||
try {
|
||||
push(queue.take());
|
||||
@@ -245,7 +254,7 @@ public class ExecutionService {
|
||||
Runnable task = () -> {
|
||||
running.add(take);
|
||||
|
||||
Failsafe.with(retryPolicy)
|
||||
Failsafe.with(VripperApplication.retryPolicy)
|
||||
.onFailure(e -> {
|
||||
if (e.getFailure() instanceof InterruptedException || (e.getFailure() instanceof FailsafeException && e.getFailure().getCause() instanceof InterruptedException)) {
|
||||
logger.debug("Job successfully interrupted");
|
||||
@@ -257,8 +266,8 @@ public class ExecutionService {
|
||||
.onComplete(e -> {
|
||||
appStateService.doneDownloadJob(take.getImage());
|
||||
logger.debug(String.format("Finished downloading %s", take.getImage().getUrl()));
|
||||
if (appSettingsService.isViewPhotos()) {
|
||||
VripperApplication.commonExecutor.submit(
|
||||
if (appSettingsService.getSettings().getViewPhotos()) {
|
||||
commonExecutor.getGeneralExecutor().submit(
|
||||
() -> thumbnailGenerator.getThumbnails()
|
||||
.get(new ThumbnailGenerator.CacheKey(take.getImage().getPostId(), take.getImageFileData().getFileName())));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
@@ -9,16 +12,18 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tn.mnlr.vripper.SpringContext;
|
||||
import tn.mnlr.vripper.exception.ValidationException;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.prefs.BackingStoreException;
|
||||
import java.util.prefs.Preferences;
|
||||
|
||||
import static java.nio.file.StandardOpenOption.*;
|
||||
|
||||
@Service
|
||||
@Getter
|
||||
@@ -26,103 +31,131 @@ import java.util.prefs.Preferences;
|
||||
public class AppSettingsService {
|
||||
|
||||
private final String MAX_TOTAL_THREADS = "MAX_TOTAL_THREADS";
|
||||
private final String baseDir;
|
||||
|
||||
@Value("${base.dir}")
|
||||
private String defaultDownloadPath;
|
||||
private final Path configPath;
|
||||
private final Logger logger = LoggerFactory.getLogger(AppSettingsService.class);
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AppSettingsService.class);
|
||||
private Settings settings = new Settings();
|
||||
|
||||
private Preferences prefs = Preferences.userNodeForPackage(AppSettingsService.class);
|
||||
|
||||
private final String DOWNLOAD_PATH = "DOWNLOAD_PATH";
|
||||
private final String MAX_THREADS = "MAX_THREADS";
|
||||
private int maxTotalThreads;
|
||||
private final String AUTO_START = "AUTO_START";
|
||||
private final String V_LOGIN = "VLOGIN";
|
||||
private final String V_USERNAME = "VUSERNAME";
|
||||
private final String V_PASSWORD = "VPASSWORD";
|
||||
private final String V_THANKS = "VTHANKS";
|
||||
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 final String NOTIFICATION = "NOTIFICATION";
|
||||
public AppSettingsService(@Value("${base.dir}") String baseDir) {
|
||||
this.baseDir = baseDir;
|
||||
this.configPath = Paths.get(baseDir, ".vripper", "config.json");
|
||||
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
restore();
|
||||
}
|
||||
|
||||
private String downloadPath;
|
||||
private int maxThreads;
|
||||
private boolean autoStart;
|
||||
private boolean vLogin;
|
||||
private String vUsername;
|
||||
private String vPassword;
|
||||
private boolean vThanks;
|
||||
private boolean desktopClipboard;
|
||||
private boolean subLocation;
|
||||
private boolean threadSubLocation;
|
||||
private boolean forceOrder;
|
||||
private boolean clearCompleted;
|
||||
private boolean darkTheme;
|
||||
private boolean viewPhotos;
|
||||
private boolean notificationEnabled;
|
||||
public void newSettings(Settings settings) {
|
||||
|
||||
public void setVPassword(String vPassword) {
|
||||
if(vPassword.isEmpty()) {
|
||||
this.vPassword = "";
|
||||
if (settings.getVLogin()) {
|
||||
if (!this.settings.getVPassword().equals(settings.getVPassword())) {
|
||||
settings.setVPassword(settings.getVPassword());
|
||||
}
|
||||
} else {
|
||||
this.vPassword = DigestUtils.md5Hex(vPassword);
|
||||
settings.setVUsername("");
|
||||
settings.setVPassword("");
|
||||
settings.setVThanks(false);
|
||||
}
|
||||
this.settings = settings;
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
public void restore() {
|
||||
try {
|
||||
settings = om.readValue(configPath.toFile(), Settings.class);
|
||||
try {
|
||||
check(this.settings);
|
||||
} catch (ValidationException e) {
|
||||
logger.error(String.format("Your settings are invalid, either remove %s, or fix it", configPath.toString()), e);
|
||||
SpringContext.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed restore user settings", e);
|
||||
settings = new Settings();
|
||||
}
|
||||
|
||||
downloadPath = prefs.get(DOWNLOAD_PATH, defaultDownloadPath);
|
||||
maxThreads = prefs.getInt(MAX_THREADS, 4);
|
||||
maxTotalThreads = prefs.getInt(MAX_TOTAL_THREADS, 0);
|
||||
autoStart = prefs.getBoolean(AUTO_START, true);
|
||||
vLogin = prefs.getBoolean(V_LOGIN, false);
|
||||
vUsername = prefs.get(V_USERNAME, "");
|
||||
vPassword = prefs.get(V_PASSWORD, "");
|
||||
vThanks = prefs.getBoolean(V_THANKS, false);
|
||||
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);
|
||||
notificationEnabled = prefs.getBoolean(NOTIFICATION, false);
|
||||
if (settings.getDownloadPath() == null) {
|
||||
settings.setDownloadPath(baseDir);
|
||||
}
|
||||
|
||||
if (settings.getMaxThreads() == null) {
|
||||
settings.setMaxThreads(4);
|
||||
}
|
||||
|
||||
if (settings.getMaxTotalThreads() == null) {
|
||||
settings.setMaxTotalThreads(0);
|
||||
}
|
||||
|
||||
if (settings.getAutoStart() == null) {
|
||||
settings.setAutoStart(true);
|
||||
}
|
||||
|
||||
if (settings.getVLogin() == null) {
|
||||
settings.setVLogin(false);
|
||||
}
|
||||
|
||||
if (settings.getVUsername() == null) {
|
||||
settings.setVUsername("");
|
||||
}
|
||||
|
||||
if (settings.getVPassword() == null) {
|
||||
settings.setVPassword("");
|
||||
}
|
||||
|
||||
if (settings.getVThanks() == null) {
|
||||
settings.setVThanks(false);
|
||||
}
|
||||
|
||||
if (settings.getDesktopClipboard() == null) {
|
||||
settings.setDesktopClipboard(false);
|
||||
}
|
||||
|
||||
if (settings.getForceOrder() == null) {
|
||||
settings.setForceOrder(false);
|
||||
}
|
||||
|
||||
if (settings.getSubLocation() == null) {
|
||||
settings.setSubLocation(false);
|
||||
}
|
||||
|
||||
if (settings.getThreadSubLocation() == null) {
|
||||
settings.setThreadSubLocation(false);
|
||||
}
|
||||
|
||||
if (settings.getClearCompleted() == null) {
|
||||
settings.setClearCompleted(false);
|
||||
}
|
||||
|
||||
if (settings.getDarkTheme() == null) {
|
||||
settings.setDarkTheme(false);
|
||||
}
|
||||
|
||||
if (settings.getViewPhotos() == null) {
|
||||
settings.setViewPhotos(false);
|
||||
}
|
||||
|
||||
if (settings.getNotification() == null) {
|
||||
settings.setNotification(false);
|
||||
}
|
||||
|
||||
|
||||
save();
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void save() {
|
||||
|
||||
prefs.put(DOWNLOAD_PATH, downloadPath);
|
||||
prefs.putInt(MAX_THREADS, maxThreads);
|
||||
prefs.putInt(MAX_TOTAL_THREADS, maxTotalThreads);
|
||||
prefs.putBoolean(AUTO_START, autoStart);
|
||||
prefs.putBoolean(V_LOGIN, vLogin);
|
||||
prefs.put(V_USERNAME, vUsername);
|
||||
prefs.put(V_PASSWORD, vPassword);
|
||||
prefs.putBoolean(V_THANKS, vThanks);
|
||||
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);
|
||||
prefs.putBoolean(NOTIFICATION, notificationEnabled);
|
||||
|
||||
try {
|
||||
prefs.sync();
|
||||
} catch (BackingStoreException e) {
|
||||
// force disable gallery
|
||||
settings.setViewPhotos(false);
|
||||
|
||||
Files.write(configPath, om.writeValueAsBytes(settings), CREATE, WRITE, TRUNCATE_EXISTING, SYNC);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to store user settings", e);
|
||||
}
|
||||
}
|
||||
@@ -141,8 +174,8 @@ public class AppSettingsService {
|
||||
throw new ValidationException(String.format("%s is not a directory", settings.getDownloadPath()));
|
||||
}
|
||||
|
||||
if (settings.getMaxTotalThreads() < 0) {
|
||||
throw new ValidationException(String.format("Invalid max global concurrent download settings, values must be in greater than %d", 1));
|
||||
if (settings.getMaxTotalThreads() < 0 || settings.getMaxTotalThreads() > 12) {
|
||||
throw new ValidationException(String.format("Invalid max global concurrent download settings, values must be in [%d,%d]", 0, 12));
|
||||
}
|
||||
|
||||
if (settings.getMaxThreads() < 1 || settings.getMaxThreads() > 4) {
|
||||
@@ -151,11 +184,11 @@ public class AppSettingsService {
|
||||
}
|
||||
|
||||
public Theme getTheme() {
|
||||
return new Theme(this.darkTheme);
|
||||
return new Theme(this.settings.getDarkTheme());
|
||||
}
|
||||
|
||||
public void setTheme(Theme theme) {
|
||||
this.darkTheme = theme.darkTheme;
|
||||
this.settings.setDarkTheme(theme.darkTheme);
|
||||
save();
|
||||
}
|
||||
|
||||
@@ -172,55 +205,50 @@ public class AppSettingsService {
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public static class Settings {
|
||||
|
||||
@JsonProperty("downloadPath")
|
||||
private String downloadPath;
|
||||
@JsonProperty("maxThreads")
|
||||
private int maxThreads;
|
||||
private Integer maxThreads;
|
||||
@JsonProperty("maxTotalThreads")
|
||||
private int maxTotalThreads;
|
||||
private Integer maxTotalThreads;
|
||||
@JsonProperty("autoStart")
|
||||
private boolean autoStart;
|
||||
private Boolean autoStart;
|
||||
@JsonProperty("vLogin")
|
||||
private boolean vLogin;
|
||||
private Boolean vLogin;
|
||||
@JsonProperty("vUsername")
|
||||
private String vUsername;
|
||||
@JsonProperty("vPassword")
|
||||
private String vPassword;
|
||||
@JsonProperty("vThanks")
|
||||
private boolean vThanks;
|
||||
private Boolean vThanks;
|
||||
@JsonProperty("desktopClipboard")
|
||||
private boolean desktopClipboard;
|
||||
private Boolean desktopClipboard;
|
||||
@JsonProperty("forceOrder")
|
||||
private boolean forceOrder;
|
||||
private Boolean forceOrder;
|
||||
@JsonProperty("subLocation")
|
||||
private boolean subLocation;
|
||||
private Boolean subLocation;
|
||||
@JsonProperty("threadSubLocation")
|
||||
private boolean threadSubLocation;
|
||||
private Boolean threadSubLocation;
|
||||
@JsonProperty("clearCompleted")
|
||||
private boolean clearCompleted;
|
||||
private Boolean clearCompleted;
|
||||
@JsonProperty("viewPhotos")
|
||||
private boolean viewPhotos;
|
||||
private Boolean viewPhotos;
|
||||
@JsonProperty("notification")
|
||||
private boolean notification;
|
||||
private Boolean notification;
|
||||
@JsonProperty("darkTheme")
|
||||
private Boolean darkTheme;
|
||||
|
||||
public Settings(String downloadPath, int maxThreads, int maxTotalThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder, boolean subLocation, boolean threadSubLocation, boolean clearCompleted, boolean viewPhotos, boolean notification) {
|
||||
this.downloadPath = downloadPath;
|
||||
this.maxThreads = maxThreads;
|
||||
this.maxTotalThreads = maxTotalThreads;
|
||||
this.autoStart = autoStart;
|
||||
this.vLogin = vLogin;
|
||||
this.vUsername = vUsername;
|
||||
this.vPassword = vPassword;
|
||||
this.vThanks = vThanks;
|
||||
this.desktopClipboard = desktopClipboard;
|
||||
this.forceOrder = forceOrder;
|
||||
this.subLocation = subLocation;
|
||||
this.threadSubLocation = threadSubLocation;
|
||||
this.clearCompleted = clearCompleted;
|
||||
this.viewPhotos = viewPhotos;
|
||||
this.notification = notification;
|
||||
public void setVPassword(String vPassword) {
|
||||
if (vPassword == null || vPassword.isEmpty()) {
|
||||
this.vPassword = "";
|
||||
} else {
|
||||
this.vPassword = DigestUtils.md5Hex(vPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tn.mnlr.vripper.entities.Image;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Service
|
||||
public class AppStateExchange {
|
||||
|
||||
private Map<String, Image> currentImages = new ConcurrentHashMap<>();
|
||||
|
||||
private Map<String, Post> currentPosts = new ConcurrentHashMap<>();
|
||||
|
||||
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();
|
||||
|
||||
public Map<String, AtomicInteger> running() {
|
||||
return runningPosts;
|
||||
}
|
||||
|
||||
public Map<String, Post> getPosts() {
|
||||
return this.currentPosts;
|
||||
}
|
||||
|
||||
public Post getPost(String postId) {
|
||||
return currentPosts.get(postId);
|
||||
}
|
||||
|
||||
public Map<String, Image> getImages() {
|
||||
return currentImages;
|
||||
}
|
||||
|
||||
public PublishProcessor<Image> liveImage() {
|
||||
return liveImageUpdates;
|
||||
}
|
||||
|
||||
public PublishProcessor<Post> livePost() {
|
||||
return livePostsState;
|
||||
}
|
||||
|
||||
public Map<String, QueuedVGLink> getQueue() {
|
||||
return grabQueue;
|
||||
}
|
||||
|
||||
public PublishProcessor<QueuedVGLink> liveQueue() {
|
||||
return liveGrabQueue;
|
||||
}
|
||||
|
||||
public synchronized void restore(Map<String, Post> read) {
|
||||
currentPosts.clear();
|
||||
currentPosts.putAll(read);
|
||||
currentPosts.values().forEach(p -> {
|
||||
if (Arrays.asList(Post.Status.DOWNLOADING, Post.Status.PARTIAL, Post.Status.PENDING).contains(p.getStatus())) {
|
||||
p.setStatus(Post.Status.STOPPED);
|
||||
}
|
||||
});
|
||||
currentImages.clear();
|
||||
read.values().stream().flatMap(e -> e.getImages().stream()).forEach(e -> currentImages.put(e.getUrl(), e));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
import lombok.Setter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tn.mnlr.vripper.entities.Image;
|
||||
@@ -13,80 +11,105 @@ import tn.mnlr.vripper.q.DownloadJob;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Getter
|
||||
public class AppStateService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AppStateService.class);
|
||||
|
||||
private Map<String, Image> currentImages = new ConcurrentHashMap<>();
|
||||
|
||||
private Map<String, Post> currentPosts = new ConcurrentHashMap<>();
|
||||
|
||||
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();
|
||||
private final PersistenceService persistenceService;
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
|
||||
@Autowired
|
||||
private PersistenceService persistenceService;
|
||||
public AppStateService(PersistenceService persistenceService, AppSettingsService appSettingsService, AppStateExchange appStateExchange) {
|
||||
this.appStateExchange = appStateExchange;
|
||||
this.persistenceService = persistenceService;
|
||||
this.appSettingsService = appSettingsService;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
public synchronized void newQueueLink(QueuedVGLink queuedVGLink) {
|
||||
appStateExchange.getQueue().put(queuedVGLink.getThreadId(), queuedVGLink);
|
||||
appStateExchange.liveQueue().onNext(queuedVGLink);
|
||||
}
|
||||
|
||||
public void onImageUpdate(Image imageState) {
|
||||
public synchronized void queueLinkUpdated(QueuedVGLink queuedVGLink) {
|
||||
appStateExchange.liveQueue().onNext(queuedVGLink);
|
||||
}
|
||||
|
||||
persistenceService.getProcessor().onNext(currentPosts);
|
||||
public synchronized void removeQueueLink(String threadId) {
|
||||
Optional.ofNullable(appStateExchange.getQueue().remove(threadId)).ifPresent(QueuedVGLink::remove);
|
||||
}
|
||||
|
||||
liveImageUpdates.onNext(imageState);
|
||||
if (imageState.isCompleted()) {
|
||||
public synchronized boolean newPost(@NonNull Post post) {
|
||||
if (appStateExchange.getPosts().containsKey(post.getPostId())) {
|
||||
return false;
|
||||
} else {
|
||||
appStateExchange.getPosts().put(post.getPostId(), post);
|
||||
appStateExchange.livePost().onNext(post);
|
||||
persistenceService.getProcessor().onNext(appStateExchange.getPosts());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Post postState = currentPosts.get(imageState.getPostId());
|
||||
public synchronized void postUpdated(@NonNull Post post) {
|
||||
appStateExchange.livePost().onNext(post);
|
||||
persistenceService.getProcessor().onNext(appStateExchange.getPosts());
|
||||
}
|
||||
|
||||
public boolean newImage(Image image) {
|
||||
if (appStateExchange.getImages().containsKey(image.getUrl())) {
|
||||
return false;
|
||||
} else {
|
||||
appStateExchange.getImages().put(image.getUrl(), image);
|
||||
appStateExchange.liveImage().onNext(image);
|
||||
persistenceService.getProcessor().onNext(appStateExchange.getPosts());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void imageUpdated(Image image) {
|
||||
|
||||
persistenceService.getProcessor().onNext(appStateExchange.getPosts());
|
||||
appStateExchange.liveImage().onNext(image);
|
||||
|
||||
if (image.isCompleted()) {
|
||||
Post postState = appStateExchange.getPosts().get(image.getPostId());
|
||||
postState.increase();
|
||||
}
|
||||
}
|
||||
|
||||
public Post getPost(String postId) {
|
||||
return currentPosts.get(postId);
|
||||
}
|
||||
|
||||
public void newDownloadJob(DownloadJob downloadJob) {
|
||||
public synchronized void newDownloadJob(@NonNull DownloadJob downloadJob) {
|
||||
String postId = downloadJob.getImage().getPostId();
|
||||
checkKeyRunningPosts(postId);
|
||||
runningPosts.get(postId).incrementAndGet();
|
||||
appStateExchange.running().get(postId).incrementAndGet();
|
||||
}
|
||||
|
||||
public synchronized void postDownloadingUpdate(String postId) {
|
||||
Post post = currentPosts.get(postId);
|
||||
public synchronized void postDownloadingUpdate(@NonNull String postId) {
|
||||
Post post = appStateExchange.getPosts().get(postId);
|
||||
if (!post.getStatus().equals(Post.Status.DOWNLOADING) && !post.getStatus().equals(Post.Status.PARTIAL)) {
|
||||
post.setStatus(Post.Status.DOWNLOADING);
|
||||
livePostsState.onNext(post);
|
||||
appStateExchange.livePost().onNext(post);
|
||||
}
|
||||
}
|
||||
|
||||
public void doneDownloadJob(Image image) {
|
||||
public synchronized void doneDownloadJob(@NonNull Image image) {
|
||||
String postId = image.getPostId();
|
||||
int i = runningPosts.get(postId).decrementAndGet();
|
||||
Post post = currentPosts.get(postId);
|
||||
if(image.getStatus().equals(Image.Status.ERROR)) {
|
||||
int i = appStateExchange.running().get(postId).decrementAndGet();
|
||||
Post post = appStateExchange.getPosts().get(postId);
|
||||
if (image.getStatus().equals(Image.Status.ERROR)) {
|
||||
post.setStatus(Post.Status.PARTIAL);
|
||||
}
|
||||
if (i == 0) {
|
||||
appStateExchange.running().remove(postId);
|
||||
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())) {
|
||||
post.setStatus(Post.Status.COMPLETE);
|
||||
if (appSettingsService.isClearCompleted()) {
|
||||
if (appSettingsService.getSettings().getClearCompleted()) {
|
||||
remove(image.getPostId());
|
||||
}
|
||||
}
|
||||
@@ -94,41 +117,52 @@ public class AppStateService {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void checkKeyRunningPosts(String key) {
|
||||
if (!runningPosts.containsKey(key)) {
|
||||
runningPosts.put(key, new AtomicInteger(0));
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void remove(String postId) {
|
||||
currentPosts.get(postId).setRemoved(true);
|
||||
runningPosts.remove(postId);
|
||||
currentPosts.remove(postId);
|
||||
currentImages.entrySet().removeIf(entry -> entry.getValue().getPostId().equals(postId));
|
||||
persistenceService.getProcessor().onNext(currentPosts);
|
||||
public boolean isRunning(String postId) {
|
||||
AtomicInteger runningCount = appStateExchange.running().get(postId);
|
||||
return runningCount != null && runningCount.get() > 0;
|
||||
}
|
||||
|
||||
public synchronized List<String> clearAll() {
|
||||
List<String> toRemove = this.currentPosts
|
||||
return this.appStateExchange.getPosts()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(e -> e.getStatus().equals(Post.Status.COMPLETE) && e.getDone().get() >= e.getTotal())
|
||||
.map(Post::getPostId)
|
||||
.peek(this::remove)
|
||||
.collect(Collectors.toList());
|
||||
toRemove.forEach(this::remove);
|
||||
return toRemove;
|
||||
}
|
||||
|
||||
public synchronized List<String> removeAll() {
|
||||
List<String> toRemove = this.currentPosts
|
||||
.values()
|
||||
.stream()
|
||||
.map(Post::getPostId)
|
||||
.collect(Collectors.toList());
|
||||
toRemove.forEach(this::remove);
|
||||
return toRemove;
|
||||
public synchronized List<String> removeAll(List<String> postIds) {
|
||||
if (postIds != null && !postIds.isEmpty()) {
|
||||
postIds.forEach(this::remove);
|
||||
return postIds;
|
||||
} else {
|
||||
return this.appStateExchange.getPosts()
|
||||
.values()
|
||||
.stream()
|
||||
.map(Post::getPostId)
|
||||
.peek(this::remove)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkKeyRunningPosts(@NonNull String key) {
|
||||
if (!appStateExchange.running().containsKey(key)) {
|
||||
appStateExchange.running().put(key, new AtomicInteger(0));
|
||||
}
|
||||
}
|
||||
|
||||
private void remove(@NonNull String postId) {
|
||||
final Post post = appStateExchange.getPosts().get(postId);
|
||||
if (post == null) {
|
||||
return;
|
||||
}
|
||||
post.setRemoved(true);
|
||||
appStateExchange.running().remove(postId);
|
||||
appStateExchange.getPosts().remove(postId);
|
||||
appStateExchange.getImages().entrySet().removeIf(entry -> entry.getValue().getPostId().equals(postId));
|
||||
persistenceService.getProcessor().onNext(appStateExchange.getPosts());
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class CachedThread {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import lombok.Getter;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class CommonExecutor {
|
||||
@Getter
|
||||
private final ExecutorService generalExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
private final ExecutorService schedulerExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||
|
||||
@Getter
|
||||
private final Scheduler scheduler = Schedulers.from(schedulerExecutor);
|
||||
|
||||
@PreDestroy
|
||||
private void destroy() throws InterruptedException {
|
||||
generalExecutor.shutdown();
|
||||
generalExecutor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
schedulerExecutor.shutdown();
|
||||
schedulerExecutor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,9 @@ import tn.mnlr.vripper.q.ExecutionService;
|
||||
@EnableScheduling
|
||||
public class GlobalStateService {
|
||||
|
||||
@Autowired
|
||||
private DownloadQ downloadQ;
|
||||
|
||||
@Autowired
|
||||
private ExecutionService executionService;
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
private final DownloadQ downloadQ;
|
||||
private final ExecutionService executionService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
|
||||
@Getter
|
||||
private GlobalState currentState;
|
||||
@@ -29,7 +24,11 @@ public class GlobalStateService {
|
||||
@Getter
|
||||
private PublishProcessor<GlobalState> liveGlobalState = PublishProcessor.create();
|
||||
|
||||
public GlobalStateService() {
|
||||
@Autowired
|
||||
public GlobalStateService(DownloadQ downloadQ, ExecutionService executionService, AppStateExchange appStateExchange) {
|
||||
this.downloadQ = downloadQ;
|
||||
this.executionService = executionService;
|
||||
this.appStateExchange = appStateExchange;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 3000)
|
||||
@@ -37,12 +36,12 @@ public class GlobalStateService {
|
||||
GlobalState newGlobalState = new GlobalState(
|
||||
executionService.runningCount(),
|
||||
downloadQ.size(),
|
||||
appStateService.getCurrentImages()
|
||||
appStateExchange.getImages()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(e -> e.getTotal() == 0 || e.getTotal() != e.getCurrent().get())
|
||||
.count(),
|
||||
appStateService.getCurrentImages()
|
||||
appStateExchange.getImages()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(e -> e.getStatus().equals(Image.Status.ERROR))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import lombok.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -7,28 +8,100 @@ import org.springframework.stereotype.Service;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PathService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PathService.class);
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
private final AppSettingsService appSettingsService;
|
||||
|
||||
private final CommonExecutor commonExecutor;
|
||||
|
||||
private final int MAX_DELETE_ATTEMPTS = 180;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
|
||||
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));
|
||||
public PathService(AppSettingsService appSettingsService, CommonExecutor commonExecutor) {
|
||||
this.appSettingsService = appSettingsService;
|
||||
this.commonExecutor = commonExecutor;
|
||||
}
|
||||
|
||||
public final String sanitize(final String folderName) {
|
||||
public final File getDownloadDestinationFolder(Post post) {
|
||||
return _getDownloadDestinationFolder(post.getForum(), post.getThreadTitle(), post.getPostFolderName());
|
||||
}
|
||||
|
||||
private File _getDownloadDestinationFolder(@NonNull String forum, @NonNull String threadTitle, @NonNull String title) {
|
||||
File sourceFolder = appSettingsService.getSettings().getSubLocation() ? new File(appSettingsService.getSettings().getDownloadPath(), sanitize(forum)) : new File(appSettingsService.getSettings().getDownloadPath());
|
||||
sourceFolder = appSettingsService.getSettings().getThreadSubLocation() ? new File(sourceFolder, threadTitle) : sourceFolder;
|
||||
return new File(sourceFolder, title);
|
||||
}
|
||||
|
||||
public synchronized final void createDefaultPostFolder(Post post) {
|
||||
File sourceFolder = _getDownloadDestinationFolder(post.getForum(), post.getThreadTitle(), sanitize(post.getTitle()));
|
||||
File destFolder = makeDirs(sourceFolder);
|
||||
post.setPostFolderName(destFolder.getName());
|
||||
}
|
||||
|
||||
public synchronized final void rename(Post post, String altName) {
|
||||
post.setTitle(altName);
|
||||
if (post.getPostFolderName() == null) {
|
||||
return;
|
||||
}
|
||||
File newDestFolder = makeDirs(_getDownloadDestinationFolder(post.getForum(), post.getThreadTitle(), sanitize(altName)));
|
||||
File currentDesFolder = getDownloadDestinationFolder(post);
|
||||
post.setPostFolderName(newDestFolder.getName());
|
||||
|
||||
List<File> files = Optional.ofNullable(currentDesFolder.listFiles()).stream().flatMap(Arrays::stream).filter(e -> !e.getName().endsWith(".tmp")).collect(Collectors.toList());
|
||||
for (File f : files) {
|
||||
try {
|
||||
Files.move(f.toPath(), Paths.get(newDestFolder.toString(), f.toPath().getFileName().toString()), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
logger.error(String.format("Failed to move files from %s to %s", currentDesFolder.toString(), newDestFolder.toString()), e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
commonExecutor.getGeneralExecutor().submit(() -> {
|
||||
int attemptCount = 0;
|
||||
while (Objects.requireNonNull(currentDesFolder.listFiles()).length != 0 && attemptCount < MAX_DELETE_ATTEMPTS) {
|
||||
try {
|
||||
attemptCount++;
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (!currentDesFolder.delete()) {
|
||||
logger.warn(String.format("Failed to remove %s", currentDesFolder.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private synchronized File makeDirs(@NonNull final File sourceFolder) {
|
||||
int counter = 1;
|
||||
File folder = sourceFolder;
|
||||
|
||||
while (folder.exists()) {
|
||||
folder = new File(sourceFolder.toString() + '.' + counter++);
|
||||
}
|
||||
|
||||
if (!folder.mkdirs()) {
|
||||
throw new RuntimeException(String.format("Failed to create the folder %s", sourceFolder.toString()));
|
||||
}
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
private String sanitize(final String folderName) {
|
||||
String sanitizedFolderName = folderName.replaceAll("\\.|\\\\|/|\\||:|\\?|\\*|\"|<|>|\\p{Cntrl}", "_");
|
||||
logger.debug(String.format("%s sanitized to %s", folderName, sanitizedFolderName));
|
||||
return sanitizedFolderName;
|
||||
|
||||
@@ -25,7 +25,6 @@ import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -36,8 +35,7 @@ public class PersistenceService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(PersistenceService.class);
|
||||
|
||||
@Autowired
|
||||
private AppStateService stateService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
|
||||
@Value("${base.dir}")
|
||||
private String baseDir;
|
||||
@@ -52,14 +50,17 @@ public class PersistenceService {
|
||||
@Getter
|
||||
private PublishProcessor<Map<String, Post>> processor = PublishProcessor.create();
|
||||
|
||||
private PersistenceService() {
|
||||
@Autowired
|
||||
private PersistenceService(AppStateExchange appStateExchange) {
|
||||
this.appStateExchange = appStateExchange;
|
||||
om = new ObjectMapper();
|
||||
om.addMixIn(Image.class, ImagePersistanceMixin.class);
|
||||
om.addMixIn(Post.class, PostPersistanceMixin.class);
|
||||
subscription = processor
|
||||
.onBackpressureLatest()
|
||||
.throttleLatest(1, TimeUnit.SECONDS)
|
||||
.doOnNext(this::persist)
|
||||
.onBackpressureBuffer()
|
||||
.buffer(1, TimeUnit.SECONDS)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.doOnNext(e -> this.persist(e.get(0)))
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ public class PersistenceService {
|
||||
logger.info(String.format("Destroying %s", PersistenceService.class.getSimpleName()));
|
||||
logger.info("Persisting data before destroying");
|
||||
if (EventListenerBean.isInit()) {
|
||||
this.persist(stateService.getCurrentPosts());
|
||||
this.persist(appStateExchange.getPosts());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,22 +141,6 @@ public class PersistenceService {
|
||||
SpringContext.close();
|
||||
}
|
||||
|
||||
Map<String, Post> read = read(jsonContent);
|
||||
|
||||
stateService.getCurrentPosts().clear();
|
||||
stateService.getCurrentPosts().putAll(read);
|
||||
stateService.getCurrentPosts().values().forEach(p -> {
|
||||
if (Arrays.asList(Post.Status.DOWNLOADING, Post.Status.PARTIAL, Post.Status.PENDING).contains(p.getStatus())) {
|
||||
p.setStatus(Post.Status.STOPPED);
|
||||
}
|
||||
});
|
||||
|
||||
stateService.getCurrentImages().clear();
|
||||
read.values().stream().flatMap(e -> e.getImages().stream()).forEach(e -> {
|
||||
e.setAppStateService(stateService);
|
||||
stateService.getCurrentImages().put(e.getUrl(), e);
|
||||
});
|
||||
|
||||
read.values().forEach(e -> e.setAppStateService(stateService));
|
||||
appStateExchange.restore(read(jsonContent));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,8 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
import tn.mnlr.vripper.exception.PostParseException;
|
||||
import tn.mnlr.vripper.host.Host;
|
||||
import tn.mnlr.vripper.q.DownloadQ;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class PostParser {
|
||||
|
||||
@@ -19,46 +15,28 @@ public class PostParser {
|
||||
|
||||
static final String VR_API = "https://vipergirls.to/vr.php";
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
private final AppStateExchange appStateExchange;
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final DownloadQ downloadQ;
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
|
||||
@Autowired
|
||||
private VipergirlsAuthService authService;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
|
||||
@Autowired
|
||||
private DownloadQ downloadQ;
|
||||
|
||||
@Autowired
|
||||
private VipergirlsAuthService vipergirlsAuthService;
|
||||
|
||||
@Autowired
|
||||
private List<Host> supportedHosts;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
|
||||
public PostParser(AppStateExchange appStateExchange, AppSettingsService appSettingsService, DownloadQ downloadQ) {
|
||||
this.appStateExchange = appStateExchange;
|
||||
this.appSettingsService = appSettingsService;
|
||||
this.downloadQ = downloadQ;
|
||||
}
|
||||
|
||||
public synchronized void addPost(String postId, String threadId) throws PostParseException {
|
||||
|
||||
if (appStateService.getCurrentPosts().containsKey(postId)) {
|
||||
if (appStateExchange.getPosts().containsKey(postId)) {
|
||||
logger.warn(String.format("skipping %s, already loaded", postId));
|
||||
return;
|
||||
}
|
||||
|
||||
VRPostParser vrPostParser = new VRPostParser(threadId, postId, cm, vipergirlsAuthService, supportedHosts);
|
||||
VRPostParser vrPostParser = new VRPostParser(threadId, postId);
|
||||
Post post = vrPostParser.parse();
|
||||
|
||||
post.setAppStateService(appStateService);
|
||||
post.getImages().forEach(e -> e.setAppStateService(appStateService));
|
||||
authService.leaveThanks(post.getUrl(), post.getPostId());
|
||||
if (appSettingsService.isAutoStart()) {
|
||||
if (appSettingsService.getSettings().getAutoStart()) {
|
||||
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);
|
||||
@@ -76,7 +54,7 @@ public class PostParser {
|
||||
}
|
||||
}
|
||||
|
||||
public VRThreadParser createVRThreadParser(String threadId) {
|
||||
return new VRThreadParser(threadId, cm, vipergirlsAuthService, supportedHosts);
|
||||
public VRThreadParser createVRThreadParser(QueuedVGLink queuedVGLink) {
|
||||
return new VRThreadParser(queuedVGLink);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,64 @@
|
||||
package tn.mnlr.vripper.services;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import tn.mnlr.vripper.SpringContext;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
@Getter
|
||||
public class QueuedVGLink {
|
||||
|
||||
private final String type = "grabQueue";
|
||||
|
||||
private final AppStateService appStateService;
|
||||
|
||||
private final String link;
|
||||
private final String threadId;
|
||||
private final String postId;
|
||||
@Setter
|
||||
boolean removed;
|
||||
|
||||
private int count = 0;
|
||||
private boolean loading = true;
|
||||
private boolean removed = false;
|
||||
|
||||
|
||||
public QueuedVGLink(String link, String threadId, String postId) {
|
||||
this.appStateService = SpringContext.getBean(AppStateService.class);
|
||||
this.link = link;
|
||||
this.threadId = threadId;
|
||||
this.postId = postId;
|
||||
}
|
||||
|
||||
public void done() {
|
||||
this.loading = false;
|
||||
this.appStateService.queueLinkUpdated(this);
|
||||
}
|
||||
|
||||
public void increment() {
|
||||
this.count++;
|
||||
this.appStateService.queueLinkUpdated(this);
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
this.removed = true;
|
||||
appStateService.queueLinkUpdated(this);
|
||||
}
|
||||
|
||||
@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);
|
||||
return Objects.equals(threadId, that.threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(link);
|
||||
return Objects.hash(threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package tn.mnlr.vripper.services;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.cache.Weigher;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
@@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.imageio.ImageIO;
|
||||
@@ -22,6 +24,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
@@ -35,15 +38,16 @@ public class ThumbnailGenerator {
|
||||
@Value("${base.dir}")
|
||||
private String baseDir;
|
||||
|
||||
@Autowired
|
||||
private PathService pathService;
|
||||
private final PathService pathService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
|
||||
@Getter
|
||||
private File cacheFolder;
|
||||
CacheLoader<CacheKey, byte[]> loader = new CacheLoader<>() {
|
||||
@Override
|
||||
public byte[] load(CacheKey key) throws Exception {
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(key.getPostId());
|
||||
Post post = appStateExchange.getPost(key.getPostId());
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(post);
|
||||
if (!destinationFolder.exists() || !destinationFolder.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
@@ -51,6 +55,12 @@ public class ThumbnailGenerator {
|
||||
}
|
||||
};
|
||||
|
||||
@Autowired
|
||||
public ThumbnailGenerator(PathService pathService, AppStateExchange appStateExchange) {
|
||||
this.pathService = pathService;
|
||||
this.appStateExchange = appStateExchange;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init() throws Exception {
|
||||
cacheFolder = new File(baseDir + File.separator + ".vripper" + File.separator + "cache");
|
||||
@@ -59,7 +69,9 @@ public class ThumbnailGenerator {
|
||||
throw new Exception(String.format("%s could not be created", cacheFolder.toString()));
|
||||
}
|
||||
thumbnails = CacheBuilder.newBuilder()
|
||||
.maximumSize(20000)
|
||||
.expireAfterAccess(Duration.ofMinutes(30))
|
||||
.weigher((Weigher<CacheKey, byte[]>) (k, v) -> v.length)
|
||||
.maximumWeight(104_857_600)
|
||||
.build(loader);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,13 @@ import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import lombok.Getter;
|
||||
import lombok.NonNull;
|
||||
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;
|
||||
|
||||
@@ -22,28 +19,24 @@ public class VGHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VGHandler.class);
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
|
||||
@Autowired
|
||||
private PostParser postParser;
|
||||
private final AppStateService appStateService;
|
||||
private final PostParser postParser;
|
||||
private final CommonExecutor commonExecutor;
|
||||
|
||||
@Getter
|
||||
private LoadingCache<String, List<VRPostState>> cache;
|
||||
private final LoadingCache<QueuedVGLink, List<VRPostState>> cache;
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
@Autowired
|
||||
public VGHandler(AppStateService appStateService, PostParser postParser, CommonExecutor commonExecutor) {
|
||||
this.appStateService = appStateService;
|
||||
this.postParser = postParser;
|
||||
this.commonExecutor = commonExecutor;
|
||||
|
||||
CacheLoader<String, List<VRPostState>> loader = new CacheLoader<>() {
|
||||
CacheLoader<QueuedVGLink, 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;
|
||||
public List<VRPostState> load(@NonNull QueuedVGLink queuedVGLink) throws Exception {
|
||||
VRThreadParser vrThreadParser = postParser.createVRThreadParser(queuedVGLink);
|
||||
return vrThreadParser.parse();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,26 +51,25 @@ public class VGHandler {
|
||||
postParser.addPost(queuedVGLink.getPostId(), queuedVGLink.getThreadId());
|
||||
} else {
|
||||
Callable<Void> cl = () -> {
|
||||
List<VRPostState> vrPostStates = cache.get(queuedVGLink.getThreadId());
|
||||
List<VRPostState> vrPostStates = cache.get(queuedVGLink);
|
||||
queuedVGLink.setCount(vrPostStates.size());
|
||||
queuedVGLink.done();
|
||||
logger.debug(String.format("%d found for %s", vrPostStates.size(), queuedVGLink.getLink()));
|
||||
if (vrPostStates.size() == 1) {
|
||||
queuedVGLink.remove();
|
||||
postParser.addPost(vrPostStates.get(0).getPostId(), vrPostStates.get(0).getThreadId());
|
||||
logger.debug(String.format("threadId %s, postId %s is added automatically for download", queuedVGLink.getThreadId(), queuedVGLink.getPostId()));
|
||||
} else {
|
||||
appStateService.getGrabQueue().put(queuedVGLink.getLink(), queuedVGLink);
|
||||
appStateService.getLiveGrabQueue().onNext(queuedVGLink);
|
||||
appStateService.newQueueLink(queuedVGLink);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
VripperApplication.commonExecutor.submit(cl);
|
||||
commonExecutor.getGeneralExecutor().submit(cl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(String url) {
|
||||
Optional.ofNullable(appStateService.getGrabQueue().remove(url)).ifPresent(e -> {
|
||||
e.setRemoved(true);
|
||||
appStateService.getLiveGrabQueue().onNext(e);
|
||||
});
|
||||
public void remove(String threadId) {
|
||||
appStateService.removeQueueLink(threadId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
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;
|
||||
@@ -10,8 +8,12 @@ import org.apache.http.client.utils.URIBuilder;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
import tn.mnlr.vripper.SpringContext;
|
||||
import tn.mnlr.vripper.VripperApplication;
|
||||
import tn.mnlr.vripper.entities.Image;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
import tn.mnlr.vripper.exception.DownloadException;
|
||||
@@ -19,15 +21,9 @@ 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.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static tn.mnlr.vripper.services.PostParser.VR_API;
|
||||
@@ -38,26 +34,22 @@ class VRPostParser {
|
||||
|
||||
private static SAXParserFactory factory = SAXParserFactory.newInstance();
|
||||
|
||||
private static RetryPolicy<Object> retryPolicy = new RetryPolicy<>()
|
||||
.handleIf(e -> e instanceof IOException)
|
||||
.withDelay(1, 3, ChronoUnit.SECONDS)
|
||||
.withMaxRetries(2)
|
||||
.withMaxDuration(Duration.of(10, ChronoUnit.SECONDS))
|
||||
.abortOn(InterruptedException.class)
|
||||
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
|
||||
private static List<String> dictionary = Arrays.asList("download", "link", "rapidgator", "filefactory", "filefox");
|
||||
|
||||
private final String threadId;
|
||||
private final String postId;
|
||||
private final ConnectionManager cm;
|
||||
private final VipergirlsAuthService vipergirlsAuthService;
|
||||
private final List<Host> supportedHosts;
|
||||
private final HtmlProcessorService htmlProcessorService;
|
||||
private final XpathService xpathService;
|
||||
|
||||
VRPostParser(String threadId, String postId, ConnectionManager cm, VipergirlsAuthService vipergirlsAuthService, List<Host> supportedHosts) {
|
||||
VRPostParser(String threadId, String postId) {
|
||||
this.threadId = threadId;
|
||||
this.postId = postId;
|
||||
this.cm = cm;
|
||||
this.vipergirlsAuthService = vipergirlsAuthService;
|
||||
this.supportedHosts = supportedHosts;
|
||||
this.cm = SpringContext.getBean(ConnectionManager.class);
|
||||
this.vipergirlsAuthService = SpringContext.getBean(VipergirlsAuthService.class);
|
||||
this.htmlProcessorService = SpringContext.getBean(HtmlProcessorService.class);
|
||||
this.xpathService = SpringContext.getBean(XpathService.class);
|
||||
}
|
||||
|
||||
public Post parse() throws PostParseException {
|
||||
@@ -71,10 +63,29 @@ class VRPostParser {
|
||||
} catch (URISyntaxException e) {
|
||||
throw new PostParseException(e);
|
||||
}
|
||||
VRPostHandler handler = new VRPostHandler(threadId, postId, supportedHosts);
|
||||
|
||||
AtomicReference<Throwable> thr = new AtomicReference<>();
|
||||
|
||||
HashMap<String, Object> metadata = new HashMap<>();
|
||||
String postUrl = String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId);
|
||||
getPostExtraMetadata(postId, metadata, postUrl, thr);
|
||||
if (thr.get() != null) {
|
||||
logger.warn(String.format("Failed to get metadata for thread %s, post %s", threadId, postId), thr.get());
|
||||
thr.set(null);
|
||||
}
|
||||
|
||||
VRApiPostHandler handler = new VRApiPostHandler(threadId, postId, postUrl, metadata);
|
||||
logger.debug(String.format("Requesting %s", httpGet));
|
||||
Post post = Failsafe.with(retryPolicy)
|
||||
Optional<Post> post = getPost(httpGet, handler, thr);
|
||||
if (thr.get() != null || post.isEmpty()) {
|
||||
logger.error(String.format("parsing failed for thread %s, post %s", threadId, postId), thr.get());
|
||||
throw new PostParseException(thr.get());
|
||||
}
|
||||
return post.get();
|
||||
}
|
||||
|
||||
private Optional<Post> getPost(HttpGet httpGet, VRApiPostHandler handler, AtomicReference<Throwable> thr) {
|
||||
return Failsafe.with(VripperApplication.retryPolicy)
|
||||
.onFailure(e -> thr.set(e.getFailure()))
|
||||
.get(() -> {
|
||||
HttpClient connection = cm.getClient().build();
|
||||
@@ -83,9 +94,33 @@ class VRPostParser {
|
||||
throw new DownloadException(String.format("Unexpected response code '%d' for %s", response.getStatusLine().getStatusCode(), httpGet));
|
||||
}
|
||||
|
||||
factory.newSAXParser().parse(response.getEntity().getContent(), handler);
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
return handler.getParsedPost();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getPostExtraMetadata(String postId, HashMap<String, Object> metadata, String url, AtomicReference<Throwable> thr) {
|
||||
HttpGet httpGet = cm.buildHttpGet(url);
|
||||
Failsafe.with(VripperApplication.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();
|
||||
Document document = htmlProcessorService.clean(EntityUtils.toString(response.getEntity()));
|
||||
Node postNode = xpathService.getAsNode(document, String.format("//li[@id='post_%s']/div[contains(@class, 'postdetails')]", postId));
|
||||
String postedBy = xpathService.getAsNode(postNode, "./div[contains(@class, 'userinfo')]//a[contains(@class, 'username')]//font").getTextContent().trim();
|
||||
metadata.put(Post.METADATA.POSTED_BY.name(), postedBy);
|
||||
|
||||
Node node = xpathService.getAsNode(document, String.format("//div[@id='post_message_%s']", postId));
|
||||
metadata.put(Post.METADATA.RESOLVED_NAME.name(), findTitleInContent(node));
|
||||
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw new PostParseException(String.format("Failed to parse thread %s, post %s", threadId, postId), e);
|
||||
} finally {
|
||||
@@ -93,37 +128,73 @@ class VRPostParser {
|
||||
}
|
||||
}
|
||||
});
|
||||
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());
|
||||
}
|
||||
|
||||
private List<String> findTitleInContent(Node node) {
|
||||
List<String> altTitle = new ArrayList<>();
|
||||
findTitle(node, altTitle, new AtomicBoolean(true));
|
||||
return altTitle;
|
||||
}
|
||||
|
||||
private void findTitle(Node node, List<String> altTitle, AtomicBoolean keepGoing) {
|
||||
if (!keepGoing.get()) {
|
||||
return;
|
||||
}
|
||||
if (node.getNodeName().equals("a") || node.getNodeName().equals("img")) {
|
||||
keepGoing.set(false);
|
||||
return;
|
||||
}
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
for (int i = 0; i < node.getChildNodes().getLength(); i++) {
|
||||
Node item = node.getChildNodes().item(i);
|
||||
findTitle(item, altTitle, keepGoing);
|
||||
if (!keepGoing.get()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (node.getNodeType() == Node.TEXT_NODE) {
|
||||
String text = node.getTextContent().trim();
|
||||
if (!text.isEmpty() && dictionary.stream().noneMatch(e -> text.toLowerCase().contains(e.toLowerCase()))) {
|
||||
altTitle.add(text);
|
||||
}
|
||||
}
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
class VRPostHandler extends DefaultHandler {
|
||||
class VRApiPostHandler extends DefaultHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VRPostHandler.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(VRApiPostHandler.class);
|
||||
|
||||
private final Collection<Host> supportedHosts;
|
||||
|
||||
private final List<Host> supportedHosts;
|
||||
private final String threadId;
|
||||
private final String postId;
|
||||
private final HashMap<String, Object> metadata;
|
||||
private final String postUrl;
|
||||
private List<Image> images = new ArrayList<>();
|
||||
private List<String> previews = new ArrayList<>();
|
||||
private String threadTitle;
|
||||
private String postTitle;
|
||||
private String forum;
|
||||
private String userHash;
|
||||
private int previewCounter = 0;
|
||||
private int index = 0;
|
||||
private int imageCount;
|
||||
|
||||
@Getter
|
||||
private Post parsedPost;
|
||||
|
||||
VRPostHandler(String threadId, String postId, List<Host> supportedHosts) {
|
||||
VRApiPostHandler(String threadId, String postId, String postUrl, HashMap<String, Object> metadata) {
|
||||
this.threadId = threadId;
|
||||
this.postId = postId;
|
||||
this.supportedHosts = supportedHosts;
|
||||
this.postUrl = postUrl;
|
||||
this.metadata = metadata;
|
||||
this.supportedHosts = SpringContext.getBeansOfType(Host.class).values();
|
||||
}
|
||||
|
||||
|
||||
public Optional<Post> getParsedPost() {
|
||||
return Optional.ofNullable(parsedPost);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -137,6 +208,9 @@ class VRPostHandler extends DefaultHandler {
|
||||
case "forum":
|
||||
forum = attributes.getValue("title").trim();
|
||||
break;
|
||||
case "user":
|
||||
userHash = attributes.getValue("hash").trim();
|
||||
break;
|
||||
case "thread":
|
||||
threadTitle = attributes.getValue("title").trim();
|
||||
break;
|
||||
@@ -155,7 +229,11 @@ class VRPostHandler extends DefaultHandler {
|
||||
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));
|
||||
try {
|
||||
images.add(new Image(mainUrl, postId, postTitle, foundHost, index));
|
||||
} catch (PostParseException e) {
|
||||
logger.error(String.format("Error occurred while parsing postId %s", postId));
|
||||
}
|
||||
} else {
|
||||
logger.warn(String.format("unsupported host for %s, skipping", mainUrl));
|
||||
}
|
||||
@@ -168,18 +246,25 @@ class VRPostHandler extends DefaultHandler {
|
||||
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
|
||||
);
|
||||
metadata.put(Post.METADATA.PREVIEWS.name(), previews);
|
||||
AtomicReference<Throwable> thr = new AtomicReference<>();
|
||||
if (thr.get() != null) {
|
||||
logger.error(String.format("Failed to get extra metadata for %s", postUrl), thr.get());
|
||||
}
|
||||
try {
|
||||
parsedPost = new Post(
|
||||
postTitle,
|
||||
postUrl,
|
||||
images,
|
||||
metadata,
|
||||
postId,
|
||||
threadId,
|
||||
threadTitle,
|
||||
forum,
|
||||
userHash);
|
||||
} catch (PostParseException e) {
|
||||
logger.error(String.format("Error occurred while parsing postId %s", postId));
|
||||
}
|
||||
}
|
||||
index = 0;
|
||||
previewCounter = 0;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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;
|
||||
@@ -13,16 +11,15 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
import tn.mnlr.vripper.SpringContext;
|
||||
import tn.mnlr.vripper.VripperApplication;
|
||||
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.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -34,45 +31,33 @@ public class VRThreadParser {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VRThreadParser.class);
|
||||
|
||||
private static RetryPolicy<Object> retryPolicy = new RetryPolicy<>()
|
||||
.handleIf(e -> e instanceof IOException)
|
||||
.withDelay(1, 3, ChronoUnit.SECONDS)
|
||||
.withMaxRetries(2)
|
||||
.withMaxDuration(Duration.of(10, ChronoUnit.SECONDS))
|
||||
.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 QueuedVGLink queuedVGLink;
|
||||
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;
|
||||
VRThreadParser(QueuedVGLink queuedVGLink) {
|
||||
this.queuedVGLink = queuedVGLink;
|
||||
this.cm = SpringContext.getBean(ConnectionManager.class);
|
||||
this.vipergirlsAuthService = SpringContext.getBean(VipergirlsAuthService.class);
|
||||
}
|
||||
|
||||
public Void parse() throws PostParseException {
|
||||
public List<VRPostState> parse() throws PostParseException {
|
||||
|
||||
logger.debug(String.format("Parsing thread %s", threadId));
|
||||
logger.debug(String.format("Parsing thread %s", queuedVGLink));
|
||||
HttpGet httpGet;
|
||||
try {
|
||||
URIBuilder uriBuilder = new URIBuilder(VR_API);
|
||||
uriBuilder.setParameter("t", threadId);
|
||||
uriBuilder.setParameter("t", queuedVGLink.getThreadId());
|
||||
httpGet = cm.buildHttpGet(uriBuilder.build());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new PostParseException(e);
|
||||
}
|
||||
|
||||
VRThreadHandler handler = new VRThreadHandler(threadId, postPublishProcessor, hosts);
|
||||
VRThreadHandler handler = new VRThreadHandler(queuedVGLink);
|
||||
AtomicReference<Throwable> thr = new AtomicReference<>();
|
||||
logger.debug(String.format("Requesting %s", httpGet));
|
||||
Failsafe.with(retryPolicy)
|
||||
List<VRPostState> posts = Failsafe.with(VripperApplication.retryPolicy)
|
||||
.onFailure(e -> thr.set(e.getFailure()))
|
||||
.get(() -> {
|
||||
HttpClient connection = cm.getClient().build();
|
||||
@@ -83,27 +68,26 @@ public class VRThreadParser {
|
||||
|
||||
try {
|
||||
factory.newSAXParser().parse(new BufferedInputStream(response.getEntity().getContent()), handler);
|
||||
return handler.getPosts();
|
||||
} catch (Exception e) {
|
||||
throw new PostParseException(String.format("Failed to parse thread %s", threadId), e);
|
||||
throw new PostParseException(String.format("Failed to parse thread %s", queuedVGLink), e);
|
||||
} finally {
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
if (thr.get() != null) {
|
||||
logger.error(String.format("parsing failed for thread %s", threadId), thr.get());
|
||||
logger.error(String.format("parsing failed for thread %s", queuedVGLink), thr.get());
|
||||
throw new PostParseException(thr.get());
|
||||
}
|
||||
return null;
|
||||
return posts;
|
||||
}
|
||||
}
|
||||
|
||||
class VRThreadHandler extends DefaultHandler {
|
||||
|
||||
private final String threadId;
|
||||
private final ReplayProcessor<VRPostState> vrPostPublishProcessor;
|
||||
private final List<Host> supportedHosts;
|
||||
private final QueuedVGLink queuedVGLink;
|
||||
private final Collection<Host> supportedHosts;
|
||||
private final Map<Host, AtomicInteger> hostMap = new HashMap<>();
|
||||
private List<String> previews = new ArrayList<>();
|
||||
private String threadTitle;
|
||||
@@ -113,10 +97,12 @@ class VRThreadHandler extends DefaultHandler {
|
||||
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;
|
||||
@Getter
|
||||
private List<VRPostState> posts = new ArrayList<>();
|
||||
|
||||
VRThreadHandler(QueuedVGLink queuedVGLink) {
|
||||
this.queuedVGLink = queuedVGLink;
|
||||
this.supportedHosts = SpringContext.getBeansOfType(Host.class).values();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -152,16 +138,17 @@ class VRThreadHandler extends DefaultHandler {
|
||||
public void endElement(String uri, String localName, String qName) {
|
||||
if ("post".equals(qName.toLowerCase())) {
|
||||
if (imageCount != 0) {
|
||||
vrPostPublishProcessor.onNext(new VRPostState(
|
||||
threadId,
|
||||
posts.add(new VRPostState(
|
||||
queuedVGLink.getThreadId(),
|
||||
postId,
|
||||
postCounter,
|
||||
postTitle,
|
||||
imageCount,
|
||||
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId),
|
||||
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", queuedVGLink, postId, postId),
|
||||
previews,
|
||||
hostMap.entrySet().stream().filter(v -> v.getValue().get() > 0).map(e -> e.getKey().getHost() + " (" + e.getValue().get() + ")").collect(Collectors.joining(", "))
|
||||
));
|
||||
queuedVGLink.increment();
|
||||
}
|
||||
previewCounter = 0;
|
||||
previews = new ArrayList<>();
|
||||
@@ -171,6 +158,6 @@ class VRThreadHandler extends DefaultHandler {
|
||||
|
||||
@Override
|
||||
public void endDocument() {
|
||||
vrPostPublishProcessor.onComplete();
|
||||
queuedVGLink.done();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import lombok.Getter;
|
||||
import org.apache.http.NameValuePair;
|
||||
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;
|
||||
@@ -17,44 +16,40 @@ 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 tn.mnlr.vripper.VripperApplication;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
import tn.mnlr.vripper.exception.VripperException;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class VipergirlsAuthService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(VipergirlsAuthService.class);
|
||||
|
||||
@Autowired
|
||||
private ConnectionManager cm;
|
||||
|
||||
@Autowired
|
||||
private HtmlProcessorService htmlProcessorService;
|
||||
|
||||
@Autowired
|
||||
private XpathService xpathService;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
private final ConnectionManager cm;
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final CommonExecutor commonExecutor;
|
||||
|
||||
@Getter
|
||||
private HttpClientContext context = HttpClientContext.create();
|
||||
private final HttpClientContext context = HttpClientContext.create();
|
||||
|
||||
@Getter
|
||||
private boolean authenticated = false;
|
||||
|
||||
@Getter
|
||||
private String loggedUser;
|
||||
|
||||
@Getter
|
||||
private PublishProcessor<String> loggedInUser = PublishProcessor.create();
|
||||
private final PublishProcessor<String> loggedInUser = PublishProcessor.create();
|
||||
|
||||
@Autowired
|
||||
public VipergirlsAuthService(ConnectionManager cm, AppSettingsService appSettingsService, CommonExecutor commonExecutor) {
|
||||
this.cm = cm;
|
||||
this.appSettingsService = appSettingsService;
|
||||
this.commonExecutor = commonExecutor;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init() {
|
||||
@@ -66,19 +61,12 @@ public class VipergirlsAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
private void destroy() throws InterruptedException {
|
||||
logger.info("Shutting down VipergirlsAuthService");
|
||||
VripperApplication.commonExecutor.shutdown();
|
||||
VripperApplication.commonExecutor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void authenticate() throws VripperException {
|
||||
|
||||
logger.info("Authenticating using ViperGirls credentials");
|
||||
authenticated = false;
|
||||
|
||||
if(!appSettingsService.isVLogin()) {
|
||||
if (!appSettingsService.getSettings().getVLogin()) {
|
||||
logger.debug("Authentication option is disabled");
|
||||
context.getCookieStore().clear();
|
||||
loggedUser = "";
|
||||
@@ -86,8 +74,8 @@ public class VipergirlsAuthService {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = appSettingsService.getVUsername();
|
||||
String password = appSettingsService.getVPassword();
|
||||
String username = appSettingsService.getSettings().getVUsername();
|
||||
String password = appSettingsService.getSettings().getVPassword();
|
||||
|
||||
if (username == null || password == null || username.isEmpty() || password.isEmpty()) {
|
||||
logger.error("Cannot authenticate with ViperGirls credentials, username or password is empty");
|
||||
@@ -100,14 +88,10 @@ public class VipergirlsAuthService {
|
||||
HttpPost postAuth = cm.buildHttpPost("https://vipergirls.to/login.php?do=login");
|
||||
List<NameValuePair> params = new ArrayList<>();
|
||||
params.add(new BasicNameValuePair("vb_login_username", username));
|
||||
params.add(new BasicNameValuePair("vb_login_password", ""));
|
||||
params.add(new BasicNameValuePair("vb_login_password_hint", "Password"));
|
||||
|
||||
params.add(new BasicNameValuePair("cookieuser", "1"));
|
||||
params.add(new BasicNameValuePair("securitytoken", "guest"));
|
||||
params.add(new BasicNameValuePair("do", "login"));
|
||||
params.add(new BasicNameValuePair("vb_login_md5password", password));
|
||||
params.add(new BasicNameValuePair("vb_login_md5password_utf", password));
|
||||
try {
|
||||
postAuth.setEntity(new UrlEncodedFormEntity(params));
|
||||
} catch (Exception e) {
|
||||
@@ -149,71 +133,40 @@ public class VipergirlsAuthService {
|
||||
loggedInUser.onNext(loggedUser);
|
||||
}
|
||||
|
||||
void leaveThanks(String postUrl, String postId) {
|
||||
VripperApplication.commonExecutor.submit(() -> {
|
||||
if (!appSettingsService.isVLogin()) {
|
||||
logger.debug("Authentication with ViperGirls option is disabled");
|
||||
return;
|
||||
}
|
||||
if (!appSettingsService.isVThanks()) {
|
||||
logger.debug("Leave thanks option is disabled");
|
||||
return;
|
||||
}
|
||||
if (!authenticated) {
|
||||
logger.error("You are not authenticated");
|
||||
return;
|
||||
}
|
||||
public void leaveThanks(Post post) {
|
||||
if (!appSettingsService.getSettings().getVLogin()) {
|
||||
logger.debug("Authentication with ViperGirls option is disabled");
|
||||
return;
|
||||
}
|
||||
if (!appSettingsService.getSettings().getVThanks()) {
|
||||
logger.debug("Leave thanks option is disabled");
|
||||
return;
|
||||
}
|
||||
if (!authenticated) {
|
||||
logger.error("You are not authenticated");
|
||||
return;
|
||||
}
|
||||
if (post.getMetadata().get(Post.METADATA.THANKED.name()) != null && (boolean) post.getMetadata().get(Post.METADATA.THANKED.name())) {
|
||||
logger.debug("Already left a thanks");
|
||||
return;
|
||||
}
|
||||
commonExecutor.getGeneralExecutor().submit(() -> {
|
||||
try {
|
||||
postThanks(postId, getSecurityToken(postUrl));
|
||||
postThanks(post);
|
||||
} catch (Exception e) {
|
||||
logger.error(String.format("Failed to leave a thanks for url %s, post id %s", postUrl, postId), e);
|
||||
logger.error(String.format("Failed to leave a thanks for url %s, post id %s", post.getUrl(), post.getPostId()), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getSecurityToken(String url) throws VripperException {
|
||||
|
||||
String securityToken;
|
||||
|
||||
HttpGet httpGet = cm.buildHttpGet(url);
|
||||
httpGet.addHeader("Referer", "https://vipergirls.to/");
|
||||
httpGet.addHeader("Host", "vipergirls.to");
|
||||
|
||||
CloseableHttpClient client = cm.getClient().build();
|
||||
|
||||
try (CloseableHttpResponse response = client.execute(httpGet, context)) {
|
||||
|
||||
String postPage = EntityUtils.toString(response.getEntity());
|
||||
Document document = htmlProcessorService.clean(postPage);
|
||||
|
||||
String thanksUrl = xpathService
|
||||
.getAsNode(document, "//li[contains(@id,'post_')][not(contains(@id,'post_thank'))]//a[@class='post_thanks_button']")
|
||||
.getAttributes()
|
||||
.getNamedItem("href")
|
||||
.getTextContent()
|
||||
.trim();
|
||||
|
||||
securityToken = Arrays.stream(thanksUrl.split("&"))
|
||||
.filter(v -> v.startsWith("securitytoken"))
|
||||
.findAny()
|
||||
.orElse("")
|
||||
.replace("securitytoken=", "");
|
||||
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
} catch (Exception e) {
|
||||
throw new VripperException(e);
|
||||
}
|
||||
return securityToken;
|
||||
}
|
||||
|
||||
private void postThanks(String postId, String securityKey) throws VripperException {
|
||||
private void postThanks(Post post) throws VripperException {
|
||||
|
||||
HttpPost postThanks = cm.buildHttpPost("https://vipergirls.to/post_thanks.php");
|
||||
List<NameValuePair> params = new ArrayList<>();
|
||||
params.add(new BasicNameValuePair("do", "post_thanks_add"));
|
||||
params.add(new BasicNameValuePair("using_ajax", "1"));
|
||||
params.add(new BasicNameValuePair("p", postId));
|
||||
params.add(new BasicNameValuePair("securitytoken", securityKey));
|
||||
params.add(new BasicNameValuePair("p", post.getPostId()));
|
||||
params.add(new BasicNameValuePair("securitytoken", post.getSecurityToken()));
|
||||
try {
|
||||
postThanks.setEntity(new UrlEncodedFormEntity(params));
|
||||
} catch (Exception e) {
|
||||
@@ -226,9 +179,16 @@ public class VipergirlsAuthService {
|
||||
CloseableHttpClient client = cm.getClient().build();
|
||||
|
||||
try (CloseableHttpResponse response = client.execute(postThanks, context)) {
|
||||
if (response.getStatusLine().getStatusCode() / 100 == 2) {
|
||||
post.getMetadata().put(Post.METADATA.THANKED.name(), true);
|
||||
}
|
||||
EntityUtils.consumeQuietly(response.getEntity());
|
||||
} catch (Exception e) {
|
||||
throw new VripperException(e);
|
||||
}
|
||||
|
||||
if (!((Boolean) post.getMetadata().get(Post.METADATA.THANKED.name()))) {
|
||||
throw new VripperException("Failed to leave");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -21,4 +22,12 @@ public class XpathService {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package tn.mnlr.vripper.web;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
|
||||
import tn.mnlr.vripper.web.wsendpoints.WebSocketHandler;
|
||||
|
||||
@Configuration
|
||||
@@ -16,6 +20,24 @@ public class WebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*").withSockJS().setClientLibraryUrl("../../assets/sockjs.min.js");
|
||||
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TaskScheduler taskScheduler() {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
|
||||
scheduler.setPoolSize(2);
|
||||
scheduler.setThreadNamePrefix("scheduled-task-");
|
||||
scheduler.setDaemon(true);
|
||||
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletServerContainerFactoryBean createWebSocketContainer() {
|
||||
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
|
||||
container.setMaxSessionIdleTimeout(0L);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-14
@@ -15,7 +15,9 @@ 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.entities.Post;
|
||||
import tn.mnlr.vripper.services.AppSettingsService;
|
||||
import tn.mnlr.vripper.services.AppStateExchange;
|
||||
import tn.mnlr.vripper.services.PathService;
|
||||
import tn.mnlr.vripper.services.ThumbnailGenerator;
|
||||
|
||||
@@ -38,16 +40,21 @@ import java.util.stream.Collectors;
|
||||
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();
|
||||
|
||||
private final PathService pathService;
|
||||
private final ThumbnailGenerator thumbnailGenerator;
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
public GalleryEndpoint(PathService pathService, ThumbnailGenerator thumbnailGenerator, AppSettingsService appSettingsService, AppStateExchange appStateExchange) {
|
||||
this.pathService = pathService;
|
||||
this.thumbnailGenerator = thumbnailGenerator;
|
||||
this.appSettingsService = appSettingsService;
|
||||
this.appStateExchange = appStateExchange;
|
||||
}
|
||||
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity handleException(Exception e) {
|
||||
@@ -60,22 +67,24 @@ public class GalleryEndpoint {
|
||||
@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()) {
|
||||
if (!appSettingsService.getSettings().getViewPhotos()) {
|
||||
return new ResponseEntity("Gallery option is disabled", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
response.addHeader("Cache-Control", cacheControl);
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(postId);
|
||||
Post post = appStateExchange.getPost(postId);
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(post);
|
||||
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()) {
|
||||
if (!appSettingsService.getSettings().getViewPhotos()) {
|
||||
return new ResponseEntity("Gallery option is disabled", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
response.addHeader("Cache-Control", cacheControl);
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(postId);
|
||||
Post post = appStateExchange.getPost(postId);
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(post);
|
||||
if (destinationFolder.exists() && destinationFolder.isDirectory()) {
|
||||
return ResponseEntity.ok(thumbnailGenerator.getThumbnails().get(new ThumbnailGenerator.CacheKey(postId, imgName)));
|
||||
}
|
||||
@@ -85,10 +94,11 @@ public class GalleryEndpoint {
|
||||
@GetMapping("/gallery/{postId}")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public ResponseEntity<List<GalleryImage>> getGallery(@PathVariable("postId") @NonNull String postId) {
|
||||
if (!appSettingsService.isViewPhotos()) {
|
||||
if (!appSettingsService.getSettings().getViewPhotos()) {
|
||||
return new ResponseEntity("Gallery option is disabled", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(postId);
|
||||
Post post = appStateExchange.getPost(postId);
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(post);
|
||||
if (destinationFolder.exists() && destinationFolder.isDirectory()) {
|
||||
return ResponseEntity.ok(
|
||||
Arrays.stream(Objects.requireNonNull(destinationFolder.listFiles()))
|
||||
|
||||
+100
-183
@@ -1,22 +1,24 @@
|
||||
package tn.mnlr.vripper.web.restendpoints;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tn.mnlr.vripper.VripperApplication;
|
||||
import tn.mnlr.vripper.entities.Post;
|
||||
import tn.mnlr.vripper.entities.Post.METADATA;
|
||||
import tn.mnlr.vripper.exception.PostParseException;
|
||||
import tn.mnlr.vripper.q.ExecutionService;
|
||||
import tn.mnlr.vripper.services.*;
|
||||
import tn.mnlr.vripper.web.restendpoints.domain.posts.*;
|
||||
import tn.mnlr.vripper.web.restendpoints.exceptions.BadRequestException;
|
||||
import tn.mnlr.vripper.web.restendpoints.exceptions.NotFoundException;
|
||||
import tn.mnlr.vripper.web.restendpoints.exceptions.ServerErrorException;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -29,38 +31,30 @@ public class PostRestEndpoint {
|
||||
|
||||
private static final Pattern VG_URL_PATTERN = Pattern.compile("https://vipergirls\\.to/threads/(\\d+)((.*p=)(\\d+))?");
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
private final AppStateService appStateService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
private final PathService pathService;
|
||||
private final VGHandler vgHandler;
|
||||
private final ExecutionService executionService;
|
||||
private final PostParser postParser;
|
||||
private final CommonExecutor commonExecutor;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService appSettingsService;
|
||||
|
||||
@Autowired
|
||||
private PathService pathService;
|
||||
|
||||
@Autowired
|
||||
private VGHandler vgHandler;
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<String> handleException(Exception e) {
|
||||
logger.error("Error when process request", e);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(e.getMessage());
|
||||
public PostRestEndpoint(AppStateService appStateService, AppStateExchange appStateExchange, PathService pathService, VGHandler vgHandler, ExecutionService executionService, PostParser postParser, CommonExecutor commonExecutor) {
|
||||
this.appStateService = appStateService;
|
||||
this.appStateExchange = appStateExchange;
|
||||
this.pathService = pathService;
|
||||
this.vgHandler = vgHandler;
|
||||
this.executionService = executionService;
|
||||
this.postParser = postParser;
|
||||
this.commonExecutor = commonExecutor;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private ExecutionService executionService;
|
||||
|
||||
|
||||
@Autowired
|
||||
private PostParser postParser;
|
||||
|
||||
@PostMapping("/post")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized ResponseEntity processPost(@RequestBody ThreadUrl _url) throws Exception {
|
||||
@ResponseStatus(code = HttpStatus.OK)
|
||||
public void processPost(@RequestBody ThreadUrl _url) {
|
||||
if (_url.getUrl() == null || _url.getUrl().isEmpty()) {
|
||||
return new ResponseEntity("Failed to process empty request", HttpStatus.BAD_REQUEST);
|
||||
throw new BadRequestException("Failed to process empty request");
|
||||
}
|
||||
List<String> urls = Arrays.stream(_url.getUrl().split("\\r?\\n")).map(String::trim).filter(e -> !e.isEmpty()).collect(Collectors.toList());
|
||||
ArrayList<QueuedVGLink> queuedVGLinks = new ArrayList<>();
|
||||
@@ -72,36 +66,33 @@ public class PostRestEndpoint {
|
||||
}
|
||||
|
||||
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);
|
||||
Matcher m = VG_URL_PATTERN.matcher(url);
|
||||
if (m.find()) {
|
||||
threadId = m.group(1);
|
||||
postId = m.group(4);
|
||||
} else {
|
||||
throw new BadRequestException(String.format("Cannot retrieve thread id from URL %s", url));
|
||||
}
|
||||
queuedVGLinks.add(new QueuedVGLink(url, threadId, postId));
|
||||
}
|
||||
vgHandler.handle(queuedVGLinks);
|
||||
return ResponseEntity.ok().build();
|
||||
try {
|
||||
vgHandler.handle(queuedVGLinks);
|
||||
} catch (Exception e) {
|
||||
throw new ServerErrorException(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/post/restart")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized void restartPost(@RequestBody @NonNull List<PostId> postIds) throws Exception {
|
||||
for (PostId postId : postIds) {
|
||||
executionService.restart(postId.getPostId());
|
||||
}
|
||||
public void restartPost(@RequestBody @NonNull List<PostId> postIds) {
|
||||
executionService.restartAll(postIds.stream().map(PostId::getPostId).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@PostMapping("/post/add")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized void addPost(@RequestBody List<PostToAdd> posts) {
|
||||
public void addPost(@RequestBody List<PostToAdd> posts) {
|
||||
for (PostToAdd post : posts) {
|
||||
VripperApplication.commonExecutor.submit(() -> {
|
||||
commonExecutor.getGeneralExecutor().submit(() -> {
|
||||
try {
|
||||
postParser.addPost(post.getPostId(), post.getThreadId());
|
||||
} catch (PostParseException e) {
|
||||
@@ -113,173 +104,99 @@ public class PostRestEndpoint {
|
||||
|
||||
@GetMapping("/post/path/{postId}")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public ResponseEntity<DownloadPath> folderPath(@PathVariable("postId") String postId) {
|
||||
File destinationFolder = pathService.getDownloadDestinationFolder(postId);
|
||||
return ResponseEntity.ok(new DownloadPath(destinationFolder.getPath()));
|
||||
public DownloadPath folderPath(@PathVariable("postId") String postId) {
|
||||
Post post = appStateExchange.getPost(postId);
|
||||
if (post.getPostFolderName() == null) {
|
||||
throw new NotFoundException("Download has not been started yet for this post");
|
||||
} else {
|
||||
return new DownloadPath(pathService.getDownloadDestinationFolder(post).getPath());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/post/restart/all")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized void restartPost() throws Exception {
|
||||
executionService.restartAll();
|
||||
public void restartPost() {
|
||||
executionService.restartAll(null);
|
||||
}
|
||||
|
||||
@PostMapping("/post/stop")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized void stop(@RequestBody @NonNull List<PostId> postIds) {
|
||||
for (PostId postId : postIds) {
|
||||
executionService.stop(postId.getPostId());
|
||||
}
|
||||
public void stop(@RequestBody @NonNull List<PostId> postIds) {
|
||||
executionService.stopAll(postIds.stream().map(PostId::getPostId).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@PostMapping("/post/stop/all")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized void stopAll() {
|
||||
executionService.stopAll();
|
||||
public void stopAll() {
|
||||
executionService.stopAll(null);
|
||||
}
|
||||
|
||||
@PostMapping("/post/remove")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized ResponseEntity<List<RemoveResult>> remove(@RequestBody @NonNull List<PostId> postIds) {
|
||||
public List<RemoveResult> remove(@RequestBody @NonNull List<PostId> postIds) {
|
||||
List<RemoveResult> result = new ArrayList<>();
|
||||
for (PostId postId : postIds) {
|
||||
executionService.stop(postId.getPostId());
|
||||
appStateService.remove(postId.getPostId());
|
||||
result.add(new RemoveResult(postId.getPostId()));
|
||||
List<String> collect = postIds.stream().map(PostId::getPostId).peek(e -> result.add(new RemoveResult(e))).collect(Collectors.toList());
|
||||
executionService.stopAll(collect);
|
||||
appStateService.removeAll(collect);
|
||||
return result;
|
||||
}
|
||||
|
||||
@PostMapping("/post/rename")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public List<AltPostName> rename(@RequestBody @NonNull List<AltPostName> postToRename) {
|
||||
for (AltPostName altPostName : postToRename) {
|
||||
Post post = appStateExchange.getPost(altPostName.getPostId());
|
||||
try {
|
||||
pathService.rename(post, altPostName.getAltName());
|
||||
} catch (Exception e) {
|
||||
throw new ServerErrorException(e.getMessage());
|
||||
}
|
||||
}
|
||||
return ResponseEntity.ok(result);
|
||||
return postToRename;
|
||||
}
|
||||
|
||||
@PostMapping("/post/rename/first")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public List<PostId> renameFirst(@RequestBody @NonNull List<PostId> postToRename) {
|
||||
for (PostId postId : postToRename) {
|
||||
Post post = appStateExchange.getPost(postId.getPostId());
|
||||
if (post.getMetadata().get(METADATA.RESOLVED_NAME.name()) != null) {
|
||||
List<String> resolvedNames = ((List<String>) post.getMetadata().get(METADATA.RESOLVED_NAME.name()));
|
||||
if (!resolvedNames.isEmpty()) {
|
||||
try {
|
||||
pathService.rename(post, resolvedNames.get(0));
|
||||
} catch (Exception e) {
|
||||
throw new ServerErrorException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return postToRename;
|
||||
}
|
||||
|
||||
@PostMapping("/post/clear/all")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized ResponseEntity<RemoveAllResult> clearAll() {
|
||||
return ResponseEntity.ok(new RemoveAllResult(appStateService.clearAll()));
|
||||
public RemoveAllResult clearAll() {
|
||||
return new RemoveAllResult(appStateService.clearAll());
|
||||
}
|
||||
|
||||
@PostMapping("/post/remove/all")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized ResponseEntity<RemoveAllResult> removeAll() {
|
||||
return ResponseEntity.ok(new RemoveAllResult(appStateService.removeAll()));
|
||||
public RemoveAllResult removeAll() {
|
||||
return new RemoveAllResult(appStateService.removeAll(null));
|
||||
}
|
||||
|
||||
@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()));
|
||||
public List<VRPostState> grab(@PathVariable("threadId") @NonNull String threadId) throws Exception {
|
||||
QueuedVGLink queuedVGLink = appStateExchange.getQueue().values().stream().filter(e -> e.getThreadId().equals(threadId)).findFirst().orElseThrow();
|
||||
return vgHandler.getCache().get(queuedVGLink);
|
||||
}
|
||||
|
||||
@PostMapping("/grab/remove")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public synchronized void grabRemove(@RequestBody @NonNull ThreadUrl threadUrl) {
|
||||
vgHandler.remove(threadUrl.getUrl());
|
||||
public ThreadId grabRemove(@RequestBody @NonNull ThreadId threadId) {
|
||||
vgHandler.remove(threadId.getThreadId());
|
||||
return threadId;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
+18
-80
@@ -1,124 +1,62 @@
|
||||
package tn.mnlr.vripper.web.restendpoints;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import tn.mnlr.vripper.exception.ValidationException;
|
||||
import tn.mnlr.vripper.services.AppSettingsService;
|
||||
import tn.mnlr.vripper.services.VipergirlsAuthService;
|
||||
import tn.mnlr.vripper.web.restendpoints.exceptions.BadRequestException;
|
||||
|
||||
@RestController
|
||||
@CrossOrigin(value = "*")
|
||||
public class SettingsRestEndpoint {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SettingsRestEndpoint.class);
|
||||
private final AppSettingsService appSettingsService;
|
||||
private final VipergirlsAuthService vipergirlsAuthService;
|
||||
|
||||
@Autowired
|
||||
private AppSettingsService settings;
|
||||
|
||||
@Autowired
|
||||
private VipergirlsAuthService vipergirlsAuthService;
|
||||
|
||||
@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());
|
||||
public SettingsRestEndpoint(AppSettingsService appSettingsService, VipergirlsAuthService vipergirlsAuthService) {
|
||||
this.appSettingsService = appSettingsService;
|
||||
this.vipergirlsAuthService = vipergirlsAuthService;
|
||||
}
|
||||
|
||||
@PostMapping("/settings/theme")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public AppSettingsService.Theme postTheme(@RequestBody AppSettingsService.Theme theme) {
|
||||
synchronized (this.settings) {
|
||||
this.settings.setTheme(theme);
|
||||
return settings.getTheme();
|
||||
synchronized (this.appSettingsService) {
|
||||
this.appSettingsService.setTheme(theme);
|
||||
return appSettingsService.getTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/settings/theme")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public AppSettingsService.Theme getTheme() {
|
||||
return settings.getTheme();
|
||||
return appSettingsService.getTheme();
|
||||
}
|
||||
|
||||
@PostMapping("/settings")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public ResponseEntity postSettings(@RequestBody AppSettingsService.Settings settings) throws Exception {
|
||||
public AppSettingsService.Settings postSettings(@RequestBody AppSettingsService.Settings settings) throws Exception {
|
||||
|
||||
synchronized (this.settings) {
|
||||
synchronized (this.appSettingsService) {
|
||||
try {
|
||||
this.settings.check(settings);
|
||||
this.appSettingsService.check(settings);
|
||||
} catch (ValidationException e) {
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.BAD_REQUEST)
|
||||
.body(e.getMessage());
|
||||
throw new BadRequestException(e.getMessage());
|
||||
}
|
||||
this.settings.setDownloadPath(settings.getDownloadPath());
|
||||
this.settings.setMaxThreads(settings.getMaxThreads());
|
||||
this.settings.setMaxTotalThreads(settings.getMaxTotalThreads());
|
||||
this.settings.setAutoStart(settings.isAutoStart());
|
||||
this.settings.setVLogin(settings.isVLogin());
|
||||
|
||||
if (settings.isVLogin()) {
|
||||
this.settings.setVUsername(settings.getVUsername());
|
||||
if (!this.settings.getVPassword().equals(settings.getVPassword())) {
|
||||
this.settings.setVPassword(settings.getVPassword());
|
||||
}
|
||||
this.settings.setVThanks(settings.isVThanks());
|
||||
} else {
|
||||
this.settings.setVUsername("");
|
||||
this.settings.setVPassword("");
|
||||
this.settings.setVThanks(false);
|
||||
}
|
||||
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.setNotificationEnabled(settings.isNotification());
|
||||
|
||||
this.settings.save();
|
||||
|
||||
this.appSettingsService.newSettings(settings);
|
||||
vipergirlsAuthService.authenticate();
|
||||
}
|
||||
return ResponseEntity.ok(getSettings());
|
||||
return getAppSettingsService();
|
||||
}
|
||||
|
||||
@GetMapping("/settings")
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public AppSettingsService.Settings getSettings() {
|
||||
public AppSettingsService.Settings getAppSettingsService() {
|
||||
|
||||
return new AppSettingsService.Settings(
|
||||
settings.getDownloadPath(),
|
||||
settings.getMaxThreads(),
|
||||
settings.getMaxTotalThreads(),
|
||||
settings.isAutoStart(),
|
||||
settings.isVLogin(),
|
||||
settings.getVUsername(),
|
||||
settings.getVPassword(),
|
||||
settings.isVThanks(),
|
||||
settings.isDesktopClipboard(),
|
||||
settings.isForceOrder(),
|
||||
settings.isSubLocation(),
|
||||
settings.isThreadSubLocation(),
|
||||
settings.isClearCompleted(),
|
||||
settings.isViewPhotos(),
|
||||
settings.isNotificationEnabled()
|
||||
);
|
||||
}
|
||||
|
||||
@Getter
|
||||
static class Response {
|
||||
String message;
|
||||
|
||||
Response(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
return appSettingsService.getSettings();
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class AltPostName {
|
||||
private String postId;
|
||||
private String altName;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class DownloadPath {
|
||||
private String path;
|
||||
|
||||
public DownloadPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class PostId {
|
||||
private String postId;
|
||||
|
||||
public PostId(String postId) {
|
||||
this.postId = postId;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class PostToAdd {
|
||||
private String threadId;
|
||||
private String postId;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class RemoveAllResult {
|
||||
private List<String> postIds;
|
||||
private int removed;
|
||||
|
||||
public RemoveAllResult(List<String> postIds) {
|
||||
this.removed = postIds.size();
|
||||
this.postIds = postIds;
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class RemoveResult {
|
||||
private String postId;
|
||||
|
||||
public RemoveResult(String postId) {
|
||||
this.postId = postId;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class ThreadId {
|
||||
private String threadId;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.domain.posts;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@ToString
|
||||
public class ThreadUrl {
|
||||
private String url;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.exceptions;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(code = HttpStatus.BAD_REQUEST)
|
||||
public class BadRequestException extends RuntimeException {
|
||||
public BadRequestException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.exceptions;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(code = HttpStatus.NOT_FOUND)
|
||||
public class NotFoundException extends RuntimeException {
|
||||
public NotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package tn.mnlr.vripper.web.restendpoints.exceptions;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public class ServerErrorException extends RuntimeException {
|
||||
public ServerErrorException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
+43
-52
@@ -2,7 +2,6 @@ package tn.mnlr.vripper.web.wsendpoints;
|
||||
|
||||
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;
|
||||
@@ -22,6 +21,7 @@ import tn.mnlr.vripper.services.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -34,36 +34,32 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);
|
||||
|
||||
public WebSocketHandler() {
|
||||
private final GlobalStateService globalStateService;
|
||||
private final AppStateExchange appStateExchange;
|
||||
private final DownloadSpeedService downloadSpeedService;
|
||||
private final VipergirlsAuthService vipergirlsAuthService;
|
||||
private final CommonExecutor commonExecutor;
|
||||
private final Map<String, Disposable> postsSubscriptions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Disposable> postDetailsSubscriptions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Disposable> stateSubscriptions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Disposable> downloadSpeedSubscriptions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Disposable> userSubscriptions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Disposable> grabQueueSubscriptions = new ConcurrentHashMap<>();
|
||||
private final Map<String, Future<Void>> threadParseRequests = new ConcurrentHashMap<>();
|
||||
|
||||
private final ObjectMapper om = new ObjectMapper();
|
||||
|
||||
@Autowired
|
||||
public WebSocketHandler(GlobalStateService globalStateService, AppStateExchange appStateExchange, DownloadSpeedService downloadSpeedService, VipergirlsAuthService vipergirlsAuthService, CommonExecutor commonExecutor) {
|
||||
this.globalStateService = globalStateService;
|
||||
this.appStateExchange = appStateExchange;
|
||||
this.downloadSpeedService = downloadSpeedService;
|
||||
this.vipergirlsAuthService = vipergirlsAuthService;
|
||||
this.commonExecutor = commonExecutor;
|
||||
|
||||
om.addMixIn(Image.class, ImageUIMixin.class).addMixIn(Post.class, PostUIMixin.class);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private GlobalStateService globalStateService;
|
||||
|
||||
@Autowired
|
||||
private AppStateService appStateService;
|
||||
|
||||
@Autowired
|
||||
private DownloadSpeedService downloadSpeedService;
|
||||
|
||||
@Autowired
|
||||
private VipergirlsAuthService vipergirlsAuthService;
|
||||
|
||||
@Autowired
|
||||
private PostParser postParser;
|
||||
|
||||
private Map<String, Disposable> postsSubscriptions = new ConcurrentHashMap<>();
|
||||
private Map<String, Disposable> postDetailsSubscriptions = 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<>();
|
||||
|
||||
private ObjectMapper om = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
|
||||
@@ -130,11 +126,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
stateSubscriptions.put(session.getId(),
|
||||
globalStateService.getLiveGlobalState()
|
||||
.subscribeOn(commonExecutor.getScheduler())
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(5000, TimeUnit.MILLISECONDS)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.map(e -> e.subList(e.size() - 1, e.size()))
|
||||
.map(Collections::singleton)
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
@@ -156,12 +150,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
downloadSpeedSubscriptions.put(session.getId(),
|
||||
downloadSpeedService.getReadBytesPerSecond()
|
||||
.subscribeOn(commonExecutor.getScheduler())
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(5000, TimeUnit.MILLISECONDS)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.map(e -> e.subList(e.size() - 1, e.size()))
|
||||
.map(e -> e.stream().map(DownloadSpeed::new).collect(Collectors.toList()))
|
||||
.map(e -> Collections.singleton(new DownloadSpeed(e)))
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
@@ -183,9 +174,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
userSubscriptions.put(session.getId(),
|
||||
vipergirlsAuthService.getLoggedInUser()
|
||||
.subscribeOn(commonExecutor.getScheduler())
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.map(e -> Collections.singletonList(new LoggedUser(e)))
|
||||
.map(e -> Collections.singleton(new LoggedUser(e)))
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
@@ -200,18 +191,18 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
send(session, new TextMessage(om.writeValueAsString(appStateService.getCurrentPosts().values())));
|
||||
send(session, new TextMessage(om.writeValueAsString(appStateExchange.getPosts().values())));
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error occurred", e);
|
||||
}
|
||||
|
||||
postsSubscriptions.put(session.getId(),
|
||||
appStateService.getLivePostsState()
|
||||
appStateExchange.livePost()
|
||||
.subscribeOn(commonExecutor.getScheduler())
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(2000, TimeUnit.MILLISECONDS, 200)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.map(e -> e.stream().distinct().collect(Collectors.toList()))
|
||||
.map(HashSet::new)
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
@@ -226,18 +217,18 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
send(session, new TextMessage(om.writeValueAsString(appStateService.getGrabQueue().values())));
|
||||
send(session, new TextMessage(om.writeValueAsString(appStateExchange.getQueue().values())));
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error occurred", e);
|
||||
}
|
||||
|
||||
grabQueueSubscriptions.put(session.getId(),
|
||||
appStateService.getLiveGrabQueue()
|
||||
appStateExchange.liveQueue()
|
||||
.subscribeOn(commonExecutor.getScheduler())
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.buffer(2000, TimeUnit.MILLISECONDS, 200)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.map(e -> e.stream().distinct().collect(Collectors.toList()))
|
||||
.map(HashSet::new)
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
@@ -253,23 +244,23 @@ public class WebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
try {
|
||||
send(session, new TextMessage(om.writeValueAsString(
|
||||
appStateService.getCurrentImages()
|
||||
appStateExchange.getImages()
|
||||
.values()
|
||||
.stream()
|
||||
.filter(e -> e.getPostId().equals(postId))
|
||||
.collect(Collectors.toList())))
|
||||
.collect(Collectors.toSet())))
|
||||
);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error occurred", e);
|
||||
}
|
||||
|
||||
postDetailsSubscriptions.put(session.getId(), appStateService.getLiveImageUpdates()
|
||||
postDetailsSubscriptions.put(session.getId(), appStateExchange.liveImage()
|
||||
.subscribeOn(commonExecutor.getScheduler())
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(Schedulers.io())
|
||||
.filter(e -> e.getPostId().equals(postId))
|
||||
.buffer(2000, TimeUnit.MILLISECONDS, 500)
|
||||
.filter(e -> !e.isEmpty())
|
||||
.map(e -> e.stream().distinct().collect(Collectors.toList()))
|
||||
.map(HashSet::new)
|
||||
.map(om::writeValueAsString)
|
||||
.map(TextMessage::new)
|
||||
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
|
||||
|
||||
@@ -5,6 +5,5 @@ logging.file=${base.dir}/.vripper/vripper.log
|
||||
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=installer
|
||||
spring.profiles.active=portable
|
||||
#spring.profiles.active=installer
|
||||
@@ -1,17 +0,0 @@
|
||||
package tn.mnlr.vripper;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
public class VripperApplicationTests {
|
||||
|
||||
@Test
|
||||
public void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-15
@@ -17,6 +17,7 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"aot": true,
|
||||
"outputPath": "dist/vripper-ui",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
@@ -27,13 +28,7 @@
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"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"
|
||||
"src/styles.scss"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
@@ -58,6 +53,10 @@
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -88,13 +87,7 @@
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"karmaConfig": "src/karma.conf.js",
|
||||
"styles": [
|
||||
"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"
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
@@ -146,4 +139,4 @@
|
||||
}
|
||||
},
|
||||
"defaultProject": "vripper-ui"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4606
-4397
File diff suppressed because it is too large
Load Diff
+40
-41
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vripper-ui",
|
||||
"version": "2.10.5",
|
||||
"version": "2.12.1",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
@@ -13,47 +13,46 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@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.2.0",
|
||||
"photoswipe": "^4.1.3",
|
||||
"rxjs": "~6.5.3",
|
||||
"tslib": "^1.9.0",
|
||||
"zone.js": "~0.8.26"
|
||||
"@angular/animations": "9.1.4",
|
||||
"@angular/cdk": "9.2.1",
|
||||
"@angular/common": "9.1.4",
|
||||
"@angular/compiler": "9.1.4",
|
||||
"@angular/core": "9.1.4",
|
||||
"@angular/flex-layout": "9.0.0-beta.29",
|
||||
"@angular/forms": "9.1.4",
|
||||
"@angular/material": "9.2.1",
|
||||
"@angular/platform-browser": "9.1.4",
|
||||
"@angular/platform-browser-dynamic": "9.1.4",
|
||||
"@angular/router": "9.1.4",
|
||||
"ag-grid-angular": "23.0.3",
|
||||
"ag-grid-community": "23.0.2",
|
||||
"core-js": "3.6.5",
|
||||
"ngx-clipboard": "13.0.0",
|
||||
"ngx-electron": "2.2.0",
|
||||
"rxjs": "6.5.5",
|
||||
"tslib": "1.11.1",
|
||||
"zone.js": "0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.803.23",
|
||||
"@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",
|
||||
"codelyzer": "~4.5.0",
|
||||
"electron": "^7.1.7",
|
||||
"jasmine-core": "~2.99.1",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "^4.2.0",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "~1.1.2",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.4.0",
|
||||
"ts-node": "~7.0.0",
|
||||
"tslint": "~5.11.0",
|
||||
"typescript": "~3.5.0"
|
||||
"@angular-devkit/build-angular": "0.901.4",
|
||||
"@angular/cli": "9.1.4",
|
||||
"@angular/compiler-cli": "9.1.4",
|
||||
"@angular/language-service": "9.1.4",
|
||||
"@types/jasmine": "3.5.10",
|
||||
"@types/jasminewd2": "2.0.8",
|
||||
"@types/node": "12.12.37",
|
||||
"codelyzer": "5.2.2",
|
||||
"electron": "8.2.4",
|
||||
"jasmine-core": "3.5.0",
|
||||
"jasmine-spec-reporter": "5.0.2",
|
||||
"karma": "5.0.4",
|
||||
"karma-chrome-launcher": "3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "2.1.1",
|
||||
"karma-jasmine": "3.1.1",
|
||||
"karma-jasmine-html-reporter": "1.5.3",
|
||||
"protractor": "5.4.4",
|
||||
"ts-node": "8.9.1",
|
||||
"tslint": "6.1.2",
|
||||
"typescript": "3.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>tn.mnlr</groupId>
|
||||
<artifactId>vripper</artifactId>
|
||||
<version>2.10.5</version>
|
||||
<version>2.12.1</version>
|
||||
</parent>
|
||||
<artifactId>vripper-ui</artifactId>
|
||||
<name>vripper-ui</name>
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
<ng-container *ngIf="appState | async as state">
|
||||
<div *ngIf="state === 'CONNECTING'" class="overlay loading" fxLayout="column" fxLayoutAlign="center center">
|
||||
<div *ngIf="loaded | async; else loading" [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>
|
||||
<ng-template #loading>
|
||||
<div class="overlay loading" fxLayout="column" fxLayoutAlign="center center">
|
||||
<div class="sk-chase">
|
||||
<div class="sk-chase-dot"></div>
|
||||
<div class="sk-chase-dot"></div>
|
||||
@@ -9,17 +19,4 @@
|
||||
<div class="sk-chase-dot"></div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AppService } from './app.service';
|
||||
import { ClipboardService } from './clipboard.service';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
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 {AppService} from './app.service';
|
||||
import {ClipboardService} from './clipboard.service';
|
||||
import {ElectronService} from 'ngx-electron';
|
||||
import {AfterViewInit, ChangeDetectionStrategy, Component, NgZone, OnDestroy, Renderer2} from '@angular/core';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {BehaviorSubject, merge, Subject, Subscription} from 'rxjs';
|
||||
import {WsConnectionService} from './ws-connection.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -15,7 +15,7 @@ import { WsConnectionService, WSState } from './ws-connection.service';
|
||||
export class AppComponent implements OnDestroy, AfterViewInit {
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private ws: WsConnectionService,
|
||||
public ws: WsConnectionService,
|
||||
public electronService: ElectronService,
|
||||
private clipboardService: ClipboardService,
|
||||
private appService: AppService,
|
||||
@@ -26,28 +26,33 @@ export class AppComponent implements OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
private subscriptions: Subscription[] = [];
|
||||
appState: Subject<string> = new BehaviorSubject('CONNECTING');
|
||||
electron: Subject<boolean>;
|
||||
|
||||
loaded: Subject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.appService.renderer = this.renderer;
|
||||
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();
|
||||
merge(this.appService.loadTheme(), this.appService.loadSettings())
|
||||
.subscribe(() => this.ngZone.run(() => this.appState.next('CONNECTED')));
|
||||
}
|
||||
}));
|
||||
this.subscriptions.push(
|
||||
this.ws.state.subscribe(online => {
|
||||
if (!online) {
|
||||
this.ngZone.run(() => this.loaded.next(false));
|
||||
setTimeout(
|
||||
() =>
|
||||
this.ngZone.run(() => {
|
||||
this.dialog.closeAll();
|
||||
}),
|
||||
500
|
||||
);
|
||||
} else {
|
||||
this.ngZone.run(() => this.loaded.next(true));
|
||||
this.clipboardService.init();
|
||||
this.subscriptions.push(merge(this.appService.loadTheme(), this.appService.loadSettings()).subscribe());
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.subscriptions.forEach(e => {
|
||||
e.unsubscribe();
|
||||
});
|
||||
this.subscriptions.forEach(e => e.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,38 @@
|
||||
import { LinkCollectorService } from './link-collector.service';
|
||||
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';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { NgModule } from '@angular/core';
|
||||
import {PostContextMenuComponent} from './posts/context-menu/post-context-menu.component';
|
||||
import {PostDetailsContextMenuComponent} from './post-progress/context-menu/post-details-context-menu.component';
|
||||
import {AppPreviewDirective} from './domain/preview-tooltip.directive';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {NgModule} from '@angular/core';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { MaterialModule } from './material.module';
|
||||
import { PostsComponent } from './posts/posts.component';
|
||||
import { PostDetailComponent } from './post-detail/post-detail.component';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { AgGridModule } from 'ag-grid-angular';
|
||||
import { PostProgressRendererComponent } from './posts/post-progress.renderer.component';
|
||||
import { PostDetailsProgressRendererComponent } from './post-detail/post-details-progress.component';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { XhrInterceptorService } from './xhr-interceptor.service';
|
||||
import { HomeComponent } from './home/home.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import {AppComponent} from './app.component';
|
||||
import {MaterialModule} from './material.module';
|
||||
import {PostsComponent} from './posts/posts.component';
|
||||
import {PostDetailComponent} from './post-progress/post-detail.component';
|
||||
import {FlexLayoutModule} from '@angular/flex-layout';
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {HttpClientModule} from '@angular/common/http';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {AgGridModule} from 'ag-grid-angular';
|
||||
import {PostProgressRendererComponent} from './posts/renderer/post-progress.renderer.component';
|
||||
import {PostDetailsProgressRendererComponent} from './post-progress/renderer/post-details-progress.component';
|
||||
import {LoginComponent} from './login/login.component';
|
||||
import {HomeComponent} from './home/home.component';
|
||||
import {SettingsComponent} from './settings/settings.component';
|
||||
|
||||
import { NgxElectronModule } from 'ngx-electron';
|
||||
import { ServerService } from './server-service';
|
||||
import { ConfirmDialogComponent } from './common/confirmation-component/confirmation-dialog';
|
||||
import { SharedService } from './posts/shared.service';
|
||||
import { MultiPostComponent } from './multi-post/multi-post.component';
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { AppPreviewComponent } from './common/preview-tooltip.component';
|
||||
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';
|
||||
import {NgxElectronModule} from 'ngx-electron';
|
||||
import {ConfirmDialogComponent} from './confirmation-component/confirmation-dialog';
|
||||
import {MultiPostComponent} from './multi-post/multi-post.component';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {AppPreviewComponent} from './domain/preview-tooltip.component';
|
||||
import {UrlRendererComponent} from './multi-post/renderer/url-renderer.component';
|
||||
import {FilterComponent} from './filter/filter.component';
|
||||
import {ScanComponent} from './scan/scan.component';
|
||||
import {StatusBarComponent} from './status-bar/status-bar.component';
|
||||
import {ToolbarComponent} from './toolbar/toolbar.component';
|
||||
import {GrabQueueComponent} from './grab-queue/grab-queue.component';
|
||||
import {UrlGrabRendererComponent} from './grab-queue/renderer/url-renderer.component';
|
||||
import {ClipboardModule} from 'ngx-clipboard';
|
||||
import {AlternativeTitleComponent} from './posts/alternative-title/alternative-title.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -64,18 +56,18 @@ import { PhotoSwipeComponent } from './photo-swipe/photo-swipe.component';
|
||||
ToolbarComponent,
|
||||
GrabQueueComponent,
|
||||
PostContextMenuComponent,
|
||||
GalleryComponent,
|
||||
PhotoSwipeComponent
|
||||
PostDetailsContextMenuComponent,
|
||||
AlternativeTitleComponent
|
||||
],
|
||||
entryComponents: [
|
||||
PostDetailComponent,
|
||||
SettingsComponent,
|
||||
ConfirmDialogComponent,
|
||||
AppPreviewComponent,
|
||||
ScanComponent,
|
||||
MultiPostComponent,
|
||||
PostContextMenuComponent,
|
||||
GalleryComponent
|
||||
// PostDetailComponent,
|
||||
// SettingsComponent,
|
||||
// ConfirmDialogComponent,
|
||||
// AppPreviewComponent,
|
||||
// ScanComponent,
|
||||
// MultiPostComponent,
|
||||
// PostContextMenuComponent,
|
||||
// PostDetailsContextMenuComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserAnimationsModule,
|
||||
@@ -92,19 +84,10 @@ import { PhotoSwipeComponent } from './photo-swipe/photo-swipe.component';
|
||||
UrlRendererComponent,
|
||||
UrlGrabRendererComponent
|
||||
]),
|
||||
OverlayModule
|
||||
],
|
||||
providers: [
|
||||
AppService,
|
||||
WsConnectionService,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: XhrInterceptorService, multi: true },
|
||||
ServerService,
|
||||
SharedService,
|
||||
SelectionService,
|
||||
PostsDataService,
|
||||
ContextMenuService,
|
||||
LinkCollectorService
|
||||
OverlayModule,
|
||||
ClipboardModule
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
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 } from '@angular/core';
|
||||
import { ServerService } from './server-service';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { ScanComponent } from './scan/scan.component';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { BreakpointState, Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import {Settings} from './domain/settings.model';
|
||||
import {environment} from 'src/environments/environment';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
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 {BreakpointObserver, Breakpoints, BreakpointState} from '@angular/cdk/layout';
|
||||
import {Title} from '@angular/platform-browser';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AppService {
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ClipboardService } from './clipboard.service';
|
||||
|
||||
describe('ClipboardService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: ClipboardService = TestBed.get(ClipboardService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Subject, Observable } from 'rxjs';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Clipboard } from 'electron';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Settings } from './common/settings.model';
|
||||
import { ServerService } from './server-service';
|
||||
import {Observable, Subject} from 'rxjs';
|
||||
import {ElectronService} from 'ngx-electron';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Clipboard} from 'electron';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Settings} from './domain/settings.model';
|
||||
import {ServerService} from './server-service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@@ -13,6 +13,7 @@ export class ClipboardService {
|
||||
private links$: Subject<string> = new Subject();
|
||||
private interval: NodeJS.Timer;
|
||||
private lastText = '';
|
||||
|
||||
constructor(
|
||||
private electronService: ElectronService,
|
||||
private serverService: ServerService,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { trigger, state, style, transition, animate } from '@angular/animations';
|
||||
|
||||
@Component({
|
||||
selector: 'app-preview',
|
||||
template: `
|
||||
<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'"
|
||||
[src]="link"
|
||||
/>
|
||||
</ng-container>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
img {
|
||||
max-height: 200px;
|
||||
max-width: 400px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
`
|
||||
],
|
||||
animations: [
|
||||
trigger('simpleFadeAnimation', [
|
||||
state('in', style({ opacity: 1 })),
|
||||
|
||||
transition(':enter', [style({ opacity: 0 }), animate(300)])
|
||||
])
|
||||
]
|
||||
})
|
||||
export class AppPreviewComponent {
|
||||
@Input() links: string[] = [];
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export interface Settings {
|
||||
downloadPath: string;
|
||||
maxThreads: number;
|
||||
maxTotalThreads: number;
|
||||
autoStart: boolean;
|
||||
vLogin: boolean;
|
||||
vUsername: string;
|
||||
vPassword: string;
|
||||
vThanks: boolean;
|
||||
desktopClipboard: boolean;
|
||||
viewPhotos: boolean;
|
||||
notification: boolean;
|
||||
}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
|
||||
import {Component, Inject} from '@angular/core';
|
||||
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
|
||||
|
||||
export interface DialogConfirmation {
|
||||
header: string;
|
||||
+3
-1
@@ -4,6 +4,8 @@ export class GrabQueueState {
|
||||
public link: string,
|
||||
public threadId: string,
|
||||
public postId: string,
|
||||
public removed: boolean
|
||||
public removed: boolean,
|
||||
public count: number,
|
||||
public loading: boolean
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface PostId {
|
||||
postId: string;
|
||||
}
|
||||
+3
-1
@@ -10,6 +10,8 @@ export class PostState {
|
||||
public done: number,
|
||||
public total: number,
|
||||
public hosts: string[],
|
||||
public previews: string[]
|
||||
public previews: string[],
|
||||
public thanked: boolean,
|
||||
public alternativeTitle: string[]
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-preview',
|
||||
template: `
|
||||
<div class="previews" style="display: inline-block; white-space: nowrap;">
|
||||
<ng-container *ngFor="let link of links">
|
||||
<img
|
||||
[src]="link"
|
||||
(load)="loaded($event)"
|
||||
/>
|
||||
</ng-container>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
img {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
margin-left: 5px;
|
||||
opacity: 0;
|
||||
object-fit: cover;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease-in, visibility 0.2s;
|
||||
}
|
||||
`
|
||||
]
|
||||
})
|
||||
export class AppPreviewComponent {
|
||||
@Input() links: string[] = [];
|
||||
|
||||
loaded(event: Event) {
|
||||
(<HTMLElement>event.target).style.visibility = 'visible';
|
||||
(<HTMLElement>event.target).style.opacity = '1';
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { AppPreviewComponent } from './preview-tooltip.component';
|
||||
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';
|
||||
import {AppPreviewComponent} from './preview-tooltip.component';
|
||||
import {ComponentRef, Directive, ElementRef, HostListener, Input, NgZone, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Overlay, OverlayPositionBuilder, OverlayRef} from '@angular/cdk/overlay';
|
||||
import {ComponentPortal} from '@angular/cdk/portal';
|
||||
|
||||
@Directive({ selector: '[appPreview]' })
|
||||
@Directive({selector: '[appPreview]'})
|
||||
export class AppPreviewDirective implements OnInit, OnDestroy {
|
||||
private overlayRef: OverlayRef;
|
||||
tooltipPortal = new ComponentPortal(AppPreviewComponent);
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface RenamePostModel {
|
||||
postId: string;
|
||||
altName: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface Settings {
|
||||
downloadPath: string;
|
||||
maxThreads: number;
|
||||
maxTotalThreads: number;
|
||||
autoStart: boolean;
|
||||
vLogin: boolean;
|
||||
vUsername: string;
|
||||
vPassword: string;
|
||||
vThanks: boolean;
|
||||
desktopClipboard: boolean;
|
||||
viewPhotos: boolean;
|
||||
notification: boolean;
|
||||
resolveTitle: boolean;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<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">
|
||||
<img
|
||||
[style.opacity]="img._initialized ? 1 : 0"
|
||||
[style.visibility]="img._initialized ? 'visible' : 'hidden'"
|
||||
(click)="openSlideshow(i)"
|
||||
(load)="loaded(img)"
|
||||
[src]="img.msrc"
|
||||
/>
|
||||
</figure>
|
||||
</ng-container>
|
||||
|
||||
<app-photo-swipe #photoSwipe></app-photo-swipe>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<div style="height: 5px; width: 100%; margin-top: 8px">
|
||||
<div *ngIf="loadProgress | async as progress">
|
||||
<mat-progress-bar
|
||||
*ngIf="!progress.done"
|
||||
[mode]="progress.loading ? 'determinate' : 'indeterminate'"
|
||||
[value]="progress.progress"
|
||||
></mat-progress-bar>
|
||||
</div>
|
||||
</div>
|
||||
<mat-dialog-actions align="end" fxFlex="nogrow">
|
||||
<button (click)="refresh()" [disabled]="disableRefresh | async" 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>
|
||||
@@ -1,24 +0,0 @@
|
||||
.container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.masonry {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.masonry-brick {
|
||||
height: 250px;
|
||||
width: 250px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
img {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.5s ease-in, visibility 0.5s;
|
||||
object-fit: cover;
|
||||
height: 250px;
|
||||
width: 250px;
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
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 title: string,
|
||||
public src: string,
|
||||
public msrc: string,
|
||||
public w: number,
|
||||
public h: number,
|
||||
public _initialized: boolean
|
||||
) {
|
||||
super(src, msrc, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
interface Progress {
|
||||
loading: boolean;
|
||||
progress: number;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
const initialProgressState: Progress = { loading: false, progress: 0, done: false };
|
||||
|
||||
@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[] = [];
|
||||
disableRefresh: Subject<boolean> = new BehaviorSubject(true);
|
||||
loadProgress: Subject<Progress> = new BehaviorSubject(initialProgressState);
|
||||
loadedImages = 0;
|
||||
|
||||
@ViewChild('photoSwipe', { static: true }) photoSwipe: PhotoSwipeComponent;
|
||||
|
||||
loaded(img: Image) {
|
||||
img._initialized = true;
|
||||
this.loadedImages++;
|
||||
this.loadProgress.next({
|
||||
loading: true,
|
||||
progress: Math.floor((this.loadedImages / this._images.length) * 100),
|
||||
done: this.loadedImages === this._images.length
|
||||
});
|
||||
if (this.loadedImages === this._images.length) {
|
||||
this.disableRefresh.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
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.loadProgress.next(initialProgressState);
|
||||
this.loadedImages = 0;
|
||||
this.disableRefresh.next(true);
|
||||
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
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
|
||||
</ag-grid-angular>
|
||||
<div style="width: 100%; height: 100%; padding-top: 5px">
|
||||
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-alpine" style="width: 100%; height: calc(100% - 5px);">
|
||||
</ag-grid-angular>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { LinkCollectorService } from './../link-collector.service';
|
||||
import { UrlGrabRendererComponent } from './url-renderer.component';
|
||||
import { GrabQueueDataSource } from './grab-queue.datasource';
|
||||
import { Component, OnInit, NgZone, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { GridOptions } from 'ag-grid-community';
|
||||
import { WsConnectionService } from '../ws-connection.service';
|
||||
import { NotificationService } from '../notification.service';
|
||||
import {LinkCollectorService} from './../link-collector.service';
|
||||
import {UrlGrabRendererComponent} from './renderer/url-renderer.component';
|
||||
import {GrabQueueDataSource} from './grab-queue.datasource';
|
||||
import {ChangeDetectionStrategy, Component, NgZone, OnInit} from '@angular/core';
|
||||
import {GridOptions} from 'ag-grid-community';
|
||||
import {WsConnectionService} from '../ws-connection.service';
|
||||
import {NotificationService} from '../notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grab-queue',
|
||||
@@ -29,7 +29,7 @@ export class GrabQueueComponent implements OnInit {
|
||||
cellRenderer: 'urlCellRenderer'
|
||||
}
|
||||
],
|
||||
rowHeight: 48,
|
||||
rowHeight: 50,
|
||||
animateRows: true,
|
||||
rowData: [],
|
||||
frameworkComponents: {
|
||||
@@ -37,7 +37,7 @@ export class GrabQueueComponent implements OnInit {
|
||||
},
|
||||
overlayLoadingTemplate: '<span></span>',
|
||||
overlayNoRowsTemplate: '<span></span>',
|
||||
getRowNodeId: data => data['link'],
|
||||
getRowNodeId: data => data['threadId'],
|
||||
onGridReady: () => {
|
||||
this.gridOptions.api.sizeColumnsToFit();
|
||||
this.dataSource = new GrabQueueDataSource(this.wsConnection, this.gridOptions, this.zone, this.notificationService);
|
||||
|
||||
@@ -1,69 +1,72 @@
|
||||
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 { NotificationService } from '../notification.service';
|
||||
import { GridOptions } from 'ag-grid-community';
|
||||
import { WsHandler } from '../ws-handler';
|
||||
import { NgZone } from '@angular/core';
|
||||
import {GrabQueueState} from '../domain/grab-queue.model';
|
||||
import {CMD} from '../domain/cmd.enum';
|
||||
import {WSMessage} from '../domain/ws-message.model';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {WsConnectionService} from '../ws-connection.service';
|
||||
import {NotificationService} from '../notification.service';
|
||||
import {GridOptions, RowNode} from 'ag-grid-community';
|
||||
import {NgZone} from '@angular/core';
|
||||
|
||||
export class GrabQueueDataSource {
|
||||
constructor(
|
||||
private wsConnectionService: WsConnectionService,
|
||||
private ws: WsConnectionService,
|
||||
private gridOptions: GridOptions,
|
||||
private zone: NgZone,
|
||||
private notificationService: NotificationService
|
||||
) {
|
||||
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
|
||||
}
|
||||
|
||||
websocketHandlerPromise: Promise<WsHandler>;
|
||||
subscriptions: Subscription[] = [];
|
||||
grabQueueSub: 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);
|
||||
console.log('Connecting to grab queue datasource');
|
||||
this.subscriptions.push(
|
||||
this.ws.state.subscribe(state => {
|
||||
if (state) {
|
||||
this.grabQueueSub = this.ws.subscribeForGrabQueue().subscribe((e: GrabQueueState[]) => {
|
||||
this.zone.run(() => {
|
||||
const toAdd = [];
|
||||
const toUpdate = [];
|
||||
const toRemove = [];
|
||||
e.forEach(v => {
|
||||
const rowNode: RowNode = this.gridOptions.api.getRowNode(v.threadId);
|
||||
if (v.removed) {
|
||||
if (rowNode != null) {
|
||||
toRemove.push(rowNode.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.gridOptions.api.getRowNode(v.link) == null) {
|
||||
toAdd.push(v);
|
||||
} else {
|
||||
toUpdate.push(v);
|
||||
if (rowNode == null) {
|
||||
toAdd.push(v);
|
||||
} else {
|
||||
toUpdate.push(v);
|
||||
}
|
||||
});
|
||||
this.gridOptions.api.updateRowData({ update: toUpdate, add: toAdd, remove: toRemove });
|
||||
const count = this.gridOptions.api.getDisplayedRowCount();
|
||||
if (count > 0 && toAdd.length > 0) {
|
||||
this.notificationService.notifyFromGrabQueue(
|
||||
'Link Collector',
|
||||
`You have ${count} ${count > 1 ? 'threads' : 'thread'} waiting in the link collector`
|
||||
);
|
||||
}
|
||||
});
|
||||
this.gridOptions.api.updateRowData({ update: toUpdate, add: toAdd, remove: toRemove });
|
||||
const count = this.gridOptions.api.getDisplayedRowCount();
|
||||
if (count > 0 && toAdd.length > 0) {
|
||||
this.notificationService.notifyFromGrabQueue(
|
||||
'Link Collector',
|
||||
`You have ${count} ${count > 1 ? 'threads' : 'thread'} waiting in the link collector`
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
handler.send(new WSMessage(CMD.GRAB_QUEUE_SUB.toString()));
|
||||
});
|
||||
this.ws.send(new WSMessage(CMD.GRAB_QUEUE_SUB.toString()));
|
||||
} else if (this.grabQueueSub != null) {
|
||||
this.grabQueueSub.unsubscribe();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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()));
|
||||
});
|
||||
this.ws.send(new WSMessage(CMD.GRAB_QUEUE_UNSUB.toString()));
|
||||
if (this.grabQueueSub != null) {
|
||||
this.grabQueueSub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user