mirror of
https://github.com/JasonHHouse/gaps.git
synced 2026-01-23 11:51:26 -06:00
Hardening code
Adding better error handling Adding address/port support
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
FROM openjdk:11.0.3-jre-slim
|
||||
|
||||
EXPOSE 32400
|
||||
|
||||
RUN mkdir -p /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY target/Gaps-0.0.4.jar /usr/src/app/
|
||||
|
||||
#ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "Gaps-0.0.4.jar"]
|
||||
ENTRYPOINT ["java", "-jar", "Gaps-0.0.4.jar"]
|
||||
ENTRYPOINT ["java", "-jar", "Gaps-0.0.4.jar"]
|
||||
|
||||
@@ -21,10 +21,12 @@ public class Gaps {
|
||||
|
||||
private List<String> movieUrls;
|
||||
|
||||
private Boolean searchFromFolder;
|
||||
|
||||
public Gaps() {
|
||||
}
|
||||
|
||||
public Gaps(String movieDbApiKey, Boolean writeToFile, String movieDbListId, Boolean searchFromPlex, Integer connectTimeout, Integer writeTimeout, Integer readTimeout, List<String> movieUrls) {
|
||||
public Gaps(String movieDbApiKey, Boolean writeToFile, String movieDbListId, Boolean searchFromPlex, Integer connectTimeout, Integer writeTimeout, Integer readTimeout, List<String> movieUrls, Boolean searchFromFolder) {
|
||||
this.movieDbApiKey = movieDbApiKey;
|
||||
this.writeToFile = writeToFile;
|
||||
this.movieDbListId = movieDbListId;
|
||||
@@ -33,6 +35,7 @@ public class Gaps {
|
||||
this.writeTimeout = writeTimeout;
|
||||
this.readTimeout = readTimeout;
|
||||
this.movieUrls = movieUrls;
|
||||
this.searchFromFolder = searchFromFolder;
|
||||
}
|
||||
|
||||
public String getMovieDbApiKey() {
|
||||
@@ -99,6 +102,14 @@ public class Gaps {
|
||||
this.movieUrls = movieUrls;
|
||||
}
|
||||
|
||||
public Boolean getSearchFromFolder() {
|
||||
return searchFromFolder;
|
||||
}
|
||||
|
||||
public void setSearchFromFolder(Boolean searchFromFolder) {
|
||||
this.searchFromFolder = searchFromFolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@@ -111,12 +122,13 @@ public class Gaps {
|
||||
Objects.equals(connectTimeout, gaps.connectTimeout) &&
|
||||
Objects.equals(writeTimeout, gaps.writeTimeout) &&
|
||||
Objects.equals(readTimeout, gaps.readTimeout) &&
|
||||
Objects.equals(movieUrls, gaps.movieUrls);
|
||||
Objects.equals(movieUrls, gaps.movieUrls) &&
|
||||
Objects.equals(searchFromFolder, gaps.searchFromFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(movieDbApiKey, writeToFile, movieDbListId, searchFromPlex, connectTimeout, writeTimeout, readTimeout, movieUrls);
|
||||
return Objects.hash(movieDbApiKey, writeToFile, movieDbListId, searchFromPlex, connectTimeout, writeTimeout, readTimeout, movieUrls, searchFromFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,6 +142,8 @@ public class Gaps {
|
||||
", writeTimeout=" + writeTimeout +
|
||||
", readTimeout=" + readTimeout +
|
||||
", movieUrls=" + movieUrls +
|
||||
", searchFromFolder=" + searchFromFolder +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.jasonhhouse.Gaps;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -10,12 +11,13 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@Controller
|
||||
public class GapsController {
|
||||
@@ -33,8 +35,62 @@ public class GapsController {
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public Future<ResponseEntity<Set<Movie>>> submit(@RequestBody Gaps gaps) {
|
||||
logger.info("submit()");
|
||||
Future future = runInOtherThread(gaps);
|
||||
return future;
|
||||
|
||||
//Error checking
|
||||
if (StringUtils.isEmpty(gaps.getMovieDbApiKey())) {
|
||||
String reason = "Missing Movie DB Api Key. This field is required for Gaps.";
|
||||
logger.error(reason);
|
||||
|
||||
Exception e = new IllegalArgumentException();
|
||||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e);
|
||||
}
|
||||
|
||||
if (BooleanUtils.isNotTrue(gaps.getSearchFromPlex()) && BooleanUtils.isNotTrue(gaps.getSearchFromFolder())) {
|
||||
String reason = "Must search from Plex and/or folders. One or both of these fields is required for Gaps.";
|
||||
logger.error(reason);
|
||||
|
||||
Exception e = new IllegalArgumentException();
|
||||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e);
|
||||
}
|
||||
|
||||
if (BooleanUtils.isNotFalse(gaps.getSearchFromPlex())) {
|
||||
if (CollectionUtils.isEmpty(gaps.getMovieUrls())) {
|
||||
String reason = "Missing Plex movie collection urls. This field is required to search from Plex.";
|
||||
logger.error(reason);
|
||||
|
||||
Exception e = new IllegalArgumentException();
|
||||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e);
|
||||
} else {
|
||||
for(String url : gaps.getMovieUrls()) {
|
||||
if(StringUtils.isEmpty(url)) {
|
||||
String reason = "Found empty Plex movie collection url. This field is required to search from Plex.";
|
||||
logger.error(reason);
|
||||
|
||||
Exception e = new IllegalArgumentException();
|
||||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Fill in default values if missing
|
||||
if(gaps.getWriteTimeout() == null) {
|
||||
logger.info("Missing write timeout. Setting default to 180 seconds.");
|
||||
gaps.setWriteTimeout(180);
|
||||
}
|
||||
|
||||
if(gaps.getConnectTimeout() == null) {
|
||||
logger.info("Missing connect timeout. Setting default to 180 seconds.");
|
||||
gaps.setConnectTimeout(180);
|
||||
}
|
||||
|
||||
if(gaps.getReadTimeout() == null) {
|
||||
logger.info("Missing read timeout. Setting default to 180 seconds.");
|
||||
gaps.setReadTimeout(180);
|
||||
}
|
||||
|
||||
|
||||
return runInOtherThread(gaps);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/status", method = RequestMethod.GET)
|
||||
@@ -66,4 +122,5 @@ public class GapsController {
|
||||
|
||||
return gapsSearch.run(properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
@@ -54,11 +55,17 @@ import org.xml.sax.SAXException;
|
||||
public class GapsSearchBean implements GapsSearch {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GapsSearchBean.class);
|
||||
|
||||
private final Set<Movie> searched;
|
||||
|
||||
private final Set<Movie> recommended;
|
||||
|
||||
private final Set<Movie> ownedMovies;
|
||||
|
||||
private final AtomicInteger totalMovieCount;
|
||||
|
||||
private final AtomicInteger searchedMovieCount;
|
||||
|
||||
private Properties properties;
|
||||
|
||||
public GapsSearchBean() {
|
||||
@@ -301,45 +308,60 @@ public class GapsSearchBean implements GapsSearch {
|
||||
}
|
||||
|
||||
for (String url : urls) {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.build();
|
||||
try {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
String body = response.body() != null ? response.body().string() : null;
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
String body = response.body() != null ? response.body().string() : null;
|
||||
|
||||
if (body == null) {
|
||||
logger.error("Body returned null from Plex");
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream fileIS = new ByteArrayInputStream(body.getBytes());
|
||||
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = builderFactory.newDocumentBuilder();
|
||||
Document xmlDocument = builder.parse(fileIS);
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
String expression = "/MediaContainer/Video";
|
||||
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
|
||||
|
||||
for (int i = 0; i < nodeList.getLength() && i < 25; i++) {
|
||||
Node node = nodeList.item(i);
|
||||
//Files can't have : so need to remove to find matches correctly
|
||||
String title = node.getAttributes().getNamedItem("title").getNodeValue().replaceAll(":", "");
|
||||
if (node.getAttributes().getNamedItem("year") == null) {
|
||||
logger.warn("Year not found for " + title);
|
||||
continue;
|
||||
if (body == null) {
|
||||
logger.error("Body returned null from Plex");
|
||||
return;
|
||||
}
|
||||
String year = node.getAttributes().getNamedItem("year").getNodeValue();
|
||||
Movie movie = new Movie(-1, title, Integer.parseInt(year), "");
|
||||
ownedMovies.add(movie);
|
||||
totalMovieCount.incrementAndGet();
|
||||
}
|
||||
logger.info(ownedMovies.size() + " movies found in plex");
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Error connecting to Plex to get Movie list", e);
|
||||
} catch (ParserConfigurationException | XPathExpressionException | SAXException e) {
|
||||
logger.error("Error parsing XML from Plex", e);
|
||||
InputStream fileIS = new ByteArrayInputStream(body.getBytes());
|
||||
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = builderFactory.newDocumentBuilder();
|
||||
Document xmlDocument = builder.parse(fileIS);
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
String expression = "/MediaContainer/Video";
|
||||
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
|
||||
|
||||
if(nodeList.getLength() == 0) {
|
||||
String reason = "No movies found in url: " + url;
|
||||
logger.warn(reason);
|
||||
}
|
||||
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
Node node = nodeList.item(i);
|
||||
//Files can't have : so need to remove to find matches correctly
|
||||
String title = node.getAttributes().getNamedItem("title").getNodeValue().replaceAll(":", "");
|
||||
if (node.getAttributes().getNamedItem("year") == null) {
|
||||
logger.warn("Year not found for " + title);
|
||||
continue;
|
||||
}
|
||||
String year = node.getAttributes().getNamedItem("year").getNodeValue();
|
||||
Movie movie = new Movie(-1, title, Integer.parseInt(year), "");
|
||||
ownedMovies.add(movie);
|
||||
totalMovieCount.incrementAndGet();
|
||||
}
|
||||
logger.info(ownedMovies.size() + " movies found in plex");
|
||||
|
||||
} catch (IOException e) {
|
||||
String reason = "Error connecting to Plex to get Movie list: " + url;
|
||||
logger.error(reason, e);
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, reason, e);
|
||||
} catch (ParserConfigurationException | XPathExpressionException | SAXException e) {
|
||||
String reason = "Error parsing XML from Plex: " + url;
|
||||
logger.error(reason, e);
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, reason, e);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
String reason = "Error with plex Url: " + url;
|
||||
logger.error(reason, e);
|
||||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,24 @@
|
||||
recommend getting those movies, legally of course.</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p class="col s12">Sets address for Gaps backend. Only change if the container doesn't run in localhost.</p>
|
||||
<div class="input-field col s2">
|
||||
<input placeholder="8080" type="text"
|
||||
id="address" type="text" class="validate" value="localhost">
|
||||
<label for="address">Address</label>
|
||||
</div>
|
||||
|
||||
<p class="col s12">Sets port for Gaps backend. Only change if 8080 was already taken. Make sure to update docker to expose the same port.</p>
|
||||
<div class="input-field col s2">
|
||||
<input placeholder="8080" type="number"
|
||||
id="port" type="text" class="validate" value="8080">
|
||||
<label for="port">Port</label>
|
||||
</div>
|
||||
|
||||
<p class="col s12">Go to <a href="https://www.themoviedb.org">The Movie DB</a> and make an API Key</p>
|
||||
<div class="input-field col s6">
|
||||
<input placeholder="Api Key"
|
||||
id="movie_db_api_key" type="text" class="validate" value="723b4c763114904392ca441909aa0375">
|
||||
id="movie_db_api_key" type="text" class="validate">
|
||||
<label for="movie_db_api_key">Movie Database Api Key*</label>
|
||||
</div>
|
||||
|
||||
@@ -86,12 +100,12 @@
|
||||
<div class="input-field col s12">
|
||||
<textarea
|
||||
placeholder="Gaps supports a single movie Url or multiple. Put each Url on a new line."
|
||||
id="plex_movie_urls" class="materialize-textarea">http://192.168.1.193:32400/library/sections/1/all/?X-Plex-Token=mQw4uawxTyYEmqNUrvBz</textarea>
|
||||
id="plex_movie_urls" class="materialize-textarea"></textarea>
|
||||
<label for="plex_movie_urls">Plex Movie Urls</label>
|
||||
</div>
|
||||
|
||||
<p class="col s12">Sets the default connect timeout for new connections. A value of 0 means no timeout,
|
||||
otherwise values must be between 1 and {@link Integer#MAX_VALUE} when converted to milliseconds.</p>
|
||||
otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.</p>
|
||||
<div class="input-field col s2">
|
||||
<input value="180" placeholder="Api Key" type="number"
|
||||
id="connect_timeout" type="text" class="validate">
|
||||
@@ -99,7 +113,7 @@
|
||||
</div>
|
||||
|
||||
<p class="col s12">Sets the default write timeout for new connections. A value of 0 means no timeout,
|
||||
otherwise values must be between 1 and {@link Integer#MAX_VALUE} when converted to milliseconds.</p>
|
||||
otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.</p>
|
||||
<div class="input-field col s2">
|
||||
<input value="180" placeholder="Api Key" type="number"
|
||||
id="write_timeout" type="text" class="validate">
|
||||
@@ -107,7 +121,7 @@
|
||||
</div>
|
||||
|
||||
<p class="col s12">Sets the default read timeout for new connections. A value of 0 means no timeout,
|
||||
otherwise values must be between 1 and {@link Integer#MAX_VALUE} when converted to milliseconds.</p>
|
||||
otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds.</p>
|
||||
<div class="input-field col s2">
|
||||
<input value="180" placeholder="Api Key" type="number"
|
||||
id="read_timeout" type="text" class="validate">
|
||||
|
||||
@@ -4,13 +4,13 @@ function start() {
|
||||
$('.modal').modal();
|
||||
}
|
||||
|
||||
var keepChecking;
|
||||
let keepChecking;
|
||||
|
||||
function onSubmitGapsSearch() {
|
||||
keepChecking = true;
|
||||
$('#progressContainer').hide();
|
||||
|
||||
var gaps = {
|
||||
const gaps = {
|
||||
movieDbApiKey: $('#movie_db_api_key').val(),
|
||||
writeToFile: true,
|
||||
movieDbListId: $('#movie_db_list_id').val(),
|
||||
@@ -18,19 +18,19 @@ function onSubmitGapsSearch() {
|
||||
connectTimeout: $('#connect_timeout').val(),
|
||||
writeTimeout: $('#write_timeout').val(),
|
||||
readTimeout: $('#read_timeout').val(),
|
||||
movieUrls: [$('#plex_movie_urls').val()]
|
||||
movieUrls: $('#plex_movie_urls').val().split("\n")
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "http://localhost:8080/submit",
|
||||
url: "http://" + $('#address').val() + ":" + $('#port').val() + "/submit",
|
||||
data: JSON.stringify(gaps),
|
||||
contentType: "application/json",
|
||||
timeout: 10000000,
|
||||
success: function (movies) {
|
||||
keepChecking = false;
|
||||
var movieHtml = "";
|
||||
movies.forEach(function(movie) {
|
||||
let movieHtml = "";
|
||||
movies.forEach(function (movie) {
|
||||
movieHtml += buildMovieDiv(movie);
|
||||
});
|
||||
|
||||
@@ -39,12 +39,21 @@ function onSubmitGapsSearch() {
|
||||
$('#searchModelTitle').text(movies.length + ' movies to add to complete your collections');
|
||||
},
|
||||
error: function (err) {
|
||||
alert(err.responseText);
|
||||
let message = "Unknown error. Check docker Gaps log file.";
|
||||
if (err) {
|
||||
message = JSON.parse(err.responseText).message;
|
||||
}
|
||||
|
||||
$('#progressContainer').hide();
|
||||
$('#searchingBody').html(message);
|
||||
$('#searchModelTitle').text("An error occurred...");
|
||||
|
||||
keepChecking = false;
|
||||
}
|
||||
})
|
||||
|
||||
$('#searchModal').modal('open');
|
||||
|
||||
|
||||
polling();
|
||||
}
|
||||
|
||||
@@ -61,21 +70,21 @@ function buildMovie(movie) {
|
||||
}
|
||||
|
||||
function polling() {
|
||||
if(keepChecking) {
|
||||
if (keepChecking) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "http://localhost:8080/status",
|
||||
contentType: "application/json",
|
||||
success: function(data) {
|
||||
if(keepChecking) {
|
||||
var obj = JSON.parse(data);
|
||||
if(!obj.searchedMovieCount && !obj.totalMovieCount && obj.totalMovieCount === 0) {
|
||||
type: "GET",
|
||||
url: "http://" + $('#address').val() + ":" + $('#port').val() + "/status",
|
||||
contentType: "application/json",
|
||||
success: function (data) {
|
||||
if (keepChecking) {
|
||||
const obj = JSON.parse(data);
|
||||
if (!obj.searchedMovieCount && !obj.totalMovieCount && obj.totalMovieCount === 0) {
|
||||
$('#searchingBody').text("Searching for movies...");
|
||||
} else {
|
||||
$('#progressContainer').show();
|
||||
var percentage = Math.trunc(obj.searchedMovieCount / obj.totalMovieCount * 100);
|
||||
$('#searchingBody').text(obj.searchedMovieCount + ' of ' + obj.totalMovieCount + " movies searched. " + percentage + "% complete.");
|
||||
$('#progressBar').css( "width", percentage + "%" );
|
||||
$('#progressBar').css("width", percentage + "%");
|
||||
}
|
||||
setTimeout(polling, 2000);
|
||||
}
|
||||
|
||||
42
test_cases.txt
Normal file
42
test_cases.txt
Normal file
@@ -0,0 +1,42 @@
|
||||
{}
|
||||
|
||||
{
|
||||
"timestamp": "2019-05-23T18:16:17.402+0000",
|
||||
"status": 422,
|
||||
"error": "Unprocessable Entity",
|
||||
"message": "Missing Movie DB Api Key. This field is required for Gaps.",
|
||||
"path": "/submit"
|
||||
}
|
||||
|
||||
|
||||
####
|
||||
|
||||
{
|
||||
"movieDbApiKey":"test"
|
||||
}
|
||||
|
||||
{
|
||||
"timestamp": "2019-05-23T18:16:32.154+0000",
|
||||
"status": 422,
|
||||
"error": "Unprocessable Entity",
|
||||
"message": "Must search from Plex and/or Folders. One or both of these fields is required for Gaps.",
|
||||
"path": "/submit"
|
||||
}
|
||||
|
||||
####
|
||||
|
||||
|
||||
{
|
||||
"movieDbApiKey":"test",
|
||||
"searchFromPlex": true
|
||||
}
|
||||
|
||||
{
|
||||
"timestamp": "2019-05-23T18:17:05.369+0000",
|
||||
"status": 422,
|
||||
"error": "Unprocessable Entity",
|
||||
"message": "Missing Plex Movie Collection Urls. This field is required to search from Plex.",
|
||||
"path": "/submit"
|
||||
}
|
||||
|
||||
####
|
||||
Reference in New Issue
Block a user