temp값 삭제

This commit is contained in:
2025-11-28 16:05:05 +09:00
parent 860a81b86c
commit 308d5442d7
64 changed files with 6 additions and 2744 deletions

View File

@@ -1,18 +0,0 @@
{
"permissions": {
"allow": [
"Bash(./gradlew.bat clean test:*)",
"Bash(curl:*)",
"Bash(./gradlew.bat test:*)",
"Bash(./gradlew.bat:*)",
"Bash(find:*)",
"Bash(claude mcp:*)",
"WebFetch(domain:github.com)",
"mcp__serena__get_symbols_overview",
"mcp__serena__list_dir",
"mcp__serena__get_current_config"
],
"deny": [],
"ask": []
}
}

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.gradle/
.idea/
.claude/
backup/
bin/
build/

View File

@@ -1,2 +0,0 @@
#Thu Nov 06 15:53:40 KST 2025
gradle.version=8.1.1

Binary file not shown.

1
.idea/.name generated
View File

@@ -1 +0,0 @@
video-url-analyzer

6
.idea/compiler.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

16
.idea/gradle.xml generated
View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

8
.idea/misc.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@@ -1,143 +0,0 @@
package com.caliverse.video.analyzer;
import com.caliverse.video.analyzer.platform.PlatformAnalyzer;
import com.caliverse.video.analyzer.platform.PlatformAnalyzerFactory;
import com.caliverse.video.analyzer.service.VideoAnalyzerService;
import com.caliverse.video.analyzer.model.VideoAnalysisResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 비디오 URL 분석 기능을 제공하는 메인 클래스입니다.
*
* <p>이 클래스는 비디오 URL을 분석하여 다음 사항을 확인합니다:
* <ul>
* <li>DRM 보호 URL 여부</li>
* <li>3D 비디오 여부</li>
* <li>AVPlayer 재생 가능 여부</li>
* </ul>
*
* <p>지원하는 플랫폼:
* <ul>
* <li>네이버TV (VOD, Clip, Live)</li>
* <li>일반 비디오 URL (mp4, m3u8, mov 등)</li>
* </ul>
*
* <h3>사용 예제</h3>
* <pre>{@code
* VideoAnalyzer analyzer = new VideoAnalyzer();
* VideoAnalysisResult result = analyzer.analyzeUrl("https://tv.naver.com/v/84373511");
*
* if (result.isSuccess()) {
* System.out.println("재생 가능: " + result.getReason());
* } else {
* System.out.println("재생 불가: " + result.getReason());
* }
* }</pre>
*
* @since 1.0.0
* @see VideoAnalysisResult
*/
public class VideoAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(VideoAnalyzer.class);
private final VideoAnalyzerService analyzerService;
private final PlatformAnalyzerFactory platformFactory;
/**
* VideoAnalyzer 인스턴스를 생성합니다.
*
* <p>생성 시 내부적으로 분석 서비스와 플랫폼별 분석기를 초기화합니다.
*/
public VideoAnalyzer() {
this.analyzerService = new VideoAnalyzerService();
this.platformFactory = new PlatformAnalyzerFactory(analyzerService);
}
/**
* 비디오 URL을 분석하여 재생 가능 여부를 판단합니다.
*
* <p>특정 플랫폼(네이버TV 등)의 URL인 경우 해당 플랫폼에 맞는 분석을 수행하며,
* 일반 URL인 경우 표준 비디오 분석을 수행합니다.
*
* <p>분석 항목:
* <ul>
* <li>보호된 URL 여부 (DRM, 서명된 URL 등)</li>
* <li>3D 비디오 여부</li>
* <li>AVPlayer 재생 가능 여부 (포맷 체크)</li>
* </ul>
*
* <p>성공 조건: 모든 항목이 재생 가능한 경우<br>
* 실패 조건: 하나라도 재생 불가능한 경우 (상세 이유 제공)
*
* @param url 분석할 비디오 URL (http:// 또는 https:// 형식)
* @return 분석 결과를 담은 {@link VideoAnalysisResult} 객체
* @see VideoAnalysisResult#isSuccess()
* @see VideoAnalysisResult#getReason()
*
* &#064;example
* <pre>{@code
* // 네이버TV URL 분석
* VideoAnalysisResult result1 = analyzer.analyzeUrl("https://tv.naver.com/v/84373511");
*
* // 일반 비디오 URL 분석
* VideoAnalysisResult result2 = analyzer.analyzeUrl("https://example.com/video.mp4");
* }</pre>
*/
public VideoAnalysisResult analyzeUrl(String url) {
// 플랫폼별 분석기 찾기
PlatformAnalyzer analyzer = platformFactory.findAnalyzer(url);
if (analyzer != null) {
return analyzer.analyze(url);
} else {
// 일반 비디오 URL로 처리
return analyzerService.analyzeVideo(url);
}
}
/**
* 현재 지원하는 플랫폼 목록을 반환합니다.
*
* <p>각 플랫폼별로 특화된 URL 분석 로직이 제공됩니다.
*
* @return 지원하는 플랫폼 이름 배열 (예: ["네이버TV"])
* <p>
* &#064;example
* <pre>{@code
* String[] platforms = analyzer.getSupportedPlatforms();
* System.out.println("지원 플랫폼: " + String.join(", ", platforms));
* // 출력: 지원 플랫폼: 네이버TV
* }</pre>
*/
public String[] getSupportedPlatforms() {
return platformFactory.getAllAnalyzers().stream()
.map(PlatformAnalyzer::getPlatformName)
.toArray(String[]::new);
}
/**
* 명령줄에서 테스트 실행을 위한 메인 메서드
*/
public static void main(String[] args) {
if (args.length < 1) {
VideoAnalyzer analyzer = new VideoAnalyzer();
logger.info("사용법: java -jar video-url-analyzer.jar <video-url>");
logger.info("지원하는 플랫폼: {}", String.join(", ", analyzer.getSupportedPlatforms()));
logger.info("예시:");
logger.info(" 네이버 TV: https://tv.naver.com/v/85477455");
logger.info(" 일반 비디오: https://example.com/video.mp4");
System.exit(1);
}
String url = args[0];
VideoAnalyzer analyzer = new VideoAnalyzer();
logger.info("=== URL 분석 결과 ===");
VideoAnalysisResult result = analyzer.analyzeUrl(url);
logger.info("{}", result);
}
}

View File

@@ -1,11 +0,0 @@
package com.caliverse.video.analyzer.entity;
public enum UrlType {
YOUTUBE,
BLOB,
STREAMING,
DIRECT_FILE,
NAVER_TV,
UNKNOWN
}

View File

@@ -1,116 +0,0 @@
package com.caliverse.video.analyzer.model;
import com.caliverse.video.global.common.Messages;
/**
* 비디오 URL 분석 결과를 담는 모델 클래스입니다.
*
* <p>이 클래스는 비디오 URL 분석의 최종 결과를 나타냅니다.
* 모든 검사 항목이 통과하면 {@code success = true}, 하나라도 실패하면
* {@code success = false}와 함께 상세한 실패 이유를 제공합니다.
*
* <h3>분석 항목</h3>
* <ul>
* <li>보호된 URL 여부: DRM, 서명된 URL 감지</li>
* <li>3D 비디오 여부: 3D 포맷 감지</li>
* <li>AVPlayer 재생 가능 여부: 지원 포맷 확인</li>
* </ul>
*
* <h3>사용 예제</h3>
* <pre>{@code
* VideoAnalyzer analyzer = new VideoAnalyzer();
* VideoAnalysisResult result = analyzer.analyzeUrl(url);
*
* if (result.isSuccess()) {
* System.out.println("재생 가능: " + result.getReason());
* // 출력: "재생 가능: 모든 검사 통과"
* } else {
* System.out.println("재생 불가");
* System.out.println(result.getReason());
* // 출력 예시:
* // 재생 불가 사유:
* // - 보호된 URL: DRM 보호 키워드 감지: widevine
* // - 3D 비디오: 3D 비디오 헤더 감지
* }
* }</pre>
*
* @since 1.0.0
* @see com.caliverse.video.analyzer.VideoAnalyzer#analyzeUrl(String)
*/
public class VideoAnalysisResult {
private final boolean success;
private final String reason;
private VideoAnalysisResult(boolean success, String reason) {
this.success = success;
this.reason = reason;
}
/**
* 성공 결과 객체를 생성합니다.
*
* <p>모든 검사 항목이 통과했을 때 사용되며, reason은 "모든 검사 통과"로 설정됩니다.
*
* @return 성공 결과 객체
*/
public static VideoAnalysisResult success() {
return new VideoAnalysisResult(true, Messages.SUCCESS_ALL_CHECKS_PASSED);
}
/**
* 실패 결과 객체를 생성합니다.
*
* <p>하나 이상의 검사 항목이 실패했을 때 사용되며,
* 상세한 실패 이유를 reason에 포함합니다.
*
* @param reason 실패 이유 (개행 문자로 구분된 상세 설명 가능)
* @return 실패 결과 객체
*/
public static VideoAnalysisResult failure(String reason) {
return new VideoAnalysisResult(false, reason);
}
/**
* 분석 결과가 성공인지 여부를 반환합니다.
*
* <p>성공 조건:
* <ul>
* <li>보호되지 않은 URL</li>
* <li>2D 비디오</li>
* <li>AVPlayer에서 재생 가능</li>
* </ul>
*
* @return 모든 검사가 통과하면 {@code true}, 하나라도 실패하면 {@code false}
*/
public boolean isSuccess() {
return success;
}
/**
* 분석 결과의 이유를 반환합니다.
*
* <p>성공한 경우: "모든 검사 통과"<br>
* 실패한 경우: 상세한 실패 이유 (각 항목별로 구분)
*
* <p>실패 이유 예시:
* <pre>
* 재생 불가 사유:
* - 보호된 URL: DRM 보호 키워드 감지: widevine
* - 재생 불가: 지원되지 않는 포맷 또는 Content-Type: video/x-flv
* </pre>
*
* @return 분석 결과 이유 (null이 아닌 문자열)
*/
public String getReason() {
return reason;
}
@Override
public String toString() {
if (success) {
return "VideoAnalysisResult{ 성공: " + reason + " }";
} else {
return "VideoAnalysisResult{ 실패: " + reason + " }";
}
}
}

View File

@@ -1,29 +0,0 @@
package com.caliverse.video.analyzer.platform;
import com.caliverse.video.analyzer.model.VideoAnalysisResult;
public interface PlatformAnalyzer {
/**
* 해당 플랫폼의 URL인지 확인합니다.
*
* @param url 확인할 URL
* @return 해당 플랫폼 URL이면 true, 아니면 false
*/
boolean canHandle(String url);
/**
* 플랫폼 이름을 반환합니다.
*
* @return 플랫폼 이름
*/
String getPlatformName();
/**
* 플랫폼별 URL 분석을 수행합니다.
*
* @param url 분석할 URL
* @return 분석 결과
*/
VideoAnalysisResult analyze(String url);
}

View File

