405 lines
15 KiB
PHP
Executable file
405 lines
15 KiB
PHP
Executable file
<?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;
|
||
}
|
||
|
||
}
|