feat: Add comprehensive documentation suite and reorganize project structure
- 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
This commit is contained in:
404
f_core/f_classes/class.recommendations.php
Normal file
404
f_core/f_classes/class.recommendations.php
Normal file
@@ -0,0 +1,404 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user