feat: 숏폼 자막 완료 텔레그램 알림 추가

This commit is contained in:
hehihoho3@gmail.com 2026-08-13 15:05:45 +09:00
parent 504d0f09d5
commit f51cab471e
2 changed files with 40 additions and 4 deletions

View File

@ -1,5 +1,6 @@
package com.hlab.yanalyst.domain.shortform; package com.hlab.yanalyst.domain.shortform;
import com.hlab.yanalyst.global.notify.TelegramNotifier;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -29,6 +30,7 @@ public class ShortformSubtitleService {
private final ShortformJobRepository jobRepository; private final ShortformJobRepository jobRepository;
private final ShortformWhisperService whisperService; private final ShortformWhisperService whisperService;
private final TelegramNotifier telegramNotifier;
@Value("${ytdlp.bin:yt-dlp}") @Value("${ytdlp.bin:yt-dlp}")
private String ytdlpBin; private String ytdlpBin;
@ -52,18 +54,47 @@ public class ShortformSubtitleService {
String vtt = collectOrTranscribe(jobId, job.getVideoId()); String vtt = collectOrTranscribe(jobId, job.getVideoId());
ShortformJob current = jobRepository.findById(jobId).orElse(null); ShortformJob current = jobRepository.findById(jobId).orElse(null);
if (current == null) return; if (current == null) return;
if (vtt == null) current.markSubtitleUnavailable("한국어 공식/자동 자막이 없습니다"); if (vtt == null) current.markSubtitleUnavailable("YouTube/Whisper 자막을 생성하지 못했습니다");
else current.storeSubtitle(vtt); else current.storeSubtitle(vtt);
jobRepository.save(current); jobRepository.save(current);
if (vtt == null) notifyFailure(current, current.getSubtitleError());
else notifyReady(current);
} catch (Exception e) { } catch (Exception e) {
log.warn("숏폼 큐 자막 수집 실패: jobId={}, videoId={}", jobId, job.getVideoId(), e); log.warn("숏폼 큐 자막 수집 실패: jobId={}, videoId={}", jobId, job.getVideoId(), e);
jobRepository.findById(jobId).ifPresent(current -> { jobRepository.findById(jobId).ifPresent(current -> {
current.markSubtitleFailed(e.getMessage()); current.markSubtitleFailed(e.getMessage());
jobRepository.save(current); jobRepository.save(current);
notifyFailure(current, current.getSubtitleError());
}); });
} }
} }
private void notifyReady(ShortformJob job) {
telegramNotifier.sendMessage("✅ <b>숏폼 자막 생성 완료</b>\n"
+ "분류: " + categoryLabel(job) + "\n"
+ "영상: <code>" + job.getVideoId() + "</code>\n"
+ "<a href=\"https://h-lab.tolag.shop/shortform?category="
+ job.categoryOrDefault().name() + "\">숏폼 큐에서 확인</a>");
}
private void notifyFailure(ShortformJob job, String reason) {
telegramNotifier.sendMessage("❌ <b>숏폼 자막 생성 실패</b>\n"
+ "분류: " + categoryLabel(job) + "\n"
+ "영상: <code>" + job.getVideoId() + "</code>\n"
+ "사유: " + escapeHtml(reason) + "\n"
+ "<a href=\"https://h-lab.tolag.shop/shortform?category="
+ job.categoryOrDefault().name() + "\">숏폼 큐에서 확인</a>");
}
private static String categoryLabel(ShortformJob job) {
return job.categoryOrDefault() == ShortformCategory.POLITICS ? "정치" : "예능";
}
private static String escapeHtml(String value) {
if (value == null || value.isBlank()) return "알 수 없는 오류";
return value.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
String collectOrTranscribe(Long jobId, String videoId) throws IOException, InterruptedException { String collectOrTranscribe(Long jobId, String videoId) throws IOException, InterruptedException {
try { try {
String vtt = collectWithRetry(videoId); String vtt = collectWithRetry(videoId);

View File

@ -1,5 +1,6 @@
package com.hlab.yanalyst.domain.shortform; package com.hlab.yanalyst.domain.shortform;
import com.hlab.yanalyst.global.notify.TelegramNotifier;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.file.Path; import java.nio.file.Path;
@ -37,7 +38,8 @@ class ShortformSubtitleServiceTest {
@Test @Test
void retriesOnceWhenFirstCollectionHasNoSubtitle() throws Exception { void retriesOnceWhenFirstCollectionHasNoSubtitle() throws Exception {
ShortformSubtitleService service = spy(new ShortformSubtitleService( ShortformSubtitleService service = spy(new ShortformSubtitleService(
mock(ShortformJobRepository.class), mock(ShortformWhisperService.class))); mock(ShortformJobRepository.class), mock(ShortformWhisperService.class),
mock(TelegramNotifier.class)));
doReturn(null, "WEBVTT\n").when(service).collect("gDfGqRDubCg"); doReturn(null, "WEBVTT\n").when(service).collect("gDfGqRDubCg");
assertThat(service.collectWithRetry("gDfGqRDubCg")).isEqualTo("WEBVTT\n"); assertThat(service.collectWithRetry("gDfGqRDubCg")).isEqualTo("WEBVTT\n");
@ -48,7 +50,8 @@ class ShortformSubtitleServiceTest {
void fallsBackToWhisperWhenYoutubeHasNoSubtitle() throws Exception { void fallsBackToWhisperWhenYoutubeHasNoSubtitle() throws Exception {
ShortformJobRepository repository = mock(ShortformJobRepository.class); ShortformJobRepository repository = mock(ShortformJobRepository.class);
ShortformWhisperService whisperService = mock(ShortformWhisperService.class); ShortformWhisperService whisperService = mock(ShortformWhisperService.class);
ShortformSubtitleService service = spy(new ShortformSubtitleService(repository, whisperService)); ShortformSubtitleService service = spy(new ShortformSubtitleService(
repository, whisperService, mock(TelegramNotifier.class)));
doReturn(null).when(service).collectWithRetry("gDfGqRDubCg"); doReturn(null).when(service).collectWithRetry("gDfGqRDubCg");
when(whisperService.transcribe("gDfGqRDubCg")).thenReturn("WEBVTT\n"); when(whisperService.transcribe("gDfGqRDubCg")).thenReturn("WEBVTT\n");
@ -64,13 +67,15 @@ class ShortformSubtitleServiceTest {
"gDfGqRDubCg", "gDfGqRDubCg",
ShortformCategory.ENTERTAINMENT); ShortformCategory.ENTERTAINMENT);
when(repository.findById(1L)).thenReturn(Optional.of(job)); when(repository.findById(1L)).thenReturn(Optional.of(job));
TelegramNotifier telegramNotifier = mock(TelegramNotifier.class);
ShortformSubtitleService service = spy(new ShortformSubtitleService( ShortformSubtitleService service = spy(new ShortformSubtitleService(
repository, mock(ShortformWhisperService.class))); repository, mock(ShortformWhisperService.class), telegramNotifier));
doReturn("WEBVTT\n").when(service).collectOrTranscribe(1L, "gDfGqRDubCg"); doReturn("WEBVTT\n").when(service).collectOrTranscribe(1L, "gDfGqRDubCg");
service.collectAsync(1L); service.collectAsync(1L);
assertThat(job.getSubtitleStatus()).isEqualTo(SubtitleStatus.READY); assertThat(job.getSubtitleStatus()).isEqualTo(SubtitleStatus.READY);
assertThat(job.getSubtitleVtt()).isEqualTo("WEBVTT\n"); assertThat(job.getSubtitleVtt()).isEqualTo("WEBVTT\n");
verify(telegramNotifier).sendMessage(org.mockito.ArgumentMatchers.contains("자막 생성 완료"));
} }
} }