Compare commits

...
14 Commits
Author SHA1 Message Date
death-claw ab80e2f3b3 v2.5.1 2019-12-09 20:26:52 +01:00
death-claw 2ef082f87e Add hosts column in Link collector grab menu
Disable download button when nothing selected in Link collector grab menu
2019-12-09 19:56:26 +01:00
death-claw a1b97e76a7 v2.5.0 2019-12-08 20:13:58 +01:00
death-claw 185a078b4d Missing change 2019-12-08 20:08:42 +01:00
death-claw 28a4c944ee Add support for multiple links at a time
Add the Link collector feature
Fix bug with previews
2019-12-08 20:07:13 +01:00
death-claw 23d3553d99 Include startup icon
Fix UI layout
2019-12-06 23:09:57 +01:00
death-claw fb09e4c46b v2.4.1 2019-11-26 20:03:10 +01:00
death-claw 4feb6f5fe1 Increase attempts for establishing ws connection between front and back end
Change default download folder to Home folder (installer), App folder (portable)
2019-11-26 20:01:08 +01:00
death-claw df1b9efbbb v2.4.0 2019-11-23 14:50:16 +01:00
death-claw 553924fa1d Pimpandhost Host support 2019-11-23 14:48:05 +01:00
death-claw a90dbd568f v2.3.2 2019-11-23 12:15:42 +01:00
death-claw ca7ca1ce8e Fix a bug with the scan component 2019-11-23 12:14:07 +01:00
death-claw b3ab239a1e v2.3.1 2019-11-22 20:07:41 +01:00
death-claw 6e212c791c Fix connexion reset issue
Fix Post Status when autostart option is selected
Add stop all button
2019-11-22 20:05:14 +01:00
63 changed files with 6375 additions and 3055 deletions
+34
View File
@@ -1,5 +1,39 @@
# Changelog
## [2.5.1] - 2019-12-09
### Changed
- Disable download button when nothing selected in Link collector grab menu
### Added
- Add hosts column in Link collector grab menu
## [2.5.0] - 2019-12-08
### Changed
- Change loading screen
- UI Fixes
- Fix bug with previews
### Added
- Add link collector feature
- Add support for multiple links at a time
## [2.4.1] - 2019-11-26
### Changed
- Increase attempts for establishing ws connection between front and back end
- Change default download folder to Home folder (installer), App folder (portable)
## [2.4.0] - 2019-11-23
### Changed
- Pimpandhost Host support
## [2.3.2] - 2019-11-23
### Changed
- Fix a bug with the scan component
## [2.3.1] - 2019-11-22
### Changed
- Fix connexion reset issue
- Fix Post Status when autostart option is selected
- Add stop all button
## [2.3.0] - 2019-11-21
### Changed
- UI enhancements and overhaul
+1 -1
View File
@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.3.0</version>
<version>2.5.1</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
+1 -6
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.3.0",
"version": "2.5.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -644,11 +644,6 @@
"delayed-stream": "~1.0.0"
}
},
"command-exists": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.8.tgz",
"integrity": "sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-electron",
"version": "2.3.0",
"version": "2.5.1",
"description": "A ripper for vipergirls.to built using web technolgies",
"main": "main.js",
"author": "death-claw <53543762+death-claw@users.noreply.github.com>",
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.3.0</version>
<version>2.5.1</version>
</parent>
<artifactId>vripper-electron</artifactId>
<name>vripper-electron</name>
+6 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.3.0</version>
<version>2.5.1</version>
</parent>
<artifactId>vripper-server</artifactId>
<name>vripper-server</name>
@@ -67,6 +67,11 @@
<artifactId>failsafe</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
<dependency>
<groupId>tn.mnlr</groupId>
<artifactId>vripper-ui</artifactId>
@@ -27,6 +27,8 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.util.Iterator;
@@ -196,6 +198,20 @@ abstract public class Host {
}
}
protected String appendUri(String uri, String appendQuery) throws URISyntaxException {
URI oldUri = new URI(uri);
String newQuery = oldUri.getQuery();
if (newQuery == null) {
newQuery = appendQuery;
} else {
newQuery += "&" + appendQuery;
}
return new URI(oldUri.getScheme(), oldUri.getAuthority(),
oldUri.getPath(), newQuery, oldUri.getFragment()).toString();
}
private void setImageRequest(final ImageFileData imageFileData) {
HttpGet httpGet = cm.buildHttpGet(imageFileData.getImageUrl());
httpGet.addHeader("Referer", imageFileData.getPageUrl());
@@ -0,0 +1,68 @@
package tn.mnlr.vripper.host;
import org.apache.http.client.protocol.HttpClientContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import tn.mnlr.vripper.exception.HostException;
import tn.mnlr.vripper.exception.XpathException;
import tn.mnlr.vripper.q.ImageFileData;
import tn.mnlr.vripper.services.ConnectionManager;
@Service
public class PimpandhostHost extends Host {
private static final Logger logger = LoggerFactory.getLogger(PimpandhostHost.class);
private static final String host = "pimpandhost.com";
private static final String IMG_XPATH = "//img[contains(@class, 'original')]";
@Autowired
private ConnectionManager cm;
@Override
public String getHost() {
return host;
}
@Override
public String getLookup() {
return host;
}
@Override
protected void setNameAndUrl(final String _url, final ImageFileData imageFileData, final HttpClientContext context) throws HostException {
String url;
try {
url = appendUri(_url.replace("http://", "https://"), "size=original");
} catch (Exception e) {
throw new HostException(e);
}
Response resp = getResponse(url, context);
Document doc = resp.getDocument();
Node imgNode;
try {
logger.debug(String.format("Looking for xpath expression %s in %s", IMG_XPATH, url));
imgNode = xpathService.getAsNode(doc, IMG_XPATH);
} catch (XpathException e) {
throw new HostException(e);
}
try {
logger.debug(String.format("Resolving name and image url for %s", url));
String imgTitle = imgNode.getAttributes().getNamedItem("alt").getTextContent().trim();
String imgUrl = "https:" + imgNode.getAttributes().getNamedItem("src").getTextContent().trim();
imageFileData.setImageUrl(imgUrl);
imageFileData.setImageName(imgTitle.isEmpty() ? imgUrl.substring(imgUrl.lastIndexOf('/') + 1) : imgTitle);
} catch (Exception e) {
throw new HostException("Unexpected error occurred", e);
}
}
}
@@ -7,6 +7,7 @@ import lombok.Setter;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.exception.ValidationException;
@@ -23,6 +24,9 @@ import java.util.prefs.Preferences;
@Setter
public class AppSettingsService {
@Value("${base.dir}")
private String defaultDownloadPath;
private static final Logger logger = LoggerFactory.getLogger(AppSettingsService.class);
private Preferences prefs = Preferences.userNodeForPackage(AppSettingsService.class);
@@ -65,7 +69,7 @@ public class AppSettingsService {
public void restore() {
downloadPath = prefs.get(DOWNLOAD_PATH, System.getProperty("user.dir"));
downloadPath = prefs.get(DOWNLOAD_PATH, defaultDownloadPath);
maxThreads = prefs.getInt(MAX_THREADS, 4);
autoStart = prefs.getBoolean(AUTO_START, true);
vLogin = prefs.getBoolean(V_LOGIN, false);
@@ -29,6 +29,10 @@ public class AppStateService {
private Map<String, AtomicInteger> runningPosts = new ConcurrentHashMap<>();
private Map<String, QueuedVGLink> grabQueue = new ConcurrentHashMap<>();
private PublishProcessor<QueuedVGLink> liveGrabQueue = PublishProcessor.create();
private PublishProcessor<Image> liveImageUpdates = PublishProcessor.create();
private PublishProcessor<Post> livePostsState = PublishProcessor.create();
@@ -8,12 +8,15 @@ import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.util.concurrent.TimeUnit;
@Service
@EnableScheduling
public class ConnectionManager {
private ConnectionManager() {
@@ -34,6 +37,11 @@ public class ConnectionManager {
pcm = new PoolingHttpClientConnectionManager();
pcm.setMaxTotal(50);
pcm.setDefaultMaxPerRoute(4);
}
@Scheduled(fixedDelay = 1000)
private void idleConnectionMonitoring() {
pcm.closeExpiredConnections();
pcm.closeIdleConnections(30, TimeUnit.SECONDS);
}
@@ -61,6 +61,7 @@ public class PostParser {
if (appSettingsService.isAutoStart()) {
logger.debug("Auto start downloads option is enabled");
logger.debug(String.format("Starting to enqueue %d jobs for %s", post.getImages().size(), post.getUrl()));
post.setStatus(Post.Status.PENDING);
try {
downloadQ.enqueue(post);
} catch (InterruptedException e) {
@@ -70,11 +71,12 @@ public class PostParser {
}
logger.debug(String.format("Done enqueuing jobs for %s", post.getUrl()));
} else {
post.setStatus(Post.Status.STOPPED);
logger.debug("Auto start downloads option is disabled");
}
}
public VRThreadParser createVRThreadParser(String threadId) {
return new VRThreadParser(threadId, cm, vipergirlsAuthService);
return new VRThreadParser(threadId, cm, vipergirlsAuthService, supportedHosts);
}
}
@@ -0,0 +1,38 @@
package tn.mnlr.vripper.services;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.Objects;
@Getter
@ToString
public class QueuedVGLink {
private final String type = "grabQueue";
private final String link;
private final String threadId;
private final String postId;
@Setter
boolean removed;
public QueuedVGLink(String link, String threadId, String postId) {
this.link = link;
this.threadId = threadId;
this.postId = postId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
QueuedVGLink that = (QueuedVGLink) o;
return Objects.equals(link, that.link);
}
@Override
public int hashCode() {
return Objects.hash(link);
}
}
@@ -0,0 +1,86 @@
package tn.mnlr.vripper.services;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tn.mnlr.vripper.VripperApplication;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
@Service
public class VGHandler {
private static final Logger logger = LoggerFactory.getLogger(VGHandler.class);
@Autowired
private AppStateService appStateService;
@Autowired
private PostParser postParser;
@Getter
private LoadingCache<String, List<VRPostState>> cache;
@PostConstruct
private void init() {
CacheLoader<String, List<VRPostState>> loader = new CacheLoader<>() {
@Override
public List<VRPostState> load(String threadId) throws Exception {
VRThreadParser vrThreadParser = postParser.createVRThreadParser(threadId);
List<VRPostState> posts = new ArrayList<>();
vrThreadParser.parse();
vrThreadParser.getPostPublishProcessor()
.onBackpressureBuffer()
.blockingSubscribe(posts::add);
return posts;
}
};
cache = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.MINUTES)
.build(loader);
}
public void handle(List<QueuedVGLink> queuedVGLinks) throws Exception {
for (QueuedVGLink queuedVGLink : queuedVGLinks) {
if (queuedVGLink.getPostId() != null) {
postParser.addPost(queuedVGLink.getPostId(), queuedVGLink.getThreadId());
} else {
appStateService.getGrabQueue().put(queuedVGLink.getLink(), queuedVGLink);
appStateService.getLiveGrabQueue().onNext(queuedVGLink);
Callable<Void> cl = () -> {
List<VRPostState> vrPostStates = cache.get(queuedVGLink.getThreadId());
logger.debug(String.format("%d found for %s", vrPostStates.size(), queuedVGLink.getLink()));
if (vrPostStates.size() == 1) {
postParser.addPost(vrPostStates.get(0).getPostId(), vrPostStates.get(0).getThreadId());
remove(queuedVGLink.getLink());
logger.debug(String.format("threadId %s, postId %s is added automatically for download", queuedVGLink.getThreadId(), queuedVGLink.getPostId()));
}
return null;
};
VripperApplication.commonExecutor.submit(cl);
}
}
}
public void remove(String url) {
Optional.ofNullable(appStateService.getGrabQueue().remove(url)).ifPresent(e -> {
e.setRemoved(true);
appStateService.getLiveGrabQueue().onNext(e);
});
}
}
@@ -99,49 +99,22 @@ class VRPostParser {
}
}
@Getter
class VRPostParseState extends VRPostState {
private final String type = "postParse";
private String postId;
private int number;
private String title;
private int imageCount;
private String url;
private List<String> previews;
VRPostParseState(String threadId, String postId, int number, String title, int imageCount, String url, List<String> previews) {
super(threadId);
this.postId = postId;
this.number = number;
this.title = title;
this.imageCount = imageCount;
this.previews = previews;
this.url = url;
}
}
class VRPostHandler extends DefaultHandler {
private static final Logger logger = LoggerFactory.getLogger(VRPostHandler.class);
private final List<Host> supportedHosts;
private final String threadId;
private final String postId;
private List<Image> images = new ArrayList<>();
private List<String> previews = new ArrayList<>();
private String threadTitle;
private String postTitle;
private String forum;
private int previewCounter = 0;
private int index = 0;
private String postTitle;
private int imageCount;
private String forum;
private List<String> previews = new ArrayList<>();
private List<Image> images = new ArrayList<>();
@Getter
private Post parsedPost;
@@ -172,7 +145,7 @@ class VRPostHandler extends DefaultHandler {
case "image":
index++;
if (previewCounter++ < 4) {
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(thumbUrl -> previews.add(thumbUrl));
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(previews::add);
}
String mainUrl = Optional.ofNullable(attributes.getValue("main_url")).map(String::trim).orElse(null);
@@ -2,11 +2,28 @@ package tn.mnlr.vripper.services;
import lombok.Getter;
@Getter
public abstract class VRPostState {
private final String threadId;
import java.util.List;
VRPostState(String threadId) {
@Getter
public class VRPostState {
private final String type = "postParse";
private final String threadId;
private String postId;
private int number;
private String title;
private int imageCount;
private String url;
private List<String> previews;
private String hosts;
VRPostState(String threadId, String postId, int number, String title, int imageCount, String url, List<String> previews, String hosts) {
this.threadId = threadId;
this.postId = postId;
this.number = number;
this.title = title;
this.imageCount = imageCount;
this.previews = previews;
this.url = url;
this.hosts = hosts;
}
}
@@ -1,6 +1,6 @@
package tn.mnlr.vripper.services;
import io.reactivex.processors.PublishProcessor;
import io.reactivex.processors.ReplayProcessor;
import lombok.Getter;
import net.jodah.failsafe.Failsafe;
import net.jodah.failsafe.RetryPolicy;
@@ -15,16 +15,17 @@ import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import tn.mnlr.vripper.exception.DownloadException;
import tn.mnlr.vripper.exception.PostParseException;
import tn.mnlr.vripper.host.Host;
import javax.xml.parsers.SAXParserFactory;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static tn.mnlr.vripper.services.PostParser.VR_API;
@@ -40,16 +41,18 @@ public class VRThreadParser {
.onFailedAttempt(e -> logger.warn(String.format("#%d tries failed", e.getAttemptCount()), e.getLastFailure()));
private static SAXParserFactory factory = SAXParserFactory.newInstance();
@Getter
private final PublishProcessor<VRPostState> postPublishProcessor = PublishProcessor.create();
private final ReplayProcessor<VRPostState> postPublishProcessor = ReplayProcessor.create();
private final String threadId;
private final ConnectionManager cm;
private final VipergirlsAuthService vipergirlsAuthService;
private final List<Host> hosts;
VRThreadParser(String threadId, ConnectionManager cm, VipergirlsAuthService vipergirlsAuthService) {
VRThreadParser(String threadId, ConnectionManager cm, VipergirlsAuthService vipergirlsAuthService, List<Host> hosts) {
this.threadId = threadId;
this.cm = cm;
this.vipergirlsAuthService = vipergirlsAuthService;
this.hosts = hosts;
}
public Void parse() throws PostParseException {
@@ -64,7 +67,7 @@ public class VRThreadParser {
throw new PostParseException(e);
}
VRThreadHandler handler = new VRThreadHandler(threadId, postPublishProcessor);
VRThreadHandler handler = new VRThreadHandler(threadId, postPublishProcessor, hosts);
AtomicReference<Throwable> thr = new AtomicReference<>();
logger.debug(String.format("Requesting %s", httpGet));
Failsafe.with(retryPolicy)
@@ -97,23 +100,25 @@ public class VRThreadParser {
class VRThreadHandler extends DefaultHandler {
private final String threadId;
private final PublishProcessor<VRPostState> vrPostPublishProcessor;
private final ReplayProcessor<VRPostState> vrPostPublishProcessor;
private final List<Host> supportedHosts;
private final Map<Host, AtomicInteger> hostMap = new HashMap<>();
private List<String> previews = new ArrayList<>();
private String threadTitle;
private String postId;
private String postTitle;
private int imageCount;
private int postCounter;
private int previewCounter = 0;
private List<String> previews = new ArrayList<>();
VRThreadHandler(String threadId, PublishProcessor<VRPostState> vrPostPublishProcessor) {
VRThreadHandler(String threadId, ReplayProcessor<VRPostState> vrPostPublishProcessor, List<Host> supportedHosts) {
this.vrPostPublishProcessor = vrPostPublishProcessor;
this.threadId = threadId;
this.supportedHosts = supportedHosts;
}
@Override
public void startDocument() {
vrPostPublishProcessor.onNext(new VRThreadParseState(threadId, "START"));
}
@Override
@@ -130,8 +135,12 @@ class VRThreadHandler extends DefaultHandler {
postTitle = Optional.ofNullable(attributes.getValue("title")).map(e -> e.trim().isEmpty() ? null : e.trim()).orElse(threadTitle);
break;
case "image":
Optional.ofNullable(attributes.getValue("main_url"))
.map(String::trim)
.flatMap(mainUrl -> supportedHosts.stream().filter(host -> host.isSupported(mainUrl)).findFirst())
.ifPresent(host -> Optional.ofNullable(hostMap.get(host)).ifPresentOrElse(AtomicInteger::incrementAndGet, () -> hostMap.put(host, new AtomicInteger(0))));
if (previewCounter++ < 4) {
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(thumbUrl -> previews.add(thumbUrl));
Optional.ofNullable(attributes.getValue("thumb_url")).map(String::trim).ifPresent(previews::add);
}
break;
}
@@ -141,36 +150,25 @@ class VRThreadHandler extends DefaultHandler {
public void endElement(String uri, String localName, String qName) {
if ("post".equals(qName.toLowerCase())) {
if (imageCount != 0) {
vrPostPublishProcessor.onNext(new VRPostParseState(
vrPostPublishProcessor.onNext(new VRPostState(
threadId,
postId,
postCounter,
postTitle,
imageCount,
String.format("https://vipergirls.to/threads/%s/?p=%s&viewfull=1#post%s", threadId, postId, postId),
previews
previews,
hostMap.entrySet().stream().map(e -> e.getKey().getHost() + " (" + e.getValue().get() + ")").collect(Collectors.joining(", "))
));
}
previewCounter = 0;
previews = new ArrayList<>();
hostMap.clear();
}
}
@Override
public void endDocument() {
vrPostPublishProcessor.onNext(new VRThreadParseState(threadId, "END"));
vrPostPublishProcessor.onComplete();
}
}
@Getter
class VRThreadParseState extends VRPostState {
private final String type = "threadParseState";
private final String state;
VRThreadParseState(String threadId, String state) {
super(threadId);
this.state = state;
}
}
@@ -16,6 +16,6 @@ public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*").withSockJS();
registry.addHandler(handler, "/endpoint").setAllowedOrigins("*").withSockJS().setClientLibraryUrl("../../assets/sockjs.min.js");
}
}
@@ -1,9 +1,6 @@
package tn.mnlr.vripper.web.restendpoints;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import lombok.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -13,16 +10,13 @@ import org.springframework.web.bind.annotation.*;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.exception.PostParseException;
import tn.mnlr.vripper.q.DownloadQ;
import tn.mnlr.vripper.services.AppSettingsService;
import tn.mnlr.vripper.services.AppStateService;
import tn.mnlr.vripper.services.PathService;
import tn.mnlr.vripper.services.PostParser;
import tn.mnlr.vripper.services.*;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@RestController
@CrossOrigin(value = "*")
@@ -41,8 +35,11 @@ public class PostRestEndpoint {
@Autowired
private PathService pathService;
@Autowired
private VGHandler vgHandler;
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception e) {
public ResponseEntity<String> handleException(Exception e) {
logger.error("Error when process request", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
@@ -57,27 +54,33 @@ public class PostRestEndpoint {
@PostMapping("/post")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity processPost(@RequestBody ThreadUrl url) throws Exception {
logger.debug(String.format("Starting to process thread: %s", url.getUrl()));
if (url.getUrl() == null || url.getUrl().isEmpty()) {
return new ResponseEntity<>("Failed to process empty request", HttpStatus.BAD_REQUEST);
} else if (!url.getUrl().startsWith("https://vipergirls.to")) {
return new ResponseEntity<>("ViperGirls only links are supported", HttpStatus.BAD_REQUEST);
public ResponseEntity processPost(@RequestBody ThreadUrl _url) throws Exception {
if (_url.getUrl() == null || _url.getUrl().isEmpty()) {
return new ResponseEntity("Failed to process empty request", HttpStatus.BAD_REQUEST);
}
String threadId, postId;
try {
Matcher m = VG_URL_PATTERN.matcher(url.getUrl());
if (m.find()) {
threadId = m.group(1);
postId = m.group(4);
} else {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url));
List<String> urls = Arrays.stream(_url.getUrl().split("\\r?\\n")).map(String::trim).filter(e -> !e.isEmpty()).collect(Collectors.toList());
for (String url : urls) {
logger.debug(String.format("Starting to process thread: %s", url));
if (!url.startsWith("https://vipergirls.to")) {
logger.error(String.format("Unsupported link %s", url));
continue;
}
} catch (Exception e) {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url), e);
String threadId, postId;
try {
Matcher m = VG_URL_PATTERN.matcher(url);
if (m.find()) {
threadId = m.group(1);
postId = m.group(4);
} else {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url));
}
} catch (Exception e) {
throw new PostParseException(String.format("Cannot retrieve thread id from URL %s", url), e);
}
vgHandler.handle(Collections.singletonList(new QueuedVGLink(url, threadId, postId)));
}
return ResponseEntity.ok(new PairThreadIdPostId(threadId, postId));
return ResponseEntity.ok().build();
}
@PostMapping("/post/restart")
@@ -131,7 +134,7 @@ public class PostRestEndpoint {
@PostMapping("/post/remove")
@ResponseStatus(value = HttpStatus.OK)
public synchronized ResponseEntity remove(@RequestBody @NonNull List<PostId> postIds) {
public synchronized ResponseEntity<List<RemoveResult>> remove(@RequestBody @NonNull List<PostId> postIds) {
List<RemoveResult> result = new ArrayList<>();
for (PostId postId : postIds) {
downloadQ.stop(postId.getPostId());
@@ -143,15 +146,27 @@ public class PostRestEndpoint {
@PostMapping("/post/clear/all")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity clearAll() {
public ResponseEntity<RemoveAllResult> clearAll() {
return ResponseEntity.ok(new RemoveAllResult(appStateService.clearAll()));
}
@PostMapping("/post/remove/all")
@ResponseStatus(value = HttpStatus.OK)
public ResponseEntity removeAll() {
public ResponseEntity<RemoveAllResult> removeAll() {
return ResponseEntity.ok(new RemoveAllResult(appStateService.removeAll()));
}
@GetMapping("/grab/{threadId}")
@ResponseStatus(value = HttpStatus.OK)
public synchronized ResponseEntity<List<VRPostState>> grab(@PathVariable("threadId") @NonNull ThreadId threadId) throws Exception {
return ResponseEntity.ok(vgHandler.getCache().get(threadId.getThreadId()));
}
@PostMapping("/grab/remove")
@ResponseStatus(value = HttpStatus.OK)
public synchronized void grabRemove(@RequestBody @NonNull ThreadUrl threadUrl) {
vgHandler.remove(threadUrl.getUrl());
}
}
@Getter
@@ -168,23 +183,21 @@ class ThreadUrl {
@Getter
@Setter
@NoArgsConstructor
class PostId {
private String postId;
class ThreadId {
private String threadId;
public PostId(String postId) {
this.postId = postId;
public ThreadId(String threadId) {
this.threadId = threadId;
}
}
@Getter
@Setter
@NoArgsConstructor
class PairThreadIdPostId {
private String threadId;
class PostId {
private String postId;
PairThreadIdPostId(String threadId, String postId) {
this.threadId = threadId;
public PostId(String postId) {
this.postId = postId;
}
}
@@ -235,4 +248,32 @@ class DownloadPath {
DownloadPath(String path) {
this.path = path;
}
}
@Getter
@Setter
@NoArgsConstructor
@ToString
class PairThreadIdPostId {
private String threadId;
private String postId;
public PairThreadIdPostId(String threadId, String postId) {
this.threadId = threadId;
this.postId = postId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PairThreadIdPostId that = (PairThreadIdPostId) o;
return Objects.equals(threadId, that.threadId) &&
Objects.equals(postId, that.postId);
}
@Override
public int hashCode() {
return Objects.hash(threadId, postId);
}
}
@@ -14,7 +14,6 @@ import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import tn.mnlr.vripper.VripperApplication;
import tn.mnlr.vripper.entities.Image;
import tn.mnlr.vripper.entities.Post;
import tn.mnlr.vripper.entities.mixin.ui.ImageUIMixin;
@@ -56,10 +55,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
private Map<String, Disposable> postsSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> postDetailsSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> vrPostParserSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> stateSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> downloadSpeedSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> userSubscriptions = new ConcurrentHashMap<>();
private Map<String, Disposable> grabQueueSubscriptions = new ConcurrentHashMap<>();
private Map<String, Future<Void>> threadParseRequests = new ConcurrentHashMap<>();
@@ -71,6 +70,9 @@ public class WebSocketHandler extends TextWebSocketHandler {
WSMessage wsMessage = om.readValue(message.getPayload(), WSMessage.class);
WSMessage.CMD cmd = WSMessage.CMD.valueOf(wsMessage.getCmd());
switch (cmd) {
case GRAB_QUEUE_SUB:
subscribeForGrabQueue(session);
break;
case GLOBAL_STATE_SUB:
subscribeForGlobalState(session);
break;
@@ -83,9 +85,6 @@ public class WebSocketHandler extends TextWebSocketHandler {
case POSTS_SUB:
subscribeForPosts(session);
break;
case THREAD_PARSING_SUB:
subscribeForThreadParsing(session, wsMessage.getPayload());
break;
case POST_DETAILS_SUB:
subscribeForPostDetails(session, wsMessage.getPayload());
break;
@@ -93,11 +92,6 @@ public class WebSocketHandler extends TextWebSocketHandler {
logger.debug(String.format("Client %s unsubscribed from post details", session.getId()));
Optional.ofNullable(postDetailsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
case THREAD_PARSING_UNSUB:
logger.debug(String.format("Client %s unsubscribed from thread parsing", session.getId()));
Optional.ofNullable(vrPostParserSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(threadParseRequests.remove(session.getId())).ifPresent(d -> d.cancel(true));
break;
case POSTS_UNSUB:
logger.debug(String.format("Client %s unsubscribed from posts", session.getId()));
Optional.ofNullable(postsSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
@@ -114,6 +108,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
logger.debug(String.format("Client %s unsubscribed from user info", session.getId()));
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
case GRAB_QUEUE_UNSUB:
logger.debug(String.format("Client %s unsubscribed from grab queue", session.getId()));
Optional.ofNullable(grabQueueSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
break;
}
}
@@ -220,30 +218,30 @@ public class WebSocketHandler extends TextWebSocketHandler {
);
}
private void subscribeForThreadParsing(WebSocketSession session, String threadId) {
private void subscribeForGrabQueue(WebSocketSession session) {
logger.debug(String.format("Client %s subscribed for thread parsing with threadId = %s", session.getId(), threadId));
if (vrPostParserSubscriptions.containsKey(session.getId())) {
vrPostParserSubscriptions.get(session.getId()).dispose();
logger.debug(String.format("Client %s subscribed for grab queue", session.getId()));
if (grabQueueSubscriptions.containsKey(session.getId())) {
grabQueueSubscriptions.get(session.getId()).dispose();
}
if (threadParseRequests.containsKey(session.getId())) {
threadParseRequests.get(session.getId()).cancel(true);
try {
send(session, new TextMessage(om.writeValueAsString(appStateService.getGrabQueue().values())));
} catch (Exception e) {
logger.error("Unexpected error occurred", e);
}
VRThreadParser vrThreadParser = postParser.createVRThreadParser(threadId);
vrPostParserSubscriptions.put(session.getId(), vrThreadParser.getPostPublishProcessor()
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.buffer(200)
.filter(e -> !e.isEmpty())
.map(om::writeValueAsString)
.map(TextMessage::new)
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
grabQueueSubscriptions.put(session.getId(),
appStateService.getLiveGrabQueue()
.onBackpressureBuffer()
.observeOn(Schedulers.io())
.buffer(2000, TimeUnit.MILLISECONDS, 200)
.filter(e -> !e.isEmpty())
.map(e -> e.stream().distinct().collect(Collectors.toList()))
.map(om::writeValueAsString)
.map(TextMessage::new)
.subscribe(msg -> send(session, msg), e -> logger.error("Failed to send data to client", e))
);
threadParseRequests.put(session.getId(), VripperApplication.commonExecutor.submit(vrThreadParser::parse));
}
private void subscribeForPostDetails(WebSocketSession session, String postId) {
@@ -295,7 +293,7 @@ public class WebSocketHandler extends TextWebSocketHandler {
Optional.ofNullable(stateSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(downloadSpeedSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(userSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(vrPostParserSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(grabQueueSubscriptions.remove(session.getId())).ifPresent(Disposable::dispose);
Optional.ofNullable(threadParseRequests.remove(session.getId())).ifPresent(d -> d.cancel(true));
logger.debug(String.format("Connection closed for client id: %s", session.getId()));
@@ -318,10 +316,10 @@ public class WebSocketHandler extends TextWebSocketHandler {
GLOBAL_STATE_UNSUB,
SPEED_SUB,
SPEED_UNSUB,
THREAD_PARSING_SUB,
THREAD_PARSING_UNSUB,
USER_SUB,
USER_UNSUB
USER_UNSUB,
GRAB_QUEUE_SUB,
GRAB_QUEUE_UNSUB
}
}
+5278 -2434
View File
File diff suppressed because it is too large Load Diff
+21 -21
View File
@@ -1,6 +1,6 @@
{
"name": "vripper-ui",
"version": "2.3.0",
"version": "2.5.1",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -13,31 +13,31 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^7.2.3",
"@angular/cdk": "^7.3.1",
"@angular/common": "~7.2.0",
"@angular/compiler": "~7.2.0",
"@angular/core": "~7.2.0",
"@angular/flex-layout": "^7.0.0-beta.23",
"@angular/forms": "~7.2.0",
"@angular/material": "^7.3.0",
"@angular/platform-browser": "~7.2.0",
"@angular/platform-browser-dynamic": "~7.2.0",
"@angular/router": "~7.2.0",
"ag-grid-angular": "^21.2.1",
"ag-grid-community": "^21.2.1",
"@angular/animations": "^8.2.14",
"@angular/cdk": "^8.2.3",
"@angular/common": "~8.2.14",
"@angular/compiler": "~8.2.14",
"@angular/core": "~8.2.14",
"@angular/flex-layout": "^8.0.0-beta.27",
"@angular/forms": "~8.2.14",
"@angular/material": "^8.2.3",
"@angular/platform-browser": "~8.2.14",
"@angular/platform-browser-dynamic": "~8.2.14",
"@angular/router": "~8.2.14",
"ag-grid-angular": "^22.1.0",
"ag-grid-community": "^22.1.0",
"core-js": "^2.5.4",
"hammerjs": "^2.0.8",
"ngx-electron": "^2.1.1",
"rxjs": "~6.3.3",
"ngx-electron": "^2.2.0",
"rxjs": "~6.5.3",
"tslib": "^1.9.0",
"zone.js": "~0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "^0.13.9",
"@angular/cli": "~7.2.2",
"@angular/compiler-cli": "^7.2.15",
"@angular/language-service": "~7.2.0",
"@angular-devkit/build-angular": "^0.803.20",
"@angular/cli": "~8.3.20",
"@angular/compiler-cli": "^8.2.14",
"@angular/language-service": "~8.2.14",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.9.4",
@@ -53,6 +53,6 @@
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~3.2.2"
"typescript": "~3.5.0"
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>tn.mnlr</groupId>
<artifactId>vripper</artifactId>
<version>2.3.0</version>
<version>2.5.1</version>
</parent>
<artifactId>vripper-ui</artifactId>
<name>vripper-ui</name>
+22 -14
View File
@@ -1,17 +1,25 @@
<div fxLayout="column" *ngIf="connecting()" class="overlay loading" fxLayoutAlign="center center">
<mat-spinner></mat-spinner>
<h2>Loading...</h2>
</div>
<div fxLayout="column" *ngIf="noConnectionState()" class="overlay no-connection" fxLayoutAlign="center center">
<mat-icon class="overlay-icon">error</mat-icon>
<h2>The app seems to be shutdown</h2>
</div>
<div [ngClass]="{ electron: electronService.isElectronApp }" fxLayout="column" id="app-container">
<div id="app-header" fxFlex="nogrow">
<app-toolbar></app-toolbar>
<ng-container *ngIf="appState | async as state">
<div *ngIf="state === 'CONNECTING'" class="overlay loading" fxLayout="column" fxLayoutAlign="center center">
<div class="sk-chase">
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
<div class="sk-chase-dot"></div>
</div>
</div>
<div id="app-body" fxFlex="grow">
<router-outlet></router-outlet>
<div *ngIf="state === 'DISCONNECTED'" class="overlay no-connection" fxLayout="column" fxLayoutAlign="center center">
<mat-icon class="overlay-icon">error</mat-icon>
<h2>Error connecting to daemon, try restarting the application</h2>
</div>
<div *ngIf="state === 'CONNECTED'" [ngClass]="{ electron: electron | async }" fxLayout="column" id="app-container">
<div fxFlex="nogrow" id="app-header">
<app-toolbar></app-toolbar>
</div>
<div fxFlex="grow" id="app-body" style="margin-bottom: 20px;">
<router-outlet></router-outlet>
</div>
<app-status-bar style="width: 100%"></app-status-bar>
</div>
</div>
</ng-container>
+72 -9
View File
@@ -1,13 +1,76 @@
#title {
user-select: none
}
.overlay-icon {
font-size: 128px;
width: 128px;
height: 128px;
font-size: 64px;
width: 64px;
height: 64px;
}
.no-connection {
color: #eb6060;
.no-connection, .loading {
background-color: #2c3e50;
}
.overlay {
position: absolute;
display: block;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
}
.sk-chase {
width: 60px;
height: 60px;
position: relative;
animation: sk-chase 2.5s infinite linear both;
}
.sk-chase-dot {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
animation: sk-chase-dot 2.0s infinite ease-in-out both;
}
.sk-chase-dot:before {
content: '';
display: block;
width: 25%;
height: 25%;
background-color: #fff;
border-radius: 100%;
animation: sk-chase-dot-before 2.0s infinite ease-in-out both;
}
.sk-chase-dot:nth-child(1) { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2) { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3) { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4) { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5) { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6) { animation-delay: -0.6s; }
.sk-chase-dot:nth-child(1):before { animation-delay: -1.1s; }
.sk-chase-dot:nth-child(2):before { animation-delay: -1.0s; }
.sk-chase-dot:nth-child(3):before { animation-delay: -0.9s; }
.sk-chase-dot:nth-child(4):before { animation-delay: -0.8s; }
.sk-chase-dot:nth-child(5):before { animation-delay: -0.7s; }
.sk-chase-dot:nth-child(6):before { animation-delay: -0.6s; }
@keyframes sk-chase {
100% { transform: rotate(360deg); }
}
@keyframes sk-chase-dot {
80%, 100% { transform: rotate(360deg); }
}
@keyframes sk-chase-dot-before {
50% {
transform: scale(0.4);
} 100%, 0% {
transform: scale(1.0);
}
}
+21 -30
View File
@@ -1,56 +1,47 @@
import { AppService } from './app.service';
import { ClipboardService } from './clipboard.service';
import { ElectronService } from 'ngx-electron';
import { Component, OnInit, OnDestroy, NgZone, AfterViewInit, Renderer2 } from '@angular/core';
import { Component, OnDestroy, AfterViewInit, Renderer2, ChangeDetectionStrategy, NgZone } from '@angular/core';
import { MatDialog } from '@angular/material';
import { Subscription } from 'rxjs';
import { Subscription, BehaviorSubject, Subject } from 'rxjs';
import { WsConnectionService, WSState } from './ws-connection.service';
import { WsHandler } from './ws-handler';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit, OnDestroy, AfterViewInit {
export class AppComponent implements OnDestroy, AfterViewInit {
constructor(
public dialog: MatDialog,
private dialog: MatDialog,
private ws: WsConnectionService,
public electronService: ElectronService,
private clipboardService: ClipboardService,
private ngZone: NgZone,
private appService: AppService,
private renderer: Renderer2
) {}
subscriptions: Subscription[] = [];
websocketHandlerPromise: Promise<WsHandler>;
currentState: WSState;
themeLoaded = false;
noConnectionState(): boolean {
return this.currentState === WSState.CLOSE || this.currentState === WSState.ERROR;
private renderer: Renderer2,
private ngZone: NgZone
) {
this.electron = new BehaviorSubject(electronService.isElectronApp);
}
connecting(): boolean {
return (this.currentState === WSState.INIT || this.currentState === WSState.CONNECTING) && !this.themeLoaded ;
}
private subscriptions: Subscription[] = [];
appState: Subject<string> = new BehaviorSubject('CONNECTING');
electron: Subject<boolean>;
ngAfterViewInit() {
this.appService.renderer = this.renderer;
}
ngOnInit() {
this.currentState = WSState.INIT;
this.subscriptions.push(this.ws.state.subscribe(e => {
this.currentState = e;
if (this.currentState === WSState.CLOSE || this.currentState === WSState.ERROR) {
this.dialog.closeAll();
} else if (this.currentState === WSState.OPEN) {
this.subscriptions.push(this.ws.state.subscribe(wsState => {
if (wsState === WSState.CLOSE || wsState === WSState.ERROR) {
setTimeout(() => this.ngZone.run(() => {
this.dialog.closeAll();
this.appState.next('DISCONNECTED');
}), 500);
} else if (wsState === WSState.OPEN) {
this.clipboardService.init();
this.appService
.loadTheme()
.subscribe(() => this.ngZone.run(() => this.themeLoaded = true));
.subscribe(() => this.ngZone.run(() => this.appState.next('CONNECTED')));
}
}));
}
+9 -3
View File
@@ -34,6 +34,8 @@ import { ScanComponent } from './scan/scan.component';
import { StatusBarComponent } from './status-bar/status-bar.component';
import { SelectionService } from './selection-service';
import { ToolbarComponent } from './toolbar/toolbar.component';
import { GrabQueueComponent } from './grab-queue/grab-queue.component';
import { UrlGrabRendererComponent } from './grab-queue/url-renderer.component';
@NgModule({
declarations: [
@@ -50,17 +52,20 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
AppPreviewComponent,
AppPreviewDirective,
UrlRendererComponent,
UrlGrabRendererComponent,
FilterComponent,
ScanComponent,
StatusBarComponent,
ToolbarComponent
ToolbarComponent,
GrabQueueComponent
],
entryComponents: [
PostDetailComponent,
SettingsComponent,
ConfirmDialogComponent,
AppPreviewComponent,
ScanComponent
ScanComponent,
MultiPostComponent
],
imports: [
BrowserAnimationsModule,
@@ -74,7 +79,8 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
AgGridModule.withComponents([
PostProgressRendererComponent,
PostDetailsProgressRendererComponent,
UrlRendererComponent
UrlRendererComponent,
UrlGrabRendererComponent
]),
OverlayModule
],
+1 -5
View File
@@ -1,6 +1,6 @@
import { MatDialog } from '@angular/material';
import { HttpClient } from '@angular/common/http';
import { Injectable, Renderer2, NgZone } from '@angular/core';
import { Injectable, Renderer2 } from '@angular/core';
import { ServerService } from './server-service';
import { tap } from 'rxjs/operators';
import { ScanComponent } from './scan/scan.component';
@@ -23,7 +23,6 @@ export class AppService {
}
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
isScanOpen = false;
updateTheme(darkTheme: boolean) {
this.darkTheme = darkTheme;
@@ -65,11 +64,8 @@ export class AppService {
}
});
scanDialog.afterOpened().subscribe(() => this.isScanOpen = true);
scanDialog.afterClosed().subscribe(() => {
smallDialogSubscription.unsubscribe();
this.isScanOpen = false;
});
}
}
+3 -1
View File
@@ -5,10 +5,12 @@ export enum CMD {
SPEED_SUB = 'SPEED_SUB',
USER_SUB = 'USER_SUB',
THREAD_PARSING_SUB = 'THREAD_PARSING_SUB',
GRAB_QUEUE_SUB = 'GRAB_QUEUE_SUB',
POSTS_UNSUB = 'POSTS_UNSUB',
POST_DETAILS_UNSUB = 'POST_DETAILS_UNSUB',
GLOBAL_STATE_UNSUB = 'GLOBAL_STATE_UNSUB',
SPEED_UNSUB = 'SPEED_UNSUB',
USER_UNSUB = 'USER_UNSUB',
THREAD_PARSING_UNSUB = 'THREAD_PARSING_UNSUB'
THREAD_PARSING_UNSUB = 'THREAD_PARSING_UNSUB',
GRAB_QUEUE_UNSUB = 'GRAB_QUEUE_UNSUB'
}
@@ -4,11 +4,10 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
@Component({
selector: 'app-preview',
template: `
<div class="previews" style="display: inline-block; white-space: nowrap;">
<div class="previews" style="display: inline-block; white-space: nowrap; height: 200px; max-width: 400px">
<ng-container *ngFor="let link of links">
<img style="display: inline-block; max-width:150px; min-width:150px; width: auto; height: auto;"
<img
[@simpleFadeAnimation]="'in'"
class="mat-elevation-z8"
[src]="link"
/>
</ng-container>
@@ -18,7 +17,7 @@ import { trigger, state, style, transition, animate } from '@angular/animations'
`
img {
max-height: 200px;
max-width: 200px;
max-width: 400px;
margin-left: 5px;
}
`
@@ -1,10 +1,10 @@
import { AppPreviewComponent } from './preview-tooltip.component';
import { Directive, Input, HostListener, ComponentRef, OnInit, ElementRef, NgZone } from '@angular/core';
import { Directive, Input, HostListener, ComponentRef, OnInit, ElementRef, NgZone, OnDestroy } from '@angular/core';
import { OverlayRef, Overlay, OverlayPositionBuilder } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
@Directive({ selector: '[appPreview]' })
export class AppPreviewDirective implements OnInit {
export class AppPreviewDirective implements OnInit, OnDestroy {
private overlayRef: OverlayRef;
tooltipPortal = new ComponentPortal(AppPreviewComponent);
@@ -21,14 +21,25 @@ export class AppPreviewDirective implements OnInit {
.withPush(true)
.withGrowAfterOpen(true)
.withPositions([{
originX: 'start',
originX: 'center',
originY: 'bottom',
overlayX: 'start',
overlayX: 'center',
overlayY: 'top',
}, {
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom',
}]);
this.overlayRef = this.overlay.create({ positionStrategy });
}
ngOnDestroy() {
this.ngZone.run(() => {
this.overlayRef.dispose();
});
}
@HostListener('mouseenter')
show() {
this.ngZone.run(() => {
@@ -6,10 +6,7 @@ export class VRPostParse {
public title: string,
public imageCount: number,
public url: string,
public previews: string[]
public previews: string[],
public hosts: string
) {}
}
export class VRThreadParseState {
constructor(public threadId: string, public state: string) {}
}
@@ -0,0 +1,2 @@
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
</ag-grid-angular>
@@ -0,0 +1,47 @@
import { UrlGrabRendererComponent } from './url-renderer.component';
import { GrabQueueDataSource } from './grab-queue.datasource';
import { Component, OnInit, NgZone } from '@angular/core';
import { GridOptions } from 'ag-grid-community';
import { WsConnectionService } from '../ws-connection.service';
@Component({
selector: 'app-grab-queue',
templateUrl: './grab-queue.component.html',
styleUrls: ['./grab-queue.component.scss']
})
export class GrabQueueComponent implements OnInit {
constructor(private wsConnection: WsConnectionService, private zone: NgZone) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
headerName: 'Url',
field: 'link',
sortable: true,
sort: 'asc',
cellRenderer: 'urlCellRenderer'
}
],
rowHeight: 48,
animateRows: true,
rowData: [],
frameworkComponents: {
urlCellRenderer: UrlGrabRendererComponent
},
overlayLoadingTemplate: '<span></span>',
overlayNoRowsTemplate: '<span></span>',
getRowNodeId: data => data['link'],
onGridReady: () => {
this.gridOptions.api.sizeColumnsToFit();
this.dataSource = new GrabQueueDataSource(this.wsConnection, this.gridOptions, this.zone);
this.dataSource.connect();
},
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit()
};
}
gridOptions: GridOptions;
dataSource: GrabQueueDataSource;
ngOnInit() {}
}
@@ -0,0 +1,60 @@
import { GrabQueueState } from './grab-queue.model';
import { CMD } from './../common/cmd.enum';
import { WSMessage } from './../common/ws-message.model';
import { Subscription } from 'rxjs';
import { WsConnectionService } from '../ws-connection.service';
import { GridOptions } from 'ag-grid-community';
import { WsHandler } from '../ws-handler';
import { NgZone } from '@angular/core';
export class GrabQueueDataSource {
constructor(
private wsConnectionService: WsConnectionService,
private gridOptions: GridOptions,
private zone: NgZone
) {
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
}
websocketHandlerPromise: Promise<WsHandler>;
subscriptions: Subscription[] = [];
connect() {
this.websocketHandlerPromise.then((handler: WsHandler) => {
console.log('Connecting to grab queue datasource');
this.subscriptions.push(
handler.subscribeForGrabQueue((e: GrabQueueState[]) => {
this.zone.run(() => {
const toAdd = [];
const toUpdate = [];
const toRemove = [];
e.forEach(v => {
if (v.removed) {
if (this.gridOptions.api.getRowNode(v.link) != null) {
toRemove.push(this.gridOptions.api.getRowNode(v.link).data);
}
return;
}
if (this.gridOptions.api.getRowNode(v.link) == null) {
toAdd.push(v);
} else {
toUpdate.push(v);
}
});
this.gridOptions.api.updateRowData({ update: toUpdate, add: toAdd, remove: toRemove });
});
})
);
handler.send(new WSMessage(CMD.GRAB_QUEUE_SUB.toString()));
});
}
disconnect() {
console.log('Disconnecting from grab queue datasource');
this.subscriptions.forEach(e => e.unsubscribe());
this.websocketHandlerPromise.then((handler: WsHandler) => {
handler.send(new WSMessage(CMD.GRAB_QUEUE_UNSUB.toString()));
});
}
}
@@ -0,0 +1,9 @@
export class GrabQueueState {
constructor(
public type: string,
public link: string,
public threadId: string,
public postId: string,
public removed: boolean
) {}
}
@@ -0,0 +1,94 @@
import { ElectronService } from 'ngx-electron';
import { MultiPostComponent } from './../multi-post/multi-post.component';
import { OnInit, OnDestroy, Component, ChangeDetectionStrategy } from '@angular/core';
import { AgRendererComponent } from 'ag-grid-angular';
import { ICellRendererParams } from 'ag-grid-community';
import { GrabQueueState } from './grab-queue.model';
import { MatDialog, MatSnackBar } from '@angular/material';
import { BreakpointObserver, BreakpointState, Breakpoints } from '@angular/cdk/layout';
import { Observable } from 'rxjs';
import { ServerService } from '../server-service';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-url-cell-grab',
template: `
<div fxLayout="row" fxLayoutAlign="space-between center">
<span><a (click)="goTo()" href="javascript:void(0)">{{ grabQueue.link }}</a></span>
<span>
<button style="margin-right: 5px" (click)="grab()" color="primary" mat-mini-fab><mat-icon>get_app</mat-icon></button>
<button (click)="remove()" color="primary" mat-mini-fab><mat-icon>delete</mat-icon></button>
</span>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UrlGrabRendererComponent implements OnInit, OnDestroy, AgRendererComponent {
constructor(
public dialog: MatDialog,
private breakpointObserver: BreakpointObserver,
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar,
private electronService: ElectronService
) {}
grabQueue: GrabQueueState;
params: ICellRendererParams;
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
ngOnInit(): void {}
ngOnDestroy(): void {}
agInit(params: ICellRendererParams): void {
this.grabQueue = params.data;
}
refresh(params: ICellRendererParams): boolean {
return false;
}
grab() {
const dialogRef = this.dialog.open(MultiPostComponent, {
width: '90%',
height: '90%',
maxWidth: '100vw',
maxHeight: '100vh',
data: this.grabQueue
});
const smallDialogSubscription = this.isExtraSmall.subscribe(result => {
if (result.matches) {
dialogRef.updateSize('100%', '100%');
} else {
dialogRef.updateSize('90%', '90%');
}
});
dialogRef.afterClosed().subscribe(() => {
if (smallDialogSubscription != null) {
smallDialogSubscription.unsubscribe();
}
});
}
goTo() {
if (this.electronService.isElectronApp) {
this.electronService.shell.openExternal(this.grabQueue.link);
} else {
window.open(this.grabQueue.link, '_blank');
}
}
remove() {
this.httpClient.post(this.serverService.baseUrl + '/grab/remove', { url: this.grabQueue.link }).subscribe(
() => {},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
}
+8 -3
View File
@@ -1,3 +1,8 @@
<div style="height: calc(100% - 20px)">
<app-posts style="width: 100%; height: 100%;"></app-posts>
</div>
<mat-tab-group dynamicHeight style="height: 100%; margin-bottom: 20px; overflow: hidden;">
<mat-tab label="Downloads">
<app-posts style="width: 100%; height: 100%;"></app-posts>
</mat-tab>
<mat-tab label="Link Collector">
<app-grab-queue style="width: 100%; height: 100%;"></app-grab-queue>
</mat-tab>
</mat-tab-group>
@@ -0,0 +1,7 @@
table {
width: 100%;
}
:host ::ng-deep .mat-tab-body-wrapper {
height: 100%;
}
+19 -12
View File
@@ -1,8 +1,9 @@
import { AppService } from './../app.service';
import { ServerService } from './../server-service';
import { ElectronService } from 'ngx-electron';
import { ClipboardService } from './../clipboard.service';
import { Component, OnInit, OnDestroy, NgZone, ChangeDetectionStrategy } from '@angular/core';
import { MatDialog, MatSnackBar } from '@angular/material';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-home',
@@ -11,12 +12,12 @@ import { MatDialog, MatSnackBar } from '@angular/material';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent implements OnInit, OnDestroy {
constructor(
private clipboardService: ClipboardService,
public dialog: MatDialog,
public electronService: ElectronService,
private appService: AppService,
private httpClient: HttpClient,
private serverService: ServerService,
private _snackBar: MatSnackBar,
private ngZone: NgZone
) {}
@@ -24,17 +25,23 @@ export class HomeComponent implements OnInit, OnDestroy {
ngOnInit() {
this.clipboardService.links.subscribe(e => {
this.ngZone.run(() => {
if (this.appService.isScanOpen) {
this._snackBar.open(e + ' was not processed, the app is busy parsing another thread', null, {
duration: 5000
});
return;
}
this.appService.scan(e);
this.httpClient
.post<{ threadId: string; postId: string }>(this.serverService.baseUrl + '/post', { url: e })
.subscribe(
response => {
this._snackBar.open('Clipboard successfully scanned', null, {
duration: 5000
});
},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
});
});
}
ngOnDestroy() {
}
ngOnDestroy() {}
}
-2
View File
@@ -7,7 +7,6 @@ import {
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
MatMenuModule,
MatDialogModule,
MatCardModule,
@@ -28,7 +27,6 @@ import {
MatIconModule,
MatInputModule,
MatFormFieldModule,
MatProgressSpinnerModule,
MatMenuModule,
MatDialogModule,
MatCardModule,
@@ -1,14 +1,27 @@
<div fxLayout="column" style="height: 100%;">
<div class="dialog-container" fxLayout="column" style="height: 100%;">
<div fxFlex="nogrow">
<form autocomplete="off" fxLayout="row" fxLayoutAlign="left center" fxLayoutGap="20px">
<mat-form-field fxFlex="1 1 50%">
<input (ngModelChange)="search($event)" matInput name="search" ngModel placeholder="Search"/>
</mat-form-field>
</form>
</div>
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
</ag-grid-angular>
<div style="height: 4px; width: 100%; padding: 5px 0 0">
<mat-progress-bar *ngIf="loading | async" mode="indeterminate"></mat-progress-bar>
<h2 class="no-wrap" mat-dialog-title>Select posts</h2>
</div>
<mat-dialog-content fxFlex="grow" fxLayout="column">
<div fxLayout="column" style="height: 100%;">
<div fxFlex="nogrow">
<form autocomplete="off" fxLayout="row" fxLayoutAlign="left center" fxLayoutGap="20px">
<mat-form-field fxFlex="1 1 50%">
<input (ngModelChange)="search($event)" matInput name="search" ngModel placeholder="Search"/>
</mat-form-field>
</form>
</div>
<ag-grid-angular [gridOptions]="gridOptions" class="ag-theme-material" style="width: 100%; height: 100%;">
</ag-grid-angular>
<div style="height: 4px; width: 100%; padding: 5px 0 0">
<mat-progress-bar *ngIf="(loading | async)" mode="indeterminate"></mat-progress-bar>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" fxFlex="nogrow">
<button (click)="onNoClick()" mat-raised-button>Close</button>
<button (click)="submit()" [disabled]="(selectedRowsCount | async) === 0" color="primary" mat-raised-button>
Download
</button>
</mat-dialog-actions>
</div>
@@ -1,15 +1,23 @@
import { VRPostParse, VRThreadParseState } from './../common/vr-post-parse.model';
import { Component, OnInit, NgZone, OnDestroy, Input, Output, EventEmitter, ChangeDetectionStrategy, AfterViewInit } from '@angular/core';
import { MatSnackBar } from '@angular/material';
import { GridOptions } from 'ag-grid-community';
import { finalize } from 'rxjs/operators';
import { VRPostParse } from './../common/vr-post-parse.model';
import {
Component,
OnInit,
NgZone,
OnDestroy,
EventEmitter,
ChangeDetectionStrategy,
AfterViewInit,
Inject
} from '@angular/core';
import { MatSnackBar, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { GridOptions, RowNode } from 'ag-grid-community';
import { UrlRendererComponent } from './url-renderer.component';
import { WsHandler } from '../ws-handler';
import { Subscription } from 'rxjs';
import { WSMessage } from '../common/ws-message.model';
import { CMD } from '../common/cmd.enum';
import { WsConnectionService } from '../ws-connection.service';
import { Subscription, concat, merge, BehaviorSubject, Subject } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { ServerService } from '../server-service';
import { GrabQueueState } from '../grab-queue/grab-queue.model';
@Component({
selector: 'app-multi-post',
@@ -18,13 +26,6 @@ import { ServerService } from '../server-service';
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
@Input()
threadId: string;
@Output()
done: EventEmitter<boolean> = new EventEmitter();
gridOptions: GridOptions;
websocketHandlerPromise: Promise<WsHandler>;
subscription: Subscription;
@@ -33,12 +34,13 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
constructor(
private ngZone: NgZone,
private _snackBar: MatSnackBar,
private wsConnectionService: WsConnectionService,
private httpClient: HttpClient,
private serverService: ServerService
) {
this.websocketHandlerPromise = this.wsConnectionService.getConnection();
}
private serverService: ServerService,
public dialogRef: MatDialogRef<MultiPostComponent>,
@Inject(MAT_DIALOG_DATA) public dialogData: GrabQueueState
) {}
selectedRowsCount: Subject<number> = new BehaviorSubject(0);
ngAfterViewInit(): void {
this.loading.emit(true);
@@ -67,6 +69,11 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
field: 'url',
cellClass: 'no-padding',
cellRenderer: 'urlCellRenderer'
},
{
headerName: 'Hosts',
field: 'hosts',
cellClass: 'no-padding'
}
],
defaultColDef: {
@@ -86,80 +93,66 @@ export class MultiPostComponent implements OnInit, OnDestroy, AfterViewInit {
getRowNodeId: data => data['url'],
onGridReady: () => {
this.gridOptions.api.sizeColumnsToFit();
this.connect();
this.grab();
},
onGridSizeChanged: () => this.gridOptions.api.sizeColumnsToFit(),
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit()
onRowDataUpdated: () => this.gridOptions.api.sizeColumnsToFit(),
onSelectionChanged: () => this.onSelectionChange(this.gridOptions.api.getSelectedNodes())
};
}
ngOnDestroy() {
this.unsubscribe();
onSelectionChange(data: RowNode[]) {
this.selectedRowsCount.next(data.length);
}
ngOnDestroy() {}
search(event) {
this.gridOptions.api.setQuickFilter(event);
}
private unsubscribe() {
if (this.subscription != null) {
this.subscription.unsubscribe();
}
this.websocketHandlerPromise.then((handler: WsHandler) => {
handler.send(new WSMessage(CMD.THREAD_PARSING_UNSUB.toString()));
});
}
connect() {
this.websocketHandlerPromise.then((handler: WsHandler) => {
console.log('Connecting to thread parsing');
this.subscription = handler.subscribeForThreadParsing(
(states: Array<VRThreadParseState>, data: Array<VRPostParse>) => {
if (data[0].threadId === this.threadId) {
this.gridOptions.api.updateRowData({ add: data });
}
if (states.length > 0 && states[states.length - 1].state === 'END' && states[0].threadId === this.threadId) {
this.ngZone.run(() => {
this.loading.emit(false);
if (this.gridOptions.api.getDisplayedRowCount() === 1) {
this.addPosts(
[this.gridOptions.api.getDisplayedRowAtIndex(0).data].map(e => ({
threadId: e.threadId,
postId: e.postId
}))
);
this.done.emit(true);
}
});
}
}
);
handler.send(new WSMessage(CMD.THREAD_PARSING_SUB.toString(), this.threadId));
});
}
submit() {
this.loading.emit(true);
const data = (<VRPostParse[]>this.gridOptions.api.getSelectedRows()).map(e => ({
postId: e.postId,
threadId: this.threadId
threadId: this.dialogData.threadId
}));
this.addPosts(data);
merge(
this.httpClient.post(this.serverService.baseUrl + '/post/add', data),
this.httpClient.post(this.serverService.baseUrl + '/grab/remove', { url: this.dialogData.link })
)
.pipe(finalize(() => {
this.dialogRef.close();
this.loading.emit(false);
}))
.subscribe(
() => {},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
}
addPosts(data: { postId: string; threadId: string }[]) {
this.httpClient.post(this.serverService.baseUrl + '/post/add', data).subscribe(
() => {
this._snackBar.open('Adding posts to queue', null, {
duration: 5000
});
this.done.emit();
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
grab() {
this.httpClient
.get<VRPostParse[]>(this.serverService.baseUrl + '/grab/' + this.dialogData.threadId)
.pipe(finalize(() => this.loading.emit(false)))
.subscribe(
data => {
this.gridOptions.api.updateRowData({ add: data });
},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
);
}
onNoClick() {
this.dialogRef.close();
}
}
@@ -22,6 +22,14 @@ export class PostDetailComponent implements OnInit, OnDestroy {
this.gridOptions = <GridOptions> {
columnDefs: [
{
headerName: '#',
field: 'index',
sortable: true,
sort: 'asc',
cellClass: 'col-white',
width: 10
},
{
headerName: 'URL',
field: 'url',
@@ -14,7 +14,8 @@
style="background-color: transparent; width: 100%; height: 100%;"
>
<div class="progress-bar" style="position: absolute; width: 100%; top: 37px; padding: 0 20px">
<mat-progress-bar [value]="details.progress" class="example-margin" color="primary" mode="determinate">
<mat-progress-bar *ngIf="(loaded | async)" [value]="details.progress" class="example-margin" color="primary"
mode="determinate">
</mat-progress-bar>
</div>
<span fxLayout="row">
@@ -9,7 +9,7 @@ import {
AfterViewInit
} from '@angular/core';
import { AgRendererComponent } from 'ag-grid-angular';
import { Subscription } from 'rxjs';
import { Subscription, Subject, BehaviorSubject } from 'rxjs';
import { PostDetails } from './post-details.model';
import { WsHandler } from '../ws-handler';
import { ICellRendererParams } from 'ag-grid-community';
@@ -34,6 +34,8 @@ export class PostDetailsProgressRendererComponent implements AgRendererComponent
subscription: Subscription;
postDetails$: EventEmitter<PostDetails> = new EventEmitter();
private postDetails: PostDetails;
loaded: Subject<boolean> = new BehaviorSubject(false);
loading;
trunc(value: number): number {
return Math.trunc(value);
@@ -64,12 +66,14 @@ export class PostDetailsProgressRendererComponent implements AgRendererComponent
ngAfterViewInit(): void {
this.postDetails$.emit(this.postDetails);
this.loading = setTimeout(() => this.loaded.next(true), 100);
}
ngOnDestroy(): void {
if (this.subscription != null) {
this.subscription.unsubscribe();
}
clearTimeout(this.loading);
}
agInit(params: ICellRendererParams): void {
@@ -1,10 +1,10 @@
export class PostDetails {
constructor(public postId: string, public postName: string, public url: string, public progress: number, public status: string) {
this.postId = postId;
this.postName = postName;
this.url = url;
this.progress = progress;
this.status = status;
}
}
constructor(
public postId: string,
public postName: string,
public url: string,
public progress: number,
public status: string,
public index: number
) {}
}
@@ -1,110 +1,3 @@
<!-- <div class="container" style="height: 100%; background-color: white">
<div
[ngClass]="{
error: postState.status === 'ERROR',
downloading: postState.status === 'DOWNLOADING' || postState.status === 'PARTIAL',
complete: postState.status === 'COMPLETE',
stopped: postState.status === 'STOPPED'
}"
class="progress-bar-back"
style="position: relative; height: 100%; max-height: 48px;"
>
<div
class="progress-foreground"
fxLayout="row"
fxLayoutAlign="space-between"
style="background-color: transparent; width: 100%; height: 100%;"
>
<div class="progress-bar" color="accent" style="position: absolute; width: 100%; top: 37px; padding: 0 50px">
<mat-progress-bar [value]="postState.progress" class="example-margin" color="primary" mode="determinate">
</mat-progress-bar>
</div>
<span fxLayout="row">
<span (click)="toggleExpand()" class="chevron">
<button *ngIf="expanded; else notExpanded" mat-icon-button>
<mat-icon>expand_more</mat-icon>
</button>
<ng-template #notExpanded>
<button mat-icon-button>
<mat-icon>chevron_right</mat-icon>
</button>
</ng-template>
</span>
<span>{{ postState.title }}</span>
<span class="filler" fxFlex="grow"></span>
</span>
<span>
<span class="progress-percentage">{{ postState.done + '/' + postState.total }}</span>
<button [matMenuTriggerFor]="menu" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button
(click)="restart()"
*ngIf="
(postState.status === 'COMPLETE' && postState.progress !== 100) ||
postState.status === 'ERROR' ||
postState.status === 'STOPPED'
"
mat-menu-item
>
<mat-icon>play_arrow</mat-icon>
<span>Start</span>
</button>
<button
(click)="stop()"
*ngIf="
postState.status === 'DOWNLOADING' || postState.status === 'PARTIAL' || postState.status === 'PENDING'
"
mat-menu-item
>
<ng-container>
<mat-icon>stop</mat-icon>
<span>Stop</span>
</ng-container>
</button>
<button (click)="seeDetails()" mat-menu-item>
<mat-icon>list</mat-icon>
<span>Files</span>
</button>
<button (click)="remove()" mat-menu-item>
<mat-icon>delete</mat-icon>
<span>Remove</span>
</button>
<button (click)="open()" *ngIf="electronService.isElectronApp" mat-menu-item>
<mat-icon>open_in_new</mat-icon>
<span>Download Location</span>
</button>
</mat-menu>
</span>
</div>
</div>
<section *ngIf="expanded">
<div class="details table">
<div class="row" style="display: table-row">
<h4 class="cell attribute">Status:</h4>
<p class="cell value">
{{ postState.status | titlecase }}
</p>
</div>
<div class="row" style="display: table-row">
<h4 class="cell attribute">Post URL:</h4>
<p class="cell value">
<a (click)="goTo()" [appPreview]="postState.previews" href="javascript:void(0)"
>https://vipergirls/threads/?p={{ postState.postId }}</a
>
</p>
</div>
<div style="display: table-row">
<h4 class="cell attribute">Host:</h4>
<p class="cell value">
{{ postState.hosts }}
</p>
</div>
</div>
</section>
</div> -->
<div class="container" style="height: 100%; background-color: white">
<ng-container *ngIf="postState$ | async as postState">
<div
@@ -124,16 +17,18 @@
fxLayoutGap="5px"
style="background-color: transparent; width: 100%; height: 100%;"
>
<div class="checkbox" style="display: inline-block; width: 5px; height: 100%;"></div>
<span fxFlex="grow" fxLayout="row" fxLayoutAlign="space-between center">
<span
><a (click)="goTo()" [appPreview]="postState.previews" href="javascript:void(0)">{{
<div class="checkbox" fxFlex="nogrow" style="width: 5px; height: 100%;"></div>
<span class="row-text" fxFlex="grow" fxLayout="row" fxLayoutAlign="space-between center" fxLayoutGap="20px">
<span [title]="postState.title">
<a (click)="goTo()" [appPreview]="postState.previews" href="javascript:void(0)">{{
postState.title
}}</a></span
>
<span>{{postState.done + '/' + postState.total}} done from {{postState.hosts}}</span>
<span [title]="postState.done + '/' + postState.total + ' from ' + postState.hosts"
>{{ postState.done + '/' + postState.total }} from {{ postState.hosts }}</span
>
</span>
<button [matMenuTriggerFor]="menu" mat-icon-button>
<button [matMenuTriggerFor]="menu" fxFlex="nogrow" mat-icon-button>
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
@@ -148,7 +43,8 @@
</mat-menu>
</div>
<div class="progress-bar" color="accent" style="position: absolute; width: 100%; top: 37px; padding: 0 10px">
<mat-progress-bar [value]="postState.progress" color="primary" mode="determinate"></mat-progress-bar>
<mat-progress-bar *ngIf="(loaded | async)" [value]="postState.progress" color="primary"
mode="determinate"></mat-progress-bar>
</div>
</div>
</ng-container>
@@ -1,3 +1,14 @@
.checkbox {
background-color: transparent;
}
.row-text {
white-space: nowrap;
overflow: hidden;
}
.row-text > span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@@ -10,7 +10,7 @@ import {
EventEmitter
} from '@angular/core';
import { AgRendererComponent } from 'ag-grid-angular';
import { Subscription, Observable } from 'rxjs';
import { Subscription, Observable, BehaviorSubject, Subject } from 'rxjs';
import { WsHandler } from '../ws-handler';
import { ICellRendererParams } from 'ag-grid-community';
import { ElectronService } from 'ngx-electron';
@@ -51,6 +51,8 @@ export class PostProgressRendererComponent implements AgRendererComponent, OnIni
expanded = false;
isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
fs;
loaded: Subject<boolean> = new BehaviorSubject(false);
loading;
trunc(value: number): number {
return Math.trunc(value);
@@ -73,12 +75,14 @@ export class PostProgressRendererComponent implements AgRendererComponent, OnIni
ngAfterViewInit(): void {
this.postState$.emit(this.postState);
this.loading = setTimeout(() => this.loaded.next(true), 100);
}
ngOnDestroy(): void {
if (this.updatesSubscription != null) {
this.updatesSubscription.unsubscribe();
}
clearTimeout(this.loading);
}
agInit(params: ICellRendererParams): void {
+17 -25
View File
@@ -3,32 +3,24 @@
<h2 class="no-wrap" mat-dialog-title>Scan</h2>
</div>
<mat-dialog-content fxFlex="grow" fxLayout="column">
<div *ngIf="!(hideScan | async)">
<form
#f="ngForm"
(ngSubmit)="submit(f)"
autocomplete="off"
fxLayout="row"
fxLayoutAlign="center center"
fxLayoutGap="20px"
>
<mat-form-field fxFlex="grow">
<input [(ngModel)]="input" matInput name="url" placeholder="Put a vipergirls.to link" required/>
</mat-form-field>
<div>
<button [disabled]="f.invalid" color="primary" mat-raised-button type="submit">Scan</button>
</div>
</form>
</div>
<ng-container *ngIf="threadId | async as thId">
<app-multi-post (done)="done($event)" *ngIf="thId != null" [threadId]="thId"
style="height: 100%"></app-multi-post>
</ng-container>
<form #f="ngForm" autocomplete="off" style="height: 100%;">
<mat-form-field fxFlex="grow">
<textarea
[(ngModel)]="input"
mat-autosize="true"
matAutosizeMinRows="5"
matInput
name="url"
placeholder="Put each link in a new line"
required
></textarea>
</mat-form-field>
</form>
<mat-progress-bar *ngIf="loading | async" mode="indeterminate"></mat-progress-bar>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button (click)="close()" mat-raised-button>Close</button>
<button (click)="addPosts()" *ngIf="threadId != null" color="primary" mat-raised-button type="submit">
Download
<mat-dialog-actions align="end" fxFlex="nogrow">
<button mat-dialog-close mat-raised-button>Close</button>
<button (click)="submit()" [disabled]="f.invalid || (loading | async)" color="primary" mat-raised-button>Scan
</button>
</mat-dialog-actions>
</div>
+12 -52
View File
@@ -2,10 +2,8 @@ import {
Component,
OnInit,
NgZone,
ViewChild,
Inject,
ChangeDetectionStrategy,
EventEmitter,
AfterViewInit
} from '@angular/core';
import { NgForm } from '@angular/forms';
@@ -13,7 +11,7 @@ import { MatSnackBar, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
import { HttpClient } from '@angular/common/http';
import { finalize } from 'rxjs/operators';
import { ServerService } from '../server-service';
import { MultiPostComponent } from '../multi-post/multi-post.component';
import { Subject, BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-scan',
@@ -31,18 +29,12 @@ export class ScanComponent implements OnInit, AfterViewInit {
@Inject(MAT_DIALOG_DATA) public data: DialogData
) {}
@ViewChild(MultiPostComponent)
multipost: MultiPostComponent;
input: string;
threadId: EventEmitter<string> = new EventEmitter();
hideScan: EventEmitter<boolean> = new EventEmitter();
loading: Subject<boolean> = new BehaviorSubject(false);
submit(form: NgForm) {
submit() {
this.ngZone.run(() => {
this.hideScan.emit(true);
this.threadId.emit(null);
this.processUrl(this.input, form);
this.processUrl(this.input);
});
}
@@ -54,62 +46,30 @@ export class ScanComponent implements OnInit, AfterViewInit {
});
}
processUrl(url: string, form?: NgForm) {
processUrl(url: string) {
this.loading.next(true);
this.httpClient
.post<{ threadId: string; postId: string }>(this.serverService.baseUrl + '/post', { url: url })
.pipe(
finalize(() => {
this.ngZone.run(() => {
if (form != null) {
form.resetForm();
this.input = null;
}
});
this.close();
this.loading.next(false);
})
)
.subscribe(response => {
this.ngZone.run(() => {
if (response.postId != null) {
this.httpClient
.post(this.serverService.baseUrl + '/post/add', [response])
.pipe(finalize(() => this.dialogRef.close()))
.subscribe(
() => {
this._snackBar.open('Adding posts to queue', null, {
duration: 5000
});
},
error => {
this._snackBar.open(error.error, null, {
duration: 5000
});
}
);
return;
}
this.threadId.emit(response.threadId);
.subscribe(response => {},
error => {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
});
}
addPosts() {
this.ngZone.run(() => {
if (this.multipost != null) {
this.multipost.submit();
}
this.dialogRef.close();
});
}
ngOnInit() {}
ngAfterViewInit(): void {
this.ngZone.run(() => {
if (this.data.url != null) {
this.hideScan.emit(true);
this.processUrl(this.data.url);
} else {
this.hideScan.emit(false);
}
});
}
@@ -94,7 +94,7 @@ export class SettingsComponent implements OnInit {
this.clipboardService.init(data);
},
error => {
this._snackBar.open(error.error, null, {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
@@ -5,6 +5,7 @@
left: 0;
white-space: nowrap;
padding: 2px 0;
z-index: 9999;
}
span {
@@ -38,9 +38,11 @@ export class StatusBarComponent implements OnInit, OnDestroy, AfterViewInit {
selected: EventEmitter<number> = new EventEmitter();
ngAfterViewInit(): void {
this.selected.emit(0);
this.globalState.emit(new GlobalState(0, 0, 0, 0));
this.downloadSpeed.emit(new DownloadSpeed('0 B'));
this.ngZone.run(() => {
this.selected.emit(0);
this.globalState.emit(new GlobalState(0, 0, 0, 0));
this.downloadSpeed.emit(new DownloadSpeed('0 B'));
});
}
ngOnInit() {
@@ -63,7 +65,9 @@ export class StatusBarComponent implements OnInit, OnDestroy, AfterViewInit {
handler.send(new WSMessage(CMD.GLOBAL_STATE_SUB.toString()));
handler.send(new WSMessage(CMD.SPEED_SUB.toString()));
});
this.subscriptions.push(this.selectionService.selected$.subscribe(selected => this.selected.emit(selected.length)));
this.subscriptions.push(
this.selectionService.selected$.subscribe(selected => this.ngZone.run(() => this.selected.emit(selected.length)))
);
}
ngOnDestroy(): void {
@@ -13,11 +13,17 @@
<mat-icon>play_arrow</mat-icon>
</button>
<button (click)="stop()" [disabled]="disableSelection | async" aria-label="Stop" mat-icon-button title="Stop">
<mat-icon>stop</mat-icon>
<mat-icon>pause</mat-icon>
</button>
<div class="or-spacer-vertical left">
<div class="mask"></div>
</div>
<button (click)="clear()" aria-label="Clear completed" mat-icon-button title="Clear completed">
<mat-icon>clear_all</mat-icon>
</button>
<button (click)="stopAll()" aria-label="Stop All" mat-icon-button title="Stop All">
<mat-icon>stop</mat-icon>
</button>
</div>
<div fxFlex="grow" id="global-search">
<form autocomplete="off">
@@ -22,6 +22,7 @@ import { PostsDataService } from '../posts-data.service';
@Component({
selector: 'app-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./tooltip.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
@@ -142,7 +143,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this._snackBar.open(`${data.removed} items cleared`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
@@ -170,7 +171,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this._snackBar.open(`${data.removed} items removed`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
@@ -185,7 +186,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this._snackBar.open(`Download stopped`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
@@ -200,7 +201,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this._snackBar.open(`Download started`, null, { duration: 5000 });
},
error => {
this._snackBar.open(error.error, null, {
this._snackBar.open(error.error || 'Unexpected error, check log file', null, {
duration: 5000
});
}
@@ -209,8 +210,10 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
}
ngAfterViewInit(): void {
this.disableSelection.next(true);
this.loggedUser.emit(new LoggedUser(null));
this.ngZone.run(() => {
this.disableSelection.next(true);
this.loggedUser.emit(new LoggedUser(null));
});
}
ngOnInit() {
@@ -228,7 +231,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.selectionService.selected$.subscribe(selected => {
this.selected = selected;
this.disableSelection.next(this.selected.length === 0);
this.ngZone.run(() => this.disableSelection.next(this.selected.length === 0));
});
}
@@ -0,0 +1,18 @@
.or-spacer-vertical {
display:inline-block;
width:1px;
position:relative;
.mask {
overflow:hidden; width:10px; height:50px;
}
&.left .mask:after {
content:'';
display:block;
margin-left:-20px;
width:20px;
height:100%;
border-radius:12px;
box-shadow:0 0 3px black;
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ export enum WSState {
OPEN
}
const maxAttemps = 5;
const maxAttemps = 15;
@Injectable()
export class WsConnectionService {
+15 -26
View File
@@ -1,3 +1,5 @@
import { element } from 'protractor';
import { GrabQueueState } from './grab-queue/grab-queue.model';
import { LoggedUser } from './common/logged-user.model';
import { Subject, Subscription } from 'rxjs';
import { WSMessage } from './common/ws-message.model';
@@ -6,7 +8,6 @@ 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';
import { VRPostParse, VRThreadParseState } from './common/vr-post-parse.model';
export class WsHandler {
constructor(private websocket: Subject<any>) {}
@@ -111,7 +112,8 @@ export class WsHandler {
element.postName,
element.url,
element.current === 0 && element.total === 0 ? 0 : (element.current / element.total) * 100,
element.status
element.status,
element.index
)
);
});
@@ -121,37 +123,24 @@ export class WsHandler {
.subscribe(e => callback(e));
}
subscribeForThreadParsing(
callback: (stateArray: Array<VRThreadParseState>, dataArray: Array<VRPostParse>) => void
): Subscription {
subscribeForGrabQueue(callback: (grabQueueStream: Array<GrabQueueState>) => void): Subscription {
return this.websocket
.pipe(
map(e => JSON.parse(e)),
filter(e => e.length > 0 && (e[0].type === 'postParse' || e[0].type === 'threadParseState')),
filter(e => e.length > 0 && e.filter(v => v.type === 'grabQueue').length > 0),
map(e => {
const stateValues: VRThreadParseState[] = [];
const dataValues: VRPostParse[] = [];
e.forEach(element => {
if (element.type === 'postParse') {
dataValues.push(
new VRPostParse(
element.threadId,
element.postId,
element.number,
element.title,
element.imageCount,
element.url,
element.previews
)
);
} else if (element.type === 'threadParseState') {
stateValues.push(new VRThreadParseState(element.threadId, element.state));
}
const grabQueue: Array<GrabQueueState> = [];
(<Array<any>>e).forEach(element => {
grabQueue.push(
new GrabQueueState(element.type, element.link, element.threadId, element.postId, element.removed)
);
});
return { states: stateValues, data: dataValues };
return grabQueue;
})
)
.subscribe(e => callback(e.states, e.data));
.subscribe(e => {
callback(e);
});
}
send(wsMessage: WSMessage) {
+7 -7
View File
@@ -1,11 +1,11 @@
@import '~@angular/material/theming';
$font-family: Fira, sans-serif;
$font-size: 14px;
$font-weight: 700;
$secondary-font-family: Fira, sans-serif;
$secondary-font-size: 14px;
$secondary-font-weight:700;
$ag-font-family: Fira, sans-serif;
$ag-font-size: 14px;
$ag-font-weight: 700;
$ag-secondary-font-family: Fira, sans-serif;
$ag-secondary-font-size: 14px;
$ag-secondary-font-weight:700;
@import '~ag-grid-community/src/styles/ag-grid.scss';
@import '~ag-grid-community/src/styles/ag-theme-material/sass/ag-theme-material.scss';
@@ -26,7 +26,7 @@ $secondary-font-weight:700;
background-color: mat-color($background, background) !important;
}
.ag-theme-material .ag-header {
.ag-theme-material .ag-header-cell {
background-color: mat-color($background, background) !important;
}
+4 -20
View File
@@ -75,26 +75,6 @@ app-root {
margin: 8px;
}
.overlay {
position: absolute;
display: block;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 2000;
}
.loading {
background-color: whitesmoke;
}
.no-connection {
background-color: whitesmoke !important;
}
#app-container {
position: fixed;
display: block;
@@ -125,3 +105,7 @@ body a {
.ag-row-selected app-progress-cell .checkbox {
background-color: orange !important;
}
.col-white {
background-color: white;
}