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:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View 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);
}
}