@@ -1,56 +0,0 @@
package com.caliverse.video.analyzer.platform;
import com.caliverse.video.analyzer.platform.impl.NaverTvPlatformAnalyzer;
import com.caliverse.video.analyzer.service.VideoAnalyzerService;
import java.util.ArrayList;
import java.util.List;
public class PlatformAnalyzerFactory {
private final List<PlatformAnalyzer> analyzers;
public PlatformAnalyzerFactory(VideoAnalyzerService videoAnalyzerService) {
this.analyzers = new ArrayList<>();
// 플랫폼 분석기들을 등록
analyzers.add(new NaverTvPlatformAnalyzer(videoAnalyzerService));
// 향후 다른 플랫폼들도 여기에 추가
// analyzers.add(new VimeoPlatformAnalyzer(videoAnalyzerService));
// analyzers.add(new DaumTvPlatformAnalyzer(videoAnalyzerService));
}
/**
* URL에 맞는 플랫폼 분석기를 찾습니다.
*
* @param url 분석할 URL
* @return 해당 플랫폼 분석기, 없으면 null
*/
public PlatformAnalyzer findAnalyzer(String url) {
for (PlatformAnalyzer analyzer : analyzers) {
if (analyzer.canHandle(url)) {
return analyzer;
}
}
return null;
}
/**
* 등록된 모든 플랫폼 분석기 목록을 반환합니다.
*
* @return 플랫폼 분석기 목록
*/
public List<PlatformAnalyzer> getAllAnalyzers() {
return new ArrayList<>(analyzers);
}
/**
* 새로운 플랫폼 분석기를 추가합니다.
*
* @param analyzer 추가할 분석기
*/
public void addAnalyzer(PlatformAnalyzer analyzer) {
analyzers.add(analyzer);
}
}

View File

@@ -1,525 +0,0 @@
package com.caliverse.video.analyzer.platform.impl;
import com.caliverse.video.analyzer.platform.PlatformAnalyzer;
import com.caliverse.video.analyzer.service.VideoAnalyzerService;
import com.caliverse.video.analyzer.model.VideoAnalysisResult;
import com.caliverse.video.global.common.CommonConstants;
import com.caliverse.video.global.common.Messages;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
public class NaverTvPlatformAnalyzer implements PlatformAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(NaverTvPlatformAnalyzer.class);
private final ObjectMapper objectMapper;
private final VideoAnalyzerService videoAnalyzerService;
public NaverTvPlatformAnalyzer(VideoAnalyzerService videoAnalyzerService) {
this.objectMapper = new ObjectMapper();
this.videoAnalyzerService = videoAnalyzerService;
}
@Override
public boolean canHandle(String url) {
if (url == null) return false;
String lowerUrl = url.toLowerCase();
return lowerUrl.contains(CommonConstants.DOMAIN_NAVER_TV) || lowerUrl.contains(CommonConstants.DOMAIN_NAVER);
}
@Override
public String getPlatformName() {
return CommonConstants.FLATFORM_NAME_NAVER_TV;
}
@Override
public VideoAnalysisResult analyze(String url) {
try {
// 1. HTML 콘텐츠 가져오기
String htmlContent = fetchHtmlContent(url);
// 2. __NEXT_DATA__ 파싱
JsonNode nextDataNode = extractNextData(htmlContent);
if (nextDataNode == null) {
return VideoAnalysisResult.failure(Messages.NAVER_TV_PARSING_FAILED);
}
// 3. props.pageProps 접근
JsonNode pageProps = nextDataNode.path("props").path("pageProps");
if (pageProps.isMissingNode()) {
return VideoAnalysisResult.failure(Messages.NAVER_TV_PARSING_FAILED);
}
// 4. 콘텐츠 타입별 처리
String videoUrl = null;
// 4-1. vodInfo 확인
JsonNode vodInfo = pageProps.path("vodInfo");
if (!vodInfo.isMissingNode()) {
videoUrl = handleVodInfo(vodInfo);
}
// 4-2. clipInfo 확인
if (videoUrl == null) {
JsonNode clipInfo = pageProps.path("clipInfo");
if (!clipInfo.isMissingNode()) {
videoUrl = handleClipInfo(clipInfo);
}
}
// 4-3. liveInfo 확인
if (videoUrl == null) {
JsonNode liveInfo = pageProps.path("liveInfo");
if (!liveInfo.isMissingNode()) {
videoUrl = handleLiveInfo(liveInfo);
}
}
if (videoUrl == null) {
return VideoAnalysisResult.failure(Messages.NAVER_TV_API_PARSING_FAILED);
}
// 5. 제한사항 확인
String restrictionReason = checkRestrictions(videoUrl);
if (restrictionReason != null) {
return VideoAnalysisResult.failure(Messages.format(Messages.NAVER_TV_RESTRICTION, restrictionReason));
}
// 6. 비디오 분석
return videoAnalyzerService.analyzeVideo(videoUrl);
} catch (Exception e) {
return VideoAnalysisResult.failure(Messages.format(Messages.NAVER_TV_ANALYSIS_ERROR, e.getMessage()));
}
}
private String fetchHtmlContent(String url) throws IOException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet(url);
request.setHeader(CommonConstants.REQUEST_HEADER_NAME_USER_AGENT, CommonConstants.REQUEST_HEADER_USER_AGENT);
try (CloseableHttpResponse response = httpClient.execute(request)) {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
}
}
/**
* HTML에서 __NEXT_DATA__ 스크립트를 파싱합니다.
*/
private JsonNode extractNextData(String htmlContent) {
try {
Matcher matcher = CommonConstants.NAVER_NEXT_DATA_PATTERN.matcher(htmlContent);
if (matcher.find()) {
String jsonData = matcher.group(1);
return objectMapper.readTree(jsonData);
}
return null;
} catch (Exception e) {
logger.warn("__NEXT_DATA__ 파싱 실패: {}", e.getMessage());
return null;
}
}
/**
* vodInfo 처리: videoId + inKey로 VOD API 호출
* 구조: vodInfo.clip.videoId, vodInfo.play.inKey
*/
private String handleVodInfo(JsonNode vodInfo) throws IOException {
String videoId = vodInfo.path("clip").path("videoId").asText(null);
String inKey = vodInfo.path("play").path("inKey").asText(null);
if (videoId != null && inKey != null) {
String apiUrl = String.format(CommonConstants.NAVER_VOD_API_URL,
videoId, URLEncoder.encode(inKey, StandardCharsets.UTF_8));
String apiResponse = fetchApiResponse(apiUrl);
return extractVideoUrl(apiResponse);
}
return null;
}
/**
* clipInfo 처리: videoId만으로 Short API 호출
*/
private String handleClipInfo(JsonNode clipInfo) throws IOException {
String videoId = clipInfo.path("videoId").asText(null);
if (videoId != null) {
String apiUrl = String.format(CommonConstants.NAVER_SHORT_API_URL, videoId);
String apiResponse = fetchApiResponse(apiUrl);
return extractVideoUrlFromShortApi(apiResponse);
}
return null;
}
/**
* liveInfo 처리: playbackBody에서 직접 m3u8 URL 추출
*/
private String handleLiveInfo(JsonNode liveInfo) {
try {
String playbackBody = liveInfo.path("playbackBody").asText(null);
if (playbackBody == null) {
return null;
}
// playbackBody는 JSON 문자열이므로 다시 파싱
JsonNode playbackNode = objectMapper.readTree(playbackBody);
// media 배열에서 HLS 프로토콜의 path 찾기
JsonNode mediaArray = playbackNode.path("media");
if (mediaArray.isArray()) {
for (JsonNode media : mediaArray) {
String protocol = media.path("protocol").asText("");
if ("HLS".equals(protocol)) {
String path = media.path("path").asText(null);
if (path != null && !path.isEmpty()) {
// \\u 이스케이프 처리
path = unescapeUnicode(path);
logger.debug("liveInfo에서 m3u8 URL 추출 성공");
return path;
}
}
}
}
return null;
} catch (Exception e) {
logger.warn("liveInfo playbackBody 파싱 실패: {}", e.getMessage());
return null;
}
}
/**
* 유니코드 이스케이프 시퀀스 처리
*/
private String unescapeUnicode(String text) {
if (text == null) {
return null;
}
return text.replace("\\u003d", "=")
.replace("\\u0026", "&")
.replace("\\u003c", "<")
.replace("\\u003e", ">");
}
private String fetchApiResponse(String apiUrl) throws IOException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet(apiUrl);
request.setHeader(CommonConstants.REQUEST_HEADER_NAME_USER_AGENT, CommonConstants.REQUEST_HEADER_USER_AGENT);
request.setHeader(CommonConstants.REQUEST_HEADER_NAME_REFERER, CommonConstants.REQUEST_HEADER_NAVER_REFERER);
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (response.getStatusLine().getStatusCode() != 200) {
throw new IOException("API 호출 실패");
}
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, StandardCharsets.UTF_8);
}
}
}
private String extractVideoUrl(String apiResponse) throws IOException {
try {
JsonNode rootNode = objectMapper.readTree(apiResponse);
// 1. MPD 구조 탐색: MPD -> Period -> AdaptationSet -> Representation
JsonNode mpdNode = rootNode.path("MPD");
if (mpdNode.isArray() && !mpdNode.isEmpty()) {
JsonNode firstMpd = mpdNode.get(0);
JsonNode periodNode = firstMpd.path("Period");
if (periodNode.isArray() && !periodNode.isEmpty()) {
JsonNode firstPeriod = periodNode.get(0);
JsonNode adaptationSetNode = firstPeriod.path("AdaptationSet");
if (adaptationSetNode.isArray() && !adaptationSetNode.isEmpty()) {
for (int i = 0; i < adaptationSetNode.size(); i++) {
JsonNode adaptationSet = adaptationSetNode.get(i);
JsonNode representationNode = adaptationSet.path("Representation");
if (representationNode.isArray() && !representationNode.isEmpty()) {
for (int j = 0; j < representationNode.size(); j++) {
JsonNode representation = representationNode.get(j);
String videoUrl = extractBaseUrlFromRepresentation(representation);
if (videoUrl != null) {
// logger.debug("비디오 URL 추출 성공 (AdaptationSet[{}], Representation[{}])", i, j);
return videoUrl;
}
}
}
String adaptationSetUrl = extractUrlFromAdaptationSet(adaptationSet);
if (adaptationSetUrl != null) {
// logger.debug("비디오 URL 추출 성공 (AdaptationSet[{}] 레벨)", i);
return adaptationSetUrl;
}
}
}
}
}
// 2. 레거시 구조 확인 (이전 API 버전 호환성)
JsonNode legacyRepresentations = rootNode.path("Representation");
if (legacyRepresentations.isArray() && legacyRepresentations.size() > 0) {
for (int i = 0; i < legacyRepresentations.size(); i++) {
JsonNode representation = legacyRepresentations.get(i);
String videoUrl = extractBaseUrlFromRepresentation(representation);
if (videoUrl != null) {
logger.debug("비디오 URL 추출 성공 (레거시 Representation[{}])", i);
return videoUrl;
}
}
}
// 3. 다른 가능한 경로들 확인
String alternativeUrl = findAlternativeVideoUrl(rootNode);
if (alternativeUrl != null) {
logger.debug("비디오 URL 추출 성공 (대안 경로)");
return alternativeUrl;
}
logger.warn("비디오 URL을 찾을 수 없습니다. JSON 구조를 확인해주세요.");
return null;
} catch (Exception e) {
throw new IOException("JSON 파싱 오류: " + e.getMessage(), e);
}
}
private String checkRestrictions(String videoUrl) {
try {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet(videoUrl);
request.setHeader(CommonConstants.REQUEST_HEADER_NAME_USER_AGENT, CommonConstants.REQUEST_HEADER_USER_AGENT);
request.setHeader(CommonConstants.REQUEST_HEADER_NAME_REFERER, CommonConstants.REQUEST_HEADER_NAVER_REFERER);
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 403) return "접근 금지 (403) - CORS 또는 권한 제한";
if (statusCode == 401) return "인 필요 (401)";
if (statusCode >= 400) return "HTTP 오류 (" + statusCode + ")";
// CORS 헤더 확인
if (response.containsHeader("Access-Control-Allow-Origin")) {
String allowOrigin = response.getFirstHeader("Access-Control-Allow-Origin").getValue();
if (!allowOrigin.equals("*") && !allowOrigin.contains("tv.naver.com")) {
return "CORS 제한 - 특정 도메인에서만 접근 가능";
}
}
return null; // 제한 없음
}
}
} catch (IOException e) {
return "네트워크 연결 오류: " + e.getMessage();
}
}
/**
* Representation 노드에서 BaseURL을 추출합니다.
*/
private String extractBaseUrlFromRepresentation(JsonNode representation) {
if (representation == null) return null;
// BaseURL 배열 형태
JsonNode baseUrls = representation.path("BaseURL");
if (baseUrls.isArray() && !baseUrls.isEmpty()) {
String url = baseUrls.get(0).asText();
if (url != null && !url.trim().isEmpty() && isValidVideoUrl(url)) {
return url;
}
}
// BaseURL 단일 문자열 형태
if (!baseUrls.isMissingNode() && baseUrls.isTextual()) {
String url = baseUrls.asText();
if (url != null && !url.trim().isEmpty() && isValidVideoUrl(url)) {
return url;
}
}
return null;
}
/**
* AdaptationSet 레벨에서 URL을 찾습니다.
*/
private String extractUrlFromAdaptationSet(JsonNode adaptationSet) {
if (adaptationSet == null) return null;
// AdaptationSet의 다양한 속성들 확인
JsonNode baseUrl = adaptationSet.path("BaseURL");
if (!baseUrl.isMissingNode()) {
if (baseUrl.isArray() && !baseUrl.isEmpty()) {
String url = baseUrl.get(0).asText();
if (isValidVideoUrl(url)) return url;
} else if (baseUrl.isTextual()) {
String url = baseUrl.asText();
if (isValidVideoUrl(url)) return url;
}
}
// SegmentTemplate이나 다른 방식으로 URL 구성이 필요한 경우
JsonNode segmentTemplate = adaptationSet.path("SegmentTemplate");
if (!segmentTemplate.isMissingNode()) {
// SegmentTemplate 기반 URL 구성 로직
return buildUrlFromSegmentTemplate(segmentTemplate);
}
return null;
}
/**
* 대안적인 비디오 URL 경로를 찾습니다.
*/
private String findAlternativeVideoUrl(JsonNode rootNode) {
// 1. 직접적인 URL 필드들 확인
String[] possibleUrlFields = {"videoUrl", "streamUrl", "playbackUrl", "mediaUrl", "url"};
for (String field : possibleUrlFields) {
JsonNode urlNode = rootNode.path(field);
if (!urlNode.isMissingNode() && urlNode.isTextual()) {
String url = urlNode.asText();
if (isValidVideoUrl(url)) return url;
}
}
// 2. 중첩된 구조에서 URL 찾기
JsonNode sources = rootNode.path("sources");
if (sources.isArray() && !sources.isEmpty()) {
for (JsonNode source : sources) {
String url = source.path("src").asText();
if (isValidVideoUrl(url)) return url;
}
}
// 3. 다른 가능한 구조들...
// 필요시 추가 경로들을 여기에 구현
return null;
}
/**
* SegmentTemplate에서 URL을 구성합니다.
*/
private String buildUrlFromSegmentTemplate(JsonNode segmentTemplate) {
// SegmentTemplate 방식은 복잡하므로 현재는 기본 구현만
JsonNode media = segmentTemplate.path("@media");
// JsonNode initialization = segmentTemplate.path("@initialization");
if (!media.isMissingNode() && media.isTextual()) {
String mediaTemplate = media.asText();
// 템플릿 변수들을 실제 값으로 치환하는 로직 필요
// 현재는 간단히 템플릿 문자열만 반환
if (mediaTemplate.contains("http")) {
return mediaTemplate;
}
}
return null;
}
/**
* 유효한 비디오 URL인지 확인합니다.
*/
private boolean isValidVideoUrl(String url) {
if (url == null || url.trim().isEmpty()) {
return false;
}
// 기본적인 URL 형식 확인
if (!url.startsWith("http://") && !url.startsWith("https://")) {
return false;
}
// 비디오 파일 확장자나 스트리밍 패턴 확인
String lowerUrl = url.toLowerCase();
return lowerUrl.contains(".mp4") ||
lowerUrl.contains(".m3u8") ||
lowerUrl.contains(".mov") ||
lowerUrl.contains(".m4v") ||
lowerUrl.contains("stream") ||
lowerUrl.contains("video");
}
/**
* 짧은 URL API 응답에서 비디오 URL을 추출합니다.
* 응답 구조: {"card":{"content":{"vod":{"playback":{"videos":{"list":[{"source":"URL"}]}}}}}}
*/
private String extractVideoUrlFromShortApi(String apiResponse) throws IOException {
try {
JsonNode rootNode = objectMapper.readTree(apiResponse);
// card.content.vod.playback.videos.list[0].source 경로로 접근
JsonNode cardNode = rootNode.path("card");
if (cardNode.isMissingNode()) {
logger.warn("짧은 URL API 응답에 'card' 노드가 없습니다.");
return null;
}
JsonNode contentNode = cardNode.path("content");
if (contentNode.isMissingNode()) {
logger.warn("짧은 URL API 응답에 'content' 노드가 없습니다.");
return null;
}
JsonNode vodNode = contentNode.path("vod");
if (vodNode.isMissingNode()) {
logger.warn("짧은 URL API 응답에 'vod' 노드가 없습니다.");
return null;
}
JsonNode playbackNode = vodNode.path("playback");
if (playbackNode.isMissingNode()) {
logger.warn("짧은 URL API 응답에 'playback' 노드가 없습니다.");
return null;
}
JsonNode videosNode = playbackNode.path("videos");
if (videosNode.isMissingNode()) {
logger.warn("짧은 URL API 응답에 'videos' 노드가 없습니다.");
return null;
}
JsonNode listNode = videosNode.path("list");
if (listNode.isArray() && !listNode.isEmpty()) {
JsonNode firstVideo = listNode.get(0);
JsonNode sourceNode = firstVideo.path("source");
if (!sourceNode.isMissingNode() && sourceNode.isTextual()) {
String videoUrl = sourceNode.asText();
if (isValidVideoUrl(videoUrl)) {
logger.debug("짧은 URL API에서 비디오 URL 추출 성공");
return videoUrl;
}
}
}
logger.warn("짧은 URL API 응답에서 비디오 URL을 찾을 수 없습니다.");
return null;
} catch (Exception e) {
throw new IOException("짧은 URL API JSON 파싱 오류: " + e.getMessage(), e);
}
}
}

