Files
XC_VM/src/modules/watch/WatchCron.php
T
Divarion-D 6cc892f9a7 refactor: eliminate proxy functions, migrate to direct class method calls
312 files changed, 2866 insertions(+), 24110 deletions(-)

Proxy function replacements (130+ functions):
- goHome() -> AdminHelpers::goHome()
- checkPermissions() -> PageAuthorization::checkPermissions()
- getBouquet/getCategory/etc -> Service::getById()
- deleteStream/deleteLine/etc -> Repository/Service::delete()
- APIRequest/systemapirequest -> ApiClient::request/systemRequest()
- prepareArray/preparecolumn -> QueryHelper methods
- confirmIDs/parserelease/etc -> AdminHelpers methods
- getSettings/clearSettingsCache -> SettingsManager methods
- destroySession -> SessionManager::clearContext()
- getMag/getEnigma/deleteMAG/etc -> MagService/EnigmaService
- getSeriesTrailer/getMovieTMDB/etc -> TMDbService methods

Removed legacy files:
- src/includes/admin.php (main proxy hub)
- src/includes/libs/ (35+ files moved to core/modules)
- src/includes/python/ (moved to bin/python)
- src/includes/api/ (moved to Controllers)
- src/includes/ts.php, reseller_api.php
- src/infrastructure/legacy/admin_proxies.php
- tools/run_scan.sh, update_redis_conf.sh

Additional fixes:
- SQL query placeholder migration (BouquetService, CategoryService)
- PHP 8.x null-safe access (live.php, server_view.php, stream.php)
- TmdbCron: extracted variable for repeated similar_text calls
2026-04-09 21:24:48 +03:00

250 lines
13 KiB
PHP

