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:
Krystie
2026-03-30 17:16:37 -07:00
parent f50b493df2
commit 9928a23c25
6 changed files with 1891 additions and 0 deletions
+422
View File
@@ -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]);
}
}