View File

@@ -1,293 +0,0 @@
package com.caliverse.video.analyzer.service;
import com.caliverse.video.analyzer.entity.UrlType;
import com.caliverse.video.analyzer.model.VideoAnalysisResult;
import com.caliverse.video.global.common.Messages;
import com.caliverse.video.global.util.HttpUtils;
import com.caliverse.video.global.util.UrlParser;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 비디오 URL을 분석하는 핵심 서비스 클래스입니다.
*/
public class VideoAnalyzerService {
private final HttpUtils httpUtils;
private final UrlParser urlParser;
/**
* 체크 결과를 담는 내부 클래스
*/
private static class CheckResult {
final boolean result;
final String reason;
CheckResult(boolean result, String reason) {
this.result = result;
this.reason = reason;
}
}
// AVPlayer에서 지원하는 비디오 포맷 목록
private static final List<String> SUPPORTED_FORMATS = Arrays.asList(
"mp4", "mov", "m4v", "m3u8", "hls"
);
// DRM 관련 키워드
private static final List<String> DRM_KEYWORDS = Arrays.asList(
"drm", "widevine", "playready", "fairplay", "encrypted", "protection"
);
public VideoAnalyzerService() {
this.httpUtils = new HttpUtils();
this.urlParser = new UrlParser();
}
/**
* URL이 보호된 URL인지 분석합니다 (DRM, CORS, Signed URL).
*/
private CheckResult checkProtection(String videoUrl) {
if (videoUrl == null || videoUrl.trim().isEmpty()) {
return new CheckResult(false, Messages.ERROR_INVALID_URL);
}
// URL 유형별로 분기 처리
UrlType urlType = determineUrlType(videoUrl);
switch (urlType) {
case YOUTUBE: // 서명 정보가 계속 필요하기때문에 불가능
return new CheckResult(false, Messages.PROTECTION_YOUTUBE_URL);
case NAVER_TV:
return new CheckResult(false, "");
case BLOB:
return checkBlobProtection(videoUrl);
case STREAMING:
return checkStreamingProtection(videoUrl);
case DIRECT_FILE:
return checkDirectFileProtection(videoUrl);
default:
return checkGenericProtection(videoUrl);
}
}
/**
* URL이 보호된 URL인지 분석합니다 (하위 호환성을 위한 메소드).
* @deprecated checkProtection 사용을 권장합니다
*/
@Deprecated
public boolean isProtectedUrl(String videoUrl) {
return checkProtection(videoUrl).result;
}
/**
* URL 유형을 판별합니다.
*/
private UrlType determineUrlType(String videoUrl) {
String lowerUrl = videoUrl.toLowerCase();
if (lowerUrl.contains("youtube.com") || lowerUrl.contains("youtu.be")) {
return UrlType.YOUTUBE;
} else if (lowerUrl.contains("naver.com") || lowerUrl.contains("naver")) {
return UrlType.NAVER_TV;
} else if (lowerUrl.startsWith("blob:")) {
return UrlType.BLOB;
} else if (lowerUrl.contains(".m3u8") || lowerUrl.contains("hls") ||
lowerUrl.contains("dash") || lowerUrl.contains("stream")) {
return UrlType.STREAMING;
} else if (lowerUrl.matches(".*\\.(mp4|mov|m4v|avi|mkv|webm)($|\\?.*)")) {
return UrlType.DIRECT_FILE;
} else {
return UrlType.UNKNOWN;
}
}
/**
* Blob URL의 보호 상태를 확인합니다.
*/
private CheckResult checkBlobProtection(String videoUrl) {
// Blob URL은 브라우저에서 생성되므로 일반적으로 보호되지 않음
// 단, 원본 소스가 보호된 경우일 수 있음
return new CheckResult(false, Messages.PROTECTION_BLOB_URL);
}
/**
* 스트리밍 URL의 보호 상태를 확인합니다.
*/
private CheckResult checkStreamingProtection(String videoUrl) {
String lowerUrl = videoUrl.toLowerCase();
// 1. DRM 관련 키워드 확인
for (String keyword : DRM_KEYWORDS) {
if (lowerUrl.contains(keyword)) {
return new CheckResult(true, Messages.format(Messages.PROTECTION_DRM_KEYWORD_DETECTED, keyword));
}
}
// 2. 토큰 기반 인증 확인
if (urlParser.hasSignatureParameters(videoUrl)) {
return new CheckResult(true, Messages.PROTECTION_SIGNED_URL);
}
// 3. HTTP 헤더 확인
try {
if (httpUtils.hasSecurityHeaders(videoUrl)) {
return new CheckResult(true, Messages.PROTECTION_SECURITY_HEADER_DETECTED);
}
return new CheckResult(false, Messages.PROTECTION_NOT_PROTECTED_STREAMING);
} catch (IOException e) {
return new CheckResult(true, Messages.format(Messages.PROTECTION_CONNECTION_FAILED, e.getMessage()));
}
}
/**
* 직접 파일 URL의 보호 상태를 확인합니다.
*/
private CheckResult checkDirectFileProtection(String videoUrl) {
String lowerUrl = videoUrl.toLowerCase();
// 1. DRM 관련 키워드 확인
for (String keyword : DRM_KEYWORDS) {
if (lowerUrl.contains(keyword)) {
return new CheckResult(true, Messages.format(Messages.PROTECTION_DRM_KEYWORD_DETECTED, keyword));
}
}
// 2. Signed URL 검사
if (urlParser.hasSignatureParameters(videoUrl)) {
return new CheckResult(true, Messages.PROTECTION_SIGNED_URL);
}
// 3. HTTP 헤더를 통한 보안 검사
try {
if (httpUtils.hasSecurityHeaders(videoUrl)) {
return new CheckResult(true, Messages.PROTECTION_SECURITY_HEADER_DETECTED);
}
return new CheckResult(false, Messages.PROTECTION_NOT_PROTECTED_DIRECT_FILE);
} catch (IOException e) {
return new CheckResult(true, Messages.format(Messages.PROTECTION_CONNECTION_FAILED, e.getMessage()));
}
}
/**
* 일반적인 URL의 보호 상태를 확인합니다.
*/
private CheckResult checkGenericProtection(String videoUrl) {
// 기존의 일반적인 보호 URL 검사 로직
return checkDirectFileProtection(videoUrl);
}
/**
* URL이 3D 비디오인지 분석합니다.
*/
private CheckResult check3DVideo(String videoUrl) {
if (videoUrl == null || videoUrl.trim().isEmpty()) {
return new CheckResult(false, Messages.ERROR_INVALID_URL);
}
// 필요시 헤더나 메타데이터 확인
try {
if (httpUtils.checkFor3DHeaders(videoUrl)) {
return new CheckResult(true, Messages.THREE_D_HEADER_DETECTED);
}
return new CheckResult(false, Messages.THREE_D_NOT_DETECTED);
} catch (IOException e) {
return new CheckResult(false, Messages.format(Messages.THREE_D_CONNECTION_FAILED, e.getMessage()));
}
}
/**
* URL이 3D 비디오인지 분석합니다 (하위 호환성을 위한 메소드).
* @deprecated check3DVideo 사용을 권장합니다
*/
@Deprecated
public boolean is3DVideo(String videoUrl) {
return check3DVideo(videoUrl).result;
}
/**
* URL이 AVPlayer에서 재생 가능한지 분석합니다.
*/
private CheckResult checkPlayability(String videoUrl) {
if (videoUrl == null || videoUrl.trim().isEmpty()) {
return new CheckResult(false, Messages.ERROR_INVALID_URL);
}
// 1. 포맷 검사 (파일 확장자)
String extension = urlParser.getExtension(videoUrl);
if (extension != null && SUPPORTED_FORMATS.contains(extension.toLowerCase())) {
return new CheckResult(true, Messages.format(Messages.PLAYABILITY_SUPPORTED_FORMAT, extension));
}
// 2. 스트리밍 URL 패턴 검사
if (urlParser.isStreamingUrl(videoUrl)) {
return new CheckResult(true, Messages.PLAYABILITY_SUPPORTED_STREAMING);
}
// 3. 필요시 콘텐츠 타입 헤더 확인
try {
String contentType = httpUtils.getContentType(videoUrl);
if (contentType != null && (
contentType.contains("video/") ||
contentType.contains("application/x-mpegURL") ||
contentType.contains("application/vnd.apple.mpegurl")
)) {
return new CheckResult(true, Messages.format(Messages.PLAYABILITY_SUPPORTED_CONTENT_TYPE, contentType));
}
return new CheckResult(false, Messages.format(Messages.PLAYABILITY_UNSUPPORTED_FORMAT, contentType));
} catch (IOException e) {
return new CheckResult(false, Messages.format(Messages.PLAYABILITY_CONNECTION_FAILED, e.getMessage()));
}
}
/**
* URL이 AVPlayer에서 재생 가능한지 분석합니다 (하위 호환성을 위한 메소드).
* @deprecated checkPlayability 사용을 권장합니다
*/
@Deprecated
public boolean isPlayableOnAVPlayer(String videoUrl) {
return checkPlayability(videoUrl).result;
}
/**
* 모든 분석을 한번에 수행하고 결과를 반환합니다.
* 보호되지 않고, 2D 비디오이고, AVPlayer에서 재생 가능하면 성공입니다.
*/
public VideoAnalysisResult analyzeVideo(String videoUrl) {
CheckResult protectionResult = checkProtection(videoUrl);
CheckResult threeDResult = check3DVideo(videoUrl);
CheckResult playabilityResult = checkPlayability(videoUrl);
// 실패 이유 수집
StringBuilder failureReasons = new StringBuilder();
// 보호된 URL이면 실패
if (protectionResult.result) {
failureReasons.append(Messages.ANALYSIS_PROTECTED_URL_PREFIX)
.append(protectionResult.reason).append("\n");
}
// 3D 비디오이면 실패
// if (threeDResult.result) {
// failureReasons.append(Messages.ANALYSIS_THREE_D_VIDEO_PREFIX)
// .append(threeDResult.reason).append("\n");
// }
// AVPlayer에서 재생 불가능하면 실패
if (!playabilityResult.result) {
failureReasons.append(Messages.ANALYSIS_NOT_PLAYABLE_PREFIX)
.append(playabilityResult.reason).append("\n");
}
// 실패 이유가 있으면 실패, 없으면 성공
if (!failureReasons.isEmpty()) {
return VideoAnalysisResult.failure(
Messages.ANALYSIS_FAILURE_HEADER + "\n" + failureReasons.toString().trim()
);
} else {
return VideoAnalysisResult.success();
}
}
}

