feat: Add enhanced playlist system with auto-play, shuffle, and collaboration
- Create comprehensive playlist management system - Implement auto-play, shuffle, and repeat modes (none/one/all) - Add collaborative playlists with granular permissions - Build playlist follow system for users - Implement playlist analytics and play tracking - Create modern playlist player UI with controls - Add privacy modes (public/unlisted/private) - Document complete API and usage Features: - Auto-play next video when current ends - Shuffle mode for random playback - Repeat modes (off, one, all) - Collaborative editing with permissions - Follow/unfollow playlists - Playlist view tracking - Modern player controls UI - Complete permission system - Production-ready with error handling
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
<?php
|
||||
/**
|
||||
* Enhanced Playlist Management Class
|
||||
* Handles creation, collaboration, and playback features
|
||||
*/
|
||||
|
||||
class VPlaylists {
|
||||
private $db;
|
||||
private $logger;
|
||||
|
||||
public function __construct() {
|
||||
global $class_database;
|
||||
$this->db = $class_database;
|
||||
$this->logger = new VLogger('playlists');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new playlist
|
||||
*/
|
||||
public function create($usr_id, $title, $description = '', $privacy = 'public', $is_collaborative = false) {
|
||||
$sql = "INSERT INTO db_playlists (usr_id, title, description, privacy, is_collaborative)
|
||||
VALUES (?, ?, ?, ?, ?)";
|
||||
|
||||
$result = $this->db->execute($sql, [$usr_id, $title, $description, $privacy, $is_collaborative ? 1 : 0]);
|
||||
|
||||
if (!$result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$playlist_id = $this->db->lastInsertId();
|
||||
$this->logger->info("Created playlist $playlist_id by user $usr_id");
|
||||
|
||||
return $playlist_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist details
|
||||
*/
|
||||
public function get($playlist_id, $current_user_id = null) {
|
||||
$sql = "SELECT p.*, u.usr_user as owner_username, u.usr_avatar as owner_avatar,
|
||||
(SELECT COUNT(*) FROM db_playlist_items WHERE playlist_id = p.playlist_id) as video_count,
|
||||
(SELECT COUNT(*) FROM db_playlist_follows WHERE playlist_id = p.playlist_id) as follower_count
|
||||
FROM db_playlists p
|
||||
LEFT JOIN db_accountuser u ON p.usr_id = u.usr_id
|
||||
WHERE p.playlist_id = ?";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id]);
|
||||
|
||||
if (!$result || $this->db->rowCount($result) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$playlist = $this->db->fetch($result);
|
||||
|
||||
// Check access permissions
|
||||
if (!$this->canView($playlist, $current_user_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if current user is following
|
||||
if ($current_user_id) {
|
||||
$playlist['is_following'] = $this->isFollowing($playlist_id, $current_user_id);
|
||||
$playlist['is_collaborator'] = $this->isCollaborator($playlist_id, $current_user_id);
|
||||
$playlist['is_owner'] = $playlist['usr_id'] == $current_user_id;
|
||||
}
|
||||
|
||||
return $playlist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update playlist
|
||||
*/
|
||||
public function update($playlist_id, $usr_id, $data) {
|
||||
// Verify ownership
|
||||
if (!$this->isOwner($playlist_id, $usr_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed = ['title', 'description', 'privacy', 'is_collaborative', 'allow_shuffle', 'auto_play', 'thumbnail'];
|
||||
$sets = [];
|
||||
$values = [];
|
||||
|
||||
foreach ($data as $field => $value) {
|
||||
if (in_array($field, $allowed)) {
|
||||
$sets[] = "$field = ?";
|
||||
$values[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($sets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$values[] = $playlist_id;
|
||||
|
||||
$sql = "UPDATE db_playlists SET " . implode(', ', $sets) . " WHERE playlist_id = ?";
|
||||
|
||||
return $this->db->execute($sql, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete playlist
|
||||
*/
|
||||
public function delete($playlist_id, $usr_id) {
|
||||
if (!$this->isOwner($playlist_id, $usr_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM db_playlists WHERE playlist_id = ?";
|
||||
return $this->db->execute($sql, [$playlist_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add video to playlist
|
||||
*/
|
||||
public function addVideo($playlist_id, $video_id, $usr_id) {
|
||||
// Check permissions
|
||||
if (!$this->canAddVideos($playlist_id, $usr_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get next position
|
||||
$sql = "SELECT COALESCE(MAX(position), 0) + 1 as next_position
|
||||
FROM db_playlist_items WHERE playlist_id = ?";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id]);
|
||||
$row = $this->db->fetch($result);
|
||||
$position = $row['next_position'];
|
||||
|
||||
// Insert item
|
||||
$sql = "INSERT INTO db_playlist_items (playlist_id, video_id, position, added_by)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE position = position"; // Prevent duplicate
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $video_id, $position, $usr_id]);
|
||||
|
||||
if ($result) {
|
||||
$this->updateVideoCount($playlist_id);
|
||||
$this->logger->info("Added video $video_id to playlist $playlist_id by user $usr_id");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove video from playlist
|
||||
*/
|
||||
public function removeVideo($playlist_id, $video_id, $usr_id) {
|
||||
if (!$this->canRemoveVideos($playlist_id, $usr_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM db_playlist_items
|
||||
WHERE playlist_id = ? AND video_id = ?";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $video_id]);
|
||||
|
||||
if ($result) {
|
||||
$this->reorderItems($playlist_id);
|
||||
$this->updateVideoCount($playlist_id);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder playlist items
|
||||
*/
|
||||
public function reorderItems($playlist_id, $item_positions = null) {
|
||||
if ($item_positions) {
|
||||
// Manual reordering with specific positions
|
||||
foreach ($item_positions as $item_id => $position) {
|
||||
$sql = "UPDATE db_playlist_items
|
||||
SET position = ?
|
||||
WHERE item_id = ? AND playlist_id = ?";
|
||||
$this->db->execute($sql, [$position, $item_id, $playlist_id]);
|
||||
}
|
||||
} else {
|
||||
// Auto-reorder to fill gaps
|
||||
$sql = "SELECT item_id FROM db_playlist_items
|
||||
WHERE playlist_id = ? ORDER BY position ASC";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id]);
|
||||
$items = $this->db->resultsToArray($result);
|
||||
|
||||
$position = 1;
|
||||
foreach ($items as $item) {
|
||||
$this->db->execute(
|
||||
"UPDATE db_playlist_items SET position = ? WHERE item_id = ?",
|
||||
[$position++, $item['item_id']]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist items (videos)
|
||||
*/
|
||||
public function getItems($playlist_id, $shuffle = false) {
|
||||
$order = $shuffle ? "RAND()" : "pi.position ASC";
|
||||
|
||||
$sql = "SELECT pi.*, v.video_title as title, v.video_duration as duration,
|
||||
v.video_views as views, v.video_thumb as thumbnail,
|
||||
u.usr_user as username
|
||||
FROM db_playlist_items pi
|
||||
LEFT JOIN db_videofiles v ON pi.video_id = v.video_id
|
||||
LEFT JOIN db_accountuser u ON v.usr_id = u.usr_id
|
||||
WHERE pi.playlist_id = ?
|
||||
ORDER BY $order";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id]);
|
||||
|
||||
if (!$result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->db->resultsToArray($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next video in playlist
|
||||
*/
|
||||
public function getNextVideo($playlist_id, $current_video_id, $shuffle = false) {
|
||||
if ($shuffle) {
|
||||
// Random next video
|
||||
$sql = "SELECT video_id FROM db_playlist_items
|
||||
WHERE playlist_id = ? AND video_id != ?
|
||||
ORDER BY RAND() LIMIT 1";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $current_video_id]);
|
||||
} else {
|
||||
// Next in order
|
||||
$sql = "SELECT pi2.video_id
|
||||
FROM db_playlist_items pi1
|
||||
JOIN db_playlist_items pi2 ON pi1.playlist_id = pi2.playlist_id
|
||||
WHERE pi1.playlist_id = ? AND pi1.video_id = ?
|
||||
AND pi2.position > pi1.position
|
||||
ORDER BY pi2.position ASC
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $current_video_id]);
|
||||
}
|
||||
|
||||
if (!$result || $this->db->rowCount($result) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $this->db->fetch($result);
|
||||
return $row['video_id'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add collaborator to playlist
|
||||
*/
|
||||
public function addCollaborator($playlist_id, $usr_id, $collaborator_id, $permissions = []) {
|
||||
if (!$this->isOwner($playlist_id, $usr_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$playlist = $this->get($playlist_id);
|
||||
if (!$playlist || !$playlist['is_collaborative']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$can_add = $permissions['can_add'] ?? 1;
|
||||
$can_remove = $permissions['can_remove'] ?? 0;
|
||||
$can_reorder = $permissions['can_reorder'] ?? 0;
|
||||
|
||||
$sql = "INSERT INTO db_playlist_collaborators
|
||||
(playlist_id, usr_id, can_add, can_remove, can_reorder)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
can_add = VALUES(can_add),
|
||||
can_remove = VALUES(can_remove),
|
||||
can_reorder = VALUES(can_reorder)";
|
||||
|
||||
return $this->db->execute($sql, [$playlist_id, $collaborator_id, $can_add, $can_remove, $can_reorder]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove collaborator
|
||||
*/
|
||||
public function removeCollaborator($playlist_id, $usr_id, $collaborator_id) {
|
||||
if (!$this->isOwner($playlist_id, $usr_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "DELETE FROM db_playlist_collaborators
|
||||
WHERE playlist_id = ? AND usr_id = ?";
|
||||
|
||||
return $this->db->execute($sql, [$playlist_id, $collaborator_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow/unfollow playlist
|
||||
*/
|
||||
public function follow($playlist_id, $usr_id) {
|
||||
$sql = "INSERT INTO db_playlist_follows (playlist_id, usr_id)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE followed_at = NOW()";
|
||||
|
||||
return $this->db->execute($sql, [$playlist_id, $usr_id]);
|
||||
}
|
||||
|
||||
public function unfollow($playlist_id, $usr_id) {
|
||||
$sql = "DELETE FROM db_playlist_follows
|
||||
WHERE playlist_id = ? AND usr_id = ?";
|
||||
|
||||
return $this->db->execute($sql, [$playlist_id, $usr_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track playlist play
|
||||
*/
|
||||
public function trackPlay($playlist_id, $video_id = null, $usr_id = null) {
|
||||
$sql = "INSERT INTO db_playlist_plays (playlist_id, video_id, usr_id)
|
||||
VALUES (?, ?, ?)";
|
||||
|
||||
$this->db->execute($sql, [$playlist_id, $video_id, $usr_id]);
|
||||
|
||||
// Update view count
|
||||
$this->db->execute(
|
||||
"UPDATE db_playlists SET view_count = view_count + 1 WHERE playlist_id = ?",
|
||||
[$playlist_id]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's playlists
|
||||
*/
|
||||
public function getUserPlaylists($usr_id, $include_private = false) {
|
||||
$where = $include_private ? "" : "AND privacy = 'public'";
|
||||
|
||||
$sql = "SELECT p.*,
|
||||
(SELECT COUNT(*) FROM db_playlist_items WHERE playlist_id = p.playlist_id) as video_count
|
||||
FROM db_playlists p
|
||||
WHERE p.usr_id = ? $where
|
||||
ORDER BY p.updated_at DESC";
|
||||
|
||||
$result = $this->db->execute($sql, [$usr_id]);
|
||||
|
||||
if (!$result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->db->resultsToArray($result);
|
||||
}
|
||||
|
||||
// Permission helpers
|
||||
|
||||
private function canView($playlist, $current_user_id) {
|
||||
if ($playlist['privacy'] === 'public') return true;
|
||||
if (!$current_user_id) return false;
|
||||
if ($playlist['usr_id'] == $current_user_id) return true;
|
||||
if ($this->isCollaborator($playlist['playlist_id'], $current_user_id)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isOwner($playlist_id, $usr_id) {
|
||||
$sql = "SELECT usr_id FROM db_playlists WHERE playlist_id = ?";
|
||||
$result = $this->db->execute($sql, [$playlist_id]);
|
||||
|
||||
if (!$result) return false;
|
||||
|
||||
$row = $this->db->fetch($result);
|
||||
return $row && $row['usr_id'] == $usr_id;
|
||||
}
|
||||
|
||||
private function isCollaborator($playlist_id, $usr_id) {
|
||||
$sql = "SELECT 1 FROM db_playlist_collaborators
|
||||
WHERE playlist_id = ? AND usr_id = ?";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $usr_id]);
|
||||
return $result && $this->db->rowCount($result) > 0;
|
||||
}
|
||||
|
||||
private function isFollowing($playlist_id, $usr_id) {
|
||||
$sql = "SELECT 1 FROM db_playlist_follows
|
||||
WHERE playlist_id = ? AND usr_id = ?";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $usr_id]);
|
||||
return $result && $this->db->rowCount($result) > 0;
|
||||
}
|
||||
|
||||
private function canAddVideos($playlist_id, $usr_id) {
|
||||
if ($this->isOwner($playlist_id, $usr_id)) return true;
|
||||
|
||||
$sql = "SELECT can_add FROM db_playlist_collaborators
|
||||
WHERE playlist_id = ? AND usr_id = ?";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $usr_id]);
|
||||
|
||||
if (!$result || $this->db->rowCount($result) == 0) return false;
|
||||
|
||||
$row = $this->db->fetch($result);
|
||||
return $row['can_add'] == 1;
|
||||
}
|
||||
|
||||
private function canRemoveVideos($playlist_id, $usr_id) {
|
||||
if ($this->isOwner($playlist_id, $usr_id)) return true;
|
||||
|
||||
$sql = "SELECT can_remove FROM db_playlist_collaborators
|
||||
WHERE playlist_id = ? AND usr_id = ?";
|
||||
|
||||
$result = $this->db->execute($sql, [$playlist_id, $usr_id]);
|
||||
|
||||
if (!$result || $this->db->rowCount($result) == 0) return false;
|
||||
|
||||
$row = $this->db->fetch($result);
|
||||
return $row['can_remove'] == 1;
|
||||
}
|
||||
|
||||
private function updateVideoCount($playlist_id) {
|
||||
$sql = "UPDATE db_playlists
|
||||
SET video_count = (SELECT COUNT(*) FROM db_playlist_items WHERE playlist_id = ?)
|
||||
WHERE playlist_id = ?";
|
||||
|
||||
return $this->db->execute($sql, [$playlist_id, $playlist_id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user