/** * Add pagination headers to a WP_REST_Response object. */ function app_v2_response_with_pagination($response, $query, $perPage, $page) { if (!$response instanceof WP_REST_Response) { $response = rest_ensure_response($response); } $response->header('X-WP-Total', (int) $query->found_posts); $response->header('X-WP-TotalPages', (int) $query->max_num_pages); $response->header('X-WP-Current-Page', $page); $response->header('X-WP-Per-Page', $perPage); return $response; } /** * Get meta_box details for a chapter post. */ function app_v2_get_chapter_meta_box($postId) { return [ 'volume' => get_post_meta($postId, 'ero_volume', true), 'chapter' => get_post_meta($postId, 'ero_chapter', true), 'title' => get_post_meta($postId, 'ero_title', true), 'series' => get_post_meta($postId, 'ero_series', true), ]; } /** * Get the last N chapters for a given series. * Returns simplified chapter objects. */ function app_v2_get_series_recent_chapters($seriesId, $limit = 3) { $catSlug = get_post_field('post_name', $seriesId); $query = new WP_Query([ 'post_type' => 'post', 'posts_per_page' => $limit, 'orderby' => 'date', 'order' => 'DESC', 'tax_query' => [[ 'taxonomy' => 'category', 'field' => 'slug', 'terms' => $catSlug, ]], 'no_found_rows' => true, ]); $chapters = []; foreach ($query->posts as $ch) { $meta = app_v2_get_chapter_meta_box($ch->ID); $chapters[] = [ 'id' => $ch->ID, 'title' => $meta['title'] ?: $ch->post_title, 'date' => $ch->post_date, 'volume' => $meta['volume'] ?: '', 'chapter' => $meta['chapter'] ?: '', ]; } return $chapters; } /** * Get latest series list (ordered by latest chapter) */ function app_v2_get_latest_series($request) { global $wpdb; $perPage = min(50, max(1, intval($request->get_param('per_page')) ?: 20)); $page = max(1, intval($request->get_param('page')) ?: 1); $offset = ($page - 1) * $perPage; $sql = " SELECT DISTINCT s.ID as series_id, s.post_title as series_name, s.post_name as series_slug, MAX(p.post_date) as last_update FROM {$wpdb->posts} s INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id IN ( SELECT t.term_id FROM {$wpdb->terms} t WHERE t.slug = s.post_name ) INNER JOIN {$wpdb->term_relationships} tr ON tr.term_taxonomy_id = tt.term_taxonomy_id INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id WHERE s.post_type = 'series' AND s.post_status = 'publish' AND p.post_type = 'post' AND p.post_status = 'publish' AND tt.taxonomy = 'category' GROUP BY s.ID ORDER BY last_update DESC LIMIT %d OFFSET %d "; $results = $wpdb->get_results($wpdb->prepare($sql, $perPage, $offset)); $countSql = " SELECT COUNT(DISTINCT s.ID) as total FROM {$wpdb->posts} s INNER JOIN {$wpdb->term_taxonomy} tt ON tt.term_id IN ( SELECT t.term_id FROM {$wpdb->terms} t WHERE t.slug = s.post_name ) INNER JOIN {$wpdb->term_relationships} tr ON tr.term_taxonomy_id = tt.term_taxonomy_id INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id WHERE s.post_type = 'series' AND s.post_status = 'publish' AND p.post_type = 'post' AND p.post_status = 'publish' AND tt.taxonomy = 'category' "; $total = $wpdb->get_var($countSql); $totalPages = ceil($total / $perPage); $seriesData = []; foreach ($results as $row) { $seriesId = $row->series_id; $seriesData[] = [ 'id' => $seriesId, 'name' => $row->series_name, 'slug' => $row->series_slug, 'coverImage' => get_the_post_thumbnail_url($seriesId, 'medium') ?: '', 'lastUpdate' => $row->last_update, 'status' => get_post_meta($seriesId, 'ero_status', true) ?: 'Unknown', 'lastChapters' => app_v2_get_series_recent_chapters($seriesId, 3), ]; } $response = rest_ensure_response($seriesData); $response->header('X-WP-Total', intval($total)); $response->header('X-WP-TotalPages', intval($totalPages)); $response->header('X-WP-Current-Page', $page); $response->header('X-WP-Per-Page', $perPage); return $response; } /** * Series details (no last chapter) */ function app_v2_get_series_details($request) { $id = intval($request['id']); if (!$id) { return new WP_Error('invalid_id', 'Invalid id', ['status' => 400]); } if (get_post_type($id) !== 'series') { return new WP_Error('not_series', 'Provided ID is not a series.', ['status' => 400]); } $seriesId = $id; $authorName = get_the_author_meta('display_name', get_post_field('post_author', $seriesId)); return rest_ensure_response([ 'id' => $seriesId, 'name' => get_the_title($seriesId), 'slug' => get_post_field('post_name', $seriesId), 'description'=> wp_strip_all_tags(get_post_field('post_content', $seriesId)), 'coverImage' => get_the_post_thumbnail_url($seriesId, 'full') ?: '', 'wpAuthor' => $authorName, 'altName' => get_post_meta($seriesId, 'ero_alt', true), 'author' => strip_tags(get_the_term_list($seriesId, 'writer', '', ', ')), 'status' => get_post_meta($seriesId, 'ero_status', true) ?: 'Unknown', 'year' => get_post_meta($seriesId, 'ero_date', true), ]); } /** * Chapter content */ function app_v2_get_chapter_content($request) { $id = intval($request['id']); $post = get_post($id); if (!$post || $post->post_type !== 'post') { return new WP_Error('invalid_chapter', 'Chapter not found', ['status' => 404]); } $series = strip_tags(get_the_term_list($id, 'category', '', ', ')); $metaTitle = get_post_meta($id, 'ero_title', true); $metaVolume = get_post_meta($id, 'ero_volume', true); $metaChapter = get_post_meta($id, 'ero_chapter', true); return rest_ensure_response([ 'id' => $post->ID, 'title' => $metaTitle ?: $post->post_title, 'content' => apply_filters('the_content', $post->post_content), 'date' => $post->post_date, 'series' => $series, 'volume' => $metaVolume ?: '', 'chapterNumber'=> $metaChapter ?: '', ]); } /** * Series list (alphabetical) */ function app_v2_get_all_series($request) { $perPage = min(100, max(1, intval($request->get_param('per_page')) ?: 50)); $page = max(1, intval($request->get_param('page')) ?: 1); $search = sanitize_text_field($request->get_param('search')); $idsParam = $request->get_param('ids'); $ids = []; if (!empty($idsParam)) { $ids = array_filter(array_map('intval', explode(',', $idsParam))); } $args = [ 'post_type' => 'series', 'posts_per_page' => $perPage, 'paged' => $page, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => false, ]; if (!empty($search)) $args['s'] = $search; if (!empty($ids)) { $args['post__in'] = $ids; $args['orderby'] = 'post__in'; } $query = new WP_Query($args); $data = []; foreach ($query->posts as $s) { $data[] = [ 'id' => $s->ID, 'name' => get_the_title($s->ID), 'slug' => $s->post_name, 'coverImage' => get_the_post_thumbnail_url($s->ID, 'medium') ?: '', 'status' => get_post_meta($s->ID, 'ero_status', true) ?: 'Unknown', 'year' => get_post_meta($s->ID, 'ero_date', true) ?: '', ]; } return app_v2_response_with_pagination(rest_ensure_response($data), $query, $perPage, $page); } /** * Chapters for a series */ function app_v2_get_series_chapters($request) { $seriesId = intval($request->get_param('series')); if (!$seriesId || get_post_type($seriesId) !== 'series') { return new WP_Error('invalid_series_id', 'Valid series ID required.', ['status' => 400]); } $perPage = max(1, intval($request->get_param('per_page')) ?: 20); $page = max(1, intval($request->get_param('page')) ?: 1); $catSlug = get_post_field('post_name', $seriesId); $orderParam = (string) $request->get_param('order'); $queryOrder = strtoupper($orderParam) === 'DESC' ? 'DESC' : 'ASC'; $query = new WP_Query([ 'post_type' => 'post', 'posts_per_page' => $perPage, 'paged' => $page, 'orderby' => 'date', 'order' => $queryOrder, 'no_found_rows' => false, 'tax_query' => [[ 'taxonomy' => 'category', 'field' => 'slug', 'terms' => $catSlug, ]], ]); $chapters = []; foreach ($query->posts as $ch) { $meta = app_v2_get_chapter_meta_box($ch->ID); $chapters[] = [ 'id' => $ch->ID, 'title' => $meta['title'] ?: $ch->post_title, 'date' => $ch->post_date, 'volume' => $meta['volume'] ?: '', 'chapter' => $meta['chapter'] ?: '', ]; } return app_v2_response_with_pagination(rest_ensure_response($chapters), $query, $perPage, $page); } /** * Fast chapter count for multiple series */ function app_v2_get_multiple_series_counts($request) { $idsParam = $request->get_param('ids'); if (empty($idsParam)) { return rest_ensure_response([]); } $ids = array_filter(array_map('intval', explode(',', $idsParam))); $results = []; foreach ($ids as $seriesId) { $catSlug = get_post_field('post_name', $seriesId); $categoryTerm = get_term_by('slug', $catSlug, 'category'); $results[$seriesId] = (!is_wp_error($categoryTerm) && $categoryTerm) ? intval($categoryTerm->count) : 0; } return rest_ensure_response($results); } /** * Fast chapter count */ function app_v2_get_series_chapters_fast_count($request) { $seriesId = intval($request->get_param('series_id')); if (!$seriesId || get_post_type($seriesId) !== 'series') { return new WP_Error('invalid_series_id', 'Valid series ID required.', ['status' => 400]); } $catSlug = get_post_field('post_name', $seriesId); $categoryTerm = get_term_by('slug', $catSlug, 'category'); return (!is_wp_error($categoryTerm) && $categoryTerm) ? intval($categoryTerm->count) : 0; } /** * Register routes */ add_action('rest_api_init', function () { register_rest_route('app/v2', '/latest', [ 'methods' => 'GET', 'callback' => 'app_v2_get_latest_series', ]); register_rest_route('app/v2', '/series/(?P\d+)', [ 'methods' => 'GET', 'callback' => 'app_v2_get_series_details', 'permission_callback' => '__return_true', ]); register_rest_route('app/v2', '/chapters/(?P\d+)', [ 'methods' => 'GET', 'callback' => 'app_v2_get_chapter_content', 'permission_callback' => '__return_true', ]); register_rest_route('app/v2', '/series', [ 'methods' => 'GET', 'callback' => 'app_v2_get_all_series', 'permission_callback' => '__return_true', ]); register_rest_route('app/v2', '/chapters', [ 'methods' => 'GET', 'callback' => 'app_v2_get_series_chapters', 'permission_callback' => '__return_true', ]); register_rest_route('app/v2', '/series/(?P\d+)/count', [ 'methods' => 'GET', 'callback' => 'app_v2_get_series_chapters_fast_count', 'permission_callback' => '__return_true', ]); register_rest_route('app/v2', '/series/counts', [ 'methods' => 'GET', 'callback' => 'app_v2_get_multiple_series_counts', 'permission_callback' => '__return_true', ]); });