View File

@@ -1,26 +0,0 @@
package com.caliverse.video.global.common;
import java.util.regex.Pattern;
public class CommonConstants {
// 도메인
public static final String DOMAIN_NAVER_TV = "naver.com";
public static final String DOMAIN_NAVER = "naver.me";
public static final String FLATFORM_NAME_NAVER_TV = "네이버TV";
// 정규표현식
public static final Pattern NAVER_NEXT_DATA_PATTERN = Pattern.compile("<script id=\"__NEXT_DATA__\" type=\"application/json\">(.+?)</script>");
public static final Pattern NAVER_VIDEO_ID_PATTERN = Pattern.compile("\"videoId\"\\s*:\\s*\"([^\"]+)\"");
public static final Pattern NAVER_IN_KEY_PATTERN = Pattern.compile("\"inKey\"\\s*:\\s*\"([^\"]+)\"");
// API URL
public static final String NAVER_VOD_API_URL = "https://apis.naver.com/neonplayer/vodplay/v3/playback/%s?key=%s";
public static final String NAVER_SHORT_API_URL = "https://api-videohub.naver.com/shortformhub/feeds/v7/card?serviceType=NTV&seedMediaId=%s&mediaType=VOD";
//헤더
public static final String REQUEST_HEADER_NAME_USER_AGENT = "User-Agent";
public static final String REQUEST_HEADER_NAME_REFERER = "Referer";
public static final String REQUEST_HEADER_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
public static final String REQUEST_HEADER_NAVER_REFERER = "https://tv.naver.com/";
}

View File

@@ -1,60 +0,0 @@
package com.caliverse.video.global.common;
/**
* 분석 결과 메시지를 관리하는 상수 클래스입니다.
*/
public class Messages {
// ========== 일반 메시지 ==========
public static final String SUCCESS_ALL_CHECKS_PASSED = "모든 검사 통과";
public static final String ERROR_INVALID_URL = "유효하지 않은 URL";
// ========== 보호 URL 체크 메시지 ==========
public static final String PROTECTION_YOUTUBE_URL = "YouTube URL (서명 정보 필요)";
public static final String PROTECTION_BLOB_URL = "Blob URL (브라우저에서 생성된 URL)";
public static final String PROTECTION_DRM_KEYWORD_DETECTED = "DRM 보호 키워드 감지: %s";
public static final String PROTECTION_SIGNED_URL = "서명된 URL (토큰 기반 인증)";
public static final String PROTECTION_SECURITY_HEADER_DETECTED = "보안 헤더 감지";
public static final String PROTECTION_NOT_PROTECTED_STREAMING = "보호되지 않은 스트리밍 URL";
public static final String PROTECTION_NOT_PROTECTED_DIRECT_FILE = "보호되지 않은 직접 파일 URL";
public static final String PROTECTION_CONNECTION_FAILED = "연결 실패 (보호된 것으로 간주): %s";
// ========== 3D 비디오 체크 메시지 ==========
public static final String THREE_D_HEADER_DETECTED = "3D 비디오 헤더 감지";
public static final String THREE_D_NOT_DETECTED = "일반 2D 비디오";
public static final String THREE_D_CONNECTION_FAILED = "연결 실패 (2D로 간주): %s";
// ========== 재생 가능성 체크 메시지 ==========
public static final String PLAYABILITY_SUPPORTED_FORMAT = "지원되는 포맷: %s";
public static final String PLAYABILITY_SUPPORTED_STREAMING = "지원되는 스트리밍 URL";
public static final String PLAYABILITY_SUPPORTED_CONTENT_TYPE = "지원되는 Content-Type: %s";
public static final String PLAYABILITY_UNSUPPORTED_FORMAT = "지원되지 않는 포맷 또는 Content-Type: %s";
public static final String PLAYABILITY_CONNECTION_FAILED = "연결 실패 (재생 불가능으로 간주): %s";
// ========== 전체 분석 결과 메시지 ==========
public static final String ANALYSIS_FAILURE_HEADER = "재생 불가 사유:";
public static final String ANALYSIS_PROTECTED_URL_PREFIX = "- 보호된 URL: ";
public static final String ANALYSIS_THREE_D_VIDEO_PREFIX = "- 3D 비디오: ";
public static final String ANALYSIS_NOT_PLAYABLE_PREFIX = "- 재생 불가: ";
// ========== 네이버TV 관련 메시지 ==========
public static final String NAVER_TV_PARSING_FAILED = "네이버TV URL 파싱 실패: videoId 또는 inKey를 찾을 수 없습니다.";
public static final String NAVER_TV_API_PARSING_FAILED = "네이버TV API 응답 파싱 실패: 비디오 URL을 찾을 수 없습니다.";
public static final String NAVER_TV_RESTRICTION = "네이버TV 제한사항: %s";
public static final String NAVER_TV_ANALYSIS_ERROR = "네이버TV 분석 중 오류 발생: %s";
private Messages() {
// 인스턴스화 방지
}
/**
* 포맷 문자열을 사용하는 메시지를 생성합니다.
*
* @param format 포맷 문자열
* @param args 포맷 인자
* @return 포맷팅된 메시지
*/
public static String format(String format, Object... args) {
return String.format(format, args);
}
}

