Cleaned up docker file

Fixed atomic integer counting
This commit is contained in:
Jason House
2019-05-21 23:06:21 -04:00
parent b11f8f6330
commit 8257e266ec
13 changed files with 608 additions and 1168 deletions
+4 -10
View File
@@ -1,14 +1,8 @@
FROM maven:3.6.0-jdk-11-slim
FROM openjdk:11.0.3-jre-slim
RUN mkdir -p /usr/src/app/src
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY startup.sh mvnw mvnw.cmd pom.xml /usr/src/app/
COPY target/Gaps-0.0.4.jar /usr/src/app/
COPY src /usr/src/app/src/
RUN chmod +x /usr/src/app/startup.sh
RUN mvn clean install -Dassembly.skipAssembly=true
CMD "/usr/src/app/startup.sh"
ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "Gaps-0.0.4.jar"]
Vendored
-286
View File
@@ -1,286 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Maven2 Start Up Batch script
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# M2_HOME - location of maven2's installed home dir
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "`uname`" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
export JAVA_HOME="`/usr/libexec/java_home`"
else
export JAVA_HOME="/Library/Java/Home"
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=`java-config --jre-home`
fi
fi
if [ -z "$M2_HOME" ] ; then
## resolve links - $0 may be a link to maven's home
PRG="$0"
# need this for relative symlinks
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG="`dirname "$PRG"`/$link"
fi
done
saveddir=`pwd`
M2_HOME=`dirname "$PRG"`/..
# make it fully qualified
M2_HOME=`cd "$M2_HOME" && pwd`
cd "$saveddir"
# echo Using m2 at $M2_HOME
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --unix "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$M2_HOME" ] &&
M2_HOME="`(cd "$M2_HOME"; pwd)`"
[ -n "$JAVA_HOME" ] &&
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
# TODO classpath?
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="`which javac`"
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=`which readlink`
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
if $darwin ; then
javaHome="`dirname \"$javaExecutable\"`"
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
else
javaExecutable="`readlink -f \"$javaExecutable\"`"
fi
javaHome="`dirname \"$javaExecutable\"`"
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
esac
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
if [ "$MVNW_VERBOSE" = true ]; then
echo "Downloading from: $jarUrl"
fi
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
if command -v wget > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found wget ... using wget"
fi
wget "$jarUrl" -O "$wrapperJarPath"
elif command -v curl > /dev/null; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found curl ... using curl"
fi
curl -o "$wrapperJarPath" "$jarUrl"
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download"
fi
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
if [ -e "$javaClass" ]; then
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Compiling MavenWrapperDownloader.java ..."
fi
# Compiling the Java class
("$JAVA_HOME/bin/javac" "$javaClass")
fi
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
# Running the downloader
if [ "$MVNW_VERBOSE" = true ]; then
echo " - Running MavenWrapperDownloader.java ..."
fi
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
if [ "$MVNW_VERBOSE" = true ]; then
echo $MAVEN_PROJECTBASEDIR
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$M2_HOME" ] &&
M2_HOME=`cygpath --path --windows "$M2_HOME"`
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
[ -n "$CLASSPATH" ] &&
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
fi
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
exec "$JAVACMD" \
$MAVEN_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
Vendored
-161
View File
@@ -1,161 +0,0 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
echo Found %WRAPPER_JAR%
) else (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %DOWNLOAD_URL%
powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
echo Finished downloading %WRAPPER_JAR%
)
@REM End of extension
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%
+1 -1
View File
@@ -16,6 +16,7 @@
<properties>
<java.version>1.8</java.version>
<start-class>com.jasonhhouse.Gaps.GapsApplication</start-class>
</properties>
<dependencies>
@@ -43,7 +44,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
-2
View File
@@ -1,2 +0,0 @@
java -jar Gaps-0.0.3.jar
PAUSE
-3
View File
@@ -1,3 +0,0 @@
#!/usr/bin/env bash
java -jar Gaps-0.0.4.jar
pause
-20
View File
@@ -12,18 +12,6 @@
<source>${project.build.directory}/${project.artifactId}-${project.version}.jar</source>
<outputDirectory></outputDirectory>
</file>
<file>
<source>${project.basedir}/run.sh</source>
<outputDirectory></outputDirectory>
</file>
<file>
<source>${project.basedir}/run.bat</source>
<outputDirectory></outputDirectory>
</file>
<file>
<source>${project.basedir}/startup.sh</source>
<outputDirectory></outputDirectory>
</file>
<file>
<source>${project.basedir}/Dockerfile</source>
<outputDirectory></outputDirectory>
@@ -32,14 +20,6 @@
<source>${project.basedir}/LICENSE</source>
<outputDirectory></outputDirectory>
</file>
<file>
<source>${project.basedir}/mvnw</source>
<outputDirectory></outputDirectory>
</file>
<file>
<source>${project.basedir}/mvnw.cmd</source>
<outputDirectory></outputDirectory>
</file>
<file>
<source>${project.basedir}/README.md</source>
<outputDirectory></outputDirectory>
@@ -10,16 +10,25 @@
package com.jasonhhouse.Gaps;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* Search for all missing movies in your plex collection by MovieDB collection.
*/
@SpringBootApplication
public class GapsApplication implements CommandLineRunner {
@EnableAsync
public class GapsApplication implements CommandLineRunner, AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return Executors.newSingleThreadExecutor();
}
public static void main(String[] args) {
SpringApplication.run(GapsApplication.class, args);
@@ -1,16 +1,17 @@
package com.jasonhhouse.Gaps;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.json.JSONObject;
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.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.async.DeferredResult;
@@ -19,6 +20,14 @@ public class GapsController {
private final Logger logger = LoggerFactory.getLogger(GapsController.class);
private final GapsSearch gapsSearch;
@Autowired
GapsController(GapsSearch gapsSearch) {
this.gapsSearch = gapsSearch;
}
@RequestMapping(value = "submit", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public DeferredResult<ResponseEntity<Set<Movie>>> submit(@RequestBody Gaps gaps) {
@@ -29,6 +38,15 @@ public class GapsController {
return deferredResult;
}
@RequestMapping(value = "/status", method = RequestMethod.GET)
@ResponseBody
ResponseEntity<String> fetchStatus() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("searchedMovieCount", gapsSearch.getSearchedMovieCount());
jsonObject.put("totalMovieCount", gapsSearch.getTotalMovieCount());
return new ResponseEntity<>(jsonObject.toString(), HttpStatus.OK);
}
private void runInOtherThread(Gaps gaps, DeferredResult<ResponseEntity<Set<Movie>>> deferredResult) {
Properties properties = new Properties();
properties.setMovieDbApiKey(gaps.getMovieDbApiKey());
@@ -46,8 +64,7 @@ public class GapsController {
folderProperties.setSearchFromFolder(false);
properties.setFolder(folderProperties);
GapsSearch gapsSearch = new GapsSearch(properties);
Set<Movie> recommendMovies = gapsSearch.run();
Set<Movie> recommendMovies = gapsSearch.run(properties);
ResponseEntity<Set<Movie>> responseEntity = new ResponseEntity<>(recommendMovies, HttpStatus.OK);
deferredResult.setResult(responseEntity);
@@ -1,549 +1,13 @@
package com.jasonhhouse.Gaps;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
public class GapsSearch{
public interface GapsSearch {
private final Logger logger = LoggerFactory.getLogger(GapsSearch.class);
Set<Movie> run(Properties properties);
private final Properties properties;
Integer getTotalMovieCount();
private final Set<Movie> searched;
private final Set<Movie> recommended;
private final Set<Movie> ownedMovies;
public GapsSearch(Properties properties) {
this.properties = properties;
this.ownedMovies = new HashSet<>();
this.searched = new HashSet<>();
this.recommended = new TreeSet<>();
}
public Set<Movie> run() {
String sessionId = null;
// Get TMDB Authorizatoin from user,
// requires user input so needs to be done early before user walks away
if (StringUtils.isNotEmpty(properties.getMovieDbListId())) {
sessionId = getTmdbAuthorization();
}
if (properties.getPlex().getSearchFromPlex()) {
findAllPlexMovies();
}
if (properties.getFolder().getSearchFromFolder()) {
findAllFolderMovies();
}
searchForMovies();
if (properties.getWriteToFile()) {
writeToFile();
}
//Always write to command line
printRecommended();
if (StringUtils.isNotEmpty(properties.getMovieDbListId())) {
createTmdbList(sessionId);
}
return recommended;
}
private void findAllFolderMovies() {
if (CollectionUtils.isEmpty(properties.getFolder().getFolders())) {
logger.error("folders property cannot be empty when searchFromFolder is true");
return;
}
if (CollectionUtils.isEmpty(properties.getFolder().getMovieFormats())) {
logger.error("movie formats property cannot be empty when searchFromFolder is true");
return;
}
for (String strFolder : properties.getFolder().getFolders()) {
File folder = new File(strFolder);
searchFolders(folder);
}
}
private void searchFolders(File folder) {
if (!folder.exists()) {
logger.warn("Folder in folders property does not exist: " + folder);
return;
}
if (!folder.isDirectory()) {
logger.warn("Folder in folders property is not a directory: " + folder);
return;
}
File[] files = folder.listFiles();
if (files == null) {
logger.warn("Folder in folders property is empty: " + folder);
return;
}
for (File file : files) {
if (file.isDirectory() && properties.getFolder().getRecursive()) {
searchFolders(file);
continue;
}
String extension = FilenameUtils.getExtension(file.toString());
if (properties.getFolder().getMovieFormats().contains(extension)) {
String fullMovie = FilenameUtils.getBaseName(file.toString());
Pattern pattern = Pattern.compile(properties.getFolder().getYearRegex());
Matcher matcher = pattern.matcher(fullMovie);
if (!matcher.find()) {
logger.warn("No regex matches found for " + fullMovie);
continue;
}
String year = matcher.group(matcher.groupCount()).replaceAll("[)(]", "");
String title = fullMovie.substring(0, fullMovie.indexOf(" ("));
Movie movie = new Movie(-1, title, Integer.parseInt(year), "");
ownedMovies.add(movie);
} else {
logger.warn("Skipping file " + file);
}
}
}
/**
* 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();
MediaType mediaType = MediaType.parse("application/octet-stream");
RequestBody.create(mediaType, "{}");
RequestBody body;
Request request = new Request.Builder()
.url("https://api.themoviedb.org/3/authentication/token/new?api_key=" + properties.getMovieDbApiKey())
.get()
.build();
String request_token;
try {
Response response = client.newCall(request).execute();
JSONObject responseJson = new JSONObject(response.body().string());
request_token = responseJson.getString("request_token");
// 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;
}
// Create the sesssion ID for MovieDB using the approved token
mediaType = MediaType.parse("application/json");
body = RequestBody.create(mediaType, "{\"request_token\":\"" + request_token + "\"}");
request = new Request.Builder()
.url("https://api.themoviedb.org/3/authentication/session/new?api_key=" + properties.getMovieDbApiKey())
.post(body)
.addHeader("content-type", "application/json")
.build();
Response response = null;
try {
response = client.newCall(request).execute();
JSONObject sessionResponse = new JSONObject(response.body().string());
return sessionResponse.getString("session_id"); // 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
*/
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(mediaType, "{\"media_id\":" + m.getMedia_id() + "}");
String url = "https://api.themoviedb.org/3/list/" + properties.getMovieDbListId()
+ "/add_item?session_id=" + sessionId + "&api_key=" + properties.getMovieDbApiKey();
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/" + properties.getMovieDbListId());
}
/**
* Connect to plex via the URL and parse all of the movies from the returned XML creating a HashSet of movies the
* user has.
*/
private void findAllPlexMovies() {
logger.info("Searching for Plex Movies...");
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(properties.getPlex().getConnectTimeout(), TimeUnit.SECONDS)
.writeTimeout(properties.getPlex().getWriteTimeout(), TimeUnit.SECONDS)
.readTimeout(properties.getPlex().getReadTimeout(), TimeUnit.SECONDS)
.build();
List<String> urls = properties.getPlex().getMovieUrls();
if (CollectionUtils.isEmpty(urls)) {
logger.info("No URLs added to plexMovieUrls. Check your application.yaml file if needed.");
return;
}
for (String url : urls) {
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
String body = response.body() != null ? response.body().string() : null;
if (body == null) {
logger.error("Body returned null from Plex");
return;
}
InputStream fileIS = new ByteArrayInputStream(body.getBytes());
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document xmlDocument = builder.parse(fileIS);
XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "/MediaContainer/Video";
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
for (int i = 0; i < nodeList.getLength() && i < 5; i++) {
Node node = nodeList.item(i);
//Files can't have : so need to remove to find matches correctly
String title = node.getAttributes().getNamedItem("title").getNodeValue().replaceAll(":", "");
if (node.getAttributes().getNamedItem("year") == null) {
logger.warn("Year not found for " + title);
continue;
}
String year = node.getAttributes().getNamedItem("year").getNodeValue();
Movie movie = new Movie(-1, title, Integer.parseInt(year), "");
ownedMovies.add(movie);
}
logger.info(ownedMovies.size() + " movies found in plex");
} catch (IOException e) {
logger.error("Error connecting to Plex to get Movie list", e);
} catch (ParserConfigurationException | XPathExpressionException | SAXException e) {
logger.error("Error parsing XML from Plex", e);
}
}
}
/**
* 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
* full properties that contains the collection id, we can search that collection id for it's list of movies. We
* compare the full collection list to the movies found in plex, any missing we add to the recommended list. To
* 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() {
logger.info("Searching for Movie Collections...");
OkHttpClient client = new OkHttpClient();
if (StringUtils.isEmpty(properties.getMovieDbApiKey())) {
logger.error("No MovieDb Key added to movieDbApiKey. Check your application.yaml file.");
return;
}
int count = 0;
for (Movie movie : ownedMovies) {
//Print the count first to handle the continue if block or the regular searching case
if (count % 10 == 0) {
logger.info(((int) ((count) / ((double) (ownedMovies.size())) * 100)) + "% Complete. Processed " + count + " files of " + ownedMovies.size() + ". ");
}
count++;
if (searched.contains(movie)) {
continue;
}
String searchMovieUrl;
try {
searchMovieUrl = "https://api.themoviedb.org/3/search/movie?api_key=" +
properties.getMovieDbApiKey() +
"&language=en-US&page=1&include_adult=false&query=" +
URLEncoder.encode(movie.getName(), "UTF-8") +
"&year=" +
movie.getYear();
Request request = new Request.Builder()
.url(searchMovieUrl)
.build();
String json;
try (Response response = client.newCall(request).execute()) {
json = response.body() != null ? response.body().string() : null;
if (json == null) {
logger.error("Body returned null from TheMovieDB for " + movie.getName());
continue;
}
JSONObject foundMovies = new JSONObject(json);
JSONArray results = foundMovies.getJSONArray("results");
if (results.length() == 0) {
logger.error("Results not found for " + movie);
logger.error("URL: " + searchMovieUrl);
continue;
}
if (results.length() > 1) {
logger.debug("Results for " + movie + " came back with " + results.length() + " results. Using first result.");
logger.debug(movie + " URL: " + searchMovieUrl);
}
JSONObject result = results.getJSONObject(0);
int id = result.getInt("id");
String movieDetailUrl = "https://api.themoviedb.org/3/movie/" + id + "?api_key=" + properties.getMovieDbApiKey() + "&language=en-US";
request = new Request.Builder()
.url(movieDetailUrl)
.build();
try (Response movieDetailResponse = client.newCall(request).execute()) {
String movieDetailJson = movieDetailResponse.body() != null ? movieDetailResponse.body().string() : null;
if (movieDetailJson == null) {
logger.error("Body returned null from TheMovieDB for details on " + movie.getName());
continue;
}
JSONObject movieDetails = new JSONObject(movieDetailJson);
if (!movieDetails.has("belongs_to_collection") || movieDetails.isNull("belongs_to_collection")) {
//No collection found, just add movie to searched and continue
searched.add(movie);
continue;
}
int collectionId = movieDetails.getJSONObject("belongs_to_collection").getInt("id");
String collectionName = movieDetails.getJSONObject("belongs_to_collection").getString("name");
String collectionUrl = "https://api.themoviedb.org/3/collection/" + collectionId + "?api_key=" + properties.getMovieDbApiKey() + "&language=en-US";
request = new Request.Builder()
.url(collectionUrl)
.build();
try (Response collectionResponse = client.newCall(request).execute()) {
String collectionJson = collectionResponse.body() != null ? collectionResponse.body().string() : null;
if (collectionJson == null) {
logger.error("Body returned null from TheMovieDB for collection information about " + movie.getName());
continue;
}
JSONObject collection = new JSONObject(collectionJson);
JSONArray parts = collection.getJSONArray("parts");
for (int i = 0; i < parts.length(); i++) {
JSONObject part = parts.getJSONObject(i);
int media_id = part.getInt("id");
//Files can't have : so need to remove to find matches correctly
String title = part.getString("original_title").replaceAll(":", "");
int year;
try {
year = Integer.parseInt(part.getString("release_date").substring(0, 4));
} catch (StringIndexOutOfBoundsException | NumberFormatException e) {
logger.warn("No year found for " + title + ". Value returned was '" + part.getString("release_date") + "'. Not adding the movie to recommended list.");
continue;
}
Movie movieFromCollection = new Movie(media_id, title, year, collectionName);
if (ownedMovies.contains(movieFromCollection)) {
searched.add(movieFromCollection);
} else if (!searched.contains(movieFromCollection) && year != 0 && year < 2019) {
recommended.add(movieFromCollection);
}
}
searched.add(movie);
} catch (IOException e) {
logger.error("Error getting collections " + movie, e);
}
} catch (IOException e) {
logger.error("Error getting movie details " + movie, e);
}
} catch (IOException e) {
logger.error("Error searching for movie " + movie, e);
logger.error("URL: " + searchMovieUrl);
} catch (JSONException e) {
logger.error("Error parsing movie " + movie + ". " + e.getMessage());
logger.error("URL: " + searchMovieUrl);
} finally {
try {
//can't have too many connections to the movie database in a specific time, have to wait
Thread.sleep(900);
} catch (InterruptedException e) {
logger.error("Error sleeping", e);
}
}
} catch (UnsupportedEncodingException e) {
logger.error("Error parsing movie URL " + movie, e);
}
}
}
/**
* Prints out all recommended files to the terminal or command line
*/
private void printRecommended() {
System.out.println(recommended.size() + " Recommended Movies");
for (Movie movie : recommended) {
System.out.println(movie.toString());
}
}
/**
* Prints out all recommended files to a text file called gaps_recommended_movies.txt
*/
private void writeToFile() {
File file = new File("gaps_recommended_movies.txt");
if (file.exists()) {
boolean deleted = file.delete();
if (!deleted) {
logger.error("Can't delete existing file gaps_recommended_movies.txt");
return;
}
}
try {
boolean created = file.createNewFile();
if (!created) {
logger.error("Can't create file gaps_recommended_movies.txt");
return;
}
} catch (IOException e) {
logger.error("Can't create file gaps_recommended_movies.txt", e);
return;
}
try (FileOutputStream outputStream = new FileOutputStream("gaps_recommended_movies.txt")) {
for (Movie movie : recommended) {
String output = movie.toString() + System.lineSeparator();
outputStream.write(output.getBytes());
}
} catch (FileNotFoundException e) {
logger.error("Can't find file gaps_recommended_movies.txt", e);
} catch (IOException e) {
logger.error("Can't write to file gaps_recommended_movies.txt", e);
}
}
public class UserInputThreadCountdown implements java.lang.Runnable {
int time_limit = 60;
Date start;
@Override
public void run() {
start = new Date();
try {
this.runTimer();
} catch (IOException e) {
e.printStackTrace();
}
}
public void runTimer() throws IOException {
long timePassedstart = 0;
do {
timePassedstart = (new Date().getTime() - start.getTime()) / 1000;
} while (timePassedstart < time_limit);
System.in.close();
}
}
Integer getSearchedMovieCount();
}
@@ -0,0 +1,567 @@
package com.jasonhhouse.Gaps;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
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.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
@Service
public class GapsSearchBean implements GapsSearch {
private final Logger logger = LoggerFactory.getLogger(GapsSearchBean.class);
private final Set<Movie> searched;
private final Set<Movie> recommended;
private final Set<Movie> ownedMovies;
private final AtomicInteger totalMovieCount;
private final AtomicInteger searchedMovieCount;
private Properties properties;
public GapsSearchBean() {
this.ownedMovies = new HashSet<>();
this.searched = new HashSet<>();
this.recommended = new TreeSet<>();
totalMovieCount = new AtomicInteger(0);
searchedMovieCount = new AtomicInteger(0);
}
@Async
@Override
public Set<Movie> run(Properties properties) {
this.properties = properties;
String sessionId = null;
// Get TMDB Authorizatoin from user,
// requires user input so needs to be done early before user walks away
if (StringUtils.isNotEmpty(properties.getMovieDbListId())) {
sessionId = getTmdbAuthorization();
}
if (properties.getPlex().getSearchFromPlex()) {
findAllPlexMovies();
}
if (properties.getFolder().getSearchFromFolder()) {
findAllFolderMovies();
}
searchForMovies();
if (properties.getWriteToFile()) {
writeToFile();
}
//Always write to command line
printRecommended();
if (StringUtils.isNotEmpty(properties.getMovieDbListId())) {
createTmdbList(sessionId);
}
return recommended;
}
@Override
public Integer getTotalMovieCount() {
return totalMovieCount.get();
}
@Override
public Integer getSearchedMovieCount() {
return searchedMovieCount.get();
}
private void findAllFolderMovies() {
if (CollectionUtils.isEmpty(properties.getFolder().getFolders())) {
logger.error("folders property cannot be empty when searchFromFolder is true");
return;
}
if (CollectionUtils.isEmpty(properties.getFolder().getMovieFormats())) {
logger.error("movie formats property cannot be empty when searchFromFolder is true");
return;
}
for (String strFolder : properties.getFolder().getFolders()) {
File folder = new File(strFolder);
searchFolders(folder);
}
}
private void searchFolders(File folder) {
if (!folder.exists()) {
logger.warn("Folder in folders property does not exist: " + folder);
return;
}
if (!folder.isDirectory()) {
logger.warn("Folder in folders property is not a directory: " + folder);
return;
}
File[] files = folder.listFiles();
if (files == null) {
logger.warn("Folder in folders property is empty: " + folder);
return;
}
for (File file : files) {
if (file.isDirectory() && properties.getFolder().getRecursive()) {
searchFolders(file);
continue;
}
String extension = FilenameUtils.getExtension(file.toString());
if (properties.getFolder().getMovieFormats().contains(extension)) {
String fullMovie = FilenameUtils.getBaseName(file.toString());
Pattern pattern = Pattern.compile(properties.getFolder().getYearRegex());
Matcher matcher = pattern.matcher(fullMovie);
if (!matcher.find()) {
logger.warn("No regex matches found for " + fullMovie);
continue;
}
String year = matcher.group(matcher.groupCount()).replaceAll("[)(]", "");
String title = fullMovie.substring(0, fullMovie.indexOf(" ("));
Movie movie = new Movie(-1, title, Integer.parseInt(year), "");
ownedMovies.add(movie);
} else {
logger.warn("Skipping file " + file);
}
}
}
/**
* 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();
MediaType mediaType = MediaType.parse("application/octet-stream");
RequestBody.create(mediaType, "{}");
RequestBody body;
Request request = new Request.Builder()
.url("https://api.themoviedb.org/3/authentication/token/new?api_key=" + properties.getMovieDbApiKey())
.get()
.build();
String request_token;
try {
Response response = client.newCall(request).execute();
JSONObject responseJson = new JSONObject(response.body().string());
request_token = responseJson.getString("request_token");
// 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;
}
// Create the sesssion ID for MovieDB using the approved token
mediaType = MediaType.parse("application/json");
body = RequestBody.create(mediaType, "{\"request_token\":\"" + request_token + "\"}");
request = new Request.Builder()
.url("https://api.themoviedb.org/3/authentication/session/new?api_key=" + properties.getMovieDbApiKey())
.post(body)
.addHeader("content-type", "application/json")
.build();
Response response = null;
try {
response = client.newCall(request).execute();
JSONObject sessionResponse = new JSONObject(response.body().string());
return sessionResponse.getString("session_id"); // 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
*/
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(mediaType, "{\"media_id\":" + m.getMedia_id() + "}");
String url = "https://api.themoviedb.org/3/list/" + properties.getMovieDbListId()
+ "/add_item?session_id=" + sessionId + "&api_key=" + properties.getMovieDbApiKey();
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/" + properties.getMovieDbListId());
}
/**
* Connect to plex via the URL and parse all of the movies from the returned XML creating a HashSet of movies the
* user has.
*/
private void findAllPlexMovies() {
logger.info("Searching for Plex Movies...");
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(properties.getPlex().getConnectTimeout(), TimeUnit.SECONDS)
.writeTimeout(properties.getPlex().getWriteTimeout(), TimeUnit.SECONDS)
.readTimeout(properties.getPlex().getReadTimeout(), TimeUnit.SECONDS)
.build();
List<String> urls = properties.getPlex().getMovieUrls();
if (CollectionUtils.isEmpty(urls)) {
logger.info("No URLs added to plexMovieUrls. Check your application.yaml file if needed.");
return;
}
for (String url : urls) {
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
String body = response.body() != null ? response.body().string() : null;
if (body == null) {
logger.error("Body returned null from Plex");
return;
}
InputStream fileIS = new ByteArrayInputStream(body.getBytes());
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document xmlDocument = builder.parse(fileIS);
XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "/MediaContainer/Video";
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
for (int i = 0; i < nodeList.getLength(); i++) {
Node node = nodeList.item(i);
//Files can't have : so need to remove to find matches correctly
String title = node.getAttributes().getNamedItem("title").getNodeValue().replaceAll(":", "");
if (node.getAttributes().getNamedItem("year") == null) {
logger.warn("Year not found for " + title);
continue;
}
String year = node.getAttributes().getNamedItem("year").getNodeValue();
Movie movie = new Movie(-1, title, Integer.parseInt(year), "");
ownedMovies.add(movie);
totalMovieCount.incrementAndGet();
}
logger.info(ownedMovies.size() + " movies found in plex");
} catch (IOException e) {
logger.error("Error connecting to Plex to get Movie list", e);
} catch (ParserConfigurationException | XPathExpressionException | SAXException e) {
logger.error("Error parsing XML from Plex", e);
}
}
}
/**
* 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
* full properties that contains the collection id, we can search that collection id for it's list of movies. We
* compare the full collection list to the movies found in plex, any missing we add to the recommended list. To
* 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() {
logger.info("Searching for Movie Collections...");
OkHttpClient client = new OkHttpClient();
if (StringUtils.isEmpty(properties.getMovieDbApiKey())) {
logger.error("No MovieDb Key added to movieDbApiKey. Check your application.yaml file.");
return;
}
for (Movie movie : ownedMovies) {
//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() + ". ");
}
searchedMovieCount.incrementAndGet();
if (searched.contains(movie)) {
continue;
}
String searchMovieUrl;
try {
searchMovieUrl = "https://api.themoviedb.org/3/search/movie?api_key=" +
properties.getMovieDbApiKey() +
"&language=en-US&page=1&include_adult=false&query=" +
URLEncoder.encode(movie.getName(), "UTF-8") +
"&year=" +
movie.getYear();
Request request = new Request.Builder()
.url(searchMovieUrl)
.build();
String json;
try (Response response = client.newCall(request).execute()) {
json = response.body() != null ? response.body().string() : null;
if (json == null) {
logger.error("Body returned null from TheMovieDB for " + movie.getName());
continue;
}
JSONObject foundMovies = new JSONObject(json);
JSONArray results = foundMovies.getJSONArray("results");
if (results.length() == 0) {
logger.error("Results not found for " + movie);
logger.error("URL: " + searchMovieUrl);
continue;
}
if (results.length() > 1) {
logger.debug("Results for " + movie + " came back with " + results.length() + " results. Using first result.");
logger.debug(movie + " URL: " + searchMovieUrl);
}
JSONObject result = results.getJSONObject(0);
int id = result.getInt("id");
String movieDetailUrl = "https://api.themoviedb.org/3/movie/" + id + "?api_key=" + properties.getMovieDbApiKey() + "&language=en-US";
request = new Request.Builder()
.url(movieDetailUrl)
.build();
try (Response movieDetailResponse = client.newCall(request).execute()) {
String movieDetailJson = movieDetailResponse.body() != null ? movieDetailResponse.body().string() : null;
if (movieDetailJson == null) {
logger.error("Body returned null from TheMovieDB for details on " + movie.getName());
continue;
}
JSONObject movieDetails = new JSONObject(movieDetailJson);
if (!movieDetails.has("belongs_to_collection") || movieDetails.isNull("belongs_to_collection")) {
//No collection found, just add movie to searched and continue
searched.add(movie);
continue;
}
int collectionId = movieDetails.getJSONObject("belongs_to_collection").getInt("id");
String collectionName = movieDetails.getJSONObject("belongs_to_collection").getString("name");
String collectionUrl = "https://api.themoviedb.org/3/collection/" + collectionId + "?api_key=" + properties.getMovieDbApiKey() + "&language=en-US";
request = new Request.Builder()
.url(collectionUrl)
.build();
try (Response collectionResponse = client.newCall(request).execute()) {
String collectionJson = collectionResponse.body() != null ? collectionResponse.body().string() : null;
if (collectionJson == null) {
logger.error("Body returned null from TheMovieDB for collection information about " + movie.getName());
continue;
}
JSONObject collection = new JSONObject(collectionJson);
JSONArray parts = collection.getJSONArray("parts");
for (int i = 0; i < parts.length(); i++) {
JSONObject part = parts.getJSONObject(i);
int media_id = part.getInt("id");
//Files can't have : so need to remove to find matches correctly
String title = part.getString("original_title").replaceAll(":", "");
int year;
try {
year = Integer.parseInt(part.getString("release_date").substring(0, 4));
} catch (StringIndexOutOfBoundsException | NumberFormatException e) {
logger.warn("No year found for " + title + ". Value returned was '" + part.getString("release_date") + "'. Not adding the movie to recommended list.");
continue;
}
Movie movieFromCollection = new Movie(media_id, title, year, collectionName);
if (ownedMovies.contains(movieFromCollection)) {
searched.add(movieFromCollection);
} else if (!searched.contains(movieFromCollection) && year != 0 && year < 2019) {
recommended.add(movieFromCollection);
}
}
searched.add(movie);
} catch (IOException e) {
logger.error("Error getting collections " + movie, e);
}
} catch (IOException e) {
logger.error("Error getting movie details " + movie, e);
}
} catch (IOException e) {
logger.error("Error searching for movie " + movie, e);
logger.error("URL: " + searchMovieUrl);
} catch (JSONException e) {
logger.error("Error parsing movie " + movie + ". " + e.getMessage());
logger.error("URL: " + searchMovieUrl);
} finally {
try {
//can't have too many connections to the movie database in a specific time, have to wait
Thread.sleep(900);
} catch (InterruptedException e) {
logger.error("Error sleeping", e);
}
}
} catch (UnsupportedEncodingException e) {
logger.error("Error parsing movie URL " + movie, e);
}
}
}
/**
* Prints out all recommended files to the terminal or command line
*/
private void printRecommended() {
System.out.println(recommended.size() + " Recommended Movies");
for (Movie movie : recommended) {
System.out.println(movie.toString());
}
}
/**
* Prints out all recommended files to a text file called gaps_recommended_movies.txt
*/
private void writeToFile() {
File file = new File("gaps_recommended_movies.txt");
if (file.exists()) {
boolean deleted = file.delete();
if (!deleted) {
logger.error("Can't delete existing file gaps_recommended_movies.txt");
return;
}
}
try {
boolean created = file.createNewFile();
if (!created) {
logger.error("Can't create file gaps_recommended_movies.txt");
return;
}
} catch (IOException e) {
logger.error("Can't create file gaps_recommended_movies.txt", e);
return;
}
try (FileOutputStream outputStream = new FileOutputStream("gaps_recommended_movies.txt")) {
for (Movie movie : recommended) {
String output = movie.toString() + System.lineSeparator();
outputStream.write(output.getBytes());
}
} catch (FileNotFoundException e) {
logger.error("Can't find file gaps_recommended_movies.txt", e);
} catch (IOException e) {
logger.error("Can't write to file gaps_recommended_movies.txt", e);
}
}
public class UserInputThreadCountdown implements java.lang.Runnable {
int time_limit = 60;
Date start;
@Override
public void run() {
start = new Date();
try {
this.runTimer();
} catch (IOException e) {
e.printStackTrace();
}
}
public void runTimer() throws IOException {
long timePassedstart = 0;
do {
timePassedstart = (new Date().getTime() - start.getTime()) / 1000;
} while (timePassedstart < time_limit);
System.in.close();
}
}
}
+1 -1
View File
@@ -86,5 +86,5 @@ gaps:
#Adjust logging as you see fit
logging:
level:
root: DEBUG
root: INFO
-139
View File
@@ -1,139 +0,0 @@
#!/bin/bash
#
# avoid dumping keys where possible
set +x
fail() {
echo -e "Fatal: $1"
exit 1
}
#
#if [[ -z $DB_API_KEY ]]; then
# fail "Need to specify DB_API_KEY as environment variable\nRefer to README.md"
#fi
#
##Must be plex or folder searching
#if [[ ( -z $SEARCH_FROM_FOLDER || $SEARCH_FROM_FOLDER = false ) && ( -z $SEARCH_FROM_PLEX || $SEARCH_FROM_PLEX = false ) ]]; then
# fail "One or both of SEARCH_FROM_FOLDER or SEARCH_FROM_PlEX must be true.\nRefer to README.md"
#fi
#
##Global to all
#PREFIX='- '
#NEW_LINE=$'\n '
#REGEX='\,'
#
##Folder
#ALL_FOLDERS=""
#ALL_MOVIE_FORMATS=""
#DEFAULT_RECURSIVE_FLAG=false
#DEFAULT_YEAR_REGEX="\(([1-2][0-9][0-9][0-9])\)"
#if [[ test -z $SEARCH_FROM_FOLDER && $SEARCH_FROM_FOLDER = true ]]; then
#
# if [[ -z $RECURSIVE ]]; then
# DEFAULT_RECURSIVE_FLAG=$RECURSIVE
# fi
#
# if [[ -x $YEAR_REGEX ]]; then
# DEFAULT_YEAR_REGEX=$YEAR_REGEX
# fi
#
# if [[ $FOLDERS =~ $REGEX ]]; then
# IFS=$REGEX read -r -a array <<< "$FOLDERS"
# for element in "${array[@]}"
# do
# ALL_FOLDERS=$ALL_FOLDERS$PREFIX$element$NEW_LINE
# done
# else
# ALL_FOLDERS=$PREFIX$FOLDERS
# fi
#
# if [[ $MOVIE_FORMATS =~ $REGEX ]]; then
# IFS=$REGEX read -r -a array <<< "$MOVIE_FORMATS"
# for element in "${array[@]}"
# do
# ALL_MOVIE_FORMATS=$ALL_MOVIE_FORMATS$PREFIX$element$NEW_LINE
# done
# else
# ALL_MOVIE_FORMATS=$PREFIX$MOVIE_FORMATS
# fi
#
#else
# echo "Not searching from Folder."
#fi
#
#
##Plex
#ALL_PLEX_ADDRESSES=""
#DEFAULT_CONNECT_TIMEOUT=180
#DEFAULT_WRITE_TIMEOUT=180
#DEFAULT_READ_TIMEOUT=180
#if [[ test -z $SEARCH_FROM_PLEX && $SEARCH_FROM_PLEX = true ]]; then
# if [[ -z $PLEX_ADDRESS ]]; then
# fail "Need to specify PLEX_ADDRESS as environment variable if setting SEARCH_FROM_PLEX true.\nRefer to README.md"
# fi
#
# if [[ $PLEX_ADDRESS =~ $REGEX ]]; then
# IFS=$REGEX read -r -a array <<< "$PLEX_ADDRESS"
# for element in "${array[@]}"
# do
# ALL_PLEX_ADDRESSES=$ALL_PLEX_ADDRESSES$PREFIX$element$NEW_LINE
# done
# else
# ALL_PLEX_ADDRESSES=$PREFIX$PLEX_ADDRESS
# fi
#
# if [[ -z ${CONNECT_TIMEOUT} ]]; then
# echo "No connect timeout found. Defaulting to 180 seconds"
# DEFAULT_CONNECT_TIMEOUT=180
# else
# DEFAULT_CONNECT_TIMEOUT=${CONNECT_TIMEOUT}
# fi
#
# if [[ -z ${WRITE_TIMEOUT} ]]; then
# echo "No connect timeout found. Defaulting to 180 seconds"
# DEFAULT_WRITE_TIMEOUT=180
# else
# DEFAULT_WRITE_TIMEOUT=${WRITE_TIMEOUT}
# fi
#
# if [[ -z ${READ_TIMEOUT} ]]; then
# echo "No connect timeout found. Defaulting to 180 seconds"
# READ_TIMEOUT=180
# else
# DEFAULT_READ_TIMEOUT=${READ_TIMEOUT}
# fi
#else
# echo "Not searching from Plex."
#fi
#
#if [[ -z $WRITE_TO_FILE ]]; then
# fail "Need to specify WRITE_TO_FILE as environment variable\nRefer to README.md"
#fi
#
#cat > /usr/src/app/src/main/resources/application.yaml <<EOF
#gaps:
# movieDbApiKey: ${DB_API_KEY}
# writeToFile: ${WRITE_TO_FILE}
# folder:
# searchFromFolder: ${SEARCH_FROM_FOLDER}
# folders:
# ${ALL_FOLDERS}
# recursive: ${DEFAULT_RECURSIVE_FLAG}
# movieFormats:
# ${ALL_MOVIE_FORMATS}
# yearRegex: ${DEFAULT_YEAR_REGEX}
# plex:
# searchFromPlex: true
# movieUrls:
# ${ALL_PLEX_ADDRESSES}
# connectTimeout: ${DEFAULT_CONNECT_TIMEOUT}
# writeTimeout: ${DEFAULT_WRITE_TIMEOUT}
# readTimeout: ${DEFAULT_READ_TIMEOUT}
# movieDbListId: $TMDBLISTID
#
#logging:
# level:
# root: INFO
#EOF
exec mvn spring-boot:run