- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
661 lines
22 KiB
PHP
661 lines
22 KiB
PHP
<?php
|
|
/*******************************************************************************************************************
|
|
| Software Name : EasyStream
|
|
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
|
|
| Software Author : (c) Sami Ahmed
|
|
|*******************************************************************************************************************
|
|
|
|
|
|*******************************************************************************************************************
|
|
| This source file is subject to the EasyStream Proprietary License Agreement.
|
|
|
|
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|
|
|*******************************************************************************************************************
|
|
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|
|
|*******************************************************************************************************************/
|
|
|
|
defined('_ISVALID') or header('Location: /error');
|
|
|
|
/**
|
|
* Content Metadata Management System
|
|
*/
|
|
class VContentManager
|
|
{
|
|
private $logger;
|
|
private $db;
|
|
private static $instance = null;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->logger = VLogger::getInstance();
|
|
$this->db = VDatabase::getInstance();
|
|
}
|
|
|
|
public static function getInstance()
|
|
{
|
|
if (self::$instance === null) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Update content metadata
|
|
* @param string $contentType Content type (video, image, audio, document)
|
|
* @param string $contentId Content ID
|
|
* @param array $metadata Metadata to update
|
|
* @param int $userId User ID (for permission check)
|
|
* @return array Update result
|
|
*/
|
|
public function updateContentMetadata($contentType, $contentId, $metadata, $userId)
|
|
{
|
|
try {
|
|
// Validate content type
|
|
$table = $this->getContentTable($contentType);
|
|
if (!$table) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Invalid content type'
|
|
];
|
|
}
|
|
|
|
// Get existing content
|
|
$content = $this->getContent($contentType, $contentId);
|
|
if (!$content) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Content not found'
|
|
];
|
|
}
|
|
|
|
// Check permissions
|
|
if ($content['usr_id'] != $userId && !$this->isAdmin($userId)) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Permission denied'
|
|
];
|
|
}
|
|
|
|
// Validate and sanitize metadata
|
|
$validatedMetadata = $this->validateMetadata($metadata);
|
|
if (!$validatedMetadata['valid']) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $validatedMetadata['error'],
|
|
'details' => $validatedMetadata['details']
|
|
];
|
|
}
|
|
|
|
// Update content
|
|
$updateData = $validatedMetadata['data'];
|
|
$updateData['updated_at'] = date('Y-m-d H:i:s');
|
|
|
|
$this->db->doUpdate($table, 'file_key', $updateData, $contentId);
|
|
|
|
// Update tags if provided
|
|
if (isset($metadata['tags'])) {
|
|
$this->updateContentTags($contentType, $contentId, $metadata['tags']);
|
|
}
|
|
|
|
// Update categories if provided
|
|
if (isset($metadata['categories'])) {
|
|
$this->updateContentCategories($contentType, $contentId, $metadata['categories']);
|
|
}
|
|
|
|
$this->logger->info('Content metadata updated', [
|
|
'content_type' => $contentType,
|
|
'content_id' => $contentId,
|
|
'user_id' => $userId,
|
|
'updated_fields' => array_keys($updateData)
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Content metadata updated successfully',
|
|
'updated_fields' => array_keys($updateData)
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to update content metadata', [
|
|
'content_type' => $contentType,
|
|
'content_id' => $contentId,
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Failed to update content metadata'
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bulk update content metadata
|
|
* @param string $contentType Content type
|
|
* @param array $contentIds Array of content IDs
|
|
* @param array $metadata Metadata to update
|
|
* @param int $userId User ID
|
|
* @return array Update result
|
|
*/
|
|
public function bulkUpdateMetadata($contentType, $contentIds, $metadata, $userId)
|
|
{
|
|
try {
|
|
$results = [
|
|
'success' => 0,
|
|
'failed' => 0,
|
|
'errors' => []
|
|
];
|
|
|
|
foreach ($contentIds as $contentId) {
|
|
$result = $this->updateContentMetadata($contentType, $contentId, $metadata, $userId);
|
|
|
|
if ($result['success']) {
|
|
$results['success']++;
|
|
} else {
|
|
$results['failed']++;
|
|
$results['errors'][$contentId] = $result['error'];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'results' => $results,
|
|
'message' => "Updated {$results['success']} items, {$results['failed']} failed"
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Bulk metadata update failed', [
|
|
'content_type' => $contentType,
|
|
'content_count' => count($contentIds),
|
|
'user_id' => $userId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Bulk update failed'
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get content categories
|
|
* @param string $contentType Content type
|
|
* @return array Categories
|
|
*/
|
|
public function getContentCategories($contentType = null)
|
|
{
|
|
try {
|
|
$whereClause = $contentType ? "WHERE content_type = ?" : "";
|
|
$params = $contentType ? [$contentType] : [];
|
|
|
|
$query = "SELECT * FROM db_content_categories {$whereClause} ORDER BY name ASC";
|
|
$result = $this->db->doQuery($query, $params);
|
|
|
|
$categories = [];
|
|
while ($row = $this->db->doFetch($result)) {
|
|
$categories[] = [
|
|
'id' => $row['id'],
|
|
'name' => $row['name'],
|
|
'slug' => $row['slug'],
|
|
'description' => $row['description'],
|
|
'content_type' => $row['content_type'],
|
|
'parent_id' => $row['parent_id'],
|
|
'sort_order' => $row['sort_order']
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'categories' => $categories
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create content category
|
|
* @param array $categoryData Category data
|
|
* @return array Creation result
|
|
*/
|
|
public function createCategory($categoryData)
|
|
{
|
|
try {
|
|
$validatedData = $this->validateCategoryData($categoryData);
|
|
if (!$validatedData['valid']) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $validatedData['error']
|
|
];
|
|
}
|
|
|
|
$categoryId = $this->db->doInsert('db_content_categories', $validatedData['data']);
|
|
|
|
if (!$categoryId) {
|
|
throw new Exception('Failed to create category');
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'category_id' => $categoryId,
|
|
'message' => 'Category created successfully'
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to create category', [
|
|
'category_data' => $categoryData,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Failed to create category'
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get popular tags
|
|
* @param string $contentType Content type filter
|
|
* @param int $limit Number of tags to return
|
|
* @return array Popular tags
|
|
*/
|
|
public function getPopularTags($contentType = null, $limit = 50)
|
|
{
|
|
try {
|
|
$whereClause = $contentType ? "WHERE content_type = ?" : "";
|
|
$params = $contentType ? [$contentType] : [];
|
|
$params[] = $limit;
|
|
|
|
$query = "SELECT tag_name, COUNT(*) as usage_count
|
|
FROM db_content_tags
|
|
{$whereClause}
|
|
GROUP BY tag_name
|
|
ORDER BY usage_count DESC, tag_name ASC
|
|
LIMIT ?";
|
|
|
|
$result = $this->db->doQuery($query, $params);
|
|
|
|
$tags = [];
|
|
while ($row = $this->db->doFetch($result)) {
|
|
$tags[] = [
|
|
'name' => $row['tag_name'],
|
|
'usage_count' => (int)$row['usage_count']
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'tags' => $tags
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search content by metadata
|
|
* @param array $searchParams Search parameters
|
|
* @param int $limit Limit
|
|
* @param int $offset Offset
|
|
* @return array Search results
|
|
*/
|
|
public function searchContent($searchParams, $limit = 20, $offset = 0)
|
|
{
|
|
try {
|
|
$contentType = $searchParams['content_type'] ?? 'video';
|
|
$table = $this->getContentTable($contentType);
|
|
|
|
if (!$table) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Invalid content type'
|
|
];
|
|
}
|
|
|
|
$whereClause = "WHERE file_active = 1";
|
|
$params = [];
|
|
|
|
// Text search
|
|
if (!empty($searchParams['query'])) {
|
|
$whereClause .= " AND (file_title LIKE ? OR file_description LIKE ?)";
|
|
$searchTerm = '%' . $searchParams['query'] . '%';
|
|
$params[] = $searchTerm;
|
|
$params[] = $searchTerm;
|
|
}
|
|
|
|
// Category filter
|
|
if (!empty($searchParams['category'])) {
|
|
$whereClause .= " AND file_category = ?";
|
|
$params[] = $searchParams['category'];
|
|
}
|
|
|
|
// User filter
|
|
if (!empty($searchParams['user_id'])) {
|
|
$whereClause .= " AND usr_id = ?";
|
|
$params[] = $searchParams['user_id'];
|
|
}
|
|
|
|
// Date range filter
|
|
if (!empty($searchParams['date_from'])) {
|
|
$whereClause .= " AND file_date >= ?";
|
|
$params[] = $searchParams['date_from'];
|
|
}
|
|
|
|
if (!empty($searchParams['date_to'])) {
|
|
$whereClause .= " AND file_date <= ?";
|
|
$params[] = $searchParams['date_to'];
|
|
}
|
|
|
|
// Privacy filter
|
|
if (!empty($searchParams['privacy'])) {
|
|
$whereClause .= " AND privacy = ?";
|
|
$params[] = $searchParams['privacy'];
|
|
}
|
|
|
|
// Sort order
|
|
$sortOrder = $this->getSortOrder($searchParams['sort'] ?? 'newest');
|
|
|
|
$query = "SELECT f.*, u.usr_user
|
|
FROM {$table} f
|
|
JOIN db_accountuser u ON f.usr_id = u.usr_id
|
|
{$whereClause}
|
|
ORDER BY {$sortOrder}
|
|
LIMIT ? OFFSET ?";
|
|
|
|
$params[] = $limit;
|
|
$params[] = $offset;
|
|
|
|
$result = $this->db->doQuery($query, $params);
|
|
|
|
$content = [];
|
|
while ($row = $this->db->doFetch($result)) {
|
|
$content[] = $this->formatContentData($row, $contentType);
|
|
}
|
|
|
|
// Get total count
|
|
$countQuery = "SELECT COUNT(*) as total FROM {$table} f {$whereClause}";
|
|
$countParams = array_slice($params, 0, -2); // Remove limit and offset
|
|
$countResult = $this->db->doQuery($countQuery, $countParams);
|
|
$totalCount = $this->db->doFetch($countResult)['total'];
|
|
|
|
return [
|
|
'success' => true,
|
|
'content' => $content,
|
|
'pagination' => [
|
|
'total' => $totalCount,
|
|
'limit' => $limit,
|
|
'offset' => $offset,
|
|
'pages' => ceil($totalCount / $limit)
|
|
]
|
|
];
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Content search failed', [
|
|
'search_params' => $searchParams,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Search failed'
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Private helper methods
|
|
*/
|
|
|
|
private function getContentTable($contentType)
|
|
{
|
|
$tables = [
|
|
'video' => 'db_videofiles',
|
|
'image' => 'db_imagefiles',
|
|
'audio' => 'db_audiofiles',
|
|
'document' => 'db_documentfiles'
|
|
];
|
|
|
|
return $tables[$contentType] ?? null;
|
|
}
|
|
|
|
private function getContent($contentType, $contentId)
|
|
{
|
|
$table = $this->getContentTable($contentType);
|
|
|
|
$query = "SELECT * FROM {$table} WHERE file_key = ?";
|
|
$result = $this->db->doQuery($query, [$contentId]);
|
|
|
|
return $this->db->doFetch($result);
|
|
}
|
|
|
|
private function validateMetadata($metadata)
|
|
{
|
|
$validatedData = [];
|
|
$errors = [];
|
|
|
|
// Title validation
|
|
if (isset($metadata['title'])) {
|
|
$title = trim($metadata['title']);
|
|
if (empty($title)) {
|
|
$errors[] = 'Title cannot be empty';
|
|
} elseif (strlen($title) > 255) {
|
|
$errors[] = 'Title too long (max 255 characters)';
|
|
} else {
|
|
$validatedData['file_title'] = $title;
|
|
}
|
|
}
|
|
|
|
// Description validation
|
|
if (isset($metadata['description'])) {
|
|
$description = trim($metadata['description']);
|
|
if (strlen($description) > 5000) {
|
|
$errors[] = 'Description too long (max 5000 characters)';
|
|
} else {
|
|
$validatedData['file_description'] = $description;
|
|
}
|
|
}
|
|
|
|
// Privacy validation
|
|
if (isset($metadata['privacy'])) {
|
|
$validPrivacy = ['public', 'unlisted', 'private'];
|
|
if (!in_array($metadata['privacy'], $validPrivacy)) {
|
|
$errors[] = 'Invalid privacy setting';
|
|
} else {
|
|
$validatedData['privacy'] = $metadata['privacy'];
|
|
}
|
|
}
|
|
|
|
// Category validation
|
|
if (isset($metadata['category'])) {
|
|
$category = trim($metadata['category']);
|
|
if (!empty($category)) {
|
|
$validatedData['file_category'] = $category;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'valid' => empty($errors),
|
|
'data' => $validatedData,
|
|
'error' => empty($errors) ? null : 'Validation failed',
|
|
'details' => $errors
|
|
];
|
|
}
|
|
|
|
private function updateContentTags($contentType, $contentId, $tags)
|
|
{
|
|
try {
|
|
// Remove existing tags
|
|
$this->db->doQuery(
|
|
"DELETE FROM db_content_tags WHERE content_type = ? AND content_id = ?",
|
|
[$contentType, $contentId]
|
|
);
|
|
|
|
// Add new tags
|
|
if (!empty($tags)) {
|
|
$tagList = is_array($tags) ? $tags : explode(',', $tags);
|
|
|
|
foreach ($tagList as $tag) {
|
|
$tag = trim($tag);
|
|
if (!empty($tag)) {
|
|
$tagData = [
|
|
'content_type' => $contentType,
|
|
'content_id' => $contentId,
|
|
'tag_name' => $tag,
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->db->doInsert('db_content_tags', $tagData);
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to update content tags', [
|
|
'content_type' => $contentType,
|
|
'content_id' => $contentId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function updateContentCategories($contentType, $contentId, $categories)
|
|
{
|
|
try {
|
|
// Remove existing category associations
|
|
$this->db->doQuery(
|
|
"DELETE FROM db_content_category_relations WHERE content_type = ? AND content_id = ?",
|
|
[$contentType, $contentId]
|
|
);
|
|
|
|
// Add new category associations
|
|
if (!empty($categories)) {
|
|
$categoryList = is_array($categories) ? $categories : [$categories];
|
|
|
|
foreach ($categoryList as $categoryId) {
|
|
if (!empty($categoryId)) {
|
|
$relationData = [
|
|
'content_type' => $contentType,
|
|
'content_id' => $contentId,
|
|
'category_id' => $categoryId,
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
];
|
|
|
|
$this->db->doInsert('db_content_category_relations', $relationData);
|
|
}
|
|
}
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
$this->logger->error('Failed to update content categories', [
|
|
'content_type' => $contentType,
|
|
'content_id' => $contentId,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function validateCategoryData($categoryData)
|
|
{
|
|
$validatedData = [];
|
|
$errors = [];
|
|
|
|
// Name validation
|
|
if (empty($categoryData['name'])) {
|
|
$errors[] = 'Category name is required';
|
|
} else {
|
|
$validatedData['name'] = trim($categoryData['name']);
|
|
$validatedData['slug'] = $this->generateSlug($validatedData['name']);
|
|
}
|
|
|
|
// Content type validation
|
|
$validTypes = ['video', 'image', 'audio', 'document', 'all'];
|
|
if (empty($categoryData['content_type']) || !in_array($categoryData['content_type'], $validTypes)) {
|
|
$errors[] = 'Valid content type is required';
|
|
} else {
|
|
$validatedData['content_type'] = $categoryData['content_type'];
|
|
}
|
|
|
|
// Optional fields
|
|
$validatedData['description'] = trim($categoryData['description'] ?? '');
|
|
$validatedData['parent_id'] = $categoryData['parent_id'] ?? null;
|
|
$validatedData['sort_order'] = (int)($categoryData['sort_order'] ?? 0);
|
|
$validatedData['created_at'] = date('Y-m-d H:i:s');
|
|
|
|
return [
|
|
'valid' => empty($errors),
|
|
'data' => $validatedData,
|
|
'error' => empty($errors) ? null : implode(', ', $errors)
|
|
];
|
|
}
|
|
|
|
private function generateSlug($name)
|
|
{
|
|
$slug = strtolower(trim($name));
|
|
$slug = preg_replace('/[^a-z0-9-]/', '-', $slug);
|
|
$slug = preg_replace('/-+/', '-', $slug);
|
|
return trim($slug, '-');
|
|
}
|
|
|
|
private function getSortOrder($sort)
|
|
{
|
|
switch ($sort) {
|
|
case 'newest':
|
|
return 'f.file_date DESC';
|
|
case 'oldest':
|
|
return 'f.file_date ASC';
|
|
case 'popular':
|
|
return 'f.file_views DESC';
|
|
case 'title':
|
|
return 'f.file_title ASC';
|
|
case 'rating':
|
|
return 'f.file_likes DESC';
|
|
default:
|
|
return 'f.file_date DESC';
|
|
}
|
|
}
|
|
|
|
private function formatContentData($row, $contentType)
|
|
{
|
|
return [
|
|
'id' => $row['file_key'],
|
|
'type' => $contentType,
|
|
'title' => $row['file_title'],
|
|
'description' => $row['file_description'] ?? '',
|
|
'category' => $row['file_category'] ?? '',
|
|
'privacy' => $row['privacy'] ?? 'public',
|
|
'views' => (int)($row['file_views'] ?? 0),
|
|
'likes' => (int)($row['file_likes'] ?? 0),
|
|
'dislikes' => (int)($row['file_dislikes'] ?? 0),
|
|
'comments' => (int)($row['file_comments_count'] ?? 0),
|
|
'uploaded_at' => $row['file_date'],
|
|
'updated_at' => $row['updated_at'] ?? $row['file_date'],
|
|
'uploader' => [
|
|
'id' => $row['usr_id'],
|
|
'username' => $row['usr_user']
|
|
]
|
|
];
|
|
}
|
|
|
|
private function isAdmin($userId)
|
|
{
|
|
try {
|
|
$query = "SELECT usr_role FROM db_accountuser WHERE usr_id = ?";
|
|
$result = $this->db->doQuery($query, [$userId]);
|
|
$user = $this->db->doFetch($result);
|
|
|
|
return $user && in_array($user['usr_role'], ['admin', 'moderator']);
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
} |