Compare commits

..
10 Commits
Author SHA1 Message Date
death-claw eeb24ae7c1 v1.4.8 2019-08-13 14:39:41 +01:00
death-claw 9b9c5ac5c8 Add option to force image ordering 2019-08-13 14:34:46 +01:00
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
31 changed files with 301 additions and 45 deletions
+20
View File
@@ -1,5 +1,25 @@
# Changelog
## [1.4.8] - 2019-08-13
### Changed
- Add option to force image ordering
## [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
+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.
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.4</version>
<version>1.4.8</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
+2
View File
@@ -41,6 +41,8 @@ function createWindow() {
win = new BrowserWindow({
width: 1024,
height: 768,
minWidth: 640,
minHeight: 480,
frame: false,
webPreferences: {
nodeIntegration: true
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "1.4.4",
"version": "1.4.8",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "1.4.4",
"version": "1.4.8",
"description": "",
"main": "main.js",
"author": "",
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.4</version>
<version>1.4.8</version>
</parent>
<artifactId>vripper-electron</artifactId>
<name>vripper-electron</name>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.4</version>
<version>1.4.8</version>
</parent>
<artifactId>vripper-server</artifactId>
<name>vripper-server</name>
@@ -9,7 +9,6 @@ import tn.mnlr.vripper.host.Host;
import tn.mnlr.vripper.services.AppStateService;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Getter
@@ -30,6 +29,8 @@ public class Image {
private String url;
private int index;
private AtomicLong current = new AtomicLong(0);
private Status status;
private BehaviorProcessor<Image> imageStateProcessor;
@@ -37,12 +38,13 @@ public class Image {
@Setter
private long total = 0;
public Image(String url, String postId, String postName, Host host, AppStateService appStateService) {
public Image(String url, String postId, String postName, Host host, AppStateService appStateService, int index) {
this.url = url;
this.postId = postId;
this.postName = postName;
this.host = host;
this.appStateService = appStateService;
this.index = index;
status = Status.PENDING;
imageStateProcessor = BehaviorProcessor.create();
appStateService.getCurrentImages().put(this.url, this);
@@ -16,15 +16,16 @@ 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.AppSettingsService;
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.getImageName().toLowerCase().endsWith(".jpeg")) {
imageFileData.setImageName(imageFileData.getImageName() + ".jpg");
}
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(), image.getIndex());
}
}
} catch (Exception e) {
@@ -121,6 +123,41 @@ abstract public class Host {
}
}
private void checkImageTypeAndRename(File outputFile, String imageName, int index) throws HostException {
try (ImageInputStream iis = ImageIO.createImageInputStream(outputFile)) {
Iterator<ImageReader> it = ImageIO.getImageReaders(iis);
if (!it.hasNext()) {
throw new HostException("Image file is not recognized!");
}
ImageReader reader = it.next();
String formatName = reader.getFormatName();
if (formatName.toUpperCase().equals("JPEG")) {
formatName = "jpg";
}
String outImageName = (appSettingsService.isForceOrder() ? String.format("%03d_", index) : "") + imageName + "." + formatName.toLowerCase();
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
@@ -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)
@@ -34,6 +34,7 @@ public class AppSettingsService {
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 String downloadPath;
private int maxThreads;
@@ -43,6 +44,7 @@ public class AppSettingsService {
private String vPassword;
private boolean vThanks;
private boolean desktopClipboard;
private boolean forceOrder;
public void setVPassword(String vPassword) {
if(vPassword.isEmpty()) {
@@ -62,6 +64,7 @@ public class AppSettingsService {
vPassword = prefs.get(V_PASSWORD, "");
vThanks = prefs.getBoolean(V_THANKS, false);
desktopClipboard = prefs.getBoolean(DESKTOP_CLIPBOARD, false);
forceOrder = prefs.getBoolean(FORCE_ORDER, false);
}
@PreDestroy
@@ -75,6 +78,7 @@ public class AppSettingsService {
prefs.put(V_PASSWORD, vPassword);
prefs.putBoolean(V_THANKS, vThanks);
prefs.putBoolean(DESKTOP_CLIPBOARD, desktopClipboard);
prefs.putBoolean(FORCE_ORDER, forceOrder);
try {
prefs.sync();
@@ -121,8 +125,10 @@ public class AppSettingsService {
private boolean vThanks;
@JsonProperty("desktopClipboard")
private boolean desktopClipboard;
@JsonProperty("forceOrder")
private boolean forceOrder;
public Settings(String downloadPath, int maxThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard) {
public Settings(String downloadPath, int maxThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder) {
this.downloadPath = downloadPath;
this.maxThreads = maxThreads;
this.autoStart = autoStart;
@@ -131,6 +137,7 @@ public class AppSettingsService {
this.vPassword = vPassword;
this.vThanks = vThanks;
this.desktopClipboard = desktopClipboard;
this.forceOrder = forceOrder;
}
}
}
@@ -45,8 +45,8 @@ public class AppStateService {
}
}
public Image createImage(String pageUrl, String postId, String postName, Host host) {
return new Image(pageUrl, postId, postName, host, this);
public Image createImage(String pageUrl, String postId, String postName, Host host, int index) {
return new Image(pageUrl, postId, postName, host, this, index);
}
public Post createPost(String title, String url, List<Image> images, Map<String, String> metadata, String postId, String postCounter) {
@@ -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));
}
}
@@ -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;
@@ -180,7 +180,7 @@ public class PostParser {
}
if (foundHost != null) {
logger.info(String.format("Found supported host %s for %s", foundHost.getClass().getSimpleName(), imageHref));
imagesList.add(appStateService.createImage(imageHref.getTextContent(), postId, postTitle, foundHost));
imagesList.add(appStateService.createImage(imageHref.getTextContent(), postId, postTitle, foundHost, imagesList.size() + 1));
} else {
logger.warn(String.format("unsupported host for %s, skipping", imageHref));
continue;
@@ -7,8 +7,8 @@ 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.services.AppSettingsService;
import tn.mnlr.vripper.exception.ValidationException;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.VipergirlsAuthService;
@RestController
@@ -57,6 +57,7 @@ public class SettingsRestEndpoint {
this.settings.setVThanks(false);
}
this.settings.setDesktopClipboard(settings.isDesktopClipboard());
this.settings.setForceOrder(settings.isForceOrder());
this.settings.save();
@@ -76,7 +77,8 @@ public class SettingsRestEndpoint {
settings.getVUsername(),
settings.getVPassword(),
settings.isVThanks(),
settings.isDesktopClipboard()
settings.isDesktopClipboard(),
settings.isForceOrder()
);
}
@@ -17,6 +17,8 @@ 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;
@@ -32,21 +34,26 @@ public class WebSocketHandler extends TextWebSocketHandler {
private static final Logger logger = LoggerFactory.getLogger(WebSocketHandler.class);
private Map<String, Disposable> stateSubscriptions = new ConcurrentHashMap<>();
public WebSocketHandler() {
om.addMixIn(Image.class, ImageUIMixin.class).addMixIn(Post.class, PostUIMixin.class);
}
@Autowired
private GlobalStateService globalStateService;
private Map<String, Disposable> postsSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> postDetailsSubscriptions = new ConcurrentHashMap<>();
private ObjectMapper om = new ObjectMapper();
@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 {
@@ -56,6 +63,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
case GLOBAL_STATE_SUB:
subscribeForGlobalState(session);
break;
case SPEED_SUB:
subscribeForSpeed(session);
break;
case POSTS_SUB:
subscribeForPosts(session);
break;
@@ -74,6 +84,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
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;
}
}
@@ -95,18 +109,39 @@ public class WebSocketHandler extends TextWebSocketHandler {
.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))
);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
private void subscribeForSpeed(WebSocketSession session) {
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());
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) {
@@ -177,6 +212,15 @@ public class WebSocketHandler extends TextWebSocketHandler {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(d -> d.dispose());
}
@Getter
private static class WSMessage {
@@ -189,7 +233,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
POSTS_UNSUB,
POST_DETAILS_UNSUB,
GLOBAL_STATE_SUB,
GLOBAL_STATE_UNSUB
GLOBAL_STATE_UNSUB,
SPEED_SUB,
SPEED_UNSUB
}
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "1.4.4",
"version": "1.4.8",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "1.4.4",
"version": "1.4.8",
"scripts": {
"ng": "ng",
"start": "ng serve",
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>1.4.4</version>
<version>1.4.8</version>
</parent>
<artifactId>vripper-ui</artifactId>
<name>vripper-ui</name>
+3 -1
View File
@@ -2,7 +2,9 @@ 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',
GLOBAL_STATE_UNSUB = 'GLOBAL_STATE_UNSUB'
GLOBAL_STATE_UNSUB = 'GLOBAL_STATE_UNSUB',
SPEED_UNSUB = 'SPEED_UNSUB'
}
@@ -0,0 +1,3 @@
export class DownloadSpeed {
constructor(public speed: string) {}
}
+2 -1
View File
@@ -39,7 +39,8 @@
<app-posts [ngStyle]="{'height': electronService.isElectronApp ? 'calc(100% - 37px)' : '100%'}"
style="width: 100%;"></app-posts>
</div>
<div style="text-align: end">Downloading: {{ globalState!.running }} | Queued: {{ globalState!.queued }} | Remaining:
<div style="text-align: end">{{ downloadSpeed!.speed + '/s' }} | Downloading: {{ globalState!.running }} | Queued: {{
globalState!.queued }} | Remaining:
{{ globalState!.remaining }} | Error: {{ globalState!.error }}
</div>
</div>
+11 -1
View File
@@ -16,6 +16,7 @@ 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',
@@ -44,6 +45,7 @@ export class HomeComponent implements OnInit, OnDestroy {
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 => {
@@ -70,7 +72,7 @@ export class HomeComponent implements OnInit, OnDestroy {
});
this.websocketHandlerPromise.then((handler: WsHandler) => {
console.log('Connecting to global state');
console.log('Connecting to global state and download speed');
this.subscriptions.push(
handler.subscribeForGlobalState((e: GlobalState[]) => {
this.ngZone.run(() => {
@@ -78,7 +80,15 @@ export class HomeComponent implements OnInit, OnDestroy {
});
})
);
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()));
});
}
@@ -35,6 +35,11 @@
/>
</mat-form-field>
<mat-checkbox color="primary" formControlName="forceOrder" name="forceOrder"
>Force image ordering (prepend incremental numbers)
</mat-checkbox
>
<mat-checkbox color="primary" formControlName="autoStart" name="autoStart"
>Auto start downloads</mat-checkbox
>
@@ -18,3 +18,7 @@
.container form section div > * {
width: 100%;
}
mat-checkbox {
display: block;
}
@@ -25,6 +25,7 @@ export class SettingsComponent implements OnInit {
downloadPath: new FormControl(''),
maxThreads: new FormControl(''),
autoStart: new FormControl(false),
forceOrder: new FormControl(false),
vLogin: new FormControl(false),
vUsername: new FormControl(''),
vPassword: new FormControl(''),
+23
View File
@@ -4,6 +4,7 @@ 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>) {}
@@ -33,6 +34,28 @@ export class WsHandler {
});
}
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(