temp값 삭제
This commit is contained in:
@@ -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
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.gradle/
|
||||
.idea/
|
||||
.claude/
|
||||
backup/
|
||||
bin/
|
||||
build/
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
#Thu Nov 06 15:53:40 KST 2025
|
||||
gradle.version=8.1.1
|
||||
Binary file not shown.
Binary file not shown.
1
.idea/.name
generated
1
.idea/.name
generated
@@ -1 +0,0 @@
|
||||
video-url-analyzer
|
||||
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@@ -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
16
.idea/gradle.xml
generated
@@ -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
8
.idea/misc.xml
generated
@@ -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
6
.idea/vcs.xml
generated
@@ -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>
|
||||
@@ -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()
|
||||
*
|
||||
* @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>
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.caliverse.video.analyzer.entity;
|
||||
|
||||
public enum UrlType {
|
||||
YOUTUBE,
|
||||
BLOB,
|
||||
STREAMING,
|
||||
DIRECT_FILE,
|
||||
NAVER_TV,
|
||||
UNKNOWN
|
||||
|
||||
}
|
||||
@@ -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 + " }";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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/";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
Manifest-Version: 1.0
|
||||
Implementation-Title: Video URL Analyzer
|
||||
Implementation-Version: 1.0.0
|
||||
Main-Class: com.caliverse.analyzer.VideoAnalyzer
|
||||
|
||||
Reference in New Issue
Block a user