Compare commits

...
10 Commits
Author SHA1 Message Date
death-claw 1c61a0b34e v2.10.2 2019-12-29 11:35:59 +01:00
death-claw 46b9449969 Upgrade electron to v7
Update the retry policy
2019-12-29 11:33:03 +01:00
death-claw cc1bbd1231 v2.10.1 2019-12-27 23:05:10 +01:00
death-claw b29cba2911 Bug fix 2019-12-27 22:58:33 +01:00
death-claw 13bde3aa2c 2.10.0 2019-12-27 21:27:27 +01:00
death-claw 35b4f5fc97 Add support for imgspice
Fix spring dependency bug
2019-12-27 21:25:33 +01:00
death-claw 1b0482b7d4 v2.9.1 2019-12-27 20:10:31 +01:00
death-claw c9ff6eeab6 Fix build issue 2019-12-27 20:09:16 +01:00
death-claw 5ac57b743d v2.9.0 2019-12-27 20:07:19 +01:00
death-claw f7b219701f Allow many hosts to run at the same time
Add title to gallery
Add global concurrent downloads
Decrease timeout for fast fail
Add counter badge in link collector
2019-12-27 20:03:35 +01:00
40 changed files with 1257 additions and 1544 deletions
+28
View File
@@ -1,5 +1,33 @@
# Changelog
## [2.10.2] - 2019-12-29
### Changed
- Upgrade electron to v7
- Bug fix
## [2.10.1] - 2019-12-27
### Changed
- Bug fix
## [2.10.0] - 2019-12-27
### Changed
- Fix spring dependency bug
### Added
- Add support for imgspice
## [2.9.0] - 2019-12-27
### Changed
- Fix build issue
## [2.9.0] - 2019-12-27
### Changed
- Decrease timeout for fast fail
### Added
- Allow many hosts to run at the same time
- Add title to gallery
- Add global concurrent downloads
- Add counter badge in link collector
## [2.8.1] - 2019-12-24
### Changed
- Fix issue with path creation
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.8.1</version>
<version>2.10.2</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
+258 -792
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.8.1",
"version": "2.10.2",
"description": "A ripper for vipergirls.to built using web technolgies",
"main": "main.js",
"author": "death-claw <53543762+death-claw@users.noreply.github.com>",
@@ -70,7 +70,7 @@
"dist": "node pre-build.js && electron-builder"
},
"devDependencies": {
"electron": "^6.0.9",
"electron": "^7.1.7",
"electron-builder": "^21.2.0"
},
"dependencies": {
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.8.1</version>
<version>2.10.2</version>
</parent>
<artifactId>vripper-electron</artifactId>
<name>vripper-electron</name>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.8.1</version>
<version>2.10.2</version>
</parent>
<artifactId>vripper-server</artifactId>
<name>vripper-server</name>
@@ -0,0 +1,24 @@
package tn.mnlr.vripper;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import tn.mnlr.vripper.services.PersistenceService;
@Component
public class EventListenerBean {
@Autowired
private PersistenceService persistenceService;
@Getter
private static boolean init = false;
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
init = true;
persistenceService.restore();
}
}
@@ -27,7 +27,9 @@ public class SpringContext implements ApplicationContextAware {
public static void close() {
logger.info("Application terminating...");
context.close();
if (context != null) {
context.close();
}
}
@@ -1,21 +1,10 @@
package tn.mnlr.vripper;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;
import tn.mnlr.vripper.exception.VripperException;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.PersistenceService;
import tn.mnlr.vripper.services.VipergirlsAuthService;
import javax.annotation.PostConstruct;
import java.io.File;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -27,51 +16,11 @@ public class VripperApplication {
public static void main(String[] args) {
try {
Runtime.getRuntime().addShutdownHook(new Thread(SpringContext::close));
SpringApplication.run(VripperApplication.class, args);
} catch (Exception e) {
logger.error("Failed to run the application", e);
}
}
@Component
public class AppCommandRunner implements CommandLineRunner {
@Autowired
private VipergirlsAuthService authService;
@Autowired
private PersistenceService persistenceService;
@Autowired
private AppSettingsService appSettingsService;
@Value("${base.dir}")
private String baseDir;
@Getter
private String dataPath;
@PostConstruct
public void init() {
dataPath = baseDir + File.separator + ".vripper" + File.separator + "data.json";
}
@Override
public void run(String... args) {
persistenceService.restore();
appSettingsService.restore();
registerShutdownHook();
try {
authService.authenticate();
} catch (VripperException e) {
logger.error("Cannot authenticate user with ViperGirls", e);
}
}
private void registerShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(SpringContext::close));
}
}
}
@@ -32,6 +32,7 @@ import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Iterator;
import java.util.Objects;
@Service
abstract public class Host {
@@ -65,6 +66,11 @@ abstract public class Host {
abstract public String getLookup();
@Override
public int hashCode() {
return Objects.hash(pathService);
}
public boolean isSupported(String url) {
return url.contains(getLookup());
}
@@ -233,8 +239,17 @@ abstract public class Host {
protected abstract void setNameAndUrl(final String url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Host host = (Host) o;
return Objects.equals(getHost(), host.getHost());
}
@Getter
public static class Response {
protected Response(Document document, Header[] headers) {
this.document = document;
this.headers = headers;
@@ -0,0 +1,62 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import tn.mnlr.vripper.exception.HostException;
import tn.mnlr.vripper.exception.XpathException;
import tn.mnlr.vripper.q.ImageFileData;
import tn.mnlr.vripper.services.ConnectionManager;
@Service
public class ImgSpiceHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(ImgSpiceHost.class);
private static final String host = "imgspice.com";
private static final String IMG_XPATH = "//img[@id='imgpreview']";
@Autowired
private ConnectionManager cm;
@Override
public String getHost() {
return host;
}
@Override
public String getLookup() {
return host;
}
@Override
protected void setNameAndUrl(final String _url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
String url = _url.replace("http://", "https://");
Response resp = getResponse(url, context);
Document doc = resp.getDocument();
Node imgNode;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
imageFileData.setImageUrl(imgUrl);
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -1,156 +1,81 @@
package tn.mnlr.vripper.q;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.host.Host;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.AppStateService;
import java.util.Arrays;
import java.util.Iterator;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.stream.Collectors;
import java.util.Map;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingDeque;
@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
private AppStateService appStateService;
private final ConcurrentHashMap<Host, BlockingDeque<DownloadJob>> downloadQ = new ConcurrentHashMap<>();
@Autowired
private ExecutionService executionService;
private AppSettingsService appSettingsService;
@Autowired
private List<Host> hosts;
private BlockingQueue<DownloadJob> downloadQ = new LinkedBlockingQueue<>();
@Getter
private boolean notPauseQ = true;
@PostConstruct
private void init() {
hosts.forEach(host -> downloadQ.put(host, new LinkedBlockingDeque<>()));
}
public void put(Image image) throws InterruptedException {
synchronized (appStateService) {
logger.debug(String.format("Enqueuing a job for %s", image.getUrl()));
image.init();
DownloadJob downloadJob = new DownloadJob(image);
downloadQ.put(downloadJob);
appStateService.newDownloadJob(downloadJob);
logger.debug(String.format("Enqueuing a job for %s", image.getUrl()));
image.init();
DownloadJob downloadJob = new DownloadJob(image);
downloadQ.get(downloadJob.getImage().getHost()).putLast(downloadJob);
appStateService.newDownloadJob(downloadJob);
}
public void rePut(final DownloadJob downloadJob) throws InterruptedException {
downloadQ.get(downloadJob.getImage().getHost()).putFirst(downloadJob);
}
public List<DownloadJob> take() throws Exception {
if (hosts.size() == 0) {
throw new Exception("No host available in th application");
}
List<DownloadJob> downloadJobs = new ArrayList<>();
for (Host host : hosts) {
for (int i = 0; i < appSettingsService.getMaxThreads(); i++) {
DownloadJob downloadJob = downloadQ.get(host).pollFirst();
if (downloadJob != null) {
downloadJobs.add(downloadJob);
}
}
}
return downloadJobs;
}
DownloadJob take() throws InterruptedException {
DownloadJob downloadJob = downloadQ.take();
logger.debug(String.format("Retrieving a job for %s", downloadJob.getImage().getUrl()));
return downloadJob;
}
public synchronized void enqueue(Post post) throws InterruptedException {
public void enqueue(Post post) throws InterruptedException {
for (Image image : post.getImages()) {
put(image);
}
}
public void restart(String postId) throws InterruptedException {
synchronized (appStateService) {
if (appStateService.getRunningPosts().get(postId) != null && appStateService.getRunningPosts().get(postId).get() > 0) {
logger.warn(String.format("Cannot restart, jobs are currently running for post id %s", postId));
return;
}
List<Image> images = appStateService.getPost(postId)
.getImages()
.stream()
.filter(e -> !e.getStatus().equals(Image.Status.COMPLETE))
.collect(Collectors.toList());
if (images.isEmpty()) {
return;
}
appStateService.getPost(postId).setStatus(Post.Status.PENDING);
logger.debug(String.format("Restarting %d jobs for post id %s", images.size(), postId));
for (Image image : images) {
put(image);
}
}
}
private void removeScheduled(Image image) {
synchronized (appStateService) {
image.setStatus(Image.Status.STOPPED);
logger.debug(String.format("Removing scheduled job for %s", image.getUrl()));
Iterator<DownloadJob> iterator = downloadQ.iterator();
boolean removed = false;
while (iterator.hasNext()) {
DownloadJob next = iterator.next();
if (next.getImage().getPostId().equals(image.getPostId())) {
iterator.remove();
appStateService.doneDownloadJob(image);
logger.debug(String.format("Scheduled job for %s is removed", image.getUrl()));
removed = true;
break;
}
}
if (!removed) {
logger.debug(String.format("Job for %s does not exist", image.getUrl()));
}
image.cleanup();
}
}
private void removeRunning(String postId) {
logger.debug(String.format("Interrupting running jobs for post id %s", postId));
executionService.stop(postId);
}
public void stop(String postId) {
try {
synchronized (appStateService) {
if (FINISHED.contains(appStateService.getPost(postId).getStatus())) {
return;
}
notPauseQ = false;
appStateService.getPost(postId).setStatus(Post.Status.STOPPED);
List<Image> images = appStateService.getPost(postId)
.getImages()
.stream()
.filter(e -> !e.getStatus().equals(Image.Status.COMPLETE))
.collect(Collectors.toList());
if (images.isEmpty()) {
return;
}
logger.debug(String.format("Stopping %d jobs for post id %s", images.size(), postId));
images.forEach(this::removeScheduled);
removeRunning(postId);
}
} finally {
notPauseQ = true;
}
}
public int size() {
return downloadQ.size();
return downloadQ.values().stream().mapToInt(BlockingDeque::size).sum();
}
public void stopAll() {
synchronized (appStateService) {
appStateService.getCurrentPosts().values().stream().map(Post::getPostId).forEach(this::stop);
}
}
public void restartAll() throws InterruptedException {
synchronized (appStateService) {
for (Post post : appStateService.getCurrentPosts().values()) {
String postId = post.getPostId();
restart(postId);
}
}
public Iterable<? extends Map.Entry<Host, BlockingDeque<DownloadJob>>> entries() {
return downloadQ.entrySet();
}
}
@@ -9,17 +9,17 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.host.Host;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.AppStateService;
import tn.mnlr.vripper.services.ThumbnailGenerator;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@@ -29,6 +29,8 @@ public class ExecutionService {
private static final Logger logger = LoggerFactory.getLogger(ExecutionService.class);
private static final List<Post.Status> FINISHED = Arrays.asList(Post.Status.ERROR, Post.Status.COMPLETE, Post.Status.STOPPED);
@Autowired
private DownloadQ downloadQ;
@@ -43,10 +45,9 @@ public class ExecutionService {
@Autowired
private AppSettingsService appSettingsService;
private final AtomicInteger threadCount = new AtomicInteger();
private ExecutorService executor = Executors.newFixedThreadPool(10);
private final ConcurrentHashMap<Host, AtomicInteger> threadCount = new ConcurrentHashMap<>();
private boolean notPauseQ = true;
private ExecutorService executor = Executors.newFixedThreadPool(20);
private Thread executionThread;
@@ -61,8 +62,9 @@ public class ExecutionService {
retryPolicy = new RetryPolicy<>()
.handleIf(e -> !(e instanceof InterruptedException))
.withBackoff(10, 60, ChronoUnit.SECONDS)
.withMaxRetries(4)
.withBackoff(1, 10, ChronoUnit.SECONDS)
.withMaxDuration(Duration.of(10, ChronoUnit.SECONDS))
.withMaxAttempts(3)
.abortOn(InterruptedException.class)
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
@@ -77,12 +79,12 @@ public class ExecutionService {
executor.shutdown();
appStateService.getCurrentPosts().keySet().forEach(p -> {
logger.debug(String.format("Stopping download jobs for %s", p));
this.stop(p);
this.stopRunning(p);
});
executor.awaitTermination(10, TimeUnit.SECONDS);
}
public void stop(String postId) {
public void stopRunning(String postId) {
List<DownloadJob> data = running
.stream()
.filter(e -> e.getImage().getPostId().equals(postId))
@@ -92,17 +94,110 @@ public class ExecutionService {
data.forEach(e -> {
futures.get(e.getImage().getUrl()).cancel(true);
if(e.getImageFileData().getImageRequest() != null) {
if (e.getImageFileData().getImageRequest() != null) {
e.getImageFileData().getImageRequest().abort();
}
e.getImage().cleanup();
});
}
private boolean canRun() {
boolean canRun = threadCount.get() < settings.getMaxThreads();
if (canRun && downloadQ.isNotPauseQ()) {
threadCount.incrementAndGet();
public synchronized void stopAll() {
appStateService.getCurrentPosts().values().stream().map(Post::getPostId).forEach(this::stop);
}
public synchronized void restartAll() throws InterruptedException {
for (Post post : appStateService.getCurrentPosts().values()) {
String postId = post.getPostId();
restart(postId);
}
}
public synchronized void restart(String postId) throws InterruptedException {
if (appStateService.getRunningPosts().get(postId) != null && appStateService.getRunningPosts().get(postId).get() > 0) {
logger.warn(String.format("Cannot restart, jobs are currently running for post id %s", postId));
return;
}
List<Image> images = appStateService.getPost(postId)
.getImages()
.stream()
.filter(e -> !e.getStatus().equals(Image.Status.COMPLETE))
.collect(Collectors.toList());
if (images.isEmpty()) {
return;
}
appStateService.getPost(postId).setStatus(Post.Status.PENDING);
logger.debug(String.format("Restarting %d jobs for post id %s", images.size(), postId));
for (Image image : images) {
downloadQ.put(image);
}
}
private synchronized void removeScheduled(Image image) {
image.setStatus(Image.Status.STOPPED);
logger.debug(String.format("Removing scheduled job for %s", image.getUrl()));
boolean removed = false;
main:
for (Map.Entry<Host, BlockingDeque<DownloadJob>> entry : downloadQ.entries()) {
Iterator<DownloadJob> iterator = entry.getValue().iterator();
while (iterator.hasNext()) {
DownloadJob next = iterator.next();
if (next.getImage().getPostId().equals(image.getPostId())) {
iterator.remove();
appStateService.doneDownloadJob(image);
logger.debug(String.format("Scheduled job for %s is removed", image.getUrl()));
removed = true;
break main;
}
}
}
if (!removed) {
logger.debug(String.format("Job for %s does not exist", image.getUrl()));
}
image.cleanup();
}
private synchronized void removeRunning(String postId) {
logger.debug(String.format("Interrupting running jobs for post id %s", postId));
stopRunning(postId);
}
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)
.getImages()
.stream()
.filter(e -> !e.getStatus().equals(Image.Status.COMPLETE))
.collect(Collectors.toList());
if (images.isEmpty()) {
return;
}
logger.debug(String.format("Stopping %d jobs for post id %s", images.size(), postId));
images.forEach(this::removeScheduled);
removeRunning(postId);
} finally {
notPauseQ = true;
}
}
private synchronized boolean canRun(Host host) {
boolean canRun;
AtomicInteger count = threadCount.get(host);
if (count == null) {
threadCount.put(host, new AtomicInteger(0));
}
canRun = threadCount.get(host).get() < settings.getMaxThreads() && threadCount.values().stream().mapToInt(AtomicInteger::get).sum() < settings.getMaxTotalThreads();
if (canRun && notPauseQ) {
threadCount.get(host).incrementAndGet();
return true;
}
return false;
@@ -110,63 +205,66 @@ public class ExecutionService {
public void start() {
while (!Thread.interrupted()) {
if (canRun()) {
DownloadJob take;
try {
take = downloadQ.take();
if (take == null) {
continue;
List<DownloadJob> take;
try {
take = downloadQ.take();
for (DownloadJob downloadJob : take) {
if (!push(downloadJob)) {
downloadQ.rePut(downloadJob);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
DownloadJob finalTake = take;
Runnable task = () -> {
running.add(finalTake);
Failsafe.with(retryPolicy)
.onFailure(e -> {
if (e.getFailure() instanceof InterruptedException || (e.getFailure() instanceof FailsafeException && e.getFailure().getCause() instanceof InterruptedException)) {
logger.debug("Job successfully interrupted");
return;
}
logger.error(String.format("Failed to download %s after %d tries", finalTake.getImage().getUrl(), e.getAttemptCount()), e.getFailure());
finalTake.getImage().setStatus(Image.Status.ERROR);
})
.onComplete(e -> {
appStateService.doneDownloadJob(finalTake.getImage());
logger.debug(String.format("Finished downloading %s", finalTake.getImage().getUrl()));
if (appSettingsService.isViewPhotos()) {
VripperApplication.commonExecutor.submit(
() -> thumbnailGenerator.getThumbnails()
.get(new ThumbnailGenerator.CacheKey(finalTake.getImage().getPostId(), finalTake.getImageFileData().getFileName())));
}
synchronized (threadCount) {
threadCount.decrementAndGet();
running.remove(finalTake);
futures.remove(finalTake.getImage().getUrl());
threadCount.notify();
}
})
.get(finalTake::call);
};
logger.debug(String.format("Scheduling a job for %s", finalTake.getImage().getUrl()));
futures.put(finalTake.getImage().getUrl(), executor.submit(task));
} else {
synchronized (threadCount) {
try {
threadCount.wait(2_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
threadCount.wait(2_000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
logger.error("Execution Service failed", e);
break;
}
}
}
private boolean push(DownloadJob take) {
if (canRun(take.getImage().getHost())) {
Runnable task = () -> {
running.add(take);
Failsafe.with(retryPolicy)
.onFailure(e -> {
if (e.getFailure() instanceof InterruptedException || (e.getFailure() instanceof FailsafeException && e.getFailure().getCause() instanceof InterruptedException)) {
logger.debug("Job successfully interrupted");
return;
}
logger.error(String.format("Failed to download %s after %d tries", take.getImage().getUrl(), e.getAttemptCount()), e.getFailure());
take.getImage().setStatus(Image.Status.ERROR);
})
.onComplete(e -> {
appStateService.doneDownloadJob(take.getImage());
logger.debug(String.format("Finished downloading %s", take.getImage().getUrl()));
if (appSettingsService.isViewPhotos()) {
VripperApplication.commonExecutor.submit(
() -> thumbnailGenerator.getThumbnails()
.get(new ThumbnailGenerator.CacheKey(take.getImage().getPostId(), take.getImageFileData().getFileName())));
}
threadCount.get(take.getImage().getHost()).decrementAndGet();
running.remove(take);
futures.remove(take.getImage().getUrl());
synchronized (threadCount) {
threadCount.notify();
}
})
.get(take::call);
};
logger.debug(String.format("Scheduling a job for %s", take.getImage().getUrl()));
futures.put(take.getImage().getUrl(), executor.submit(task));
return true;
} else {
return false;
}
}
public int runningCount() {
return running.size();
}
@@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.exception.ValidationException;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
@@ -24,6 +25,8 @@ import java.util.prefs.Preferences;
@Setter
public class AppSettingsService {
private final String MAX_TOTAL_THREADS = "MAX_TOTAL_THREADS";
@Value("${base.dir}")
private String defaultDownloadPath;
@@ -33,6 +36,7 @@ public class AppSettingsService {
private final String DOWNLOAD_PATH = "DOWNLOAD_PATH";
private final String MAX_THREADS = "MAX_THREADS";
private int maxTotalThreads;
private final String AUTO_START = "AUTO_START";
private final String V_LOGIN = "VLOGIN";
private final String V_USERNAME = "VUSERNAME";
@@ -48,6 +52,12 @@ public class AppSettingsService {
private String downloadPath;
private int maxThreads;
@PostConstruct
private void init() {
restore();
}
private boolean autoStart;
private boolean vLogin;
private String vUsername;
@@ -73,6 +83,7 @@ public class AppSettingsService {
downloadPath = prefs.get(DOWNLOAD_PATH, defaultDownloadPath);
maxThreads = prefs.getInt(MAX_THREADS, 4);
maxTotalThreads = prefs.getInt(MAX_TOTAL_THREADS, 8);
autoStart = prefs.getBoolean(AUTO_START, true);
vLogin = prefs.getBoolean(V_LOGIN, false);
vUsername = prefs.get(V_USERNAME, "");
@@ -92,6 +103,7 @@ public class AppSettingsService {
prefs.put(DOWNLOAD_PATH, downloadPath);
prefs.putInt(MAX_THREADS, maxThreads);
prefs.putInt(MAX_TOTAL_THREADS, maxTotalThreads);
prefs.putBoolean(AUTO_START, autoStart);
prefs.putBoolean(V_LOGIN, vLogin);
prefs.put(V_USERNAME, vUsername);
@@ -126,6 +138,10 @@ public class AppSettingsService {
throw new ValidationException(String.format("%s is not a directory", settings.getDownloadPath()));
}
if (settings.getMaxTotalThreads() < 1) {
throw new ValidationException(String.format("Invalid max global concurrent download settings, values must be in greater than %d", 1));
}
if (settings.getMaxThreads() < 1 || settings.getMaxThreads() > 4) {
throw new ValidationException(String.format("Invalid max concurrent download settings, values must be in [%d,%d]", 1, 4));
}
@@ -159,6 +175,8 @@ public class AppSettingsService {
private String downloadPath;
@JsonProperty("maxThreads")
private int maxThreads;
@JsonProperty("maxTotalThreads")
private int maxTotalThreads;
@JsonProperty("autoStart")
private boolean autoStart;
@JsonProperty("vLogin")
@@ -182,9 +200,10 @@ public class AppSettingsService {
@JsonProperty("viewPhotos")
private boolean viewPhotos;
public Settings(String downloadPath, int maxThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder, boolean subLocation, boolean threadSubLocation, boolean clearCompleted, boolean viewPhotos) {
public Settings(String downloadPath, int maxThreads, int maxTotalThreads, boolean autoStart, boolean vLogin, String vUsername, String vPassword, boolean vThanks, boolean desktopClipboard, boolean forceOrder, boolean subLocation, boolean threadSubLocation, boolean clearCompleted, boolean viewPhotos) {
this.downloadPath = downloadPath;
this.maxThreads = maxThreads;
this.maxTotalThreads = maxTotalThreads;
this.autoStart = autoStart;
this.vLogin = vLogin;
this.vUsername = vUsername;
@@ -59,7 +59,7 @@ public class AppStateService {
return currentPosts.get(postId);
}
public synchronized void newDownloadJob(DownloadJob downloadJob) {
public void newDownloadJob(DownloadJob downloadJob) {
String postId = downloadJob.getImage().getPostId();
checkKeyRunningPosts(postId);
runningPosts.get(postId).incrementAndGet();
@@ -26,9 +26,9 @@ public class ConnectionManager {
private PoolingHttpClientConnectionManager pcm;
private RequestConfig rc = RequestConfig.custom()
.setConnectionRequestTimeout(10_000)
.setConnectTimeout(10_000)
.setSocketTimeout(10_000)
.setConnectionRequestTimeout(5_000)
.setConnectTimeout(5_000)
.setSocketTimeout(5_000)
.setCookieSpec(CookieSpecs.STANDARD)
.build();
@@ -7,9 +7,10 @@ import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.EventListenerBean;
import tn.mnlr.vripper.SpringContext;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.entities.mixin.persistance.ImagePersistanceMixin;
@@ -38,8 +39,11 @@ public class PersistenceService {
@Autowired
private AppStateService stateService;
@Autowired
private VripperApplication.AppCommandRunner appCommandRunner;
@Value("${base.dir}")
private String baseDir;
@Getter
private String dataPath;
private ObjectMapper om;
@@ -61,7 +65,8 @@ public class PersistenceService {
@PostConstruct
public void init() {
File dataFile = new File(appCommandRunner.getDataPath());
dataPath = baseDir + File.separator + ".vripper" + File.separator + "data.json";
File dataFile = new File(dataPath);
if (!dataFile.exists()) {
try {
if (dataFile.getParentFile().mkdirs()) {
@@ -92,12 +97,14 @@ public class PersistenceService {
this.subscription.dispose();
logger.info(String.format("Destroying %s", PersistenceService.class.getSimpleName()));
logger.info("Persisting data before destroying");
this.persist(stateService.getCurrentPosts());
if (EventListenerBean.isInit()) {
this.persist(stateService.getCurrentPosts());
}
}
private void persist(Map<String, Post> currentPosts) {
try (PrintWriter out = new PrintWriter(appCommandRunner.getDataPath(), StandardCharsets.UTF_8)) {
try (PrintWriter out = new PrintWriter(dataPath, StandardCharsets.UTF_8)) {
out.print(om.writeValueAsString(currentPosts));
} catch (IOException e) {
logger.error("Failed to persist app state", e);
@@ -111,11 +118,11 @@ public class PersistenceService {
} catch (IOException e) {
logger.error("Failed to read app state", e);
long timestamp = new Date().getTime();
logger.warn(String.format("trying to rename old data file from %s to %s", appCommandRunner.getDataPath(), appCommandRunner.getDataPath() + "." + timestamp + ".old"));
logger.warn(String.format("trying to rename old data file from %s to %s", dataPath, dataPath + "." + timestamp + ".old"));
try {
Files.move(new File(appCommandRunner.getDataPath()).toPath(), new File(appCommandRunner.getDataPath() + "." + timestamp + ".old").toPath());
Files.move(new File(dataPath).toPath(), new File(dataPath + "." + timestamp + ".old").toPath());
} catch (IOException ex) {
logger.error(String.format("Failed to rename %s to %s", appCommandRunner.getDataPath(), appCommandRunner.getDataPath() + ".old"));
logger.error(String.format("Failed to rename %s to %s", dataPath, dataPath + ".old"));
SpringContext.close();
}
}
@@ -127,7 +134,7 @@ public class PersistenceService {
String jsonContent = null;
try {
jsonContent = String.join("", Files.readAllLines(Paths.get(appCommandRunner.getDataPath()), StandardCharsets.UTF_8));
jsonContent = String.join("", Files.readAllLines(Paths.get(dataPath), StandardCharsets.UTF_8));
} catch (Exception e) {
logger.error("data file cannot be read, previous state cannot be restored", e);
SpringContext.close();
@@ -45,7 +45,7 @@ public class PostParser {
}
public void addPost(String postId, String threadId) throws PostParseException {
public synchronized void addPost(String postId, String threadId) throws PostParseException {
if (appStateService.getCurrentPosts().containsKey(postId)) {
logger.warn(String.format("skipping %s, already loaded", postId));
@@ -22,6 +22,7 @@ import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
@@ -39,8 +40,9 @@ class VRPostParser {
private static RetryPolicy<Object> retryPolicy = new RetryPolicy<>()
.handleIf(e -> e instanceof IOException)
.withBackoff(5, 30, ChronoUnit.SECONDS)
.withMaxRetries(4)
.withDelay(1, 3, ChronoUnit.SECONDS)
.withMaxRetries(2)
.withMaxDuration(Duration.of(10, ChronoUnit.SECONDS))
.abortOn(InterruptedException.class)
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
@@ -21,6 +21,7 @@ import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
@@ -35,8 +36,9 @@ public class VRThreadParser {
private static RetryPolicy<Object> retryPolicy = new RetryPolicy<>()
.handleIf(e -> e instanceof IOException)
.withBackoff(5, 30, ChronoUnit.SECONDS)
.withMaxRetries(4)
.withDelay(1, 3, ChronoUnit.SECONDS)
.withMaxRetries(2)
.withMaxDuration(Duration.of(10, ChronoUnit.SECONDS))
.abortOn(InterruptedException.class)
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
private static SAXParserFactory factory = SAXParserFactory.newInstance();
@@ -59,6 +59,11 @@ public class VipergirlsAuthService {
@PostConstruct
private void init() {
context.setCookieStore(new BasicCookieStore());
try {
authenticate();
} catch (VripperException e) {
logger.error("Cannot authenticate user with ViperGirls", e);
}
}
@PreDestroy
@@ -1,5 +1,8 @@
package tn.mnlr.vripper.web.restendpoints;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
@@ -26,6 +29,7 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -89,6 +93,7 @@ public class GalleryEndpoint {
return ResponseEntity.ok(
Arrays.stream(Objects.requireNonNull(destinationFolder.listFiles()))
.filter(f -> !f.getName().endsWith("tmp"))
.filter(f -> f.getName().toLowerCase().endsWith(".jpg") || f.getName().toLowerCase().endsWith(".jpeg"))
.sorted(Comparator.comparing(File::getName))
.map(GalleryImage::fromFile)
.filter(Objects::nonNull)
@@ -104,13 +109,44 @@ public class GalleryEndpoint {
@NoArgsConstructor
class GalleryImage {
private static final CacheLoader<File, Dimension> loader = new CacheLoader<>() {
@Override
public Dimension load(File file) {
try (ImageInputStream in = ImageIO.createImageInputStream(file)) {
final Iterator<ImageReader> readers = ImageIO.getImageReaders(in);
if (readers.hasNext()) {
ImageReader reader = readers.next();
try {
reader.setInput(in);
return new Dimension(reader.getWidth(0), reader.getHeight(0));
} finally {
reader.dispose();
}
} else {
logger.error(String.format("No reader found for image %s", file.toString()));
return null;
}
} catch (Exception e) {
logger.error(String.format("Failed to create image object for %s", file.toString()), e);
return null;
}
}
};
private static final LoadingCache<File, Dimension> cache = CacheBuilder.newBuilder()
.maximumSize(20000)
.build(loader);
private static final Logger logger = LoggerFactory.getLogger(GalleryImage.class);
private String title;
private String src;
private String msrc;
private double w;
private double h;
public GalleryImage(String src, String msrc, double w, double h) {
public GalleryImage(String title, String src, String msrc, double w, double h) {
this.title = title;
this.src = src;
this.msrc = msrc;
this.w = w;
@@ -118,26 +154,16 @@ class GalleryImage {
}
public static GalleryImage fromFile(File file) {
Dimension dimension;
try (ImageInputStream in = ImageIO.createImageInputStream(file)) {
final Iterator<ImageReader> readers = ImageIO.getImageReaders(in);
if (readers.hasNext()) {
ImageReader reader = readers.next();
try {
reader.setInput(in);
dimension = new Dimension(reader.getWidth(0), reader.getHeight(0));
} finally {
reader.dispose();
}
} else {
logger.error(String.format("No reader found for image %s", file.toString()));
return null;
}
} catch (Exception e) {
logger.error(String.format("Failed to create image object for %s", file.toString()), e);
Dimension dimension = null;
try {
dimension = cache.get(file);
} catch (ExecutionException e) {
logger.error(String.format("Failed to get image dimensions for %s", file.toString()), e);
}
if (dimension == null) {
return null;
}
return new GalleryImage(file.getName(), file.getName(), dimension.getWidth(), dimension.getHeight());
return new GalleryImage(file.getName(), file.getName(), file.getName(), dimension.getWidth(), dimension.getHeight());
}
}
@@ -9,7 +9,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.exception.PostParseException;
import tn.mnlr.vripper.q.DownloadQ;
import tn.mnlr.vripper.q.ExecutionService;
import tn.mnlr.vripper.services.*;
import java.io.File;
@@ -47,14 +47,16 @@ public class PostRestEndpoint {
}
@Autowired
private DownloadQ downloadQ;
private ExecutionService executionService;
// private DownloadQ downloadQ;
@Autowired
private PostParser postParser;
@PostMapping("/post")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity processPost(@RequestBody ThreadUrl _url) throws Exception {
public synchronized ResponseEntity processPost(@RequestBody ThreadUrl _url) throws Exception {
if (_url.getUrl() == null || _url.getUrl().isEmpty()) {
return new ResponseEntity("Failed to process empty request", HttpStatus.BAD_REQUEST);
}
@@ -87,13 +89,13 @@ public class PostRestEndpoint {
@ResponseStatus(value = HttpStatus.OK)
public synchronized void restartPost(@RequestBody @NonNull List<PostId> postIds) throws Exception {
for (PostId postId : postIds) {
downloadQ.restart(postId.getPostId());
executionService.restart(postId.getPostId());
}
}
@PostMapping("/post/add")
@ResponseStatus(value = HttpStatus.OK)
public void addPost(@RequestBody List<PostToAdd> posts) {
public synchronized void addPost(@RequestBody List<PostToAdd> posts) {
for (PostToAdd post : posts) {
VripperApplication.commonExecutor.submit(() -> {
try {
@@ -114,22 +116,22 @@ public class PostRestEndpoint {
@PostMapping("/post/restart/all")
@ResponseStatus(value = HttpStatus.OK)
public void restartPost() throws Exception {
downloadQ.restartAll();
public synchronized void restartPost() throws Exception {
executionService.restartAll();
}
@PostMapping("/post/stop")
@ResponseStatus(value = HttpStatus.OK)
public synchronized void stop(@RequestBody @NonNull List<PostId> postIds) {
for (PostId postId : postIds) {
downloadQ.stop(postId.getPostId());
executionService.stop(postId.getPostId());
}
}
@PostMapping("/post/stop/all")
@ResponseStatus(value = HttpStatus.OK)
public void stopAll() {
downloadQ.stopAll();
public synchronized void stopAll() {
executionService.stopAll();
}
@PostMapping("/post/remove")
@@ -137,7 +139,7 @@ public class PostRestEndpoint {
public synchronized ResponseEntity<List<RemoveResult>> remove(@RequestBody @NonNull List<PostId> postIds) {
List<RemoveResult> result = new ArrayList<>();
for (PostId postId : postIds) {
downloadQ.stop(postId.getPostId());
executionService.stop(postId.getPostId());
appStateService.remove(postId.getPostId());
result.add(new RemoveResult(postId.getPostId()));
}
@@ -146,13 +148,13 @@ public class PostRestEndpoint {
@PostMapping("/post/clear/all")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<RemoveAllResult> clearAll() {
public synchronized ResponseEntity<RemoveAllResult> clearAll() {
return ResponseEntity.ok(new RemoveAllResult(appStateService.clearAll()));
}
@PostMapping("/post/remove/all")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<RemoveAllResult> removeAll() {
public synchronized ResponseEntity<RemoveAllResult> removeAll() {
return ResponseEntity.ok(new RemoveAllResult(appStateService.removeAll()));
}
@@ -60,6 +60,7 @@ public class SettingsRestEndpoint {
}
this.settings.setDownloadPath(settings.getDownloadPath());
this.settings.setMaxThreads(settings.getMaxThreads());
this.settings.setMaxTotalThreads(settings.getMaxTotalThreads());
this.settings.setAutoStart(settings.isAutoStart());
this.settings.setVLogin(settings.isVLogin());
@@ -95,6 +96,7 @@ public class SettingsRestEndpoint {
return new AppSettingsService.Settings(
settings.getDownloadPath(),
settings.getMaxThreads(),
settings.getMaxTotalThreads(),
settings.isAutoStart(),
settings.isVLogin(),
settings.getVUsername(),
@@ -6,5 +6,5 @@ server.port=${vripper.server.port:8080}
management.endpoints.web.exposure.include=shutdown
management.endpoint.shutdown.enabled=true
endpoints.shutdown.enabled=true
spring.profiles.active=portable
#spring.profiles.active=installer
#spring.profiles.active=portable
spring.profiles.active=installer
+448 -425
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "2.8.1",
"version": "2.10.2",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -43,7 +43,7 @@
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
"codelyzer": "~4.5.0",
"electron": "^6.0.9",
"electron": "^7.1.7",
"jasmine-core": "~2.99.1",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^4.2.0",
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.8.1</version>
<version>2.10.2</version>
</parent>
<artifactId>vripper-ui</artifactId>
<name>vripper-ui</name>
+3 -1
View File
@@ -1,3 +1,4 @@
import { LinkCollectorService } from './link-collector.service';
import { ContextMenuService } from './ctxt-menu.service';
import { PostContextMenuComponent } from './posts/post-context-menu.component';
import { PostsDataService } from './posts-data.service';
@@ -101,7 +102,8 @@ import { PhotoSwipeComponent } from './photo-swipe/photo-swipe.component';
SharedService,
SelectionService,
PostsDataService,
ContextMenuService
ContextMenuService,
LinkCollectorService
],
bootstrap: [AppComponent]
})
@@ -1,6 +1,7 @@
export interface Settings {
downloadPath: string;
maxThreads: number;
maxTotalThreads: number;
autoStart: boolean;
vLogin: boolean;
vUsername: string;
@@ -7,7 +7,14 @@ import { PostState } from '../posts/post-state.model';
import { BehaviorSubject, Subject } from 'rxjs';
class Image extends IImage {
constructor(public src: string, public msrc: string, public w: number, public h: number, public _initialized: boolean) {
constructor(
public title: string,
public src: string,
public msrc: string,
public w: number,
public h: number,
public _initialized: boolean
) {
super(src, msrc, w, h);
}
}
@@ -1,16 +1,18 @@
import { LinkCollectorService } from './../link-collector.service';
import { UrlGrabRendererComponent } from './url-renderer.component';
import { GrabQueueDataSource } from './grab-queue.datasource';
import { Component, OnInit, NgZone } from '@angular/core';
import { Component, OnInit, NgZone, ChangeDetectionStrategy } from '@angular/core';
import { GridOptions } from 'ag-grid-community';
import { WsConnectionService } from '../ws-connection.service';
@Component({
selector: 'app-grab-queue',
templateUrl: './grab-queue.component.html',
styleUrls: ['./grab-queue.component.scss']
styleUrls: ['./grab-queue.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GrabQueueComponent implements OnInit {
constructor(private wsConnection: WsConnectionService, private zone: NgZone) {
constructor(private wsConnection: WsConnectionService, private zone: NgZone, private linkCollectorService: LinkCollectorService) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
@@ -37,7 +39,10 @@ export class GrabQueueComponent implements OnInit {
this.dataSource.connect();
},
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit()
onRowDataUpdated: () => {
this.linkCollectorService.setCount(this.gridOptions.api.getDisplayedRowCount());
this.gridOptions.api.sizeColumnsToFit();
}
};
}
+7 -1
View File
@@ -2,7 +2,13 @@
<mat-tab label="Downloads">
<app-posts style="width: 100%; height: 100%;"></app-posts>
</mat-tab>
<mat-tab label="Link Collector">
<mat-tab>
<ng-template mat-tab-label>
<span style="margin-right: 5px;">Link Collector</span>
<ng-container *ngIf="linkCollectorService.count | async as count">
<mat-icon [matBadgeHidden]="count < 1" [matBadge]="count">link</mat-icon>
</ng-container>
</ng-template>
<app-grab-queue style="width: 100%; height: 100%;"></app-grab-queue>
</mat-tab>
</mat-tab-group>
+3 -1
View File
@@ -1,3 +1,4 @@
import { LinkCollectorService } from './../link-collector.service';
import { ServerService } from './../server-service';
import { ElectronService } from 'ngx-electron';
import { ClipboardService } from './../clipboard.service';
@@ -19,7 +20,8 @@ export class HomeComponent implements OnInit, OnDestroy {
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar,
private ngZone: NgZone
private ngZone: NgZone,
public linkCollectorService: LinkCollectorService
) {}
ngOnInit() {
@@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import { Subject, Observable, BehaviorSubject } from 'rxjs';
@Injectable()
export class LinkCollectorService {
private _count$: Subject<number> = new BehaviorSubject(0);
get count(): Observable<number> {
return this._count$.asObservable();
}
setCount(count: number) {
this._count$.next(count);
}
}
+4 -2
View File
@@ -17,7 +17,8 @@ import {
MatDividerModule,
MatChipsModule,
MatListModule,
MatProgressSpinnerModule
MatProgressSpinnerModule,
MatBadgeModule
} from '@angular/material';
@NgModule({
@@ -38,7 +39,8 @@ import {
MatDividerModule,
MatChipsModule,
MatListModule,
MatProgressSpinnerModule
MatProgressSpinnerModule,
MatBadgeModule
],
providers: [MatSnackBar]
})
@@ -25,18 +25,32 @@
</button>
</div>
<mat-form-field>
<input
formControlName="maxThreads"
type="number"
matInput
placeholder="Max concurrent downloads"
name="maxThreads"
min="1"
max="4"
required
/>
</mat-form-field>
<div fxLayout="row" fxLayoutAlign="center center" fxLayoutGap="10px">
<mat-form-field fxFlex="grow">
<input
formControlName="maxThreads"
matInput
max="4"
min="1"
name="maxThreads"
placeholder="Max concurrent downloads per host"
required
type="number"
/>
</mat-form-field>
<mat-form-field fxFlex="grow">
<input
formControlName="maxTotalThreads"
matInput
min="1"
name="maxTotalThreads"
placeholder="Max global concurrent downloads"
required
type="number"
/>
</mat-form-field>
</div>
<mat-checkbox color="primary" formControlName="subLocation" name="subLocation"
>Save posts in sub folders
@@ -64,7 +78,7 @@
</mat-checkbox>
<mat-checkbox color="primary" formControlName="viewPhotos" name="viewPhotos">
Enable View photo in gallery option
Enable photo gallery
</mat-checkbox>
<mat-slide-toggle color="primary" formControlName="vLogin" name="vLogin">
@@ -31,6 +31,7 @@ export class SettingsComponent implements OnInit {
generalSettingsForm = new FormGroup({
downloadPath: new FormControl(''),
maxThreads: new FormControl(''),
maxTotalThreads: new FormControl(''),
autoStart: new FormControl(false),
forceOrder: new FormControl(false),
subLocation: new FormControl(false),
@@ -1,5 +1,5 @@
export const environment = {
production: true,
localhost: '',
version: '2.8.1'
version: '2.10.2'
};
+1 -1
View File
@@ -5,7 +5,7 @@
export const environment = {
production: false,
localhost: 'http://localhost:8080',
version: '2.8.1'
version: '2.10.2'
};
/*