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:
373
f_core/f_classes/class.subtitles.php
Normal file
373
f_core/f_classes/class.subtitles.php
Normal file
@@ -0,0 +1,373 @@
|
||||
<?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');
|
||||
|
||||
/**
|
||||
* Subtitle/Caption Management Class
|
||||
*
|
||||
* Handles:
|
||||
* - Subtitle file uploads (.srt, .vtt)
|
||||
* - SRT to VTT conversion
|
||||
* - Multiple language tracks
|
||||
* - Default subtitle selection
|
||||
* - Auto-generated caption integration
|
||||
* - Subtitle deletion and management
|
||||
*/
|
||||
class VSubtitles
|
||||
{
|
||||
/**
|
||||
* Upload subtitle file for a video
|
||||
*
|
||||
* @param int $file_id File ID
|
||||
* @param string $file_type File type (video, short, live, audio)
|
||||
* @param array $subtitle_file $_FILES array element
|
||||
* @param string $language Language code (en, es, fr, etc.)
|
||||
* @param string $label Display label (English, Spanish, etc.)
|
||||
* @param bool $is_default Set as default track
|
||||
* @return array ['success' => bool, 'message' => string, 'sub_id' => int]
|
||||
*/
|
||||
public static function uploadSubtitle($file_id, $file_type, $subtitle_file, $language = 'en', $label = 'English', $is_default = false)
|
||||
{
|
||||
global $cfg, $class_database;
|
||||
|
||||
// Get configuration
|
||||
$config = $class_database->getConfigurations('subtitles_enabled,subtitles_max_size,subtitles_allowed_formats,subtitles_auto_convert,subtitles_max_per_video,subtitles_require_approval');
|
||||
|
||||
// Check if subtitles are enabled
|
||||
if ($config['subtitles_enabled'] != 1) {
|
||||
return ['success' => false, 'message' => 'Subtitles are not enabled on this platform'];
|
||||
}
|
||||
|
||||
// Validate file upload
|
||||
if (!isset($subtitle_file['tmp_name']) || $subtitle_file['error'] !== UPLOAD_ERR_OK) {
|
||||
return ['success' => false, 'message' => 'No file uploaded or upload error occurred'];
|
||||
}
|
||||
|
||||
// Validate file size
|
||||
if ($subtitle_file['size'] > $config['subtitles_max_size']) {
|
||||
$max_mb = round($config['subtitles_max_size'] / 1048576, 2);
|
||||
return ['success' => false, 'message' => "Subtitle file too large. Maximum size: {$max_mb}MB"];
|
||||
}
|
||||
|
||||
// Validate file format
|
||||
$file_ext = strtolower(pathinfo($subtitle_file['name'], PATHINFO_EXTENSION));
|
||||
$allowed_formats = explode(',', $config['subtitles_allowed_formats']);
|
||||
|
||||
if (!in_array($file_ext, $allowed_formats)) {
|
||||
return ['success' => false, 'message' => 'Invalid subtitle format. Allowed: ' . implode(', ', $allowed_formats)];
|
||||
}
|
||||
|
||||
// Check subtitle count limit
|
||||
$current_count = self::getSubtitleCount($file_id, $file_type);
|
||||
if ($current_count >= $config['subtitles_max_per_video']) {
|
||||
return ['success' => false, 'message' => "Maximum {$config['subtitles_max_per_video']} subtitle tracks allowed per video"];
|
||||
}
|
||||
|
||||
// Get user ID
|
||||
$usr_id = isset($_SESSION['USER_ID']) ? (int) $_SESSION['USER_ID'] : 0;
|
||||
if ($usr_id == 0) {
|
||||
return ['success' => false, 'message' => 'You must be logged in to upload subtitles'];
|
||||
}
|
||||
|
||||
// Verify file ownership
|
||||
if (!self::verifyFileOwnership($file_id, $file_type, $usr_id)) {
|
||||
return ['success' => false, 'message' => 'You do not have permission to add subtitles to this file'];
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
$unique_filename = self::generateSubtitleFilename($file_id, $file_type, $language, $file_ext);
|
||||
$subtitle_dir = $cfg['main_dir'] . '/f_data/data_subtitles/';
|
||||
$destination = $subtitle_dir . $unique_filename;
|
||||
|
||||
// Ensure directory exists
|
||||
if (!file_exists($subtitle_dir)) {
|
||||
mkdir($subtitle_dir, 0755, true);
|
||||
}
|
||||
|
||||
// Move uploaded file
|
||||
if (!move_uploaded_file($subtitle_file['tmp_name'], $destination)) {
|
||||
return ['success' => false, 'message' => 'Failed to save subtitle file'];
|
||||
}
|
||||
|
||||
// Convert SRT to VTT if needed
|
||||
$final_format = $file_ext;
|
||||
if ($file_ext === 'srt' && $config['subtitles_auto_convert'] == 1) {
|
||||
$vtt_filename = self::convertSrtToVtt($destination, $unique_filename);
|
||||
if ($vtt_filename) {
|
||||
unlink($destination); // Remove SRT file
|
||||
$unique_filename = $vtt_filename;
|
||||
$final_format = 'vtt';
|
||||
}
|
||||
}
|
||||
|
||||
// If this is set as default, unset other defaults
|
||||
if ($is_default) {
|
||||
self::unsetDefaultSubtitles($file_id, $file_type);
|
||||
}
|
||||
|
||||
// Insert into database
|
||||
$active = ($config['subtitles_require_approval'] == 1 && !self::isAdmin()) ? 0 : 1;
|
||||
|
||||
$sql = "INSERT INTO `db_subtitles`
|
||||
(`file_id`, `file_type`, `usr_id`, `sub_language`, `sub_label`, `sub_filename`,
|
||||
`sub_format`, `sub_kind`, `sub_default`, `sub_filesize`, `active`)
|
||||
VALUES (%d, '%s', %d, '%s', '%s', '%s', '%s', 'subtitles', %d, %d, %d)";
|
||||
|
||||
$class_database->doQuery($sql,
|
||||
$file_id,
|
||||
$class_database->safe_input($file_type),
|
||||
$usr_id,
|
||||
$class_database->safe_input($language),
|
||||
$class_database->safe_input($label),
|
||||
$class_database->safe_input($unique_filename),
|
||||
$final_format,
|
||||
$is_default ? 1 : 0,
|
||||
filesize($subtitle_dir . $unique_filename),
|
||||
$active
|
||||
);
|
||||
|
||||
$sub_id = $class_database->insert_id();
|
||||
|
||||
// Update subtitle count in file table
|
||||
self::updateSubtitleCount($file_id, $file_type);
|
||||
|
||||
$message = $active ? 'Subtitle uploaded successfully' : 'Subtitle uploaded and pending approval';
|
||||
|
||||
return ['success' => true, 'message' => $message, 'sub_id' => $sub_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subtitles for a file
|
||||
*
|
||||
* @param int $file_id File ID
|
||||
* @param string $file_type File type
|
||||
* @param bool $active_only Only return active subtitles
|
||||
* @return array Array of subtitle records
|
||||
*/
|
||||
public static function getSubtitles($file_id, $file_type, $active_only = true)
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$active_clause = $active_only ? "AND `active` = 1" : "";
|
||||
|
||||
$sql = "SELECT * FROM `db_subtitles`
|
||||
WHERE `file_id` = %d AND `file_type` = '%s' $active_clause
|
||||
ORDER BY `sub_default` DESC, `sub_language` ASC";
|
||||
|
||||
$result = $class_database->doQuery($sql, $file_id, $class_database->safe_input($file_type));
|
||||
|
||||
$subtitles = [];
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$subtitles[] = $row;
|
||||
}
|
||||
|
||||
return $subtitles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subtitle track URL for player
|
||||
*
|
||||
* @param int $file_id File ID
|
||||
* @param string $file_type File type
|
||||
* @return array Array of subtitle tracks for video player
|
||||
*/
|
||||
public static function getSubtitleTracksForPlayer($file_id, $file_type)
|
||||
{
|
||||
global $cfg;
|
||||
|
||||
$subtitles = self::getSubtitles($file_id, $file_type, true);
|
||||
$tracks = [];
|
||||
|
||||
foreach ($subtitles as $sub) {
|
||||
$tracks[] = [
|
||||
'kind' => $sub['sub_kind'],
|
||||
'label' => $sub['sub_label'],
|
||||
'srclang' => $sub['sub_language'],
|
||||
'src' => $cfg['main_url'] . '/f_data/data_subtitles/' . $sub['sub_filename'],
|
||||
'default' => $sub['sub_default'] == 1
|
||||
];
|
||||
}
|
||||
|
||||
return $tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete subtitle track
|
||||
*
|
||||
* @param int $sub_id Subtitle ID
|
||||
* @return bool Success
|
||||
*/
|
||||
public static function deleteSubtitle($sub_id)
|
||||
{
|
||||
global $cfg, $class_database;
|
||||
|
||||
// Get subtitle info
|
||||
$sql = "SELECT * FROM `db_subtitles` WHERE `sub_id` = %d LIMIT 1";
|
||||
$result = $class_database->doQuery($sql, $sub_id);
|
||||
$subtitle = $result->fetch_assoc();
|
||||
|
||||
if (!$subtitle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
$usr_id = isset($_SESSION['USER_ID']) ? (int) $_SESSION['USER_ID'] : 0;
|
||||
if ($subtitle['usr_id'] != $usr_id && !self::isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete file
|
||||
$file_path = $cfg['main_dir'] . '/f_data/data_subtitles/' . $subtitle['sub_filename'];
|
||||
if (file_exists($file_path)) {
|
||||
unlink($file_path);
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
$sql = "DELETE FROM `db_subtitles` WHERE `sub_id` = %d";
|
||||
$class_database->doQuery($sql, $sub_id);
|
||||
|
||||
// Update count
|
||||
self::updateSubtitleCount($subtitle['file_id'], $subtitle['file_type']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SRT to VTT format
|
||||
*
|
||||
* @param string $srt_path Path to SRT file
|
||||
* @param string $original_filename Original filename
|
||||
* @return string|false VTT filename or false on failure
|
||||
*/
|
||||
private static function convertSrtToVtt($srt_path, $original_filename)
|
||||
{
|
||||
global $cfg;
|
||||
|
||||
$srt_content = file_get_contents($srt_path);
|
||||
if ($srt_content === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add WEBVTT header
|
||||
$vtt_content = "WEBVTT\n\n";
|
||||
|
||||
// Convert timestamps from SRT format (00:00:00,000) to VTT format (00:00:00.000)
|
||||
$vtt_content .= str_replace(',', '.', $srt_content);
|
||||
|
||||
// Generate VTT filename
|
||||
$vtt_filename = str_replace('.srt', '.vtt', $original_filename);
|
||||
$vtt_path = $cfg['main_dir'] . '/f_data/data_subtitles/' . $vtt_filename;
|
||||
|
||||
// Write VTT file
|
||||
if (file_put_contents($vtt_path, $vtt_content) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $vtt_filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique subtitle filename
|
||||
*/
|
||||
private static function generateSubtitleFilename($file_id, $file_type, $language, $ext)
|
||||
{
|
||||
return $file_type . '_' . $file_id . '_' . $language . '_' . time() . '.' . $ext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset default flag for all other subtitles
|
||||
*/
|
||||
private static function unsetDefaultSubtitles($file_id, $file_type)
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$sql = "UPDATE `db_subtitles` SET `sub_default` = 0
|
||||
WHERE `file_id` = %d AND `file_type` = '%s'";
|
||||
|
||||
$class_database->doQuery($sql, $file_id, $class_database->safe_input($file_type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subtitle count for a file
|
||||
*/
|
||||
private static function getSubtitleCount($file_id, $file_type)
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$sql = "SELECT COUNT(*) as count FROM `db_subtitles`
|
||||
WHERE `file_id` = %d AND `file_type` = '%s' AND `active` = 1";
|
||||
|
||||
$result = $class_database->doQuery($sql, $file_id, $class_database->safe_input($file_type));
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
return (int) $row['count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update subtitle count in file table
|
||||
*/
|
||||
private static function updateSubtitleCount($file_id, $file_type)
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$count = self::getSubtitleCount($file_id, $file_type);
|
||||
|
||||
$table_map = [
|
||||
'video' => 'db_videofiles',
|
||||
'short' => 'db_shortfiles',
|
||||
'live' => 'db_livefiles',
|
||||
'audio' => 'db_audiofiles'
|
||||
];
|
||||
|
||||
if (isset($table_map[$file_type])) {
|
||||
$sql = "UPDATE `{$table_map[$file_type]}` SET `subtitle_count` = %d WHERE `db_id` = %d";
|
||||
$class_database->doQuery($sql, $count, $file_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify file ownership
|
||||
*/
|
||||
private static function verifyFileOwnership($file_id, $file_type, $usr_id)
|
||||
{
|
||||
global $class_database;
|
||||
|
||||
$table_map = [
|
||||
'video' => 'db_videofiles',
|
||||
'short' => 'db_shortfiles',
|
||||
'live' => 'db_livefiles',
|
||||
'audio' => 'db_audiofiles'
|
||||
];
|
||||
|
||||
if (!isset($table_map[$file_type])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "SELECT `usr_id` FROM `{$table_map[$file_type]}` WHERE `db_id` = %d LIMIT 1";
|
||||
$result = $class_database->doQuery($sql, $file_id);
|
||||
$row = $result->fetch_assoc();
|
||||
|
||||
return $row && $row['usr_id'] == $usr_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user is admin
|
||||
*/
|
||||
private static function isAdmin()
|
||||
{
|
||||
return isset($_SESSION['USER_ADMIN']) && $_SESSION['USER_ADMIN'] == 1;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user