Sync current dev state
This commit is contained in:
698
f_scripts/fe/js/api-helper.js
Normal file
698
f_scripts/fe/js/api-helper.js
Normal file
@@ -0,0 +1,698 @@
|
||||
/**
|
||||
* 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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
async put(endpoint, data = {}) {
|
||||
return this.request(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
* @param {string} endpoint API endpoint
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
async getCurrentUser() {
|
||||
if (!this.isAuthenticated()) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
return this.get('/auth.php?action=verify_token');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify token validity
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
async getVideos(params = {}) {
|
||||
return this.get('/videos.php', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get video by ID
|
||||
* @param {string} videoId Video ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getVideo(videoId) {
|
||||
return this.get(`/videos.php?id=${videoId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload video metadata
|
||||
* @param {Object} videoData Video metadata
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async createVideo(videoData) {
|
||||
return this.post('/videos.php', videoData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update video
|
||||
* @param {string} videoId Video ID
|
||||
* @param {Object} videoData Updated video data
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async updateVideo(videoId, videoData) {
|
||||
return this.put(`/videos.php?id=${videoId}`, videoData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete video
|
||||
* @param {string} videoId Video ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async deleteVideo(videoId) {
|
||||
return this.delete(`/videos.php?id=${videoId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search videos
|
||||
* @param {string} query Search query
|
||||
* @param {Object} params Additional parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
async getMyProfile() {
|
||||
return this.get('/user.php?action=me');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
* @param {Object} userData Updated user data
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async updateProfile(userData) {
|
||||
return this.put('/user.php', userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload user avatar
|
||||
* @param {File} file Avatar file
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
async getUserSubscriptions() {
|
||||
return this.get('/user.php?action=subscriptions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's subscribers
|
||||
* @param {number} userId User ID (optional)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
async updateComment(commentId, commentText) {
|
||||
return this.put(`/comments.php?id=${commentId}`, {
|
||||
comment_text: commentText
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete comment
|
||||
* @param {number} commentId Comment ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async deleteComment(commentId) {
|
||||
return this.delete(`/comments.php?id=${commentId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like/unlike comment
|
||||
* @param {number} commentId Comment ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
async getSubscriptionFeed(params = {}) {
|
||||
return this.get('/subscriptions.php?action=feed', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if subscribed to a channel
|
||||
* @param {number} channelId Channel ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async checkSubscription(channelId) {
|
||||
return this.get('/subscriptions.php?action=check', { channel_id: channelId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a channel
|
||||
* @param {number} channelId Channel ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async subscribe(channelId) {
|
||||
return this.post('/subscriptions.php', { channel_id: channelId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from a channel
|
||||
* @param {number} channelId Channel ID
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async unsubscribe(channelId) {
|
||||
return this.delete(`/subscriptions.php?channel_id=${channelId}`);
|
||||
}
|
||||
|
||||
// ===== Social API Methods =====
|
||||
|
||||
/**
|
||||
* Get social feed
|
||||
* @param {Object} params Query parameters
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
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<Object>}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user