6 * HOUR_IN_SECONDS, 'display' => 'Every 6 hours'];
return $s;
});
add_action('init', function () {
if (!wp_next_scheduled('anj_yt_sync_event')) {
wp_schedule_event(time() + 300, 'anj_6h', 'anj_yt_sync_event');
}
});
add_action('anj_yt_sync_event', 'anj_yt_sync');
/* ---------- طلب من YouTube API ---------- */
function anj_yt_api($endpoint, $args) {
$s = get_option(ANJ_YT_OPT, []);
if (empty($s['key'])) throw new Exception('لم يتم إدخال مفتاح API.');
$args['key'] = $s['key'];
$r = wp_remote_get('https://www.googleapis.com/youtube/v3/' . $endpoint . '?' . http_build_query($args), ['timeout' => 25]);
if (is_wp_error($r)) throw new Exception($r->get_error_message());
$b = json_decode(wp_remote_retrieve_body($r), true);
if (!is_array($b)) throw new Exception('رد غير مفهوم من يوتيوب.');
if (isset($b['error'])) throw new Exception('YouTube: ' . ($b['error']['message'] ?? 'خطأ غير معروف'));
return $b;
}
/* كل عناصر قائمة تشغيل (مع تجاوز حد 50 لكل صفحة) */
function anj_yt_playlist_ids($playlist_id) {
$ids = [];
$token = '';
do {
$a = ['part' => 'contentDetails,status', 'playlistId' => $playlist_id, 'maxResults' => 50];
if ($token) $a['pageToken'] = $token;
$b = anj_yt_api('playlistItems', $a);
foreach ($b['items'] ?? [] as $it) {
if (($it['status']['privacyStatus'] ?? '') === 'public') {
$ids[] = $it['contentDetails']['videoId'];
}
}
$token = $b['nextPageToken'] ?? '';
} while ($token);
return $ids;
}
/* PT1H2M3S → ثوانٍ */
function anj_yt_seconds($iso) {
if (!preg_match('/P(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/', (string)$iso, $m)) return 0;
return ((int)($m[1] ?? 0)) * 86400 + ((int)($m[2] ?? 0)) * 3600 + ((int)($m[3] ?? 0)) * 60 + (int)($m[4] ?? 0);
}
/* هل الفيديو Short؟ (رابط /shorts يعطي 200 للـ Shorts ويحوّل للفيديو العادي) */
function anj_yt_is_short($id, $sec) {
if ($sec > 180) return 0;
$r = wp_remote_head('https://www.youtube.com/shorts/' . $id, ['redirection' => 0, 'timeout' => 10]);
if (is_wp_error($r)) return $sec <= 60 ? 1 : 0;
return wp_remote_retrieve_response_code($r) == 200 ? 1 : 0;
}
/* ---------- المزامنة ---------- */
function anj_yt_sync() {
@set_time_limit(300);
$s = get_option(ANJ_YT_OPT, []);
$handle = ltrim(trim($s['handle'] ?? 'Chem31phys'), '@');
try {
// 1) القناة وقائمة الرفع
$ch = anj_yt_api('channels', ['part' => 'contentDetails', 'forHandle' => $handle]);
if (empty($ch['items'][0])) throw new Exception('لم يتم العثور على القناة @' . $handle);
$channel_id = $ch['items'][0]['id'];
$uploads = $ch['items'][0]['contentDetails']['relatedPlaylists']['uploads'];
// 2) كل الفيديوهات العامة
$ids = array_values(array_unique(anj_yt_playlist_ids($uploads)));
// 3) قوائم التشغيل: فيديو ← أسماء القوائم
$pl_map = [];
$token = '';
do {
$a = ['part' => 'snippet', 'channelId' => $channel_id, 'maxResults' => 50];
if ($token) $a['pageToken'] = $token;
$b = anj_yt_api('playlists', $a);
foreach ($b['items'] ?? [] as $pl) {
$title = $pl['snippet']['title'];
foreach (anj_yt_playlist_ids($pl['id']) as $vid) $pl_map[$vid][] = $title;
}
$token = $b['nextPageToken'] ?? '';
} while ($token);
// 4) تفاصيل الفيديوهات (50 في كل طلب)
$old = get_option(ANJ_YT_DATA, []);
$old_sh = [];
foreach ($old['videos'] ?? [] as $v) $old_sh[$v['i']] = $v['sh'];
$videos = [];
foreach (array_chunk($ids, 50) as $chunk) {
$b = anj_yt_api('videos', ['part' => 'snippet,contentDetails,statistics,status', 'id' => implode(',', $chunk)]);
foreach ($b['items'] ?? [] as $v) {
if (($v['status']['privacyStatus'] ?? '') !== 'public') continue;
if (($v['snippet']['liveBroadcastContent'] ?? 'none') !== 'none') continue;
$id = $v['id'];
$sec = anj_yt_seconds($v['contentDetails']['duration'] ?? '');
$kw = implode(' ', array_slice($v['snippet']['tags'] ?? [], 0, 15)) . ' ' .
mb_substr($v['snippet']['description'] ?? '', 0, 200);
$videos[] = [
'i' => $id,
't' => $v['snippet']['title'],
'd' => $sec,
'dt' => substr($v['snippet']['publishedAt'] ?? '', 0, 10),
'v' => (int)($v['statistics']['viewCount'] ?? 0),
'sh' => array_key_exists($id, $old_sh) ? $old_sh[$id] : anj_yt_is_short($id, $sec),
'pl' => $pl_map[$id] ?? [],
'k' => trim($kw),
];
}
}
if (!$videos) throw new Exception('لم يرجع يوتيوب أي فيديو. لم يتم استبدال البيانات القديمة.');
update_option(ANJ_YT_DATA, ['updated' => time(), 'channel' => '@' . $handle, 'videos' => $videos], false);
update_option(ANJ_YT_STATUS, ['ok' => true, 'time' => time(), 'msg' => count($videos) . ' فيديو'], false);
return true;
} catch (Exception $e) {
// عند أي خطأ تبقى آخر بيانات ناجحة كما هي
update_option(ANJ_YT_STATUS, ['ok' => false, 'time' => time(), 'msg' => $e->getMessage()], false);
return false;
}
}
/* ---------- رابط البيانات للصفحة ---------- */
add_action('rest_api_init', function () {
register_rest_route('alnojoum/v1', '/videos', [
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => function () {
$d = get_option(ANJ_YT_DATA, ['updated' => 0, 'videos' => []]);
$r = rest_ensure_response($d);
$r->header('Cache-Control', 'public, max-age=600');
return $r;
},
]);
});
/* ---------- صفحة الإعدادات (للمدير فقط) ---------- */
add_action('admin_menu', function () {
add_management_page('مكتبة فيديوهات يوتيوب', 'مكتبة يوتيوب', 'manage_options', 'anj-yt', 'anj_yt_admin_page');
});
add_action('admin_post_anj_yt_save', function () {
if (!current_user_can('manage_options')) wp_die('غير مسموح');
check_admin_referer('anj_yt_save');
update_option(ANJ_YT_OPT, [
'key' => sanitize_text_field(wp_unslash($_POST['key'] ?? '')),
'handle' => sanitize_text_field(wp_unslash($_POST['handle'] ?? 'Chem31phys')),
], false);
anj_yt_sync();
wp_safe_redirect(admin_url('tools.php?page=anj-yt&done=1'));
exit;
});
add_action('admin_post_anj_yt_refresh', function () {
if (!current_user_can('manage_options')) wp_die('غير مسموح');
check_admin_referer('anj_yt_refresh');
anj_yt_sync();
wp_safe_redirect(admin_url('tools.php?page=anj-yt&done=1'));
exit;
});
function anj_yt_admin_page() {
$s = get_option(ANJ_YT_OPT, ['key' => '', 'handle' => 'Chem31phys']);
$st = get_option(ANJ_YT_STATUS, []);
$d = get_option(ANJ_YT_DATA, []);
$next = wp_next_scheduled('anj_yt_sync_event');
?>
مكتبة فيديوهات يوتيوب – أكاديمية النجوم
عدد الفيديوهات في المكتبة الآن:
· التحديث التلقائي القادم:
الإعدادات