<?php
/**
* WatchCron — крон-задача Watch Folder.
*
* @package XC_VM_Module_Watch
* @author Divarion_D <https://github.com/Divarion-D>
* @copyright 2025-2026 Vateron Media
* @link https://github.com/Vateron-Media/XC_VM
* @license AGPL-3.0 https://www.gnu.org/licenses/agpl-3.0.html
*/
require_once __DIR__ . '/../../core/Process/Thread.php';
require_once __DIR__ . '/../../core/Process/Multithread.php';
class WatchCron {
/**
* Получить категории Watch из БД.
*
* @param int|null $rType Тип категории (1=movie, 2=series)
* @return array
*/
public static function getWatchCategories($rType = null) {
global $db;
$rReturn = array();
if ($rType) {
$db->query('SELECT * FROM `watch_categories` WHERE `type` = ? ORDER BY `genre_id` ASC;', $rType);
} else {
$db->query('SELECT * FROM `watch_categories` ORDER BY `genre_id` ASC;');
}
foreach ($db->get_rows() as $rRow) {
$rReturn[$rRow['genre_id']] = $rRow;
}
return $rReturn;
}
/**
* Получить букет по ID.
*
* @param int $rID
* @return array|null
*/
public static function getBouquet($rID) {
global $db;
$db->query('SELECT * FROM `bouquets` WHERE `id` = ?;', $rID);
if ($db->num_rows() != 1) {
} else {
return $db->get_row();
}
}
/**
* Обработать файлы букетов во временной директории.
*/
public static function checkBouquets() {
global $db;
$a39a336ad3894348 = array();
$rBouquets = glob(WATCH_TMP_PATH . '*.bouquet');
foreach ($rBouquets as $D3e2134ebfab5c71) {
$rBouquet = json_decode(file_get_contents($D3e2134ebfab5c71), true);
if (isset($a39a336ad3894348[$rBouquet['bouquet_id']])) {
} else {
$a39a336ad3894348[$rBouquet['bouquet_id']] = array('movie' => array(), 'series' => array());
}
$a39a336ad3894348[$rBouquet['bouquet_id']][$rBouquet['type']][] = $rBouquet['id'];
unlink($D3e2134ebfab5c71);
}
foreach ($a39a336ad3894348 as $rBouquetID => $rBouquetData) {
$rBouquet = self::getBouquet($rBouquetID);
if (!$rBouquet) {
} else {
foreach (array('movie', 'series') as $rType) {
if ($rType == 'movie') {
$rColumn = 'bouquet_movies';
} else {
$rColumn = 'bouquet_series';
}
$rChannels = json_decode($rBouquet[$rColumn], true);
foreach ($rBouquetData[$rType] as $rID) {
if (0 >= intval($rID) || in_array($rID, $rChannels)) {
} else {
$rChannels[] = $rID;
}
}
$db->query('UPDATE `bouquets` SET `' . $rColumn . '` = ? WHERE `id` = ?;', '[' . implode(',', array_map('intval', $rChannels)) . ']', $rBouquetID);
}
}
}
}
/**
* Основная точка входа крона Watch Folder.
* Заменяет loadCron().
*/
public static function run() {
global $db;
global $rThreadCount;
global $rScanOffset;
global $F7fa29461a8a5ee2;
global $rForce;
$rWatchCategories = array(1 => self::getWatchCategories(1), 2 => self::getWatchCategories(2));
if (0 >= count(glob(WATCH_TMP_PATH . '*.bouquet'))) {
} else {
self::checkBouquets();
}
if (!$rForce) {
$db->query("SELECT * FROM `watch_folders` WHERE `type` <> 'plex' AND `server_id` = ? AND `active` = 1 AND (UNIX_TIMESTAMP() - `last_run` > ? OR `last_run` IS NULL) ORDER BY `id` ASC;", SERVER_ID, $rScanOffset);
} else {
$db->query("SELECT * FROM `watch_folders` WHERE `type` <> 'plex' AND `server_id` = ? AND `id` = ?;", SERVER_ID, $rForce);
}
$rRows = $db->get_rows();
if (0 >= count($rRows)) {
} else {
shell_exec('rm -f ' . WATCH_TMP_PATH . '*.wpid');
$rSeriesTMDB = $rStreamDatabase = array();
$rTMDBDatabase = array('movie' => array(), 'series' => array());
echo 'Generating cache...' . "\n";
$db->query('SELECT `id`, `tmdb_id` FROM `streams_series` WHERE `tmdb_id` IS NOT NULL AND `tmdb_id` > 0;');
foreach ($db->get_rows() as $rRow) {
$rSeriesTMDB[$rRow['id']] = $rRow['tmdb_id'];
}
$db->query('SELECT `streams`.`id`, `streams_episodes`.`series_id`, `streams_episodes`.`season_num`, `streams_episodes`.`episode_num`, `streams`.`stream_source` FROM `streams_episodes` LEFT JOIN `streams` ON `streams`.`id` = `streams_episodes`.`stream_id` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams_servers`.`server_id` = ?;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rStreamDatabase[] = $rRow['stream_source'];
$rTMDBID = $rSeriesTMDB[$rRow['series_id']];
if (!$rTMDBID) {
} else {
list($rSource) = json_decode($rRow['stream_source'], true);
$rTMDBDatabase['series'][$rTMDBID][$rRow['season_num'] . '_' . $rRow['episode_num']] = array('id' => $rRow['id'], 'source' => $rSource);
}
}
$db->query('SELECT `streams`.`id`, `streams`.`stream_source`, `streams`.`movie_properties` FROM `streams` LEFT JOIN `streams_servers` ON `streams_servers`.`stream_id` = `streams`.`id` WHERE `streams`.`type` = 2 AND `streams_servers`.`server_id` = ?;', SERVER_ID);
foreach ($db->get_rows() as $rRow) {
$rStreamDatabase[] = $rRow['stream_source'];
$rTMDBID = (json_decode($rRow['movie_properties'], true)['tmdb_id'] ?: null);
if (!$rTMDBID) {
} else {
list($rSource) = json_decode($rRow['stream_source'], true);
$rTMDBDatabase['movie'][$rTMDBID] = array('id' => $rRow['id'], 'source' => $rSource);
}
}
exec('find ' . WATCH_TMP_PATH . ' -maxdepth 1 -name "*.cache" -print0 | xargs -0 rm');
foreach ($rTMDBDatabase['series'] as $rTMDBID => $rData) {
file_put_contents(WATCH_TMP_PATH . 'series_' . $rTMDBID . '.cache', json_encode($rData));
}
foreach ($rTMDBDatabase['movie'] as $rTMDBID => $rData) {
file_put_contents(WATCH_TMP_PATH . 'movie_' . $rTMDBID . '.cache', json_encode($rData));
}
unset($rTMDBDatabase);
echo 'Finished generating cache!' . "\n";
}
foreach ($rRows as $rRow) {
$db->query('UPDATE `watch_folders` SET `last_run` = UNIX_TIMESTAMP() WHERE `id` = ?;', $rRow['id']);
$rExtensions = json_decode($rRow['allowed_extensions'], true);
if ($rExtensions) {
} else {
$rExtensions = array();
}
if (count($rExtensions) == 0) {
$rExtensions = array('mp4', 'mkv', 'avi', 'mpg', 'flv', '3gp', 'm4v', 'flv', 'wmv', 'mov', 'ts');
}
$rSubtitles = $rFiles = array();
if (0 < strlen($rRow['rclone_dir'])) {
$rCommand = 'rclone --config "' . CONFIG_PATH . 'rclone.conf" lsjson ' . escapeshellarg($rRow['rclone_dir']) . ' -R --fast-list --files-only';
exec($rCommand, $a364ed03b3639bd1, $Ee034ad5c6b0c8a3);
$rData = implode(' ', $a364ed03b3639bd1);
if (!substr($rData, 0, 1) != '[') {
} else {
$rData = '[' . explode('[', $rData, 1)[1];
}
$a364ed03b3639bd1 = json_decode($rData, true);
foreach ($a364ed03b3639bd1 as $rFile) {
$rFile['Path'] = rtrim($rRow['directory'], '/') . '/' . $rFile['Path'];
if (!(count($rExtensions) == 0 || in_array(strtolower(pathinfo($rFile['Name'])['extension']), $rExtensions))) {
} else {
$rFiles[] = $rFile['Path'];
}
if (!isset($rRow['auto_subtitles'])) {
} else {
if (!in_array(strtolower(pathinfo($rFile['Path'])['extension']), array('srt', 'sub', 'sbv'))) {
} else {
$rSubtitles[] = $rFile['Path'];
}
}
}
} else {
if (0 < count($rExtensions)) {
$rExtensions = escapeshellcmd(implode('|', $rExtensions));
$rCommand = '/usr/bin/find "' . escapeshellcmd($rRow['directory']) . '" -regex ".*\\.\\(' . $rExtensions . '\\)"';
} else {
$rCommand = '/usr/bin/find "' . escapeshellcmd($rRow['directory']) . '"';
}
exec($rCommand, $rFiles, $Ee034ad5c6b0c8a3);
if (isset($rRow['auto_subtitles'])) {
$rExtensions = escapeshellcmd(implode('|', array('srt', 'sub', 'sbv')));
$rCommand = '/usr/bin/find "' . escapeshellcmd($rRow['directory']) . '" -regex ".*\\.\\(' . $rExtensions . '\\)"';
exec($rCommand, $rSubtitles, $Ee034ad5c6b0c8a3);
} else {
$rSubtitles = array();
}
}
$rThreadData = array();
foreach ($rFiles as $rFile) {
if (time() - filemtime($rFile) >= 30) {
if (in_array(json_encode(array('s:' . SERVER_ID . ':' . $rFile), JSON_UNESCAPED_UNICODE), $rStreamDatabase)) {
} else {
$rPathInfo = pathinfo($rFile);
$d8c5b5dc1e354db6 = array();
if (!isset($rRow['auto_subtitles'])) {
} else {
foreach (array('srt', 'sub', 'sbv') as $rExt) {
$rSubtitle = $rPathInfo['dirname'] . '/' . $rPathInfo['filename'] . '.' . $rExt;
if (!in_array($rSubtitle, $rSubtitles)) {
} else {
$d8c5b5dc1e354db6 = array('files' => array($rSubtitle), 'names' => array('Subtitles'), 'charset' => array('UTF-8'), 'location' => SERVER_ID);
break;
}
}
}
$rThreadData[] = array('folder_id' => $rRow['id'], 'type' => $rRow['type'], 'directory' => $rRow['directory'], 'file' => $rFile, 'subtitles' => $d8c5b5dc1e354db6, 'category_id' => $rRow['category_id'], 'bouquets' => $rRow['bouquets'], 'disable_tmdb' => $rRow['disable_tmdb'], 'ignore_no_match' => $rRow['ignore_no_match'], 'fb_bouquets' => $rRow['fb_bouquets'], 'fb_category_id' => $rRow['fb_category_id'], 'language' => $rRow['language'], 'watch_categories' => $rWatchCategories, 'read_native' => $rRow['read_native'], 'movie_symlink' => $rRow['movie_symlink'], 'remove_subtitles' => $rRow['remove_subtitles'], 'auto_encode' => $rRow['auto_encode'], 'auto_upgrade' => $rRow['auto_upgrade'], 'fallback_title' => $rRow['fallback_title'], 'ffprobe_input' => $rRow['ffprobe_input'], 'transcode_profile_id' => $rRow['transcode_profile_id'], 'max_genres' => intval(SettingsManager::getAll()['max_genres']), 'duplicate_tmdb' => $rRow['duplicate_tmdb'], 'target_container' => $rRow['target_container'], 'alternative_titles' => SettingsManager::getAll()['alternative_titles'], 'fallback_parser' => SettingsManager::getAll()['fallback_parser']);
if (!(0 < $F7fa29461a8a5ee2 && count($rThreadData) == $F7fa29461a8a5ee2)) {
} else {
break;
}
}
}
}
if (count($rThreadData) > 0) {
echo 'Scan complete! Adding ' . count($rThreadData) . ' files...' . "\n";
}
$cacheDataKey = array();
foreach ($rThreadData as $rData) {
$rCommand = '/usr/bin/timeout 60 ' . PHP_BIN . ' ' . MAIN_HOME . 'console.php watch_item "' . base64_encode(json_encode($rData, JSON_UNESCAPED_UNICODE)) . '"';
$cacheDataKey[] = $rCommand;
}
$db->close_mysql();
if ($rThreadCount <= 1) {
foreach ($cacheDataKey as $rCommand) {
shell_exec($rCommand);
}
} else {
$cacheMetadataKey = new Multithread($cacheDataKey, $rThreadCount);
$cacheMetadataKey->run();
}
$db->db_connect();
self::checkBouquets();
}
}
}