mirror of
https://github.com/ceratic/MediaCollectorLibary.git
synced 2026-05-13 23:56:46 +02:00
...
This commit is contained in:
@@ -10,7 +10,6 @@ use Exception;
|
||||
class XbvrSyncService extends BaseSyncService
|
||||
{
|
||||
private Client $httpClient;
|
||||
private ?string $apiKey;
|
||||
private string $baseUrl;
|
||||
private ImageDownloader $imageDownloader;
|
||||
private int $processedCount = 0;
|
||||
@@ -22,24 +21,22 @@ class XbvrSyncService extends BaseSyncService
|
||||
parent::__construct($pdo, $source);
|
||||
|
||||
// Initialize properties first before using them
|
||||
$this->apiKey = $source['api_key'];
|
||||
$this->baseUrl = rtrim($source['api_url'], '/');
|
||||
|
||||
$this->httpClient = new Client([
|
||||
'timeout' => 30,
|
||||
'headers' => [
|
||||
'User-Agent' => 'MediaCollector/1.0',
|
||||
'X-API-Key' => $source['api_key']
|
||||
'User-Agent' => 'MediaCollector/1.0'
|
||||
]
|
||||
]);
|
||||
|
||||
$this->imageDownloader = new ImageDownloader('public/images', $this->apiKey);
|
||||
$this->imageDownloader = new ImageDownloader('public/images');
|
||||
}
|
||||
|
||||
protected function executeSync(string $syncType): void
|
||||
{
|
||||
if (empty($this->apiKey) || empty($this->baseUrl)) {
|
||||
throw new Exception('XBVR API key and URL not configured');
|
||||
if (empty($this->baseUrl)) {
|
||||
throw new Exception('XBVR URL not configured');
|
||||
}
|
||||
|
||||
$this->logProgress('Starting XBVR library sync...');
|
||||
@@ -52,12 +49,17 @@ class XbvrSyncService extends BaseSyncService
|
||||
|
||||
private function syncScenes(): void
|
||||
{
|
||||
try {
|
||||
try {
|
||||
$scenes = $this->getXbvrScenes();
|
||||
|
||||
foreach ($scenes as $sceneData) {
|
||||
$this->syncScene($sceneData);
|
||||
$this->processedCount++;
|
||||
try {
|
||||
$this->syncScene($sceneData);
|
||||
$this->processedCount++;
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Error processing XBVR scene {$sceneData['id']}: " . $e->getMessage());
|
||||
$this->processedCount++; // Still count as processed even if failed
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress('Error syncing XBVR scenes: ' . $e->getMessage());
|
||||
@@ -67,16 +69,73 @@ class XbvrSyncService extends BaseSyncService
|
||||
private function getXbvrScenes(): array
|
||||
{
|
||||
try {
|
||||
// XBVR API endpoint for scenes
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/api/scene");
|
||||
$this->logProgress("Fetching XBVR DeoVR main response from: {$this->baseUrl}/deovr");
|
||||
|
||||
$data = json_decode($response->getBody(), true);
|
||||
// Step 1: Fetch the main DeoVR response containing the video list
|
||||
$response = $this->httpClient->get("{$this->baseUrl}/deovr", [
|
||||
'timeout' => 30,
|
||||
'connect_timeout' => 10
|
||||
]);
|
||||
|
||||
if (!isset($data['scenes'])) {
|
||||
throw new Exception('No scenes found in XBVR');
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new Exception("XBVR DeoVR API returned status: " . $response->getStatusCode());
|
||||
}
|
||||
|
||||
return $data['scenes'];
|
||||
$mainData = json_decode($response->getBody(), true);
|
||||
$this->logProgress("XBVR DeoVR main response received successfully");
|
||||
|
||||
// Step 2: Extract the video list from the main response
|
||||
$videoList = $this->extractVideoList($mainData);
|
||||
$videoList = $videoList[0]['list'];
|
||||
|
||||
if (empty($videoList)) {
|
||||
throw new Exception("No videos found in XBVR DeoVR response");
|
||||
}
|
||||
|
||||
|
||||
|
||||
$this->logProgress("Found " . count($videoList) . " videos in XBVR list");
|
||||
|
||||
// Step 3: Fetch detailed information for each video
|
||||
$detailedScenes = [];
|
||||
$processedCount = 0;
|
||||
|
||||
foreach ($videoList as $videoItem) {
|
||||
|
||||
try {
|
||||
$detailUrl = $this->extractDetailUrl($videoItem);
|
||||
if (!$detailUrl) {
|
||||
$this->logProgress("No detail URL found for video: " . ($videoItem['title'] ?? 'Unknown'));
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logProgress("Fetching details for: " . ($videoItem['title'] ?? 'Unknown'));
|
||||
|
||||
$detailResponse = $this->httpClient->get($detailUrl, [
|
||||
'timeout' => 30,
|
||||
'connect_timeout' => 10
|
||||
]);
|
||||
|
||||
if ($detailResponse->getStatusCode() === 200) {
|
||||
$detailData = json_decode($detailResponse->getBody(), true);
|
||||
$detailedScenes[] = $detailData;
|
||||
$processedCount++;
|
||||
$this->logProgress("Successfully fetched details for: " . ($detailData['title'] ?? 'Unknown'));
|
||||
} else {
|
||||
$this->logProgress("Failed to fetch details from {$detailUrl}: Status " . $detailResponse->getStatusCode());
|
||||
}
|
||||
|
||||
// Add small delay to be respectful to the API
|
||||
usleep(100000); // 0.1 second delay
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Error fetching details for video: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->logProgress("Successfully processed {$processedCount} out of " . count($videoList) . " videos");
|
||||
return $detailedScenes;
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('Failed to fetch XBVR scenes: ' . $e->getMessage());
|
||||
}
|
||||
@@ -84,6 +143,8 @@ class XbvrSyncService extends BaseSyncService
|
||||
|
||||
private function syncScene(array $sceneData): void
|
||||
{
|
||||
$this->logProgress("Processing XBVR scene: " . json_encode(array_slice($sceneData, 0, 5)));
|
||||
|
||||
$adultVideoModel = new AdultVideo($this->pdo);
|
||||
|
||||
// Check if scene already exists by xbvr_id in metadata
|
||||
@@ -103,84 +164,221 @@ class XbvrSyncService extends BaseSyncService
|
||||
}
|
||||
}
|
||||
|
||||
// Download images locally
|
||||
$coverFilename = null;
|
||||
$screenshotFilename = null;
|
||||
// Map XBVR/DeoVR fields to our database structure
|
||||
// Based on the 46367.json example structure
|
||||
$mappedData = [
|
||||
'title' => $sceneData['title'] ?? 'Untitled VR Scene',
|
||||
'overview' => $sceneData['description'] ?? null,
|
||||
'release_date' => isset($sceneData['date']) ? date('Y-m-d', $sceneData['date']) : null,
|
||||
'runtime_minutes' => isset($sceneData['videoLength']) ? round($sceneData['videoLength'] / 60) : null,
|
||||
'rating' => $sceneData['rating_avg'] ?? null,
|
||||
'director' => null, // DeoVR doesn't seem to have director info
|
||||
'cast' => [], // Will be extracted from categories/actors if available
|
||||
'tags' => [], // Will be extracted from categories
|
||||
];
|
||||
|
||||
// Extract image URLs from XBVR API response
|
||||
// Handle categories/tags from DeoVR format
|
||||
$tags = [];
|
||||
if (isset($sceneData['categories']) && is_array($sceneData['categories'])) {
|
||||
foreach ($sceneData['categories'] as $category) {
|
||||
if (isset($category['tag']['name'])) {
|
||||
$tags[] = $category['tag']['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
$mappedData['tags'] = $tags;
|
||||
|
||||
// Handle actors (DeoVR format might have actors array or might be null)
|
||||
$castData = [];
|
||||
if (isset($sceneData['actors']) && is_array($sceneData['actors']) && !empty($sceneData['actors'])) {
|
||||
foreach ($sceneData['actors'] as $actor) {
|
||||
if (isset($actor['name'])) {
|
||||
$castData[] = $actor['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logProgress("Mapped DeoVR scene data: title='{$mappedData['title']}', tags=" . json_encode($tags) . ", cast=" . json_encode($castData));
|
||||
|
||||
// Extract image URLs from DeoVR API response - try multiple possible field names
|
||||
$coverUrl = null;
|
||||
$screenshotUrl = null;
|
||||
|
||||
if (!empty($sceneData['cover_url'])) {
|
||||
$coverUrl = $sceneData['cover_url'];
|
||||
$this->logProgress("Cover URL: " . $coverUrl);
|
||||
// Try different possible cover image field names for DeoVR
|
||||
$coverFields = ['thumbnailUrl', 'cover_url', 'cover', 'poster_url', 'poster', 'thumbnail_url', 'thumbnail', 'image_url', 'image'];
|
||||
foreach ($coverFields as $field) {
|
||||
if (!empty($sceneData[$field])) {
|
||||
$coverUrl = $sceneData[$field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($sceneData['screenshot_url'])) {
|
||||
$screenshotUrl = $sceneData['screenshot_url'];
|
||||
$this->logProgress("Screenshot URL: " . $screenshotUrl);
|
||||
// Try different possible screenshot field names for DeoVR
|
||||
$screenshotFields = ['screenshot_url', 'screenshot', 'preview_url', 'preview', 'thumb_url', 'thumb'];
|
||||
foreach ($screenshotFields as $field) {
|
||||
if (!empty($sceneData[$field])) {
|
||||
$screenshotUrl = $sceneData[$field];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($coverUrl)) {
|
||||
$coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover');
|
||||
$localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos');
|
||||
if ($localCoverPath) {
|
||||
$sceneData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
|
||||
$this->logProgress("Downloaded cover: " . $localCoverPath);
|
||||
$this->logProgress("DeoVR Cover URL: " . $coverUrl);
|
||||
}
|
||||
|
||||
if (!empty($screenshotUrl)) {
|
||||
$this->logProgress("DeoVR Screenshot URL: " . $screenshotUrl);
|
||||
}
|
||||
|
||||
if (!empty($coverUrl)) {
|
||||
// Validate URL before attempting download
|
||||
if (filter_var($coverUrl, FILTER_VALIDATE_URL)) {
|
||||
try {
|
||||
$coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover');
|
||||
$localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos');
|
||||
if ($localCoverPath) {
|
||||
$mappedData['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
|
||||
$this->logProgress("Downloaded cover: " . $localCoverPath);
|
||||
} else {
|
||||
$this->logProgress("Failed to download cover from: " . $coverUrl);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Exception downloading cover from {$coverUrl}: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->logProgress("Failed to download cover from: " . $coverUrl);
|
||||
$this->logProgress("Invalid cover URL: " . $coverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($screenshotUrl)) {
|
||||
$screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot');
|
||||
$localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos');
|
||||
if ($localScreenshotPath) {
|
||||
$sceneData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
|
||||
$this->logProgress("Downloaded screenshot: " . $localScreenshotPath);
|
||||
// Validate URL before attempting download
|
||||
if (filter_var($screenshotUrl, FILTER_VALIDATE_URL)) {
|
||||
try {
|
||||
$screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot');
|
||||
$localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos');
|
||||
if ($localScreenshotPath) {
|
||||
$mappedData['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
|
||||
$this->logProgress("Downloaded screenshot: " . $localScreenshotPath);
|
||||
} else {
|
||||
$this->logProgress("Failed to download screenshot from: " . $screenshotUrl);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Exception downloading screenshot from {$screenshotUrl}: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->logProgress("Failed to download screenshot from: " . $screenshotUrl);
|
||||
$this->logProgress("Invalid screenshot URL: " . $screenshotUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle actors
|
||||
$actors = $this->syncActors($sceneData['cast'] ?? []);
|
||||
$actors = $this->syncActors($castData);
|
||||
|
||||
$sceneData = [
|
||||
'title' => $sceneData['title'] ?: 'Untitled VR Scene',
|
||||
'overview' => $sceneData['synopsis'] ?? null,
|
||||
'release_date' => $sceneData['release_date'] ? date('Y-m-d', strtotime($sceneData['release_date'])) : null,
|
||||
'runtime_minutes' => $sceneData['duration'] ?? null,
|
||||
'rating' => $sceneData['rating'] ?? null,
|
||||
$sceneDataForDb = [
|
||||
'title' => $mappedData['title'],
|
||||
'overview' => $mappedData['overview'],
|
||||
'release_date' => $mappedData['release_date'],
|
||||
'runtime_minutes' => $mappedData['runtime_minutes'],
|
||||
'rating' => $mappedData['rating'],
|
||||
'source_id' => $this->source['id'],
|
||||
'external_id' => $sceneData['id'],
|
||||
'metadata' => json_encode([
|
||||
'xbvr_id' => $sceneData['id'],
|
||||
'xbvr_url' => $sceneData['scene_url'] ?? null,
|
||||
'cast' => $sceneData['cast'] ?? [],
|
||||
'xbvr_url' => $sceneData['scene_url'] ?? $sceneData['url'] ?? null,
|
||||
'cast' => $castData,
|
||||
'actors' => $actors,
|
||||
'tags' => $sceneData['tags'] ?? [],
|
||||
'tags' => $mappedData['tags'],
|
||||
'is_available' => $sceneData['is_available'] ?? true,
|
||||
'is_watched' => $sceneData['is_watched'] ?? false,
|
||||
'watch_count' => $sceneData['watch_count'] ?? 0,
|
||||
'video_length' => $sceneData['video_length'] ?? null,
|
||||
'video_length' => $sceneData['videoLength'] ?? null,
|
||||
'video_width' => $sceneData['video_width'] ?? null,
|
||||
'video_height' => $sceneData['video_height'] ?? null,
|
||||
'video_codec' => $sceneData['video_codec'] ?? null,
|
||||
'file_path' => $sceneData['file_path'] ?? null,
|
||||
'cover_url' => $sceneData['cover_url'] ?? null,
|
||||
'local_cover_path' => $sceneData['local_cover_path'] ?? null,
|
||||
'screenshot_url' => $sceneData['screenshot_url'] ?? null,
|
||||
'local_screenshot_path' => $sceneData['local_screenshot_path'] ?? null
|
||||
'file_path' => $sceneData['file_path'] ?? $sceneData['path'] ?? null,
|
||||
'cover_url' => $coverUrl,
|
||||
'local_cover_path' => $mappedData['local_cover_path'] ?? null,
|
||||
'screenshot_url' => $screenshotUrl,
|
||||
'local_screenshot_path' => $mappedData['local_screenshot_path'] ?? null,
|
||||
'deoVR_format' => true, // Mark that this came from DeoVR API
|
||||
'paysite' => $sceneData['paysite']['name'] ?? null,
|
||||
'is3d' => $sceneData['is3d'] ?? false,
|
||||
'screenType' => $sceneData['screenType'] ?? null,
|
||||
'stereoMode' => $sceneData['stereoMode'] ?? null,
|
||||
'fullVideoReady' => $sceneData['fullVideoReady'] ?? false,
|
||||
'fullAccess' => $sceneData['fullAccess'] ?? false
|
||||
])
|
||||
];
|
||||
|
||||
if ($existingScene) {
|
||||
$adultVideoModel->update($existingScene['id'], $sceneData);
|
||||
// For existing scenes, check if we need to update images
|
||||
$existingMetadata = json_decode($existingScene['metadata'], true);
|
||||
|
||||
// Only download images if they don't already exist locally
|
||||
if (empty($existingMetadata['local_cover_path']) && !empty($coverUrl)) {
|
||||
// Validate URL before attempting download
|
||||
if (filter_var($coverUrl, FILTER_VALIDATE_URL)) {
|
||||
try {
|
||||
$coverFilename = $this->imageDownloader->generateFilename($coverUrl, 'cover');
|
||||
$localCoverPath = $this->imageDownloader->downloadImage($coverUrl, $coverFilename, 'adult_videos');
|
||||
if ($localCoverPath) {
|
||||
$sceneDataForDb['local_cover_path'] = $this->imageDownloader->getPublicUrl($localCoverPath);
|
||||
$this->logProgress("Downloaded cover: " . $localCoverPath);
|
||||
} else {
|
||||
$this->logProgress("Failed to download cover from: " . $coverUrl);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Exception downloading cover from {$coverUrl}: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->logProgress("Invalid cover URL: " . $coverUrl);
|
||||
}
|
||||
} else {
|
||||
// Keep existing local cover path
|
||||
$sceneDataForDb['local_cover_path'] = $existingMetadata['local_cover_path'] ?? null;
|
||||
if (!empty($sceneDataForDb['local_cover_path'])) {
|
||||
$this->logProgress("Using existing cover: " . $sceneDataForDb['local_cover_path']);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($existingMetadata['local_screenshot_path']) && !empty($screenshotUrl)) {
|
||||
// Validate URL before attempting download
|
||||
if (filter_var($screenshotUrl, FILTER_VALIDATE_URL)) {
|
||||
try {
|
||||
$screenshotFilename = $this->imageDownloader->generateFilename($screenshotUrl, 'screenshot');
|
||||
$localScreenshotPath = $this->imageDownloader->downloadImage($screenshotUrl, $screenshotFilename, 'adult_videos');
|
||||
if ($localScreenshotPath) {
|
||||
$sceneDataForDb['local_screenshot_path'] = $this->imageDownloader->getPublicUrl($localScreenshotPath);
|
||||
$this->logProgress("Downloaded screenshot: " . $localScreenshotPath);
|
||||
} else {
|
||||
$this->logProgress("Failed to download screenshot from: " . $screenshotUrl);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Exception downloading screenshot from {$screenshotUrl}: " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$this->logProgress("Invalid screenshot URL: " . $screenshotUrl);
|
||||
}
|
||||
} else {
|
||||
// Keep existing local screenshot path
|
||||
$sceneDataForDb['local_screenshot_path'] = $existingMetadata['local_screenshot_path'] ?? null;
|
||||
if (!empty($sceneDataForDb['local_screenshot_path'])) {
|
||||
$this->logProgress("Using existing screenshot: " . $sceneDataForDb['local_screenshot_path']);
|
||||
}
|
||||
}
|
||||
|
||||
$adultVideoModel->update($existingScene['id'], $sceneDataForDb);
|
||||
$adultVideoId = $existingScene['id'];
|
||||
$this->updatedCount++;
|
||||
|
||||
// Create actor relationships for existing scene
|
||||
$this->createActorRelationships($adultVideoId, $actors);
|
||||
} else {
|
||||
$adultVideoModel->create($sceneData);
|
||||
$adultVideoModel->create($sceneDataForDb);
|
||||
$adultVideoId = $this->pdo->lastInsertId();
|
||||
$this->newCount++;
|
||||
|
||||
// Create actor relationships for new scene
|
||||
$this->createActorRelationships($adultVideoId, $actors);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +436,48 @@ class XbvrSyncService extends BaseSyncService
|
||||
}
|
||||
}
|
||||
|
||||
private function extractVideoList(array $mainData): array
|
||||
{
|
||||
// Try different possible keys for the video list
|
||||
$possibleKeys = ['Recent', 'scenes', 'content', 'videos'];
|
||||
|
||||
foreach ($possibleKeys as $key) {
|
||||
if (isset($mainData[$key]) && is_array($mainData[$key])) {
|
||||
$this->logProgress("Found video list under key: '{$key}' with " . count($mainData[$key]) . " items");
|
||||
return $mainData[$key];
|
||||
}
|
||||
}
|
||||
|
||||
// If no standard key found, look for arrays that might contain video data
|
||||
foreach ($mainData as $key => $value) {
|
||||
if (is_array($value) && count($value) > 0) {
|
||||
// Check if this looks like a video list by examining the first item
|
||||
$firstItem = $value[0];
|
||||
if (isset($firstItem['title']) || isset($firstItem['video_url'])) {
|
||||
$this->logProgress("Found video list under key: '{$key}' with " . count($value) . " items");
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logProgress("No video list found. Available keys: " . implode(', ', array_keys($mainData)));
|
||||
return [];
|
||||
}
|
||||
|
||||
private function extractDetailUrl(array $videoItem): ?string
|
||||
{
|
||||
// Try different possible URL field names
|
||||
$possibleUrlFields = ['video_url', 'url', 'detail_url', 'scene_url'];
|
||||
|
||||
foreach ($possibleUrlFields as $field) {
|
||||
if (!empty($videoItem[$field])) {
|
||||
return $videoItem[$field];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getProcessedCount(): int
|
||||
{
|
||||
return $this->processedCount;
|
||||
@@ -257,4 +497,27 @@ class XbvrSyncService extends BaseSyncService
|
||||
{
|
||||
return 0; // XBVR doesn't provide deletion info in this context
|
||||
}
|
||||
|
||||
private function createActorRelationships(int $adultVideoId, array $actors): void
|
||||
{
|
||||
foreach ($actors as $actor) {
|
||||
if (!isset($actor['id'])) continue;
|
||||
|
||||
try {
|
||||
// Insert relationship into pivot table (ignore duplicates)
|
||||
$stmt = $this->pdo->prepare("
|
||||
INSERT IGNORE INTO actor_adult_video (adult_video_id, actor_id, created_at, updated_at)
|
||||
VALUES (:adult_video_id, :actor_id, NOW(), NOW())
|
||||
");
|
||||
$stmt->execute([
|
||||
'adult_video_id' => $adultVideoId,
|
||||
'actor_id' => $actor['id']
|
||||
]);
|
||||
|
||||
$this->logProgress("Created relationship: Adult Video {$adultVideoId} -> Actor {$actor['name']} ({$actor['id']})");
|
||||
} catch (Exception $e) {
|
||||
$this->logProgress("Failed to create relationship for Adult Video {$adultVideoId} and Actor {$actor['name']}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user