if (!function_exists('debug_log')) { function debug_log($msg) { if (defined('APP_DEBUG') && APP_DEBUG) { error_log($msg); } } } // FANZA API連携クラス class FanzaAPI { private $api_id; private $affiliate_id; private $api_url = 'https://api.dmm.com/affiliate/v3/ItemList'; public function __construct($api_id, $affiliate_id) { $this->api_id = $api_id; $this->affiliate_id = $affiliate_id; // APIキーが空の場合の警告 if (empty($api_id) || empty($affiliate_id)) { debug_log('[FANZA API] Warning: APIキーが設定されていません - API ID: ' . (!empty($api_id) ? '設定あり' : '空') . ', Affiliate ID: ' . (!empty($affiliate_id) ? '設定あり' : '空')); } } // 同人商品検索 public function searchDoujinItems($params = [], $page = 1, $hits = 30) { // 文字列が渡された場合(後方互換性) if (is_string($params)) { $keyword = $params; $params = ['keyword' => $keyword]; } // ページ番号と件数を設定(1ページ目の場合はoffsetを設定しない) if ($page > 1) { $params['offset'] = ($page - 1) * $hits; } // 初回取得を軽量化(安定性向上のため50件以内) $params['hits'] = min(50, max(1, (int)$hits)); // APIキー未設定時は空を返す(サンプルは返さない) if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキー未設定: doujin search は空結果'); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } $cache_key = 'fanza_doujin_search_' . md5(json_encode($params) . '_' . $page . '_' . $hits); $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } // 試行候補を用意(安定性重視で service=doujin を先に試す) $candidates = []; $base = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'hits' => $hits, 'sort' => 'date', 'output' => 'json' ]; $params_sort = $params['sort'] ?? 'date'; // ソートの正規化 if (!in_array($params_sort, ['release','-release','date','-date','price','-price','review','-review'], true)) { $params_sort = 'date'; } // 1) service=doujin(floorなし) $c1 = array_merge($base, $params, ['service' => 'doujin', 'sort' => $params_sort]); unset($c1['floor']); $candidates[] = $c1; // 2) service=digital + floor=digital_doujin $c2 = array_merge($base, $params, ['service' => 'digital', 'sort' => $params_sort, 'floor' => 'digital_doujin']); $candidates[] = $c2; // 3) service=doujin + site=DMM.com(環境によって成功する場合あり) $c3 = $c1; $c3['site'] = 'DMM.com'; $candidates[] = $c3; foreach ($candidates as $i => $req) { $url = $this->api_url . '?' . http_build_query($req); $resp = $this->callAPI($url); if ($resp && isset($resp['result']) && !empty($resp['result']['items'])) { set_cache($cache_key, $resp['result']); return $resp['result']; } } // 失敗時は空 debug_log('[FANZA API] 同人商品検索API: 全候補で0件/失敗 - params: ' . json_encode($params)); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } // 同人商品サンプルデータ生成 private function getSampleDoujinItems($params) { $kw = trim($params['keyword'] ?? ''); $sample_items = [ [ 'content_id' => 'd_doujin001', 'title' => 'サンプル同人作品 1 - エロ漫画集 NTR 巨乳', 'imageURL' => [ 'small' => 'https://via.placeholder.com/200x280/ff9800/ffffff?text=Sample+1', 'large' => 'https://via.placeholder.com/400x560/ff9800/ffffff?text=Sample+1' ], 'affiliateURL' => '#', 'date' => date('Y-m-d'), 'volume' => '30ページ', 'comment' => 'これはサンプル同人作品の説明文です。実際のFANZA APIが動作すると、本物の商品データが表示されます。', 'iteminfo' => [ 'genre' => [['name' => 'NTR', 'id' => '1'], ['name' => 'おねショタ', 'id' => '2']], 'maker' => [['name' => 'サンプルサークル', 'id' => '1']], 'author' => [['name' => 'サンプル作者', 'id' => '1']] ], 'prices' => ['price' => 550] ], [ 'content_id' => 'd_doujin002', 'title' => 'サンプル同人作品 2 - CG集 3DCG 制服', 'imageURL' => [ 'small' => 'https://via.placeholder.com/200x280/e65100/ffffff?text=Sample+2', 'large' => 'https://via.placeholder.com/400x560/e65100/ffffff?text=Sample+2' ], 'affiliateURL' => '#', 'date' => date('Y-m-d', strtotime('-1 day')), 'volume' => '50枚', 'comment' => '高品質なCG集のサンプルです。FANZA APIが正常動作すると実際の商品が表示されます。', 'iteminfo' => [ 'genre' => [['name' => '快楽堕ち', 'id' => '3'], ['name' => '3DCG', 'id' => '4']], 'maker' => [['name' => 'テストサークル', 'id' => '2']], 'author' => [['name' => 'テスト作者', 'id' => '2']] ], 'prices' => ['price' => 770] ] ]; // キーワード簡易フィルタ(タイトル・ジャンル・サークル・作者) if ($kw !== '') { $filtered = array_filter($sample_items, function($it) use ($kw){ if (stripos($it['title'] ?? '', $kw) !== false) return true; foreach (['genre','maker','author'] as $k) { if (!empty($it['iteminfo'][$k]) && is_array($it['iteminfo'][$k])) { foreach ($it['iteminfo'][$k] as $e) { if (!empty($e['name']) && stripos($e['name'], $kw) !== false) return true; } } } return false; }); if (!empty($filtered)) $sample_items = array_values($filtered); } return [ 'result_count' => count($sample_items), 'total_count' => count($sample_items), 'first_position' => 1, 'items' => $sample_items ]; } // APIキーが設定されているかチェック private function isAPIKeySet() { return !empty($this->api_id) && !empty($this->affiliate_id); } // 商品検索 public function searchItems($params = [], $page = 1, $hits = 30) { if (is_string($params)) { $params = ['keyword' => $params]; } if ($page > 1) { $params['offset'] = (($page - 1) * $hits) + 1; } $params['hits'] = min(100, max(1, (int)$hits)); if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキー未設定: search items は空結果'); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } $cache_key = 'fanza_search_' . md5(json_encode($params) . '_' . $page . '_' . $hits); $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $sort = $params['sort'] ?? 'date'; switch ($sort) { case 'rank': case 'popular': $sort = 'date'; break; } // start は使わず offset に統一 unset($params['start']); $base = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'hits' => min(100, max(1, (int)$hits)), 'sort' => $sort, 'output' => 'json' ]; // 複数候補を試す $candidates = []; // FANZA / videoa $candidates[] = array_merge($base, $params, [ 'site' => 'FANZA', 'service' => 'digital', 'floor' => 'videoa' ]); // DMM.com / videoa $candidates[] = array_merge($base, $params, [ 'site' => 'DMM.com', 'service' => 'digital', 'floor' => 'videoa' ]); // FANZA / video $candidates[] = array_merge($base, $params, [ 'site' => 'FANZA', 'service' => 'digital', 'floor' => 'video' ]); // DMM.com / video $candidates[] = array_merge($base, $params, [ 'site' => 'DMM.com', 'service' => 'digital', 'floor' => 'video' ]); foreach ($candidates as $req) { $url = $this->api_url . '?' . http_build_query($req); $response = $this->callAPI($url); if ($response && isset($response['result']) && !empty($response['result']['items'])) { if (!empty($params['hybrid'])) { $response['result'] = $this->applyHybridFiltering($response['result'], $params['hits'] ?? 30); } set_cache($cache_key, $response['result']); return $response['result']; } } error_log('[FANZA API] 商品検索API呼び出しに失敗、空結果 - params: ' . json_encode($params, JSON_UNESCAPED_UNICODE)); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } // 通販(物販)検索(FANZA mono) public function searchTsuhanItems($params = [], $page = 1, $hits = 30) { // 文字列が来たらキーワードとして扱う if (is_string($params)) { $params = ['keyword' => $params]; } if ($page > 1) { $params['offset'] = ($page - 1) * $hits; } $params['hits'] = min(100, max(1, (int)$hits)); if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキー未設定: tsuhan は空結果'); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } $cache_key = 'fanza_tsuhan_' . md5(json_encode($params) . '_' . $page . '_' . $hits); $cached = get_cache($cache_key); if ($cached !== false) return $cached; // デフォルトパラメータ(mono=物販) $default = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'service' => 'mono', 'hits' => min(100, max(1, (int)$hits)), 'sort' => 'date', 'output' => 'json' ]; $request = array_merge($default, $params); // ソートパラメータ調整 if (isset($request['sort'])) { switch ($request['sort']) { case 'release': $request['sort'] = '-date'; break; case 'date': default: $request['sort'] = 'date'; break; } } $url = $this->api_url . '?' . http_build_query($request); $resp = $this->callAPI($url); if ($resp && isset($resp['result']) && !empty($resp['result']['items'])) { set_cache($cache_key, $resp['result'], 1800); return $resp['result']; } // Fallback: DMM.com サイトで再試行 $request['site'] = 'DMM.com'; $url2 = $this->api_url . '?' . http_build_query($request); $resp2 = $this->callAPI($url2); if ($resp2 && isset($resp2['result']) && !empty($resp2['result']['items'])) { set_cache($cache_key, $resp2['result'], 1800); return $resp2['result']; } debug_log('[FANZA API] 通販API失敗、空結果'); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } // 月額(見放題)検索(FANZA monthly) public function searchMonthlyItems($params = [], $page = 1, $hits = 30) { if (is_string($params)) { $params = ['keyword' => $params]; } if ($page > 1) { $params['offset'] = ($page - 1) * $hits; } $params['hits'] = min(100, max(1, (int)$hits)); if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキー未設定: monthly は空結果'); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } $cache_key = 'fanza_monthly_' . md5(json_encode($params) . '_' . $page . '_' . $hits); $cached = get_cache($cache_key); if ($cached !== false) return $cached; $default = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'service' => 'monthly', 'hits' => min(100, max(1, (int)$hits)), 'sort' => 'date', 'output' => 'json' ]; $request = array_merge($default, $params); // ソート調整 if (isset($request['sort'])) { switch ($request['sort']) { case 'release': $request['sort'] = '-date'; break; case 'date': default: $request['sort'] = 'date'; break; } } $url = $this->api_url . '?' . http_build_query($request); $resp = $this->callAPI($url); if ($resp && isset($resp['result']) && !empty($resp['result']['items'])) { set_cache($cache_key, $resp['result']); return $resp['result']; } // Fallback DMM.com $request['site'] = 'DMM.com'; $url2 = $this->api_url . '?' . http_build_query($request); $resp2 = $this->callAPI($url2); if ($resp2 && isset($resp2['result']) && !empty($resp2['result']['items'])) { set_cache($cache_key, $resp2['result']); return $resp2['result']; } debug_log('[FANZA API] 月額API失敗、空結果'); return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } // 通販サンプルデータ private function getSampleTsuhanItems($params = []) { $items = []; for ($i=1; $i<=12; $i++) { $items[] = [ 'content_id' => 't_sample'.str_pad($i,2,'0',STR_PAD_LEFT), 'title' => '通販サンプル商品 '.$i, 'date' => date('Y-m-d', strtotime('-'.rand(0,10).' days')), 'imageURL' => [ 'small' => 'https://via.placeholder.com/240x240/ffb74d/ffffff?text=T'.$i, 'large' => 'https://via.placeholder.com/600x600/ff9800/ffffff?text=T'.$i ], 'affiliateURL' => '#', 'iteminfo' => [ 'maker' => [['name' => 'サンプルストア']], ], 'prices' => ['price' => rand(1000,5000)] ]; } return [ 'items' => $items, 'total_count' => count($items), 'first_position' => 1 ]; } /** * ハイブリッドフィルタリング * 動画ありを優先、少ない場合は画像のみも追加 */ private function applyHybridFiltering($result, $requested_hits = 30) { if (!isset($result['items']) || empty($result['items'])) { return $result; } $items = $result['items']; // 動画あり・なしで分類 $items_with_video = []; $items_without_video = []; foreach ($items as $item) { if (!empty($item['sampleMovieURL']) && is_array($item['sampleMovieURL'])) { // size_で始まるキーがあるかチェック $has_movie = false; foreach ($item['sampleMovieURL'] as $key => $value) { if (strpos($key, 'size_') === 0 && !empty($value)) { $has_movie = true; break; } } if ($has_movie) { $items_with_video[] = $item; } else { $items_without_video[] = $item; } } else { $items_without_video[] = $item; } } // ハイブリッド処理 $final_items = $items_with_video; // 動画ありが要求数の半分未満の場合、画像のみも追加 if (count($items_with_video) < $requested_hits / 2) { $needed = $requested_hits - count($items_with_video); $additional = array_slice($items_without_video, 0, $needed); $final_items = array_merge($final_items, $additional); } // 要求数に調整 $final_items = array_slice($final_items, 0, $requested_hits); // 結果を更新 $result['items'] = $final_items; $result['total_count_with_video'] = count($items_with_video); $result['total_count_without_video'] = count($items_without_video); return $result; } // 商品詳細取得 public function getItemDetail($content_id, $service = 'digital') { if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキー未設定'); return null; } $content_id = trim((string)$content_id); if ($content_id === '') { return null; } $is_doujin = (strpos($content_id, 'd_') === 0); if ($is_doujin) { $patterns = [ ['site' => 'FANZA', 'service' => 'doujin'], ['site' => 'DMM.com', 'service' => 'doujin'], ]; } else { // h_ / n_ もまずは動画として試す $patterns = [ ['site' => 'FANZA', 'service' => 'digital', 'floor' => 'videoa'], ['site' => 'DMM.com', 'service' => 'digital', 'floor' => 'videoa'], ['site' => 'FANZA', 'service' => 'digital', 'floor' => 'video'], ['site' => 'DMM.com', 'service' => 'digital', 'floor' => 'video'], ['site' => 'FANZA', 'service' => 'digital', 'floor' => 'amateur'], ['site' => 'FANZA', 'service' => 'mono'], ['site' => 'DMM.com', 'service' => 'mono'], ]; } foreach ($patterns as $p) { // ① まず cid 直指定で試す $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => $p['site'], 'service' => $p['service'], 'cid' => $content_id, 'hits' => 10, 'output' => 'json', ]; if (!empty($p['floor'])) { $params['floor'] = $p['floor']; } $url = 'https://api.dmm.com/affiliate/v3/ItemList?' . http_build_query($params); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_USERAGENT => 'Mozilla/5.0', ]); $json = curl_exec($ch); $errno = curl_errno($ch); $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (!$errno && $httpCode === 200 && $json) { $data = json_decode($json, true); if (is_array($data) && !empty($data['result']['items']) && is_array($data['result']['items'])) { foreach ($data['result']['items'] as $candidate) { $candidate_id = (string)($candidate['content_id'] ?? ''); if (strcasecmp($candidate_id, $content_id) === 0) { return $candidate; } } } } // ② cid で取れなければ keyword 検索で exact 一致を拾う // ただし mono / doujin は keyword 検索しない if ($is_mono || $is_doujin) { continue; } $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => $p['site'], 'service' => $p['service'], 'keyword' => $content_id, 'hits' => 20, 'output' => 'json', ]; if (!empty($p['floor'])) { $params['floor'] = $p['floor']; } $url = 'https://api.dmm.com/affiliate/v3/ItemList?' . http_build_query($params); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_USERAGENT => 'Mozilla/5.0', ]); $json = curl_exec($ch); $errno = curl_errno($ch); $httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($errno || $httpCode !== 200 || !$json) { continue; } $data = json_decode($json, true); if (!is_array($data) || empty($data['result']['items']) || !is_array($data['result']['items'])) { continue; } foreach ($data['result']['items'] as $candidate) { $candidate_id = (string)($candidate['content_id'] ?? ''); $product_id = (string)($candidate['product_id'] ?? ''); if ( strcasecmp($candidate_id, $content_id) === 0 || strcasecmp($product_id, $content_id) === 0 ) { return $candidate; } } } return null; } // フロア一覧取得 public function getFloors() { // APIキーが設定されていない場合はサンプルデータを返す if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキーが未設定のためフロアサンプルデータを返します'); return $this->getSampleFloors(); } $cache_key = 'fanza_floors'; $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $floor_url = 'https://api.dmm.com/affiliate/v3/FloorList'; $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'output' => 'json' ]; $url = $floor_url . '?' . http_build_query($params); $response = $this->callAPI($url); if ($response && isset($response['result'])) { set_cache($cache_key, $response['result'], 86400); // 24時間キャッシュ return $response['result']; } // API呼び出しが失敗した場合もサンプルデータを返す debug_log('[FANZA API] Floor API呼び出しに失敗、サンプルデータを返します'); return $this->getSampleFloors(); } // ジャンル一覧取得 public function getGenres() { $cache_key = 'fanza_genres'; $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $genre_url = 'https://api.dmm.com/affiliate/v3/GenreSearch'; $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', 'output' => 'json' ]; $url = $genre_url . '?' . http_build_query($params); $response = $this->callAPI($url); if ($response && isset($response['result'])) { set_cache($cache_key, $response['result'], 86400); // 24時間キャッシュ return $response['result']; } return []; } // 女優一覧取得 public function getActresses($params = []) { // APIキーが設定されていない場合はサンプルデータを返す if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキーが未設定のため女優サンプルデータを返します'); return $this->getSampleActresses(); } $cache_key = 'fanza_actresses_' . md5(json_encode($params)); $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $actress_url = 'https://api.dmm.com/affiliate/v3/ActressSearch'; $default_params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'hits' => 100, 'output' => 'json' ]; $request_params = array_merge($default_params, $params); $url = $actress_url . '?' . http_build_query($request_params); $response = $this->callAPI($url); if ($response && isset($response['result'])) { set_cache($cache_key, $response['result'], 86400); // 24時間キャッシュ return $response['result']; } // 失敗時は空 debug_log('[FANZA API] 女優API呼び出しに失敗、空を返します'); return []; } // メーカー一覧取得 public function getMakers($params = []) { // APIキーが設定されていない場合はサンプルデータを返す if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキーが未設定のためメーカーサンプルデータを返します'); return $this->getSampleMakers(); } $cache_key = 'fanza_makers_' . md5(json_encode($params)); $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $maker_url = 'https://api.dmm.com/affiliate/v3/MakerSearch'; $default_params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', 'hits' => 500, 'output' => 'json' ]; $request_params = array_merge($default_params, $params); $url = $maker_url . '?' . http_build_query($request_params); $response = $this->callAPI($url); if ($response && isset($response['result'])) { set_cache($cache_key, $response['result'], 86400); // 24時間キャッシュ return $response['result']; } // 失敗時は空 debug_log('[FANZA API] メーカーAPI呼び出しに失敗、空を返します'); return []; } // API呼び出し共通処理(file_get_contents版 - cURL不要) private function callAPI($url) { $attempts = 0; $response = false; $http = 0; do { $attempts++; // 1) cURLが使えるなら優先(ホスティング環境の制限回避) if (function_exists('curl_init')) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Charset: UTF-8' ]); $response = curl_exec($ch); $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($response === false || $http >= 400) { // HTTPエラーの可視化(調査用) if ($http >= 400) { debug_log('[FANZA API] HTTP error ' . $http . ' - URL: ' . $url); } else { error_log('[FANZA API] cURL error - no response - URL: ' . $url); } $response = false; } } else { // 2) cURLが無ければ file_get_contents $context = stream_context_create([ 'http' => [ 'timeout' => 30, 'method' => 'GET', 'header' => [ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept-Charset: UTF-8' ] ], 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false ] ]); $response = @file_get_contents($url, false, $context); } if ($response === false && $attempts < 2) { usleep(200000); } } while ($response === false && $attempts < 2); if ($response !== false && is_string($response)) { // BOMを除去(もし存在する場合) $response = preg_replace('/^\xEF\xBB\xBF/', '', $response); $data = json_decode($response, true); if (json_last_error() === JSON_ERROR_NONE) { // エラーレスポンスかチェック if (isset($data['result']['status']) && $data['result']['status'] == 400) { derror_log('[FANZA API] API Error 400: ' . json_encode($data['result'], JSON_UNESCAPED_UNICODE)); return null; } // 正常レスポンスの場合 // debug_log('[FANZA API] Success - URL: ' . $url); if (isset($data['result'])) { // debug_log('[FANZA API] Result keys: ' . implode(', ', array_keys($data['result']))); } // キャッシュ処理なし(searchItemsで処理) return $data; } else { // error_log('[FANZA API] JSON Parse Error: ' . json_last_error_msg()); // debug_log('[FANZA API] Response (first 500 chars): ' . substr($response, 0, 500)); } } else { // HTTP エラーの詳細(file_get_contents系) $error = error_get_last(); if (!empty($error['message'])) { error_log('[FANZA API] HTTP request error - ' . $error['message'] . ' - URL: ' . $url); } else { debug_log('[FANZA API] HTTP request error (unknown) - URL: ' . $url); } // debug_log('[FANZA API] API Error - URL: ' . $url); // debug_log('[FANZA API] HTTP Error: ' . ($error['message'] ?? 'Unknown error')); } return null; } // 新着商品取得 public function getNewItems($page = 1, $hits = 30) { return $this->searchItems([ 'sort' => 'date', 'hits' => $hits, 'start' => ($page - 1) * $hits ]); } // 人気商品取得 public function getPopularItems($page = 1, $hits = 30) { return $this->searchItems([ 'sort' => 'rank', 'hits' => $hits, 'start' => ($page - 1) * $hits ]); } // 期間別人気商品取得 public function getPopularItemsByPeriod($period = 'monthly', $page = 1, $hits = 30) { if (!$this->isAPIKeySet()) { return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } // キャッシュキー $cache_key = 'fanza_popular_' . $period . '_' . $page . '_' . $hits; $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } // APIパラメータ $url = 'https://api.dmm.com/affiliate/v3/ItemList'; $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'service' => 'digital', 'floor' => 'videoa', 'hits' => $hits, 'sort' => 'rank', 'output' => 'json' ]; // 1ページ目以外の場合のみoffsetを設定 if ($page > 1) { $params['offset'] = ($page - 1) * $hits; } // 期間別ランキングのargumentパラメータを追加 switch ($period) { case 'daily': $params['argument'] = 'daily'; break; case 'weekly': $params['argument'] = 'weekly'; break; case 'monthly': $params['argument'] = 'monthly'; break; case 'yearly': $params['argument'] = 'yearly'; break; case 'all': // 全期間の場合はargumentを指定しない break; default: $params['argument'] = 'monthly'; break; } $request_url = $url . '?' . http_build_query($params); // デバッグログ // debug_log('[FANZA API] Popular items request - Period: ' . $period . ', URL: ' . $request_url); $response = $this->callAPI($request_url); if ($response && isset($response['result'])) { set_cache($cache_key, $response['result'], 3600); // 1時間キャッシュ return $response['result']; } // フォールバックしない return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } /** * 汎用: サービス別の期間人気商品取得 * @param string $service 'digital' | 'doujin' | 'mono' * @param string $period 'daily' | 'weekly' | 'monthly' | 'yearly' | 'all' * @param int $page * @param int $hits * @param array $extra 追加のAPIパラメータ(例: floor) */ public function getPopularItemsByPeriodGeneric($service = 'digital', $period = 'monthly', $page = 1, $hits = 30, $extra = []) { if (!$this->isAPIKeySet()) { return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } $cache_key = 'fanza_popular_generic_' . $service . '_' . $period . '_' . $page . '_' . $hits . '_' . md5(json_encode($extra)); $cached = get_cache($cache_key); if ($cached !== false) return $cached; $url = 'https://api.dmm.com/affiliate/v3/ItemList'; $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'service' => $service, 'hits' => min(100, max(1, (int)$hits)), 'sort' => 'rank', 'output' => 'json' ]; // 同人は digital+digital_doujin を第一候補に $isDoujin = ($service === 'doujin'); if ($isDoujin) { $params['service'] = 'digital'; if (empty($extra['floor'])) $extra['floor'] = 'digital_doujin'; } // 追加パラメータ(floorなど) if (is_array($extra)) { foreach ($extra as $k => $v) { if ($v !== '' && $v !== null) $params[$k] = $v; } } if ($page > 1) { $params['offset'] = ($page - 1) * $hits; } switch ($period) { case 'daily': $params['argument'] = 'daily'; break; case 'weekly': $params['argument'] = 'weekly'; break; case 'monthly': $params['argument'] = 'monthly'; break; case 'yearly': $params['argument'] = 'yearly'; break; case 'all': /* 全期間: argumentなし */ break; default: $params['argument'] = 'monthly'; break; } $request_url = $url . '?' . http_build_query($params); $resp = $this->callAPI($request_url); if ($resp && isset($resp['result']) && !empty($resp['result']['items'])) { set_cache($cache_key, $resp['result'], 3600); return $resp['result']; } // フォールバック: 同人は service=doujin でも試す if ($isDoujin) { $params_fb = $params; unset($params_fb['floor']); $params_fb['service'] = 'doujin'; $request_url_fb = $url . '?' . http_build_query($params_fb); $resp_fb = $this->callAPI($request_url_fb); if ($resp_fb && isset($resp_fb['result']) && !empty($resp_fb['result']['items'])) { set_cache($cache_key, $resp_fb['result'], 3600); return $resp_fb['result']; } } // mono の場合、DMM.comにフォールバック if ($service === 'mono') { $params['site'] = 'DMM.com'; $request_url2 = $url . '?' . http_build_query($params); $resp2 = $this->callAPI($request_url2); if ($resp2 && isset($resp2['result']) && !empty($resp2['result']['items'])) { set_cache($cache_key, $resp2['result'], 3600); return $resp2['result']; } } // フォールバックしない return ['items'=>[], 'total_count'=>0, 'first_position'=>1]; } // 評価順商品取得 public function getHighRatedItems($page = 1, $hits = 30) { return $this->searchItems([ 'sort' => 'review', 'hits' => $hits, 'start' => ($page - 1) * $hits ]); } // キーワード検索 public function searchByKeyword($keyword, $page = 1, $hits = 30, $sort = 'date') { // FANZA APIのソートパラメータをそのまま使用 return $this->searchItems([ 'keyword' => $keyword, 'hits' => $hits, 'sort' => $sort ], $page, $hits); } // ジャンル検索 public function searchByGenre($genre_id, $page = 1, $hits = 30, $sort = 'date') { return $this->searchItems([ 'genre_id' => $genre_id, 'hits' => $hits, 'sort' => $sort ], $page, $hits); } // 女優検索 public function searchByActress($actress_id, $page = 1, $hits = 30, $sort = 'date') { return $this->searchItems([ 'actress_id' => $actress_id, 'hits' => $hits, 'sort' => $sort ], $page, $hits); } // メーカー検索 public function searchByMaker($maker_id, $page = 1, $hits = 30, $sort = 'date') { return $this->searchItems([ 'maker_id' => $maker_id, 'hits' => $hits, 'sort' => $sort ], $page, $hits); } // シリーズ検索 public function searchBySeries($series_id, $page = 1, $hits = 30, $sort = 'date') { return $this->searchItems([ 'series_id' => $series_id, 'hits' => $hits, 'sort' => $sort ], $page, $hits); } // レーベル検索 public function searchByLabel($label_id, $page = 1, $hits = 30) { return $this->searchItems([ 'label_id' => $label_id, 'hits' => $hits, 'start' => ($page - 1) * $hits ]); } // 人気キーワード取得 public function getPopularKeywords($period = 'daily') { $cache_key = 'fanza_keywords_' . $period; $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } // キーワードランキングは商品検索から頻出キーワードを抽出 // FANZA APIには直接的なキーワードランキングAPIがないため // 期間パラメータ $period_map = [ 'daily' => 'day', 'weekly' => 'week', 'monthly' => 'month', 'yearly' => 'year', 'all' => 'all' ]; // 期間別のダミーキーワードデータを返す $keywords_data = [ 'daily' => [ ['keyword' => '巨乳 OL', 'count' => 1523, 'point' => 2890], ['keyword' => '人妻 不倫', 'count' => 1422, 'point' => 2688], ['keyword' => '素人 ナンパ', 'count' => 1390, 'point' => 2488], ['keyword' => '制服 JK', 'count' => 1288, 'point' => 2366], ['keyword' => '中出し 素人', 'count' => 1188, 'point' => 2188] ], 'weekly' => [ ['keyword' => '熟女 中出し', 'count' => 8522, 'point' => 15688], ['keyword' => 'NTR 人妻', 'count' => 7990, 'point' => 14488], ['keyword' => '痴漢 電車', 'count' => 7588, 'point' => 13988], ['keyword' => 'レイプ 輪姦', 'count' => 6988, 'point' => 12788], ['keyword' => 'OL 巨乳', 'count' => 6788, 'point' => 12188] ], 'monthly' => [ ['keyword' => '素人 個人撮影', 'count' => 35220, 'point' => 68880], ['keyword' => '人妻 寝取られ', 'count' => 32990, 'point' => 64488], ['keyword' => 'JK 制服', 'count' => 30588, 'point' => 59988], ['keyword' => '巨乳 パイズリ', 'count' => 28988, 'point' => 56788], ['keyword' => 'ギャル 中出し', 'count' => 26788, 'point' => 52188] ], 'yearly' => [ ['keyword' => '熟女', 'count' => 422220, 'point' => 826880], ['keyword' => '巨乳', 'count' => 392990, 'point' => 784488], ['keyword' => '人妻', 'count' => 365880, 'point' => 719988], ['keyword' => '素人', 'count' => 348980, 'point' => 676788], ['keyword' => '中出し', 'count' => 326780, 'point' => 632188] ], 'all' => [ ['keyword' => '巨乳', 'count' => 1822220, 'point' => 3626880], ['keyword' => '熟女', 'count' => 1692990, 'point' => 3384488], ['keyword' => '素人', 'count' => 1565880, 'point' => 3119988], ['keyword' => '人妻', 'count' => 1448980, 'point' => 2876788], ['keyword' => '中出し', 'count' => 1326780, 'point' => 2632188] ] ]; $keywords = $keywords_data[$period] ?? $keywords_data['daily']; // より多くのキーワードを追加 $additional_keywords = [ ['keyword' => 'コスプレ', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => '美少女', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => 'アナル', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => 'レズ', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => '3P', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => 'SM', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => '乱交', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => 'フェラ', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => '潮吹き', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)], ['keyword' => '露出', 'count' => rand(500, 1000), 'point' => rand(1000, 2000)] ]; foreach ($additional_keywords as $kw) { $keywords[] = $kw; } // ポイント順でソート usort($keywords, function($a, $b) { return $b['point'] - $a['point']; }); // 上位30件を返す $keywords = array_slice($keywords, 0, 30); set_cache($cache_key, $keywords, 3600); // 1時間キャッシュ return $keywords; } /** * 女優一覧取得 */ public function getActressList($page = 1, $limit = 30) { $cache_key = 'fanza_actresses_list_' . $page . '_' . $limit; $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'hits' => $limit, 'output' => 'json' ]; $url = 'https://api.dmm.com/affiliate/v3/ActressSearch?' . http_build_query($params); $response = $this->callAPI($url); if ($response) { set_cache($cache_key, $response, 3600); // 1時間キャッシュ } return $response; } /** * 女優検索(頭文字) */ public function searchActressByInitial($initial, $page = 1, $limit = 30) { $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'initial' => $initial, 'hits' => $limit, 'output' => 'json' ]; $url = 'https://api.dmm.com/affiliate/v3/ActressSearch?' . http_build_query($params); $response = $this->callAPI($url); return $response; } /** * ジャンル一覧取得 */ public function getGenreList() { if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキー未設定: getGenreList は空'); return []; } $cache_key = 'fanza_all_genres'; $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', 'hits' => '500', // 最大数を取得 'output' => 'json' ]; $url = 'https://api.dmm.com/affiliate/v3/GenreSearch?' . http_build_query($params); // API呼び出しの試行 $result = $this->callAPI($url); // API呼び出しが失敗した場合もサンプルデータを返す if (!$result) { debug_log('[FANZA API] Genre API呼び出しに失敗、サンプルデータを返します'); return $this->getSampleGenres(); } if ($result && isset($result['result']['genre'])) { $genres = $result['result']['genre']; // ひらがな順にソート usort($genres, function($a, $b) { $a_ruby = $a['ruby'] ?? $a['name']; $b_ruby = $b['ruby'] ?? $b['name']; return strcmp($a_ruby, $b_ruby); }); set_cache($cache_key, $genres, 86400); // 24時間キャッシュ return $genres; } return []; } /** * 女優ランキング取得 * FANZA APIでは商品の人気順から女優を抽出する */ public function getActressRanking($period = 'all', $limit = 50) { $cache_key = 'fanza_actress_ranking_' . $period . '_' . $limit; $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } // 期間別の人気商品を取得 $sort = 'rank'; $hits = 100; // より多くの商品から女優を抽出 $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'service' => 'digital', 'floor' => 'videoa', 'hits' => $hits, 'sort' => $sort, 'output' => 'json' ]; // 期間に応じたパラメータを追加 switch ($period) { case 'daily': $params['gte_date'] = date('Y-m-d\T00:00:00', strtotime('-1 day')); break; case 'weekly': $params['gte_date'] = date('Y-m-d\T00:00:00', strtotime('-7 days')); break; case 'monthly': $params['gte_date'] = date('Y-m-d\T00:00:00', strtotime('-30 days')); break; case 'yearly': $params['gte_date'] = date('Y-m-d\T00:00:00', strtotime('-365 days')); break; case 'all': default: // 全期間は特に期間指定なし break; } $url = $this->api_url . '?' . http_build_query($params); $response = $this->callAPI($url); if ($response && isset($response['result']['items'])) { // 商品から女優を抽出してランキング化 $actress_count = []; foreach ($response['result']['items'] as $item) { if (isset($item['iteminfo']['actress']) && is_array($item['iteminfo']['actress'])) { foreach ($item['iteminfo']['actress'] as $actress) { $actress_id = $actress['id']; if (!isset($actress_count[$actress_id])) { $actress_count[$actress_id] = [ 'actress' => $actress, 'count' => 0 ]; } $actress_count[$actress_id]['count']++; } } } // 出演作品数でソート usort($actress_count, function($a, $b) { return $b['count'] - $a['count']; }); // 上位のみ返す $ranking = array_slice($actress_count, 0, $limit); set_cache($cache_key, $ranking, 3600); // 1時間キャッシュ return $ranking; } return []; } /** * シリーズ一覧取得 */ public function getSeriesList($params = []) { // APIキーが設定されていない場合はサンプルデータを返す if (!$this->isAPIKeySet()) { debug_log('[FANZA API] APIキーが未設定のためシリーズサンプルデータを返します'); return $this->getSampleSeries()['series']; } $cache_key = 'fanza_series_list_' . md5(json_encode($params)); $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $series_url = 'https://api.dmm.com/affiliate/v3/SeriesSearch'; $default_params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', // 動画フロア 'hits' => 100, 'output' => 'json' ]; $request_params = array_merge($default_params, $params); $url = $series_url . '?' . http_build_query($request_params); $response = $this->callAPI($url); if ($response && isset($response['result']['series'])) { $series = $response['result']['series']; set_cache($cache_key, $series, 86400); // 24時間キャッシュ return $series; } // API呼び出しが失敗した場合もサンプルデータを返す debug_log('[FANZA API] シリーズAPI呼び出しに失敗、サンプルデータを返します'); return $this->getSampleSeries()['series']; } /** * シリーズ一覧(ページング情報付き) */ public function getSeriesListPaged($page = 1, $hits = 100) { // APIキーが設定されていない場合はサンプル if (!$this->isAPIKeySet()) { $series = $this->getSampleSeries()['series']; return [ 'series' => array_slice($series, ($page-1)*$hits, $hits), 'total_count' => count($series) ]; } $series_url = 'https://api.dmm.com/affiliate/v3/SeriesSearch'; $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', 'hits' => $hits, 'output' => 'json' ]; if ($page > 1) { $params['offset'] = ($page - 1) * $hits + 1; // DMMは1始まりのことがある } $url = $series_url . '?' . http_build_query($params); $response = $this->callAPI($url); if ($response && isset($response['result']['series'])) { $res = $response['result']; return [ 'series' => $res['series'], 'total_count' => $res['total_count'] ?? count($res['series']) ]; } // フォールバック $series = $this->getSampleSeries()['series']; return [ 'series' => array_slice($series, ($page-1)*$hits, $hits), 'total_count' => count($series) ]; } /** * シリーズ検索(頭文字) */ public function searchSeriesByInitial($initial, $page = 1, $limit = 30) { $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', 'initial' => $initial, 'hits' => $limit, 'output' => 'json' ]; if ($page > 1) { $params['offset'] = ($page - 1) * $limit + 1; } $url = 'https://api.dmm.com/affiliate/v3/SeriesSearch?' . http_build_query($params); $response = $this->callAPI($url); if ($response && isset($response['result'])) { return $response['result']; } return null; } /** * 作者(監督)一覧取得 */ public function getAuthorList($params = []) { $cache_key = 'fanza_author_list_' . md5(json_encode($params)); $cached_data = get_cache($cache_key); if ($cached_data !== false) { return $cached_data; } $author_url = 'https://api.dmm.com/affiliate/v3/AuthorSearch'; $default_params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', // 動画フロア 'hits' => 100, 'output' => 'json' ]; $request_params = array_merge($default_params, $params); $url = $author_url . '?' . http_build_query($request_params); $response = $this->callAPI($url); if ($response && isset($response['result']['author'])) { $authors = $response['result']['author']; set_cache($cache_key, $authors, 86400); // 24時間キャッシュ return $authors; } return []; } /** * 作者(監督)検索(頭文字) */ public function searchAuthorByInitial($initial, $page = 1, $limit = 30) { $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', 'initial' => $initial, 'hits' => $limit, 'output' => 'json' ]; $url = 'https://api.dmm.com/affiliate/v3/AuthorSearch?' . http_build_query($params); $response = $this->callAPI($url); if ($response && isset($response['result'])) { return $response['result']; } return null; } /** * 監督名で作品検索 */ public function searchByDirector($director_name, $page = 1, $hits = 30, $sort = 'date') { $sort_param = '-date'; if ($sort === 'rank') $sort_param = 'rank'; elseif ($sort === 'review') $sort_param = 'review'; elseif ($sort === 'price') $sort_param = 'price'; return $this->searchItems([ 'article' => 'director', 'article_id' => $director_name, 'hits' => $hits, 'sort' => $sort_param ]); } /** * メーカー検索(頭文字) */ public function searchMakerByInitial($initial, $page = 1, $limit = 30) { $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'floor_id' => '43', 'initial' => $initial, 'hits' => $limit, 'output' => 'json' ]; $url = 'https://api.dmm.com/affiliate/v3/MakerSearch?' . http_build_query($params); $response = $this->callAPI($url); if ($response && isset($response['result'])) { return $response['result']; } return null; } /** * メーカー一覧取得(ページング対応) */ public function getMakerList($page = 1, $limit = 30) { $params = [ 'floor_id' => '43', 'hits' => $limit ]; // 1ページ目以外の場合のみoffsetを設定 if ($page > 1) { $params['offset'] = ($page - 1) * $limit; } $result = $this->getMakers($params); return $result; } /** * サンプルジャンルデータ(API失敗時のフォールバック) */ private function getSampleGenres() { return [ ['id' => '1001', 'name' => '巨乳', 'ruby' => 'きょにゅう'], ['id' => '1002', 'name' => '人妻', 'ruby' => 'ひとづま'], ['id' => '1003', 'name' => '熟女', 'ruby' => 'じゅくじょ'], ['id' => '1004', 'name' => '素人', 'ruby' => 'しろうと'], ['id' => '1005', 'name' => '制服', 'ruby' => 'せいふく'], ['id' => '1006', 'name' => 'OL', 'ruby' => 'おーえる'], ['id' => '1007', 'name' => '美少女', 'ruby' => 'びしょうじょ'], ['id' => '1008', 'name' => '学園もの', 'ruby' => 'がくえんもの'], ['id' => '1009', 'name' => '痴漢', 'ruby' => 'ちかん'], ['id' => '1010', 'name' => '3P', 'ruby' => 'すりーぴー'], ['id' => '1011', 'name' => '乱交', 'ruby' => 'らんこう'], ['id' => '1012', 'name' => '企画', 'ruby' => 'きかく'], ['id' => '1013', 'name' => '単体作品', 'ruby' => 'たんたいさくひん'], ['id' => '1014', 'name' => 'メイド', 'ruby' => 'めいど'], ['id' => '1015', 'name' => '看護師', 'ruby' => 'かんごし'], ['id' => '1016', 'name' => '女教師', 'ruby' => 'じょきょうし'], ['id' => '1017', 'name' => 'ナンパ', 'ruby' => 'なんぱ'], ['id' => '1018', 'name' => '中出し', 'ruby' => 'なかだし'], ['id' => '1019', 'name' => 'フェラ', 'ruby' => 'ふぇら'], ['id' => '1020', 'name' => '手コキ', 'ruby' => 'てこき'] ]; } /** * 同人ジャンル一覧(ページング対応) */ public function getDoujinGenreListPaged($page = 1, $hits = 100) { if (!$this->isAPIKeySet()) { return ['genre'=>[], 'total_count'=>0]; } $url = 'https://api.dmm.com/affiliate/v3/GenreSearch'; $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'floor_id' => '81', // digital_doujin 'hits' => $hits, 'output' => 'json' ]; if ($page > 1) { $params['offset'] = ($page - 1) * $hits + 1; } $full = $url . '?' . http_build_query($params); $response = $this->callAPI($full); if ($response && isset($response['result']['genre'])) { $res = $response['result']; return [ 'genre' => $res['genre'], 'total_count' => $res['total_count'] ?? count($res['genre']) ]; } // フォールバック: service=doujin でも試行 $params_fallback = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'service' => 'doujin', 'hits' => $hits, 'output' => 'json' ]; if ($page > 1) { $params_fallback['offset'] = ($page - 1) * $hits + 1; } $full_fb = $url . '?' . http_build_query($params_fallback); $resp_fb = $this->callAPI($full_fb); if ($resp_fb && isset($resp_fb['result']['genre'])) { $res = $resp_fb['result']; return [ 'genre' => $res['genre'], 'total_count' => $res['total_count'] ?? count($res['genre']) ]; } // 失敗時は空 return ['genre'=>[], 'total_count'=>0]; } /** * 同人シリーズ一覧(ページング対応) */ public function getDoujinSeriesListPaged($page = 1, $hits = 60) { if (!$this->isAPIKeySet()) { return ['series'=>[], 'total_count'=>0]; } $url = 'https://api.dmm.com/affiliate/v3/SeriesSearch'; $params = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'floor_id' => '81', // digital_doujin 'hits' => $hits, 'output' => 'json' ]; if ($page > 1) { $params['offset'] = ($page - 1) * $hits + 1; } $full = $url . '?' . http_build_query($params); $response = $this->callAPI($full); if ($response && isset($response['result']['series'])) { $res = $response['result']; return [ 'series' => $res['series'], 'total_count' => $res['total_count'] ?? count($res['series']) ]; } // フォールバック: service=doujin でも試行 $params_fb = [ 'api_id' => $this->api_id, 'affiliate_id' => $this->affiliate_id, 'site' => 'FANZA', 'service' => 'doujin', 'hits' => $hits, 'output' => 'json' ]; if ($page > 1) { $params_fb['offset'] = ($page - 1) * $hits + 1; } $full2 = $url . '?' . http_build_query($params_fb); $resp2 = $this->callAPI($full2); if ($resp2 && isset($resp2['result']['series'])) { $res = $resp2['result']; return [ 'series' => $res['series'], 'total_count' => $res['total_count'] ?? count($res['series']) ]; } // 失敗時は空 return ['series'=>[], 'total_count'=>0]; } /** * サンプル女優データ(API失敗時のフォールバック) */ private function getSampleActresses() { return [ 'actress' => [ ['id' => '2001', 'name' => '山田花子', 'ruby' => 'やまだはなこ'], ['id' => '2002', 'name' => '田中美穂', 'ruby' => 'たなかみほ'], ['id' => '2003', 'name' => '鈴木愛', 'ruby' => 'すずきあい'], ['id' => '2004', 'name' => '佐藤麻衣', 'ruby' => 'さとうまい'], ['id' => '2005', 'name' => '伊藤美咲', 'ruby' => 'いとうみさき'], ['id' => '2006', 'name' => '高橋彩', 'ruby' => 'たかはしあや'], ['id' => '2007', 'name' => '小林真由', 'ruby' => 'こばやしまゆ'], ['id' => '2008', 'name' => '加藤優花', 'ruby' => 'かとうゆか'], ['id' => '2009', 'name' => '吉田奈々', 'ruby' => 'よしだなな'], ['id' => '2010', 'name' => '渡辺みなみ', 'ruby' => 'わたなべみなみ'] ] ]; } /** * サンプルメーカーデータ(API失敗時のフォールバック) */ private function getSampleMakers() { return [ 'maker' => [ ['id' => '3001', 'name' => 'サンプルメーカー1', 'ruby' => 'さんぷるめーかーいち'], ['id' => '3002', 'name' => 'サンプルメーカー2', 'ruby' => 'さんぷるめーかーに'], ['id' => '3003', 'name' => 'サンプルメーカー3', 'ruby' => 'さんぷるめーかーさん'], ['id' => '3004', 'name' => 'サンプルメーカー4', 'ruby' => 'さんぷるめーかーよん'], ['id' => '3005', 'name' => 'サンプルメーカー5', 'ruby' => 'さんぷるめーかーご'] ] ]; } /** * サンプルシリーズデータ(API失敗時のフォールバック) */ private function getSampleSeries() { return [ 'series' => [ ['id' => '4001', 'name' => 'サンプルシリーズ1', 'ruby' => 'さんぷるしりーずいち'], ['id' => '4002', 'name' => 'サンプルシリーズ2', 'ruby' => 'さんぷるしりーずに'], ['id' => '4003', 'name' => 'サンプルシリーズ3', 'ruby' => 'さんぷるしりーずさん'], ['id' => '4004', 'name' => 'サンプルシリーズ4', 'ruby' => 'さんぷるしりーずよん'], ['id' => '4005', 'name' => 'サンプルシリーズ5', 'ruby' => 'さんぷるしりーずご'] ] ]; } /** * サンプルフロアデータ(API失敗時のフォールバック) */ private function getSampleFloors() { return [ 'site' => [ [ 'name' => 'FANZA', 'code' => 'FANZA', 'service' => [ [ 'name' => 'デジタル', 'code' => 'digital', 'floor' => [ ['name' => '動画', 'code' => 'videoa'], ['name' => '電子書籍', 'code' => 'book'], ['name' => '同人', 'code' => 'doujin'], ['name' => 'PCゲーム', 'code' => 'pcgame'] ] ] ] ] ] ]; } /** * サンプル商品データ(API失敗時のフォールバック) */ private function getSampleItems($params = []) { $keyword = $params['keyword'] ?? ''; $hits = $params['hits'] ?? 30; // サンプル商品データ $sample_items = [ [ 'content_id' => 'sample001', 'title' => 'サンプル動画1:美しい女優による素晴らしい作品(巨乳 制服)', 'date' => date('Y-m-d'), 'imageURL' => [ 'large' => 'https://pics.dmm.co.jp/digital/video/sample001/sample001pl.jpg', 'list' => 'https://pics.dmm.co.jp/digital/video/sample001/sample001pt.jpg' ], 'sampleMovieURL' => [ 'size_720_480' => 'https://cc3001.dmm.co.jp/litevideo/freepv/sample001_dmb_w.mp4' ], 'URL' => 'https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=sample001/', 'price' => '300', 'actress' => [ ['id' => '1001', 'name' => 'サンプル女優1'] ] ], [ 'content_id' => 'sample002', 'title' => 'サンプル動画2:話題の新作アダルトビデオ(人妻 NTR)', 'date' => date('Y-m-d', strtotime('-1 day')), 'imageURL' => [ 'large' => 'https://pics.dmm.co.jp/digital/video/sample002/sample002pl.jpg', 'list' => 'https://pics.dmm.co.jp/digital/video/sample002/sample002pt.jpg' ], 'sampleMovieURL' => [ 'size_720_480' => 'https://cc3001.dmm.co.jp/litevideo/freepv/sample002_dmb_w.mp4' ], 'URL' => 'https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=sample002/', 'price' => '250', 'actress' => [ ['id' => '1002', 'name' => 'サンプル女優2'] ] ], [ 'content_id' => 'sample003', 'title' => 'サンプル動画3:人気シリーズの最新作(3DCG 快楽堕ち)', 'date' => date('Y-m-d', strtotime('-2 days')), 'imageURL' => [ 'large' => 'https://pics.dmm.co.jp/digital/video/sample003/sample003pl.jpg', 'list' => 'https://pics.dmm.co.jp/digital/video/sample003/sample003pt.jpg' ], 'sampleMovieURL' => [], 'URL' => 'https://www.dmm.co.jp/digital/videoa/-/detail/=/cid=sample003/', 'price' => '400', 'actress' => [ ['id' => '1003', 'name' => 'サンプル女優3'] ] ] ]; // キーワード検索の場合はフィルタリング if (!empty($keyword)) { $filtered = array_filter($sample_items, function($item) use ($keyword) { return stripos($item['title'], $keyword) !== false; }); if (!empty($filtered)) { $sample_items = array_values($filtered); } } // 要求数に応じて調整 $sample_items = array_slice($sample_items, 0, $hits); return [ 'items' => array_values($sample_items), 'total_count' => count($sample_items), 'first_position' => 1 ]; } /** * サンプル商品詳細データ(API失敗時のフォールバック) */ private function getSampleItemDetail($content_id) { return [ 'content_id' => $content_id, 'product_id' => $content_id, 'title' => 'サンプル動画タイトル ' . $content_id, 'URL' => '#', 'affiliateURL' => '#', 'imageURL' => [ 'small' => 'https://via.placeholder.com/120x90?text=Sample', 'medium' => 'https://via.placeholder.com/200x150?text=Sample', 'large' => 'https://via.placeholder.com/300x225?text=Sample' ], 'sampleMovieURL' => [ 'size_560_360' => 'https://via.placeholder.com/560x360.mp4?text=Sample+Video' ], 'sampleImageURL' => [ 'sample_s' => [ 'image' => [ 'https://via.placeholder.com/100x75?text=S1', 'https://via.placeholder.com/100x75?text=S2', 'https://via.placeholder.com/100x75?text=S3' ] ] ], 'prices' => [ 'price' => '1500円' ], 'date' => date('Y-m-d H:i:s'), 'iteminfo' => [ 'actress' => [ ['id' => '1001', 'name' => 'サンプル女優', 'ruby' => 'さんぷるじょゆう'] ], 'genre' => [ ['id' => '1001', 'name' => '巨乳'], ['id' => '1002', 'name' => '人妻'] ], 'series' => [ ['id' => '2001', 'name' => 'サンプルシリーズ', 'ruby' => 'さんぷるしりーず'] ], 'maker' => [ ['id' => '3001', 'name' => 'サンプルメーカー', 'ruby' => 'さんぷるめーかー'] ], 'director' => [ ['id' => '4001', 'name' => 'サンプル監督', 'ruby' => 'さんぷるかんとく'] ] ], 'volume' => '120', 'review' => [ 'count' => 25, 'average' => '4.2' ] ]; } } // FANZA API 標準 if (!empty($item['imageURL']) && is_array($item['imageURL'])) { if ($size === 'large' && !empty($item['imageURL']['large'])) { return (string)$item['imageURL']['large']; } if (!empty($item['imageURL']['small'])) { return (string)$item['imageURL']['small']; } if (!empty($item['imageURL']['list'])) { return (string)$item['imageURL']['list']; } } // 直接キー対応 if ($size === 'large' && !empty($item['large_image'])) { return (string)$item['large_image']; } if (!empty($item['small_image'])) { return (string)$item['small_image']; } if (!empty($item['image'])) { return (string)$item['image']; } // DB保存系 if (!empty($item['ogp_image'])) { return (string)$item['ogp_image']; } if (!empty($item['package_image'])) { return (string)$item['package_image']; } return '/images/noimage.jpg'; } } // ============================================ // 互換ラッパー: fanza_search_items() 安全版 // (実装が見つからない場合は空配列を返して落ちないようにする) // ============================================ if (!function_exists('fanza_search_items')) { function fanza_search_items(array $params = []) { // もし将来 FanzaApiClient や他の関数を用意したら、 // ここでそれを呼ぶように書き換えればOK。 // 現時点では何も実装されていないので、 // サイトが落ちないように「空配列」を返しておく。 debug_log('fanza_search_items() fallback: 実装が見つからないため空配列を返しました'); return []; } }