mirror of
https://github.com/JasonHHouse/gaps.git
synced 2026-02-05 01:59:35 -06:00
Working example for getting recommended movies
This commit is contained in:
@@ -18,32 +18,11 @@ import org.jetbrains.annotations.NotNull;
|
||||
*/
|
||||
public interface GapsSearch {
|
||||
|
||||
/**
|
||||
* Kicks of searching for all missing movies
|
||||
*/
|
||||
@Deprecated
|
||||
void run();
|
||||
|
||||
/**
|
||||
* Kicks of searching for all missing movies
|
||||
*/
|
||||
void run(String machineIdentifier, Integer key);
|
||||
|
||||
/**
|
||||
* @return The total count of movies to be searched
|
||||
*/
|
||||
@NotNull Integer getTotalMovieCount();
|
||||
|
||||
/**
|
||||
* @return The current count of movies searched
|
||||
*/
|
||||
@NotNull Integer getSearchedMovieCount();
|
||||
|
||||
/**
|
||||
* @return The movies that are missing from collections
|
||||
*/
|
||||
@NotNull CopyOnWriteArrayList<Movie> getRecommendedMovies();
|
||||
|
||||
/**
|
||||
* Cancel the current search
|
||||
*/
|
||||
|
||||
27
Core/src/main/java/com/jasonhhouse/gaps/Payload.java
Normal file
27
Core/src/main/java/com/jasonhhouse/gaps/Payload.java
Normal file
@@ -0,0 +1,27 @@
|
||||
package com.jasonhhouse.gaps;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
|
||||
public enum Payload {
|
||||
OWNED_MOVIES_CANNOT_BE_EMPTY(0, "Owned movies list cannot be empty. You must search first."),
|
||||
SEARCH_CANCELLED(1, "Search cancelled."),
|
||||
SEARCH_FAILED(2, "Search failed. Check docker Gaps log file."),
|
||||
SEARCH_SUCCESSFUL(3, "Search successful.");
|
||||
|
||||
final private int code;
|
||||
final private String reason;
|
||||
|
||||
Payload(int code, String reason) {
|
||||
this.code = code;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -40,13 +40,11 @@ public class LibraryController {
|
||||
|
||||
private final IoService ioService;
|
||||
private final GapsService gapsService;
|
||||
private final GapsSearch gapsSearch;
|
||||
|
||||
@Autowired
|
||||
public LibraryController(IoService ioService, GapsService gapsService, GapsSearch gapsSearch) {
|
||||
public LibraryController(IoService ioService, GapsService gapsService) {
|
||||
this.ioService = ioService;
|
||||
this.gapsService = gapsService;
|
||||
this.gapsSearch = gapsSearch;
|
||||
|
||||
if (CollectionUtils.isEmpty(gapsService.getPlexSearch().getPlexServers())) {
|
||||
//Only add if empty, otherwise the server information should be correct
|
||||
@@ -61,28 +59,21 @@ public class LibraryController {
|
||||
LOGGER.info("getLibraries()");
|
||||
|
||||
boolean plexServersFound;
|
||||
List<Movie> movies;
|
||||
PlexServer plexServer;
|
||||
PlexLibrary plexLibrary;
|
||||
if (CollectionUtils.isNotEmpty(gapsService.getPlexSearch().getPlexServers())) {
|
||||
//Read first plex servers movies
|
||||
plexServer = gapsService.getPlexSearch().getPlexServers().stream().findFirst().orElse(new PlexServer());
|
||||
plexLibrary = plexServer.getPlexLibraries().stream().findFirst().orElse(new PlexLibrary());
|
||||
movies = ioService.readOwnedMovies(plexServer.getMachineIdentifier(), plexLibrary.getKey());
|
||||
plexServersFound = true;
|
||||
} else {
|
||||
plexServer = new PlexServer();
|
||||
plexLibrary = new PlexLibrary();
|
||||
movies = Collections.emptyList();
|
||||
plexServersFound = false;
|
||||
}
|
||||
|
||||
Map<String, PlexServer> plexServerMap = gapsService.getPlexSearch().getPlexServers().stream().collect(Collectors.toMap(PlexServer::getMachineIdentifier, Function.identity()));
|
||||
|
||||
if (CollectionUtils.isEmpty(movies)) {
|
||||
LOGGER.info("No owned movies found.");
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(gapsService.getPlexSearch().getMovieDbApiKey())) {
|
||||
try {
|
||||
PlexSearch plexSearch = ioService.readProperties();
|
||||
@@ -99,14 +90,12 @@ public class LibraryController {
|
||||
}
|
||||
|
||||
ModelAndView modelAndView = new ModelAndView("libraries");
|
||||
modelAndView.addObject("movies", movies);
|
||||
modelAndView.addObject("plexServers", plexServerMap);
|
||||
modelAndView.addObject("plexSearch", gapsService.getPlexSearch());
|
||||
modelAndView.addObject("plexServer", plexServer);
|
||||
modelAndView.addObject("plexLibrary", plexLibrary);
|
||||
modelAndView.addObject("plexServersFound", plexServersFound);
|
||||
return modelAndView;
|
||||
//}
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET,
|
||||
@@ -121,7 +110,7 @@ public class LibraryController {
|
||||
|
||||
if (CollectionUtils.isEmpty(movies)) {
|
||||
objectNode.put("success", false);
|
||||
LOGGER.warn("Could not save PlexLibrary");
|
||||
LOGGER.warn("Could not find Plex Library movies");
|
||||
} else {
|
||||
|
||||
String output;
|
||||
@@ -139,25 +128,4 @@ public class LibraryController {
|
||||
return ResponseEntity.ok().body(objectNode.toString());
|
||||
}
|
||||
|
||||
@MessageMapping("/search/cancel")
|
||||
public void cancelSearching() {
|
||||
LOGGER.info("cancelSearching()");
|
||||
gapsSearch.cancelSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main REST call to start Gaps searching for missing movies
|
||||
*
|
||||
* @param gaps Needs the gaps object to get started with Plex information and TMDB key
|
||||
*/
|
||||
@RequestMapping(value = "search/start/{machineIdentifier}/{key}",
|
||||
method = RequestMethod.PUT)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public void postStartSearching(@PathVariable("machineIdentifier") final String machineIdentifier, @PathVariable("key") final Integer key) {
|
||||
LOGGER.info("postStartSearching( " + machineIdentifier + ", " + key + " )");
|
||||
|
||||
ioService.migrateJsonSeedFileIfNeeded();
|
||||
gapsSearch.run(machineIdentifier, key);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,18 +11,32 @@ package com.jasonhhouse.gaps.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.jasonhhouse.gaps.GapsSearch;
|
||||
import com.jasonhhouse.gaps.GapsService;
|
||||
import com.jasonhhouse.gaps.Movie;
|
||||
import com.jasonhhouse.gaps.service.BindingErrorsService;
|
||||
import com.jasonhhouse.gaps.PlexLibrary;
|
||||
import com.jasonhhouse.gaps.PlexServer;
|
||||
import com.jasonhhouse.gaps.service.IoService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
@@ -30,45 +44,73 @@ import org.springframework.web.servlet.ModelAndView;
|
||||
public class RecommendedController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RecommendedController.class);
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final BindingErrorsService bindingErrorsService;
|
||||
private final IoService ioService;
|
||||
private final GapsService gapsService;
|
||||
private final GapsSearch gapsSearch;
|
||||
|
||||
@Autowired
|
||||
public RecommendedController(BindingErrorsService bindingErrorsService, IoService ioService) {
|
||||
this.bindingErrorsService = bindingErrorsService;
|
||||
public RecommendedController(IoService ioService, GapsService gapsService, GapsSearch gapsSearch) {
|
||||
this.ioService = ioService;
|
||||
this.gapsService = gapsService;
|
||||
this.gapsSearch = gapsSearch;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET,
|
||||
path = "/recommended")
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/recommended")
|
||||
public ModelAndView getRecommended() {
|
||||
LOGGER.info("getRecommended()");
|
||||
String recommended = null;
|
||||
if (ioService.doesRecommendedFileExist()) {
|
||||
recommended = ioService.getRecommendedMovies();
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(recommended)) {
|
||||
//Show empty page
|
||||
return new ModelAndView("emptyState");
|
||||
PlexServer plexServer;
|
||||
PlexLibrary plexLibrary;
|
||||
if (CollectionUtils.isNotEmpty(gapsService.getPlexSearch().getPlexServers())) {
|
||||
//Read first plex servers movies
|
||||
plexServer = gapsService.getPlexSearch().getPlexServers().stream().findFirst().orElse(new PlexServer());
|
||||
plexLibrary = plexServer.getPlexLibraries().stream().findFirst().orElse(new PlexLibrary());
|
||||
} else {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
try {
|
||||
Movie[] recommendedMovies = objectMapper.readValue(recommended, Movie[].class);
|
||||
LOGGER.info("recommended.length:" + recommendedMovies.length);
|
||||
|
||||
ModelAndView modelAndView = new ModelAndView("recommended");
|
||||
modelAndView.addObject("recommended", recommendedMovies);
|
||||
modelAndView.addObject("urls", buildUrls(recommendedMovies));
|
||||
return modelAndView;
|
||||
} catch (JsonProcessingException e) {
|
||||
LOGGER.error("Could not parse Recommended JSON", e);
|
||||
return bindingErrorsService.getErrorPage();
|
||||
}
|
||||
|
||||
|
||||
plexServer = new PlexServer();
|
||||
plexLibrary = new PlexLibrary();
|
||||
}
|
||||
|
||||
Map<String, PlexServer> plexServerMap = gapsService.getPlexSearch().getPlexServers().stream().collect(Collectors.toMap(PlexServer::getMachineIdentifier, Function.identity()));
|
||||
|
||||
ModelAndView modelAndView = new ModelAndView("recommended");
|
||||
modelAndView.addObject("plexServers", plexServerMap);
|
||||
modelAndView.addObject("plexSearch", gapsService.getPlexSearch());
|
||||
modelAndView.addObject("plexServer", plexServer);
|
||||
modelAndView.addObject("plexLibrary", plexLibrary);
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET,
|
||||
path = "/recommended/{machineIdentifier}/{key}")
|
||||
@ResponseBody
|
||||
public ResponseEntity<String> getRecommended(@PathVariable("machineIdentifier") final String machineIdentifier, @PathVariable("key") final Integer key) {
|
||||
LOGGER.info("getRecommended( " + machineIdentifier + ", " + key + " )");
|
||||
|
||||
List<Movie> movies = ioService.readRecommendedMovies(machineIdentifier, key);
|
||||
|
||||
ObjectNode objectNode = objectMapper.createObjectNode();
|
||||
|
||||
if (CollectionUtils.isEmpty(movies)) {
|
||||
objectNode.put("success", false);
|
||||
LOGGER.warn("Could not find Plex Library recommended movies");
|
||||
} else {
|
||||
|
||||
String output;
|
||||
try {
|
||||
output = objectMapper.writeValueAsString(movies);
|
||||
objectNode.put("success", true);
|
||||
} catch (JsonProcessingException e) {
|
||||
LOGGER.error("Failed to turn PlexLibrary Movies to JSON", e);
|
||||
objectNode.put("success", false);
|
||||
output = "";
|
||||
}
|
||||
objectNode.put("movies", output);
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().body(objectNode.toString());
|
||||
}
|
||||
|
||||
private List<String> buildUrls(Movie[] movies) {
|
||||
@@ -90,4 +132,34 @@ public class RecommendedController {
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start Gaps searching for missing movies
|
||||
*
|
||||
* @param machineIdentifier plex server id
|
||||
* @param key plex library key
|
||||
*/
|
||||
@RequestMapping(value = "/recommended/find/{machineIdentifier}/{key}",
|
||||
method = RequestMethod.PUT)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
public void putFindRecommencedMovies(@PathVariable("machineIdentifier") final String machineIdentifier, @PathVariable("key") final Integer key) {
|
||||
LOGGER.info("putFindRecommencedMovies( " + machineIdentifier + ", " + key + " )");
|
||||
|
||||
ioService.migrateJsonSeedFileIfNeeded();
|
||||
gapsSearch.run(machineIdentifier, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel Gaps searching for missing movies
|
||||
*
|
||||
* @param machineIdentifier plex server id
|
||||
* @param key plex library key
|
||||
*/
|
||||
@MessageMapping("/recommended/cancel/{machineIdentifier}/{key}")
|
||||
public void cancelSearching(@PathVariable("machineIdentifier") final String machineIdentifier, @PathVariable("key") final Integer key) {
|
||||
LOGGER.info("cancelSearching( " + machineIdentifier + ", " + key + " )");
|
||||
gapsSearch.cancelSearch();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ public class SearchController {
|
||||
|
||||
/**
|
||||
* Main REST call to start Gaps searching for missing movies
|
||||
*
|
||||
* @param gaps Needs the gaps object to get started with Plex information and TMDB key
|
||||
*/
|
||||
@RequestMapping(value = "startSearching", method = RequestMethod.POST)
|
||||
@ResponseStatus(value = HttpStatus.OK)
|
||||
@@ -54,6 +52,7 @@ public class SearchController {
|
||||
|
||||
ioService.migrateJsonSeedFileIfNeeded();
|
||||
|
||||
gapsSearch.run();
|
||||
throw new IllegalStateException("Need to pass in machineIdentifier and plex key");
|
||||
//gapsSearch.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jason H House
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.jasonhhouse.gaps.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jasonhhouse.gaps.Movie;
|
||||
import com.jasonhhouse.gaps.service.BindingErrorsService;
|
||||
import com.jasonhhouse.gaps.service.IoService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
@RestController
|
||||
public class TestController {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
|
||||
|
||||
private final BindingErrorsService bindingErrorsService;
|
||||
private final IoService ioService;
|
||||
|
||||
@Autowired
|
||||
public TestController(BindingErrorsService bindingErrorsService, IoService ioService) {
|
||||
this.bindingErrorsService = bindingErrorsService;
|
||||
this.ioService = ioService;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET,
|
||||
path = "/test")
|
||||
public ModelAndView getRecommended() {
|
||||
LOGGER.info("getRecommended()");
|
||||
String recommended = null;
|
||||
if (ioService.doesRecommendedFileExist()) {
|
||||
recommended = ioService.getRecommendedMovies();
|
||||
}
|
||||
|
||||
if (StringUtils.isEmpty(recommended)) {
|
||||
//Show empty page
|
||||
return new ModelAndView("emptyState");
|
||||
} else {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
try {
|
||||
Movie[] recommendedMovies = objectMapper.readValue(recommended, Movie[].class);
|
||||
LOGGER.info("recommended.length:" + recommendedMovies.length);
|
||||
|
||||
ModelAndView modelAndView = new ModelAndView("test");
|
||||
modelAndView.addObject("recommended", recommendedMovies);
|
||||
modelAndView.addObject("address", "174.58.64.67");
|
||||
modelAndView.addObject("port", 32400);
|
||||
modelAndView.addObject("plexToken", "xPUCxLh4cTz8pcgorQQs");
|
||||
return modelAndView;
|
||||
} catch (JsonProcessingException e) {
|
||||
LOGGER.error("Could not parse Recommended JSON", e);
|
||||
return bindingErrorsService.getErrorPage();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> buildUrls(Movie[] movies) {
|
||||
LOGGER.info("buildUrls( " + Arrays.toString(movies) + " ) ");
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (Movie movie : movies) {
|
||||
if (movie.getTvdbId() != -1) {
|
||||
urls.add("https://www.themoviedb.org/movie/" + movie.getTvdbId());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (StringUtils.isNotEmpty(movie.getImdbId())) {
|
||||
urls.add("https://www.imdb.com/title/" + movie.getImdbId() + "/");
|
||||
continue;
|
||||
}
|
||||
|
||||
urls.add(null);
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
@@ -18,84 +18,49 @@ import com.fasterxml.jackson.databind.node.JsonNodeType;
|
||||
import com.jasonhhouse.gaps.GapsSearch;
|
||||
import com.jasonhhouse.gaps.GapsService;
|
||||
import com.jasonhhouse.gaps.Movie;
|
||||
import com.jasonhhouse.gaps.MoviePair;
|
||||
import com.jasonhhouse.gaps.PlexLibrary;
|
||||
import com.jasonhhouse.gaps.Payload;
|
||||
import com.jasonhhouse.gaps.SearchCancelledException;
|
||||
import com.jasonhhouse.gaps.SearchResults;
|
||||
import com.jasonhhouse.gaps.UrlGenerator;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
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;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
@Service
|
||||
public class GapsSearchService implements GapsSearch {
|
||||
|
||||
public static final String ID_IDX_START = "://";
|
||||
|
||||
public static final String ID_IDX_END = "?";
|
||||
|
||||
public static final String COLLECTION_ID = "belongs_to_collection";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(GapsSearchService.class);
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private final List<Movie> everyMovie;
|
||||
|
||||
private final TreeSet<Movie> searched;
|
||||
|
||||
private final TreeSet<Movie> recommended;
|
||||
|
||||
private final TreeSet<Movie> ownedMovies;
|
||||
|
||||
private final AtomicInteger totalMovieCount;
|
||||
|
||||
private final AtomicInteger searchedMovieCount;
|
||||
|
||||
private final AtomicBoolean cancelSearch;
|
||||
|
||||
private final UrlGenerator urlGenerator;
|
||||
@@ -112,69 +77,49 @@ public class GapsSearchService implements GapsSearch {
|
||||
public GapsSearchService(@Qualifier("real") UrlGenerator urlGenerator, SimpMessagingTemplate template, IoService ioService, GapsService gapsService) {
|
||||
this.template = template;
|
||||
this.gapsService = gapsService;
|
||||
this.ownedMovies = new TreeSet<>();
|
||||
this.searched = new TreeSet<>();
|
||||
this.recommended = new TreeSet<>();
|
||||
this.everyMovie = new ArrayList<>();
|
||||
this.urlGenerator = urlGenerator;
|
||||
this.ioService = ioService;
|
||||
|
||||
totalMovieCount = new AtomicInteger();
|
||||
tempTvdbCounter = new AtomicInteger();
|
||||
searchedMovieCount = new AtomicInteger();
|
||||
cancelSearch = new AtomicBoolean(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
@Deprecated
|
||||
public void run() {
|
||||
throw new IllegalStateException("Do not run this anymore");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(String machineIdentifier, Integer key) {
|
||||
LOGGER.info("run( " + machineIdentifier + ", " + key + " )");
|
||||
|
||||
searched.clear();
|
||||
ownedMovies.clear();
|
||||
recommended.clear();
|
||||
everyMovie.clear();
|
||||
everyMovie.addAll(ioService.readMovieIdsFromFile());
|
||||
totalMovieCount.set(0);
|
||||
searchedMovieCount.set(0);
|
||||
cancelSearch.set(false);
|
||||
|
||||
|
||||
final Set<Movie> recommended = new TreeSet<>();
|
||||
final Set<Movie> searched = new TreeSet<>();
|
||||
final List<Movie> everyMovie = new ArrayList<>(ioService.readMovieIdsFromFile());
|
||||
final Set<Movie> ownedMovies = new TreeSet<>(ioService.readOwnedMovies(machineIdentifier, key));
|
||||
final AtomicInteger searchedMovieCount = new AtomicInteger(0);
|
||||
|
||||
if(CollectionUtils.isEmpty(ownedMovies)) {
|
||||
String reason = "Owned movies cannot be empty";
|
||||
LOGGER.error(reason);
|
||||
template.convertAndSend("/finishedSearching", Payload.OWNED_MOVIES_CANNOT_BE_EMPTY);
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, reason);
|
||||
}
|
||||
|
||||
try {
|
||||
Map<MoviePair, Movie> previousMovies = new HashMap<>();
|
||||
|
||||
gapsService
|
||||
.getPlexSearch()
|
||||
.getPlexServers()
|
||||
.forEach(plexServer -> plexServer
|
||||
.getPlexLibraries()
|
||||
.stream()
|
||||
.filter(PlexLibrary::getSelected)
|
||||
.forEach(plexLibrary -> {
|
||||
everyMovie
|
||||
.forEach(movie -> {
|
||||
previousMovies.put(new MoviePair(movie.getName(), movie.getYear()), movie);
|
||||
});
|
||||
}));
|
||||
|
||||
List<String> urls = generatePlexUrls(machineIdentifier, key);
|
||||
findAllPlexMovies(previousMovies, urls);
|
||||
|
||||
StopWatch watch = new StopWatch();
|
||||
watch.start();
|
||||
searchForMovies();
|
||||
searchForMovies(ownedMovies, everyMovie, recommended, searched, searchedMovieCount);
|
||||
watch.stop();
|
||||
LOGGER.info("Time Elapsed: " + TimeUnit.MILLISECONDS.toSeconds(watch.getTime()) + " seconds.");
|
||||
LOGGER.info("Times used TVDB ID: " + tempTvdbCounter);
|
||||
} catch (SearchCancelledException e) {
|
||||
String reason = "Search cancelled";
|
||||
LOGGER.error(reason, e);
|
||||
template.convertAndSend("/finishedSearching", false);
|
||||
template.convertAndSend("/finishedSearching", Payload.OWNED_MOVIES_CANNOT_BE_EMPTY);
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, reason, e);
|
||||
} catch (IOException e) {
|
||||
String reason = "Search failed";
|
||||
LOGGER.error(reason, e);
|
||||
template.convertAndSend("/finishedSearching", Payload.SEARCH_FAILED);
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, reason, e);
|
||||
} finally {
|
||||
cancelSearch.set(true);
|
||||
@@ -183,14 +128,8 @@ public class GapsSearchService implements GapsSearch {
|
||||
//Always write to log
|
||||
ioService.writeRecommendedToFile(recommended, machineIdentifier, key);
|
||||
ioService.writeMovieIdsToFile(new TreeSet<>(everyMovie));
|
||||
ioService.writeOwnedMoviesToFile(ownedMovies, machineIdentifier, key);
|
||||
|
||||
template.convertAndSend("/finishedSearching", true);
|
||||
|
||||
LOGGER.info("Owned");
|
||||
for (Movie movie : ownedMovies) {
|
||||
LOGGER.info(movie.toString());
|
||||
}
|
||||
template.convertAndSend("/finishedSearching", Payload.SEARCH_SUCCESSFUL);
|
||||
|
||||
LOGGER.info("Recommended");
|
||||
for (Movie movie : recommended) {
|
||||
@@ -198,32 +137,10 @@ public class GapsSearchService implements GapsSearch {
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Integer getTotalMovieCount() {
|
||||
return totalMovieCount.get();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Integer getSearchedMovieCount() {
|
||||
return searchedMovieCount.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CopyOnWriteArrayList<Movie> getRecommendedMovies() {
|
||||
return new CopyOnWriteArrayList<>(recommended);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelSearch() {
|
||||
LOGGER.debug("cancelSearch()");
|
||||
cancelSearch.set(true);
|
||||
searched.clear();
|
||||
ownedMovies.clear();
|
||||
recommended.clear();
|
||||
totalMovieCount.set(0);
|
||||
searchedMovieCount.set(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,263 +149,6 @@ public class GapsSearchService implements GapsSearch {
|
||||
return !cancelSearch.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Using TMDB api (V3), get access to user list and add recommended movies to
|
||||
*/
|
||||
/* private @Nullable String getTmdbAuthorization() {
|
||||
// Create the request_token request
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
|
||||
HttpUrl url = new HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host("api.themoviedb.org")
|
||||
.addPathSegment("3")
|
||||
.addPathSegment("authentication")
|
||||
.addPathSegment("token")
|
||||
.addPathSegment("new")
|
||||
.addQueryParameter("api_key", gapsService.getPlexSearch().getMovieDbApiKey())
|
||||
.build();
|
||||
|
||||
MediaType mediaType = MediaType.parse("application/octet-stream");
|
||||
RequestBody.create("{}", mediaType);
|
||||
RequestBody body;
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build();
|
||||
|
||||
String request_token;
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
JsonNode responseJson = objectMapper.readTree(response.body().string());
|
||||
request_token = responseJson.get("request_token").asText();
|
||||
|
||||
// Have user click link to authorize the token
|
||||
LOGGER.info("\n############################################\n" +
|
||||
"Click the link below to authorize TMDB list access: \n" +
|
||||
"https://www.themoviedb.org/authenticate/" + request_token + "\n" +
|
||||
"Press enter to continue\n" +
|
||||
"############################################\n");
|
||||
new Thread(new UserInputThreadCountdown()).start();
|
||||
System.in.read();
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Unable to authenticate tmdb, and add movies to list. ", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
url = new HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host("api.themoviedb.org")
|
||||
.addPathSegment("3")
|
||||
.addPathSegment("authentication")
|
||||
.addPathSegment("session")
|
||||
.addPathSegment("new")
|
||||
.addQueryParameter("api_key", gapsService.getPlexSearch().getMovieDbApiKey())
|
||||
.build();
|
||||
|
||||
// Create the sesssion ID for MovieDB using the approved token
|
||||
mediaType = MediaType.parse("application/json");
|
||||
body = RequestBody.create("{\"request_token\":\"" + request_token + "\"}", mediaType);
|
||||
request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.addHeader("content-type", "application/json")
|
||||
.build();
|
||||
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
JsonNode sessionResponse = objectMapper.readTree(response.body().string());
|
||||
return sessionResponse.get("session_id").asText(); // TODO: Save sessionID to file for reuse
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Unable to create session id: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Using TMDB api (V3), get access to user list and add recommended movies to
|
||||
*/
|
||||
//ToDo
|
||||
/* private void createTmdbList(@Nullable String sessionId) {
|
||||
OkHttpClient client;
|
||||
MediaType mediaType = MediaType.parse("application/json");
|
||||
RequestBody body;
|
||||
|
||||
// Add item to TMDB list specified by user
|
||||
int counter = 0;
|
||||
if (sessionId != null)
|
||||
for (Movie m : recommended) {
|
||||
client = new OkHttpClient();
|
||||
|
||||
body = RequestBody.create("{\"media_id\":" + m.getTvdbId() + "}", mediaType);
|
||||
|
||||
HttpUrl url = new HttpUrl.Builder()
|
||||
.scheme("https")
|
||||
.host("api.themoviedb.org")
|
||||
.addPathSegment("3")
|
||||
.addPathSegment("list")
|
||||
.addPathSegment(gaps.getMovieDbListId())
|
||||
.addPathSegment("add_item")
|
||||
.addQueryParameter("session_id", sessionId)
|
||||
.addQueryParameter("api_key", gaps.getMovieDbApiKey())
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.addHeader("content-type", "application/json;charset=utf-8")
|
||||
.build();
|
||||
|
||||
try {
|
||||
|
||||
Response response = client.newCall(request).execute();
|
||||
if (response.isSuccessful())
|
||||
counter++;
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Unable to add movie: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
LOGGER.info(counter + " Movies added to list. \nList located at: https://www.themoviedb.org/list/" + gaps.getMovieDbListId());
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Connect to plex via the URL and parse all the movies from the returned XML creating a HashSet of movies the
|
||||
* user has.
|
||||
*/
|
||||
private void findAllPlexMovies(Map<MoviePair, Movie> previousMovies, List<String> urls) throws SearchCancelledException {
|
||||
LOGGER.info("findAllPlexMovies()");
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(180, TimeUnit.SECONDS)
|
||||
.writeTimeout(180, TimeUnit.SECONDS)
|
||||
.readTimeout(180, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
if (CollectionUtils.isEmpty(urls)) {
|
||||
LOGGER.info("No URLs added to plexMovieUrls. Check your application.yaml file if needed.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (String url : urls) {
|
||||
//Cancel search if needed
|
||||
if (cancelSearch.get()) {
|
||||
throw new SearchCancelledException("Search cancelled");
|
||||
}
|
||||
|
||||
try {
|
||||
HttpUrl httpUrl = urlGenerator.generatePlexUrl(url);
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(httpUrl)
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
String body = response.body() != null ? response.body().string() : null;
|
||||
|
||||
if (StringUtils.isBlank(body)) {
|
||||
String reason = "Body returned empty from Plex";
|
||||
LOGGER.error(reason);
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, reason);
|
||||
}
|
||||
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
Node node = nodeList.item(i);
|
||||
|
||||
Node nodeTitle = node.getAttributes().getNamedItem("title");
|
||||
|
||||
if (nodeTitle == null) {
|
||||
String reason = "Missing title from Video element in Plex";
|
||||
LOGGER.error(reason);
|
||||
throw new NullPointerException(reason);
|
||||
}
|
||||
|
||||
//Files can't have : so need to remove to find matches correctly
|
||||
String title = nodeTitle.getNodeValue().replaceAll(":", "");
|
||||
if (node.getAttributes().getNamedItem("year") == null) {
|
||||
LOGGER.warn("Year not found for " + title);
|
||||
continue;
|
||||
}
|
||||
int year = Integer.parseInt(node.getAttributes().getNamedItem("year").getNodeValue());
|
||||
|
||||
String guid = "";
|
||||
if (node.getAttributes().getNamedItem("guid") != null) {
|
||||
guid = node.getAttributes().getNamedItem("guid").getNodeValue();
|
||||
}
|
||||
|
||||
String thumbnail = "";
|
||||
if (node.getAttributes().getNamedItem("thumb") != null) {
|
||||
thumbnail = node.getAttributes().getNamedItem("thumb").getNodeValue();
|
||||
}
|
||||
|
||||
Movie movie;
|
||||
if (guid.contains("com.plexapp.agents.themoviedb")) {
|
||||
//ToDo
|
||||
//Find out what it looks like in TMDB
|
||||
//language = ??
|
||||
guid = guid.substring(guid.indexOf(ID_IDX_START) + ID_IDX_START.length(), guid.indexOf(ID_IDX_END));
|
||||
movie = getOrCreateOwnedMovie(previousMovies, title, year, thumbnail, Integer.parseInt(guid), null, null, -1, null);
|
||||
} else if (guid.contains("com.plexapp.agents.imdb://")) {
|
||||
String language = guid.substring(guid.indexOf("?lang=") + "?lang=".length());
|
||||
language = new Locale(language, "").getDisplayLanguage();
|
||||
guid = guid.substring(guid.indexOf(ID_IDX_START) + ID_IDX_START.length(), guid.indexOf(ID_IDX_END));
|
||||
movie = getOrCreateOwnedMovie(previousMovies, title, year, thumbnail, -1, guid, language, -1, null);
|
||||
} else {
|
||||
LOGGER.warn("Cannot handle guid value of " + guid);
|
||||
movie = getOrCreateOwnedMovie(previousMovies, title, year, thumbnail, -1, null, null, -1, null);
|
||||
}
|
||||
|
||||
ownedMovies.add(movie);
|
||||
totalMovieCount.incrementAndGet();
|
||||
}
|
||||
LOGGER.debug(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 | NullPointerException e) {
|
||||
String reason = "Error with plex Url: " + url;
|
||||
LOGGER.error(reason, e);
|
||||
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, reason, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Movie getOrCreateOwnedMovie(Map<MoviePair, Movie> previousMovies, String title, int year, String thumbnail, int tvdbId, String imdbId, String language, int collection, String collectionName) {
|
||||
MoviePair moviePair = new MoviePair(title, year);
|
||||
if (previousMovies.containsKey(moviePair)) {
|
||||
return previousMovies.get(moviePair);
|
||||
} else {
|
||||
return new Movie.Builder(title, year)
|
||||
.setPosterUrl(thumbnail)
|
||||
.setTvdbId(tvdbId)
|
||||
.setImdbId(imdbId)
|
||||
.setLanguage(language)
|
||||
.setCollectionId(collection)
|
||||
.setCollection(collectionName)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* With all of the movies to search, now the connections to MovieDB need to be made. First we must search for
|
||||
* movie keys by movie name and year. With the movie key we can get full properties of a movie. Once we have the
|
||||
@@ -497,13 +157,19 @@ public class GapsSearchService implements GapsSearch {
|
||||
* optimize some network calls, we add movies found in a collection and in plex to our already searched list, so we
|
||||
* don't re-query collections again and again.
|
||||
*/
|
||||
private void searchForMovies() throws SearchCancelledException {
|
||||
private void searchForMovies(Set<Movie> ownedMovies, List<Movie> everyMovie, Set<Movie> recommended, Set<Movie> searched,
|
||||
AtomicInteger searchedMovieCount) throws SearchCancelledException, IOException {
|
||||
LOGGER.info("searchForMovies()");
|
||||
OkHttpClient client = new OkHttpClient();
|
||||
|
||||
if (StringUtils.isEmpty(gapsService.getPlexSearch().getMovieDbApiKey())) {
|
||||
LOGGER.error("No MovieDb Key added to movieDbApiKey. Need to submit movieDbApiKey on each request.");
|
||||
return;
|
||||
gapsService.updatePlexSearch(ioService.readProperties());
|
||||
|
||||
if (StringUtils.isEmpty(gapsService.getPlexSearch().getMovieDbApiKey())) {
|
||||
final String error = "No MovieDb Key found. Need to configure key first.";
|
||||
LOGGER.error(error);
|
||||
throw new IllegalStateException(error);
|
||||
}
|
||||
}
|
||||
|
||||
for (Movie movie : ownedMovies) {
|
||||
@@ -520,7 +186,7 @@ public class GapsSearchService implements GapsSearch {
|
||||
|
||||
//Print the count first to handle the continue if block or the regular searching case
|
||||
if (searchedMovieCount.get() % 10 == 0) {
|
||||
LOGGER.info(((int) ((searchedMovieCount.get()) / ((double) (totalMovieCount.get())) * 100)) + "% Complete. Processed " + searchedMovieCount.get() + " files of " + totalMovieCount.get() + ". ");
|
||||
LOGGER.info(((int) ((searchedMovieCount.get()) / ((double) (ownedMovies.size())) * 100)) + "% Complete. Processed " + searchedMovieCount.get() + " files of " + ownedMovies.size() + ". ");
|
||||
}
|
||||
searchedMovieCount.incrementAndGet();
|
||||
|
||||
@@ -537,12 +203,12 @@ public class GapsSearchService implements GapsSearch {
|
||||
if (movie.getTvdbId() != -1 && movie.getCollectionId() != -1) {
|
||||
LOGGER.info("Used Collection ID to get " + movie.getName());
|
||||
tempTvdbCounter.incrementAndGet();
|
||||
handleCollection(movie, client, languageCode);
|
||||
handleCollection(ownedMovies, everyMovie, recommended, searched, searchedMovieCount, movie, client, languageCode);
|
||||
continue;
|
||||
} else if (movie.getTvdbId() != -1) {
|
||||
LOGGER.info("Used TVDB ID to get " + movie.getName());
|
||||
tempTvdbCounter.incrementAndGet();
|
||||
searchMovieDetails(movie, client, languageCode);
|
||||
searchMovieDetails(ownedMovies, everyMovie, recommended, searched, searchedMovieCount, movie, client, languageCode);
|
||||
continue;
|
||||
} else if (StringUtils.isNotBlank(movie.getImdbId())) {
|
||||
LOGGER.info("Used 'find' to search for " + movie.getName());
|
||||
@@ -611,7 +277,7 @@ public class GapsSearchService implements GapsSearch {
|
||||
everyMovie.add(newMovie);
|
||||
}
|
||||
|
||||
searchMovieDetails(movie, client, languageCode);
|
||||
searchMovieDetails(ownedMovies, everyMovie, recommended, searched, searchedMovieCount, movie, client, languageCode);
|
||||
} catch (JsonProcessingException e) {
|
||||
LOGGER.error("Error parsing movie " + movie + ". " + e.getMessage());
|
||||
LOGGER.error("URL: " + searchMovieUrl);
|
||||
@@ -644,7 +310,8 @@ public class GapsSearchService implements GapsSearch {
|
||||
}
|
||||
}
|
||||
|
||||
private void searchMovieDetails(Movie movie, OkHttpClient client, String languageCode) {
|
||||
private void searchMovieDetails(Set<Movie> ownedMovies, List<Movie> everyMovie, Set<Movie> recommended, Set<Movie> searched,
|
||||
AtomicInteger searchedMovieCount, Movie movie, OkHttpClient client, String languageCode) {
|
||||
LOGGER.info("searchMovieDetails()");
|
||||
HttpUrl movieDetailUrl = urlGenerator.generateMovieDetailUrl(gapsService.getPlexSearch().getMovieDbApiKey(), String.valueOf(movie.getImdbId()), languageCode);
|
||||
|
||||
@@ -690,14 +357,15 @@ public class GapsSearchService implements GapsSearch {
|
||||
everyMovie.add(newMovie);
|
||||
}
|
||||
|
||||
handleCollection(movie, client, languageCode);
|
||||
handleCollection(ownedMovies, everyMovie, recommended, searched, searchedMovieCount, movie, client, languageCode);
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Error getting movie details " + movie, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCollection(Movie movie, OkHttpClient client, String languageCode) {
|
||||
private void handleCollection(Set<Movie> ownedMovies, List<Movie> everyMovie, Set<Movie> recommended, Set<Movie> searched,
|
||||
AtomicInteger searchedMovieCount, Movie movie, OkHttpClient client, String languageCode) {
|
||||
LOGGER.info("handleCollection()");
|
||||
HttpUrl collectionUrl = urlGenerator.generateCollectionUrl(gapsService.getPlexSearch().getMovieDbApiKey(), String.valueOf(movie.getCollectionId()), languageCode);
|
||||
|
||||
@@ -789,7 +457,7 @@ public class GapsSearchService implements GapsSearch {
|
||||
|
||||
if (ownedMovies.contains(ownedMovieFromCollection)) {
|
||||
searched.add(movieFromCollection);
|
||||
sendEmptySearchUpdate();
|
||||
sendEmptySearchUpdate(ownedMovies.size(), searchedMovieCount);
|
||||
} else if (!searched.contains(movieFromCollection) && year != 0 && year < Year.now().getValue()) {
|
||||
// Get recommended Movie details from MovieDB API
|
||||
HttpUrl movieDetailUrl = urlGenerator.generateMovieDetailUrl(gapsService.getPlexSearch().getMovieDbApiKey(), String.valueOf(movieFromCollection.getTvdbId()), languageCode);
|
||||
@@ -843,7 +511,7 @@ public class GapsSearchService implements GapsSearch {
|
||||
LOGGER.info("/newMovieFound:" + recommendedMovie.toString());
|
||||
|
||||
//Send message over websocket
|
||||
SearchResults searchResults = new SearchResults(getSearchedMovieCount(), getTotalMovieCount(), recommendedMovie);
|
||||
SearchResults searchResults = new SearchResults(searchedMovieCount.get(), ownedMovies.size(), recommendedMovie);
|
||||
template.convertAndSend("/newMovieFound", objectMapper.writeValueAsString(searchResults));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -851,7 +519,7 @@ public class GapsSearchService implements GapsSearch {
|
||||
}
|
||||
|
||||
} else {
|
||||
sendEmptySearchUpdate();
|
||||
sendEmptySearchUpdate(ownedMovies.size(), searchedMovieCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -862,69 +530,11 @@ public class GapsSearchService implements GapsSearch {
|
||||
searched.add(movie);
|
||||
}
|
||||
|
||||
private void sendEmptySearchUpdate() throws JsonProcessingException {
|
||||
private void sendEmptySearchUpdate(int totalMovieCount, AtomicInteger searchedMovieCount) throws JsonProcessingException {
|
||||
//Send message over websocket
|
||||
//No new movie, just updated counts
|
||||
SearchResults searchResults = new SearchResults(getSearchedMovieCount(), getTotalMovieCount(), null);
|
||||
SearchResults searchResults = new SearchResults(searchedMovieCount.get(), totalMovieCount, null);
|
||||
template.convertAndSend("/newMovieFound", objectMapper.writeValueAsString(searchResults));
|
||||
}
|
||||
|
||||
|
||||
/* public static class UserInputThreadCountdown implements Runnable {
|
||||
|
||||
int time_limit = 60;
|
||||
|
||||
Date start;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
start = new Date();
|
||||
try {
|
||||
this.runTimer();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void runTimer() throws IOException {
|
||||
long timePassedStart;
|
||||
do {
|
||||
timePassedStart = (new Date().getTime() - start.getTime()) / 1000;
|
||||
} while (timePassedStart < time_limit);
|
||||
System.in.close();
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
private List<String> generatePlexUrls() {
|
||||
LOGGER.info("generatePlexUrls()");
|
||||
List<String> urls = new ArrayList<>();
|
||||
gapsService.getPlexSearch()
|
||||
.getPlexServers()
|
||||
.forEach(plexServer -> urls
|
||||
.addAll(plexServer
|
||||
.getPlexLibraries()
|
||||
.stream()
|
||||
.filter(PlexLibrary::getSelected)
|
||||
.map(plexLibrary -> "http://" + plexServer.getAddress() + ":" + plexServer.getPort() + "/library/sections/" + plexLibrary.getKey() + "/all/?X-Plex-Token=" + plexServer.getPlexToken())
|
||||
.collect(Collectors.toList())));
|
||||
LOGGER.info("URLS: " + urls.size());
|
||||
return urls;
|
||||
}
|
||||
|
||||
private List<String> generatePlexUrls(String machineIdentifier, Integer key) {
|
||||
LOGGER.info("generatePlexUrls( " + machineIdentifier + ", " + key + " )");
|
||||
return gapsService
|
||||
.getPlexSearch()
|
||||
.getPlexServers()
|
||||
.stream()
|
||||
.filter(plexServer -> plexServer.getMachineIdentifier().equals(machineIdentifier))
|
||||
.map(plexServer -> plexServer
|
||||
.getPlexLibraries()
|
||||
.stream()
|
||||
.filter(plexLibrary -> plexLibrary.getKey().equals(key))
|
||||
.map(plexLibrary -> "http://" + plexServer.getAddress() + ":" + plexServer.getPort() + "/library/sections/" + plexLibrary.getKey() + "/all/?X-Plex-Token=" + plexServer.getPlexToken())
|
||||
.findFirst().orElse(null))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,14 +89,32 @@ public class IoService {
|
||||
return new File(STORAGE_FOLDER + RECOMMENDED_MOVIES).exists();
|
||||
}
|
||||
|
||||
public @NotNull String getRecommendedMovies() {
|
||||
try {
|
||||
Path path = new File(STORAGE_FOLDER + RECOMMENDED_MOVIES).toPath();
|
||||
return new String(Files.readAllBytes(path));
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Check for the recommended file next time", e);
|
||||
return "";
|
||||
public @NotNull List<Movie> readRecommendedMovies(String machineIdentifier, int key) {
|
||||
LOGGER.info("readRecommendedMovies( " + machineIdentifier + ", " + key + " )");
|
||||
|
||||
final File ownedMovieFile = new File(STORAGE_FOLDER + machineIdentifier + File.separator + key + File.separator + RECOMMENDED_MOVIES);
|
||||
|
||||
if (!ownedMovieFile.exists()) {
|
||||
LOGGER.warn(ownedMovieFile + " does not exist");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(ownedMovieFile))) {
|
||||
StringBuilder fullFile = new StringBuilder();
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
fullFile.append(line);
|
||||
}
|
||||
|
||||
return objectMapper.readValue(fullFile.toString(), new TypeReference<List<Movie>>() {
|
||||
});
|
||||
} catch (FileNotFoundException e) {
|
||||
LOGGER.error("Can't find file " + ownedMovieFile, e);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Can't read the file " + ownedMovieFile, e);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public boolean doesRssFileExist() {
|
||||
|
||||
@@ -8,21 +8,7 @@
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
$(document).ready(function () {
|
||||
moviesTable = $('#movies').DataTable();
|
||||
});
|
||||
|
||||
/*
|
||||
* Copyright 2019 Jason H House
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
let libraryTitle, noMovieContainer, movieContainer, movieSearchContainer;
|
||||
let libraryTitle, notSearchedYetContainer, movieContainer, searchContainer, noMovieContainer;
|
||||
let plexServers;
|
||||
let plexServer;
|
||||
let moviesTable;
|
||||
@@ -30,9 +16,7 @@ let libraryKey;
|
||||
|
||||
let stompClient;
|
||||
let backButton;
|
||||
let copyToClipboard;
|
||||
let searchResults;
|
||||
let progressContainer;
|
||||
let searchTitle;
|
||||
let searchDescription;
|
||||
let movieCounter;
|
||||
@@ -43,30 +27,36 @@ jQuery(function ($) {
|
||||
});
|
||||
|
||||
libraryTitle = $('#libraryTitle');
|
||||
noMovieContainer = $('#noMovieContainer');
|
||||
notSearchedYetContainer = $('#notSearchedYetContainer');
|
||||
movieContainer = $('#movieContainer');
|
||||
noMovieContainer = $('#noMovieContainer');
|
||||
movieSearchingContainer = $('#movieSearchingContainer');
|
||||
plexServers = JSON.parse($('#plexServers').val());
|
||||
plexServer = JSON.parse($('#plexServer').val());
|
||||
libraryKey = $('#libraryKey').val();
|
||||
|
||||
backButton = $('#cancel');
|
||||
copyToClipboard = $('#copyToClipboard');
|
||||
|
||||
searchResults = [];
|
||||
movieSearchContainer = $('#movieSearchContainer');
|
||||
progressContainer = $('#progressContainer');
|
||||
searchContainer = $('#searchContainer');
|
||||
searchTitle = $('#searchTitle');
|
||||
searchDescription = $('#searchDescription');
|
||||
|
||||
moviesTable = $('#movies').DataTable({
|
||||
"initComplete": function (settings, json) {
|
||||
getMoviesForTable(`/libraries/${plexServer.machineIdentifier}/${libraryKey}`);
|
||||
initComplete: function () {
|
||||
getMoviesForTable(`/recommended/${plexServer.machineIdentifier}/${libraryKey}`);
|
||||
},
|
||||
ordering: false,
|
||||
deferRender: true,
|
||||
search: true,
|
||||
columns: [
|
||||
{
|
||||
data: "card",
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
row.address = plexServer.address;
|
||||
row.port = plexServer.port;
|
||||
row.plexToken = plexServer.plexToken;
|
||||
|
||||
const plexServerCard = $("#movieCard").html();
|
||||
const theTemplate = Handlebars.compile(plexServerCard);
|
||||
return theTemplate(row);
|
||||
@@ -76,6 +66,7 @@ jQuery(function ($) {
|
||||
},
|
||||
{
|
||||
data: "title",
|
||||
searchable: true,
|
||||
visible: false,
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display' && row.name) {
|
||||
@@ -86,6 +77,7 @@ jQuery(function ($) {
|
||||
},
|
||||
{
|
||||
data: "year",
|
||||
searchable: true,
|
||||
visible: false,
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display' && row.year) {
|
||||
@@ -96,6 +88,7 @@ jQuery(function ($) {
|
||||
},
|
||||
{
|
||||
data: "language",
|
||||
searchable: true,
|
||||
visible: false,
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display' && row.language) {
|
||||
@@ -105,24 +98,17 @@ jQuery(function ($) {
|
||||
}
|
||||
},
|
||||
{
|
||||
data: "collection",
|
||||
data: "summary",
|
||||
searchable: true,
|
||||
visible: false,
|
||||
render: function (data, type, row) {
|
||||
if (type === 'display' && row.collection) {
|
||||
return row.collection;
|
||||
if (type === 'display' && row.overview) {
|
||||
return row.overview;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
},
|
||||
],
|
||||
select: {
|
||||
style: 'os',
|
||||
selector: 'td:not(:last-child)' // no row selection on last column
|
||||
},
|
||||
rowCallback: function (row, data) {
|
||||
// Set the checked state of the checkbox in the table
|
||||
$('input.editor-active', row).prop('checked', data.active == 1);
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -130,18 +116,22 @@ jQuery(function ($) {
|
||||
stompClient = Stomp.over(socket);
|
||||
stompClient.connect({}, function () {
|
||||
|
||||
stompClient.subscribe('/finishedSearching', function (successful) {
|
||||
progressContainer.hide();
|
||||
stompClient.subscribe('/finishedSearching', function (payload) {
|
||||
searchContainer.css({'display': 'none'});
|
||||
|
||||
backButton.text('Restart');
|
||||
disconnect();
|
||||
if (successful) {
|
||||
if (payload && payload.code === 3) {
|
||||
searchTitle.text(`Search Complete`);
|
||||
searchDescription.text(`${movieCounter} movies to add to complete your collections. Below is everything Gaps found that is missing from your movie collections.`);
|
||||
setCopyToClipboardEnabled(true);
|
||||
} else {
|
||||
searchTitle.text("Search Failed");
|
||||
searchDescription.text("Unknown error. Check docker Gaps log file.");
|
||||
searchDescription.text(payload.reason);
|
||||
setCopyToClipboardEnabled(false);
|
||||
movieContainer.css({'display': 'none'});
|
||||
notSearchedYetContainer.css({'display': 'none'});
|
||||
noMovieContainer.show(100);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,10 +167,11 @@ function switchPlexLibrary(machineIdentifier, key) {
|
||||
const plexLibrary = plexServer.plexLibraries.find(plexServer => plexServer.key === parseInt(key));
|
||||
libraryTitle.text(`${plexServer.friendlyName} - ${plexLibrary.title}`);
|
||||
|
||||
notSearchedYetContainer.css({'display': 'none'});
|
||||
moviesTable.data().clear();
|
||||
moviesTable.rows().invalidate().draw();
|
||||
|
||||
getMoviesForTable(`/libraries/${machineIdentifier}/${libraryKey}`);
|
||||
getMoviesForTable(`/recommended/${machineIdentifier}/${libraryKey}`);
|
||||
}
|
||||
|
||||
function getMoviesForTable(url) {
|
||||
@@ -192,57 +183,55 @@ function getMoviesForTable(url) {
|
||||
success: function (result) {
|
||||
if (result.success) {
|
||||
movieContainer.show(100);
|
||||
noMovieContainer.css({'display':'none'});
|
||||
notSearchedYetContainer.css({'display': 'none'});
|
||||
moviesTable.rows.add(JSON.parse(result.movies)).draw();
|
||||
} else {
|
||||
movieContainer.css({'display':'none'});
|
||||
noMovieContainer.show(100);
|
||||
movieContainer.css({'display': 'none'});
|
||||
notSearchedYetContainer.show(100);
|
||||
}
|
||||
}, error: function () {
|
||||
movieContainer.css({'display':'none'});
|
||||
noMovieContainer.show(100);
|
||||
movieContainer.css({'display': 'none'});
|
||||
notSearchedYetContainer.show(100);
|
||||
//Show error + error
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
stompClient.send("/cancelSearching");
|
||||
stompClient.send(`/recommended/cancel/${plexServer.machineIdentifier}/${libraryKey}`);
|
||||
|
||||
//Navigate Home
|
||||
location.assign("/");
|
||||
}
|
||||
|
||||
function viewRss() {
|
||||
location.assign("rssCheck");
|
||||
}
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
disconnect();
|
||||
};
|
||||
|
||||
function setCopyToClipboardEnabled(bool) {
|
||||
if (bool) {
|
||||
copyToClipboard.removeClass('disabled');
|
||||
$('#copyToClipboard').removeClass('disabled');
|
||||
} else {
|
||||
copyToClipboard.addClass('disabled');
|
||||
$('#copyToClipboard').addClass('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
function searchForMovies() {
|
||||
movieContainer.show(100);
|
||||
movieSearchContainer.show(100);
|
||||
noMovieContainer.css({'display':'none'});
|
||||
searchContainer.show(100);
|
||||
notSearchedYetContainer.css({'display': 'none'});
|
||||
noMovieContainer.css({'display': 'none'});
|
||||
|
||||
//reset movie counter;
|
||||
movieCounter = 0;
|
||||
progressContainer.show();
|
||||
searchTitle.text("Searching for Movies");
|
||||
searchDescription.text("Gaps is looking through your Plex libraries. This could take a while so just sit tight, and we'll find all the missing movies for you.");
|
||||
|
||||
//ToDo
|
||||
//Change to searching with recommended
|
||||
$.ajax({
|
||||
type: "PUT",
|
||||
url: `/search/start/${plexServer.machineIdentifier}/${libraryKey}`,
|
||||
url: `/recommended/find/${plexServer.machineIdentifier}/${libraryKey}`,
|
||||
contentType: "application/json"
|
||||
});
|
||||
|
||||
@@ -260,21 +249,21 @@ function showSearchStatus(obj) {
|
||||
if (!obj) {
|
||||
searchDescription.html("");
|
||||
} else {
|
||||
let percentage = Math.trunc(obj.searchedMovieCount / obj.totalMovieCount * 100);
|
||||
searchDescription.html(`${obj.searchedMovieCount} of ${obj.totalMovieCount} movies searched. ${percentage}% complete.`);
|
||||
obj.percentage = Math.trunc(obj.searchedMovieCount / obj.totalMovieCount * 100);
|
||||
|
||||
const plexServerCard = $("#updateSearchDescription").html();
|
||||
const theTemplate = Handlebars.compile(plexServerCard);
|
||||
searchDescription.html(theTemplate(obj));
|
||||
}
|
||||
}
|
||||
|
||||
function encodeQueryData(data) {
|
||||
const ret = [];
|
||||
for (let d in data) {
|
||||
ret.push(encodeURIComponent(d) + '=' + encodeURIComponent(data[d]));
|
||||
}
|
||||
return ret.join('&');
|
||||
}
|
||||
|
||||
function copy(arr) {
|
||||
const stringified = arr.join('\r\n');
|
||||
$('<input>').val(stringified).appendTo('body').select();
|
||||
document.execCommand('copy');
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
copy(searchResults);
|
||||
$('#copiedToClipboard').show();
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jason H House
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
$(document).ready(function () {
|
||||
moviesTable = $('#movies').DataTable();
|
||||
});
|
||||
@@ -47,33 +47,129 @@
|
||||
<div class="container">
|
||||
<h3 class="top-margin">Recommended Movies</h3>
|
||||
|
||||
<div class="top-margin">
|
||||
<table id="movies" class="display" style="width:100%">
|
||||
<h4 class="top-margin" id="libraryTitle"
|
||||
th:text="${plexServer.friendlyName} + ' - ' + ${plexLibrary.title}"></h4>
|
||||
|
||||
<div class="dropdown show">
|
||||
<a class="btn btn-primary dropdown-toggle" href="#" role="button" id="dropdownMenuLink"
|
||||
data-toggle="dropdown"
|
||||
aria-haspopup="true" aria-expanded="false">
|
||||
Recommended
|
||||
</a>
|
||||
|
||||
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
|
||||
<div th:each="instance : ${plexServers}" th:remove="tag">
|
||||
<a href="javascript:void(0)"
|
||||
th:data-machineIdentifier="*{instance.value.machineIdentifier}"
|
||||
th:data-key="*{plexLibrary.key}"
|
||||
th:onclick="switchPlexLibrary(this.getAttribute('data-machineIdentifier'), this.getAttribute('data-key'))"
|
||||
class="dropdown-item"
|
||||
th:each="plexLibrary : *{instance.value.plexLibraries}"
|
||||
data-ol-has-click-handler=""
|
||||
th:text="(*{instance.value.friendlyName} + ' - ' + *{plexLibrary.title})"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="notSearchedYetContainer" class="top-margin bottom-margin" style="display: none;">
|
||||
<div class="card mx-auto" style="width: 24rem;">
|
||||
<img src="/images/mind_the_gap.png" class="card-img-top" alt="...">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Your movies are really missing</h5>
|
||||
<p class="card-text">You need to run Gaps at least once to have found the missing movies.</p>
|
||||
<a href="javascript:void(0)" onclick="searchForMovies()" class="btn btn-primary">Search</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="noMovieContainer" class="top-margin bottom-margin" style="display: none;">
|
||||
<div class="card mx-auto" style="width: 24rem;">
|
||||
<img src="/images/mind_the_gap.png" class="card-img-top" alt="...">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Your movies are really missing</h5>
|
||||
<p class="card-text">You need to find your owned movies first.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="movieContainer" class="top-margin bottom-margin" style="display: none;">
|
||||
|
||||
<div id="searchContainer" style="display: none;">
|
||||
<div id="copiedToClipboard" class="alert alert-primary alert-dismissible fade show gaps-hide" role="alert">
|
||||
Copied to Clipboard
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2 id="searchTitle" class="top-margin">Searching for Movies</h2>
|
||||
|
||||
<div id="searchDescription" class="top-margin">
|
||||
<p>Finding all movies in Plex</p>
|
||||
</div>
|
||||
|
||||
<div id="progressContainer" class="progress top-margin">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar"
|
||||
aria-valuenow="75"
|
||||
aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="movies" class="display top-margin" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Year</th>
|
||||
<th>Collection</th>
|
||||
<th>Link</th>
|
||||
<th>Movie</th>
|
||||
<th hidden>Title</th>
|
||||
<th hidden>Year</th>
|
||||
<th hidden>Language</th>
|
||||
<th hidden>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="movie, status : ${recommended}">
|
||||
<td th:text="${movie.name}" class="word-break"></td>
|
||||
<td th:text="${movie.year}"></td>
|
||||
<td th:text="${movie.collection}" class="word-break"></td>
|
||||
<td><a target="_blank" th:href="${urls.get(status.index)}">Details</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<button type="button" class="btn btn-secondary" onclick="copyToClipboard()">Copy</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="searchForMovies()">Re-Search</button>
|
||||
<button type="button" class="btn btn-warning" onclick="cancel()">Cancel</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script id="movieCard" type="text/x-handlebars-template">
|
||||
<div>{{json movie}}</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-2">
|
||||
<img style="height: 225px; width: 150px; display: block;"
|
||||
src="{{poster_url}}"
|
||||
class="card-img" alt="Plex Poster">
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{name}} ({{year}})</h5>
|
||||
<p class="card-text text-muted">{{overview}}</p>
|
||||
<p class="card-text"><small class="text-info">English</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="updateSearchDescription" type="text/x-handlebars-template">
|
||||
<p class="card-text">{{searchedMovieCount}} of {{totalMovieCount}} movies searched. {{percentage}}% complete.</p>
|
||||
</script>
|
||||
|
||||
<input id="plexServers" name="plexServers" th:value="${{plexServers}}" type="hidden"/>
|
||||
<input id="plexServer" name="plexServer" th:value="${{plexServer}}" type="hidden"/>
|
||||
<input id="plexSearch" name="plexSearch" th:value="${{plexSearch}}" type="hidden"/>
|
||||
<input id="libraryKey" name="libraryKey" th:value="${{plexLibrary.key}}" type="hidden"/>
|
||||
|
||||
<script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
|
||||
<script type="text/javascript" src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="text/javascript" src="/js/datatables.min.js"></script>
|
||||
<script type="text/javascript" src="/js/sockjs-1.4.0.min.js"></script>
|
||||
<script type="text/javascript" src="/js/stomp-2.3.3.min.js"></script>
|
||||
<script type="text/javascript" src="/js/handlebars.js"></script>
|
||||
<script type="text/javascript" src="/js/recommended.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,92 +0,0 @@
|
||||
<!--
|
||||
- Copyright 2019 Jason H House
|
||||
-
|
||||
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
-
|
||||
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
-
|
||||
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-->
|
||||
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<title>Gaps</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link rel="shortcut icon" href="/images/gaps.ico"/>
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/css/datatables.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/css/dataTables.bootstrap4.min.css"/>
|
||||
<link rel="stylesheet" href="/css/input.min.css">
|
||||
|
||||
<!--Let browser know website is optimized for mobile-->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<a class="navbar-brand" href="/">Gaps</a>
|
||||
<div class="collapse navbar-collapse" id="navbarColor01">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/configuration">Configuration</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/libraries">Libraries</a>
|
||||
</li>
|
||||
<li class="nav-item" aria-current="page">
|
||||
<a class="nav-link" href="/recommended">Recommended</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/rssCheck">RSS</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
<h3 class="top-margin">Recommended Movies</h3>
|
||||
|
||||
<div class="top-margin">
|
||||
<table id="movies" class="display" style="width:100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Movie</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="movie, status : ${recommended}">
|
||||
<td>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-2">
|
||||
<img style="height: 100%; width: 150px; display: block;" th:src="'http://' + ${address}+ ':' + ${port} + ${movie.posterUrl} + '/?X-Plex-Token='+ ${plexToken}" class="card-img" alt="Plex Poster">
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title" th:text="${movie.name} + ' (' + ${movie.year} + ')'"></h5>
|
||||
<p class="card-text" th:text="${movie.collection}"></p>
|
||||
<p class="card-text">Though Goofy always means well, his amiable cluelessness and klutzy pratfalls regularly embarrass his awkward adolescent son, Max. When Max's lighthearted prank on his high-school principal finally gets his longtime crush, Roxanne, to notice him, he asks her on a date. Max's trouble at school convinces Goofy that he and the boy need to bond over a cross-country fishing trip like the one he took with his dad when he was Max's age, which throws a kink in his son's plans to impress Roxanne.</p>
|
||||
<p class="card-text"><small class="text-muted">English</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
|
||||
<script type="text/javascript" src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script type="text/javascript" src="/js/datatables.min.js"></script>
|
||||
<script type="text/javascript" src="/js/sockjs-1.4.0.min.js"></script>
|
||||
<script type="text/javascript" src="/js/stomp-2.3.3.min.js"></script>
|
||||
<script type="text/javascript" src="/js/test.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jason H House
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.jasonhhouse.gaps;
|
||||
|
||||
import com.jasonhhouse.gaps.service.GapsSearchService;
|
||||
import com.jasonhhouse.gaps.service.IoService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
|
||||
@SpringBootTest(classes = GapsApplication.class)
|
||||
class GapsSearchServiceTest {
|
||||
|
||||
private GapsUrlGeneratorTest gapsUrlGeneratorTest;
|
||||
private GapsSearchService gapsSearch;
|
||||
|
||||
@Mock
|
||||
private IoService ioService;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
gapsUrlGeneratorTest = new GapsUrlGeneratorTest();
|
||||
SimpMessagingTemplate template = new SimpMessagingTemplate((message, l) -> true);
|
||||
|
||||
GapsService gapsService = new GapsServiceTest();
|
||||
|
||||
gapsSearch = new GapsSearchService(gapsUrlGeneratorTest, template, ioService, gapsService);
|
||||
|
||||
// Create a MockWebServer. These are lean enough that you can create a new
|
||||
// instance for every unit test.
|
||||
MockWebServer server = new MockWebServer();
|
||||
gapsUrlGeneratorTest.setMockWebServer(server);
|
||||
gapsUrlGeneratorTest.setupServer();
|
||||
server.start();
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultTotalMovieCount() {
|
||||
assertEquals(0, gapsSearch.getTotalMovieCount(), "Should be zero by default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultSearchedMovieCount() {
|
||||
assertEquals(0, gapsSearch.getSearchedMovieCount(), "Should be zero by default");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultRecommendedMovieCount() {
|
||||
assertEquals(0, gapsSearch.getRecommendedMovies().size(), "Should be zero by default");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user