Java tutorial
/* * Copyright (c) 2004-2016 YAMJ Members * https://github.com/orgs/YAMJ/people * * This file is part of the Yet Another Movie Jukebox (YAMJ) project. * * YAMJ is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * YAMJ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with YAMJ. If not, see <http://www.gnu.org/licenses/>. * * Web: https://github.com/YAMJ/yamj-v2 * */ package com.moviejukebox.reader; import static com.moviejukebox.tools.StringTools.isValidString; import com.moviejukebox.model.*; import com.moviejukebox.model.enumerations.CodecSource; import com.moviejukebox.model.enumerations.CodecType; import com.moviejukebox.model.enumerations.DirtyFlag; import com.moviejukebox.plugin.*; import com.moviejukebox.scanner.MovieFilenameScanner; import com.moviejukebox.tools.*; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.util.*; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.pojava.datetime.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.*; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; /** * Class to read the NFO files * * @author stuart.boston */ public final class MovieNFOReader { private static final Logger LOG = LoggerFactory.getLogger(MovieNFOReader.class); /** * Node Type: Movie */ public static final String TYPE_MOVIE = "movie"; /** * Node Type: TV Show */ public static final String TYPE_TVSHOW = "tvshow"; /** * Node Type: Episode */ public static final String TYPE_EPISODE = "episodedetails"; /** * Plugin ID */ public static final String NFO_PLUGIN_ID = "NFO"; // Other properties private static final String XML_START = "<"; private static final String XML_END = "</"; private static final String ERROR_FIXIT = "Failed parsing NFO file: {}. Does not seem to be an XML format. Error: {}"; private static final boolean SKIP_NFO_URL = PropertiesUtil.getBooleanProperty("filename.nfo.skipUrl", Boolean.TRUE); private static final boolean SKIP_NFO_TRAILER = PropertiesUtil.getBooleanProperty("filename.nfo.skipTrailer", Boolean.FALSE); private static final boolean SKIP_NFO_CAST = PropertiesUtil.getBooleanProperty("filename.nfo.skipCast", Boolean.FALSE); private static final boolean SKIP_NFO_CREW = PropertiesUtil.getBooleanProperty("filename.nfo.skipCrew", Boolean.FALSE); private static final AspectRatioTools ASPECT_TOOLS = new AspectRatioTools(); private static final boolean CERT_FROM_MPAA = PropertiesUtil.getBooleanProperty("imdb.getCertificationFromMPAA", Boolean.TRUE); private static final String IMDB_PREFERRED_COUNTRY = PropertiesUtil.getProperty("imdb.preferredCountry", "USA"); private static final String LANGUAGE_DELIMITER = PropertiesUtil.getProperty("mjb.language.delimiter", Movie.SPACE_SLASH_SPACE); // Fanart settings private static final String FANART_TOKEN = PropertiesUtil.getProperty("mjb.scanner.fanartToken", ".fanart"); private static final String FANART_EXT = PropertiesUtil.getProperty("fanart.format", "jpg"); // Patterns private static final String SPLIT_GENRE = "(?<!-)/|,|\\|"; // Caters for the case where "-/" is not wanted as part of the split // Max People values private static final int MAX_COUNT_ACTOR = PropertiesUtil.getReplacedIntProperty("movie.actor.maxCount", "plugin.people.maxCount.actor", 10); /** * This is a utility class and cannot be instantiated. */ private MovieNFOReader() { throw new UnsupportedOperationException("Class cannot be instantiated"); } /** * Try and read a NFO file for information * * First try as XML format file, then check to see if it contains XML and text and split it to read each part * * @param nfoFile * @param movie * @return */ public static boolean readNfoFile(File nfoFile, Movie movie) { String nfoText = FileTools.readFileToString(nfoFile); boolean parsedNfo = Boolean.FALSE; // Was the NFO XML parsed correctly or at all boolean hasXml = Boolean.FALSE; if (StringUtils.containsIgnoreCase(nfoText, XML_START + TYPE_MOVIE) || StringUtils.containsIgnoreCase(nfoText, XML_START + TYPE_TVSHOW) || StringUtils.containsIgnoreCase(nfoText, XML_START + TYPE_EPISODE)) { hasXml = Boolean.TRUE; } // If the file has XML tags in it, try reading it as a pure XML file if (hasXml) { parsedNfo = readXmlNfo(nfoFile, movie); } // If it has XML in it, but didn't parse correctly, try splitting it out if (hasXml && !parsedNfo) { int posMovie = findPosition(nfoText, TYPE_MOVIE); int posTv = findPosition(nfoText, TYPE_TVSHOW); int posEp = findPosition(nfoText, TYPE_EPISODE); int start = Math.min(posMovie, Math.min(posTv, posEp)); posMovie = StringUtils.indexOf(nfoText, XML_END + TYPE_MOVIE); posTv = StringUtils.indexOf(nfoText, XML_END + TYPE_TVSHOW); posEp = StringUtils.indexOf(nfoText, XML_END + TYPE_EPISODE); int end = Math.max(posMovie, Math.max(posTv, posEp)); if ((end > -1) && (end > start)) { end = StringUtils.indexOf(nfoText, '>', end) + 1; // Send text to be read String nfoTrimmed = StringUtils.substring(nfoText, start, end); parsedNfo = readXmlNfo(nfoTrimmed, movie, nfoFile.getName()); nfoTrimmed = StringUtils.remove(nfoText, nfoTrimmed); if (parsedNfo && nfoTrimmed.length() > 0) { // We have some text left, so scan that with the text scanner readTextNfo(nfoTrimmed, movie); } } } // If the XML wasn't found or parsed correctly, then fall back to the old method if (parsedNfo) { LOG.debug("Successfully scanned {} as XML format", nfoFile.getName()); } else { parsedNfo = MovieNFOReader.readTextNfo(nfoText, movie); if (parsedNfo) { LOG.debug("Successfully scanned {} as text format", nfoFile.getName()); } else { LOG.debug("Failed to find any information in {}", nfoFile.getName()); } } return Boolean.FALSE; } /** * Find the position of the string or return the maximum * * @param nfoText * @param xmlType * @return */ private static int findPosition(final String nfoText, final String xmlType) { int pos = StringUtils.indexOf(nfoText, XML_START + xmlType); return (pos == -1 ? Integer.MAX_VALUE : pos); } /** * Used to parse out the XML NFO data from a string. * * @param nfoText * @param movie * @param nfoFilename * @return */ public static boolean readXmlNfo(String nfoText, Movie movie, String nfoFilename) { return convertNfoToDoc(null, nfoText, movie, nfoFilename); } /** * Used to parse out the XML NFO data from a file. * * This is generic for movie and TV show files as they are both nearly identical. * * @param nfoFile * @param movie * @return */ public static boolean readXmlNfo(File nfoFile, Movie movie) { return convertNfoToDoc(nfoFile, null, movie, nfoFile.getName()); } /** * Scan a text file for information * * @param nfo * @param movie * @return */ public static boolean readTextNfo(String nfo, Movie movie) { boolean foundInfo = DatabasePluginController.scanNFO(nfo, movie); LOG.debug("Scanning NFO for Poster URL"); int urlStartIndex = 0; while (urlStartIndex >= 0 && urlStartIndex < nfo.length()) { int currentUrlStartIndex = nfo.indexOf("http://", urlStartIndex); if (currentUrlStartIndex >= 0) { int currentUrlEndIndex = nfo.indexOf("jpg", currentUrlStartIndex); if (currentUrlEndIndex < 0) { currentUrlEndIndex = nfo.indexOf("JPG", currentUrlStartIndex); } if (currentUrlEndIndex >= 0) { int nextUrlStartIndex = nfo.indexOf("http://", currentUrlStartIndex); // look for shortest http:// while ((nextUrlStartIndex != -1) && (nextUrlStartIndex < currentUrlEndIndex + 3)) { currentUrlStartIndex = nextUrlStartIndex; nextUrlStartIndex = nfo.indexOf("http://", currentUrlStartIndex + 1); } // Check to see if the URL has <fanart> at the beginning and ignore it if it does (Issue 706) if ((currentUrlStartIndex < 8) || (nfo.substring(currentUrlStartIndex - 8, currentUrlStartIndex) .compareToIgnoreCase("<fanart>") != 0)) { String foundUrl = nfo.substring(currentUrlStartIndex, currentUrlEndIndex + 3); // Check for some invalid characters to see if the URL is valid if (foundUrl.contains(" ") || foundUrl.contains("*")) { urlStartIndex = currentUrlStartIndex + 3; } else { LOG.debug("Poster URL found in nfo = {}", foundUrl); movie.setPosterURL(nfo.substring(currentUrlStartIndex, currentUrlEndIndex + 3)); urlStartIndex = -1; movie.setDirty(DirtyFlag.POSTER, Boolean.TRUE); foundInfo = Boolean.TRUE; } } else { LOG.debug("Poster URL ignored in NFO because it's a fanart URL"); // Search for the URL again urlStartIndex = currentUrlStartIndex + 3; } } else { urlStartIndex = currentUrlStartIndex + 3; } } else { urlStartIndex = -1; } } return foundInfo; } /** * Take either a file or a String and process the NFO * * @param nfoFile * @param nfoString * @param movie * @param nfoFilename * @return */ private static boolean convertNfoToDoc(File nfoFile, final String nfoString, Movie movie, final String nfoFilename) { Document xmlDoc; String filename; if (StringUtils.isBlank(nfoFilename) && nfoFile != null) { filename = nfoFile.getName(); } else { filename = nfoFilename; } try { if (nfoFile == null) { // Assume we're using the string xmlDoc = DOMHelper.getDocFromString(nfoString); } else { xmlDoc = DOMHelper.getDocFromFile(nfoFile); } } catch (SAXParseException ex) { LOG.debug(ERROR_FIXIT, filename, ex.getMessage()); return Boolean.FALSE; } catch (MalformedURLException ex) { LOG.debug(ERROR_FIXIT, filename, ex.getMessage()); return Boolean.FALSE; } catch (IOException ex) { LOG.debug(ERROR_FIXIT, filename, ex.getMessage()); return Boolean.FALSE; } catch (ParserConfigurationException | SAXException ex) { LOG.debug(ERROR_FIXIT, filename, ex.getMessage()); return Boolean.FALSE; } return parseXmlNfo(xmlDoc, movie, filename); } /** * Parse the XML document for NFO information * * @param xmlDoc * @param movie * @param nfoFilename * @return */ private static boolean parseXmlNfo(Document xmlDoc, Movie movie, String nfoFilename) { NodeList nlMovies; // Determine if the NFO file is for a TV Show or Movie so the default ID can be set boolean isTv = xmlDoc.getElementsByTagName(TYPE_TVSHOW).getLength() > 0; // Issue 2559, check in NFO if the movie is a TV show, even if no Season/Episode are in filename if (movie.isTVShow() || isTv) { nlMovies = xmlDoc.getElementsByTagName(TYPE_TVSHOW); isTv = Boolean.TRUE; movie.setMovieType(Movie.TYPE_TVSHOW); // Issue 2559, check in NFO if the movie is a TV show, and override the type } else { nlMovies = xmlDoc.getElementsByTagName(TYPE_MOVIE); isTv = Boolean.FALSE; } // Issue 2542: There should only be 1 movie/tvshow node in the NFO if (nlMovies.getLength() > 0) { Node nMovie = nlMovies.item(0); if (nMovie.getNodeType() == Node.ELEMENT_NODE) { Element eCommon = (Element) nMovie; // Get all of the title elements from the NFO file parseTitle(eCommon, movie); if (OverrideTools.checkOverwriteYear(movie, NFO_PLUGIN_ID)) { String tempYear = DOMHelper.getValueFromElement(eCommon, "year"); if (!parseYear(tempYear, movie)) { LOG.warn("Invalid year: '{}' in {}", tempYear, nfoFilename); } } // ID specific to TV Shows if (movie.isTVShow()) { String tvdbid = DOMHelper.getValueFromElement(eCommon, "tvdbid"); if (isValidString(tvdbid)) { movie.setId(TheTvDBPlugin.THETVDB_PLUGIN_ID, tvdbid); } } // Get all of the other IDs parseIds(eCommon.getElementsByTagName("id"), movie, isTv); // Get the watched status try { movie.setWatchedNFO(Boolean.parseBoolean(DOMHelper.getValueFromElement(eCommon, "watched"))); } catch (Exception ignore) { // Don't change the watched status } // Get the show status if (movie.isTVShow()) { String status = DOMHelper.getValueFromElement(eCommon, "status"); if (isValidString(status)) { movie.setShowStatus(XML_START); } } // Get the sets parseSets(eCommon.getElementsByTagName("set"), movie); // Rating int rating = parseRating(DOMHelper.getValueFromElement(eCommon, "rating")); if (rating > -1) { movie.addRating(NFO_PLUGIN_ID, rating); } // Runtime if (OverrideTools.checkOverwriteRuntime(movie, NFO_PLUGIN_ID)) { parseRuntime(eCommon, movie); } // Certification if (OverrideTools.checkOverwriteCertification(movie, NFO_PLUGIN_ID)) { parseCertification(eCommon, movie); } // Plot if (OverrideTools.checkOverwritePlot(movie, NFO_PLUGIN_ID)) { movie.setPlot(DOMHelper.getValueFromElement(eCommon, "plot"), NFO_PLUGIN_ID); } // Outline if (OverrideTools.checkOverwriteOutline(movie, NFO_PLUGIN_ID)) { movie.setOutline(DOMHelper.getValueFromElement(eCommon, "outline"), NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteGenres(movie, NFO_PLUGIN_ID)) { List<String> newGenres = new ArrayList<>(); parseGenres(eCommon.getElementsByTagName("genre"), newGenres); movie.setGenres(newGenres, NFO_PLUGIN_ID); } // Premiered & Release Date movieDate(movie, DOMHelper.getValueFromElement(eCommon, "premiered")); movieDate(movie, DOMHelper.getValueFromElement(eCommon, "releasedate")); if (OverrideTools.checkOverwriteQuote(movie, NFO_PLUGIN_ID)) { movie.setQuote(DOMHelper.getValueFromElement(eCommon, "quote"), NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteTagline(movie, NFO_PLUGIN_ID)) { movie.setTagline(DOMHelper.getValueFromElement(eCommon, "tagline"), NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteCompany(movie, NFO_PLUGIN_ID)) { movie.setCompany(DOMHelper.getValueFromElement(eCommon, "studio"), NFO_PLUGIN_ID); movie.setCompany(DOMHelper.getValueFromElement(eCommon, "company"), NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteCountry(movie, NFO_PLUGIN_ID)) { movie.setCountries(DOMHelper.getValueFromElement(eCommon, "country"), NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteTop250(movie, NFO_PLUGIN_ID)) { movie.setTop250(DOMHelper.getValueFromElement(eCommon, "top250"), NFO_PLUGIN_ID); } // Poster and Fanart if (!SKIP_NFO_URL) { movie.setPosterURL(DOMHelper.getValueFromElement(eCommon, "thumb")); movie.setFanartURL(DOMHelper.getValueFromElement(eCommon, "fanart")); if (StringTools.isValidString(movie.getFanartURL())) { movie.setFanartFilename(movie.getBaseName() + FANART_TOKEN + "." + FANART_EXT); } } // Trailers if (!SKIP_NFO_TRAILER) { parseTrailers(eCommon.getElementsByTagName("trailer"), movie); } // Director and Writers if (!SKIP_NFO_CREW) { parseDirectors(eCommon.getElementsByTagName("director"), movie); List<Node> writerNodes = new ArrayList<>(); // get writers list NodeList nlWriters = eCommon.getElementsByTagName("writer"); if (nlWriters != null && nlWriters.getLength() > 0) { for (int looper = 0; looper < nlWriters.getLength(); looper++) { Node node = nlWriters.item(looper); if (node.getNodeType() == Node.ELEMENT_NODE) { writerNodes.add(node); } } } // get credits list (old style) nlWriters = eCommon.getElementsByTagName("credits"); if (nlWriters != null && nlWriters.getLength() > 0) { for (int looper = 0; looper < nlWriters.getLength(); looper++) { Node node = nlWriters.item(looper); if (node.getNodeType() == Node.ELEMENT_NODE) { writerNodes.add(node); } } } // parse writers parseWriters(writerNodes, movie); } // Actors if (!SKIP_NFO_CAST) { parseActors(eCommon.getElementsByTagName("actor"), movie); } // FPS if (OverrideTools.checkOverwriteFPS(movie, NFO_PLUGIN_ID)) { float tmpFps = NumberUtils.toFloat(DOMHelper.getValueFromElement(eCommon, "fps"), -1F); if (tmpFps > -1F) { movie.setFps(tmpFps, NFO_PLUGIN_ID); } } // VideoSource: Issue 506 - Even though it's not strictly XBMC standard if (OverrideTools.checkOverwriteVideoSource(movie, NFO_PLUGIN_ID)) { // Issue 2531: Try the alternative "videoSource" movie.setVideoSource(DOMHelper.getValueFromElement(eCommon, "videosource", "videoSource"), NFO_PLUGIN_ID); } // Video Output if (OverrideTools.checkOverwriteVideoOutput(movie, NFO_PLUGIN_ID)) { String tempString = DOMHelper.getValueFromElement(eCommon, "videooutput"); movie.setVideoOutput(tempString, NFO_PLUGIN_ID); } // Parse the video info parseFileInfo(movie, DOMHelper.getElementByName(eCommon, "fileinfo")); } } // Parse the episode details if (movie.isTVShow()) { parseAllEpisodeDetails(movie, xmlDoc.getElementsByTagName(TYPE_EPISODE)); } return Boolean.TRUE; } /** * Parse the FileInfo section * * @param movie * @param eFileInfo */ private static void parseFileInfo(Movie movie, Element eFileInfo) { if (eFileInfo == null) { return; } if (OverrideTools.checkOverwriteContainer(movie, NFO_PLUGIN_ID)) { String container = DOMHelper.getValueFromElement(eFileInfo, "container"); movie.setContainer(container, NFO_PLUGIN_ID); } Element eStreamDetails = DOMHelper.getElementByName(eFileInfo, "streamdetails"); if (eStreamDetails == null) { return; } // Video NodeList nlStreams = eStreamDetails.getElementsByTagName("video"); Node nStreams; for (int looper = 0; looper < nlStreams.getLength(); looper++) { nStreams = nlStreams.item(looper); if (nStreams.getNodeType() == Node.ELEMENT_NODE) { Element eStreams = (Element) nStreams; String temp = DOMHelper.getValueFromElement(eStreams, "codec"); if (isValidString(temp)) { Codec videoCodec = new Codec(CodecType.VIDEO); videoCodec.setCodecSource(CodecSource.NFO); videoCodec.setCodec(temp); movie.addCodec(videoCodec); } if (OverrideTools.checkOverwriteAspectRatio(movie, NFO_PLUGIN_ID)) { temp = DOMHelper.getValueFromElement(eStreams, "aspect"); movie.setAspectRatio(ASPECT_TOOLS.cleanAspectRatio(temp), NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteResolution(movie, NFO_PLUGIN_ID)) { movie.setResolution(DOMHelper.getValueFromElement(eStreams, "width"), DOMHelper.getValueFromElement(eStreams, "height"), NFO_PLUGIN_ID); } } } // End of VIDEO // Audio nlStreams = eStreamDetails.getElementsByTagName("audio"); for (int looper = 0; looper < nlStreams.getLength(); looper++) { nStreams = nlStreams.item(looper); if (nStreams.getNodeType() == Node.ELEMENT_NODE) { Element eStreams = (Element) nStreams; String aCodec = DOMHelper.getValueFromElement(eStreams, "codec").trim(); String aLanguage = DOMHelper.getValueFromElement(eStreams, "language"); String aChannels = DOMHelper.getValueFromElement(eStreams, "channels"); // If the codec is lowercase, covert it to uppercase, otherwise leave it alone if (StringUtils.isAllLowerCase(aCodec)) { aCodec = aCodec.toUpperCase(); } if (StringTools.isValidString(aLanguage)) { aLanguage = MovieFilenameScanner.determineLanguage(aLanguage); } Codec audioCodec = new Codec(CodecType.AUDIO, aCodec); audioCodec.setCodecSource(CodecSource.NFO); audioCodec.setCodecLanguage(aLanguage); audioCodec.setCodecChannels(aChannels); movie.addCodec(audioCodec); } } // End of AUDIO // Update the language if (OverrideTools.checkOverwriteLanguage(movie, NFO_PLUGIN_ID)) { Set<String> langs = new HashSet<>(); // Process the languages and remove any duplicates for (Codec codec : movie.getCodecs()) { if (codec.getCodecType() == CodecType.AUDIO) { langs.add(codec.getCodecLanguage()); } } // Remove UNKNOWN if it is NOT the only entry if (langs.contains(Movie.UNKNOWN) && langs.size() > 1) { langs.remove(Movie.UNKNOWN); } else if (langs.isEmpty()) { // Add the language as UNKNOWN by default. langs.add(Movie.UNKNOWN); } // Build the language string StringBuilder movieLanguage = new StringBuilder(); for (String lang : langs) { if (movieLanguage.length() > 0) { movieLanguage.append(LANGUAGE_DELIMITER); } movieLanguage.append(lang); } movie.setLanguage(movieLanguage.toString(), NFO_PLUGIN_ID); } // Subtitles List<String> subtitles = new ArrayList<>(); nlStreams = eStreamDetails.getElementsByTagName("subtitle"); for (int looper = 0; looper < nlStreams.getLength(); looper++) { nStreams = nlStreams.item(looper); if (nStreams.getNodeType() == Node.ELEMENT_NODE) { Element eStreams = (Element) nStreams; subtitles.add(DOMHelper.getValueFromElement(eStreams, "language")); } } SubtitleTools.setMovieSubtitles(movie, subtitles); } /** * Process all the Episode Details * * @param movie * @param nlEpisodeDetails */ private static void parseAllEpisodeDetails(Movie movie, NodeList nlEpisodeDetails) { Node nEpisodeDetails; for (int looper = 0; looper < nlEpisodeDetails.getLength(); looper++) { nEpisodeDetails = nlEpisodeDetails.item(looper); if (nEpisodeDetails.getNodeType() == Node.ELEMENT_NODE) { Element eEpisodeDetail = (Element) nEpisodeDetails; parseSingleEpisodeDetail(eEpisodeDetail).updateMovie(movie); } } } /** * Parse a single episode detail element * * @param movie * @param eEpisodeDetails * @return */ private static EpisodeDetail parseSingleEpisodeDetail(Element eEpisodeDetails) { EpisodeDetail epDetail = new EpisodeDetail(); if (eEpisodeDetails == null) { return epDetail; } epDetail.setTitle(DOMHelper.getValueFromElement(eEpisodeDetails, "title")); String tempValue = DOMHelper.getValueFromElement(eEpisodeDetails, "season"); if (StringUtils.isNumeric(tempValue)) { epDetail.setSeason(Integer.parseInt(tempValue)); } tempValue = DOMHelper.getValueFromElement(eEpisodeDetails, "episode"); if (StringUtils.isNumeric(tempValue)) { epDetail.setEpisode(Integer.parseInt(tempValue)); } epDetail.setPlot(DOMHelper.getValueFromElement(eEpisodeDetails, "plot")); tempValue = DOMHelper.getValueFromElement(eEpisodeDetails, "rating"); int rating = parseRating(tempValue); if (rating > -1) { // Looks like a valid rating epDetail.setRating(String.valueOf(rating)); } tempValue = DOMHelper.getValueFromElement(eEpisodeDetails, "aired"); if (isValidString(tempValue)) { try { epDetail.setFirstAired(DateTimeTools.convertDateToString(new DateTime(tempValue))); } catch (Exception ignore) { // Set the aired date if there is an exception epDetail.setFirstAired(tempValue); } } epDetail.setAirsAfterSeason( DOMHelper.getValueFromElement(eEpisodeDetails, "airsafterseason", "airsAfterSeason")); epDetail.setAirsBeforeSeason( DOMHelper.getValueFromElement(eEpisodeDetails, "airsbeforeseason", "airsBeforeSeason")); epDetail.setAirsBeforeEpisode( DOMHelper.getValueFromElement(eEpisodeDetails, "airsbeforeepisode", "airsBeforeEpisode")); return epDetail; } /** * Convert the date string to a date and update the movie object * * @param movie * @param dateString */ public static void movieDate(Movie movie, final String dateString) { String parseDate = StringUtils.normalizeSpace(dateString); if (StringTools.isNotValidString(parseDate)) { // No date, so return return; } Date parsedDate; try { parsedDate = DateTimeTools.parseStringToDate(parseDate); } catch (IllegalArgumentException ex) { LOG.warn("Failed parsing NFO file for movie: {}. Please fix or remove it.", movie.getBaseFilename()); LOG.warn("premiered or releasedate does not contain a valid date: {}", parseDate); LOG.warn(SystemTools.getStackTrace(ex)); if (OverrideTools.checkOverwriteReleaseDate(movie, NFO_PLUGIN_ID)) { movie.setReleaseDate(parseDate, NFO_PLUGIN_ID); } return; } if (parsedDate != null) { try { if (OverrideTools.checkOverwriteReleaseDate(movie, NFO_PLUGIN_ID)) { movie.setReleaseDate(DateTimeTools.convertDateToString(parsedDate, "yyyy-MM-dd"), NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteYear(movie, NFO_PLUGIN_ID)) { movie.setYear(DateTimeTools.convertDateToString(parsedDate, "yyyy"), NFO_PLUGIN_ID); } } catch (Exception ex) { LOG.warn("Failed formatting parsed date: {}", parsedDate); LOG.warn(SystemTools.getStackTrace(ex)); } } } /** * Parse Genres from the XML NFO file * * Caters for multiple genres on the same line and multiple lines. * * @param nlElements * @param movie */ private static void parseGenres(NodeList nlElements, List<String> newGenres) { Node nElements; for (int looper = 0; looper < nlElements.getLength(); looper++) { nElements = nlElements.item(looper); if (nElements.getNodeType() == Node.ELEMENT_NODE) { Element eGenre = (Element) nElements; NodeList nlNames = eGenre.getElementsByTagName("name"); if ((nlNames != null) && (nlNames.getLength() > 0)) { parseGenres(nlNames, newGenres); } else { newGenres.addAll(StringTools.splitList(eGenre.getTextContent(), SPLIT_GENRE)); } } } } /** * Parse Actors from the XML NFO file. * * @param nlElements * @param movie */ private static void parseActors(NodeList nlElements, Movie movie) { // check if we have a node if (nlElements == null || nlElements.getLength() == 0) { return; } // check if we should override boolean overrideActors = OverrideTools.checkOverwriteActors(movie, NFO_PLUGIN_ID); boolean overridePeopleActors = OverrideTools.checkOverwritePeopleActors(movie, NFO_PLUGIN_ID); if (!overrideActors && !overridePeopleActors) { // nothing to do if nothing should be overridden return; } // count for already set actors int count = 0; // flag to indicate if cast must be cleared boolean clearCast = Boolean.TRUE; boolean clearPeopleCast = Boolean.TRUE; for (int actorLoop = 0; actorLoop < nlElements.getLength(); actorLoop++) { // Get all the name/role/thumb nodes Node nActors = nlElements.item(actorLoop); NodeList nlCast = nActors.getChildNodes(); Node nElement; String aName = Movie.UNKNOWN; String aRole = Movie.UNKNOWN; String aThumb = Movie.UNKNOWN; Boolean firstActor = Boolean.TRUE; if (nlCast.getLength() > 1) { for (int looper = 0; looper < nlCast.getLength(); looper++) { nElement = nlCast.item(looper); if (nElement.getNodeType() == Node.ELEMENT_NODE) { Element eCast = (Element) nElement; if ("name".equalsIgnoreCase(eCast.getNodeName())) { if (firstActor) { firstActor = Boolean.FALSE; } else { if (overrideActors) { // clear cast if not already done if (clearCast) { movie.clearCast(); clearCast = Boolean.FALSE; } // add actor movie.addActor(aName, NFO_PLUGIN_ID); } if (overridePeopleActors && (count < MAX_COUNT_ACTOR)) { // clear people cast if not already done if (clearPeopleCast) { movie.clearPeopleCast(); clearPeopleCast = Boolean.FALSE; } // add actor if (movie.addActor(Movie.UNKNOWN, aName, aRole, aThumb, Movie.UNKNOWN, NFO_PLUGIN_ID)) { count++; } } } aName = eCast.getTextContent(); aRole = Movie.UNKNOWN; aThumb = Movie.UNKNOWN; } else if ("role".equalsIgnoreCase(eCast.getNodeName()) && StringUtils.isNotBlank(eCast.getTextContent())) { aRole = eCast.getTextContent(); } else if ("thumb".equalsIgnoreCase(eCast.getNodeName()) && StringUtils.isNotBlank(eCast.getTextContent())) { // thumb will be skipped if there's nothing in there aThumb = eCast.getTextContent(); } // There's a case where there might be a different node here that isn't name, role or thumb, but that will be ignored } } } else { // This looks like a Mede8er node in the "<actor>Actor Name</actor>" format, so just get the text element aName = nActors.getTextContent(); } if (overrideActors) { // clear cast if not already done if (clearCast) { movie.clearCast(); clearCast = Boolean.FALSE; } // add actor movie.addActor(aName, NFO_PLUGIN_ID); } if (overridePeopleActors && (count < MAX_COUNT_ACTOR)) { // clear people cast if not already done if (clearPeopleCast) { movie.clearPeopleCast(); clearPeopleCast = Boolean.FALSE; } // add actor if (movie.addActor(Movie.UNKNOWN, aName, aRole, aThumb, Movie.UNKNOWN, NFO_PLUGIN_ID)) { count++; } } } } /** * Parse Writers from the XML NFO file * * @param nlElements * @param movie */ private static void parseWriters(List<Node> nlWriters, Movie movie) { // check if we have nodes if (nlWriters == null || nlWriters.isEmpty()) { return; } // check if we should override boolean overrideWriters = OverrideTools.checkOverwriteWriters(movie, NFO_PLUGIN_ID); boolean overridePeopleWriters = OverrideTools.checkOverwritePeopleWriters(movie, NFO_PLUGIN_ID); if (!overrideWriters && !overridePeopleWriters) { // nothing to do if nothing should be overridden return; } Set<String> newWriters = new LinkedHashSet<>(); for (Node nWriter : nlWriters) { NodeList nlChilds = ((Element) nWriter).getChildNodes(); Node nChilds; for (int looper = 0; looper < nlChilds.getLength(); looper++) { nChilds = nlChilds.item(looper); if (nChilds.getNodeType() == Node.TEXT_NODE) { newWriters.add(nChilds.getNodeValue()); } } } if (overrideWriters) { movie.setWriters(newWriters, NFO_PLUGIN_ID); } if (overridePeopleWriters) { movie.setPeopleWriters(newWriters, NFO_PLUGIN_ID); } } /** * Parse Directors from the XML NFO file * * @param nlElements * @param movie */ private static void parseDirectors(NodeList nlElements, Movie movie) { // check if we have a node if (nlElements == null || nlElements.getLength() == 0) { return; } // check if we should override boolean overrideDirectors = OverrideTools.checkOverwriteDirectors(movie, NFO_PLUGIN_ID); boolean overridePeopleDirectors = OverrideTools.checkOverwritePeopleDirectors(movie, NFO_PLUGIN_ID); if (!overrideDirectors && !overridePeopleDirectors) { // nothing to do if nothing should be overridden return; } List<String> newDirectors = new ArrayList<>(); Node nElements; for (int looper = 0; looper < nlElements.getLength(); looper++) { nElements = nlElements.item(looper); if (nElements.getNodeType() == Node.ELEMENT_NODE) { Element eDirector = (Element) nElements; newDirectors.add(eDirector.getTextContent()); } } if (overrideDirectors) { movie.setDirectors(newDirectors, NFO_PLUGIN_ID); } if (overridePeopleDirectors) { movie.setPeopleDirectors(newDirectors, NFO_PLUGIN_ID); } } /** * Parse Trailers from the XML NFO file * * @param nlElements * @param movie */ private static void parseTrailers(NodeList nlElements, Movie movie) { Node nElements; for (int looper = 0; looper < nlElements.getLength(); looper++) { nElements = nlElements.item(looper); if (nElements.getNodeType() == Node.ELEMENT_NODE) { Element eTrailer = (Element) nElements; String trailer = eTrailer.getTextContent().trim(); if (!trailer.isEmpty()) { ExtraFile ef = new ExtraFile(); ef.setNewFile(Boolean.FALSE); ef.setFilename(trailer); movie.addExtraFile(ef); } } } } /** * Parse Sets from the XML NFO file * * @param nlElements * @param movie */ private static void parseSets(NodeList nlElements, Movie movie) { Node nElements; for (int looper = 0; looper < nlElements.getLength(); looper++) { nElements = nlElements.item(looper); if (nElements.getNodeType() == Node.ELEMENT_NODE) { Element eId = (Element) nElements; String setOrder = eId.getAttribute("order"); if (StringUtils.isNumeric(setOrder)) { movie.addSet(eId.getTextContent(), Integer.parseInt(setOrder)); } else { movie.addSet(eId.getTextContent()); } } } } /** * Parse Certification from the XML NFO file * * @param eCommon * @param movie */ private static void parseCertification(Element eCommon, Movie movie) { if (eCommon == null) { return; } if (!OverrideTools.checkOverwriteCertification(movie, NFO_PLUGIN_ID)) { return; } String tempCert; if (CERT_FROM_MPAA) { tempCert = DOMHelper.getValueFromElement(eCommon, "mpaa"); if (isValidString(tempCert)) { // Issue 333 movie.setCertification(StringTools.processMpaaCertification(tempCert), NFO_PLUGIN_ID); } } else { tempCert = DOMHelper.getValueFromElement(eCommon, "certification"); if (isValidString(tempCert)) { int countryPos = tempCert.lastIndexOf(IMDB_PREFERRED_COUNTRY); if (countryPos > 0) { // We've found the country, so extract just that tag tempCert = tempCert.substring(countryPos); int pos = tempCert.indexOf(':'); if (pos > 0) { int endPos = tempCert.indexOf(" /"); if (endPos > 0) { // This is in the middle of the string tempCert = tempCert.substring(pos + 1, endPos); } else { // This is at the end of the string tempCert = tempCert.substring(pos + 1); } } } else if (StringUtils.containsIgnoreCase(tempCert, "Rated")) { // Extract the MPAA rating from the certification tempCert = StringTools.processMpaaCertification(tempCert); } else { // The country wasn't found in the value, so grab the last one int pos = tempCert.lastIndexOf(':'); if (pos > 0) { // Strip the country code from the rating for certification like "UK:PG-12" tempCert = tempCert.substring(pos + 1); } } movie.setCertification(tempCert.trim(), NFO_PLUGIN_ID); } } } /** * Parse Runtime from the XML NFO file * * @param eCommon * @param movie */ public static void parseRuntime(Element eCommon, Movie movie) { if (OverrideTools.checkOverwriteRuntime(movie, NFO_PLUGIN_ID)) { String runtime = DOMHelper.getValueFromElement(eCommon, "runtime"); // Save the first runtime to use if no preferred one is found String prefRuntime = null; // Split the runtime into individual parts for (String rtSingle : runtime.split("\\|")) { // IF we don't have a current preferred runtime, set it now. if (StringUtils.isBlank(prefRuntime)) { prefRuntime = rtSingle; } // Check to see if we have our preferred country in the string if (StringUtils.containsIgnoreCase(rtSingle, IMDB_PREFERRED_COUNTRY)) { // Lets get the country runtime prefRuntime = rtSingle.substring( rtSingle.indexOf(IMDB_PREFERRED_COUNTRY) + IMDB_PREFERRED_COUNTRY.length() + 1); } } movie.setRuntime(prefRuntime, NFO_PLUGIN_ID); } } /** * Parse the rating from the passed string and normalise it * * @param ratingString * @param movie * @return true if the rating was successfully parsed. */ private static int parseRating(String ratingString) { if (StringTools.isNotValidString(ratingString)) { // Rating isn't valid, so skip it return -1; } float rating = NumberUtils.toFloat(ratingString, 0.0f); if (rating > 0.0f) { if (rating <= 10.0f) { return Math.round(rating * 10f); } return Math.round(rating * 1f); } // negative or zero, so return zero return 0; } /** * Parse all the IDs associated with the movie from the XML NFO file * * @param nlElements * @param movie * @param isTv */ private static void parseIds(NodeList nlElements, Movie movie, boolean isTv) { Node nElements; for (int looper = 0; looper < nlElements.getLength(); looper++) { nElements = nlElements.item(looper); if (nElements.getNodeType() == Node.ELEMENT_NODE) { Element eId = (Element) nElements; String movieDb = eId.getAttribute("moviedb"); if (StringTools.isNotValidString(movieDb)) { // Decide which default plugin ID to use if (isTv) { movieDb = TheTvDBPlugin.THETVDB_PLUGIN_ID; } else { movieDb = ImdbPlugin.IMDB_PLUGIN_ID; } } setMovieId(movie, movieDb, eId.getTextContent()); // Process the TMDB id setMovieId(movie, TheMovieDbPlugin.TMDB_PLUGIN_ID, eId.getAttribute("TMDB")); } } } private static void setMovieId(Movie movie, final String movieDb, final String value) { if (StringTools.isValidString(movieDb) && StringTools.isValidString(value)) { LOG.debug("Found {} ID: {}", movieDb, value); movie.setId(movieDb, value); } } /** * Parse all the title information from the XML NFO file * * @param eCommon * @param movie */ private static void parseTitle(Element eCommon, Movie movie) { if (eCommon == null) { return; } // Determine title elements String titleMain = DOMHelper.getValueFromElement(eCommon, "title"); String titleSort = DOMHelper.getValueFromElement(eCommon, "sorttitle", "sortTitle"); String titleOrig = DOMHelper.getValueFromElement(eCommon, "originaltitle", "originalTitle"); if (OverrideTools.checkOverwriteOriginalTitle(movie, NFO_PLUGIN_ID)) { movie.setOriginalTitle(titleOrig, NFO_PLUGIN_ID); } if (OverrideTools.checkOverwriteTitle(movie, NFO_PLUGIN_ID)) { movie.setTitle(titleMain, NFO_PLUGIN_ID); } // Set the title sort if (isValidString(titleSort)) { movie.setTitleSort(titleSort); } } /** * Parse the year from the XML NFO file * * @param tempYear * @param movie * @return */ private static boolean parseYear(String tempYear, Movie movie) { // START year if (StringUtils.isNumeric(tempYear) && tempYear.length() == 4) { if (OverrideTools.checkOverwriteYear(movie, NFO_PLUGIN_ID)) { movie.setYear(tempYear, NFO_PLUGIN_ID); } return Boolean.TRUE; } if (StringUtils.isBlank(tempYear)) { // The year is blank, so skip it. return Boolean.TRUE; } return Boolean.FALSE; } }