false, 'Logger' => null, 'ReleaseImage' => null, 'Settings' => null, 'TMDb' => null, ]; $options += $defaults; $this->pdo = ($options['Settings'] instanceof DB ? $options['Settings'] : new DB()); $this->releaseImage = ($options['ReleaseImage'] instanceof ReleaseImage ? $options['ReleaseImage'] : new ReleaseImage($this->pdo)); $this->client = new Client(); $this->tmdbtoken = new ApiToken(Settings::settingValue('APIs..tmdbkey')); $this->tmdbclient = new TmdbClient( $this->tmdbtoken, [ 'cache' => [ 'enabled' => false, ], ] ); $this->fanartapikey = Settings::settingValue('APIs..fanarttvkey'); $this->fanart = new FanartTV($this->fanartapikey); $this->omdbapikey = Settings::settingValue('APIs..omdbkey'); $this->lookuplanguage = Settings::settingValue('indexer.categorise.imdblanguage') !== '' ? (string) Settings::settingValue('indexer.categorise.imdblanguage') : 'en'; $this->imdburl = Settings::settingValue('indexer.categorise.imdburl') === 0 ? false : true; $this->movieqty = Settings::settingValue('..maximdbprocessed') !== '' ? Settings::settingValue('..maximdbprocessed') : 100; $this->searchEngines = true; $this->showPasswords = Releases::showPasswords(); $this->debug = NN_DEBUG; $this->echooutput = ($options['Echo'] && NN_ECHOCLI && $this->pdo->cli); $this->imgSavePath = NN_COVERS.'movies'.DS; $this->service = ''; $this->catWhere = 'PARTITION (movies)'; if (NN_DEBUG || NN_LOGGING) { $this->debug = true; try { $this->debugging = new Logger(); } catch (LoggerException $error) { $this->_debug = false; } } } /** * @param $imdbId * @return array|bool|\Illuminate\Database\Eloquent\Model|null|static */ public function getMovieInfo($imdbId) { return MovieInfo::query()->where('imdbid', $imdbId)->first(); } /** * Get info for multiple IMDB id's. * * @param array $imdbIDs * * @return array */ public function getMovieInfoMultiImdb($imdbIDs): array { return $this->pdo->query( sprintf( ' SELECT DISTINCT movieinfo.*, releases.imdbid AS relimdb FROM movieinfo LEFT OUTER JOIN releases ON releases.imdbid = movieinfo.imdbid WHERE movieinfo.imdbid IN (%s)', str_replace( ',,', ',', str_replace( ['(,', ' ,', ', )', ',)'], '', implode(',', $imdbIDs) ) ) ), true, NN_CACHE_EXPIRY_MEDIUM ); } /** * Get movie releases with covers for movie browse page. * * @param $cat * @param $start * @param $num * @param $orderBy * @param $maxAge * @param array $excludedCats * * @return array|bool|\PDOStatement */ public function getMovieRange($cat, $start, $num, $orderBy, $maxAge = -1, array $excludedCats = []) { $catsrch = ''; if (count($cat) > 0 && $cat[0] !== -1) { $catsrch = (new Category(['Settings' => $this->pdo]))->getCategorySearch($cat); } $order = $this->getMovieOrder($orderBy); $movies = $this->pdo->queryCalc( sprintf( " SELECT SQL_CALC_FOUND_ROWS m.imdbid, GROUP_CONCAT(r.id ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_id FROM movieinfo m LEFT JOIN releases r USING (imdbid) WHERE r.nzbstatus = 1 AND m.title != '' AND m.imdbid != '0000000' AND r.passwordstatus %s %s %s %s %s GROUP BY m.imdbid ORDER BY %s %s %s", $this->showPasswords, $this->getBrowseBy(), (! empty($catsrch) ? $catsrch : ''), ( $maxAge > 0 ? 'AND r.postdate > NOW() - INTERVAL '.$maxAge.'DAY ' : '' ), (count($excludedCats) > 0 ? ' AND r.categories_id NOT IN ('.implode(',', $excludedCats).')' : ''), $order[0], $order[1], ($start === false ? '' : ' LIMIT '.$num.' OFFSET '.$start) ), true, NN_CACHE_EXPIRY_MEDIUM ); $movieIDs = $releaseIDs = false; if (is_array($movies['result'])) { foreach ($movies['result'] as $movie => $id) { $movieIDs[] = $id['imdbid']; $releaseIDs[] = $id['grp_release_id']; } } $sql = sprintf( " SELECT GROUP_CONCAT(r.id ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_id, GROUP_CONCAT(r.rarinnerfilecount ORDER BY r.postdate DESC SEPARATOR ',') AS grp_rarinnerfilecount, GROUP_CONCAT(r.haspreview ORDER BY r.postdate DESC SEPARATOR ',') AS grp_haspreview, GROUP_CONCAT(r.passwordstatus ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_password, GROUP_CONCAT(r.guid ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_guid, GROUP_CONCAT(rn.releases_id ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_nfoid, GROUP_CONCAT(g.name ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_grpname, GROUP_CONCAT(r.searchname ORDER BY r.postdate DESC SEPARATOR '#') AS grp_release_name, GROUP_CONCAT(r.postdate ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_postdate, GROUP_CONCAT(r.size ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_size, GROUP_CONCAT(r.totalpart ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_totalparts, GROUP_CONCAT(r.comments ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_comments, GROUP_CONCAT(r.grabs ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_grabs, GROUP_CONCAT(df.failed ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_failed, GROUP_CONCAT(cp.title, ' > ', c.title ORDER BY r.postdate DESC SEPARATOR ',') AS grp_release_catname, m.*, g.name AS group_name, rn.releases_id AS nfoid FROM releases r LEFT OUTER JOIN groups g ON g.id = r.groups_id LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id LEFT OUTER JOIN categories c ON c.id = r.categories_id LEFT OUTER JOIN categories cp ON cp.id = c.parentid INNER JOIN movieinfo m ON m.imdbid = r.imdbid WHERE m.imdbid IN (%s) AND r.id IN (%s) %s GROUP BY m.imdbid ORDER BY %s %s", (is_array($movieIDs) ? implode(',', $movieIDs) : -1), (is_array($releaseIDs) ? implode(',', $releaseIDs) : -1), (! empty($catsrch) ? $catsrch : ''), $order[0], $order[1] ); $return = $this->pdo->query($sql, true, NN_CACHE_EXPIRY_MEDIUM); if (! empty($return)) { $return[0]['_totalcount'] = $movies['total'] ?? 0; } return $return; } /** * Get the order type the user requested on the movies page. * * @param $orderBy * * @return array */ protected function getMovieOrder($orderBy): array { $orderArr = explode('_', (($orderBy === '') ? 'MAX(r.postdate)' : $orderBy)); switch ($orderArr[0]) { case 'title': $orderField = 'm.title'; break; case 'year': $orderField = 'm.year'; break; case 'rating': $orderField = 'm.rating'; break; case 'posted': default: $orderField = 'MAX(r.postdate)'; break; } return [$orderField, isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1]) ? $orderArr[1] : 'desc']; } /** * Order types for movies page. * * @return array */ public function getMovieOrdering(): array { return ['title_asc', 'title_desc', 'year_asc', 'year_desc', 'rating_asc', 'rating_desc']; } /** * @return string */ protected function getBrowseBy(): string { $browseBy = ' '; $browseByArr = ['title', 'director', 'actors', 'genre', 'rating', 'year', 'imdb']; foreach ($browseByArr as $bb) { if (isset($_REQUEST[$bb]) && ! empty($_REQUEST[$bb])) { $bbv = stripslashes($_REQUEST[$bb]); if ($bb === 'rating') { $bbv .= '.'; } if ($bb === 'imdb') { $browseBy .= sprintf('AND m.%sid = %d', $bb, $bbv); } else { $browseBy .= 'AND m.'.$bb.' '.$this->pdo->likeString($bbv, true, true); } } } return $browseBy; } /** * @var null|TraktTv */ public $traktTv = null; /** * @var OMDbAPI|null */ public $omdbApi = null; /** * Get trailer using IMDB Id. * * @param int $imdbID * * @return bool|string * @throws \Exception */ public function getTrailer($imdbID) { if (! is_numeric($imdbID)) { return false; } $trailer = MovieInfo::query()->where('imdbid', $imdbID)->where('trailer', '!=', '')->first(['trailer']); if ($trailer !== null) { return $trailer['trailer']; } if ($this->traktTv === null) { $this->traktTv = new TraktTv(['Settings' => $this->pdo]); } $data = $this->traktTv->client->movieSummary('tt'.$imdbID, 'full'); if ($data) { $this->parseTraktTv($data); if (! empty($data['trailer'])) { return $data['trailer']; } } $trailer = Utility::imdb_trailers($imdbID); if ($trailer) { MovieInfo::query()->where('imdbid', $imdbID)->update(['trailer' => $trailer]); return $trailer; } return false; } /** * Parse trakt info, insert into DB. * * @param array $data * * @return mixed */ public function parseTraktTv(&$data) { if (empty($data['ids']['imdb'])) { return false; } if (! empty($data['trailer'])) { $data['trailer'] = str_ireplace( 'http://', 'https://', str_ireplace('watch?v=', 'embed/', $data['trailer']) ); return $data['trailer']; } $imdbid = (strpos($data['ids']['imdb'], 'tt') === 0) ? substr($data['ids']['imdb'], 2) : $data['ids']['imdb']; $cover = 0; if (is_file($this->imgSavePath.$imdbid).'-cover.jpg') { $cover = 1; } else { $link = $this->checkTraktValue($data['images']['poster']['thumb']); if ($link) { $cover = $this->releaseImage->saveImage($imdbid.'-cover', $link, $this->imgSavePath); } } $this->update([ 'genres' => $this->checkTraktValue($data['genres']), 'imdbid' => $this->checkTraktValue($imdbid), 'language' => $this->checkTraktValue($data['language']), 'plot' => $this->checkTraktValue($data['overview']), 'rating' => round($this->checkTraktValue($data['rating']), 1), 'tagline' => $this->checkTraktValue($data['tagline']), 'title' => $this->checkTraktValue($data['title']), 'tmdbid' => $this->checkTraktValue($data['ids']['tmdb']), 'trailer' => $this->checkTraktValue($data['trailer']), 'cover' => $cover, 'year' => $this->checkTraktValue($data['year']), ]); } /** * Checks if the value is set and not empty, returns it, else empty string. * * @param mixed $value * * @return string */ private function checkTraktValue($value): string { if (is_array($value) && ! empty($value)) { $temp = ''; foreach ($value as $val) { if (! is_array($val) && ! is_object($val)) { $temp .= (string) $val; } } $value = $temp; } return ! empty($value) ? $value : ''; } /** * Create click-able links to IMDB actors/genres/directors/etc.. * * @param $data * @param $field * * @return string */ public function makeFieldLinks($data, $field): string { if (! isset($data[$field]) || $data[$field] === '') { return ''; } $tmpArr = explode(', ', $data[$field]); $newArr = []; $i = 0; foreach ($tmpArr as $ta) { if (trim($ta) === '') { continue; } if ($i > 5) { break; } //only use first 6 $newArr[] = ''.$ta.''; $i++; } return implode(', ', $newArr); } /** * Get array of column keys, for inserting / updating. * * @return array */ public function getColumnKeys(): array { return [ 'actors', 'backdrop', 'cover', 'director', 'genre', 'imdbid', 'language', 'plot', 'rating', 'tagline', 'title', 'tmdbid', 'trailer', 'type', 'year', ]; } /** * Update movie on movie-edit page. * * @param array $values Array of keys/values to update. See $validKeys * * @return int|bool */ public function update(array $values) { if (! count($values)) { return false; } $validKeys = $this->getColumnKeys(); $query = [ '0' => 'INSERT INTO movieinfo (updated_at, created_at, ', '1' => ' VALUES (NOW(), NOW(), ', '2' => 'ON DUPLICATE KEY UPDATE updated_at = NOW(), ', ]; $found = 0; foreach ($values as $key => $value) { if (! empty($value) && in_array($key, $validKeys, false)) { $found++; $query[0] .= "$key, "; if (in_array($key, ['genre', 'language'], false)) { $value = substr($value, 0, 64); } $value = $this->pdo->escapeString($value); $query[1] .= "$value, "; $query[2] .= "$key = $value, "; } } if (! $found) { return false; } foreach ($query as $key => $value) { $query[$key] = rtrim($value, ', '); } return $this->pdo->queryInsert($query[0].') '.$query[1].') '.$query[2]); } /** * Check if a variable is set and not a empty string. * * @param $variable * * @return bool */ protected function checkVariable(&$variable): bool { return ! empty($variable) ? true : false; } /** * Returns a tmdb, imdb or trakt variable, the one that is set. Empty string if both not set. * * @param string $variable1 * @param string $variable2 * @param string $variable3 * * @return array|string */ protected function setVariables(&$variable1, &$variable2, &$variable3, &$variable4) { if ($this->checkVariable($variable1)) { return $variable1; } if ($this->checkVariable($variable2)) { return $variable2; } if ($this->checkVariable($variable3)) { return $variable3; } if ($this->checkVariable($variable4)) { return $variable4; } return ''; } /** * Fetch IMDB/TMDB/TRAKT info for the movie. * * @param $imdbId * * @return bool */ public function updateMovieInfo($imdbId): bool { if ($this->echooutput && $this->service !== '') { ColorCLI::doEcho(ColorCLI::primary('Fetching IMDB info from TMDB and/or Trakt using IMDB id: '.$imdbId)); } // Check TMDB for IMDB info. $tmdb = $this->fetchTMDBProperties($imdbId); // Check IMDB for movie info. $imdb = $this->fetchIMDBProperties($imdbId); // Check TRAKT for movie info $trakt = $this->fetchTraktTVProperties($imdbId); // Check OMDb for movie info $omdb = $this->fetchOmdbAPIProperties($imdbId); if (! $imdb && ! $tmdb && ! $trakt && ! $omdb) { return false; } // Check FanArt.tv for cover and background images. $fanart = $this->fetchFanartTVProperties($imdbId); $mov = []; $mov['cover'] = $mov['backdrop'] = $mov['banner'] = $movieID = 0; $mov['type'] = $mov['director'] = $mov['actors'] = $mov['language'] = ''; $mov['imdbid'] = $imdbId; $mov['tmdbid'] = (! isset($tmdb['tmdbid']) || $tmdb['tmdbid'] === '') ? 0 : $tmdb['tmdbid']; // Prefer Fanart.tv cover over TMDB,TMDB over IMDB and IMDB over OMDB. if ($this->checkVariable($fanart['cover'])) { $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $fanart['cover'], $this->imgSavePath); } elseif ($this->checkVariable($tmdb['cover'])) { $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $tmdb['cover'], $this->imgSavePath); } elseif ($this->checkVariable($imdb['cover'])) { $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $imdb['cover'], $this->imgSavePath); } elseif ($this->checkVariable($omdb['cover'])) { $mov['cover'] = $this->releaseImage->saveImage($imdbId.'-cover', $omdb['cover'], $this->imgSavePath); } // Backdrops. if ($this->checkVariable($fanart['backdrop'])) { $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $fanart['backdrop'], $this->imgSavePath, 1920, 1024); } elseif ($this->checkVariable($tmdb['backdrop'])) { $mov['backdrop'] = $this->releaseImage->saveImage($imdbId.'-backdrop', $tmdb['backdrop'], $this->imgSavePath, 1920, 1024); } // Banner if ($this->checkVariable($fanart['banner'])) { $mov['banner'] = $this->releaseImage->saveImage($imdbId.'-banner', $fanart['banner'], $this->imgSavePath); } //RottenTomatoes rating from OmdbAPI if ($this->checkVariable($omdb['rtRating'])) { $mov['rtrating'] = $omdb['rtRating']; } $mov['title'] = $this->setVariables($imdb['title'], $tmdb['title'], $trakt['title'], $omdb['title']); $mov['rating'] = $this->setVariables($imdb['rating'], $tmdb['rating'], $trakt['rating'], $omdb['rating']); $mov['plot'] = $this->setVariables($imdb['plot'], $tmdb['plot'], $trakt['overview'], $omdb['plot']); $mov['tagline'] = $this->setVariables($imdb['tagline'], $tmdb['tagline'], $trakt['tagline'], $omdb['tagline']); $mov['year'] = $this->setVariables($imdb['year'], $tmdb['year'], $trakt['year'], $omdb['year']); $mov['genre'] = $this->setVariables($imdb['genre'], $tmdb['genre'], $trakt['genres'], $omdb['genre']); if ($this->checkVariable($imdb['type'])) { $mov['type'] = $imdb['type']; } if ($this->checkVariable($imdb['director'])) { $mov['director'] = is_array($imdb['director']) ? implode(', ', array_unique($imdb['director'])) : $imdb['director']; } elseif ($this->checkVariable($omdb['director'])) { $mov['director'] = is_array($omdb['director']) ? implode(', ', array_unique($omdb['director'])) : $omdb['director']; } if ($this->checkVariable($imdb['actors'])) { $mov['actors'] = is_array($imdb['actors']) ? implode(', ', array_unique($imdb['actors'])) : $imdb['actors']; } elseif ($this->checkVariable($omdb['actors'])) { $mov['actors'] = is_array($omdb['actors']) ? implode(', ', array_unique($omdb['actors'])) : $omdb['actors']; } if ($this->checkVariable($imdb['language'])) { $mov['language'] = is_array($imdb['language']) ? implode(', ', array_unique($imdb['language'])) : $imdb['language']; } elseif ($this->checkVariable($omdb['language'])) { $mov['language'] = is_array($imdb['language']) ? implode(', ', array_unique($omdb['language'])) : $omdb['language']; } if (is_array($mov['genre'])) { $mov['genre'] = implode(', ', array_unique($mov['genre'])); } if (is_array($mov['type'])) { $mov['type'] = implode(', ', array_unique($mov['type'])); } $mov['title'] = html_entity_decode($mov['title'], ENT_QUOTES, 'UTF-8'); $mov['title'] = str_replace(['/', '\\'], '', $mov['title']); $movieID = $this->update([ 'actors' => html_entity_decode($mov['actors'], ENT_QUOTES, 'UTF-8'), 'backdrop' => $mov['backdrop'], 'cover' => $mov['cover'], 'director' => html_entity_decode($mov['director'], ENT_QUOTES, 'UTF-8'), 'genre' => html_entity_decode($mov['genre'], ENT_QUOTES, 'UTF-8'), 'imdbid' => $mov['imdbid'], 'language' => html_entity_decode($mov['language'], ENT_QUOTES, 'UTF-8'), 'plot' => html_entity_decode(preg_replace('/\s+See full summary ยป/', ' ', $mov['plot']), ENT_QUOTES, 'UTF-8'), 'rating' => round($mov['rating'], 1), 'rtrating' => $mov['rtrating'] ?? 'N/A', 'tagline' => html_entity_decode($mov['tagline'], ENT_QUOTES, 'UTF-8'), 'title' => $mov['title'], 'tmdbid' => $mov['tmdbid'], 'type' => html_entity_decode(ucwords(preg_replace('/[\.\_]/', ' ', $mov['type'])), ENT_QUOTES, 'UTF-8'), 'year' => $mov['year'], ]); if ($this->echooutput && $this->service !== '') { ColorCLI::doEcho( ColorCLI::headerOver(($movieID !== 0 ? 'Added/updated movie: ' : 'Nothing to update for movie: ')). ColorCLI::primary( $mov['title']. ' ('. $mov['year']. ') - '. $mov['imdbid'] ) ); } return $movieID !== 0; } /** * Fetch FanArt.tv backdrop / cover / title. * * @param $imdbId * * @return bool|array */ protected function fetchFanartTVProperties($imdbId) { if ($this->fanartapikey !== '') { $art = $this->fanart->getMovieFanart('tt'.$imdbId); if (isset($art) && $art !== false) { if (isset($art['status']) && $art['status'] === 'error') { return false; } $ret = []; if ($this->checkVariable($art['moviebackground'][0]['url'])) { $ret['backdrop'] = $art['moviebackground'][0]['url']; } elseif ($this->checkVariable($art['moviethumb'][0]['url'])) { $ret['backdrop'] = $art['moviethumb'][0]['url']; } if ($this->checkVariable($art['movieposter'][0]['url'])) { $ret['cover'] = $art['movieposter'][0]['url']; } if ($this->checkVariable($art['moviebanner'][0]['url'])) { $ret['banner'] = $art['moviebanner'][0]['url']; } if (isset($ret['backdrop'], $ret['cover'])) { $ret['title'] = $imdbId; if (isset($art['name'])) { $ret['title'] = $art['name']; } if ($this->echooutput) { ColorCLI::doEcho(ColorCLI::alternateOver('Fanart Found ').ColorCLI::headerOver($ret['title']), true); } return $ret; } } } return false; } /** * Fetch info for IMDB id from TMDB. * * @param $imdbId * @param bool $text * * @return array|bool */ public function fetchTMDBProperties($imdbId, $text = false) { $lookupId = ($text === false ? 'tt'.$imdbId : $imdbId); try { $tmdbLookup = $this->tmdbclient->getMoviesApi()->getMovie($lookupId); } catch (TmdbApiException $e) { return false; } $ret = []; $ret['title'] = $tmdbLookup['original_title']; if ($this->currentTitle !== '') { // Check the similarity. similar_text($this->currentTitle, $ret['title'], $percent); if ($percent < 40) { if ($this->debug) { $this->debugging->log( __CLASS__, __FUNCTION__, 'Found ('. $ret['title']. ') from TMDB, but it\'s only '. $percent. '% similar to ('. $this->currentTitle.')', Logger::LOG_INFO ); } return false; } } $ret['tmdbid'] = $tmdbLookup['id']; $ImdbID = str_replace('tt', '', $tmdbLookup['imdb_id']); $ret['imdb_id'] = $ImdbID; $vote = $tmdbLookup['vote_average']; if (isset($vote)) { $ret['rating'] = ($vote === 0) ? '' : $vote; } $overview = $tmdbLookup['overview']; if (! empty($overview)) { $ret['plot'] = $overview; } $tagline = $tmdbLookup['tagline']; if (! empty($tagline)) { $ret['tagline'] = $tagline; } $released = $tmdbLookup['release_date']; if (! empty($released)) { $ret['year'] = date('Y', strtotime($released)); } $genresa = $tmdbLookup['genres']; if (! empty($genresa) && count($genresa) > 0) { $genres = []; foreach ($genresa as $genre) { $genres[] = $genre['name']; } $ret['genre'] = $genres; } $posterp = $tmdbLookup['poster_path']; if (! empty($posterp)) { $ret['cover'] = 'http://image.tmdb.org/t/p/w185'.$posterp; } $backdrop = $tmdbLookup['backdrop_path']; if (! empty($backdrop)) { $ret['backdrop'] = 'http://image.tmdb.org/t/p/original'.$backdrop; } if ($this->echooutput) { ColorCLI::doEcho(ColorCLI::primaryOver('TMDb Found ').ColorCLI::headerOver($ret['title']), true); } return $ret; } /** * @param $imdbId * * @return array|bool * @throws \Exception */ protected function fetchIMDBProperties($imdbId) { $imdb_regex = [ 'title' => '/(.*?)\s?\(.*?<\/title>/i', 'tagline' => '/taglines:<\/h4>\s([^<]+)/i', 'plot' => '/<p itemprop="description">\s*?(.*?)\s*?<\/p>/i', 'rating' => '/"ratingValue">([\d.]+)<\/span>/i', 'year' => '/<title>.*?\(.*?(\d{4}).*?<\/title>/i', 'cover' => '/<link rel=\'image_src\' href="(http:\/\/ia\.media-imdb\.com.+\.jpg)">/', ]; $imdb_regex_multi = [ 'genre' => '/href="\/genre\/(.*?)\?/i', 'language' => '/<a href="\/language\/.+?\'url\'>(.+?)<\/a>/s', 'type' => '/<meta property=\'og\:type\' content=\"(.+)\" \/>/i', ]; try { $buffer = $this->client->get( 'http://'.($this->imdburl === false ? 'www' : 'akas').'.imdb.com/title/tt'.$imdbId.'/', ['headers' => [ 'Accept-Language' => (Settings::settingValue('indexer.categorise.imdblanguage') !== '') ? Settings::settingValue('indexer.categorise.imdblanguage') : 'en', 'useragent' => 'Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) '. 'Version/4.0.4 Mobile/7B334b Safari/531.21.102011-10-16 20:23:10', 'foo=bar', ], ] )->getBody()->getContents(); } catch (RequestException $e) { if ($e->hasResponse()) { if ($e->getCode() === 404) { ColorCLI::doEcho(ColorCLI::notice('Data not available on IMDB server')); } elseif ($e->getCode() === 503) { ColorCLI::doEcho(ColorCLI::notice('IMDB service unavailable')); } else { ColorCLI::doEcho(ColorCLI::notice('Unable to fetch data from IMDB, http error reported: '.$e->getCode())); } } } catch (\RuntimeException $e) { ColorCLI::doEcho(ColorCLI::notice('Runtime error: '.$e->getCode())); } if (isset($buffer) && $buffer !== false) { $ret = []; foreach ($imdb_regex as $field => $regex) { if (preg_match($regex, $buffer, $matches)) { $match = $matches[1]; $match1 = strip_tags(trim(rtrim($match))); $ret[$field] = $match1; } } $matches = []; foreach ($imdb_regex_multi as $field => $regex) { if (preg_match_all($regex, $buffer, $matches)) { $match2 = $matches[1]; $match3 = array_map('trim', $match2); $ret[$field] = $match3; } } if ($this->currentTitle !== '' && isset($ret['title'])) { // Check the similarity. similar_text($this->currentTitle, $ret['title'], $percent); if ($percent < 40) { if ($this->debug) { $this->debugging->log( __CLASS__, __FUNCTION__, 'Found ('. $ret['title']. ') from IMDB, but it\'s only '. $percent. '% similar to ('. $this->currentTitle.')', Logger::LOG_INFO ); } return false; } } // Actors. if (preg_match('/<table class="cast_list">(.+?)<\/table>/s', $buffer, $hit)) { if (preg_match_all('/<span class="itemprop" itemprop="name">\s*(.+?)\s*<\/span>/i', $hit[0], $results, PREG_PATTERN_ORDER)) { $ret['actors'] = $results[1]; } } // Directors. if (preg_match('/itemprop="directors?".+?<\/div>/s', $buffer, $hit)) { if (preg_match_all('/"name">(.*?)<\/span>/is', $hit[0], $results, PREG_PATTERN_ORDER)) { $ret['director'] = $results[1]; } } if ($this->echooutput && isset($ret['title'])) { ColorCLI::doEcho(ColorCLI::headerOver('IMDb Found ').ColorCLI::primaryOver($ret['title']), true); } return $ret; } return false; } /** * Fetch TraktTV backdrop / cover / title. * * @param $imdbId * * @return bool|array */ protected function fetchTraktTVProperties($imdbId) { if ($this->traktTv === null) { $this->traktTv = new TraktTv(['Settings' => $this->pdo]); } $resp = $this->traktTv->client->movieSummary('tt'.$imdbId, 'full'); if ($resp !== false) { $ret = []; if (isset($resp['ids']['trakt'])) { $ret['id'] = $resp['ids']['trakt']; } if (isset($resp['title'])) { $ret['title'] = $resp['title']; } else { return false; } if ($this->echooutput) { ColorCLI::doEcho(ColorCLI::alternateOver('Trakt Found ').ColorCLI::headerOver($ret['title']), true); } return $ret; } return false; } /** * Fetch OMDb backdrop / cover / title. * * @param $imdbId * * @return bool|array */ protected function fetchOmdbAPIProperties($imdbId) { if ($this->omdbapikey !== '' && $this->omdbApi === null) { $this->omdbApi = new OMDbAPI($this->omdbapikey); $resp = $this->omdbApi->fetch('i', 'tt'.$imdbId); if (is_object($resp) && $resp->message === 'OK' && $resp->data->Response !== 'False') { $ret = [ 'title' => ! empty($resp->data->Title) ? $resp->data->Title : '', 'cover' => ! empty($resp->data->Poster) ? $resp->data->Poster : '', 'genre' => ! empty($resp->data->Genre) ? $resp->data->Genre : '', 'year' => ! empty($resp->data->Year) ? $resp->data->Year : '', 'plot' => ! empty($resp->data->Plot) ? $resp->data->Plot : '', 'rating' => ! empty($resp->data->imdbRating) ? $resp->data->imdbRating : '', 'rtRating' => ! empty($resp->data->Ratings[1]->Value) ? $resp->data->Ratings[1]->Value : '', 'tagline' => ! empty($resp->data->Tagline) ? $resp->data->Tagline : '', 'director' => ! empty($resp->data->Director) ? $resp->data->Director : '', 'actors' => ! empty($resp->data->Actors) ? $resp->data->Actors : '', 'language' => ! empty($resp->data->Language) ? $resp->data->Language : '', 'boxOffice' => ! empty($resp->data->BoxOffice) ? $resp->data->BoxOffice : '', ]; if ($this->echooutput) { ColorCLI::doEcho(ColorCLI::alternateOver('OMDbAPI Found ').ColorCLI::headerOver($ret['title']), true); } return $ret; } return false; } return false; } /** * Update a release with a IMDB id. * * @param string $buffer Data to parse a IMDB id/Trakt Id from. * @param string $service Method that called this method. * @param int $id id of the release. * @param int $processImdb To get IMDB info on this IMDB id or not. * * @return string */ public function doMovieUpdate($buffer, $service, $id, $processImdb = 1): string { $imdbID = false; if (is_string($buffer) && preg_match('/(?:imdb.*?)?(?:tt|Title\?)(?P<imdbid>\d{5,7})/i', $buffer, $matches)) { $imdbID = $matches['imdbid']; } if ($imdbID !== false) { $this->service = $service; if ($this->echooutput && $this->service !== '') { ColorCLI::doEcho(ColorCLI::headerOver($service.' found IMDBid: ').ColorCLI::primary('tt'.$imdbID)); } $this->pdo->queryExec(sprintf('UPDATE releases SET imdbid = %s WHERE id = %d', $this->pdo->escapeString($imdbID), $id)); // If set, scan for imdb info. if ($processImdb === 1) { $movCheck = $this->getMovieInfo($imdbID); if ($movCheck === false || (isset($movCheck['updated_at']) && (time() - strtotime($movCheck['updated_at'])) > 2592000)) { if ($this->updateMovieInfo($imdbID) === false) { $this->pdo->queryExec(sprintf('UPDATE releases %s SET imdbid = 0000000 WHERE id = %d', $this->catWhere, $id)); } } } } return $imdbID; } /** * Process releases with no IMDB id's. * * @param string $groupID (Optional) id of a group to work on. * @param string $guidChar (Optional) First letter of a release GUID to use to get work. * @param int $lookupIMDB (Optional) 0 Don't lookup IMDB, 1 lookup IMDB, 2 lookup IMDB on releases that were renamed. */ public function processMovieReleases($groupID = '', $guidChar = '', $lookupIMDB = 1): void { if ($lookupIMDB === 0) { return; } // Get all releases without an IMDB id. $res = $this->pdo->query( sprintf( ' SELECT searchname, id FROM releases %s WHERE imdbid IS NULL AND nzbstatus = 1 %s %s %s LIMIT %d', $this->catWhere, ($groupID === '' ? '' : ('AND groups_id = '.$groupID)), ($guidChar === '' ? '' : 'AND leftguid = '.$this->pdo->escapeString($guidChar)), ($lookupIMDB === 2 ? 'AND isrenamed = 1' : ''), $this->movieqty ) ); $movieCount = count($res); if ($movieCount > 0) { if ($this->traktTv === null) { $this->traktTv = new TraktTv(['Settings' => $this->pdo]); } if ($this->echooutput && $movieCount > 1) { ColorCLI::doEcho(ColorCLI::header('Processing '.$movieCount.' movie releases.')); } // Loop over releases. foreach ($res as $arr) { // Try to get a name/year. if ($this->parseMovieSearchName($arr['searchname']) === false) { //We didn't find a name, so set to all 0's so we don't parse again. $this->pdo->queryExec(sprintf('UPDATE releases %s SET imdbid = 0000000 WHERE id = %d', $this->catWhere, $arr['id'])); continue; } $this->currentRelID = $arr['id']; $movieName = $this->currentTitle; if ($this->currentYear !== false) { $movieName .= ' ('.$this->currentYear.')'; } if ($this->echooutput) { ColorCLI::doEcho(ColorCLI::primaryOver('Looking up: ').ColorCLI::headerOver($movieName), true); } $movieUpdated = false; // Check local DB. $getIMDBid = $this->localIMDBsearch(); if ($getIMDBid !== false) { $imdbID = $this->doMovieUpdate('tt'.$getIMDBid, 'Local DB', $arr['id']); if ($imdbID !== false) { $movieUpdated = true; } } // Check OMDbAPI if ($movieUpdated === false) { $omdbTitle = strtolower(str_replace(' ', '_', $this->currentTitle)); if ($this->omdbapikey !== '' && $this->omdbApi === null) { $this->omdbApi = new OMDbAPI($this->omdbapikey); $buffer = $this->omdbApi->search($omdbTitle, 'movie'); if (is_object($buffer) && $buffer->message === 'OK' && $buffer->data->Response !== 'False') { $getIMDBid = $buffer->data->Search[0]->imdbID; if (! empty($getIMDBid)) { $imdbID = $this->doMovieUpdate($getIMDBid, 'OMDbAPI', $arr['id']); if ($imdbID !== false) { $movieUpdated = true; } } } } } // Check on Trakt. if ($movieUpdated === false) { $data = $this->traktTv->client->movieSummary($movieName, 'full'); if ($data !== false) { $this->parseTraktTv($data); if (! empty($data['ids']['imdb'])) { $imdbID = $this->doMovieUpdate($data['ids']['imdb'], 'Trakt', $arr['id']); if ($imdbID !== false) { $movieUpdated = true; } } } } // Try on search engines. if ($movieUpdated === false) { if ($this->searchEngines && $this->currentYear !== false) { if ($this->imdbIDFromEngines() === true) { $movieUpdated = true; } } } // We failed to get an IMDB id from all sources. if ($movieUpdated === false) { $this->pdo->queryExec(sprintf('UPDATE releases %s SET imdbid = 0000000 WHERE id = %d', $this->catWhere, $arr['id'])); } } } } /** * Try to fetch an IMDB id locally. * * @return int|bool Int, the imdbid when true, Bool when false. */ protected function localIMDBsearch() { $query = 'SELECT imdbid FROM movieinfo'; $andYearIn = ''; //If we found a year, try looking in a 4 year range. if ($this->currentYear !== false) { $start = (int) $this->currentYear - 2; $end = (int) $this->currentYear + 2; $andYearIn = 'AND year IN ('; while ($start < $end) { $andYearIn .= $start.','; $start++; } $andYearIn .= $end.')'; } $IMDBCheck = $this->pdo->queryOneRow( sprintf('%s WHERE title %s %s', $query, $this->pdo->likeString($this->currentTitle), $andYearIn) ); // Look by %word%word%word% etc.. if ($IMDBCheck === false) { $pieces = explode(' ', $this->currentTitle); $tempTitle = '%'; foreach ($pieces as $piece) { $tempTitle .= str_replace(["'", '!', '"'], '', $piece).'%'; } $IMDBCheck = $this->pdo->queryOneRow( sprintf( "%s WHERE replace(replace(title, \"'\", ''), '!', '') %s %s", $query, $this->pdo->likeString($tempTitle), $andYearIn ) ); } // Try replacing er with re ? if ($IMDBCheck === false) { $tempTitle = str_replace('er', 're', $this->currentTitle); if ($tempTitle !== $this->currentTitle) { $IMDBCheck = $this->pdo->queryOneRow( sprintf( '%s WHERE title %s %s', $query, $this->pdo->likeString($tempTitle), $andYearIn ) ); // Final check if everything else failed. if ($IMDBCheck === false) { $pieces = explode(' ', $tempTitle); $tempTitle = '%'; foreach ($pieces as $piece) { $tempTitle .= str_replace(["'", '!', '"'], '', $piece).'%'; } $IMDBCheck = $this->pdo->queryOneRow( sprintf( "%s WHERE replace(replace(replace(title, \"'\", ''), '!', ''), '\"', '') %s %s", $query, $this->pdo->likeString($tempTitle), $andYearIn ) ); } } } return $IMDBCheck === false ? false : ( is_numeric($IMDBCheck['imdbid']) ? (int) $IMDBCheck['imdbid'] : false ); } /** * Try to get an IMDB id from search engines. * * @return bool */ protected function imdbIDFromEngines(): bool { if ($this->googleLimit < 41 && (time() - $this->googleBan) > 600) { if ($this->googleSearch() === true) { return true; } } if ($this->yahooLimit < 41 && $this->yahooSearch() === true) { return true; } return false; } /** * Try to find a IMDB id on google.com. * * @return bool */ protected function googleSearch(): bool { try { $buffer = $this->client->get( 'https://www.google.com/search?hl=en&as_q=&as_epq='. urlencode( $this->currentTitle. ' '. $this->currentYear ). '&as_oq=&as_eq=&as_nlo=&as_nhi=&lr=&cr=&as_qdr=all&as_sitesearch='. urlencode('www.imdb.com/title/'). '&as_occt=title&safe=images&tbs=&as_filetype=&as_rights=' )->getBody()->getContents(); } catch (RequestException $e) { if ($e->hasResponse()) { if ($e->getCode() === 404) { ColorCLI::doEcho(ColorCLI::notice('Data not available on Google search')); } elseif ($e->getCode() === 503) { ColorCLI::doEcho(ColorCLI::notice('Google service unavailable')); } else { ColorCLI::doEcho(ColorCLI::notice('Unable to fetch data from Google, http error reported: '.$e->getCode())); } } } catch (\RuntimeException $e) { ColorCLI::doEcho(ColorCLI::notice('Runtime error: '.$e->getCode())); } // Make sure we got some data. if (! empty($buffer)) { $this->googleLimit++; if (preg_match('/(To continue, please type the characters below)|(- did not match any documents\.)/i', $buffer, $matches)) { if (! empty($matches[1])) { $this->googleBan = time(); } } elseif ($this->doMovieUpdate($buffer, 'Google.com', $this->currentRelID) !== false) { return true; } } return false; } /** * Try to find a IMDB id on bing.com. * * @return bool */ protected function bingSearch(): bool { try { $buffer = $this->client->get( 'http://www.bing.com/search?q='. urlencode( '("'. $this->currentTitle. '" and "'. $this->currentYear. '") site:www.imdb.com/title/' ). '&qs=n&form=QBLH&filt=all' )->getBody()->getContents(); } catch (RequestException $e) { if ($e->hasResponse()) { if ($e->getCode() === 404) { ColorCLI::doEcho(ColorCLI::notice('Data not available on Bing search')); } elseif ($e->getCode() === 503) { ColorCLI::doEcho(ColorCLI::notice('Bing search service unavailable')); } else { ColorCLI::doEcho(ColorCLI::notice('Unable to fetch data from Bing search , http error reported: '.$e->getCode())); } } } catch (\RuntimeException $e) { ColorCLI::doEcho(ColorCLI::notice('Runtime error: '.$e->getCode())); } if (! empty($buffer)) { $this->bingLimit++; if ($this->doMovieUpdate($buffer, 'Bing.com', $this->currentRelID) !== false) { return true; } } return false; } /** * Try to find a IMDB id on yahoo.com. * * @return bool */ protected function yahooSearch(): bool { try { $buffer = $this->client->get( 'http://search.yahoo.com/search?n=10&ei=UTF-8&va_vt=title&vo_vt=any&ve_vt=any&vp_vt=any&vf=all&vm=p&fl=0&fr=fp-top&p='. urlencode( ''. implode( '+', explode( ' ', preg_replace( '/\s+/', ' ', preg_replace( '/\W/', ' ', $this->currentTitle ) ) ) ). '+'. $this->currentYear ). '&vs='. urlencode('www.imdb.com/title/') )->getBody()->getContents(); } catch (RequestException $e) { if ($e->hasResponse()) { if ($e->getCode() === 404) { ColorCLI::doEcho(ColorCLI::notice('Data not available on Yahoo search')); } elseif ($e->getCode() === 503) { ColorCLI::doEcho(ColorCLI::notice('Yahoo search service unavailable')); } else { ColorCLI::doEcho(ColorCLI::notice('Unable to fetch data from Yahoo search, http error reported: '.$e->getCode())); } } } catch (\RuntimeException $e) { ColorCLI::doEcho(ColorCLI::notice('Runtime error: '.$e->getCode())); } if (! empty($buffer)) { $this->yahooLimit++; if ($this->doMovieUpdate($buffer, 'Yahoo.com', $this->currentRelID) !== false) { return true; } } return false; } /** * Parse a movie name from a release search name. * * @param string $releaseName * * @return bool */ protected function parseMovieSearchName($releaseName): bool { $name = $year = ''; $followingList = '[^\w]((1080|480|720)p|AC3D|Directors([^\w]CUT)?|DD5\.1|(DVD|BD|BR)(Rip)?|BluRay|divx|HDTV|iNTERNAL|LiMiTED|(Real\.)?Proper|RE(pack|Rip)|Sub\.?(fix|pack)|Unrated|WEB-DL|(x|H)[-._ ]?264|xvid)[^\w]'; /* Initial scan of getting a year/name. * [\w. -]+ Gets 0-9a-z. - characters, most scene movie titles contain these chars. * ie: [61420]-[FULL]-[a.b.foreignEFNet]-[ Coraline.2009.DUTCH.INTERNAL.1080p.BluRay.x264-VeDeTT ]-[21/85] - "vedett-coralien-1080p.r04" yEnc * Then we look up the year, (19|20)\d\d, so $matches[1] would be Coraline $matches[2] 2009 */ if (preg_match('/(?P<name>[\w. -]+)[^\w](?P<year>(19|20)\d\d)/i', $releaseName, $matches)) { $name = $matches['name']; $year = $matches['year']; /* If we didn't find a year, try to get a name anyways. * Try to look for a title before the $followingList and after anything but a-z0-9 two times or more (-[ for example) */ } elseif (preg_match('/([^\w]{2,})?(?P<name>[\w .-]+?)'.$followingList.'/i', $releaseName, $matches)) { $name = $matches['name']; } // Check if we got something. if ($name !== '') { // If we still have any of the words in $followingList, remove them. $name = preg_replace('/'.$followingList.'/i', ' ', $name); // Remove periods, underscored, anything between parenthesis. $name = preg_replace('/\(.*?\)|[._]/i', ' ', $name); // Finally remove multiple spaces and trim leading spaces. $name = trim(preg_replace('/\s{2,}/', ' ', $name)); // Check if the name is long enough and not just numbers. if (strlen($name) > 4 && ! preg_match('/^\d+$/', $name)) { if ($this->debug && $this->echooutput) { ColorCLI::doEcho("DB name: {$releaseName}", true); } $this->currentTitle = $name; $this->currentYear = ($year === '' ? false : $year); return true; } } return false; } /** * Get IMDB genres. * * @return array */ public function getGenres(): array { return [ 'Action', 'Adventure', 'Animation', 'Biography', 'Comedy', 'Crime', 'Documentary', 'Drama', 'Family', 'Fantasy', 'Film-Noir', 'Game-Show', 'History', 'Horror', 'Music', 'Musical', 'Mystery', 'News', 'Reality-TV', 'Romance', 'Sci-Fi', 'Sport', 'Talk-Show', 'Thriller', 'War', 'Western', ]; } }