View File

@@ -1,165 +0,0 @@
package com.caliverse.video.global.util;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* HTTP 요청 관련 유틸리티 클래스입니다.
*/
public class HttpUtils {
private static final Logger logger = LoggerFactory.getLogger(HttpUtils.class);
private static final int CONNECTION_TIMEOUT = 5000; // 5초
/**
* URL의 HTTP 헤더에서 보안 관련 헤더가 있는지 확인합니다.
*
* @param url 확인할 URL
* @return 보안 헤더가 있으면 true, 없으면 false
* @throws IOException HTTP 요청 실패 시
*/
public boolean hasSecurityHeaders(String url) throws IOException {
HttpHead request = new HttpHead(url);
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(CONNECTION_TIMEOUT)
.setSocketTimeout(CONNECTION_TIMEOUT)
.build();
request.setConfig(config);
try (CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(request)) {
// CORS 헤더 확인
if (response.containsHeader("Access-Control-Allow-Origin")) {
return true;
}
// 인증 관련 헤더 확인
if (response.containsHeader("WWW-Authenticate") ||
response.containsHeader("Authorization")) {
return true;
}
// DRM 관련 헤더 확인
if (response.containsHeader("X-DRM-Type") ||
response.containsHeader("X-Content-Protection")) {
return true;
}
// 기타 보안 헤더 확인
for (Header header : response.getAllHeaders()) {
String headerName = header.getName().toLowerCase();
String headerValue = header.getValue().toLowerCase();
if (headerName.contains("token") ||
headerName.contains("auth") ||
headerName.contains("drm") ||
headerValue.contains("token") ||
headerValue.contains("protection")) {
return true;
}
}
return false;
}
}
/**
* URL의 HTTP 헤더에서 3D 비디오 관련 정보가 있는지 확인합니다.
*
* @param url 확인할 URL
* @return 3D 비디오 헤더가 있으면 true, 없으면 false
* @throws IOException HTTP 요청 실패 시
*/
public boolean checkFor3DHeaders(String url) throws IOException {
HttpHead request = new HttpHead(url);
// User-Agent 헤더 추가 (일반적인 브라우저로 위장)
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36");
// 기타 일반적인 브라우저 헤더들 추가
request.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/json");
request.setHeader("Accept-Language", "ko-KR,ko;q=0.9,en;q=0.8");
request.setHeader("Accept-Encoding", "gzip, deflate, br");
request.setHeader("Connection", "keep-alive");
request.setHeader("Upgrade-Insecure-Requests", "1");
request.setHeader("Referer", "https://tv.naver.com");
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(CONNECTION_TIMEOUT)
.setSocketTimeout(CONNECTION_TIMEOUT)
.build();
request.setConfig(config);
try (CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode >= 400) {
logger.warn("HTTP Error {} for URL: {}", statusCode, url);
return false; // 에러 발생 시 보안 헤더 없다고 간주
}
// 3D 관련 커스텀 헤더 확인
if (response.containsHeader("X-Video-Type")) {
String videoType = response.getFirstHeader("X-Video-Type").getValue();
if (videoType.toLowerCase().contains("3d") ||
videoType.toLowerCase().contains("stereoscopic")) {
return true;
}
}
// 기타 헤더에서 3D 관련 키워드 확인
for (Header header : response.getAllHeaders()) {
String headerValue = header.getValue().toLowerCase();
if (headerValue.contains("3d") ||
headerValue.contains("stereoscopic")) {
return true;
}
}
// 콘텐츠 타입 헤더 확인
if (response.containsHeader(HttpHeaders.CONTENT_TYPE)) {
String contentType = response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue();
return contentType.contains("3d") || contentType.contains("stereoscopic");
}
return false;
}
}
/**
* URL의 HTTP 헤더에서 Content-Type을 가져옵니다.
*
* @param url 확인할 URL
* @return Content-Type 문자열, 헤더가 없거나 요청 실패 시 null
* @throws IOException HTTP 요청 실패 시
*/
public String getContentType(String url) throws IOException {
HttpHead request = new HttpHead(url);
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(CONNECTION_TIMEOUT)
.setSocketTimeout(CONNECTION_TIMEOUT)
.build();
request.setConfig(config);
try (CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(request)) {
if (response.containsHeader(HttpHeaders.CONTENT_TYPE)) {
return response.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue();
}
return null;
}
}
}

View File

