mirror of
https://github.com/YGGverse/YGGtracker.git
synced 2025-01-22 20:54:52 +00:00
implement peers online scrape
This commit is contained in:
parent
3b5d51d478
commit
e36830442c
@ -65,11 +65,14 @@ git checkout -b my-pr-branch-name
|
|||||||
+ [x] Sensitive
|
+ [x] Sensitive
|
||||||
+ [ ] Comments
|
+ [ ] Comments
|
||||||
+ [ ] Features
|
+ [ ] Features
|
||||||
|
+ [x] Scrape trackers
|
||||||
|
+ [x] Peers
|
||||||
|
+ [x] Completed
|
||||||
|
+ [x] Leechers
|
||||||
+ [x] Stars
|
+ [x] Stars
|
||||||
+ [x] Downloads
|
+ [x] Downloads
|
||||||
+ [ ] Comments
|
+ [ ] Comments
|
||||||
+ [ ] Views
|
+ [ ] Views
|
||||||
+ [ ] Peers
|
|
||||||
+ [ ] Info page
|
+ [ ] Info page
|
||||||
|
|
||||||
* [ ] User
|
* [ ] User
|
||||||
@ -96,6 +99,7 @@ git checkout -b my-pr-branch-name
|
|||||||
#### Components
|
#### Components
|
||||||
|
|
||||||
[Icons](https://icons.getbootstrap.com)
|
[Icons](https://icons.getbootstrap.com)
|
||||||
|
[PHP Scrapper](https://github.com/medariox/scrapeer)
|
||||||
|
|
||||||
#### Feedback
|
#### Feedback
|
||||||
|
|
||||||
|
Binary file not shown.
@ -1,4 +1,6 @@
|
|||||||
@reboot searchd
|
@reboot searchd
|
||||||
@reboot indexer --all --rotate
|
@reboot indexer --all --rotate
|
||||||
|
|
||||||
* * * * * indexer magnet --rotate
|
* * * * * indexer magnet --rotate
|
||||||
|
|
||||||
|
* * * * * /usr/bin/php /YGGtracker/src/crontab/scrape.php
|
@ -100,3 +100,7 @@ define('TRACKER_LINKS', (object)
|
|||||||
|
|
||||||
// Yggdrasil
|
// Yggdrasil
|
||||||
define('YGGDRASIL_URL_REGEX', '/^0{0,1}[2-3][a-f0-9]{0,2}:/'); // thanks to @ygguser (https://github.com/YGGverse/YGGo/issues/1#issuecomment-1498182228 )
|
define('YGGDRASIL_URL_REGEX', '/^0{0,1}[2-3][a-f0-9]{0,2}:/'); // thanks to @ygguser (https://github.com/YGGverse/YGGo/issues/1#issuecomment-1498182228 )
|
||||||
|
|
||||||
|
// Crawler
|
||||||
|
define('CRAWLER_SCRAPE_QUEUE_LIMIT', 1);
|
||||||
|
define('CRAWLER_SCRAPE_TIME_OFFLINE_TIMEOUT', 60*60*24);
|
144
src/crontab/scrape.php
Normal file
144
src/crontab/scrape.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Lock multi-thread execution
|
||||||
|
$semaphore = sem_get(crc32('yggtracker.crontab.scrape'), 1);
|
||||||
|
|
||||||
|
if (false === sem_acquire($semaphore, true)) {
|
||||||
|
|
||||||
|
exit (PHP_EOL . 'yggtracker.crontab.scrape process locked by another thread.' . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load system dependencies
|
||||||
|
require_once(__DIR__ . '/../config/app.php');
|
||||||
|
require_once(__DIR__ . '/../library/database.php');
|
||||||
|
require_once(__DIR__ . '/../library/scrapeer.php');
|
||||||
|
|
||||||
|
// Init Debug
|
||||||
|
$debug = [
|
||||||
|
'time' => [
|
||||||
|
'ISO8601' => date('c'),
|
||||||
|
'total' => microtime(true),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Connect DB
|
||||||
|
try {
|
||||||
|
|
||||||
|
$db = new Database(DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD);
|
||||||
|
|
||||||
|
} catch(Exception $e) {
|
||||||
|
|
||||||
|
var_dump($e);
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Scraper
|
||||||
|
try {
|
||||||
|
|
||||||
|
$scraper = new Scrapeer\Scraper();
|
||||||
|
|
||||||
|
} catch(Exception $e) {
|
||||||
|
|
||||||
|
var_dump($e);
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin
|
||||||
|
try {
|
||||||
|
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// Reset time offline by timeout
|
||||||
|
$db->resetMagnetToAddressTrackerTimeOfflineByTimeout(
|
||||||
|
CRAWLER_SCRAPE_TIME_OFFLINE_TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($db->getMagnetToAddressTrackerScrapeQueue(CRAWLER_SCRAPE_QUEUE_LIMIT) as $queue)
|
||||||
|
{
|
||||||
|
if ($addressTracker = $db->getAddressTracker($queue->addressTrackerId))
|
||||||
|
{
|
||||||
|
// Build url
|
||||||
|
$scheme = $db->getScheme($addressTracker->schemeId);
|
||||||
|
$host = $db->getHost($addressTracker->hostId);
|
||||||
|
$port = $db->getPort($addressTracker->portId);
|
||||||
|
$uri = $db->getUri($addressTracker->uriId);
|
||||||
|
|
||||||
|
$url = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
|
||||||
|
$host->value,
|
||||||
|
$port->value,
|
||||||
|
$uri->value) : sprintf('%s://%s%s', $scheme->value,
|
||||||
|
$host->value,
|
||||||
|
$uri->value);
|
||||||
|
|
||||||
|
$hash = str_replace('urn:btih:', false, $db->getMagnet($queue->magnetId)->xt);
|
||||||
|
|
||||||
|
if ($scrape = $scraper->scrape([$hash], [$url], null, 1))
|
||||||
|
{
|
||||||
|
$db->updateMagnetToAddressTrackerTimeOffline(
|
||||||
|
$queue->magnetToAddressTrackerId,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isset($scrape[$hash]['seeders']))
|
||||||
|
{
|
||||||
|
$db->updateMagnetToAddressTrackerSeeders(
|
||||||
|
$queue->magnetToAddressTrackerId,
|
||||||
|
(int) $scrape[$hash]['seeders'],
|
||||||
|
time()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($scrape[$hash]['completed']))
|
||||||
|
{
|
||||||
|
$db->updateMagnetToAddressTrackerCompleted(
|
||||||
|
$queue->magnetToAddressTrackerId,
|
||||||
|
(int) $scrape[$hash]['completed'],
|
||||||
|
time()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($scrape[$hash]['leechers']))
|
||||||
|
{
|
||||||
|
$db->updateMagnetToAddressTrackerLeechers(
|
||||||
|
$queue->magnetToAddressTrackerId,
|
||||||
|
(int) $scrape[$hash]['leechers'],
|
||||||
|
time()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$db->updateMagnetToAddressTrackerTimeOffline(
|
||||||
|
$queue->magnetToAddressTrackerId,
|
||||||
|
time()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
|
||||||
|
} catch (EXception $e) {
|
||||||
|
|
||||||
|
$db->rollback();
|
||||||
|
|
||||||
|
var_dump($e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug output
|
||||||
|
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
|
||||||
|
|
||||||
|
print_r(
|
||||||
|
array_merge($debug, [
|
||||||
|
'db' => [
|
||||||
|
'total' => [
|
||||||
|
'select' => $db->getDebug()->query->select->total,
|
||||||
|
'insert' => $db->getDebug()->query->insert->total,
|
||||||
|
'update' => $db->getDebug()->query->update->total,
|
||||||
|
'delete' => $db->getDebug()->query->delete->total,
|
||||||
|
]
|
||||||
|
]
|
||||||
|
])
|
||||||
|
);
|
@ -653,6 +653,50 @@ class Database {
|
|||||||
return $this->_db->lastInsertId();
|
return $this->_db->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function updateMagnetToAddressTrackerSeeders(int $magnetToAddressTrackerId, int $seeders, int $timeUpdated) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->update->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `seeders` = ?, `timeUpdated` = ? WHERE `magnetToAddressTrackerId` = ?');
|
||||||
|
|
||||||
|
$query->execute([$seeders, $timeUpdated, $magnetToAddressTrackerId]);
|
||||||
|
|
||||||
|
return $query->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateMagnetToAddressTrackerCompleted(int $magnetToAddressTrackerId, int $completed, int $timeUpdated) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->update->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `completed` = ?, `timeUpdated` = ? WHERE `magnetToAddressTrackerId` = ?');
|
||||||
|
|
||||||
|
$query->execute([$completed, $timeUpdated, $magnetToAddressTrackerId]);
|
||||||
|
|
||||||
|
return $query->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateMagnetToAddressTrackerLeechers(int $magnetToAddressTrackerId, int $leechers, int $timeUpdated) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->update->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `leechers` = ?, `timeUpdated` = ? WHERE `magnetToAddressTrackerId` = ?');
|
||||||
|
|
||||||
|
$query->execute([$leechers, $timeUpdated, $magnetToAddressTrackerId]);
|
||||||
|
|
||||||
|
return $query->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateMagnetToAddressTrackerTimeOffline(int $magnetToAddressTrackerId, int $timeOffline) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->update->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `timeOffline` = ? WHERE `magnetToAddressTrackerId` = ?');
|
||||||
|
|
||||||
|
$query->execute([$timeOffline, $magnetToAddressTrackerId]);
|
||||||
|
|
||||||
|
return $query->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
public function deleteMagnetToAddressTrackerByMagnetId(int $magnetId) : int {
|
public function deleteMagnetToAddressTrackerByMagnetId(int $magnetId) : int {
|
||||||
|
|
||||||
$this->_debug->query->delete->total++;
|
$this->_debug->query->delete->total++;
|
||||||
@ -686,6 +730,38 @@ class Database {
|
|||||||
return $query->fetchAll();
|
return $query->fetchAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getMagnetToAddressTrackerScrapeQueue(int $limit) {
|
||||||
|
|
||||||
|
$this->_debug->query->select->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('SELECT * FROM `magnetToAddressTracker`
|
||||||
|
|
||||||
|
WHERE `timeOffline` IS NULL
|
||||||
|
|
||||||
|
ORDER BY `timeUpdated` ASC, RAND()
|
||||||
|
|
||||||
|
LIMIT ' . (int) $limit);
|
||||||
|
|
||||||
|
$query->execute();
|
||||||
|
|
||||||
|
return $query->fetchAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetMagnetToAddressTrackerTimeOfflineByTimeout(int $timeOffline) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->update->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('UPDATE `magnetToAddressTracker` SET `timeOffline` = NULL WHERE `timeOffline` < ?');
|
||||||
|
|
||||||
|
$query->execute(
|
||||||
|
[
|
||||||
|
time() - $timeOffline
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $query->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
public function initMagnetToAddressTrackerId(int $magnetId, int $addressTrackerId) : int {
|
public function initMagnetToAddressTrackerId(int $magnetId, int $addressTrackerId) : int {
|
||||||
|
|
||||||
if ($result = $this->findMagnetToAddressTracker($magnetId, $addressTrackerId)) {
|
if ($result = $this->findMagnetToAddressTracker($magnetId, $addressTrackerId)) {
|
||||||
@ -942,28 +1018,17 @@ class Database {
|
|||||||
return $this->_db->lastInsertId();
|
return $this->_db->lastInsertId();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMagnetDownloadsTotal(int $magnetId) : int {
|
public function getMagnetDownloadsTotalByUserId(int $magnetId) : int {
|
||||||
|
|
||||||
$this->_debug->query->select->total++;
|
$this->_debug->query->select->total++;
|
||||||
|
|
||||||
$query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetDownload` WHERE `magnetId` = ?');
|
$query = $this->_db->prepare('SELECT COUNT(DISTINCT `userId`) AS `result` FROM `magnetDownload` WHERE `magnetId` = ?');
|
||||||
|
|
||||||
$query->execute([$magnetId]);
|
$query->execute([$magnetId]);
|
||||||
|
|
||||||
return $query->fetch()->result;
|
return $query->fetch()->result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteMagnetDownloadByUserId(int $magnetId, int $userId) : int {
|
|
||||||
|
|
||||||
$this->_debug->query->delete->total++;
|
|
||||||
|
|
||||||
$query = $this->_db->prepare('DELETE FROM `magnetDownload` WHERE `magnetId` = ? AND `userId` = ?');
|
|
||||||
|
|
||||||
$query->execute([$magnetId, $userId]);
|
|
||||||
|
|
||||||
return $query->rowCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function findMagnetDownloadsTotalByUserId(int $magnetId, int $userId) : int {
|
public function findMagnetDownloadsTotalByUserId(int $magnetId, int $userId) : int {
|
||||||
|
|
||||||
$this->_debug->query->select->total++;
|
$this->_debug->query->select->total++;
|
||||||
@ -974,4 +1039,38 @@ class Database {
|
|||||||
|
|
||||||
return $query->fetch()->result;
|
return $query->fetch()->result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Magnet view
|
||||||
|
public function addMagnetView(int $magnetId, int $userId, int $timeAdded) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->insert->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('INSERT INTO `magnetView` SET `magnetId` = ?, `userId` = ?, `timeAdded` = ?');
|
||||||
|
|
||||||
|
$query->execute([$magnetId, $userId, $timeAdded]);
|
||||||
|
|
||||||
|
return $this->_db->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMagnetViewsTotal(int $magnetId) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->select->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetView` WHERE `magnetId` = ?');
|
||||||
|
|
||||||
|
$query->execute([$magnetId]);
|
||||||
|
|
||||||
|
return $query->fetch()->result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findMagnetViewsTotalByUserId(int $magnetId, int $userId) : int {
|
||||||
|
|
||||||
|
$this->_debug->query->select->total++;
|
||||||
|
|
||||||
|
$query = $this->_db->prepare('SELECT COUNT(*) AS `result` FROM `magnetView` WHERE `magnetId` = ? AND `userId` = ?');
|
||||||
|
|
||||||
|
$query->execute([$magnetId, $userId]);
|
||||||
|
|
||||||
|
return $query->fetch()->result;
|
||||||
|
}
|
||||||
}
|
}
|
692
src/library/scrapeer.php
Normal file
692
src/library/scrapeer.php
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Scrapeer, a tiny PHP library that lets you scrape
|
||||||
|
* HTTP(S) and UDP trackers for torrent information.
|
||||||
|
*
|
||||||
|
* This file is extensively based on Johannes Zinnau's
|
||||||
|
* work, which can be found at https://goo.gl/7hyjde
|
||||||
|
*
|
||||||
|
* Licensed under a Creative Commons
|
||||||
|
* Attribution-ShareAlike 3.0 Unported License
|
||||||
|
* http://creativecommons.org/licenses/by-sa/3.0
|
||||||
|
*
|
||||||
|
* @package Scrapeer
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Scrapeer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The one and only class you'll ever need.
|
||||||
|
*/
|
||||||
|
class Scraper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current version of Scrapeer
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
const VERSION = '0.5.4';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of errors
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $errors = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of infohashes to scrape
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $infohashes = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout for a single tracker
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $timeout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates the scraper
|
||||||
|
*
|
||||||
|
* @throws \RangeException In case of invalid amount of info-hashes.
|
||||||
|
*
|
||||||
|
* @param array|string $hashes List (>1) or string of infohash(es).
|
||||||
|
* @param array|string $trackers List (>1) or string of tracker(s).
|
||||||
|
* @param int|null $max_trackers Optional. Maximum number of trackers to be scraped, Default all.
|
||||||
|
* @param int $timeout Optional. Maximum time for each tracker scrape in seconds, Default 2.
|
||||||
|
* @param bool $announce Optional. Use announce instead of scrape, Default false.
|
||||||
|
* @return array List of results.
|
||||||
|
*/
|
||||||
|
public function scrape( $hashes, $trackers, $max_trackers = null, $timeout = 2, $announce = false ) {
|
||||||
|
$final_result = array();
|
||||||
|
|
||||||
|
if ( empty( $trackers ) ) {
|
||||||
|
$this->errors[] = 'No tracker specified, aborting.';
|
||||||
|
return $final_result;
|
||||||
|
} else if ( ! is_array( $trackers ) ) {
|
||||||
|
$trackers = array( $trackers );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_int( $timeout ) ) {
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
} else {
|
||||||
|
$this->timeout = 2;
|
||||||
|
$this->errors[] = 'Timeout must be an integer. Using default value.';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->infohashes = $this->normalize_infohashes( $hashes );
|
||||||
|
} catch ( \RangeException $e ) {
|
||||||
|
$this->errors[] = $e->getMessage();
|
||||||
|
return $final_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$max_iterations = is_int( $max_trackers ) ? $max_trackers : count( $trackers );
|
||||||
|
foreach ( $trackers as $index => $tracker ) {
|
||||||
|
if ( ! empty( $this->infohashes ) && $index < $max_iterations ) {
|
||||||
|
$info = parse_url( $tracker );
|
||||||
|
$protocol = $info['scheme'];
|
||||||
|
$host = $info['host'];
|
||||||
|
if ( empty( $protocol ) || empty( $host ) ) {
|
||||||
|
$this->errors[] = 'Skipping invalid tracker (' . $tracker . ').';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$port = isset( $info['port'] ) ? $info['port'] : null;
|
||||||
|
$path = isset( $info['path'] ) ? $info['path'] : null;
|
||||||
|
$passkey = $this->get_passkey( $path );
|
||||||
|
$result = $this->try_scrape( $protocol, $host, $port, $passkey, $announce );
|
||||||
|
$final_result = array_merge( $final_result, $result );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return $final_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the given hashes
|
||||||
|
*
|
||||||
|
* @throws \RangeException If amount of valid infohashes > 64 or < 1.
|
||||||
|
*
|
||||||
|
* @param array $infohashes List of infohash(es).
|
||||||
|
* @return array Normalized infohash(es).
|
||||||
|
*/
|
||||||
|
private function normalize_infohashes( $infohashes ) {
|
||||||
|
if ( ! is_array( $infohashes ) ) {
|
||||||
|
$infohashes = array( $infohashes );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $infohashes as $index => $infohash ) {
|
||||||
|
if ( ! preg_match( '/^[a-f0-9]{40}$/i', $infohash ) ) {
|
||||||
|
$this->errors[] = 'Invalid infohash skipped (' . $infohash . ').';
|
||||||
|
unset( $infohashes[ $index ] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$total_infohashes = count( $infohashes );
|
||||||
|
if ( $total_infohashes > 64 || $total_infohashes < 1 ) {
|
||||||
|
throw new \RangeException( 'Invalid amount of valid infohashes (' . $total_infohashes . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$infohashes = array_values( $infohashes );
|
||||||
|
|
||||||
|
return $infohashes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the passkey found in the scrape request.
|
||||||
|
*
|
||||||
|
* @param string $path Path from the scrape request.
|
||||||
|
* @return string Passkey or empty string.
|
||||||
|
*/
|
||||||
|
private function get_passkey( $path ) {
|
||||||
|
if ( ! is_null( $path ) && preg_match( '/[a-z0-9]{32}/i', $path, $matches ) ) {
|
||||||
|
return '/' . $matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to scrape with a single tracker.
|
||||||
|
*
|
||||||
|
* @throws \Exception In case of unsupported protocol.
|
||||||
|
*
|
||||||
|
* @param string $protocol Protocol of the tracker.
|
||||||
|
* @param string $host Domain or address of the tracker.
|
||||||
|
* @param int $port Optional. Port number of the tracker.
|
||||||
|
* @param string $passkey Optional. Passkey provided in the scrape request.
|
||||||
|
* @param bool $announce Optional. Use announce instead of scrape, Default false.
|
||||||
|
* @return array List of results.
|
||||||
|
*/
|
||||||
|
private function try_scrape( $protocol, $host, $port, $passkey, $announce ) {
|
||||||
|
$infohashes = $this->infohashes;
|
||||||
|
$this->infohashes = array();
|
||||||
|
$results = array();
|
||||||
|
try {
|
||||||
|
switch ( $protocol ) {
|
||||||
|
case 'udp':
|
||||||
|
$port = isset( $port ) ? $port : 80;
|
||||||
|
$results = $this->scrape_udp( $infohashes, $host, $port, $announce );
|
||||||
|
break;
|
||||||
|
case 'http':
|
||||||
|
$port = isset( $port ) ? $port : 80;
|
||||||
|
$results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce );
|
||||||
|
break;
|
||||||
|
case 'https':
|
||||||
|
$port = isset( $port ) ? $port : 443;
|
||||||
|
$results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce );
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new \Exception( 'Unsupported protocol (' . $protocol . '://' . $host . ').' );
|
||||||
|
}
|
||||||
|
} catch ( \Exception $e ) {
|
||||||
|
$this->infohashes = $infohashes;
|
||||||
|
$this->errors[] = $e->getMessage();
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates the HTTP(S) scraping
|
||||||
|
*
|
||||||
|
* @param array|string $infohashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $protocol Protocol to use for the scraping.
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Optional. Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
|
||||||
|
* @param string $passkey Optional. Passkey provided in the scrape request.
|
||||||
|
* @param bool $announce Optional. Use announce instead of scrape, Default false.
|
||||||
|
* @return array List of results.
|
||||||
|
*/
|
||||||
|
private function scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce ) {
|
||||||
|
if ( true === $announce ) {
|
||||||
|
$response = $this->http_announce( $infohashes, $protocol, $host, $port, $passkey );
|
||||||
|
} else {
|
||||||
|
$query = $this->http_query( $infohashes, $protocol, $host, $port, $passkey );
|
||||||
|
$response = $this->http_request( $query, $host, $port );
|
||||||
|
}
|
||||||
|
$results = $this->http_data( $response, $infohashes, $host );
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the HTTP(S) query
|
||||||
|
*
|
||||||
|
* @param array|string $infohashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $protocol Protocol to use for the scraping.
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
|
||||||
|
* @param string $passkey Optional. Passkey provided in the scrape request.
|
||||||
|
* @return string Request query.
|
||||||
|
*/
|
||||||
|
private function http_query( $infohashes, $protocol, $host, $port, $passkey ) {
|
||||||
|
$tracker_url = $protocol . '://' . $host . ':' . $port . $passkey;
|
||||||
|
$scrape_query = '';
|
||||||
|
|
||||||
|
foreach ( $infohashes as $index => $infohash ) {
|
||||||
|
if ( $index > 0 ) {
|
||||||
|
$scrape_query .= '&info_hash=' . urlencode( pack( 'H*', $infohash ) );
|
||||||
|
} else {
|
||||||
|
$scrape_query .= '/scrape?info_hash=' . urlencode( pack( 'H*', $infohash ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$request_query = $tracker_url . $scrape_query;
|
||||||
|
|
||||||
|
return $request_query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the query and returns the result
|
||||||
|
*
|
||||||
|
* @throws \Exception If the connection can't be established.
|
||||||
|
* @throws \Exception If the response isn't valid.
|
||||||
|
*
|
||||||
|
* @param string $query The query that will be executed.
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
|
||||||
|
* @return string Request response.
|
||||||
|
*/
|
||||||
|
private function http_request( $query, $host, $port ) {
|
||||||
|
$context = stream_context_create( array(
|
||||||
|
'http' => array(
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) {
|
||||||
|
throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( substr( $response, 0, 12 ) !== 'd5:filesd20:' ) {
|
||||||
|
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the query, sends the announce request and returns the data
|
||||||
|
*
|
||||||
|
* @throws \Exception If the connection can't be established.
|
||||||
|
*
|
||||||
|
* @param array|string $infohashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $protocol Protocol to use for the scraping.
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
|
||||||
|
* @param string $passkey Optional. Passkey provided in the scrape request.
|
||||||
|
* @return string Request response.
|
||||||
|
*/
|
||||||
|
private function http_announce( $infohashes, $protocol, $host, $port, $passkey ) {
|
||||||
|
$tracker_url = $protocol . '://' . $host . ':' . $port . $passkey;
|
||||||
|
$context = stream_context_create( array(
|
||||||
|
'http' => array(
|
||||||
|
'timeout' => $this->timeout,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
$response_data = '';
|
||||||
|
foreach ( $infohashes as $infohash ) {
|
||||||
|
$query = $tracker_url . '/announce?info_hash=' . urlencode( pack( 'H*', $infohash ) );
|
||||||
|
if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) {
|
||||||
|
throw new \Exception( 'Invalid announce connection (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( substr( $response, 0, 12 ) !== 'd8:completei' ||
|
||||||
|
substr( $response, 0, 46 ) === 'd8:completei0e10:downloadedi0e10:incompletei1e' ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ben_hash = '20:' . pack( 'H*', $infohash ) . 'd';
|
||||||
|
$response_data .= $ben_hash . $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the response and returns the data
|
||||||
|
*
|
||||||
|
* @param string $response The response that will be parsed.
|
||||||
|
* @param array $infohashes List of infohash(es).
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @return array Parsed data.
|
||||||
|
*/
|
||||||
|
private function http_data( $response, $infohashes, $host ) {
|
||||||
|
$torrents_data = array();
|
||||||
|
|
||||||
|
foreach ( $infohashes as $infohash ) {
|
||||||
|
$ben_hash = '20:' . pack( 'H*', $infohash ) . 'd';
|
||||||
|
$start_pos = strpos( $response, $ben_hash );
|
||||||
|
if ( false !== $start_pos ) {
|
||||||
|
$start = $start_pos + 24;
|
||||||
|
$head = substr( $response, $start );
|
||||||
|
$end = strpos( $head, 'ee' ) + 1;
|
||||||
|
$data = substr( $response, $start, $end );
|
||||||
|
|
||||||
|
$seeders = '8:completei';
|
||||||
|
$torrent_info['seeders'] = $this->get_information( $data, $seeders, 'e' );
|
||||||
|
|
||||||
|
$completed = '10:downloadedi';
|
||||||
|
$torrent_info['completed'] = $this->get_information( $data, $completed, 'e' );
|
||||||
|
|
||||||
|
$leechers = '10:incompletei';
|
||||||
|
$torrent_info['leechers'] = $this->get_information( $data, $leechers, 'e' );
|
||||||
|
|
||||||
|
$torrents_data[ $infohash ] = $torrent_info;
|
||||||
|
} else {
|
||||||
|
$this->collect_infohash( $infohash );
|
||||||
|
$this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $torrents_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a string and returns the data between $start and $end.
|
||||||
|
*
|
||||||
|
* @param string $data The data that will be parsed.
|
||||||
|
* @param string $start Beginning part of the data.
|
||||||
|
* @param string $end Ending part of the data.
|
||||||
|
* @return int Parsed information or 0.
|
||||||
|
*/
|
||||||
|
private function get_information( $data, $start, $end ) {
|
||||||
|
$start_pos = strpos( $data, $start );
|
||||||
|
if ( false !== $start_pos ) {
|
||||||
|
$start = $start_pos + strlen( $start );
|
||||||
|
$head = substr( $data, $start );
|
||||||
|
$end = strpos( $head, $end );
|
||||||
|
$information = substr( $data, $start, $end );
|
||||||
|
|
||||||
|
return (int) $information;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates the UDP scraping
|
||||||
|
*
|
||||||
|
* @param array|string $infohashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Optional. Port number of the tracker, Default 80.
|
||||||
|
* @param bool $announce Optional. Use announce instead of scrape, Default false.
|
||||||
|
* @return array List of results.
|
||||||
|
*/
|
||||||
|
private function scrape_udp( $infohashes, $host, $port, $announce ) {
|
||||||
|
list( $socket, $transaction_id, $connection_id ) = $this->prepare_udp( $host, $port );
|
||||||
|
|
||||||
|
if ( true === $announce ) {
|
||||||
|
$response = $this->udp_announce( $socket, $infohashes, $connection_id );
|
||||||
|
$keys = 'Nleechers/Nseeders';
|
||||||
|
$start = 12;
|
||||||
|
$end = 16;
|
||||||
|
$offset = 20;
|
||||||
|
} else {
|
||||||
|
$response = $this->udp_scrape( $socket, $infohashes, $connection_id, $transaction_id, $host, $port );
|
||||||
|
$keys = 'Nseeders/Ncompleted/Nleechers';
|
||||||
|
$start = 8;
|
||||||
|
$end = $offset = 12;
|
||||||
|
}
|
||||||
|
$results = $this->udp_scrape_data( $response, $infohashes, $host, $keys, $start, $end, $offset );
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the UDP connection
|
||||||
|
*
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Optional. Port number of the tracker, Default 80.
|
||||||
|
* @return array Created socket, transaction ID and connection ID.
|
||||||
|
*/
|
||||||
|
private function prepare_udp( $host, $port ) {
|
||||||
|
$socket = $this->udp_create_connection( $host, $port );
|
||||||
|
$transaction_id = $this->udp_connection_request( $socket );
|
||||||
|
$connection_id = $this->udp_connection_response( $socket, $transaction_id, $host, $port );
|
||||||
|
|
||||||
|
return array( $socket, $transaction_id, $connection_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the UDP socket and establishes the connection
|
||||||
|
*
|
||||||
|
* @throws \Exception If the socket couldn't be created or connected to.
|
||||||
|
*
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Port number of the tracker, Default 80.
|
||||||
|
* @return resource $socket Created and connected socket.
|
||||||
|
*/
|
||||||
|
private function udp_create_connection( $host, $port ) {
|
||||||
|
if ( false === ( $socket = @socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ) ) ) {
|
||||||
|
throw new \Exception( "Couldn't create socket." );
|
||||||
|
}
|
||||||
|
|
||||||
|
$timeout = $this->timeout;
|
||||||
|
socket_set_option( $socket, SOL_SOCKET, SO_RCVTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) );
|
||||||
|
socket_set_option( $socket, SOL_SOCKET, SO_SNDTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) );
|
||||||
|
if ( false === @socket_connect( $socket, $host, $port ) ) {
|
||||||
|
throw new \Exception( "Couldn't connect to socket." );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes to the connected socket and returns the transaction ID
|
||||||
|
*
|
||||||
|
* @throws \Exception If the socket couldn't be written to.
|
||||||
|
*
|
||||||
|
* @param resource $socket The socket resource.
|
||||||
|
* @return int The transaction ID.
|
||||||
|
*/
|
||||||
|
private function udp_connection_request( $socket ) {
|
||||||
|
$connection_id = "\x00\x00\x04\x17\x27\x10\x19\x80";
|
||||||
|
$action = pack( 'N', 0 );
|
||||||
|
$transaction_id = mt_rand( 0, 2147483647 );
|
||||||
|
$buffer = $connection_id . $action . pack( 'N', $transaction_id );
|
||||||
|
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
|
||||||
|
socket_close( $socket );
|
||||||
|
throw new \Exception( "Couldn't write to socket." );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $transaction_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the connection response and returns the connection ID
|
||||||
|
*
|
||||||
|
* @throws \Exception If anything fails with the scraping.
|
||||||
|
*
|
||||||
|
* @param resource $socket The socket resource.
|
||||||
|
* @param int $transaction_id The transaction ID.
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Port number of the tracker, Default 80.
|
||||||
|
* @return string The connection ID.
|
||||||
|
*/
|
||||||
|
private function udp_connection_response( $socket, $transaction_id, $host, $port ) {
|
||||||
|
if ( false === ( $response = @socket_read( $socket, 16 ) ) ) {
|
||||||
|
socket_close( $socket );
|
||||||
|
throw new \Exception( 'Invalid scrape connection! (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strlen( $response ) < 16 ) {
|
||||||
|
socket_close( $socket );
|
||||||
|
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = unpack( 'Naction/Ntransaction_id', $response );
|
||||||
|
if ( 0 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
|
||||||
|
socket_close( $socket );
|
||||||
|
throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$connection_id = substr( $response, 8, 8 );
|
||||||
|
|
||||||
|
return $connection_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the socket response and returns the torrent data
|
||||||
|
*
|
||||||
|
* @throws \Exception If anything fails while reading the response.
|
||||||
|
*
|
||||||
|
* @param resource $socket The socket resource.
|
||||||
|
* @param array $hashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $connection_id The connection ID.
|
||||||
|
* @param int $transaction_id The transaction ID.
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param int $port Port number of the tracker, Default 80.
|
||||||
|
* @return string Response data.
|
||||||
|
*/
|
||||||
|
private function udp_scrape( $socket, $hashes, $connection_id, $transaction_id, $host, $port ) {
|
||||||
|
$this->udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id );
|
||||||
|
|
||||||
|
$read_length = 8 + ( 12 * count( $hashes ) );
|
||||||
|
if ( false === ( $response = @socket_read( $socket, $read_length ) ) ) {
|
||||||
|
socket_close( $socket );
|
||||||
|
throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
socket_close( $socket );
|
||||||
|
|
||||||
|
if ( strlen( $response ) < $read_length ) {
|
||||||
|
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = unpack( 'Naction/Ntransaction_id', $response );
|
||||||
|
if ( 2 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
|
||||||
|
throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes to the connected socket
|
||||||
|
*
|
||||||
|
* @throws \Exception If the socket couldn't be written to.
|
||||||
|
*
|
||||||
|
* @param resource $socket The socket resource.
|
||||||
|
* @param array $hashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $connection_id The connection ID.
|
||||||
|
* @param int $transaction_id The transaction ID.
|
||||||
|
*/
|
||||||
|
private function udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id ) {
|
||||||
|
$action = pack( 'N', 2 );
|
||||||
|
|
||||||
|
$infohashes = '';
|
||||||
|
foreach ( $hashes as $infohash ) {
|
||||||
|
$infohashes .= pack( 'H*', $infohash );
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer = $connection_id . $action . pack( 'N', $transaction_id ) . $infohashes;
|
||||||
|
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
|
||||||
|
socket_close( $socket );
|
||||||
|
throw new \Exception( "Couldn't write to socket." );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the announce to the connected socket
|
||||||
|
*
|
||||||
|
* @throws \Exception If the socket couldn't be written to.
|
||||||
|
*
|
||||||
|
* @param resource $socket The socket resource.
|
||||||
|
* @param array $hashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $connection_id The connection ID.
|
||||||
|
* @return string Torrent(s) data.
|
||||||
|
*/
|
||||||
|
private function udp_announce( $socket, $hashes, $connection_id ) {
|
||||||
|
$action = pack( 'N', 1 );
|
||||||
|
$downloaded = $left = $uploaded = "\x30\x30\x30\x30\x30\x30\x30\x30";
|
||||||
|
$peer_id = $this->random_peer_id();
|
||||||
|
$event = pack( 'N', 3 );
|
||||||
|
$ip_addr = pack( 'N', 0 );
|
||||||
|
$key = pack( 'N', mt_rand( 0, 2147483647 ) );
|
||||||
|
$num_want = -1;
|
||||||
|
$ann_port = pack( 'N', mt_rand( 0, 255 ) );
|
||||||
|
|
||||||
|
$response_data = '';
|
||||||
|
foreach ( $hashes as $infohash ) {
|
||||||
|
$transaction_id = mt_rand( 0, 2147483647 );
|
||||||
|
$buffer = $connection_id . $action . pack( 'N', $transaction_id ) . pack( 'H*', $infohash ) .
|
||||||
|
$peer_id . $downloaded . $left . $uploaded . $event . $ip_addr . $key . $num_want . $ann_port;
|
||||||
|
|
||||||
|
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
|
||||||
|
socket_close( $socket );
|
||||||
|
throw new \Exception( "Couldn't write announce to socket." );
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->udp_verify_announce( $socket, $transaction_id );
|
||||||
|
if ( false === $response ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response_data .= $response;
|
||||||
|
}
|
||||||
|
socket_close( $socket );
|
||||||
|
|
||||||
|
return $response_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random peer ID
|
||||||
|
*
|
||||||
|
* @return string Generated peer ID.
|
||||||
|
*/
|
||||||
|
private function random_peer_id() {
|
||||||
|
$identifier = '-SP0054-';
|
||||||
|
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||||
|
$peer_id = $identifier . substr( str_shuffle( $chars ), 0, 12 );
|
||||||
|
|
||||||
|
return $peer_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the correctness of the announce response
|
||||||
|
*
|
||||||
|
* @param resource $socket The socket resource.
|
||||||
|
* @param int $transaction_id The transaction ID.
|
||||||
|
* @return string Response data.
|
||||||
|
*/
|
||||||
|
private function udp_verify_announce( $socket, $transaction_id ) {
|
||||||
|
if ( false === ( $response = @socket_read( $socket, 20 ) ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( strlen( $response ) < 20 ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = unpack( 'Naction/Ntransaction_id', $response );
|
||||||
|
if ( 1 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the socket response and returns the torrent data
|
||||||
|
*
|
||||||
|
* @param string $response Data from the request response.
|
||||||
|
* @param array $hashes List (>1) or string of infohash(es).
|
||||||
|
* @param string $host Domain or IP address of the tracker.
|
||||||
|
* @param string $keys Keys for the unpacked information.
|
||||||
|
* @param int $start Start of the content we want to unpack.
|
||||||
|
* @param int $end End of the content we want to unpack.
|
||||||
|
* @param int $offset Offset to the next content part.
|
||||||
|
* @return array Scraped torrent data.
|
||||||
|
*/
|
||||||
|
private function udp_scrape_data( $response, $hashes, $host, $keys, $start, $end, $offset ) {
|
||||||
|
$torrents_data = array();
|
||||||
|
|
||||||
|
foreach ( $hashes as $infohash ) {
|
||||||
|
$byte_string = substr( $response, $start, $end );
|
||||||
|
$data = unpack( 'N', $byte_string );
|
||||||
|
$content = $data[1];
|
||||||
|
if ( ! empty( $content ) ) {
|
||||||
|
$results = unpack( $keys, $byte_string );
|
||||||
|
$torrents_data[ $infohash ] = $results;
|
||||||
|
} else {
|
||||||
|
$this->collect_infohash( $infohash );
|
||||||
|
$this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.';
|
||||||
|
}
|
||||||
|
$start += $offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $torrents_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects info-hashes that couldn't be scraped.
|
||||||
|
*
|
||||||
|
* @param string $infohash Infohash that wasn't scraped.
|
||||||
|
*/
|
||||||
|
private function collect_infohash( $infohash ) {
|
||||||
|
$this->infohashes[] = $infohash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there are any errors
|
||||||
|
*
|
||||||
|
* @return bool True or false, depending if errors are present or not.
|
||||||
|
*/
|
||||||
|
public function has_errors() {
|
||||||
|
return ! empty( $this->errors );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the errors that were logged
|
||||||
|
*
|
||||||
|
* @return array All the logged errors.
|
||||||
|
*/
|
||||||
|
public function get_errors() {
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
}
|
@ -129,6 +129,10 @@
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.padding-y-4 {
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
.padding-x-4 {
|
.padding-x-4 {
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
|
@ -88,6 +88,17 @@ else
|
|||||||
{
|
{
|
||||||
if ($magnet = $db->getMagnet($result->magnetid))
|
if ($magnet = $db->getMagnet($result->magnetid))
|
||||||
{
|
{
|
||||||
|
// Get access info
|
||||||
|
$accessRead = ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved));
|
||||||
|
$accessEdit = ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST));
|
||||||
|
|
||||||
|
// Update magnet viwed
|
||||||
|
if ($accessRead)
|
||||||
|
{
|
||||||
|
$db->addMagnetView($magnet->magnetId, $userId, time());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keywords
|
||||||
$keywords = [];
|
$keywords = [];
|
||||||
|
|
||||||
foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $keyword)
|
foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $keyword)
|
||||||
@ -95,6 +106,57 @@ else
|
|||||||
$keywords[] = $db->getKeywordTopic($keyword->keywordTopicId)->value;
|
$keywords[] = $db->getKeywordTopic($keyword->keywordTopicId)->value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scrapes
|
||||||
|
$localScrape = (object)
|
||||||
|
[
|
||||||
|
'seeders' => 0,
|
||||||
|
'completed' => 0,
|
||||||
|
'leechers' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$totalScrape = (object)
|
||||||
|
[
|
||||||
|
'seeders' => 0,
|
||||||
|
'completed' => 0,
|
||||||
|
'leechers' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$trackers = [];
|
||||||
|
|
||||||
|
foreach (TRACKER_LINKS as $tracker)
|
||||||
|
{
|
||||||
|
$trackers[] = $tracker->announce;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($db->findAddressTrackerByMagnetId($magnet->magnetId) as $magnetToAddressTracker)
|
||||||
|
{
|
||||||
|
if ($addressTracker = $db->getAddressTracker($magnetToAddressTracker->addressTrackerId))
|
||||||
|
{
|
||||||
|
$scheme = $db->getScheme($addressTracker->schemeId);
|
||||||
|
$host = $db->getHost($addressTracker->hostId);
|
||||||
|
$port = $db->getPort($addressTracker->portId);
|
||||||
|
$uri = $db->getUri($addressTracker->uriId);
|
||||||
|
|
||||||
|
$url = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
|
||||||
|
$host->value,
|
||||||
|
$port->value,
|
||||||
|
$uri->value) : sprintf('%s://%s%s', $scheme->value,
|
||||||
|
$host->value,
|
||||||
|
$uri->value);
|
||||||
|
|
||||||
|
if (in_array($url, $trackers))
|
||||||
|
{
|
||||||
|
$localScrape->seeders += (int) $magnetToAddressTracker->seeders;
|
||||||
|
$localScrape->completed += (int) $magnetToAddressTracker->completed;
|
||||||
|
$localScrape->leechers += (int) $magnetToAddressTracker->leechers;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalScrape->seeders += (int) $magnetToAddressTracker->seeders;
|
||||||
|
$totalScrape->completed += (int) $magnetToAddressTracker->completed;
|
||||||
|
$totalScrape->leechers += (int) $magnetToAddressTracker->leechers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$response->magnets[] = (object)
|
$response->magnets[] = (object)
|
||||||
[
|
[
|
||||||
'magnetId' => $magnet->magnetId,
|
'magnetId' => $magnet->magnetId,
|
||||||
@ -128,9 +190,14 @@ else
|
|||||||
],
|
],
|
||||||
'access' => (object)
|
'access' => (object)
|
||||||
[
|
[
|
||||||
'read' => ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved)),
|
'read' => $accessRead,
|
||||||
'edit' => ($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST)),
|
'edit' => $accessEdit,
|
||||||
],
|
],
|
||||||
|
'scrape' => (object)
|
||||||
|
[
|
||||||
|
'local' => $localScrape,
|
||||||
|
'total' => $totalScrape
|
||||||
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,8 +265,23 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL ?>
|
|||||||
<?php echo !$magnet->public || !$magnet->approved ? 'opacity-06 opacity-hover-1' : false ?>">
|
<?php echo !$magnet->public || !$magnet->approved ? 'opacity-06 opacity-hover-1' : false ?>">
|
||||||
<div class="<?php echo $magnet->sensitive ? 'bloor-2 bloor-hover-0' : false ?>">
|
<div class="<?php echo $magnet->sensitive ? 'bloor-2 bloor-hover-0' : false ?>">
|
||||||
<a name="magnet-<?php echo $magnet->magnetId ?>"></a>
|
<a name="magnet-<?php echo $magnet->magnetId ?>"></a>
|
||||||
<h2><?php echo $magnet->metaTitle ?></h2>
|
<h2 class="margin-b-8"><?php echo $magnet->metaTitle ?></h2>
|
||||||
<div class="float-right opacity-0 parent-hover-opacity-09">
|
<div class="float-right opacity-0 parent-hover-opacity-09">
|
||||||
|
<?php if (!$magnet->public) { ?>
|
||||||
|
<span class="margin-l-8" title="<?php echo _('Private') ?>">
|
||||||
|
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="m10.79 12.912-1.614-1.615a3.5 3.5 0 0 1-4.474-4.474l-2.06-2.06C.938 6.278 0 8 0 8s3 5.5 8 5.5a7.029 7.029 0 0 0 2.79-.588zM5.21 3.088A7.028 7.028 0 0 1 8 2.5c5 0 8 5.5 8 5.5s-.939 1.721-2.641 3.238l-2.062-2.062a3.5 3.5 0 0 0-4.474-4.474L5.21 3.089z"/>
|
||||||
|
<path d="M5.525 7.646a2.5 2.5 0 0 0 2.829 2.829l-2.83-2.829zm4.95.708-2.829-2.83a2.5 2.5 0 0 1 2.829 2.829zm3.171 6-12-12 .708-.708 12 12-.708.708z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<?php } ?>
|
||||||
|
<?php if (!$magnet->approved) { ?>
|
||||||
|
<span class="margin-l-8" title="<?php echo _('Waiting for approve') ?>">
|
||||||
|
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hourglass-split" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<?php } ?>
|
||||||
<?php if ($magnet->access->edit) { ?>
|
<?php if ($magnet->access->edit) { ?>
|
||||||
<a class="text-color-green margin-l-12" href="<?php echo WEBSITE_URL ?>/edit.php?magnetId=<?php echo $magnet->magnetId ?>" title="<?php echo _('Edit') ?>">
|
<a class="text-color-green margin-l-12" href="<?php echo WEBSITE_URL ?>/edit.php?magnetId=<?php echo $magnet->magnetId ?>" title="<?php echo _('Edit') ?>">
|
||||||
<svg class="text-color-green" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16">
|
<svg class="text-color-green" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16">
|
||||||
@ -229,24 +311,41 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL ?>
|
|||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
</div>
|
||||||
<?php } ?>
|
<?php } ?>
|
||||||
</div>
|
<div class="width-100 padding-y-4"></div>
|
||||||
<div class="margin-t-8">
|
<!-- DOUBTS
|
||||||
<?php if (!$magnet->public) { ?>
|
<span class="margin-t-8 margin-r-8 cursor-default" title="<?php echo $magnet->timeUpdated ? _('Updated') : _('Added') ?>">
|
||||||
<span class="margin-r-8" title="<?php echo _('Private') ?>">
|
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clock" viewBox="0 0 16 16">
|
||||||
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash-fill" viewBox="0 0 16 16">
|
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"/>
|
||||||
<path d="m10.79 12.912-1.614-1.615a3.5 3.5 0 0 1-4.474-4.474l-2.06-2.06C.938 6.278 0 8 0 8s3 5.5 8 5.5a7.029 7.029 0 0 0 2.79-.588zM5.21 3.088A7.028 7.028 0 0 1 8 2.5c5 0 8 5.5 8 5.5s-.939 1.721-2.641 3.238l-2.062-2.062a3.5 3.5 0 0 0-4.474-4.474L5.21 3.089z"/>
|
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||||
<path d="M5.525 7.646a2.5 2.5 0 0 0 2.829 2.829l-2.83-2.829zm4.95.708-2.829-2.83a2.5 2.5 0 0 1 2.829 2.829zm3.171 6-12-12 .708-.708 12 12-.708.708z"/>
|
</svg>
|
||||||
</svg>
|
<sup><?php echo $magnet->timeUpdated ? $magnet->timeUpdated : $magnet->timeAdded ?></sup>
|
||||||
</span>
|
</span>
|
||||||
<?php } ?>
|
-->
|
||||||
<?php if (!$magnet->approved) { ?>
|
<span class="margin-t-8 margin-r-8 cursor-default">
|
||||||
<span class="margin-r-8" title="<?php echo _('Waiting for approve') ?>">
|
<sup>
|
||||||
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-hourglass-split" viewBox="0 0 16 16">
|
<?php echo $magnet->timeUpdated ? _('Updated') : _('Added') ?>
|
||||||
<path d="M2.5 15a.5.5 0 1 1 0-1h1v-1a4.5 4.5 0 0 1 2.557-4.06c.29-.139.443-.377.443-.59v-.7c0-.213-.154-.451-.443-.59A4.5 4.5 0 0 1 3.5 3V2h-1a.5.5 0 0 1 0-1h11a.5.5 0 0 1 0 1h-1v1a4.5 4.5 0 0 1-2.557 4.06c-.29.139-.443.377-.443.59v.7c0 .213.154.451.443.59A4.5 4.5 0 0 1 12.5 13v1h1a.5.5 0 0 1 0 1h-11zm2-13v1c0 .537.12 1.045.337 1.5h6.326c.216-.455.337-.963.337-1.5V2h-7zm3 6.35c0 .701-.478 1.236-1.011 1.492A3.5 3.5 0 0 0 4.5 13s.866-1.299 3-1.48V8.35zm1 0v3.17c2.134.181 3 1.48 3 1.48a3.5 3.5 0 0 0-1.989-3.158C8.978 9.586 8.5 9.052 8.5 8.351z"/>
|
<?php echo $magnet->timeUpdated ? $magnet->timeUpdated : $magnet->timeAdded ?>
|
||||||
</svg>
|
</sup>
|
||||||
</span>
|
</span>
|
||||||
<?php } ?>
|
<span class="margin-t-8 margin-r-8 cursor-default opacity-0 parent-hover-opacity-09" title="<?php echo _('Seeders - yggdrasil / total') ?>">
|
||||||
<sup><?php echo $magnet->timeUpdated ? sprintf('Updated %s', $magnet->timeUpdated) : sprintf('Added %s', $magnet->timeAdded) ?></sup>
|
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-up" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"/>
|
||||||
|
</svg>
|
||||||
|
<sup><?php echo $magnet->scrape->local->seeders ?> / <?php echo $magnet->scrape->total->seeders ?></sup>
|
||||||
|
</span>
|
||||||
|
<span class="margin-t-8 margin-r-8 cursor-default opacity-0 parent-hover-opacity-09" title="<?php echo _('Completed - yggdrasil / total') ?>">
|
||||||
|
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-down" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 1a.5.5 0 0 1 .5.5v11.793l3.146-3.147a.5.5 0 0 1 .708.708l-4 4a.5.5 0 0 1-.708 0l-4-4a.5.5 0 0 1 .708-.708L7.5 13.293V1.5A.5.5 0 0 1 8 1z"/>
|
||||||
|
</svg>
|
||||||
|
<sup><?php echo $magnet->scrape->local->completed ?> / <?php echo $magnet->scrape->total->completed ?></sup>
|
||||||
|
</span>
|
||||||
|
<span class="margin-t-8 margin-r-8 cursor-default opacity-0 parent-hover-opacity-09" title="<?php echo _('Leechers - yggdrasil / total') ?>">
|
||||||
|
<svg class="width-13px" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cup-hot" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M.5 6a.5.5 0 0 0-.488.608l1.652 7.434A2.5 2.5 0 0 0 4.104 16h5.792a2.5 2.5 0 0 0 2.44-1.958l.131-.59a3 3 0 0 0 1.3-5.854l.221-.99A.5.5 0 0 0 13.5 6H.5ZM13 12.5a2.01 2.01 0 0 1-.316-.025l.867-3.898A2.001 2.001 0 0 1 13 12.5ZM2.64 13.825 1.123 7h11.754l-1.517 6.825A1.5 1.5 0 0 1 9.896 15H4.104a1.5 1.5 0 0 1-1.464-1.175Z"/>
|
||||||
|
<path d="m4.4.8-.003.004-.014.019a4.167 4.167 0 0 0-.204.31 2.327 2.327 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.31 3.31 0 0 1-.202.388 5.444 5.444 0 0 1-.253.382l-.018.025-.005.008-.002.002A.5.5 0 0 1 3.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 3.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 3 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 4.4.8Zm3 0-.003.004-.014.019a4.167 4.167 0 0 0-.204.31 2.327 2.327 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.31 3.31 0 0 1-.202.388 5.444 5.444 0 0 1-.253.382l-.018.025-.005.008-.002.002A.5.5 0 0 1 6.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 6.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 6 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 7.4.8Zm3 0-.003.004-.014.019a4.077 4.077 0 0 0-.204.31 2.337 2.337 0 0 0-.141.267c-.026.06-.034.092-.037.103v.004a.593.593 0 0 0 .091.248c.075.133.178.272.308.445l.01.012c.118.158.26.347.37.543.112.2.22.455.22.745 0 .188-.065.368-.119.494a3.198 3.198 0 0 1-.202.388 5.385 5.385 0 0 1-.252.382l-.019.025-.005.008-.002.002A.5.5 0 0 1 9.6 4.2l.003-.004.014-.019a4.149 4.149 0 0 0 .204-.31 2.06 2.06 0 0 0 .141-.267c.026-.06.034-.092.037-.103a.593.593 0 0 0-.09-.252A4.334 4.334 0 0 0 9.6 2.8l-.01-.012a5.099 5.099 0 0 1-.37-.543A1.53 1.53 0 0 1 9 1.5c0-.188.065-.368.119-.494.059-.138.134-.274.202-.388a5.446 5.446 0 0 1 .253-.382l.025-.035A.5.5 0 0 1 10.4.8Z"/>
|
||||||
|
</svg>
|
||||||
|
<sup><?php echo $magnet->scrape->local->leechers ?> / <?php echo $magnet->scrape->total->leechers ?></sup>
|
||||||
|
</span>
|
||||||
<span class="float-right margin-l-12">
|
<span class="float-right margin-l-12">
|
||||||
<a href="<?php echo WEBSITE_URL ?>/action.php?target=star&magnetId=<?php echo $magnet->magnetId ?>&callback=<?php echo base64_encode(sprintf('%s/index.php?query=%s#magnet-%s', WEBSITE_URL, urlencode($request->query), $magnet->magnetId)) ?>" title="<?php echo _('Star') ?>">
|
<a href="<?php echo WEBSITE_URL ?>/action.php?target=star&magnetId=<?php echo $magnet->magnetId ?>&callback=<?php echo base64_encode(sprintf('%s/index.php?query=%s#magnet-%s', WEBSITE_URL, urlencode($request->query), $magnet->magnetId)) ?>" title="<?php echo _('Star') ?>">
|
||||||
<?php if ($magnet->star->status) { ?>
|
<?php if ($magnet->star->status) { ?>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user