diff --git a/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java b/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java index a64b3e9..2f542d3 100644 --- a/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java +++ b/src/main/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClient.java @@ -32,6 +32,8 @@ public class FacebookPageFeedClient implements SocialFeedClient { /** Facebook가 데이터센터별로 빈 피드를 돌려주는 경우가 있어 공식 로케일 호스트를 순서대로 시도한다. */ private static final List RENDERER_HOSTS = List.of( "www.facebook.com", "ko-kr.facebook.com", "en-gb.facebook.com"); + private static final long READER_RECENT_CACHE_SECONDS = 3_600; + private static final long READER_BASELINE_CACHE_SECONDS = 2_592_000; private final RestTemplate restTemplate; private final ObjectMapper objectMapper; @@ -60,12 +62,14 @@ public class FacebookPageFeedClient implements SocialFeedClient { } } if (readerFallbackEnabled) { - try { - SocialFeedBatch batch = fetchViaReader(channel, false); - if (batch.videos().isEmpty()) batch = fetchViaReader(channel, true); - if (!batch.videos().isEmpty()) return batch; - } catch (IllegalStateException e) { - lastFailure = e; + for (ReaderAttempt attempt : readerAttempts(channel.getLastCollectedAt() == null)) { + try { + SocialFeedBatch batch = fetchViaReader(channel, attempt); + if (!batch.videos().isEmpty()) return batch; + lastFailure = new IllegalStateException("Facebook Reader 공개 피드에 릴이 없습니다."); + } catch (IllegalStateException e) { + lastFailure = e; + } } } throw new IllegalStateException("Facebook 공개 피드를 읽지 못했습니다: " @@ -73,14 +77,15 @@ public class FacebookPageFeedClient implements SocialFeedClient { } /** 운영 서버 IP가 Facebook에서 차단될 때 공개 페이지를 브라우저로 읽어 주는 Reader API를 사용한다. */ - private SocialFeedBatch fetchViaReader(SocialChannel channel, boolean noCache) { - String source = channel.getSourceUrl().replaceAll("/+$", "") + "/reels/"; - URI uri = URI.create("https://r.jina.ai/" + source); + private SocialFeedBatch fetchViaReader(SocialChannel channel, ReaderAttempt attempt) { + URI uri = readerUri(channel.getSourceUrl(), attempt.sourceScheme()); HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.USER_AGENT, "Mozilla/5.0"); + headers.setAccept(List.of(MediaType.TEXT_PLAIN)); + headers.setAcceptLanguageAsLocales(List.of(Locale.ENGLISH)); headers.set("X-Engine", "browser"); - if (noCache) headers.set("X-No-Cache", "true"); - else headers.set("X-Cache-Tolerance", "3600"); + if (attempt.noCache()) headers.set("X-No-Cache", "true"); + else headers.set("X-Cache-Tolerance", Long.toString(attempt.cacheToleranceSeconds())); try { ResponseEntity response = restTemplate.exchange(uri, HttpMethod.GET, new HttpEntity<>(headers), String.class); @@ -94,6 +99,24 @@ public class FacebookPageFeedClient implements SocialFeedClient { } } + static URI readerUri(String channelUrl, String sourceScheme) { + String source = channelUrl.replaceFirst("^https?", sourceScheme).replaceAll("/+$", "") + "/reels/"; + return URI.create("https://r.jina.ai/" + source); + } + + static List readerAttempts(boolean baselineCollection) { + long fallbackTolerance = baselineCollection + ? READER_BASELINE_CACHE_SECONDS + : READER_RECENT_CACHE_SECONDS; + return List.of( + new ReaderAttempt("https", true, 0), + new ReaderAttempt("http", true, 0), + new ReaderAttempt("https", false, READER_RECENT_CACHE_SECONDS), + new ReaderAttempt("http", false, fallbackTolerance)); + } + + record ReaderAttempt(String sourceScheme, boolean noCache, long cacheToleranceSeconds) { } + private SocialFeedBatch fetchFromHost(SocialChannel channel, String host) { try { Map config = new LinkedHashMap<>(); diff --git a/src/test/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClientTest.java b/src/test/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClientTest.java index 83bac32..a01a05b 100644 --- a/src/test/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClientTest.java +++ b/src/test/java/com/hlab/yanalyst/domain/social/FacebookPageFeedClientTest.java @@ -2,6 +2,8 @@ package com.hlab.yanalyst.domain.social; import org.junit.jupiter.api.Test; +import java.net.URI; + import static org.assertj.core.api.Assertions.assertThat; class FacebookPageFeedClientTest { @@ -50,4 +52,18 @@ class FacebookPageFeedClientTest { .containsExactly("2234599224051571", "926151600080380"); assertThat(batch.videos().get(0).thumbnailUrl()).isEqualTo("https://cdn.example/one.jpg?x=1&y=2"); } + + @Test + void retriesReaderWithBothSourceSchemesAndUsesLongCacheOnlyForBaseline() { + assertThat(FacebookPageFeedClient.readerUri("https://www.facebook.com/phutunet/", "http")) + .isEqualTo(URI.create("https://r.jina.ai/http://www.facebook.com/phutunet/reels/")); + + assertThat(FacebookPageFeedClient.readerAttempts(true)) + .extracting(FacebookPageFeedClient.ReaderAttempt::sourceScheme) + .containsExactly("https", "http", "https", "http"); + assertThat(FacebookPageFeedClient.readerAttempts(true).get(3).cacheToleranceSeconds()) + .isEqualTo(2_592_000); + assertThat(FacebookPageFeedClient.readerAttempts(false).get(3).cacheToleranceSeconds()) + .isEqualTo(3_600); + } }