@@ -1,575 +0,0 @@
package com.caliverse.video.global.util;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* oEmbed API를 활용하여 영상 메타데이터를 가져오는 유틸리티 클래스입니다.
*
* <p>지원 플랫폼:
* <ul>
* <li>네이버TV</li>
* <li>YouTube</li>
* <li>Vimeo</li>
* </ul>
*/
public class OEmbedFetcher {
private static final Logger logger = LoggerFactory.getLogger(OEmbedFetcher.class);
private final ObjectMapper objectMapper;
// oEmbed 엔드포인트 매핑
private static final Map<String, String> OEMBED_ENDPOINTS = new HashMap<>();
static {
OEMBED_ENDPOINTS.put("naver.com", "https://tv.naver.com/oembed");
OEMBED_ENDPOINTS.put("naver.me", "https://tv.naver.com/oembed");
OEMBED_ENDPOINTS.put("youtube.com", "https://www.youtube.com/oembed");
OEMBED_ENDPOINTS.put("youtu.be", "https://www.youtube.com/oembed");
OEMBED_ENDPOINTS.put("vimeo.com", "https://vimeo.com/api/oembed.json");
}
public OEmbedFetcher() {
this.objectMapper = new ObjectMapper();
}
/**
* oEmbed API를 통해 영상 메타데이터를 가져옵니다.
*
* @param videoUrl 영상 URL
* @return oEmbed 응답 데이터
* @throws IOException API 요청 실패 시
*/
public OEmbedResponse fetchOEmbedData(String videoUrl) throws IOException {
// 단축 URL(naver.me)인 경우 실제 URL로 변환
String resolvedUrl = resolveShortUrl(videoUrl);
logger.debug("원본 URL: {}, 해결된 URL: {}", videoUrl, resolvedUrl);
String oembedEndpoint = getOEmbedEndpoint(resolvedUrl);
if (oembedEndpoint == null) {
throw new IllegalArgumentException("지원하지 않는 플랫폼입니다: " + resolvedUrl);
}
String oembedUrl = buildOEmbedUrl(oembedEndpoint, resolvedUrl);
logger.debug("oEmbed URL: {}", oembedUrl);
String jsonResponse = fetchOEmbedJson(oembedUrl);
OEmbedResponse response = parseOEmbedResponse(jsonResponse);
// 네이버TV 쇼츠의 경우 oEmbed가 빈 응답을 반환하면 직접 메타데이터 구성
if (isEmptyResponse(response) && isNaverTvShortUrl(resolvedUrl)) {
response = buildNaverTvShortResponse(resolvedUrl);
logger.debug("네이버TV 쇼츠 URL에 대한 fallback 응답 생성");
}
return response;
}
/**
* 단축 URL을 실제 URL로 변환합니다.
* naver.me와 같은 단축 URL의 경우 HTTP 리다이렉트를 따라가서 최종 URL을 반환합니다.
*
* @param url 원본 URL
* @return 해결된 URL (단축 URL이 아니면 원본 URL 반환)
* @throws IOException 리다이렉트 처리 실패 시
*/
private String resolveShortUrl(String url) throws IOException {
// naver.me 단축 URL인지 확인
if (!url.toLowerCase().contains("naver.me")) {
return url;
}
try (CloseableHttpClient httpClient = HttpClients.custom()
.disableRedirectHandling() // 자동 리다이렉트 비활성화
.build()) {
HttpGet request = new HttpGet(url);
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
// 리다이렉트 상태 코드 확인 (301, 302, 303, 307, 308)
if (statusCode >= 300 && statusCode < 400) {
String location = response.getFirstHeader("Location").getValue();
if (location != null && !location.isEmpty()) {
logger.debug("단축 URL 리다이렉트: {} -> {}", url, location);
return location;
}
}
// 리다이렉트가 없으면 원본 URL 반환
return url;
}
}
}
/**
* URL에서 적절한 oEmbed 엔드포인트를 찾습니다.
*/
private String getOEmbedEndpoint(String videoUrl) {
String lowerUrl = videoUrl.toLowerCase();
for (Map.Entry<String, String> entry : OEMBED_ENDPOINTS.entrySet()) {
if (lowerUrl.contains(entry.getKey())) {
return entry.getValue();
}
}
return null;
}
/**
* oEmbed API URL을 생성합니다.
*/
private String buildOEmbedUrl(String endpoint, String videoUrl) {
try {
String encodedUrl = URLEncoder.encode(videoUrl, StandardCharsets.UTF_8.toString());
return endpoint + "?url=" + encodedUrl + "&format=json";
} catch (Exception e) {
throw new RuntimeException("URL 인코딩 실패", e);
}
}
/**
* oEmbed API를 호출하여 JSON 응답을 가져옵니다.
*/
private String fetchOEmbedJson(String oembedUrl) throws IOException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet(oembedUrl);
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
request.setHeader("Accept", "application/json");
try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new IOException("oEmbed API 호출 실패. 상태 코드: " + statusCode);
}
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
}
}
}
/**
* oEmbed JSON 응답을 파싱합니다.
*/
private OEmbedResponse parseOEmbedResponse(String jsonResponse) throws IOException {
try {
JsonNode rootNode = objectMapper.readTree(jsonResponse);
OEmbedResponse response = new OEmbedResponse();
response.setType(getStringValue(rootNode, "type"));
response.setVersion(getStringValue(rootNode, "version"));
response.setTitle(getStringValue(rootNode, "title"));
response.setAuthorName(getStringValue(rootNode, "author_name"));
response.setAuthorUrl(getStringValue(rootNode, "author_url"));
response.setProviderName(getStringValue(rootNode, "provider_name"));
response.setProviderUrl(getStringValue(rootNode, "provider_url"));
response.setThumbnailUrl(getStringValue(rootNode, "thumbnail_url"));
response.setThumbnailWidth(getIntValue(rootNode, "thumbnail_width"));
response.setThumbnailHeight(getIntValue(rootNode, "thumbnail_height"));
response.setWidth(getIntValue(rootNode, "width"));
response.setHeight(getIntValue(rootNode, "height"));
response.setHtml(getStringValue(rootNode, "html"));
// 원본 JSON도 저장
response.setRawJson(jsonResponse);
return response;
} catch (Exception e) {
throw new IOException("oEmbed 응답 파싱 실패: " + e.getMessage(), e);
}
}
private String getStringValue(JsonNode node, String fieldName) {
JsonNode fieldNode = node.get(fieldName);
return fieldNode != null && !fieldNode.isNull() ? fieldNode.asText() : null;
}
private Integer getIntValue(JsonNode node, String fieldName) {
JsonNode fieldNode = node.get(fieldName);
return fieldNode != null && !fieldNode.isNull() ? fieldNode.asInt() : null;
}
/**
* oEmbed 응답이 비어있는지 확인합니다.
*/
private boolean isEmptyResponse(OEmbedResponse response) {
return response != null &&
response.getType() == null &&
response.getTitle() == null &&
(response.getWidth() == null || response.getWidth() == 0) &&
(response.getHeight() == null || response.getHeight() == 0);
}
/**
* 네이버TV 쇼츠 URL인지 확인합니다.
*/
private boolean isNaverTvShortUrl(String url) {
return url != null && url.toLowerCase().contains("tv.naver.com/h/");
}
/**
* 네이버TV 쇼츠 URL에서 ID를 추출하고 기본 메타데이터를 생성합니다.
*/
private OEmbedResponse buildNaverTvShortResponse(String url) {
try {
// URL에서 ID 추출: https://tv.naver.com/h/87349660 -> 87349660
String id = extractNaverTvId(url);
if (id == null) {
return null;
}
// HTML 페이지에서 메타데이터 추출
MetaData metaData = extractMetaDataFromHtml(url);
// embed URL 생성
String embedUrl = "https://tv.naver.com/embed/" + id + "?autoPlay=true";
String html = "<iframe width='544' height='306' src='" + embedUrl +
"' frameborder='0' scrolling='no' allowfullscreen></iframe>";
OEmbedResponse response = new OEmbedResponse();
response.setType("video");
response.setVersion("1.0");
response.setProviderName("NAVERTV");
response.setProviderUrl("https://tv.naver.com");
response.setWidth(544);
response.setHeight(306);
response.setHtml(html);
// HTML에서 추출한 메타데이터 설정
if (metaData != null) {
response.setTitle(metaData.title);
response.setAuthorName(metaData.authorName);
response.setThumbnailUrl(metaData.thumbnailUrl);
if (metaData.thumbnailUrl != null) {
// 네이버TV 썸네일은 일반적으로 16:9 비율
response.setThumbnailWidth(720);
response.setThumbnailHeight(405);
}
}
// JSON 구성
String rawJson = buildShortResponseJson(response, embedUrl);
response.setRawJson(rawJson);
return response;
} catch (Exception e) {
logger.warn("네이버TV 쇼츠 응답 생성 실패: {}", e.getMessage());
return null;
}
}
/**
* HTML 페이지에서 Open Graph 메타데이터를 추출합니다.
*/
private MetaData extractMetaDataFromHtml(String url) {
try {
String htmlContent = fetchHtmlContent(url);
MetaData metaData = new MetaData();
// og:description에서 제목 추출
metaData.title = extractMetaTag(htmlContent, "og:description");
// og:title에서 작성자 이름 추출
metaData.authorName = extractMetaTag(htmlContent, "og:title");
// og:image에서 썸네일 URL 추출
metaData.thumbnailUrl = extractMetaTag(htmlContent, "og:image");
logger.debug("쇼츠 메타데이터 추출 - 제목: {}, 작성자: {}", metaData.title, metaData.authorName);
return metaData;
} catch (Exception e) {
logger.warn("HTML 메타데이터 추출 실패: {}", e.getMessage());
return null;
}
}
/**
* HTML에서 특정 meta 태그의 content 값을 추출합니다.
*/
private String extractMetaTag(String htmlContent, String property) {
try {
// <meta property="og:xxx" content="value"/> 패턴 매칭
String pattern = "<meta\\s+property=\"" + property + "\"\\s+content=\"([^\"]+)\"";
java.util.regex.Pattern regexPattern = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher matcher = regexPattern.matcher(htmlContent);
if (matcher.find()) {
return matcher.group(1);
}
return null;
} catch (Exception e) {
logger.warn("meta 태그 추출 실패: {}", e.getMessage());
return null;
}
}
/**
* 네이버TV 쇼츠 URL의 HTML을 가져옵니다.
*/
private String fetchHtmlContent(String url) throws IOException {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet request = new HttpGet(url);
request.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
try (CloseableHttpResponse response = httpClient.execute(request)) {
if (response.getStatusLine().getStatusCode() != 200) {
throw new IOException("HTML 페이지 가져오기 실패");
}
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
}
}
}
/**
* 쇼츠 응답 JSON을 생성합니다.
*/
private String buildShortResponseJson(OEmbedResponse response, String embedUrl) {
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"type\":\"video\",");
json.append("\"version\":\"1.0\",");
json.append("\"provider_name\":\"NAVERTV\",");
json.append("\"provider_url\":\"https://tv.naver.com\",");
if (response.getTitle() != null) {
json.append("\"title\":\"").append(escapeJson(response.getTitle())).append("\",");
}
if (response.getAuthorName() != null) {
json.append("\"author_name\":\"").append(escapeJson(response.getAuthorName())).append("\",");
}
if (response.getThumbnailUrl() != null) {
json.append("\"thumbnail_url\":\"").append(escapeJson(response.getThumbnailUrl())).append("\",");
json.append("\"thumbnail_width\":").append(response.getThumbnailWidth()).append(",");
json.append("\"thumbnail_height\":").append(response.getThumbnailHeight()).append(",");
}
json.append("\"width\":544,");
json.append("\"height\":306,");
json.append("\"html\":\"").append(escapeJson(response.getHtml())).append("\",");
json.append("\"videoType\":\"short\",");
json.append("\"playerUrl\":\"").append(embedUrl).append("\"");
json.append("}");
return json.toString();
}
/**
* JSON 문자열을 이스케이프 처리합니다.
*/
private String escapeJson(String text) {
if (text == null) return "";
return text.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
/**
* 네이버TV URL에서 비디오 ID를 추출합니다.
* 예: https://tv.naver.com/h/87349660 -> 87349660
*/
private String extractNaverTvId(String url) {
try {
// /h/ 또는 /v/ 패턴에서 ID 추출
String[] patterns = {"/h/", "/v/", "/l/"};
for (String pattern : patterns) {
int index = url.indexOf(pattern);
if (index != -1) {
String remaining = url.substring(index + pattern.length());
// ? 또는 / 이전까지가 ID
int endIndex = remaining.indexOf('?');
if (endIndex == -1) {
endIndex = remaining.indexOf('/');
}
if (endIndex == -1) {
return remaining;
} else {
return remaining.substring(0, endIndex);
}
}
}
return null;
} catch (Exception e) {
logger.warn("네이버TV ID 추출 실패: {}", e.getMessage());
return null;
}
}
/**
* 메타데이터를 담는 내부 클래스입니다.
*/
private static class MetaData {
String title;
String authorName;
String thumbnailUrl;
}
/**
* oEmbed 응답 데이터를 담는 클래스입니다.
*/
public static class OEmbedResponse {
private String type;
private String version;
private String title;
private String authorName;
private String authorUrl;
private String providerName;
private String providerUrl;
private String thumbnailUrl;
private Integer thumbnailWidth;
private Integer thumbnailHeight;
private Integer width;
private Integer height;
private String html;
private String rawJson;
// Getters and Setters
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthorName() {
return authorName;
}
public void setAuthorName(String authorName) {
this.authorName = authorName;
}
public String getAuthorUrl() {
return authorUrl;
}
public void setAuthorUrl(String authorUrl) {
this.authorUrl = authorUrl;
}
public String getProviderName() {
return providerName;
}
public void setProviderName(String providerName) {
this.providerName = providerName;
}
public String getProviderUrl() {
return providerUrl;
}
public void setProviderUrl(String providerUrl) {
this.providerUrl = providerUrl;
}
public String getThumbnailUrl() {
return thumbnailUrl;
}
public void setThumbnailUrl(String thumbnailUrl) {
this.thumbnailUrl = thumbnailUrl;
}
public Integer getThumbnailWidth() {
return thumbnailWidth;
}
public void setThumbnailWidth(Integer thumbnailWidth) {
this.thumbnailWidth = thumbnailWidth;
}
public Integer getThumbnailHeight() {
return thumbnailHeight;
}
public void setThumbnailHeight(Integer thumbnailHeight) {
this.thumbnailHeight = thumbnailHeight;
}
public Integer getWidth() {
return width;
}
public void setWidth(Integer width) {
this.width = width;
}
public Integer getHeight() {
return height;
}
public void setHeight(Integer height) {
this.height = height;
}
public String getHtml() {
return html;
}
public void setHtml(String html) {
this.html = html;
}
public String getRawJson() {
return rawJson;
}
public void setRawJson(String rawJson) {
this.rawJson = rawJson;
}
@Override
public String toString() {
return "OEmbedResponse{" +
"type='" + type + '\'' +
", version='" + version + '\'' +
", title='" + title + '\'' +
", authorName='" + authorName + '\'' +
", providerName='" + providerName + '\'' +
", thumbnailUrl='" + thumbnailUrl + '\'' +
", width=" + width +
", height=" + height +
'}';
}
}
}

View File

@@ -1,107 +0,0 @@
package com.caliverse.video.global.util;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
/**
* URL 분석을 위한 유틸리티 클래스입니다.
*/
public class UrlParser {
// 서명된 URL에 포함될 수 있는 파라미터 목록
private static final List<String> SIGNATURE_PARAMS = Arrays.asList(
"token", "signature", "sig", "expires", "exp", "auth",
"key", "access_token", "api_key", "hmac", "hash"
);
// 스트리밍 서비스의 URL 패턴 목록
private static final List<String> STREAMING_DOMAINS = Arrays.asList(
"vimeo.com", "dailymotion.com",
"netflix.com", "hulu.com", "amazon.com/video", "player.vimeo.com",
"twitch.tv", "hls.", ".m3u8"
);
/**
* URL에서 파일 확장자를 추출합니다.
*
* @param url 분석할 URL 문자열
* @return 파일 확장자, 없으면 null
*/
public String getExtension(String url) {
if (url == null || url.trim().isEmpty()) {
return null;
}
try {
URL urlObj = new URL(url);
String path = urlObj.getPath();
if (path != null && path.contains(".")) {
int lastDotIndex = path.lastIndexOf(".");
int queryIndex = path.indexOf("?", lastDotIndex);
if (queryIndex > 0) {
return path.substring(lastDotIndex + 1, queryIndex);
} else {
return path.substring(lastDotIndex + 1);
}
}
} catch (MalformedURLException e) {
// URL 파싱 실패 시 null 반환
}
return null;
}
/**
* URL이 서명된 URL인지 확인합니다.
*
* @param url 분석할 URL 문자열
* @return 서명된 URL이면 true, 아니면 false
*/
public boolean hasSignatureParameters(String url) {
if (url == null || !url.contains("?")) {
return false;
}
String lowerUrl = url.toLowerCase();
String queryPart = lowerUrl.substring(lowerUrl.indexOf('?') + 1);
String[] params = queryPart.split("&");
for (String param : params) {
for (String sigParam : SIGNATURE_PARAMS) {
if (param.startsWith(sigParam + "=") || param.contains("=" + sigParam)) {
return true;
}
}
}
return false;
}
/**
* URL이 스트리밍 서비스 URL인지 확인합니다.
*
* @param url 분석할 URL 문자열
* @return 스트리밍 URL이면 true, 아니면 false
*/
public boolean isStreamingUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
String lowerUrl = url.toLowerCase();
for (String domain : STREAMING_DOMAINS) {
if (lowerUrl.contains(domain)) {
return true;
}
}
// HLS 스트리밍 확인
return lowerUrl.contains("m3u8") || lowerUrl.contains("mpd") || lowerUrl.contains("manifest");
}
}

View File

