Compare commits

...
12 Commits
Author SHA1 Message Date
death-claw bf2e8c4bb6 v1.4.7 2019-08-13 11:21:54 +01:00
death-claw 5d449fa4e5 Fixed a bug in image filename extensions 2019-08-13 11:07:11 +01:00
death-claw c8b76b2dfb v1.4.6
* Fixed a bug when creating image file names
* Remove unnecessary logs
* Fix data.json init
2019-08-06 16:44:38 +01:00
death-claw e3e9935e15 * Remove old data.json
* Switch to fixedDelay scheduler for speed calculation
2019-08-03 18:19:24 +01:00
death-claw 83b41763bd Next snapshot version 2019-08-03 18:17:44 +01:00
death-clawandGitHub ec479cb920 Update README.md 2019-08-03 17:02:15 +01:00
death-claw 4827a88ac4 Include a README.md 2019-08-03 16:26:19 +01:00
death-claw d4980ad6a3 v1.4.5
* Add download speed to status bar
* Add min width and height
2019-08-03 13:37:07 +01:00
death-claw f9fc8de3a5 v1.4.4 2019-08-01 22:04:27 +01:00
death-claw 9a9df9c8e9 * Implement start, stop
* Fix npm dependencies
* Enhance UI
* Refactor maven build
2019-08-01 21:18:04 +01:00
death-claw 46083b6c91 * Check response code when downloading host page
* Fix image name of ImgboxHost
* Add start and stop button in UI
2019-07-30 19:06:32 +01:00
death-claw 8abd8dbf9e v1.4.4-SNAPSHOT
* Better app management
* Imagetwist support
* Global state feature
2019-07-19 22:48:45 +01:00
44 changed files with 1408 additions and 418 deletions
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## [1.4.7] - 2019-08-13
### Changed
- Fixed a bug in image filename extensions
## [1.4.6] - 2019-08-06
### Changed
- Fixed a bug when creating image file names
- Remove unnecessary logs
- Fix data.json init
## [1.4.5] - 2019-08-03
### Changed
- Add min width and height for electron app
### Added
- Add download speed to status bar
## [1.4.4] - 2019-08-01
### Changed
- Fix names for ImgboxHost
- Enhance UI
- Minor fixes
### Added
- Add start and stop button
## [1.4.3] - 2019-07-14
### Changed
- Update some messages
+26
View File
@@ -0,0 +1,26 @@
# Viper Ripper!
This is my spin for a 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.
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
To build the desktop app:
mvn clean install -Pelectron
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.
+2 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.3</version>
<version>1.4.7</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
@@ -15,5 +15,6 @@
<modules>
<module>vripper-server</module>
<module>vripper-ui</module>
<module>vripper-electron</module>
</modules>
</project>
+42 -8
View File
@@ -6,13 +6,17 @@ const { spawn } = require("child_process");
const { ipcMain } = require("electron");
const commandExists = require("command-exists").sync;
const { dialog } = require("electron");
const axios = require('axios');
const appDir = process.env.APPDIR;
console.log('App path', path.join(app.getPath('exe'), "../bin/vripper-server.jar"));
console.log('App image path', appDir);
let win;
let vripperServer;
let serverPort;
const maxTerminationAttemps = 5;
let terminationAttemps = 0;
let terminationInteval;
let terminated = false;
process.on("uncaughtException", err => {
dialog.showErrorBox(err.message, err.stack);
@@ -37,6 +41,8 @@ function createWindow() {
win = new BrowserWindow({
width: 1024,
height: 768,
minWidth: 640,
minHeight: 480,
frame: false,
webPreferences: {
nodeIntegration: true
@@ -62,6 +68,7 @@ function createWindow() {
}
getPort().then(port => {
serverPort = port;
ipcMain.on("get-port", event => {
event.reply("port", port);
});
@@ -70,7 +77,14 @@ getPort().then(port => {
"-jar",
appDir !== undefined ? path.join(appDir, "bin/vripper-server.jar") :
path.join(app.getPath('exe'), "../bin/vripper-server.jar")
], {stdio: 'ignore'});
], {
stdio: 'ignore'
});
vripperServer.on('exit', (code, signal) => {
console.log(`vripper server terminated, code = ${code}, signal = ${signal}`);
terminated = true;
app.quit();
});
});
const gotTheLock = app.requestSingleInstanceLock();
@@ -89,10 +103,30 @@ if (!gotTheLock) {
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
if(vripperServer != null) {
vripperServer.kill("SIGTERM");
}
app.quit();
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
vripperServer.kill('SIGKILL');
terminated = true;
app.quit();
});
}
});
+41 -4
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "1.4.3",
"version": "1.4.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -225,6 +225,15 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -1199,6 +1208,29 @@
}
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -1494,6 +1526,11 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
"is-buffer": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
},
"is-ci": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
@@ -1732,9 +1769,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"loud-rejection": {
"version": "1.6.0",
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "1.4.3",
"version": "1.4.7",
"description": "",
"main": "main.js",
"author": "",
@@ -27,7 +27,7 @@
],
"extraFiles": [
{
"from": "../vripper-server/target/vripper-server-${version}.jar",
"from": "../vripper-server/target/vripper-server-${version}-electron.jar",
"to": "bin/vripper-server.jar"
}
],
@@ -36,7 +36,7 @@
},
"linux": {
"target": [
"AppImage"
"zip"
]
}
},
@@ -49,6 +49,7 @@
"electron-builder": "^20.44.4"
},
"dependencies": {
"axios": "^0.19.0",
"cheerio": "^1.0.0-rc.3",
"command-exists": "^1.2.8",
"copy-dir": "^1.1.0",
+77
View File
@@ -0,0 +1,77 @@
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.7</version>
</parent>
<artifactId>vripper-electron</artifactId>
<name>vripper-electron</name>
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<classifier>${buildClassifier}</classifier>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>electron</id>
<properties>
<buildClassifier>electron</buildClassifier>
</properties>
<dependencies>
<dependency>
<groupId>tn.mnlr</groupId>
<artifactId>vripper-server</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
<classifier>${buildClassifier}</classifier>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.3</version>
<configuration>
<nodeVersion>v12.2.0</nodeVersion>
<npmVersion>6.9.0</npmVersion>
<workingDirectory>build-dir</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run dist</arguments>
</configuration>
<phase>generate-resources</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
File diff suppressed because one or more lines are too long
+58 -16
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.3</version>
<version>1.4.7</version>
</parent>
<artifactId>vripper-server</artifactId>
<name>vripper-server</name>
@@ -28,7 +28,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@@ -69,33 +72,29 @@
<artifactId>vripper-ui</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
<classifier>${buildClassifier}</classifier>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<classifier>${buildClassifier}</classifier>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<id>repackage</id>
<goals>
<goal>copy-resources</goal>
<goal>repackage</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/static</outputDirectory>
<resources>
<resource>
<directory>${project.parent.basedir}/vripper-ui/dist/vripper-ui/
</directory>
</resource>
</resources>
<classifier>${buildClassifier}</classifier>
</configuration>
</execution>
</executions>
@@ -149,4 +148,47 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>web</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<buildClassifier>web</buildClassifier>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/static</outputDirectory>
<resources>
<resource>
<directory>${project.parent.basedir}/vripper-ui/dist/vripper-ui/
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>electron</id>
<properties>
<buildClassifier>electron</buildClassifier>
</properties>
</profile>
</profiles>
</project>
@@ -7,7 +7,6 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
import tn.mnlr.vripper.services.PersistenceService;
import java.util.Map;
@@ -18,8 +17,6 @@ public class SpringContext implements ApplicationContextAware {
private static ConfigurableApplicationContext context;
private PersistenceService persistenceService;
/**
* Returns the Spring managed bean instance of the given class type (if it exists).
* Returns null otherwise.
@@ -27,6 +27,8 @@ public class Post {
private String title;
private String postCounter;
private String url;
private List<Image> images;
@@ -39,12 +41,13 @@ public class Post {
private boolean removed = false;
public Post(String title, String url, List<Image> images, Map<String, String> metadata, String postId, AppStateService appStateService) {
public Post(String title, String url, List<Image> images, Map<String, String> metadata, String postId, String postCounter, AppStateService appStateService) {
this.title = title;
this.url = url;
this.images = images;
this.metadata = metadata;
this.postId = postId;
this.postCounter = postCounter;
this.appStateService = appStateService;
total = images.size();
status = Status.PENDING;
@@ -11,20 +11,21 @@ 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.services.AppSettingsService;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.exception.DownloadException;
import tn.mnlr.vripper.exception.HostException;
import tn.mnlr.vripper.exception.HtmlProcessorException;
import tn.mnlr.vripper.q.ImageFileData;
import tn.mnlr.vripper.services.ConnectionManager;
import tn.mnlr.vripper.services.HtmlProcessorService;
import tn.mnlr.vripper.services.XpathService;
import tn.mnlr.vripper.services.*;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.Random;
@Service
@@ -46,6 +47,9 @@ abstract public class Host {
@Autowired
private ConnectionManager cm;
@Autowired
private DownloadSpeedService downloadSpeedService;
abstract protected String getHost();
public boolean isSupported(String url) {
@@ -72,17 +76,12 @@ abstract public class Host {
* END HOST SPECIFIC
*/
if(!imageFileData.getImageName().toLowerCase().endsWith(".jpg")) {
imageFileData.setImageName(imageFileData.getImageName() + ".jpg");
}
File destinationFolder = new File(appSettingsService.getDownloadPath(), sanitize(image.getPostName()));
imageFileData.setImageName(formatImageFileName(imageFileData.getImageName()));
File destinationFolder = new File(appSettingsService.getDownloadPath(), sanitize(image.getPostName() + "_" + image.getPostId()));
logger.info(String.format("Saving to %s", destinationFolder.getPath()));
if (!destinationFolder.exists()) {
logger.info(String.format("Creating %s", destinationFolder.getPath()));
destinationFolder.mkdirs();
} else {
logger.warn(String.format("%s already exists, the file will be overridden", destinationFolder.getPath()));
}
HttpClient client = cm.getClient().build();
@@ -95,9 +94,10 @@ abstract public class Host {
throw new DownloadException(String.format("Server returned code %d", response.getStatusLine().getStatusCode()));
}
File outputFile = new File(destinationFolder.getPath() + File.separator + imageFileData.getImageName() + ".tmp");
try (
InputStream downloadStream = response.getEntity().getContent();
FileOutputStream fos = new FileOutputStream(destinationFolder.getPath() + File.separator + imageFileData.getImageName())
FileOutputStream fos = new FileOutputStream(outputFile)
) {
image.setTotal(response.getEntity().getContentLength());
@@ -109,8 +109,10 @@ abstract public class Host {
while ((read = downloadStream.read(buffer, 0, READ_BUFFER_SIZE)) != -1) {
fos.write(buffer, 0, read);
image.increase(read);
downloadSpeedService.increase(read);
}
EntityUtils.consumeQuietly(response.getEntity());
checkImageTypeAndRename(outputFile, imageFileData.getImageName());
}
}
} catch (Exception e) {
@@ -121,6 +123,42 @@ abstract public class Host {
}
}
private void checkImageTypeAndRename(File outputFile, String imageName) throws HostException {
try {
ImageInputStream iis = ImageIO.createImageInputStream(outputFile);
Iterator<ImageReader> it = ImageIO.getImageReaders(iis);
if (!it.hasNext()) {
throw new HostException("Image file is not recognized!");
}
ImageReader reader = it.next();
String formatName = reader.getFormatName();
if (formatName.toUpperCase().equals("JPEG")) {
formatName = "JPG";
}
String outImageName = imageName + "." + formatName.toUpperCase();
outputFile.renameTo(new File(outputFile.getParent(), outImageName));
} catch (Exception e) {
throw new HostException("Failed to rename output file", e);
}
}
/**
* Will sanitize the image name and remove extension
*
* @param imageName
* @return
*/
protected String formatImageFileName(String imageName) {
int extensionIndex = imageName.lastIndexOf('.');
String fileName;
if (extensionIndex != -1) {
fileName = imageName.substring(0, extensionIndex);
} else {
fileName = imageName;
}
return sanitize(fileName);
}
/**
* Just for testing, you may ignore
* @throws Exception
@@ -155,6 +193,9 @@ abstract public class Host {
Header[] headers;
logger.info(String.format("Requesting %s", url));
try (CloseableHttpResponse response = (CloseableHttpResponse) client.execute(httpGet)) {
if (response.getStatusLine().getStatusCode() / 100 != 2) {
throw new HostException(String.format("Unexpected response code: %d", response.getStatusLine().getStatusCode()));
}
headers = response.getAllHeaders();
basePage = EntityUtils.toString(response.getEntity());
logger.debug(String.format("%s response: %n%s", url, basePage));
@@ -0,0 +1,54 @@
package tn.mnlr.vripper.host;
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;
@Service
public class ImageTwistHost extends Host {
public 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;
@Override
protected String getHost() {
return host;
}
@Override
protected void setNameAndUrl(final String url, final ImageFileData imageFileData) throws HostException {
Document doc = getResponse(url).getDocument();
Node imgNode;
try {
logger.info(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.info(String.format("Resolving name and image url for %s", url));
String imgTitle = Optional.ofNullable(imgNode.getAttributes().getNamedItem("alt")).map(Node::getTextContent).map(String::trim).orElse(null);
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
imageFileData.setImageUrl(imgUrl);
imageFileData.setImageName(imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -46,7 +46,7 @@ public class ImgboxHost extends Host {
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
imageFileData.setImageUrl(imgUrl);
imageFileData.setImageName(imgTitle.substring(8));
imageFileData.setImageName(imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
@@ -9,6 +9,7 @@ import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.services.AppStateService;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
@@ -18,6 +19,8 @@ import java.util.stream.Collectors;
@Service
public class DownloadQ {
private static final List<Post.Status> FINISHED = Arrays.asList(Post.Status.ERROR, Post.Status.COMPLETE, Post.Status.STOPPED);
private static final Logger logger = LoggerFactory.getLogger(DownloadQ.class);
@Autowired
@@ -102,6 +105,9 @@ public class DownloadQ {
public synchronized void stop(String postId) {
try {
if (FINISHED.contains(appStateService.getPost(postId).getStatus())) {
return;
}
notPauseQ = false;
appStateService.getPost(postId).setStatus(Post.Status.STOPPED);
List<Image> images = appStateService.getPost(postId)
@@ -119,4 +125,19 @@ public class DownloadQ {
notPauseQ = true;
}
}
public int size() {
return downloadQ.size();
}
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);
}
}
}
@@ -7,8 +7,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.AppStateService;
import javax.annotation.PostConstruct;
@@ -105,6 +105,9 @@ public class ExecutionService {
DownloadJob take = null;
try {
take = downloadQ.take();
if (take == null) {
continue;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
@@ -127,7 +130,7 @@ public class ExecutionService {
appStateService.doneDownloadJob(finalTake.getImage());
logger.info(String.format("Finished downloading %s", finalTake.getImage().getUrl()));
synchronized (threadCount) {
int i = threadCount.decrementAndGet();
threadCount.decrementAndGet();
running.remove(finalTake);
futures.remove(finalTake.getImage().getUrl());
threadCount.notify();
@@ -149,4 +152,8 @@ public class ExecutionService {
}
}
}
public int runningCount() {
return running.size();
}
}
@@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.exception.ValidationException;
import javax.annotation.PreDestroy;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
@@ -63,6 +64,7 @@ public class AppSettingsService {
desktopClipboard = prefs.getBoolean(DESKTOP_CLIPBOARD, false);
}
@PreDestroy
public void save() {
prefs.put(DOWNLOAD_PATH, downloadPath);
@@ -49,8 +49,8 @@ public class AppStateService {
return new Image(pageUrl, postId, postName, host, this);
}
public Post createPost(String title, String url, List<Image> images, Map<String, String> metadata, String postId) {
return new Post(title, url, images, metadata, postId, this);
public Post createPost(String title, String url, List<Image> images, Map<String, String> metadata, String postId, String postCounter) {
return new Post(title, url, images, metadata, postId, postCounter, this);
}
public Post getPost(String postId) {
@@ -79,7 +79,9 @@ public class AppStateService {
if (post.getImages().stream().map(Image::getStatus).filter(e -> e.equals(Image.Status.ERROR)).count() > 0) {
post.setStatus(Post.Status.ERROR);
} else {
post.setStatus(Post.Status.COMPLETE);
if (!Post.Status.STOPPED.equals(post.getStatus())) {
post.setStatus(Post.Status.COMPLETE);
}
}
livePostsState.onNext(post);
}
@@ -0,0 +1,26 @@
package tn.mnlr.vripper.services;
import lombok.Getter;
@Getter
public class DownloadSpeed {
private final String type = "downSpeed";
private String speed;
public DownloadSpeed(long bytes) {
speed = formatSI(bytes);
}
private String formatSI(long bytes) {
return humanReadableByteCount(bytes, true);
}
private String humanReadableByteCount(long bytes, boolean si) {
int unit = si ? 1000 : 1024;
if (bytes < unit) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(unit));
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}
}
@@ -0,0 +1,28 @@
package tn.mnlr.vripper.services;
import io.reactivex.processors.PublishProcessor;
import lombok.Getter;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicLong;
@Service
@EnableScheduling
public class DownloadSpeedService {
private AtomicLong read = new AtomicLong(0);
@Getter
private PublishProcessor<Long> readBytesPerSecond = PublishProcessor.create();
public void increase(long read) {
this.read.addAndGet(read);
}
@Scheduled(fixedDelay = 1000)
private void calc() {
readBytesPerSecond.onNext(read.getAndSet(0));
}
}
@@ -0,0 +1,39 @@
package tn.mnlr.vripper.services;
import lombok.Getter;
import java.util.Objects;
@Getter
public class GlobalState {
private final String type = "globalState";
private long running;
private long queued;
private long remaining;
private long error;
public GlobalState(long running, long queued, long remaining, long error) {
this.running = running;
this.queued = queued;
this.remaining = remaining;
this.error = error;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GlobalState that = (GlobalState) o;
return running == that.running &&
queued == that.queued &&
remaining == that.remaining &&
error == that.error &&
Objects.equals(type, that.type);
}
@Override
public int hashCode() {
return Objects.hash(type, running, queued, remaining, error);
}
}
@@ -0,0 +1,52 @@
package tn.mnlr.vripper.services;
import io.reactivex.processors.PublishProcessor;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.q.DownloadQ;
import tn.mnlr.vripper.q.ExecutionService;
@Service
@EnableScheduling
public class GlobalStateService {
@Autowired
private DownloadQ downloadQ;
@Autowired
private ExecutionService executionService;
@Autowired
private AppStateService appStateService;
@Getter
private GlobalState currentState;
@Getter
private PublishProcessor<GlobalState> liveGlobalState = PublishProcessor.create();
@Scheduled(fixedDelay = 3000)
private void interval() {
GlobalState newGlobalState = new GlobalState(
executionService.runningCount(),
downloadQ.size(),
appStateService.getCurrentImages()
.values()
.stream()
.filter(e -> e.getTotal() == 0 || e.getTotal() != e.getCurrent().get())
.count(),
appStateService.getCurrentImages()
.values()
.stream()
.filter(e -> e.getStatus().equals(Image.Status.ERROR))
.count());
if (!newGlobalState.equals(currentState)) {
currentState = newGlobalState;
liveGlobalState.onNext(currentState);
}
}
}
@@ -16,9 +16,10 @@ import tn.mnlr.vripper.entities.mixin.persistance.PostPersistanceMixin;
import javax.annotation.PreDestroy;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
@@ -48,6 +49,9 @@ public class PersistenceService {
try {
if(dataFile.createNewFile()) {
logger.info("Data file successfully created");
try (FileWriter fw = new FileWriter(dataFile)) {
fw.write("{}");
}
} else {
logger.info("Data file already exists");
}
@@ -100,7 +104,7 @@ public class PersistenceService {
String jsonContent;
try {
jsonContent = Files.readAllLines(Paths.get(VripperApplication.dataPath), Charset.forName("UTF-8")).stream().collect(Collectors.joining());
jsonContent = Files.readAllLines(Paths.get(VripperApplication.dataPath), StandardCharsets.UTF_8).stream().collect(Collectors.joining());
} catch (Exception e) {
logger.warn("data file not found, previous state cannot be restored", e);
return;
@@ -29,7 +29,8 @@ public class PostParser {
private final static String POSTS_XPATH = "//li[contains(@id,'post_')][not(contains(@id,'post_thank'))]";
private final static String REAL_THREAD_XPATH = ".//a[@class='postcounter']";
private final static String THREAD_TITLE_XPATH = "//li[contains(@class, 'lastnavbit')]/span";
private final static String POST_TITLE_XPATH = ".//h2";
private final static String POST_TITLE_XPATH = ".//h2[contains(@class, 'title')]";
private final static String POST_COUNTER_XPATH = ".//a[contains(@class, 'postcounter')]";
private final static String POST_LINKS_XPATH = ".//a";
@Autowired
@@ -147,6 +148,20 @@ public class PostParser {
throw new PostParseException(e);
}
String postCounter;
try {
logger.info(String.format("Finding post's counter"));
Node counterNode = xpathService.getAsNode(postsNodeList.item(i), POST_COUNTER_XPATH);
if (counterNode != null) {
postCounter = counterNode.getTextContent().trim();
logger.info(String.format("Found post's counter: %s", postCounter));
} else {
postCounter = "";
}
} catch (Exception e) {
throw new PostParseException(e);
}
ArrayList<Image> imagesList = new ArrayList<>();
try {
logger.info(String.format("Finding all links for post with id %s using xpath %s", postId, POST_LINKS_XPATH));
@@ -177,7 +192,7 @@ public class PostParser {
if (!imagesList.isEmpty()) {
logger.info(String.format("Found %d images for post with id %s", imagesList.size(), postId));
posts.add(appStateService.createPost(postTitle, realUrl, imagesList, null, postId));
posts.add(appStateService.createPost(postTitle, realUrl, imagesList, null, postId, postCounter));
} else {
logger.warn(String.format("No images found for post with id %s, skipping", postId));
}
@@ -90,12 +90,24 @@ public class PostRestEndpoint {
downloadQ.restart(postId.getPostId());
}
@PostMapping("/post/restart/all")
@ResponseStatus(value = HttpStatus.OK)
public void restartPost() throws Exception {
downloadQ.restartAll();
}
@PostMapping("/post/stop")
@ResponseStatus(value = HttpStatus.OK)
public void stop(@RequestBody PostId postId) {
downloadQ.stop(postId.getPostId());
}
@PostMapping("/post/stop/all")
@ResponseStatus(value = HttpStatus.OK)
public void stopAll() {
downloadQ.stopAll();
}
@PostMapping("/post/remove")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity remove(@RequestBody PostId postId) {
@@ -17,8 +17,12 @@ import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.entities.mixin.ui.ImageUIMixin;
import tn.mnlr.vripper.entities.mixin.ui.PostUIMixin;
import tn.mnlr.vripper.services.AppStateService;
import tn.mnlr.vripper.services.DownloadSpeed;
import tn.mnlr.vripper.services.DownloadSpeedService;
import tn.mnlr.vripper.services.GlobalStateService;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@@ -30,37 +34,38 @@ public class WebSocketHandler extends TextWebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);
@Getter
private static class WSMessage {
private String cmd;
private String payload;
enum CMD {
POSTS_SUB,
POST_DETAILS_SUB,
POSTS_UNSUB,
POST_DETAILS_UNSUB
}
}
public WebSocketHandler() {
om.addMixIn(Image.class, ImageUIMixin.class).addMixIn(Post.class, PostUIMixin.class);
}
private Map<String, Disposable> postsSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> postDetailsSubscriptions = new ConcurrentHashMap<>();
private ObjectMapper om = new ObjectMapper();
@Autowired
private GlobalStateService globalStateService;
@Autowired
private AppStateService appStateService;
@Autowired
private DownloadSpeedService downloadSpeedService;
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 ObjectMapper om = new ObjectMapper();
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
WSMessage wsMessage = om.readValue(message.getPayload(), WSMessage.class);
WSMessage.CMD cmd = WSMessage.CMD.valueOf(wsMessage.getCmd());
switch (cmd) {
case GLOBAL_STATE_SUB:
subscribeForGlobalState(session);
break;
case SPEED_SUB:
subscribeForSpeed(session);
break;
case POSTS_SUB:
subscribeForPosts(session);
break;
@@ -75,9 +80,70 @@ public class WebSocketHandler extends TextWebSocketHandler {
logger.info(String.format("Client %s unsubscribed from posts", session.getId()));
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
break;
case GLOBAL_STATE_UNSUB:
logger.info(String.format("Client %s unsubscribed from global state", session.getId()));
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
break;
case SPEED_UNSUB:
logger.info(String.format("Client %s unsubscribed from download speed info", session.getId()));
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
break;
}
}
private void subscribeForGlobalState(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for global state", session.getId()));
if (stateSubscriptions.containsKey(session.getId())) {
stateSubscriptions.get(session.getId()).dispose();
}
try {
send(session, new TextMessage(om.writeValueAsString(Arrays.asList(globalStateService.getCurrentState()))));
} catch (Exception e) {
logger.error("Unexpected error occurred", e);
}
stateSubscriptions.put(session.getId(),
globalStateService.getLiveGlobalState()
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.map(Arrays::asList)
.filter(e -> !e.isEmpty())
.map(e -> e.subList(0, 1))
.map(om::writeValueAsString)
.map(TextMessage::new)
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
);
}
private void subscribeForSpeed(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for download speed info", session.getId()));
if (downloadSpeedSubscriptions.containsKey(session.getId())) {
downloadSpeedSubscriptions.get(session.getId()).dispose();
}
try {
send(session, new TextMessage(om.writeValueAsString(Arrays.asList(new DownloadSpeed(0)))));
} catch (Exception e) {
logger.error("Unexpected error occurred", e);
}
downloadSpeedSubscriptions.put(session.getId(),
downloadSpeedService.getReadBytesPerSecond()
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.map(Arrays::asList)
.filter(e -> !e.isEmpty())
.map(e -> e.subList(0, 1))
.map(e -> e.stream().map(DownloadSpeed::new).collect(Collectors.toList()))
.map(om::writeValueAsString)
.map(TextMessage::new)
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
);
}
private void subscribeForPosts(WebSocketSession session) {
logger.info(String.format("Client %s subscribed for posts", session.getId()));
@@ -151,5 +217,25 @@ public class WebSocketHandler extends TextWebSocketHandler {
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
}
@Getter
private static class WSMessage {
private String cmd;
private String payload;
enum CMD {
POSTS_SUB,
POST_DETAILS_SUB,
POSTS_UNSUB,
POST_DETAILS_UNSUB,
GLOBAL_STATE_SUB,
GLOBAL_STATE_UNSUB,
SPEED_SUB,
SPEED_UNSUB
}
}
}
@@ -2,4 +2,7 @@ logging.level.org.springframework.web=INFO
logging.level.root=INFO
logging.level.org.apache.http=INFO
logging.file=${user.dir}/vripper.log
server.port=${vripper.server.port:8080}
server.port=${vripper.server.port:8080}
management.endpoints.web.exposure.include=shutdown
management.endpoint.shutdown.enabled=true
endpoints.shutdown.enabled=true
+302 -207
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "1.4.3",
"version": "1.4.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1759,12 +1759,6 @@
"integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==",
"dev": true
},
"array-slice": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
"integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
"dev": true
},
"array-union": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
@@ -2226,31 +2220,59 @@
"dev": true
},
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"dev": true,
"requires": {
"bytes": "3.0.0",
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"dev": true
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
"dev": true
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
}
}
},
@@ -2637,12 +2659,6 @@
"integrity": "sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA==",
"dev": true
},
"circular-json": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz",
"integrity": "sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ==",
"dev": true
},
"class-utils": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -2798,15 +2814,6 @@
"integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
"dev": true
},
"combine-lists": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz",
"integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=",
"dev": true,
"requires": {
"lodash": "^4.5.0"
}
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
@@ -2903,36 +2910,21 @@
}
},
"connect": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
"integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz",
"integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==",
"dev": true,
"requires": {
"debug": "2.6.9",
"finalhandler": "1.1.0",
"parseurl": "~1.3.2",
"finalhandler": "1.1.2",
"parseurl": "~1.3.3",
"utils-merge": "1.0.1"
},
"dependencies": {
"finalhandler": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
"integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
"dev": true,
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.1",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.3.1",
"unpipe": "~1.0.0"
}
},
"statuses": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"dev": true
}
}
@@ -3202,9 +3194,9 @@
}
},
"date-format": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
"integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
"integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==",
"dev": true
},
"date-now": {
@@ -3879,56 +3871,6 @@
"integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
"dev": true
},
"expand-braces": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz",
"integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=",
"dev": true,
"requires": {
"array-slice": "^0.2.3",
"array-unique": "^0.2.1",
"braces": "^0.1.2"
},
"dependencies": {
"array-unique": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
"integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
"dev": true
},
"braces": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz",
"integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=",
"dev": true,
"requires": {
"expand-range": "^0.1.0"
}
},
"expand-range": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz",
"integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=",
"dev": true,
"requires": {
"is-number": "^0.1.1",
"repeat-string": "^0.2.2"
}
},
"is-number": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz",
"integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=",
"dev": true
},
"repeat-string": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz",
"integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=",
"dev": true
}
}
},
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -4401,9 +4343,9 @@
}
},
"flatted": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
"integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
"integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
"dev": true
},
"flush-write-stream": {
@@ -6075,28 +6017,27 @@
}
},
"karma": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/karma/-/karma-3.1.4.tgz",
"integrity": "sha512-31Vo8Qr5glN+dZEVIpnPCxEGleqE0EY6CtC2X9TagRV3rRQ3SNrvfhddICkJgUK3AgqpeKSZau03QumTGhGoSw==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/karma/-/karma-4.2.0.tgz",
"integrity": "sha512-fmCuxN1rwJxTdZfOXK5LjlmS4Ana/OvzNMpkyLL/TLE8hmgSkpVpMYQ7RTVa8TNKRVQDZNl5W1oF5cfKfgIMlA==",
"dev": true,
"requires": {
"bluebird": "^3.3.0",
"body-parser": "^1.16.1",
"chokidar": "^2.0.3",
"braces": "^3.0.2",
"chokidar": "^3.0.0",
"colors": "^1.1.0",
"combine-lists": "^1.0.0",
"connect": "^3.6.0",
"core-js": "^2.2.0",
"core-js": "^3.1.3",
"di": "^0.0.1",
"dom-serialize": "^2.2.0",
"expand-braces": "^0.1.1",
"flatted": "^2.0.0",
"glob": "^7.1.1",
"graceful-fs": "^4.1.2",
"http-proxy": "^1.13.0",
"isbinaryfile": "^3.0.0",
"lodash": "^4.17.5",
"log4js": "^3.0.0",
"lodash": "^4.17.11",
"log4js": "^4.0.0",
"mime": "^2.3.1",
"minimatch": "^3.0.2",
"optimist": "^0.6.1",
@@ -6110,17 +6051,137 @@
"useragent": "2.3.0"
},
"dependencies": {
"mime": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
"integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
"anymatch": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.0.3.tgz",
"integrity": "sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==",
"dev": true,
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
}
},
"binary-extensions": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
"integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
"dev": true
},
"braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"requires": {
"fill-range": "^7.0.1"
}
},
"chokidar": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.0.2.tgz",
"integrity": "sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==",
"dev": true,
"requires": {
"anymatch": "^3.0.1",
"braces": "^3.0.2",
"fsevents": "^2.0.6",
"glob-parent": "^5.0.0",
"is-binary-path": "^2.1.0",
"is-glob": "^4.0.1",
"normalize-path": "^3.0.0",
"readdirp": "^3.1.1"
}
},
"core-js": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz",
"integrity": "sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ==",
"dev": true
},
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"requires": {
"to-regex-range": "^5.0.1"
}
},
"fsevents": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz",
"integrity": "sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==",
"dev": true,
"optional": true
},
"glob-parent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz",
"integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==",
"dev": true,
"requires": {
"is-glob": "^4.0.1"
}
},
"is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"requires": {
"binary-extensions": "^2.0.0"
}
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"mime": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz",
"integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==",
"dev": true
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"readdirp": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.1.tgz",
"integrity": "sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ==",
"dev": true,
"requires": {
"picomatch": "^2.0.4"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"requires": {
"is-number": "^7.0.0"
}
}
}
},
@@ -6294,9 +6355,9 @@
}
},
"lodash": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"lodash.clonedeep": {
@@ -6318,31 +6379,31 @@
"dev": true
},
"log4js": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-3.0.6.tgz",
"integrity": "sha512-ezXZk6oPJCWL483zj64pNkMuY/NcRX5MPiB0zE6tjZM137aeusrOnW1ecxgF9cmwMWkBMhjteQxBPoZBh9FDxQ==",
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/log4js/-/log4js-4.5.1.tgz",
"integrity": "sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==",
"dev": true,
"requires": {
"circular-json": "^0.5.5",
"date-format": "^1.2.0",
"debug": "^3.1.0",
"rfdc": "^1.1.2",
"streamroller": "0.7.0"
"date-format": "^2.0.0",
"debug": "^4.1.1",
"flatted": "^2.0.0",
"rfdc": "^1.1.4",
"streamroller": "^1.0.6"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
@@ -6753,9 +6814,9 @@
}
},
"mixin-deep": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
"integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
"integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"dev": true,
"requires": {
"for-in": "^1.0.2",
@@ -7782,6 +7843,12 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
},
"picomatch": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz",
"integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==",
"dev": true
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@@ -8264,25 +8331,47 @@
"dev": true
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"dev": true,
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"dev": true
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==",
"dev": true
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
}
}
},
@@ -8633,9 +8722,9 @@
"dev": true
},
"rfdc": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.2.tgz",
"integrity": "sha512-92ktAgvZhBzYTIK0Mja9uen5q5J3NRVMoDkJL2VMwq6SXjVCgqvQeVP2XAaUY6HT+XpQYeLSjb3UoitBryKmdA==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz",
"integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==",
"dev": true
},
"rimraf": {
@@ -8950,9 +9039,9 @@
"dev": true
},
"set-value": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
"integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
"integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
@@ -9652,15 +9741,16 @@
"dev": true
},
"streamroller": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-0.7.0.tgz",
"integrity": "sha512-WREzfy0r0zUqp3lGO096wRuUp7ho1X6uo/7DJfTlEi0Iv/4gT7YHqXDjKC2ioVGBZtE8QzsQD9nx1nIuoZ57jQ==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/streamroller/-/streamroller-1.0.6.tgz",
"integrity": "sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==",
"dev": true,
"requires": {
"date-format": "^1.2.0",
"debug": "^3.1.0",
"mkdirp": "^0.5.1",
"readable-stream": "^2.3.0"
"async": "^2.6.2",
"date-format": "^2.0.0",
"debug": "^3.2.6",
"fs-extra": "^7.0.1",
"lodash": "^4.17.14"
},
"dependencies": {
"debug": {
@@ -9672,10 +9762,21 @@
"ms": "^2.1.1"
}
},
"fs-extra": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
@@ -10311,13 +10412,30 @@
"dev": true
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
"integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.18"
"mime-types": "~2.1.24"
},
"dependencies": {
"mime-db": {
"version": "1.40.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
"dev": true
},
"mime-types": {
"version": "2.1.24",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
"dev": true,
"requires": {
"mime-db": "1.40.0"
}
}
}
},
"typedarray": {
@@ -10359,38 +10477,15 @@
"dev": true
},
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
"integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
"integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"dev": true,
"requires": {
"arr-union": "^3.1.0",
"get-value": "^2.0.6",
"is-extendable": "^0.1.1",
"set-value": "^0.4.3"
},
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": {
"is-extendable": "^0.1.0"
}
},
"set-value": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
"integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
"is-extendable": "^0.1.1",
"is-plain-object": "^2.0.1",
"to-object-path": "^0.3.0"
}
}
"set-value": "^2.0.1"
}
},
"unique-filename": {
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "vripper-ui",
"version": "1.4.3",
"version": "1.4.7",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build-electron": "ng build --base-href ./",
"prod": "ng build --prod",
"build-electron": "ng build --prod --base-href ./",
"build-web": "ng build --prod",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
@@ -45,7 +45,7 @@
"electron": "^5.0.5",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~3.1.1",
"karma": "^4.2.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~1.1.2",
+96 -31
View File
@@ -5,46 +5,111 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.3</version>
<version>1.4.7</version>
</parent>
<artifactId>vripper-ui</artifactId>
<name>vripper-ui</name>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.3</version>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<nodeVersion>v12.2.0</nodeVersion>
<npmVersion>6.9.0</npmVersion>
<workingDirectory>build-dir</workingDirectory>
<classifier>${buildClassifier}</classifier>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run prod</arguments>
</configuration>
<phase>generate-resources</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>web</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<buildClassifier>web</buildClassifier>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.3</version>
<configuration>
<nodeVersion>v12.2.0</nodeVersion>
<npmVersion>6.9.0</npmVersion>
<workingDirectory>build-dir</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build-web</arguments>
</configuration>
<phase>generate-resources</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>electron</id>
<properties>
<buildClassifier>electron</buildClassifier>
</properties>
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.3</version>
<configuration>
<nodeVersion>v12.2.0</nodeVersion>
<npmVersion>6.9.0</npmVersion>
<workingDirectory>build-dir</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build-electron</arguments>
</configuration>
<phase>generate-resources</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
+3 -3
View File
@@ -12,6 +12,7 @@ import { ServerService } from './server-service';
export class ClipboardService {
private links$: Subject<string> = new Subject();
private interval: NodeJS.Timer;
private lastText = '';
constructor(
private electronService: ElectronService,
private serverService: ServerService,
@@ -47,12 +48,11 @@ export class ClipboardService {
}
const clipboard: Clipboard = this.electronService.clipboard;
let lastText = '';
this.interval = setInterval(() => {
const text = clipboard.readText();
if (this.textHasDiff(text, lastText)) {
lastText = text;
if (this.textHasDiff(text, this.lastText)) {
this.lastText = text;
if (text.indexOf('https://vipergirls.to/threads') !== -1) {
this.links$.next(text);
}
+5 -1
View File
@@ -1,6 +1,10 @@
export enum CMD {
POSTS_SUB = 'POSTS_SUB',
POST_DETAILS_SUB = 'POST_DETAILS_SUB',
GLOBAL_STATE_SUB = 'GLOBAL_STATE_SUB',
SPEED_SUB = 'SPEED_SUB',
POSTS_UNSUB = 'POSTS_UNSUB',
POST_DETAILS_UNSUB = 'POST_DETAILS_UNSUB'
POST_DETAILS_UNSUB = 'POST_DETAILS_UNSUB',
GLOBAL_STATE_UNSUB = 'GLOBAL_STATE_UNSUB',
SPEED_UNSUB = 'SPEED_UNSUB'
}
@@ -0,0 +1,3 @@
export class DownloadSpeed {
constructor(public speed: string) {}
}
@@ -0,0 +1,3 @@
export class GlobalState {
constructor(public running: number, public queued: number, public remaining: number, public error: number) {}
}
+18 -8
View File
@@ -19,18 +19,28 @@
</div>
</form>
</div>
<div fxLayout="row" fxLayoutAlign="end" fxLayoutGap="10px">
<button (click)="clear()" aria-label="Clear completed" color="primary" mat-stroked-button title="Clear completed">
Clear completed
<div fxLayout="row" fxLayoutAlign="end" fxLayoutGap="10px" id="controls">
<button (click)="restartAll()" aria-label="Start" color="primary" mat-mini-fab title="Start">
<mat-icon>play_arrow</mat-icon>
</button>
<button (click)="stopAll()" aria-label="Stop" color="primary" mat-mini-fab title="Stop">
<mat-icon>stop</mat-icon>
</button>
<div fxFlex="grow"></div>
<button (click)="clear()" aria-label="Clear completed" color="primary" mat-mini-fab title="Clear completed">
<mat-icon>clear_all</mat-icon>
</button>
<button (click)="remove()" aria-label="Remove All" color="primary" mat-stroked-button title="Remove All">
Remove All
<button (click)="remove()" aria-label="Remove All" color="primary" mat-mini-fab title="Remove All">
<mat-icon>delete_forever</mat-icon>
</button>
</div>
<div fxLayout="column" fxFlex="grow">
<app-posts [ngStyle]="{'height': electronService.isElectronApp ? 'calc(100% - 27px)' : '100%'}" fxFlex="nogrow"
style="width: 100%; overflow-x: hidden"></app-posts>
<mat-divider></mat-divider>
<div [fxFlex]="electronService.isElectronApp ? '0 0 calc(100% - 190px)' : '0 0 calc(100% - 163px)'">
<app-posts [ngStyle]="{'height': electronService.isElectronApp ? 'calc(100% - 37px)' : '100%'}"
style="width: 100%;"></app-posts>
</div>
<div style="text-align: end">{{ downloadSpeed!.speed + '/s' }} | Downloading: {{ globalState!.running }} | Queued: {{
globalState!.queued }} | Remaining:
{{ globalState!.remaining }} | Error: {{ globalState!.error }}
</div>
</div>
@@ -0,0 +1,4 @@
div#controls {
margin-left: 10px;
margin-right: 10px;
}
+145 -64
View File
@@ -1,37 +1,107 @@
import { GlobalState } from './../common/global-state.model';
import { ElectronService } from 'ngx-electron';
import { ClipboardService } from './../clipboard.service';
import { ParseResponse } from './../common/parse-response.model';
import { Component, OnInit, ViewChild, Inject } from '@angular/core';
import { Component, OnInit, ViewChild, Inject, NgZone, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { finalize, filter, flatMap } from 'rxjs/operators';
import { MatSnackBar, MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material';
import { MatSnackBar, MatDialog } from '@angular/material';
import { NgForm } from '@angular/forms';
import { ServerService } from '../server-service';
import { RemoveAllResponse } from '../common/remove-all-response.model';
import { PostsComponent } from '../posts/posts.component';
import { ConfirmDialogComponent } from '../common/confirmation-component/confirmation-dialog';
import { WsConnectionService } from '../ws-connection.service';
import { WsHandler } from '../ws-handler';
import { Subscription } from 'rxjs';
import { WSMessage } from '../common/ws-message.model';
import { CMD } from '../common/cmd.enum';
import { DownloadSpeed } from '../common/download-speed.model';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
export class HomeComponent implements OnInit, OnDestroy {
@ViewChild(PostsComponent)
private postsComponent: PostsComponent;
loading = false;
input: string;
constructor(
private httpClient: HttpClient,
private _snackBar: MatSnackBar,
private serverService: ServerService,
private clipboardService: ClipboardService,
public dialog: MatDialog,
public electronService: ElectronService
public electronService: ElectronService,
private ngZone: NgZone,
private wsConnectionService: WsConnectionService,
) {
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
}
websocketHandlerPromise: Promise<WsHandler>;
subscriptions: Subscription[] = [];
globalState: GlobalState = new GlobalState(0, 0, 0, 0);
downloadSpeed: DownloadSpeed = new DownloadSpeed('0 B');
ngOnInit() {
this.clipboardService.links.subscribe(e => {
this.ngZone.run(() => {
this.loading = true;
this.httpClient
.post<ParseResponse>(this.serverService.baseUrl + '/clipboard/post', { url: e })
.pipe(
finalize(() => {
this.loading = false;
})
)
.subscribe(
data => {
this._snackBar.open(`${data.parsed} posts parsed`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
});
this.websocketHandlerPromise.then((handler: WsHandler) => {
console.log('Connecting to global state and download speed');
this.subscriptions.push(
handler.subscribeForGlobalState((e: GlobalState[]) => {
this.ngZone.run(() => {
this.globalState = e[0];
});
})
);
this.subscriptions.push(
handler.subscribeForSpeed((e: DownloadSpeed[]) => {
this.ngZone.run(() => {
this.downloadSpeed = e[0];
});
})
);
handler.send(new WSMessage(CMD.GLOBAL_STATE_SUB.toString()));
handler.send(new WSMessage(CMD.SPEED_SUB.toString()));
});
}
submit(form: NgForm) {
this.ngZone.run(() => {
this.loading = true;
this.httpClient
.post<ParseResponse>(this.serverService.baseUrl + '/clipboard/post', { url: e })
.post<ParseResponse>(this.serverService.baseUrl + '/post', { url: this.input })
.pipe(
finalize(() => {
this.loading = false;
this.input = null;
form.resetForm();
})
)
.subscribe(
@@ -47,28 +117,12 @@ export class HomeComponent implements OnInit {
});
}
@ViewChild(PostsComponent)
private postsComponent: PostsComponent;
loading = false;
input: string;
ngOnInit() {}
submit(form: NgForm) {
this.loading = true;
this.httpClient
.post<ParseResponse>(this.serverService.baseUrl + '/post', { url: this.input })
.pipe(
finalize(() => {
this.loading = false;
this.input = null;
form.resetForm();
})
)
.subscribe(
clear() {
this.ngZone.run(() => {
this.httpClient.post<RemoveAllResponse>(this.serverService.baseUrl + '/post/clear/all', {}).subscribe(
data => {
this._snackBar.open(`${data.parsed} posts parsed`, null, { duration: 5000 });
this.postsComponent.removeRows(data.postIds);
this._snackBar.open(`${data.removed} items cleared`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
@@ -76,47 +130,74 @@ export class HomeComponent implements OnInit {
});
}
);
}
clear() {
this.httpClient.post<RemoveAllResponse>(this.serverService.baseUrl + '/post/clear/all', {}).subscribe(
data => {
this.postsComponent.removeRows(data.postIds);
this._snackBar.open(`${data.removed} items cleared`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}
remove() {
this.ngZone.run(() => {
this.dialog
.open(ConfirmDialogComponent, {
maxHeight: '100vh',
maxWidth: '100vw',
height: '200px',
width: '60%',
data: { header: 'Confirmation', content: 'Are you sure you want to remove all items ?' }
})
.afterClosed()
.pipe(
filter(e => e === 'yes'),
flatMap(e => this.httpClient.post<RemoveAllResponse>(this.serverService.baseUrl + '/post/remove/all', {}))
)
.subscribe(
data => {
this.postsComponent.removeRows(data.postIds);
this._snackBar.open(`${data.removed} items removed`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}
this.dialog
.open(ConfirmDialogComponent, {
maxHeight: '100vh',
maxWidth: '100vw',
height: '200px',
width: '60%',
data: {header: 'Confirmation', content: 'Are you sure you want to remove all items ?'}
})
.afterClosed()
.pipe(
filter(e => e === 'yes'),
flatMap(e => this.httpClient.post<RemoveAllResponse>(this.serverService.baseUrl + '/post/remove/all', {}))
)
.subscribe(
data => {
this.postsComponent.removeRows(data.postIds);
this._snackBar.open(`${data.removed} items removed`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
stopAll() {
this.ngZone.run(() => {
this.httpClient.post(this.serverService.baseUrl + '/post/stop/all', {})
.subscribe(
() => {
this._snackBar.open(`Download stopped`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}
restartAll() {
this.ngZone.run(() => {
this.httpClient.post(this.serverService.baseUrl + '/post/restart/all', {})
.subscribe(
() => {
this._snackBar.open(`Download started`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
});
}
ngOnDestroy() {
this.subscriptions.forEach(e => e.unsubscribe());
this.websocketHandlerPromise.then((handler: WsHandler) => {
handler.send(new WSMessage(CMD.GLOBAL_STATE_UNSUB.toString()));
});
}
}
+4 -2
View File
@@ -14,7 +14,8 @@ import {
MatSlideToggleModule,
MatSnackBarModule,
MatSnackBar,
MatTabsModule
MatTabsModule,
MatDividerModule
} from '@angular/material';
@NgModule({
@@ -32,7 +33,8 @@ import {
MatCardModule,
MatSlideToggleModule,
MatSnackBarModule,
MatTabsModule
MatTabsModule,
MatDividerModule
],
providers: [MatSnackBar]
})
@@ -30,6 +30,7 @@ export class PostDetailComponent implements OnInit, OnDestroy {
cellClass: 'no-padding'
}
],
enableBrowserTooltips: true,
rowHeight: 48,
rowData: [],
frameworkComponents: {
@@ -10,67 +10,96 @@ import { ICellRendererParams } from 'ag-grid-community';
selector: 'app-progress-cell',
template: `
<div style="height: 100%;">
<div class="progress-text" fxLayout="row" fxLayoutAlign="space-between">
<div>{{ postState.title + postState.postCounter }}</div>
<div>{{ trunc(postState.progress) + '%' }}</div>
</div>
<div
class="progress-bar"
[ngClass]="{
complete: postState.status === 'COMPLETE',
downloading: postState.status === 'DOWNLOADING',
stopped: postState.status === 'STOPPED',
partial: postState.status === 'PARTIAL'
'complete': postState.status === 'COMPLETE',
'downloading': postState.status === 'DOWNLOADING',
'stopped': postState.status === 'STOPPED',
'partial': postState.status === 'PARTIAL',
'error': postState.status === 'ERROR',
'pending': postState.status === 'PENDING'
}"
[style.width]="trunc(postState.progress) + '%'"
></div>
<div
class="progress-bar-back"
fxLayout="row"
fxLayoutAlign="space-between"
[ngClass]="{ error: postState.status === 'ERROR', pending: postState.status === 'PENDING' }"
>
<div>{{ postState.title }}</div>
<div>{{ trunc(postState.progress) + '%' }}</div>
</div>
[ngClass]="{
'complete-back': postState.status === 'COMPLETE',
'downloading-back': postState.status === 'DOWNLOADING',
'stopped-back': postState.status === 'STOPPED',
'partial-back': postState.status === 'PARTIAL',
'error-back': postState.status === 'ERROR',
'pending-back': postState.status === 'PENDING'
}"
></div>
</div>
`,
styles: [
`
.progress-bar-back {
.progress-text {
position: relative;
height: 45px;
bottom: 45px;
padding: 0 8px;
transition: background-color 0.5s;
z-index: 2;
}
.progress-bar {
background-color: #87a2c7;
position: relative;
height: 100%;
bottom: 45px;
transition: width 0.5s, background-color 0.5s;
z-index: 1;
}
.progress-bar-back {
position: relative;
height: 100%;
bottom: 90px;
transition: background-color 0.5s;
}
.complete {
background-color: #3865a3;
}
.complete-back {
background-color: lightgrey;
}
.pending {
background-color: white;
}
.pending-back {
background-color: lightgrey;
}
.error {
background-color: #eb6060;
}
.error-back {
background-color: #eb6060;
}
.downloading {
background-color: #87a2c7;
}
.downloading-back {
background-color: white;
}
.stopped {
background-color: grey;
}
.stopped-back {
background-color: lightgrey;
}
.partial {
background-color: #ffcc00;
background-color: #ffcc00;
}
.partial-back {
background-color: white;
}
`
]
})
export class PostProgressRendererComponent implements AgRendererComponent, OnInit, OnDestroy {
constructor(
private wsConnectionService: WsConnectionService,
private zone: NgZone
) {
constructor(private wsConnectionService: WsConnectionService, private zone: NgZone) {
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
}
@@ -2,6 +2,7 @@ export class PostState {
constructor(
public type: string,
public postId: string,
public postCounter: string,
public title: string,
public progress: number,
public status: string,
@@ -37,6 +37,7 @@ export class PostsComponent implements OnInit, OnDestroy {
suppressAutoSize: true
}
],
enableBrowserTooltips: true,
rowHeight: 48,
rowData: [],
frameworkComponents: {
+50
View File
@@ -3,10 +3,59 @@ import { WSMessage } from './common/ws-message.model';
import { PostState } from './posts/post-state.model';
import { map, filter } from 'rxjs/operators';
import { PostDetails } from './post-detail/post-details.model';
import { GlobalState } from './common/global-state.model';
import { DownloadSpeed } from './common/download-speed.model';
export class WsHandler {
constructor(private websocket: Subject<any>) {}
subscribeForGlobalState(callback: (stateStream: Array<GlobalState>) => void): Subscription {
return this.websocket
.pipe(
map(e => JSON.parse(e)),
filter(e => e.length > 0 && e.filter(v => v.type === 'globalState').length > 0),
map(e => {
const state: Array<GlobalState> = [];
(<Array<any>>e).forEach(element => {
state.push(
new GlobalState(
element.running,
element.queued,
element.remaining,
element.error
)
);
});
return state;
})
)
.subscribe(e => {
callback(e);
});
}
subscribeForSpeed(callback: (speedStream: Array<DownloadSpeed>) => void): Subscription {
return this.websocket
.pipe(
map(e => JSON.parse(e)),
filter(e => e.length > 0 && e.filter(v => v.type === 'downSpeed').length > 0),
map(e => {
const speed: Array<DownloadSpeed> = [];
(<Array<any>>e).forEach(element => {
speed.push(
new DownloadSpeed(
element.speed
)
);
});
return speed;
})
)
.subscribe(e => {
callback(e);
});
}
subscribeForPosts(callback: (postStream: Array<PostState>) => void): Subscription {
return this.websocket
.pipe(
@@ -19,6 +68,7 @@ export class WsHandler {
new PostState(
element.type,
element.postId,
element.postCounter,
element.title,
element.done === 0 && element.total === 0 ? 0 : (element.done / element.total) * 100,
element.status,
+8
View File
@@ -65,3 +65,11 @@ div.ag-cell.no-padding {
text-overflow:ellipsis;
white-space: nowrap;
}
.ag-body-horizontal-scroll {
display: none !important;
}
.ag-center-cols-viewport {
overflow: hidden !important;
}