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;
|
||||
}
|
||||
183
f_scripts/migrations/update_jw_branding.php
Normal file
183
f_scripts/migrations/update_jw_branding.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
/*
|
||||
* EasyStream – JW Player Branding Migration
|
||||
* Safely updates serialized JW Player config in `db_fileplayers` to remove legacy ViewShark branding.
|
||||
* Usage: php f_scripts/migrations/update_jw_branding.php
|
||||
*/
|
||||
|
||||
define('_ISVALID', true);
|
||||
|
||||
// Bootstrap core to reuse DB + config
|
||||
require_once __DIR__ . '/../../f_core/config.core.php';
|
||||
|
||||
/**
|
||||
* Unserialize safely (no objects), returning array or null
|
||||
*/
|
||||
function safe_unserialize($str)
|
||||
{
|
||||
if (!is_string($str) || $str === '') {
|
||||
return null;
|
||||
}
|
||||
// PHP >= 7 allows allowed_classes => false
|
||||
$data = @unserialize($str, ['allowed_classes' => false]);
|
||||
if ($data === false || !is_array($data)) {
|
||||
// Try without options for older compatibility
|
||||
$data = @unserialize($str);
|
||||
if (!is_array($data)) {
|
||||
// Some environments may deliver escaped content from DB; try un-escaping
|
||||
$clean = stripslashes($str);
|
||||
$data = @unserialize($clean, ['allowed_classes' => false]);
|
||||
if ($data === false || !is_array($data)) {
|
||||
$data = @unserialize($clean);
|
||||
if (!is_array($data)) {
|
||||
// Last resort: stripcslashes
|
||||
$clean2 = stripcslashes($str);
|
||||
$data = @unserialize($clean2, ['allowed_classes' => false]);
|
||||
if ($data === false || !is_array($data)) {
|
||||
$data = @unserialize($clean2);
|
||||
if (!is_array($data)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply branding changes to JW config array
|
||||
*/
|
||||
function apply_branding(array $cfg, string $siteName, string $siteUrl): array
|
||||
{
|
||||
$changed = false;
|
||||
|
||||
$replacements = [
|
||||
// Remove external logo file by default; admin can set later from UI
|
||||
'jw_logo_file' => '',
|
||||
// Point links to local site if available
|
||||
'jw_logo_link' => $siteUrl,
|
||||
// Update right-click text/link
|
||||
'jw_rc_text' => 'Powered by ' . ($siteName ?: 'EasyStream'),
|
||||
'jw_rc_link' => $siteUrl,
|
||||
];
|
||||
|
||||
foreach ($replacements as $key => $value) {
|
||||
if (array_key_exists($key, $cfg) && $cfg[$key] !== $value) {
|
||||
$cfg[$key] = $value;
|
||||
$changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If jw_share_enabled is set, ensure no external domains are hardcoded in share link defaults
|
||||
if (isset($cfg['jw_share_link']) && is_string($cfg['jw_share_link'])) {
|
||||
// Replace known legacy domains if present
|
||||
$legacy = ['viewsharkdemo.com', 'viewshark.com'];
|
||||
foreach ($legacy as $dom) {
|
||||
if (stripos($cfg['jw_share_link'], $dom) !== false) {
|
||||
$cfg['jw_share_link'] = '';
|
||||
$changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cfg['__changed__'] = $changed; // marker for caller
|
||||
return $cfg;
|
||||
}
|
||||
|
||||
try {
|
||||
global $db, $cfg; // from config.core.php
|
||||
|
||||
if (!isset($db)) {
|
||||
throw new Exception('Database connection not initialized.');
|
||||
}
|
||||
|
||||
$siteName = $cfg['site_name'] ?? 'EasyStream';
|
||||
$siteUrl = $cfg['site_url'] ?? '';
|
||||
|
||||
$targets = ['jw_local', 'jw_embed'];
|
||||
$updated = 0;
|
||||
|
||||
foreach ($targets as $name) {
|
||||
$rs = $db->Execute("SELECT `db_id`, `db_config` FROM `db_fileplayers` WHERE `db_name` = ? LIMIT 1", [$name]);
|
||||
if ($rs && !$rs->EOF) {
|
||||
$dbId = (int)$rs->fields['db_id'];
|
||||
$raw = $rs->fields['db_config'];
|
||||
$arr = safe_unserialize($raw);
|
||||
|
||||
if (is_array($arr)) {
|
||||
$arr = apply_branding($arr, $siteName, $siteUrl);
|
||||
$changed = !empty($arr['__changed__']);
|
||||
unset($arr['__changed__']);
|
||||
|
||||
if ($changed) {
|
||||
$serialized = serialize($arr);
|
||||
$ok = $db->Execute("UPDATE `db_fileplayers` SET `db_config` = ? WHERE `db_id` = ?", [$serialized, $dbId]);
|
||||
if ($ok) {
|
||||
echo "Updated branding for {$name} (db_id={$dbId})\n";
|
||||
$updated++;
|
||||
} else {
|
||||
echo "Failed to update {$name} (db_id={$dbId}): " . $db->ErrorMsg() . "\n";
|
||||
}
|
||||
} else {
|
||||
echo "No changes needed for {$name}\n";
|
||||
}
|
||||
} else {
|
||||
// Fallback: attempt in-place serialized string rewriting for known keys
|
||||
$orig = (string)$raw;
|
||||
$updatedSerialized = $orig;
|
||||
|
||||
$kv = [
|
||||
'jw_logo_file' => '',
|
||||
'jw_logo_link' => $siteUrl,
|
||||
'jw_rc_text' => 'Powered by ' . ($siteName ?: 'EasyStream'),
|
||||
'jw_rc_link' => $siteUrl,
|
||||
];
|
||||
|
||||
$didChange = false;
|
||||
foreach ($kv as $k => $v) {
|
||||
$escapedKey = preg_quote($k, '/');
|
||||
$replacement = function ($m) use ($v) {
|
||||
$len = strlen($v);
|
||||
$safe = str_replace('"', '"', $v); // value is plain; ensure quotes are safe
|
||||
return $m[1] . 's:' . $len . ':"' . $safe . '";';
|
||||
};
|
||||
$pattern = '/(s:\d+:"' . $escapedKey . '";s:)\d+:("[\s\S]*?");/';
|
||||
$new = preg_replace_callback($pattern, $replacement, $updatedSerialized, 1, $count);
|
||||
if ($count > 0 && is_string($new)) {
|
||||
$updatedSerialized = $new;
|
||||
$didChange = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($didChange) {
|
||||
$ok = $db->Execute("UPDATE `db_fileplayers` SET `db_config` = ? WHERE `db_id` = ?", [$updatedSerialized, $dbId]);
|
||||
if ($ok) {
|
||||
echo "Updated branding (fallback) for {$name} (db_id={$dbId})\n";
|
||||
$updated++;
|
||||
} else {
|
||||
echo "Failed fallback update {$name} (db_id={$dbId}): " . $db->ErrorMsg() . "\n";
|
||||
}
|
||||
} else {
|
||||
$snippet = substr((string)$raw, 0, 160);
|
||||
echo "Could not unserialize config for {$name}; snippet: " . str_replace(["\n","\r"], ['\\n',''], $snippet) . "\n";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "No db_fileplayers row found for {$name}; skipping.\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "Done. Updated {$updated} row(s).\n";
|
||||
exit(0);
|
||||
} catch (Throwable $e) {
|
||||
// Best-effort logging
|
||||
if (class_exists('VLogger')) {
|
||||
$logger = VLogger::getInstance();
|
||||
$logger->log(VLogger::ERROR, 'JW branding migration failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
fwrite(STDERR, 'Error: ' . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user