@@ -1,349 +0,0 @@
package com.video.analyzer;
import com.caliverse.video.global.util.OEmbedFetcher;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* OEmbedFetcher 테스트 클래스입니다.
*
* 실제 API를 호출하여 oEmbed 데이터를 가져오는 테스트를 수행합니다.
*/
public class OEmbedFetcherTest {
private OEmbedFetcher oembedFetcher;
@Before
public void setUp() {
oembedFetcher = new OEmbedFetcher();
}
@Test
public void testFetchOEmbed_NaverTV() {
// 네이버TV URL 테스트
String naverUrl = "https://tv.naver.com/v/40687083";
try {
OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(naverUrl);
assertNotNull("응답이 null이 아니어야 합니다", response);
assertNotNull("타입이 있어야 합니다", response.getType());
assertNotNull("제목이 있어야 합니다", response.getTitle());
assertNotNull("제공자 이름이 있어야 합니다", response.getProviderName());
// 결과 출력
System.out.println("=== 네이버TV oEmbed 결과 ===");
System.out.println("타입: " + response.getType());
System.out.println("제목: " + response.getTitle());
System.out.println("작성자: " + response.getAuthorName());
System.out.println("제공자: " + response.getProviderName());
System.out.println("썸네일 URL: " + response.getThumbnailUrl());
System.out.println("썸네일 크기: " + response.getThumbnailWidth() + "x" + response.getThumbnailHeight());
System.out.println("비디오 크기: " + response.getWidth() + "x" + response.getHeight());
System.out.println("\n원본 JSON:");
System.out.println(response.getRawJson());
} catch (Exception e) {
System.err.println("네이버TV oEmbed 테스트 실패: " + e.getMessage());
e.printStackTrace();
}
}
// @Test
// public void testFetchOEmbed_NaverTV() {
// // 네이버TV URL 테스트
// String naverUrl = "https://tv.naver.com/v/40687083";
//
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(naverUrl);
//
// assertNotNull("응답이 null이 아니어야 합니다", response);
// assertNotNull("타입이 있어야 합니다", response.getType());
// assertNotNull("제목이 있어야 합니다", response.getTitle());
// assertNotNull("제공자 이름이 있어야 합니다", response.getProviderName());
//
// // 결과 출력
// System.out.println("=== 네이버TV oEmbed 결과 ===");
// System.out.println("타입: " + response.getType());
// System.out.println("제목: " + response.getTitle());
// System.out.println("작성자: " + response.getAuthorName());
// System.out.println("제공자: " + response.getProviderName());
// System.out.println("썸네일 URL: " + response.getThumbnailUrl());
// System.out.println("썸네일 크기: " + response.getThumbnailWidth() + "x" + response.getThumbnailHeight());
// System.out.println("비디오 크기: " + response.getWidth() + "x" + response.getHeight());
// System.out.println("\n원본 JSON:");
// System.out.println(response.getRawJson());
//
// } catch (Exception e) {
// System.err.println("네이버TV oEmbed 테스트 실패: " + e.getMessage());
// e.printStackTrace();
// }
// }
//
// @Test
// public void testFetchOEmbed_NaverTVLive() {
// // 네이버TV URL 테스트
// String naverUrl = "https://tv.naver.com/l/171651";
//
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(naverUrl);
//
// assertNotNull("응답이 null이 아니어야 합니다", response);
// assertNotNull("타입이 있어야 합니다", response.getType());
// assertNotNull("제목이 있어야 합니다", response.getTitle());
// assertNotNull("제공자 이름이 있어야 합니다", response.getProviderName());
//
// // 결과 출력
// System.out.println("=== 네이버TV 라이브 oEmbed 결과 ===");
// System.out.println("타입: " + response.getType());
// System.out.println("제목: " + response.getTitle());
// System.out.println("작성자: " + response.getAuthorName());
// System.out.println("제공자: " + response.getProviderName());
// System.out.println("썸네일 URL: " + response.getThumbnailUrl());
// System.out.println("썸네일 크기: " + response.getThumbnailWidth() + "x" + response.getThumbnailHeight());
// System.out.println("비디오 크기: " + response.getWidth() + "x" + response.getHeight());
// System.out.println("\n원본 JSON:");
// System.out.println(response.getRawJson());
//
// } catch (Exception e) {
// System.err.println("네이버TV 라이브 oEmbed 테스트 실패: " + e.getMessage());
// e.printStackTrace();
// }
// }
//
// @Test
// public void testFetchOEmbed_NaverTVShort() {
// // 네이버TV 쇼츠 URL 테스트
// // oEmbed API가 빈 응답을 반환하면 fallback으로 embed URL을 생성합니다.
// String naverUrl = "https://tv.naver.com/h/87349660";
//
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(naverUrl);
//
// // fallback 응답이 생성되어야 함
// assertNotNull("응답이 null이 아니어야 합니다", response);
// assertNotNull("타입이 있어야 합니다", response.getType());
// assertNotNull("제공자 이름이 있어야 합니다", response.getProviderName());
// assertEquals("타입은 video여야 합니다", "video", response.getType());
// assertEquals("제공자는 NAVERTV여야 합니다", "NAVERTV", response.getProviderName());
//
// // HTML에 embed URL이 포함되어 있어야 함
// assertNotNull("HTML이 있어야 합니다", response.getHtml());
// assertTrue("HTML에 embed URL이 포함되어야 합니다",
// response.getHtml().contains("https://tv.naver.com/embed/87349660"));
//
// // 결과 출력
// System.out.println("=== 네이버TV 쇼츠 oEmbed 결과 (fallback) ===");
// System.out.println("타입: " + response.getType());
// System.out.println("제목: " + response.getTitle());
// System.out.println("작성자: " + response.getAuthorName());
// System.out.println("제공자: " + response.getProviderName());
// System.out.println("썸네일 URL: " + response.getThumbnailUrl());
// System.out.println("썸네일 크기: " + response.getThumbnailWidth() + "x" + response.getThumbnailHeight());
// System.out.println("비디오 크기: " + response.getWidth() + "x" + response.getHeight());
// System.out.println("HTML: " + response.getHtml());
// System.out.println("\n원본 JSON:");
// System.out.println(response.getRawJson());
//
// } catch (Exception e) {
// System.err.println("네이버TV 쇼츠 oEmbed 테스트 실패: " + e.getMessage());
// e.printStackTrace();
// fail("예외가 발생하면 안됩니다: " + e.getMessage());
// }
// }
//
// @Test
// public void testFetchOEmbed_NaverTVShare() {
// // 네이버TV 단축 URL 테스트
// // naver.me URL은 쇼츠(/h/) 형식으로 리다이렉트되며,
// // fallback으로 embed URL을 생성합니다.
// String naverUrl = "https://naver.me/ID3ODX2j";
//
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(naverUrl);
//
// // fallback 응답이 생성되어야 함
// assertNotNull("응답이 null이 아니어야 합니다", response);
// assertNotNull("타입이 있어야 합니다", response.getType());
// assertNotNull("제공자 이름이 있어야 합니다", response.getProviderName());
// assertEquals("타입은 video여야 합니다", "video", response.getType());
//
// // 결과 출력
// System.out.println("=== 네이버TV 공유 oEmbed 결과 ===");
// System.out.println("타입: " + response.getType());
// System.out.println("제목: " + response.getTitle());
// System.out.println("작성자: " + response.getAuthorName());
// System.out.println("제공자: " + response.getProviderName());
// System.out.println("썸네일 URL: " + response.getThumbnailUrl());
// System.out.println("썸네일 크기: " + response.getThumbnailWidth() + "x" + response.getThumbnailHeight());
// System.out.println("비디오 크기: " + response.getWidth() + "x" + response.getHeight());
// System.out.println("HTML: " + response.getHtml());
// System.out.println("\n원본 JSON:");
// System.out.println(response.getRawJson());
//
// } catch (Exception e) {
// System.err.println("네이버TV 공유 oEmbed 테스트 실패: " + e.getMessage());
// e.printStackTrace();
// fail("예외가 발생하면 안됩니다: " + e.getMessage());
// }
// }
//
// @Test
// public void testFetchOEmbed_YouTube() {
// // YouTube URL 테스트
// String youtubeUrl = "https://www.youtube.com/watch?v=dQw4w9WgXcQ";
//
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(youtubeUrl);
//
// assertNotNull("응답이 null이 아니어야 합니다", response);
// assertNotNull("타입이 있어야 합니다", response.getType());
// assertNotNull("제목이 있어야 합니다", response.getTitle());
//
// // 결과 출력
// System.out.println("\n=== YouTube oEmbed 결과 ===");
// System.out.println("타입: " + response.getType());
// System.out.println("제목: " + response.getTitle());
// System.out.println("작성자: " + response.getAuthorName());
// System.out.println("제공자: " + response.getProviderName());
// System.out.println("썸네일 URL: " + response.getThumbnailUrl());
// System.out.println("썸네일 크기: " + response.getThumbnailWidth() + "x" + response.getThumbnailHeight());
// System.out.println("비디오 크기: " + response.getWidth() + "x" + response.getHeight());
// System.out.println("\n원본 JSON:");
// System.out.println(response.getRawJson());
//
// } catch (Exception e) {
// System.err.println("YouTube oEmbed 테스트 실패: " + e.getMessage());
// e.printStackTrace();
// }
// }
//
// @Test
// public void testFetchOEmbed_YouTubeShortUrl() {
// // YouTube 단축 URL 테스트
// String youtubeShortUrl = "https://youtu.be/dQw4w9WgXcQ";
//
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(youtubeShortUrl);
//
// assertNotNull("응답이 null이 아니어야 합니다", response);
// assertNotNull("타입이 있어야 합니다", response.getType());
//
// // 결과 출력
// System.out.println("\n=== YouTube 단축 URL oEmbed 결과 ===");
// System.out.println(response);
//
// } catch (Exception e) {
// System.err.println("YouTube 단축 URL oEmbed 테스트 실패: " + e.getMessage());
// e.printStackTrace();
// }
// }
//
// @Test
// public void testFetchOEmbed_Vimeo() {
// // Vimeo URL 테스트
// String vimeoUrl = "https://vimeo.com/76979871";
//
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(vimeoUrl);
//
// assertNotNull("응답이 null이 아니어야 합니다", response);
// assertNotNull("타입이 있어야 합니다", response.getType());
// assertNotNull("제목이 있어야 합니다", response.getTitle());
//
// // 결과 출력
// System.out.println("\n=== Vimeo oEmbed 결과 ===");
// System.out.println("타입: " + response.getType());
// System.out.println("제목: " + response.getTitle());
// System.out.println("작성자: " + response.getAuthorName());
// System.out.println("제공자: " + response.getProviderName());
// System.out.println("썸네일 URL: " + response.getThumbnailUrl());
// System.out.println("썸네일 크기: " + response.getThumbnailWidth() + "x" + response.getThumbnailHeight());
// System.out.println("비디오 크기: " + response.getWidth() + "x" + response.getHeight());
// System.out.println("\n원본 JSON:");
// System.out.println(response.getRawJson());
//
// } catch (Exception e) {
// System.err.println("Vimeo oEmbed 테스트 실패: " + e.getMessage());
// e.printStackTrace();
// }
// }
//
// @Test
// public void testFetchOEmbed_UnsupportedPlatform() {
// // 지원하지 않는 플랫폼 테스트
// String unsupportedUrl = "https://example.com/video";
//
// try {
// oembedFetcher.fetchOEmbedData(unsupportedUrl);
// fail("지원하지 않는 플랫폼에 대해 예외가 발생해야 합니다");
// } catch (IllegalArgumentException e) {
// // 예상된 예외
// assertTrue("지원하지 않는 플랫폼 메시지를 포함해야 합니다",
// e.getMessage().contains("지원하지 않는 플랫폼"));
// System.out.println("\n지원하지 않는 플랫폼 예외 처리 확인: " + e.getMessage());
// } catch (Exception e) {
// fail("IllegalArgumentException이 발생해야 합니다: " + e.getClass().getName());
// }
// }
//
// @Test
// public void testFetchOEmbed_MultipleNaverTVVideos() {
// // 여러 네이버TV URL 테스트
// String[] naverUrls = {
// "https://tv.naver.com/v/40687083",
// "https://tv.naver.com/v/84373511",
// "https://tv.naver.com/v/12345678" // 존재하지 않을 수 있는 URL
// };
//
// System.out.println("\n=== 여러 네이버TV URL 테스트 ===");
// for (String url : naverUrls) {
// try {
// OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(url);
// System.out.println("\nURL: " + url);
// System.out.println("제목: " + response.getTitle());
// System.out.println("작성자: " + response.getAuthorName());
// System.out.println("썸네일: " + response.getThumbnailUrl());
// System.out.println("성공!");
// } catch (Exception e) {
// System.err.println("\nURL: " + url);
// System.err.println("실패: " + e.getMessage());
// }
// }
// }
@Test
public void testOEmbedResponse_AllFields() {
// 네이버TV URL로 모든 필드 확인
String naverUrl = "https://tv.naver.com/v/40687083";
try {
OEmbedFetcher.OEmbedResponse response = oembedFetcher.fetchOEmbedData(naverUrl);
System.out.println("\n=== oEmbed 응답 모든 필드 확인 ===");
System.out.println("Type: " + response.getType());
System.out.println("Version: " + response.getVersion());
System.out.println("Title: " + response.getTitle());
System.out.println("Author Name: " + response.getAuthorName());
System.out.println("Author URL: " + response.getAuthorUrl());
System.out.println("Provider Name: " + response.getProviderName());
System.out.println("Provider URL: " + response.getProviderUrl());
System.out.println("Thumbnail URL: " + response.getThumbnailUrl());
System.out.println("Thumbnail Width: " + response.getThumbnailWidth());
System.out.println("Thumbnail Height: " + response.getThumbnailHeight());
System.out.println("Width: " + response.getWidth());
System.out.println("Height: " + response.getHeight());
System.out.println("HTML: " + (response.getHtml() != null ? response.getHtml().substring(0, Math.min(100, response.getHtml().length())) + "..." : "null"));
// 필수 필드 확인
assertNotNull("Title은 null이 아니어야 합니다", response.getTitle());
assertNotNull("Provider Name은 null이 아니어야 합니다", response.getProviderName());
} catch (Exception e) {
System.err.println("테스트 실패: " + e.getMessage());
e.printStackTrace();
}
}
}

