diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java index 475d09b..7d50042 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java @@ -299,13 +299,13 @@ public class ChannelService { /** * videos.list 로 상세를 받아 ChannelVideo 를 upsert 한다. * - * @param source 저장할 출처. CHANNEL(등록 채널) / SOURCE(소재 원본) / RIVAL(경쟁 쇼츠) - * @param shortsOnly null 이면 전체, TRUE 면 Shorts 만, FALSE 면 롱폼만 저장 + * @param source 저장할 출처. CHANNEL(등록 채널) / SOURCE(소재 원본) / RIVAL(경쟁 채널) + * @param minDurationSec 이 길이(초) 미만이거나 길이를 모르는 영상은 건너뜀(null 이면 전체) * @param publishedAfter 이 시각 이전 업로드는 건너뜀(null 이면 제한 없음) * @return 저장·갱신한 영상 수 */ private int upsertVideos(Channel channel, List videoIds, String source, - Boolean shortsOnly, LocalDateTime publishedAfter) { + Integer minDurationSec, LocalDateTime publishedAfter) { String apiUrl = UriComponentsBuilder.fromHttpUrl("https://www.googleapis.com/youtube/v3/videos") // status 는 임베드 가능 여부(embeddable) 때문에 필요하다. part 를 늘려도 쿼터는 그대로다. .queryParam("part", "snippet,statistics,contentDetails,status") @@ -339,8 +339,8 @@ public class ChannelService { Integer durationSec = VideoMetrics.parseDurationSec(duration); Boolean isShorts = VideoMetrics.isShorts(durationSec); - // 포맷/기간 필터 — 피드 수집에서만 사용(일반 채널 수집은 둘 다 null) - if (shortsOnly != null && shortsOnly != isShorts) continue; + // 길이/기간 필터 — 피드 수집에서만 사용(일반 채널 수집은 둘 다 null) + if (minDurationSec != null && (durationSec == null || durationSec < minDurationSec)) continue; if (publishedAfter != null && publishedAt.isBefore(publishedAfter)) continue; java.math.BigDecimal viewsPerHour = VideoMetrics.viewsPerHour(viewCount, publishedAt); @@ -390,15 +390,16 @@ public class ChannelService { /** * 피드 시드 채널의 최근 업로드를 1페이지(최대 50건)만 수집한다. - * 역할과 무관하게 롱폼만 저장한다 — 쇼츠는 이미 잘린 결과물이라 소재가 아니다. + * 역할과 무관하게 {@code minDurationSec} 이상 롱폼만 저장한다 — 짧은 클립·쇼츠는 자를 원본이 못 된다. * * @param channel SOURCE 또는 RIVAL 역할의 채널 * @param publishedAfter 이 시각 이후 업로드만 수집 + * @param minDurationSec 이 길이(초) 미만은 수집하지 않음 * @return 저장·갱신된 영상 수 * @throws IllegalStateException uploads 플레이리스트를 찾을 수 없을 때 */ @Transactional - public int collectFeedVideos(Channel channel, LocalDateTime publishedAfter) { + public int collectFeedVideos(Channel channel, LocalDateTime publishedAfter, int minDurationSec) { String role = channel.roleOrDefault(); String uploadsPlaylistId = channel.getUploadsPlaylistId(); if (uploadsPlaylistId == null || uploadsPlaylistId.isBlank()) { @@ -422,9 +423,8 @@ public class ChannelService { } if (videoIds.isEmpty()) return 0; - // 피드는 롱폼만 수집한다 (쇼츠는 이미 잘린 결과물 — 자를 원본이 아니다) String source = ChannelRole.RIVAL.equals(role) ? ChannelRole.RIVAL : ChannelRole.SOURCE; - return upsertVideos(channel, videoIds, source, Boolean.FALSE, publishedAfter); + return upsertVideos(channel, videoIds, source, minDurationSec, publishedAfter); } diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/FeedCollectionService.java b/src/main/java/com/hlab/yanalyst/domain/channel/FeedCollectionService.java index 6736e6e..499bcf1 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/FeedCollectionService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/FeedCollectionService.java @@ -18,7 +18,8 @@ import java.util.Map; /** * 소재 발굴 피드 수집기. * - *

SOURCE(웹예능 공식채널)와 RIVAL(경쟁 클립 채널)의 신규 롱폼을 주기적으로 받아온다(쇼츠는 수집하지 않는다). + *

SOURCE(웹예능 공식채널)와 RIVAL(경쟁 클립 채널)의 신규 롱폼을 주기적으로 받아온다 + * (min-duration-sec 미만의 짧은 클립·쇼츠는 수집하지 않는다). * uploads 플레이리스트 1페이지만 조회하므로 채널당 약 2 units — 검색(search.list 100 units)보다 훨씬 싸다. */ @Slf4j @@ -40,6 +41,10 @@ public class FeedCollectionService { @Value("${hlab.feed.period-days:14}") private int periodDays; + /** 이 길이(초) 미만은 수집하지 않는다 — 자를 원본이 못 되는 짧은 클립·쇼츠 차단. 기본 10분. */ + @Value("${hlab.feed.min-duration-sec:600}") + private int minDurationSec; + /** * role 컬럼이 없던 시절의 기존 채널은 role 이 null 이다. 부팅 시 1회 MY 로 백필한다. * (ddl-auto:update 는 DEFAULT 를 채워주지 않으므로 애플리케이션에서 처리) @@ -78,7 +83,7 @@ public class FeedCollectionService { continue; } try { - saved += channelService.collectFeedVideos(c, publishedAfter); + saved += channelService.collectFeedVideos(c, publishedAfter, minDurationSec); markSuccess(c.getId()); ok++; } catch (Exception e) { diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/FeedService.java b/src/main/java/com/hlab/yanalyst/domain/channel/FeedService.java index 599f9dc..45e399e 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/FeedService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/FeedService.java @@ -37,6 +37,10 @@ public class FeedService { @org.springframework.beans.factory.annotation.Value("${hlab.feed.person.max-people:10}") private int maxPeople; + /** 수집 하한과 같은 값 — 이미 담겨 있는 더 짧은 영상도 화면에서 걸러 일관되게 보인다. */ + @org.springframework.beans.factory.annotation.Value("${hlab.feed.min-duration-sec:600}") + private int minDurationSec; + /** * 피드 카드 목록(항상 최신순). * @@ -79,6 +83,8 @@ public class FeedService { default -> { /* 미인식 값은 필터 없음 */ } } } + // 수집 하한을 화면에도 적용 — 하한을 올렸을 때 과거에 담긴 짧은 영상이 남아 보이지 않게 + minSec = (minSec == null) ? Integer.valueOf(minDurationSec) : Integer.valueOf(Math.max(minSec, minDurationSec)); List rows = channelVideoRepository.feed( source, publishedAfter, channelFilter, minSec, maxSec, hideWorked, diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e91960d..281b251 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -100,11 +100,12 @@ hlab: # 발굴 대상 포맷 기본값: LONG_FORM(롱폼) | SHORTS. UI '지금 발굴'에서 건별 선택 가능. format: ${DISCOVERY_FORMAT:LONG_FORM} - # 소재 발굴 피드: 소스(웹예능 공식채널)·경쟁(클립 채널)의 신규 롱폼만 최신순으로 수집 (쇼츠 제외) + # 소재 발굴 피드: 소스(웹예능 공식채널)·경쟁(클립 채널)의 신규 롱폼만 최신순으로 수집 (10분 미만 제외) feed: enabled: ${FEED_ENABLED:true} cron: ${FEED_CRON:0 15 */3 * * *} # 3시간마다 (소재 선점은 업로드 후 몇 시간이 승부) period-days: ${FEED_PERIOD_DAYS:14} # 이보다 오래된 업로드는 수집하지 않음 + min-duration-sec: ${FEED_MIN_DURATION_SEC:600} # 이보다 짧은 영상은 수집·표시하지 않음 (기본 10분) seed: max-keywords: ${FEED_SEED_MAX_KEYWORDS:8} # 시드 자동 발굴 시 사용할 상위 해시태그 수(1개당 100 units) min-tag-count: ${FEED_SEED_MIN_TAG_COUNT:2} # 이 횟수 미만 해시태그는 프로그램명으로 보지 않음 diff --git a/src/main/resources/templates/feed.html b/src/main/resources/templates/feed.html index 59a9a9f..42db95e 100644 --- a/src/main/resources/templates/feed.html +++ b/src/main/resources/templates/feed.html @@ -11,7 +11,7 @@