feat: 소재 피드 10분 이상만 수집·표시 (min-duration-sec)
피드 수집 필터를 쇼츠/롱폼 구분에서 최소 길이(초) 기준으로 교체. 기본 600초(10분) — FEED_MIN_DURATION_SEC 로 조정 가능. 화면 조회에도 같은 하한을 적용해 이미 담긴 10분 미만 영상도 소스·경쟁 탭에서 바로 사라진다. 길이 필터 라벨(공식클립 10~15분)도 정리. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e964cecceb
commit
d4547056e3
@ -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<String> 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -18,7 +18,8 @@ import java.util.Map;
|
||||
/**
|
||||
* 소재 발굴 피드 수집기.
|
||||
*
|
||||
* <p>SOURCE(웹예능 공식채널)와 RIVAL(경쟁 클립 채널)의 신규 롱폼을 주기적으로 받아온다(쇼츠는 수집하지 않는다).
|
||||
* <p>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) {
|
||||
|
||||
@ -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<ChannelVideo> rows = channelVideoRepository.feed(
|
||||
source, publishedAfter, channelFilter, minSec, maxSec, hideWorked,
|
||||
|
||||
@ -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} # 이 횟수 미만 해시태그는 프로그램명으로 보지 않음
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>소재 피드</h1>
|
||||
<p class="sub">소재 원본(웹예능)과 경쟁 채널의 신규 롱폼 업로드를 최신순으로 봅니다.</p>
|
||||
<p class="sub">소재 원본(웹예능)과 경쟁 채널의 신규 롱폼(10분 이상) 업로드를 최신순으로 봅니다.</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="openPersons()">
|
||||
@ -64,7 +64,7 @@
|
||||
<label class="field" for="fLength" id="lengthField">길이
|
||||
<select id="fLength" onchange="applyFilters()">
|
||||
<option value="">전체</option>
|
||||
<option value="CLIP">공식클립 (~15분)</option>
|
||||
<option value="CLIP">공식클립 (10~15분)</option>
|
||||
<option value="FULL">풀에피 (15분+)</option>
|
||||
</select>
|
||||
</label>
|
||||
@ -102,7 +102,7 @@
|
||||
<div class="modal-body">
|
||||
<p class="text-sm text-muted mb-3" style="line-height:1.6;">
|
||||
<b>소재 원본</b>은 유퀴즈·핑계고 같은 웹예능 공식채널,
|
||||
<b>경쟁 채널</b>은 같은 소재를 다루는 예능짤 채널입니다. 두 역할 모두 롱폼만 수집합니다.
|
||||
<b>경쟁 채널</b>은 같은 소재를 다루는 예능짤 채널입니다. 두 역할 모두 10분 이상 롱폼만 수집합니다.
|
||||
</p>
|
||||
|
||||
<div class="seed-add">
|
||||
@ -645,7 +645,7 @@
|
||||
<button class="btn btn-secondary" onclick="runCollect()">지금 수집</button>`);
|
||||
} else {
|
||||
showState('inbox', '아직 소재가 없습니다',
|
||||
'경쟁 예능짤 채널을 시드로 등록한 뒤 수집하세요. (롱폼만 수집됩니다)',
|
||||
'경쟁 예능짤 채널을 시드로 등록한 뒤 수집하세요. (10분 이상만 수집됩니다)',
|
||||
`<button class="btn btn-primary" onclick="openSeeds()">시드 등록하기</button>
|
||||
<button class="btn btn-secondary" onclick="runCollect()">지금 수집</button>`);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user