View File

@@ -1,97 +0,0 @@
package com.video.analyzer;
import com.caliverse.video.analyzer.model.VideoAnalysisResult;
import com.caliverse.video.analyzer.platform.PlatformAnalyzer;
import com.caliverse.video.analyzer.platform.PlatformAnalyzerFactory;
import com.caliverse.video.analyzer.platform.impl.NaverTvPlatformAnalyzer;
import com.caliverse.video.analyzer.service.VideoAnalyzerService;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* PlatformAnalyzer와 관련된 테스트 클래스입니다.
*/
public class PlatformAnalyzerTest {
private VideoAnalyzerService videoAnalyzerService;
private NaverTvPlatformAnalyzer naverTvAnalyzer;
private PlatformAnalyzerFactory factory;
@Before
public void setUp() {
videoAnalyzerService = new VideoAnalyzerService();
naverTvAnalyzer = new NaverTvPlatformAnalyzer(videoAnalyzerService);
factory = new PlatformAnalyzerFactory(videoAnalyzerService);
}
@Test
public void testNaverTvAnalyzer_CanHandle() {
// 네이버TV URL 처리 가능 여부 확인
assertTrue("네이버TV URL을 처리할 수 있어야 합니다",
naverTvAnalyzer.canHandle("https://tv.naver.com/v/123"));
assertTrue("네이버 공유 URL을 처리할 수 있어야 합니다",
naverTvAnalyzer.canHandle("https://naver.me/FK09F4E7"));
assertTrue("대소문자 구분 없이 처리할 수 있어야 합니다",
naverTvAnalyzer.canHandle("https://TV.NAVER.COM/v/456"));
assertFalse("null URL은 처리할 수 없어야 합니다",
naverTvAnalyzer.canHandle(null));
assertFalse("다른 도메인은 처리할 수 없어야 합니다",
naverTvAnalyzer.canHandle("https://youtube.com/watch?v=123"));
}
@Test
public void testNaverTvAnalyzer_GetPlatformName() {
// 플랫폼 이름 확인
assertEquals("플랫폼 이름은 '네이버TV'여야 합니다",
"네이버TV", naverTvAnalyzer.getPlatformName());
}
@Test
public void testNaverTvAnalyzer_Analyze_ReturnsResult() {
// 네이버TV URL 분석 시 VideoAnalysisResult 반환 확인
String naverUrl = "https://tv.naver.com/v/84373511";
VideoAnalysisResult result = naverTvAnalyzer.analyze(naverUrl);
assertNotNull("분석 결과는 null이 아니어야 합니다", result);
assertNotNull("success 값은 null이 아니어야 합니다", result.isSuccess());
assertNotNull("reason은 null이 아니어야 합니다", result.getReason());
// 결과 출력 (디버깅용)
System.out.println("네이버TV 분석 결과: " + result);
}
@Test
public void testFactory_FindAnalyzer_NaverTv() {
// 네이버TV URL에 대한 분석기 찾기
PlatformAnalyzer naverAnalyzer = factory.findAnalyzer("https://tv.naver.com/v/123");
assertNotNull("네이버TV 분석기를 찾아야 합니다", naverAnalyzer);
assertEquals("플랫폼 이름은 '네이버TV'여야 합니다",
"네이버TV", naverAnalyzer.getPlatformName());
}
@Test
public void testFactory_FindAnalyzer_UnknownUrl() {
// 알 수 없는 URL에 대한 분석기 찾기
PlatformAnalyzer unknownAnalyzer = factory.findAnalyzer("https://example.com/video.mp4");
assertNull("알 수 없는 URL은 null을 반환해야 합니다", unknownAnalyzer);
}
@Test
public void testFactory_GetAllAnalyzers() {
var analyzers = factory.getAllAnalyzers();
assertNotNull("분석기 목록은 null이 아니어야 합니다", analyzers);
assertFalse("최소 1개 이상의 분석기가 있어야 합니다", analyzers.isEmpty());
// 네이버TV 분석기가 포함되어 있는지 확인
boolean hasNaverTv = analyzers.stream()
.anyMatch(analyzer -> "네이버TV".equals(analyzer.getPlatformName()));
assertTrue("네이버TV 분석기가 포함되어 있어야 합니다", hasNaverTv);
}
}

View File

@@ -1,130 +0,0 @@
package com.video.analyzer;
import com.caliverse.video.analyzer.VideoAnalyzer;
import com.caliverse.video.analyzer.model.VideoAnalysisResult;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* VideoAnalyzer의 통합 테스트 클래스입니다.
*/
public class VideoAnalyzerTest {
private VideoAnalyzer analyzer;
@Before
public void setUp() {
analyzer = new VideoAnalyzer();
}
@Test
public void testAnalyzeUrl_Success() {
// 재생 가능한 일반 비디오 URL (mp4)
String videoUrl = "https://example.com/video.mp4";
VideoAnalysisResult result = analyzer.analyzeUrl(videoUrl);
assertNotNull("결과는 null이 아니어야 합니다", result);
assertNotNull("reason은 null이 아니어야 합니다", result.getReason());
}
@Test
public void testAnalyzeUrl_NaverTv() {
// 네이버TV URL
String naverUrl = "https://tv.naver.com/v/84373511";
VideoAnalysisResult result = analyzer.analyzeUrl(naverUrl);
assertNotNull("결과는 null이 아니어야 합니다", result);
assertNotNull("reason은 null이 아니어야 합니다", result.getReason());
// 결과 출력 (디버깅용)
System.out.println("네이버TV 분석 결과: " + result);
}
@Test
public void testAnalyzeUrl_Naver() {
// 네이버TV URL
String naverUrl = "https://naver.me/ID3ODX2j";
VideoAnalysisResult result = analyzer.analyzeUrl(naverUrl);
assertNotNull("결과는 null이 아니어야 합니다", result);
assertNotNull("reason은 null이 아니어야 합니다", result.getReason());
// 결과 출력 (디버깅용)
System.out.println("네이버 공유 분석 결과: " + result);
}
@Test
public void testAnalyzeUrl_NaverTv2() {
// 네이버TV URL
String naverUrl = "https://tv.naver.com/h/87349660";
VideoAnalysisResult result = analyzer.analyzeUrl(naverUrl);
assertNotNull("결과는 null이 아니어야 합니다", result);
assertNotNull("reason은 null이 아니어야 합니다", result.getReason());
// 결과 출력 (디버깅용)
System.out.println("네이버 숏츠 분석 결과: " + result);
}
@Test
public void testAnalyzeUrl_NaverTvLive() {
// 네이버TV URL
String naverUrl = "https://tv.naver.com/l/171651";
VideoAnalysisResult result = analyzer.analyzeUrl(naverUrl);
assertNotNull("결과는 null이 아니어야 합니다", result);
assertNotNull("reason은 null이 아니어야 합니다", result.getReason());
// 결과 출력 (디버깅용)
System.out.println("네이버 라이브 분석 결과: " + result);
}
@Test
public void testSupportedPlatforms() {
String[] platforms = analyzer.getSupportedPlatforms();
assertNotNull("플랫폼 목록은 null이 아니어야 합니다", platforms);
assertTrue("최소 1개 이상의 플랫폼을 지원해야 합니다", platforms.length > 0);
// 기본 플랫폼 확인
boolean hasNaverTv = false;
for (String platform : platforms) {
if ("네이버TV".equals(platform)) {
hasNaverTv = true;
break;
}
}
assertTrue("네이버TV를 지원해야 합니다", hasNaverTv);
}
@Test
public void testAnalyzeUrl_Result_HasProperties() {
// VideoAnalysisResult가 올바른 속성을 가지고 있는지 확인
String testUrl = "https://example.com/test.mp4";
VideoAnalysisResult result = analyzer.analyzeUrl(testUrl);
// result 객체가 올바른 메소드를 가지고 있는지 확인
assertNotNull(result);
String reason = result.getReason();
assertNotNull("reason은 항상 값이 있어야 합니다", reason);
assertFalse("reason은 빈 문자열이 아니어야 합니다", reason.trim().isEmpty());
// toString()이 정상 작동하는지 확인
String resultString = result.toString();
assertNotNull(resultString);
assertTrue("toString()은 'VideoAnalysisResult'를 포함해야 합니다",
resultString.contains("VideoAnalysisResult"));
}
}

Binary file not shown.

View File

@@ -1,5 +0,0 @@
Manifest-Version: 1.0
Implementation-Title: Video URL Analyzer
Implementation-Version: 1.0.0
Main-Class: com.caliverse.analyzer.VideoAnalyzer