Animarr/src/Animarr/Downloader.php

405 lines
15 KiB
PHP
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Animarr;
use Animarr\Extractor\SceneExtractor;
use Animarr\Release\MultiRelease;
use Animarr\Release\Release;
use Animarr\Source\Source;
use Animarr\Torrent\Torrent;
class Downloader{
private $db;
private $extractor;
private $download = [];
private $ignore = [];
private $useLink = false;
public function __construct(Database $db){
$this->log("Starting up...");
$this->extractor = new SceneExtractor($db->getAniDB());
$this->db = $db;
$this->log("Done!");
}
public static function log($log, $type = "INFO"){
if(defined("NO_LOG") and NO_LOG){
return;
}
$log = "[$type] > $log" . PHP_EOL;
echo $log;
file_put_contents(Database::getConfigKey("log.path"), $log, FILE_APPEND);
}
public static function debug($log){
self::log($log, "DEBUG");
}
public function setUseLink($val){
$this->useLink = (bool) $val;
}
/**
* @return Release[]
*/
public function getDownloads(){
return $this->download;
}
public function getAutoAddFolder($url){
return Torrent::isPrivateTorrent($url) ? Database::getConfigKey("torrent.folder.private.blackhole", "./AutoAdd") : Database::getConfigKey("torrent.folder.blackhole", "./AutoAdd");
}
public function handleNewDownloads(){
foreach($this->getDownloads() as $r){
if(strpos($r->getDownloadLink(), "urn:btih:") !== false){
//Magnet
file_put_contents($this->getAutoAddFolder($r->getDownloadLink()) . "/" . $r->getId() . ".magnet", $r->getDownloadLink() . PHP_EOL);
}else{
file_put_contents($this->getAutoAddFolder($r->getDownloadLink()) . "/" . $r->getId() . ".torrent", Torrent::getTorrent($r->getDownloadLink(), Database::getConfigKey("torrent.cache.folder", null)));
}
$this->log("Downloaded \"".$r->getOriginalTitle()."\"", "FETCH");
}
$this->download = [];
}
private function handleDirCompletedDownloads($f, $downloadFolder, $moveToFolder, Release $release, $baseRelease = null){
if($release->getType() === Release::TYPE_SINGLE or $release->getType() === Release::TYPE_SPECIAL){
$this->log("Directory \"$f\" has single release type!", "WARNING");
//$this->ignore[$f] = true;
//return false;
}
$this->log("Processing directory \"$f\"", "POST");
$added = $this->handleCompletedDownloads($downloadFolder . "/" . $f, $moveToFolder, $release);
if(Database::getConfigKey("post.deletefailed", true)){
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($downloadFolder . "/" . $f, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
if($fileinfo->isDir() and !$this->useLink){
$this->log("Deleting remaining folder \"".$fileinfo->getRealPath()."\"", "POST");
rmdir($fileinfo->getRealPath());
}
//$todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
//$this->log("Deleting remaining \"".$fileinfo->getRealPath()."\"", "POST");
//$todo($fileinfo->getRealPath());
}
}
if(!$this->useLink){
rmdir($downloadFolder . "/" . $f);
}
return $added;
}
public function handleCompletedDownloads($downloadFolder, $moveToFolder = null, $baseRelease = null){
$added = 0;
foreach(scandir($downloadFolder) as $f){
if($f{0} === "." or isset($this->ignore[$f])){
continue;
}
$oldAdded = $added;
if($baseRelease !== null and is_dir($downloadFolder . "/" . $f)){
$this->log("Ignoring nested folder \"$f\"", "ERROR");
continue;
}
$release = $this->extractor->extractInformation($f);
if($release === null){
if(is_dir($downloadFolder . "/" . $f)){
$this->log("Could not match \"$f\", but processing folder", "POST");
$added += $this->handleCompletedDownloads($downloadFolder . "/" . $f, $moveToFolder, false);
}else{
$this->log("Ignoring \"$f\"", "ERROR");
}
if($oldAdded === $added){
$this->ignore[$f] = true;
}
continue;
}
$this->log("Processing \"$f\"", "POST");
$info = $this->db->getAniDB()->matchRelease($release);
if($info === null){
$this->log("Could not match \"$f\" [".$release->getTitle().", type: ".$release->getType()."]!", "ERROR");
if(!is_dir($downloadFolder . "/" . $f)){
if(!$this->useLink and Database::getConfigKey("post.deletefailed", true)){
$this->log("Deleting release \"". $f . "\"", "POST");
@unlink($downloadFolder . "/" . $f);
}
$this->ignore[$f] = true;
}else{
$added += $this->handleDirCompletedDownloads($f, $downloadFolder, $moveToFolder, $release, $baseRelease);
}
if($oldAdded === $added){
$this->ignore[$f] = true;
}
continue;
}
if(!$this->db->isTracking($info["aid"])){
if(Database::getConfigKey("post.autotrack", false)){
Downloader::log("Auto tracking \"".$info["title"]."\" [anidb-" . $info["aid"] ."]", "TRACK");
$this->db->addTrack($info["aid"]);
//$this->db->updateTrackEpisodes($info["aid"], -1);
}else{
continue;
}
}
$this->log("Match found \"".$info["title"]."\" [anidb-" . $info["aid"]."]", "POST");
if(is_dir($downloadFolder . "/" . $f)){
$added += $this->handleDirCompletedDownloads($f, $downloadFolder, $moveToFolder, $release, $baseRelease);
if($oldAdded === $added){
$this->ignore[$f] = true;
}
}else{
//TODO: match files to see that they are correct
$this->log("Type: ".$release->getType().", episode ". $release->getNumber(), "POST");
if($release->getType() === Release::TYPE_SPECIAL or $release->getNumber() <= 0){
if(!Database::getConfigKey("post.specials", true)){
if(!$this->useLink and Database::getConfigKey("post.deletefailed", true)){
$this->log("Deleting release \"". $f . "\"", "POST");
@unlink($downloadFolder . "/" . $f);
}
$this->ignore[$f] = true;
continue;
}
$downloadTo = $this->db->getTrackFolder($info["aid"]) . "/Specials";
@mkdir($downloadTo, 0777, true);
if(file_exists($downloadTo . "/" . $f)){
$this->ignore[$f] = true;
continue;
}
}else{
$downloadTo = $this->db->getTrackFolder($info["aid"]);
@mkdir($downloadTo, 0777, true);
if(file_exists($downloadTo . "/" . $f)){
$this->log("Release already exists, aborting!", "ERROR");
if(!$this->useLink){
@unlink($downloadFolder . "/" . $f);
}
$this->ignore[$f] = true;
continue;
}
$expected = $this->db->getTrackEpisode($info["aid"], $release->getNumber());
if($expected !== null and Database::getConfigKey("post.matchrelease", true)){
$c1 = str_replace(" ", "", self::removeExtension(strtolower($expected->getOriginalTitle())));
$c2 = str_replace(" ", "", self::removeExtension(strtolower($release->getOriginalTitle())));
if($c1 != $c2){
$this->log("Expected \"".$expected->getOriginalTitle()."\", got \"".$release->getOriginalTitle()."\"! Check in progress...", "WARN");
if($expected->getCRC() !== null){
if($expected->getCRC() === $release->getCRC()){
$this->log("Title CRC matches, continue!", "POST");
}else{
$this->log("CRC mismatch! [". $expected->getCRC() ." != ". $release->getCRC() ."], aborting!", "ERROR");
if(!$this->useLink and Database::getConfigKey("post.deletefailed", true)){
$this->log("Deleting release \"". $f . "\"", "POST");
@unlink($downloadFolder . "/" . $f);
}else{
$this->ignore[$f] = true;
}
continue;
}
}else if($expected->getGroup() != $release->getGroup() or ($expected->getQuality() !== Release::QUALITY_UNKNOWN and $expected->getQuality() != $release->getQuality()) or ($expected->getSource() !== Release::SOURCE_UNKNOWN and $expected->getSource() != $release->getSource())){
$this->log("Release does not match, aborting!", "ERROR");
if(!$this->useLink and Database::getConfigKey("post.deletefailed", true)){
$this->log("Deleting release \"". $f . "\"", "POST");
@unlink($downloadFolder . "/" . $f);
}else{
$this->ignore[$f] = true;
}
continue;
}
}
}
if(Database::getConfigKey("post.verifycrc", false) and $release->getCRC() !== null){
$this->log("Checking CRC [". $release->getCRC() ."]", "POST");
$CRC = strtoupper(hash_file('crc32b', $downloadFolder . "/" . $f));
if($CRC !== $release->getCRC()){
$this->log("CRC mismatch! [". $release->getCRC() ." != $CRC], aborting!", "ERROR");
if(!$this->useLink and Database::getConfigKey("post.deletefailed", true)){
$this->log("Deleting release \"". $f . "\"", "POST");
@unlink($downloadFolder . "/" . $f);
}else{
$this->ignore[$f] = true;
}
continue;
}
}
if(Database::getConfigKey("post.deleteold", false)){
foreach ($this->getReleasesInFolder($downloadTo, $info["aid"], $baseRelease) as $r2){
if($r2->getType() === $release->getType() and $r2->getNumber() === $release->getNumber()){
$this->log("Deleting old release \"". $r2->getOriginalTitle() . "\"", "POST");
@unlink($downloadTo . "/" . $r2->getOriginalTitle());
}
}
}
}
$added++;
if($this->useLink){
$this->log("Ref-linking to \"".$downloadTo . "/" . $f."\"", "POST");
//if(!self::createReflink($downloadFolder . "/" . $f, $downloadTo . "/" . $f)){
//$this->log("Could not reflink, soft-linking!", "POST");
if(!link($downloadFolder . "/" . $f, $downloadTo . "/" . $f)){
$this->log("Could not link, copying!", "POST");
copy($downloadFolder . "/" . $f, $downloadTo . "/" . $f);
}
//}
@chmod($downloadTo . "/" . $f, 0777);
}else{
$this->log("Moving to \"".$downloadTo . "/" . $f."\"", "POST");
if(!rename($downloadFolder . "/" . $f, $downloadTo . "/" . $f)){
$this->log("Copy-moving to \"".$downloadTo . "/" . $f."\"", "POST");
copy($downloadFolder . "/" . $f, $downloadTo . "/" . $f);
unlink($downloadFolder . "/" . $f);
}
@chmod($downloadTo . "/" . $f, 0777);
}
$this->ignore[$f] = true;
}
}
return $added;
}
private static function createReflink($origin, $destination){
$ret = null;
$out = [];
system("cp --reflink=always ". escapeshellarg($origin) ." ". escapeshellarg($destination) ."", $out, $ret);
return $ret === 0;
}
private static function removeExtension($name){
$pos = strrpos($name, ".");
if($pos !== false and $pos > strlen($name) - 8){
return pathinfo($name, PATHINFO_FILENAME);
}
return $name;
}
/**
* @param $path
* @param null $aid
* @param Release|null $baseRelease
* @return Release[]
*/
public function getReleasesInFolder($path, $aid = null, $baseRelease = null){
$releases = [];
foreach(scandir($path) as $f) {
if ($f{0} === ".") {
continue;
}
$release = $this->extractor->extractInformation($f);
if($release === null){
continue;
}
if($aid !== null){
$info = $this->db->getAniDB()->matchRelease($release);
if($info === null or $info["aid"] != $aid){
continue;
}
}
$releases[] = $release;
}
return $releases;
}
private function cleanNameForFolder($name){
return str_replace(["../", "/..", "/", "\x00"], ["", "", "", ""], $name);
}
public function addToDownload($anidbId, Release $release, $moveToFolder = null){
$toDownload = $release->getParent() !== null ? $release->getParent() : $release;
if($moveToFolder !== null){
$moveToFolder = $this->db->getTrackFolder($anidbId, $moveToFolder . "/[Animarr] " . $this->cleanNameForFolder($this->db->getAniDB()->getAnime($anidbId)["title"])) . (($release->getType() === Release::TYPE_SPECIAL or $release->getNumber() <= 0) ?"/Specials" : "");
}
if(($toDownload->getType() === Release::TYPE_SINGLE and $release->getNumber() >= 0) or $toDownload instanceof MultiRelease){
if($toDownload instanceof MultiRelease and isset($this->download[$toDownload->getId()])){
return true;
}
$number = 0;
foreach($toDownload->getContents($this->extractor) as $r){
if($r->getType() !== Release::TYPE_SINGLE or $r->getNumber() < 0){
continue;
}
$info = $this->db->getAniDB()->matchRelease($r);
if($info === null or $info["aid"] != $anidbId){
continue;
}
$existing = $this->db->getTrackEpisode($anidbId, $r->getNumber());
if($existing !== null and $existing->getId() !== $r->getId()){
//TODO
Downloader::log("There is an already existing episode! \"".$r->getOriginalTitle()."\"", "ADD");
continue;
//unset($this->download[$episodes[$r->getNumber()]["id"]]);
}
$this->db->saveTrackEpisode($anidbId, $r->getNumber(), $r);
if($moveToFolder !== null){
if(file_exists($moveToFolder . "/" . $r->getOriginalTitle())){
Downloader::log("Download already exists! \"".$r->getOriginalTitle()."\"", "ADD");
continue;
}
}
Downloader::log("Added from batch: \"".$r->getOriginalTitle()."\"", "ADD");
++$number;
}
if($number === 0){
Downloader::log("Couldn't add any episode!", "ERROR");
return false;
}
}else{
return false;
}
$this->forceAddToDownload($toDownload);
return true;
}
public function forceAddToDownload(Release $release){
$this->download[$release->getId()] = $release;
}
}