mirror of
https://github.com/idanoo/qbittorrent-cleaner
synced 2025-07-22 08:29:12 +00:00
Initial commit
This commit is contained in:
parent
7450b4cd56
commit
7b9cda2e64
2 changed files with 262 additions and 0 deletions
17
README.md
17
README.md
|
@ -1 +1,18 @@
|
|||
# qbittorrent-cleaner
|
||||
|
||||
Hacked up script used to purge orphaned files from qBitTorrent.
|
||||
Forget to tick 'Remove content'?
|
||||
Misconfigured Unpackarr and have leftover uncompressed files?
|
||||
|
||||
Me too!
|
||||
|
||||
Install php & php-curl. Danger script is dangerous.
|
||||
|
||||
Will delete files.
|
||||
|
||||
```php
|
||||
QBT_HOST=http://127.0.0.01:8080 \
|
||||
QBT_USER=admin \
|
||||
QBT_PASS=pass \
|
||||
php clean.php "/path/to/downloads/folder"
|
||||
```
|
||||
|
|
245
clean.php
Normal file
245
clean.php
Normal file
|
@ -0,0 +1,245 @@
|
|||
<?php
|
||||
|
||||
die("Remove this when you understand how horrible this script is.\n" .
|
||||
"This will delete stuff. Make sure the directory you pass matches \n" .
|
||||
"where your torrents save to. Use at your own risk.\n");
|
||||
|
||||
// Get vars
|
||||
$host = getenv("QBT_HOST") or die("QBT_HOST environment variable not set");
|
||||
$user = getenv("QBT_USER") or die("QBT_USER environment variable not set");
|
||||
$pass = getenv("QBT_PASS") or die("QBT_PASS environment variable not set");
|
||||
if (!isset($argv[1])) {
|
||||
die("Usage: php clean.php \"/path/to/downloads/folder\"\n");
|
||||
}
|
||||
|
||||
$directory = $argv[1];
|
||||
if (!is_dir($directory)) {
|
||||
die("Invalid download folder specified.\n");
|
||||
}
|
||||
|
||||
// Get new api
|
||||
try {
|
||||
$api = new qBtAPI($host, $user, $pass);
|
||||
|
||||
// Query all torrents
|
||||
$torrents = $api->getAllTorrents();
|
||||
echo 'Retrieved ' . count($torrents) . ' torrents from API' . PHP_EOL;
|
||||
|
||||
// Query all files from torrents
|
||||
$filesInqBitTorrent = $api->getAllTorrentsFiles($torrents);
|
||||
echo 'Retrieved ' . count($filesInqBitTorrent) . ' files from API' . PHP_EOL;
|
||||
|
||||
// Query all files on disk
|
||||
$filesOnDisk = Utils::listFilesInDirectory($directory);
|
||||
echo 'Retrieved ' . count($filesOnDisk) . ' files from path ' . $directory . PHP_EOL;
|
||||
|
||||
$i = 0;
|
||||
$filesize = 0;
|
||||
sleep(5);
|
||||
foreach ($filesOnDisk as $file) {
|
||||
// Check if file is in qBittorrent
|
||||
if (!array_key_exists($file, $filesInqBitTorrent)) {
|
||||
$filesize += filesize($file) ?: 0;
|
||||
if (!unlink($file)) {
|
||||
echo "Failed to delete file: $file\n";
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
$base = log($filesize) / log(1024);
|
||||
$suffix = array("", "k", "M", "G", "T")[floor($base)];
|
||||
$sizeies = pow(1024, $base - floor($base)) . $suffix;
|
||||
echo 'Deleted ' . $i . ' files (' . $sizeies . ') that are not in qBittorrent.' . PHP_EOL;
|
||||
} catch (Exception $e) {
|
||||
die("Error: " . $e->getMessage() . "\n");
|
||||
}
|
||||
|
||||
|
||||
class qBtAPI {
|
||||
private $host;
|
||||
private $user;
|
||||
private $pass;
|
||||
private $cookie;
|
||||
|
||||
/**
|
||||
* Constructor for qBtAPI
|
||||
*
|
||||
* @param string $host
|
||||
* @param string $user
|
||||
* @param string $pass
|
||||
*
|
||||
* @throws Exception if login fails
|
||||
*/
|
||||
public function __construct(string $host, string $user, string $pass) {
|
||||
$this->host = rtrim($host, '/');
|
||||
$this->user = $user;
|
||||
$this->pass = $pass;
|
||||
|
||||
if (!$this->login()) {
|
||||
throw new Exception("Failed to authenticate with qBittorrent API. Check your credentials.");
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string API endpoint for login */
|
||||
private const pathLogin = '/api/v2/auth/login';
|
||||
|
||||
/** @var string Key for content path in torrent data */
|
||||
public const contentPathKey = 'content_path';
|
||||
|
||||
/** @var string Key for root path in torrent data */
|
||||
public const rootPathKey = 'root_path';
|
||||
|
||||
/** @var string Key for save path in torrent data */
|
||||
public const savePathKey = 'save_path';
|
||||
|
||||
/** @var string Key for file path in torrent data */
|
||||
public const filePathKey = 'name';
|
||||
|
||||
/**
|
||||
* Login to qBittorrent API
|
||||
*
|
||||
* @param string $host
|
||||
* @param string $user
|
||||
* @param string $pass
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function login(): bool
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $this->host . static::pathLogin);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['username' => $this->user, 'password' => $this->pass]));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);;
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'User-Agent: qBittorrent-Cleaner'
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Get login cookie if set
|
||||
$cookies = [];
|
||||
preg_match_all('/^Set-Cookie:\s*([^;]*)/mi', $response, $matches);
|
||||
foreach($matches[1] as $item) {
|
||||
parse_str($item, $cookie);
|
||||
$cookies = array_merge($cookies, $cookie);
|
||||
}
|
||||
if (!empty($cookies)) {
|
||||
$this->cookie = http_build_query($cookies, '', '; ');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all torrents from qBittorrent API
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllTorrents(): array
|
||||
{
|
||||
return $this->queryAPI('GET', '/api/v2/torrents/info');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all torrents from qBittorrent API
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAllTorrentsFiles(array $torrents): array
|
||||
{
|
||||
$files = [];
|
||||
foreach ($torrents as $torrent) {
|
||||
// Single file torrent
|
||||
if (empty($torrent[static::rootPathKey])) {
|
||||
$files[$torrent[static::contentPathKey]] = true;
|
||||
continue; // Skip torrents without content path
|
||||
}
|
||||
|
||||
// Torrent is folder
|
||||
$torrentFiles = $this->queryAPI('GET', '/api/v2/torrents/files?hash=' . $torrent['hash']);
|
||||
foreach ($torrentFiles as $torrentFile) {
|
||||
$files[$torrent[static::savePathKey] . '/' . $torrentFile[static::filePathKey]] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get torrent files from qBittorrent API
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $path
|
||||
* @param array|null $data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function queryAPI(string $method = 'GET', string $path = '', ?array $data = null): array
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $this->host . $path);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
curl_setopt($ch, CURLOPT_COOKIE, $this->cookie);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'User-Agent: qBittorrent-Cleaner'
|
||||
]);
|
||||
|
||||
|
||||
// Add data if set
|
||||
if ($data) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
// get http response code
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("API request failed with HTTP code: $httpCode. Response: $response");
|
||||
}
|
||||
|
||||
$dataArray = json_decode($response, true) ?? [];
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception("Failed to decode JSON response: " . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $dataArray;
|
||||
}
|
||||
}
|
||||
|
||||
class Utils {
|
||||
/**
|
||||
* List all files in a directory recursively
|
||||
*
|
||||
* @param string $folder
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function listFilesInDirectory(string $folder): array
|
||||
{
|
||||
$files = [];
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($folder, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$files[] = $file->getPathname();
|
||||
} else if ($file->isDir()) {
|
||||
$files = array_merge($files, static::listFilesInDirectory($file->getPathname()));
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue