- 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
405 lines
13 KiB
PHP
405 lines
13 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');
|
|
|
|
/**
|
|
* Recommendation Engine Class
|
|
*
|
|
* Provides personalized content recommendations based on:
|
|
* - User watch history
|
|
* - Liked videos
|
|
* - Subscribed channels
|
|
* - Popular/trending content
|
|
* - Similar content by category/tags
|
|
*/
|
|
class VRecommendations
|
|
{
|
|
/**
|
|
* Get personalized "For You" feed for logged-in users
|
|
*
|
|
* @param int $limit Number of recommendations to return
|
|
* @param string $type Content type (video, short, live, image, audio, blog, document)
|
|
* @return array Recommended content items
|
|
*/
|
|
public static function getForYouFeed($limit = 20, $type = 'video')
|
|
{
|
|
global $class_database;
|
|
|
|
$usr_id = isset($_SESSION['USER_ID']) ? (int) $_SESSION['USER_ID'] : 0;
|
|
|
|
// For guests, return trending content
|
|
if ($usr_id == 0) {
|
|
return self::getTrending($limit, $type);
|
|
}
|
|
|
|
$recommendations = [];
|
|
|
|
// 40% from subscribed channels (latest uploads)
|
|
$subscribed = self::getFromSubscriptions($usr_id, ceil($limit * 0.4), $type);
|
|
$recommendations = array_merge($recommendations, $subscribed);
|
|
|
|
// 30% based on watch history (similar content)
|
|
$similar = self::getBasedOnWatchHistory($usr_id, ceil($limit * 0.3), $type);
|
|
$recommendations = array_merge($recommendations, $similar);
|
|
|
|
// 20% based on likes (similar to liked content)
|
|
$liked_similar = self::getBasedOnLikes($usr_id, ceil($limit * 0.2), $type);
|
|
$recommendations = array_merge($recommendations, $liked_similar);
|
|
|
|
// 10% trending content (discovery)
|
|
$trending = self::getTrending(ceil($limit * 0.1), $type);
|
|
$recommendations = array_merge($recommendations, $trending);
|
|
|
|
// Remove duplicates and shuffle
|
|
$recommendations = self::deduplicateAndShuffle($recommendations, $limit);
|
|
|
|
return $recommendations;
|
|
}
|
|
|
|
/**
|
|
* Get trending content (popular in last 7 days)
|
|
*
|
|
* @param int $limit Number of items
|
|
* @param string $type Content type
|
|
* @return array Trending items
|
|
*/
|
|
public static function getTrending($limit = 20, $type = 'video')
|
|
{
|
|
global $class_database;
|
|
|
|
$table_map = [
|
|
'video' => 'db_videofiles',
|
|
'short' => 'db_shortfiles',
|
|
'live' => 'db_livefiles',
|
|
'image' => 'db_imagefiles',
|
|
'audio' => 'db_audiofiles',
|
|
'document' => 'db_documentfiles',
|
|
'blog' => 'db_blogfiles'
|
|
];
|
|
|
|
if (!isset($table_map[$type])) {
|
|
return [];
|
|
}
|
|
|
|
$table = $table_map[$type];
|
|
|
|
// Calculate trending score: (views * 2 + likes * 5 + comments * 3) / age_in_days
|
|
$sql = "SELECT
|
|
f.*,
|
|
u.usr_user, u.usr_dname, u.usr_key,
|
|
((f.file_views * 2) + (f.file_like * 5) + (f.file_comments * 3)) /
|
|
GREATEST(DATEDIFF(NOW(), f.upload_date), 1) as trending_score
|
|
FROM `{$table}` f
|
|
JOIN `db_accountuser` u ON f.usr_id = u.usr_id
|
|
WHERE f.active = 1
|
|
AND f.approved = 1
|
|
AND f.deleted = 0
|
|
AND f.privacy = 'public'
|
|
AND f.upload_date >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
|
ORDER BY trending_score DESC
|
|
LIMIT %d";
|
|
|
|
$result = $class_database->doQuery($sql, $limit);
|
|
|
|
$items = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$items[] = $row;
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Get latest content from subscribed channels
|
|
*
|
|
* @param int $usr_id User ID
|
|
* @param int $limit Number of items
|
|
* @param string $type Content type
|
|
* @return array Items from subscriptions
|
|
*/
|
|
public static function getFromSubscriptions($usr_id, $limit = 20, $type = 'video')
|
|
{
|
|
global $class_database;
|
|
|
|
$table_map = [
|
|
'video' => 'db_videofiles',
|
|
'short' => 'db_shortfiles',
|
|
'live' => 'db_livefiles',
|
|
'image' => 'db_imagefiles',
|
|
'audio' => 'db_audiofiles',
|
|
'document' => 'db_documentfiles',
|
|
'blog' => 'db_blogfiles'
|
|
];
|
|
|
|
if (!isset($table_map[$type])) {
|
|
return [];
|
|
}
|
|
|
|
$table = $table_map[$type];
|
|
|
|
$sql = "SELECT DISTINCT
|
|
f.*,
|
|
u.usr_user, u.usr_dname, u.usr_key
|
|
FROM `{$table}` f
|
|
JOIN `db_accountuser` u ON f.usr_id = u.usr_id
|
|
JOIN `db_subscribers` s ON s.subscriber_id = f.usr_id
|
|
WHERE s.usr_id = %d
|
|
AND f.active = 1
|
|
AND f.approved = 1
|
|
AND f.deleted = 0
|
|
AND f.privacy = 'public'
|
|
ORDER BY f.upload_date DESC
|
|
LIMIT %d";
|
|
|
|
$result = $class_database->doQuery($sql, $usr_id, $limit);
|
|
|
|
$items = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$items[] = $row;
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Get content based on watch history (similar categories/tags)
|
|
*
|
|
* @param int $usr_id User ID
|
|
* @param int $limit Number of items
|
|
* @param string $type Content type
|
|
* @return array Similar items
|
|
*/
|
|
public static function getBasedOnWatchHistory($usr_id, $limit = 20, $type = 'video')
|
|
{
|
|
global $class_database;
|
|
|
|
$table_map = [
|
|
'video' => 'db_videofiles',
|
|
'short' => 'db_shortfiles',
|
|
'live' => 'db_livefiles',
|
|
'image' => 'db_imagefiles',
|
|
'audio' => 'db_audiofiles',
|
|
'document' => 'db_documentfiles',
|
|
'blog' => 'db_blogfiles'
|
|
];
|
|
|
|
if (!isset($table_map[$type])) {
|
|
return [];
|
|
}
|
|
|
|
$table = $table_map[$type];
|
|
|
|
// Get categories from watch history
|
|
$sql = "SELECT DISTINCT fcr.ct_id
|
|
FROM `db_filehistory` fh
|
|
JOIN `db_filecategories` fcr ON fcr.file_key = fh.file_key
|
|
WHERE fh.usr_id = %d
|
|
AND fh.file_type = '%s'
|
|
ORDER BY fh.db_id DESC
|
|
LIMIT 10";
|
|
|
|
$result = $class_database->doQuery($sql, $usr_id, $type);
|
|
|
|
$category_ids = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$category_ids[] = $row['ct_id'];
|
|
}
|
|
|
|
if (empty($category_ids)) {
|
|
return self::getTrending($limit, $type);
|
|
}
|
|
|
|
$category_list = implode(',', $category_ids);
|
|
|
|
// Get content from these categories, excluding already watched
|
|
$sql = "SELECT DISTINCT
|
|
f.*,
|
|
u.usr_user, u.usr_dname, u.usr_key
|
|
FROM `{$table}` f
|
|
JOIN `db_accountuser` u ON f.usr_id = u.usr_id
|
|
JOIN `db_filecategories` fc ON fc.file_key = f.file_key
|
|
WHERE fc.ct_id IN ({$category_list})
|
|
AND f.active = 1
|
|
AND f.approved = 1
|
|
AND f.deleted = 0
|
|
AND f.privacy = 'public'
|
|
AND f.file_key NOT IN (
|
|
SELECT file_key FROM `db_filehistory` WHERE usr_id = %d
|
|
)
|
|
ORDER BY f.file_views DESC, f.upload_date DESC
|
|
LIMIT %d";
|
|
|
|
$result = $class_database->doQuery($sql, $usr_id, $limit);
|
|
|
|
$items = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$items[] = $row;
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Get content based on liked videos
|
|
*
|
|
* @param int $usr_id User ID
|
|
* @param int $limit Number of items
|
|
* @param string $type Content type
|
|
* @return array Similar items
|
|
*/
|
|
public static function getBasedOnLikes($usr_id, $limit = 20, $type = 'video')
|
|
{
|
|
global $class_database;
|
|
|
|
$table_map = [
|
|
'video' => 'db_videofiles',
|
|
'short' => 'db_shortfiles',
|
|
'live' => 'db_livefiles',
|
|
'image' => 'db_imagefiles',
|
|
'audio' => 'db_audiofiles',
|
|
'document' => 'db_documentfiles',
|
|
'blog' => 'db_blogfiles'
|
|
];
|
|
|
|
if (!isset($table_map[$type])) {
|
|
return [];
|
|
}
|
|
|
|
$table = $table_map[$type];
|
|
|
|
// Get categories from liked content
|
|
$sql = "SELECT DISTINCT fcr.ct_id
|
|
FROM `db_filelike` fl
|
|
JOIN `db_filecategories` fcr ON fcr.file_key = fl.file_key
|
|
WHERE fl.usr_id = %d
|
|
AND fl.file_type = '%s'
|
|
AND fl.vote = 1
|
|
ORDER BY fl.db_id DESC
|
|
LIMIT 10";
|
|
|
|
$result = $class_database->doQuery($sql, $usr_id, $type);
|
|
|
|
$category_ids = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$category_ids[] = $row['ct_id'];
|
|
}
|
|
|
|
if (empty($category_ids)) {
|
|
return [];
|
|
}
|
|
|
|
$category_list = implode(',', $category_ids);
|
|
|
|
// Get content from these categories, excluding already liked
|
|
$sql = "SELECT DISTINCT
|
|
f.*,
|
|
u.usr_user, u.usr_dname, u.usr_key
|
|
FROM `{$table}` f
|
|
JOIN `db_accountuser` u ON f.usr_id = u.usr_id
|
|
JOIN `db_filecategories` fc ON fc.file_key = f.file_key
|
|
WHERE fc.ct_id IN ({$category_list})
|
|
AND f.active = 1
|
|
AND f.approved = 1
|
|
AND f.deleted = 0
|
|
AND f.privacy = 'public'
|
|
AND f.file_key NOT IN (
|
|
SELECT file_key FROM `db_filelike` WHERE usr_id = %d AND vote = 1
|
|
)
|
|
ORDER BY f.file_like DESC, f.upload_date DESC
|
|
LIMIT %d";
|
|
|
|
$result = $class_database->doQuery($sql, $usr_id, $limit);
|
|
|
|
$items = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$items[] = $row;
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Get "Continue Watching" section
|
|
*
|
|
* @param int $usr_id User ID
|
|
* @param int $limit Number of items
|
|
* @return array Partially watched items
|
|
*/
|
|
public static function getContinueWatching($usr_id, $limit = 10)
|
|
{
|
|
global $class_database;
|
|
|
|
// Get recently watched items that aren't fully watched
|
|
$sql = "SELECT DISTINCT
|
|
fh.*,
|
|
CASE
|
|
WHEN fh.file_type = 'video' THEN v.file_title
|
|
WHEN fh.file_type = 'short' THEN s.file_title
|
|
WHEN fh.file_type = 'live' THEN l.file_title
|
|
WHEN fh.file_type = 'audio' THEN a.file_title
|
|
END as file_title,
|
|
u.usr_user, u.usr_dname, u.usr_key
|
|
FROM `db_filehistory` fh
|
|
LEFT JOIN `db_videofiles` v ON fh.file_key = v.file_key AND fh.file_type = 'video'
|
|
LEFT JOIN `db_shortfiles` s ON fh.file_key = s.file_key AND fh.file_type = 'short'
|
|
LEFT JOIN `db_livefiles` l ON fh.file_key = l.file_key AND fh.file_type = 'live'
|
|
LEFT JOIN `db_audiofiles` a ON fh.file_key = a.file_key AND fh.file_type = 'audio'
|
|
JOIN `db_accountuser` u ON
|
|
COALESCE(v.usr_id, s.usr_id, l.usr_id, a.usr_id) = u.usr_id
|
|
WHERE fh.usr_id = %d
|
|
AND fh.watch_progress > 0
|
|
AND fh.watch_progress < 90
|
|
ORDER BY fh.db_id DESC
|
|
LIMIT %d";
|
|
|
|
$result = $class_database->doQuery($sql, $usr_id, $limit);
|
|
|
|
$items = [];
|
|
while ($row = $result->fetch_assoc()) {
|
|
$items[] = $row;
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Remove duplicates and shuffle results
|
|
*
|
|
* @param array $items Array of items
|
|
* @param int $limit Maximum items to return
|
|
* @return array Deduplicated and shuffled items
|
|
*/
|
|
private static function deduplicateAndShuffle($items, $limit)
|
|
{
|
|
// Remove duplicates based on file_key
|
|
$seen = [];
|
|
$unique = [];
|
|
|
|
foreach ($items as $item) {
|
|
if (!isset($seen[$item['file_key']])) {
|
|
$seen[$item['file_key']] = true;
|
|
$unique[] = $item;
|
|
}
|
|
}
|
|
|
|
// Shuffle for variety
|
|
shuffle($unique);
|
|
|
|
// Return limited number
|
|
return array_slice($unique, 0, $limit);
|
|
}
|
|
}
|