인물 추적은 65초 컷으로 쇼츠를 걸렀지만 유튜브 쇼츠가 최대 3분까지 허용되면서 1~3분짜리 쇼츠가 통과해 인물 탭에 들어왔다. 수집(PersonPicks)과 조회(feedByPerson) 모두 180초 컷으로 올려 이미 담긴 1~3분 영상도 탭에서 사라지게 했다. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
93 lines
4.4 KiB
Java
93 lines
4.4 KiB
Java
package com.hlab.yanalyst.domain.channel;
|
|
|
|
import java.time.LocalDateTime;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* 인물 검색 결과에서 실제로 담을 영상을 고르는 순수 로직.
|
|
*
|
|
* <p>검색은 최신순 50건을 주는데 대부분이 쇼츠다. 롱폼만 남기고, 내 채널·경쟁 채널 것은
|
|
* 소재가 아니므로 뺀다.
|
|
*/
|
|
public final class PersonPicks {
|
|
|
|
/**
|
|
* 제목에 이게 들어가면 버린다. 아이돌 이름으로 검색하면 음악방송 직캠·무대·교차편집이
|
|
* 결과를 뒤덮어 정작 예능 출연분이 묻힌다. 말로 터지는 순간이 소재이므로 무대 영상은 소용없다.
|
|
*/
|
|
private static final List<String> STAGE_MARKERS = List.of(
|
|
"직캠", "페이스캠", "풀캠", "fancam", "focus cam",
|
|
"교차편집", "stage mix", "stagemix", "무대교차",
|
|
"음중", "쇼챔", "엠카", "뮤직뱅크", "인기가요", "music bank", "show champion",
|
|
"performance video", "mv", "m/v", "lyric video", "cover", "커버"
|
|
);
|
|
|
|
/**
|
|
* 이 길이 이하는 쇼츠로 보고 버린다. 유튜브 쇼츠가 최대 3분(180초)까지 허용되어
|
|
* {@link VideoMetrics#isShorts}의 65초 컷으로는 1~3분짜리 쇼츠가 통과한다.
|
|
* 어차피 3분 이하는 자를 원본으로도 부족하다.
|
|
*/
|
|
public static final int SHORTS_MAX_SEC = 180;
|
|
|
|
private PersonPicks() {}
|
|
|
|
/** 음악방송 무대·직캠·커버 영상인가(제목 기준). */
|
|
public static boolean isStageVideo(String title) {
|
|
if (title == null || title.isBlank()) return false;
|
|
String t = title.toLowerCase(java.util.Locale.ROOT);
|
|
for (String marker : STAGE_MARKERS) {
|
|
if (t.contains(marker)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 검색 결과 1건.
|
|
*
|
|
* @param ytChannelId 업로드 채널. 내 채널/경쟁 채널 제외 판정에 쓴다
|
|
*/
|
|
public record Found(String videoId, String title, String ytChannelId, String channelTitle,
|
|
Integer durationSec, LocalDateTime publishedAt, Long viewCount) {}
|
|
|
|
/**
|
|
* 담을 영상만 고른다.
|
|
*
|
|
* @param found 검색 결과
|
|
* @param excludeChannels 제외할 채널 ID(내 채널 OWN, 경쟁 채널 RIVAL)
|
|
* @param publishedAfter 이 시각 이전 업로드는 제외(null 이면 제한 없음)
|
|
* @param minViewsPerHour 시간당 조회수 하한. 인물명 검색에는 팬채널·커버곡·개인 브이로그가
|
|
* 대량으로 딸려 오는데(실측 조회수 30회 미만), 생 조회수로 자르면 방금 올라온
|
|
* 영상까지 걸리므로 시간당 조회수로 거른다. 0 이하면 미적용
|
|
* @return 롱폼이면서 무대 영상이 아니고 제외 대상도 아닌 것들 (입력 순서 유지)
|
|
*/
|
|
public static List<Found> keep(List<Found> found, Set<String> excludeChannels,
|
|
LocalDateTime publishedAfter, double minViewsPerHour) {
|
|
List<Found> out = new ArrayList<>();
|
|
if (found == null) return out;
|
|
|
|
for (Found f : found) {
|
|
if (f == null || f.videoId() == null || f.videoId().isBlank()) continue;
|
|
// 쇼츠 제외 — 인물 추적의 목적은 자를 원본(롱폼)을 찾는 것
|
|
if (f.durationSec() == null || f.durationSec() <= SHORTS_MAX_SEC) continue;
|
|
if (f.ytChannelId() != null && excludeChannels != null
|
|
&& excludeChannels.contains(f.ytChannelId())) continue;
|
|
if (publishedAfter != null && f.publishedAt() != null
|
|
&& f.publishedAt().isBefore(publishedAfter)) continue;
|
|
if (isStageVideo(f.title())) continue;
|
|
if (minViewsPerHour > 0 && !meetsVelocity(f, minViewsPerHour)) continue;
|
|
out.add(f);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 시간당 조회수가 하한 이상인가. 계산에 필요한 값이 없으면 통과시킨다(과도한 배제 방지). */
|
|
private static boolean meetsVelocity(Found f, double minViewsPerHour) {
|
|
if (f.viewCount() == null || f.publishedAt() == null) return true;
|
|
java.math.BigDecimal vph = VideoMetrics.viewsPerHour(f.viewCount(), f.publishedAt());
|
|
if (vph == null) return true;
|
|
return vph.doubleValue() >= minViewsPerHour;
|
|
}
|
|
}
|