/** * EasyStream API Helper * Modern fetch-based API client with JWT token management */ class EasyStreamAPI { constructor(baseURL = '/api') { this.baseURL = baseURL; this.token = this.getStoredToken(); this.tokenExpiry = this.getStoredTokenExpiry(); } /** * Store JWT token in localStorage * @param {string} token JWT token * @param {number} expiresIn Expiry time in seconds */ setToken(token, expiresIn = 86400) { this.token = token; this.tokenExpiry = Date.now() + (expiresIn * 1000); if (typeof localStorage !== 'undefined') { localStorage.setItem('jwt_token', token); localStorage.setItem('jwt_expiry', this.tokenExpiry.toString()); } } /** * Get stored token from localStorage * @returns {string|null} */ getStoredToken() { if (typeof localStorage === 'undefined') return null; const expiry = localStorage.getItem('jwt_expiry'); if (expiry && Date.now() > parseInt(expiry)) { // Token expired, clear it this.clearToken(); return null; } return localStorage.getItem('jwt_token'); } /** * Get stored token expiry * @returns {number|null} */ getStoredTokenExpiry() { if (typeof localStorage === 'undefined') return null; const expiry = localStorage.getItem('jwt_expiry'); return expiry ? parseInt(expiry) : null; } /** * Clear stored token */ clearToken() { this.token = null; this.tokenExpiry = null; if (typeof localStorage !== 'undefined') { localStorage.removeItem('jwt_token'); localStorage.removeItem('jwt_expiry'); } } /** * Check if user is authenticated * @returns {boolean} */ isAuthenticated() { if (!this.token) return false; if (this.tokenExpiry && Date.now() > this.tokenExpiry) { this.clearToken(); return false; } return true; } /** * Get authorization headers * @returns {Object} */ getAuthHeaders() { const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; if (this.token) { headers['Authorization'] = `Bearer ${this.token}`; } return headers; } /** * Make API request * @param {string} endpoint API endpoint * @param {Object} options Fetch options * @returns {Promise} */ async request(endpoint, options = {}) { const url = endpoint.startsWith('http') ? endpoint : `${this.baseURL}${endpoint}`; const config = { ...options, headers: { ...this.getAuthHeaders(), ...options.headers } }; try { const response = await fetch(url, config); // Handle different response types const contentType = response.headers.get('content-type'); let data; if (contentType && contentType.includes('application/json')) { data = await response.json(); } else { data = await response.text(); } if (!response.ok) { // Handle authentication errors if (response.status === 401) { this.clearToken(); throw new Error(data.message || 'Authentication failed'); } throw new Error(data.message || `Request failed with status ${response.status}`); } return data; } catch (error) { console.error('API request failed:', error); throw error; } } /** * GET request * @param {string} endpoint API endpoint * @param {Object} params Query parameters * @returns {Promise} */ async get(endpoint, params = {}) { const queryString = new URLSearchParams(params).toString(); const url = queryString ? `${endpoint}?${queryString}` : endpoint; return this.request(url, { method: 'GET' }); } /** * POST request * @param {string} endpoint API endpoint * @param {Object} data Request body * @returns {Promise} */ async post(endpoint, data = {}) { return this.request(endpoint, { method: 'POST', body: JSON.stringify(data) }); } /** * PUT request * @param {string} endpoint API endpoint * @param {Object} data Request body * @returns {Promise} */ async put(endpoint, data = {}) { return this.request(endpoint, { method: 'PUT', body: JSON.stringify(data) }); } /** * DELETE request * @param {string} endpoint API endpoint * @returns {Promise} */ async delete(endpoint) { return this.request(endpoint, { method: 'DELETE' }); } // ===== Authentication Methods ===== /** * Login with JWT token * @param {string} identifier Username or email * @param {string} password Password * @param {number} expiresIn Optional token expiry in seconds * @returns {Promise} */ async login(identifier, password, expiresIn = null) { try { const data = await this.post('/auth.php?action=login_token', { identifier, password, expires_in: expiresIn }); if (data.success && data.token) { this.setToken(data.token, data.expires_in || 86400); } return data; } catch (error) { console.error('Login failed:', error); throw error; } } /** * Logout and clear token * @returns {Promise} */ async logout() { try { // Optionally call backend logout endpoint // (though with JWT, client-side token removal is usually sufficient) const data = await this.post('/auth.php?action=logout', {}); this.clearToken(); return data; } catch (error) { // Clear token even if API call fails this.clearToken(); throw error; } } /** * Get current user info * @returns {Promise} */ async getCurrentUser() { if (!this.isAuthenticated()) { throw new Error('Not authenticated'); } return this.get('/auth.php?action=verify_token'); } /** * Verify token validity * @returns {Promise} */ async verifyToken() { if (!this.token) return false; try { const data = await this.get('/auth.php?action=verify_token'); return data.success && data.valid; } catch (error) { this.clearToken(); return false; } } // ===== Video API Methods ===== /** * Get videos list * @param {Object} params Query parameters (page, limit, sort, etc.) * @returns {Promise} */ async getVideos(params = {}) { return this.get('/videos.php', params); } /** * Get video by ID * @param {string} videoId Video ID * @returns {Promise} */ async getVideo(videoId) { return this.get(`/videos.php?id=${videoId}`); } /** * Upload video metadata * @param {Object} videoData Video metadata * @returns {Promise} */ async createVideo(videoData) { return this.post('/videos.php', videoData); } /** * Update video * @param {string} videoId Video ID * @param {Object} videoData Updated video data * @returns {Promise} */ async updateVideo(videoId, videoData) { return this.put(`/videos.php?id=${videoId}`, videoData); } /** * Delete video * @param {string} videoId Video ID * @returns {Promise} */ async deleteVideo(videoId) { return this.delete(`/videos.php?id=${videoId}`); } /** * Search videos * @param {string} query Search query * @param {Object} params Additional parameters * @returns {Promise} */ async searchVideos(query, params = {}) { return this.get('/videos.php', { action: 'search', q: query, ...params }); } /** * Like/unlike video * @param {string} fileKey Video file key * @param {string} likeType 'like' or 'dislike' * @returns {Promise} */ async likeVideo(fileKey, likeType = 'like') { return this.post('/videos.php?action=like', { file_key: fileKey, like_type: likeType }); } /** * Increment video view count * @param {string} fileKey Video file key * @returns {Promise} */ async recordVideoView(fileKey) { return this.post('/videos.php?action=view', { file_key: fileKey }); } /** * Add/remove video from watch later * @param {string} fileKey Video file key * @returns {Promise} */ async toggleWatchLater(fileKey) { return this.post('/videos.php?action=watch_later', { file_key: fileKey }); } // ===== User API Methods ===== /** * Get user profile * @param {string} userId User ID (optional, defaults to current user) * @returns {Promise} */ async getUserProfile(userId = null) { if (userId) { return this.get(`/user.php?id=${userId}`); } return this.get('/user.php?action=profile'); } /** * Get current user's profile (alias) * @returns {Promise} */ async getMyProfile() { return this.get('/user.php?action=me'); } /** * Update user profile * @param {Object} userData Updated user data * @returns {Promise} */ async updateProfile(userData) { return this.put('/user.php', userData); } /** * Upload user avatar * @param {File} file Avatar file * @returns {Promise} */ async uploadAvatar(file) { const formData = new FormData(); formData.append('avatar', file); return this.request('/user.php?action=avatar', { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` }, body: formData }); } /** * Get user statistics * @returns {Promise} */ async getUserStats() { return this.get('/user.php?action=stats'); } /** * Get user's videos * @param {number} userId User ID (optional) * @param {Object} params Query parameters * @returns {Promise} */ async getUserVideos(userId = null, params = {}) { const queryParams = userId ? { ...params, id: userId } : params; return this.get('/user.php?action=videos', queryParams); } /** * Get user's subscriptions * @returns {Promise} */ async getUserSubscriptions() { return this.get('/user.php?action=subscriptions'); } /** * Get user's subscribers * @param {number} userId User ID (optional) * @returns {Promise} */ async getUserSubscribers(userId = null) { const params = userId ? { id: userId } : {}; return this.get('/user.php?action=subscribers', params); } // ===== Comments API Methods ===== /** * Get comments for a video * @param {string} fileKey Video file key * @param {Object} params Query parameters * @returns {Promise} */ async getComments(fileKey, params = {}) { return this.get('/comments.php', { file_key: fileKey, ...params }); } /** * Get single comment with replies * @param {number} commentId Comment ID * @returns {Promise} */ async getComment(commentId) { return this.get(`/comments.php?id=${commentId}`); } /** * Create new comment * @param {string} fileKey Video file key * @param {string} commentText Comment text * @param {number} parentId Parent comment ID (for replies) * @returns {Promise} */ async createComment(fileKey, commentText, parentId = null) { return this.post('/comments.php?action=create', { file_key: fileKey, comment_text: commentText, parent_id: parentId }); } /** * Update comment * @param {number} commentId Comment ID * @param {string} commentText Updated text * @returns {Promise} */ async updateComment(commentId, commentText) { return this.put(`/comments.php?id=${commentId}`, { comment_text: commentText }); } /** * Delete comment * @param {number} commentId Comment ID * @returns {Promise} */ async deleteComment(commentId) { return this.delete(`/comments.php?id=${commentId}`); } /** * Like/unlike comment * @param {number} commentId Comment ID * @returns {Promise} */ async likeComment(commentId) { return this.post('/comments.php?action=like', { comment_id: commentId }); } /** * Report comment * @param {number} commentId Comment ID * @param {string} reason Report reason * @returns {Promise} */ async reportComment(commentId, reason) { return this.post('/comments.php?action=report', { comment_id: commentId, reason: reason }); } // ===== Subscriptions API Methods ===== /** * Get user's subscriptions * @returns {Promise} */ async getSubscriptions() { return this.get('/subscriptions.php?action=list'); } /** * Get channel's subscribers * @param {number} channelId Channel ID * @param {Object} params Query parameters * @returns {Promise} */ async getSubscribers(channelId, params = {}) { return this.get('/subscriptions.php?action=subscribers', { channel_id: channelId, ...params }); } /** * Get subscription feed (videos from subscribed channels) * @param {Object} params Query parameters * @returns {Promise} */ async getSubscriptionFeed(params = {}) { return this.get('/subscriptions.php?action=feed', params); } /** * Check if subscribed to a channel * @param {number} channelId Channel ID * @returns {Promise} */ async checkSubscription(channelId) { return this.get('/subscriptions.php?action=check', { channel_id: channelId }); } /** * Subscribe to a channel * @param {number} channelId Channel ID * @returns {Promise} */ async subscribe(channelId) { return this.post('/subscriptions.php', { channel_id: channelId }); } /** * Unsubscribe from a channel * @param {number} channelId Channel ID * @returns {Promise} */ async unsubscribe(channelId) { return this.delete(`/subscriptions.php?channel_id=${channelId}`); } // ===== Social API Methods ===== /** * Get social feed * @param {Object} params Query parameters * @returns {Promise} */ async getSocialFeed(params = {}) { return this.get('/social.php', params); } // ===== Helper Methods ===== /** * Upload file with progress tracking * @param {string} endpoint Upload endpoint * @param {File} file File to upload * @param {Object} additionalData Additional form data * @param {Function} onProgress Progress callback * @returns {Promise} */ async uploadFile(endpoint, file, additionalData = {}, onProgress = null) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('file', file); // Add additional data Object.keys(additionalData).forEach(key => { formData.append(key, additionalData[key]); }); // Progress tracking if (onProgress && xhr.upload) { xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentComplete = (e.loaded / e.total) * 100; onProgress(percentComplete, e.loaded, e.total); } }); } // Handle completion xhr.addEventListener('load', () => { if (xhr.status >= 200 && xhr.status < 300) { try { const response = JSON.parse(xhr.responseText); resolve(response); } catch (e) { resolve({ success: true, data: xhr.responseText }); } } else { try { const error = JSON.parse(xhr.responseText); reject(new Error(error.message || `Upload failed with status ${xhr.status}`)); } catch (e) { reject(new Error(`Upload failed with status ${xhr.status}`)); } } }); // Handle errors xhr.addEventListener('error', () => { reject(new Error('Upload failed due to network error')); }); xhr.addEventListener('abort', () => { reject(new Error('Upload aborted')); }); // Add auth header if (this.token) { xhr.setRequestHeader('Authorization', `Bearer ${this.token}`); } // Send request const url = endpoint.startsWith('http') ? endpoint : `${this.baseURL}${endpoint}`; xhr.open('POST', url); xhr.send(formData); }); } /** * Handle API errors consistently * @param {Error} error Error object * @param {Function} callback Optional callback */ handleError(error, callback = null) { console.error('API Error:', error); // Show user-friendly error message const message = error.message || 'An unexpected error occurred'; if (callback) { callback(message); } else if (typeof window !== 'undefined' && window.showNotification) { window.showNotification('error', message); } else { alert(message); } return { success: false, error: message }; } } // Create global instance if (typeof window !== 'undefined') { window.EasyStreamAPI = EasyStreamAPI; window.api = new EasyStreamAPI(); } // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = EasyStreamAPI; }