Showing owned movies properly now

This commit is contained in:
jhouse
2020-02-06 08:56:32 +09:00
parent d6fa6668c8
commit 3dc596c018
14 changed files with 691 additions and 198 deletions

View File

@@ -21,8 +21,14 @@ 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
*/

View File

@@ -38,6 +38,8 @@ public final class Movie implements Comparable<Movie> {
public static final String LANGUAGE = "language";
public static final String OVERVIEW = "overview";
private final String name;
private final int year;
@@ -59,7 +61,11 @@ public final class Movie implements Comparable<Movie> {
@Nullable
private String language;
private Movie(String name, int year, @Nullable String posterUrl, @Nullable String collection, int collectionId, int tvdbId, @Nullable String imdbId, @Nullable String language) {
@Nullable
private String overview;
private Movie(String name, int year, @Nullable String posterUrl, @Nullable String collection, int collectionId, int tvdbId,
@Nullable String imdbId, @Nullable String language, @Nullable String overview) {
this.name = name;
this.year = year;
this.posterUrl = posterUrl;
@@ -68,6 +74,7 @@ public final class Movie implements Comparable<Movie> {
this.tvdbId = tvdbId;
this.imdbId = imdbId;
this.language = language;
this.overview = overview;
}
public void setCollection(@Nullable String collection) {
@@ -117,6 +124,11 @@ public final class Movie implements Comparable<Movie> {
return language;
}
@Nullable
public String getOverview() {
return overview;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -163,6 +175,8 @@ public final class Movie implements Comparable<Movie> {
", collectionId=" + collectionId +
", tvdbId=" + tvdbId +
", imdbId='" + imdbId + '\'' +
", language='" + language + '\'' +
", overview='" + overview + '\'' +
'}';
}
@@ -188,6 +202,8 @@ public final class Movie implements Comparable<Movie> {
private String language;
private String overview;
public Builder(String name, int year) {
this.name = name;
this.year = year;
@@ -197,10 +213,11 @@ public final class Movie implements Comparable<Movie> {
this.posterUrl = "";
this.collectionId = -1;
this.language = "en";
this.overview = "";
}
public Movie build() {
return new Movie(name, year, posterUrl, collection, collectionId, tvdbId, imdbId, language);
return new Movie(name, year, posterUrl, collection, collectionId, tvdbId, imdbId, language, overview);
}
public Builder setPosterUrl(String posterUrl) {
@@ -232,5 +249,10 @@ public final class Movie implements Comparable<Movie> {
this.language = language;
return this;
}
public Builder setOverview(String overview) {
this.overview = overview;
return this;
}
}
}

View File

@@ -11,6 +11,7 @@
package com.jasonhhouse.gaps;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
/**
@@ -29,4 +30,10 @@ public interface PlexQuery {
* @param plexServer the search parameters
*/
void queryPlexServer(@NotNull PlexServer plexServer);
/**
* Connect to plex via the URL and parse all the movies from the returned XML creating a HashSet of movies the
* user has.
*/
List<Movie> findAllPlexMovies(Map<MoviePair, Movie> previousMovies, @NotNull String url);
}

View File

@@ -55,13 +55,19 @@ public class MovieDeserializer extends StdDeserializer<Movie> {
language = node.get(Movie.LANGUAGE).asText();
}
String overview = null;
if (node.has(Movie.OVERVIEW)) {
overview = node.get(Movie.OVERVIEW).asText();
}
Movie.Builder builder = new Movie.Builder(name, year)
.setTvdbId(tvdbId)
.setImdbId(imdbId)
.setCollectionId(collectionId)
.setCollection(collection)
.setPosterUrl(posterUrl)
.setLanguage(language);
.setLanguage(language)
.setOverview(overview);
return builder.build();
}

View File

@@ -36,6 +36,7 @@ public class MovieSerializer extends StdSerializer<Movie> {
jsonGenerator.writeNumberField(Movie.COLLECTION_ID, movie.getCollectionId());
jsonGenerator.writeStringField(Movie.COLLECTION, movie.getCollection());
jsonGenerator.writeStringField(Movie.LANGUAGE, movie.getLanguage());
jsonGenerator.writeStringField(Movie.OVERVIEW, movie.getOverview());
jsonGenerator.writeEndObject();
}
}

View File

@@ -3,6 +3,7 @@ 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.PlexLibrary;
@@ -20,11 +21,14 @@ 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;
@@ -36,11 +40,13 @@ public class LibraryController {
private final IoService ioService;
private final GapsService gapsService;
private final GapsSearch gapsSearch;
@Autowired
public LibraryController(IoService ioService, GapsService gapsService) {
public LibraryController(IoService ioService, GapsService gapsService, GapsSearch gapsSearch) {
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
@@ -134,7 +140,7 @@ public class LibraryController {
ObjectNode objectNode = objectMapper.createObjectNode();
if (movies == null) {
if (CollectionUtils.isEmpty(movies)) {
objectNode.put("success", false);
LOGGER.warn("Could not save PlexLibrary");
} else {
@@ -154,31 +160,25 @@ public class LibraryController {
return ResponseEntity.ok().body(objectNode.toString());
}
@MessageMapping("/search/cancel")
public void cancelSearching() {
LOGGER.info("cancelSearching()");
gapsSearch.cancelSearch();
}
/* private List<String> buildUrls(List<Movie> movies) {
LOGGER.info("buildUrls( " + movies + " ) ");
List<String> urls = new ArrayList<>();
for (Movie movie : movies) {
if (movie.getTvdbId() != -1) {
urls.add("https://www.themoviedb.org/movie/" + movie.getTvdbId());
continue;
}
/**
* 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 + " )");
if (StringUtils.isNotEmpty(movie.getImdbId())) {
urls.add("https://www.imdb.com/title/" + movie.getImdbId() + "/");
continue;
}
ioService.migrateJsonSeedFileIfNeeded();
gapsSearch.run(machineIdentifier, key);
}
urls.add(null);
}
return urls;
}*/
/*
@InitBinder
public void initBinder(WebDataBinder binder) {
LOGGER.info("initBinder()");
binder.addCustomFormatter(new PlexServersFormatter());
}*/
}

View File

@@ -11,64 +11,98 @@
package com.jasonhhouse.gaps.controller;
import com.jasonhhouse.gaps.GapsService;
import com.jasonhhouse.gaps.PlexSearchFormatter;
import com.jasonhhouse.gaps.service.BindingErrorsService;
import java.util.ArrayList;
import com.jasonhhouse.gaps.Movie;
import com.jasonhhouse.gaps.MoviePair;
import com.jasonhhouse.gaps.PlexLibrary;
import com.jasonhhouse.gaps.PlexQuery;
import com.jasonhhouse.gaps.service.IoService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections4.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
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.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping(value = "/plexMovieList")
@RequestMapping(value = "/plex")
public class PlexMovieListController {
private static final Logger LOGGER = LoggerFactory.getLogger(PlexMovieListController.class);
private final BindingErrorsService bindingErrorsService;
private final IoService ioService;
private final GapsService gapsService;
private final PlexQuery plexQuery;
@Autowired
public PlexMovieListController(BindingErrorsService bindingErrorsService, GapsService gapsService) {
this.bindingErrorsService = bindingErrorsService;
public PlexMovieListController(IoService ioService, GapsService gapsService, PlexQuery plexQuery) {
this.ioService = ioService;
this.gapsService = gapsService;
}
@RequestMapping(method = RequestMethod.POST,
produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView postPlexMovieList(@RequestParam ArrayList<String> selectedLibraries) {
LOGGER.info("postPlexMovieList( " + selectedLibraries + " )");
if (CollectionUtils.isEmpty(selectedLibraries)) {
return bindingErrorsService.getErrorPage();
}
gapsService.updateLibrarySelections(selectedLibraries);
ModelAndView modelAndView = new ModelAndView("plexMovieList");
LOGGER.info(gapsService.getPlexSearch().toString());
modelAndView.addObject("plexSearch", gapsService.getPlexSearch());
return modelAndView;
this.plexQuery = plexQuery;
}
@RequestMapping(method = RequestMethod.GET,
produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView getPlexMovieList() {
LOGGER.info("getPlexMovieList()");
return new ModelAndView("plexMovieList");
value = "/movies/{machineIdentifier}/{key}")
@ResponseBody
public ResponseEntity<List<Movie>> getPlexMovies(@PathVariable("machineIdentifier") final String machineIdentifier, @PathVariable("key") final Integer key) {
LOGGER.info("getPlexMovies( " + machineIdentifier + ", " + key + " )");
List<Movie> ownedMovies = ioService.readOwnedMovies(machineIdentifier, key);
if (CollectionUtils.isNotEmpty(ownedMovies)) {
ioService.writeOwnedMoviesToFile(ownedMovies, machineIdentifier, key);
return ResponseEntity.ok().body(ownedMovies);
}
Set<Movie> everyMovie = ioService.readMovieIdsFromFile();
Map<MoviePair, Movie> previousMovies = generateOwnedMovieMap(everyMovie);
String url = generatePlexUrl(machineIdentifier, key);
ownedMovies = plexQuery.findAllPlexMovies(previousMovies, url);
ioService.writeOwnedMoviesToFile(ownedMovies, machineIdentifier, key);
return ResponseEntity.ok().body(ownedMovies);
}
@InitBinder
public void initBinder(WebDataBinder binder) {
LOGGER.info("initBinder()");
binder.addCustomFormatter(new PlexSearchFormatter(), "plexSearch");
//binder.setValidator(new PlexLibrariesValidator());
private Map<MoviePair, Movie> generateOwnedMovieMap(Set<Movie> everyMovie) {
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);
});
}));
return previousMovies;
}
private String generatePlexUrl(String machineIdentifier, Integer key) {
LOGGER.info("generatePlexUrl( " + 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))
.findFirst()
.orElse("");
}
}

View File

@@ -127,8 +127,14 @@ public class GapsSearchService implements GapsSearch {
@Override
@Async
@Deprecated
public void run() {
LOGGER.info("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();
@@ -140,14 +146,6 @@ public class GapsSearchService implements GapsSearch {
cancelSearch.set(false);
try {
//ToDo
String sessionId = null;
// // Get TMDB Authorization from user,
// // requires user input so needs to be done early before user walks away
//if (StringUtils.isNotEmpty(gaps.getMovieDbListId())) {
// sessionId = getTmdbAuthorization(gaps);
//}
Map<MoviePair, Movie> previousMovies = new HashMap<>();
gapsService
@@ -164,7 +162,8 @@ public class GapsSearchService implements GapsSearch {
});
}));
findAllPlexMovies(previousMovies);
List<String> urls = generatePlexUrls(machineIdentifier, key);
findAllPlexMovies(previousMovies, urls);
StopWatch watch = new StopWatch();
watch.start();
@@ -172,11 +171,6 @@ public class GapsSearchService implements GapsSearch {
watch.stop();
LOGGER.info("Time Elapsed: " + TimeUnit.MILLISECONDS.toSeconds(watch.getTime()) + " seconds.");
LOGGER.info("Times used TVDB ID: " + tempTvdbCounter);
/*if (StringUtils.isNotEmpty(gaps.getMovieDbListId())) {
createTmdbList(sessionId);
}*/
} catch (SearchCancelledException e) {
String reason = "Search cancelled";
LOGGER.error(reason, e);
@@ -187,18 +181,9 @@ public class GapsSearchService implements GapsSearch {
}
//Always write to log
ioService.writeRecommendedToFile(recommended);
ioService.writeRecommendedToFile(recommended, machineIdentifier, key);
ioService.writeMovieIdsToFile(new TreeSet<>(everyMovie));
gapsService.getPlexSearch()
.getPlexServers()
.forEach(plexServer -> plexServer
.getPlexLibraries()
.stream()
.filter(PlexLibrary::getSelected)
.forEach(plexLibrary -> {
ioService.writeOwnedMoviesToFile(plexServer, plexLibrary.getKey(), ownedMovies);
}));
ioService.writeOwnedMoviesToFile(ownedMovies, machineIdentifier, key);
template.convertAndSend("/finishedSearching", true);
@@ -371,7 +356,7 @@ public class GapsSearchService implements GapsSearch {
* 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) throws SearchCancelledException {
private void findAllPlexMovies(Map<MoviePair, Movie> previousMovies, List<String> urls) throws SearchCancelledException {
LOGGER.info("findAllPlexMovies()");
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(180, TimeUnit.SECONDS)
@@ -379,8 +364,6 @@ public class GapsSearchService implements GapsSearch {
.readTimeout(180, TimeUnit.SECONDS)
.build();
List<String> urls = generatePlexUrls();
if (CollectionUtils.isEmpty(urls)) {
LOGGER.info("No URLs added to plexMovieUrls. Check your application.yaml file if needed.");
return;
@@ -819,7 +802,7 @@ public class GapsSearchService implements GapsSearch {
String movieDetailJson = movieDetailResponse.body() != null ? movieDetailResponse.body().string() : null;
LOGGER.debug(movieDetailJson);
LOGGER.info(movieDetailJson);
if (movieDetailJson == null) {
LOGGER.error("Body returned null from TheMovieDB for details on " + movie.getName());
@@ -849,16 +832,18 @@ public class GapsSearchService implements GapsSearch {
.setImdbId(movieDet.get("imdb_id").asText())
.setCollectionId(movie.getCollectionId())
.setCollection(movie.getCollection())
.setPosterUrl("https://image.tmdb.org/t/p/w185/" + movieDet.get("poster_path").asText())
.setOverview(movieDet.get("overview").asText())
.build();
if (recommended.add(recommendedMovie)) {
// Write current list of recommended movies to file.
ioService.writeRssFile(recommended);
LOGGER.info("/newMovieFound:" + movieFromCollection.toString());
LOGGER.info("/newMovieFound:" + recommendedMovie.toString());
//Send message over websocket
SearchResults searchResults = new SearchResults(getSearchedMovieCount(), getTotalMovieCount(), movieFromCollection);
SearchResults searchResults = new SearchResults(getSearchedMovieCount(), getTotalMovieCount(), recommendedMovie);
template.convertAndSend("/newMovieFound", objectMapper.writeValueAsString(searchResults));
}
} catch (Exception e) {
@@ -926,4 +911,20 @@ public class GapsSearchService implements GapsSearch {
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());
}
}

View File

@@ -30,6 +30,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
@@ -170,9 +171,12 @@ public class IoService {
/**
* Prints out all recommended movies to recommendedMovies.json
*/
public void writeRecommendedToFile(Set<Movie> recommended) {
public void writeRecommendedToFile(Set<Movie> recommended, String machineIdentifier, Integer key) {
LOGGER.info("writeRecommendedToFile()");
final String fileName = STORAGE_FOLDER + RECOMMENDED_MOVIES;
final String fileName = STORAGE_FOLDER + machineIdentifier + File.separator + key + File.separator + RECOMMENDED_MOVIES;
makeFolder(machineIdentifier, key);
File file = new File(fileName);
writeMovieIdsToFile(recommended, file);
}
@@ -180,17 +184,35 @@ public class IoService {
/**
* Prints out all recommended movies to recommendedMovies.json
*/
public void writeOwnedMoviesToFile(PlexServer plexServer, int key, Set<Movie> ownedMovies) {
public void writeOwnedMoviesToFile(Set<Movie> ownedMovies, String machineIdentifier, int key) {
LOGGER.info("writeOwnedMoviesToFile()");
final String fileName = STORAGE_FOLDER + plexServer.getMachineIdentifier() + File.separator + key + File.separator + OWNED_MOVIES;
final String fileName = STORAGE_FOLDER + machineIdentifier + File.separator + key + File.separator + OWNED_MOVIES;
File folder = new File(STORAGE_FOLDER + plexServer.getMachineIdentifier() + File.separator + key);
makeFolder(machineIdentifier, key);
File file = new File(fileName);
writeMovieIdsToFile(ownedMovies, file);
}
/**
* Prints out all recommended movies to recommendedMovies.json
*/
public void writeOwnedMoviesToFile(List<Movie> ownedMovies, String machineIdentifier, int key) {
LOGGER.info("writeOwnedMoviesToFile()");
final String fileName = STORAGE_FOLDER + machineIdentifier + File.separator + key + File.separator + OWNED_MOVIES;
makeFolder(machineIdentifier, key);
File file = new File(fileName);
writeMovieIdsToFile( new HashSet<>(ownedMovies), file);
}
private void makeFolder(String machineIdentifier, int key) {
File folder = new File(STORAGE_FOLDER + machineIdentifier + File.separator + key);
if (!folder.exists()) {
folder.mkdirs();
}
File file = new File(fileName);
writeMovieIdsToFile(ownedMovies, file);
}
public boolean doOwnedMoviesFilesExist(List<PlexServer> plexServers) {

View File

@@ -10,15 +10,22 @@
package com.jasonhhouse.gaps.service;
import com.jasonhhouse.gaps.Movie;
import com.jasonhhouse.gaps.MoviePair;
import com.jasonhhouse.gaps.PlexLibrary;
import com.jasonhhouse.gaps.PlexQuery;
import com.jasonhhouse.gaps.PlexServer;
import com.jasonhhouse.gaps.UrlGenerator;
import com.sun.org.apache.xml.internal.dtm.ref.DTMNodeList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
@@ -34,6 +41,8 @@ import org.apache.commons.lang3.StringUtils;
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.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
@@ -46,8 +55,19 @@ import org.xml.sax.SAXException;
@Service
public class PlexQueryImpl implements PlexQuery {
public static final String ID_IDX_START = "://";
public static final String ID_IDX_END = "?";
private static final Logger LOGGER = LoggerFactory.getLogger(PlexQueryImpl.class);
private final UrlGenerator urlGenerator;
@Autowired
public PlexQueryImpl(@Qualifier("real") UrlGenerator urlGenerator) {
this.urlGenerator = urlGenerator;
}
@Override
public void getLibraries(@NotNull PlexServer plexServer) {
LOGGER.info("queryPlexLibraries()");
@@ -208,6 +228,143 @@ public class PlexQueryImpl implements PlexQuery {
}
}
@Override
public List<Movie> findAllPlexMovies(Map<MoviePair, Movie> previousMovies, @NotNull String url) {
LOGGER.info("findAllPlexMovies()");
List<Movie> ownedMovies = new ArrayList<>();
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(180, TimeUnit.SECONDS)
.writeTimeout(180, TimeUnit.SECONDS)
.readTimeout(180, TimeUnit.SECONDS)
.build();
if (StringUtils.isEmpty(url)) {
LOGGER.info("No URL added to findAllPlexMovies().");
return ownedMovies;
}
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);
return ownedMovies;
}
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();
}
String summary = "";
if (node.getAttributes().getNamedItem("summary") != null) {
summary = node.getAttributes().getNamedItem("summary").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, summary);
} 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, summary);
} else {
LOGGER.warn("Cannot handle guid value of " + guid);
movie = getOrCreateOwnedMovie(previousMovies, title, year, thumbnail, -1, null, null, -1, null, summary);
}
ownedMovies.add(movie);
}
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);
}
return ownedMovies;
}
private Movie getOrCreateOwnedMovie(Map<MoviePair, Movie> previousMovies, String title, int year, String thumbnail, int tvdbId, String imdbId, String language, int collection, String collectionName, String summary) {
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)
.setOverview(summary)
.build();
}
}
private <T> T parseXml(Response response, HttpUrl url, String expression) throws XPathExpressionException, IOException, SAXException, ParserConfigurationException {
String body = response.body() != null ? response.body().string() : null;

View File

@@ -8,66 +8,50 @@
* 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;
let libraryTitle, noMovieContainer, movieContainer;
let plexServers;
let plexServer;
let moviesTable;
let key;
let libraryKey;
jQuery(function ($) {
//const plexSearch = JSON.parse($('#plexSearch').val());
Handlebars.registerHelper('json', function (context) {
return JSON.stringify(context);
});
libraryTitle = $('#libraryTitle');
noMovieContainer = $('#noMovieContainer');
movieContainer = $('#movieContainer');
plexServers = JSON.parse($('#plexServers').val());
plexServer = JSON.parse($('#plexServer').val());
key = $('#key').val();
libraryKey = $('#libraryKey').val();
moviesTable = $('#movies').DataTable({
"initComplete": function (settings, json) {
$.ajax({
type: "GET",
url: `/libraries/${plexServer.machineIdentifier}/${key}`,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
//need to check result for valid output or not searched yet
console.log(JSON.parse(result.movies));
moviesTable.rows.add(JSON.parse(result.movies)).draw();
}, error: function () {
//Show error
moviesTable.rows().invalidate().draw();
}
});
initComplete: function () {
getMoviesForTable(`/libraries/${plexServer.machineIdentifier}/${libraryKey}`);
},
ordering: false,
deferRender: true,
search: true,
columns: [
{
data: "card",
render: function (data, type, row) {
if (type === 'display') {
const obj = {
name: row.name,
year: row.year,
collection: row.collection,
poster_url: row.poster_url,
address: plexServer.address,
port: plexServer.port,
plexToken: plexServer.plexToken
};
row.address = plexServer.address;
row.port = plexServer.port;
row.plexToken = plexServer.plexToken;
const plexServerCard = $("#movieCard").html();
const theTemplate = Handlebars.compile(plexServerCard);
return theTemplate(obj);
return theTemplate(row);
}
return "";
}
},
{
data: "title",
searchable: true,
visible: false,
render: function (data, type, row) {
if (type === 'display' && row.name) {
@@ -78,6 +62,7 @@ jQuery(function ($) {
},
{
data: "year",
searchable: true,
visible: false,
render: function (data, type, row) {
if (type === 'display' && row.year) {
@@ -88,6 +73,7 @@ jQuery(function ($) {
},
{
data: "language",
searchable: true,
visible: false,
render: function (data, type, row) {
if (type === 'display' && row.language) {
@@ -97,30 +83,23 @@ 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);
}
]
});
});
function switchPlexLibrary(machineIdentifier, key) {
window.key = key;
libraryKey = key;
plexServer = plexServers[machineIdentifier];
const plexLibrary = plexServer.plexLibraries.find(plexServer => plexServer.key === parseInt(key));
libraryTitle.text(`${plexServer.friendlyName} - ${plexLibrary.title}`);
@@ -128,18 +107,42 @@ function switchPlexLibrary(machineIdentifier, key) {
moviesTable.data().clear();
moviesTable.rows().invalidate().draw();
getMoviesForTable(`/libraries/${machineIdentifier}/${libraryKey}`);
}
function getMoviesForTable(url) {
$.ajax({
type: "GET",
url: `/libraries/${machineIdentifier}/${key}`,
url: url,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
//need to check result for valid output or not searched yet
console.log(JSON.parse(result.movies));
moviesTable.rows.add(JSON.parse(result.movies)).draw();
if (result.success) {
movieContainer.show(100);
noMovieContainer.css({'display': 'none'});
moviesTable.rows.add(JSON.parse(result.movies)).draw();
} else {
movieContainer.css({'display': 'none'});
noMovieContainer.show(100);
}
}, error: function () {
//Show error
moviesTable.rows().invalidate().draw();
movieContainer.css({'display': 'none'});
noMovieContainer.show(100);
//Show error + error
}
});
}
function searchForMovies() {
movieContainer.show(100);
noMovieContainer.css({'display': 'none'});
$.ajax({
type: "GET",
url: `/plex/movies/${plexServer.machineIdentifier}/${libraryKey}`,
contentType: "application/json",
success: function (data) {
moviesTable.rows.add(data).draw();
}
});
}

View File

@@ -10,4 +10,271 @@
$(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 plexServers;
let plexServer;
let moviesTable;
let libraryKey;
let stompClient;
let backButton;
let copyToClipboard;
let searchResults;
let progressContainer;
let searchTitle;
let searchDescription;
let movieCounter;
jQuery(function ($) {
Handlebars.registerHelper('json', function (context) {
return JSON.stringify(context);
});
libraryTitle = $('#libraryTitle');
noMovieContainer = $('#noMovieContainer');
movieContainer = $('#movieContainer');
plexServers = JSON.parse($('#plexServers').val());
plexServer = JSON.parse($('#plexServer').val());
libraryKey = $('#libraryKey').val();
backButton = $('#cancel');
copyToClipboard = $('#copyToClipboard');
searchResults = [];
movieSearchContainer = $('#movieSearchContainer');
progressContainer = $('#progressContainer');
searchTitle = $('#searchTitle');
searchDescription = $('#searchDescription');
moviesTable = $('#movies').DataTable({
"initComplete": function (settings, json) {
getMoviesForTable(`/libraries/${plexServer.machineIdentifier}/${libraryKey}`);
},
ordering: false,
columns: [
{
data: "card",
render: function (data, type, row) {
if (type === 'display') {
const plexServerCard = $("#movieCard").html();
const theTemplate = Handlebars.compile(plexServerCard);
return theTemplate(row);
}
return "";
}
},
{
data: "title",
visible: false,
render: function (data, type, row) {
if (type === 'display' && row.name) {
return row.name;
}
return "";
}
},
{
data: "year",
visible: false,
render: function (data, type, row) {
if (type === 'display' && row.year) {
return row.year;
}
return "";
}
},
{
data: "language",
visible: false,
render: function (data, type, row) {
if (type === 'display' && row.language) {
return row.language;
}
return "";
}
},
{
data: "collection",
visible: false,
render: function (data, type, row) {
if (type === 'display' && row.collection) {
return row.collection;
}
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);
}
});
const socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function () {
stompClient.subscribe('/finishedSearching', function (successful) {
progressContainer.hide();
backButton.text('Restart');
disconnect();
if (successful) {
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.");
setCopyToClipboardEnabled(false);
}
});
stompClient.subscribe('/newMovieFound', function (status) {
const obj = JSON.parse(status.body);
showSearchStatus(obj);
function buildUrl(nextMovie) {
if (nextMovie.tvdbId) {
return `https://www.themoviedb.org/movie/${nextMovie.tvdbId}`;
}
if (nextMovie.imdbId) {
return `https://www.imdb.com/title/${nextMovie.imdbId}/`
}
return undefined;
}
if (obj.nextMovie) {
movieCounter++;
moviesTable.row.add(obj.nextMovie).draw();
searchResults.push(`${obj.nextMovie.name} (${obj.nextMovie.year}) in collection '${obj.nextMovie.collection}'`)
}
});
});
});
function switchPlexLibrary(machineIdentifier, key) {
libraryKey = key;
plexServer = plexServers[machineIdentifier];
const plexLibrary = plexServer.plexLibraries.find(plexServer => plexServer.key === parseInt(key));
libraryTitle.text(`${plexServer.friendlyName} - ${plexLibrary.title}`);
moviesTable.data().clear();
moviesTable.rows().invalidate().draw();
getMoviesForTable(`/libraries/${machineIdentifier}/${libraryKey}`);
}
function getMoviesForTable(url) {
$.ajax({
type: "GET",
url: url,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (result) {
if (result.success) {
movieContainer.show(100);
noMovieContainer.css({'display':'none'});
moviesTable.rows.add(JSON.parse(result.movies)).draw();
} else {
movieContainer.css({'display':'none'});
noMovieContainer.show(100);
}
}, error: function () {
movieContainer.css({'display':'none'});
noMovieContainer.show(100);
//Show error + error
}
});
}
function cancel() {
stompClient.send("/cancelSearching");
//Navigate Home
location.assign("/");
}
function viewRss() {
location.assign("rssCheck");
}
window.onbeforeunload = function () {
disconnect();
};
function setCopyToClipboardEnabled(bool) {
if (bool) {
copyToClipboard.removeClass('disabled');
} else {
copyToClipboard.addClass('disabled');
}
}
function searchForMovies() {
movieContainer.show(100);
movieSearchContainer.show(100);
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.");
$.ajax({
type: "PUT",
url: `/search/start/${plexServer.machineIdentifier}/${libraryKey}`,
contentType: "application/json"
});
showSearchStatus();
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
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.`);
}
}
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');
}

View File

@@ -56,11 +56,6 @@
<div class="dropdown-menu" aria-labelledby="dropdownMenuLink">
<div th:each="instance : ${plexServers}" th:remove="tag">
<!--<a th:onclick="${'switchPlexLibrary(' + instance.value.machineIdentifier + ','+ plexLibrary.key + ');'}"
class="dropdown-item" href="javascript:void(0)"
th:each="plexLibrary : *{instance.value.plexLibraries}"
th:text="(*{instance.value.friendlyName} + ' - ' + *{plexLibrary.title})"></a>-->
<a href="javascript:void(0)"
th:data-machineIdentifier="*{instance.value.machineIdentifier}"
th:data-key="*{plexLibrary.key}"
@@ -73,18 +68,18 @@
</div>
</div>
<div class="top-margin">
<div id="noMovieContainer" class="top-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)" class="btn btn-primary">Search</a>
<a href="javascript:void(0)" onclick="searchForMovies()" class="btn btn-primary">Search</a>
</div>
</div>
</div>
<div class="top-margin">
<div id="movieContainer" class="top-margin" style="display: none;">
<table id="movies" class="display" style="width:100%">
<thead>
<tr>
@@ -92,7 +87,7 @@
<th hidden>Title</th>
<th hidden>Year</th>
<th hidden>Language</th>
<th hidden>Collection</th>
<th hidden>Summary</th>
</tr>
</thead>
</table>
@@ -106,22 +101,15 @@
<div class="card mb-3">
<div class="row no-gutters">
<div class="col-md-2">
<img style="height: 100%; width: 150px; display: block;"
<img style="height: 225px; width: 150px; display: block;"
src="http://{{address}}:{{port}}{{poster_url}}/?X-Plex-Token={{plexToken}}"
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">{{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>
<p class="card-text text-muted">{{overview}}</p>
<p class="card-text"><small class="text-info">English</small></p>
</div>
</div>
</div>
@@ -131,13 +119,11 @@
<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="key" name="key" th:value="${{plexLibrary.key}}" 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/libraries.js"></script>
</body>

View File

@@ -67,23 +67,4 @@ class GapsSearchServiceTest {
assertEquals(0, gapsSearch.getRecommendedMovies().size(), "Should be zero by default");
}
@Test
void emptyGapsProperty() {
Assertions.assertThrows(ResponseStatusException.class, () -> {
gapsSearch.run();
}, "Should throw exception when not searching from folder and Plex");
}
@Test
void noBodyMovieXmlFromPlex() {
gapsUrlGeneratorTest.generatePlexUrl(GapsUrlGeneratorTest.PLEX_EMPTY_URL);
Assertions.assertThrows(ResponseStatusException.class, () -> gapsSearch.run(), "Should throw exception that the body was empty");
}
@Test
void emptyMovieXmlFromPlex() {
gapsUrlGeneratorTest.generatePlexUrl(GapsUrlGeneratorTest.EMPTY_MOVIE_PLEX_URL);
Assertions.assertThrows(ResponseStatusException.class, () -> gapsSearch.run(), "Should throw exception that the title was missing from the video element");
}
}