diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e65f3da..67269d6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,10 @@ "permissions": { "allow": [ "PowerShell($env:JAVA_HOME=\"D:\\\\Development\\\\app\\\\JDK\\\\jdk-21.0.5\"; .\\\\gradlew.bat compileJava --console=plain 2>&1 | Select-Object -Last 15)", - "PowerShell($env:JAVA_HOME=\"D:\\\\Development\\\\app\\\\JDK\\\\jdk-21.0.5\"; .\\\\gradlew.bat clean compileJava --console=plain 2>&1 | Select-Object -Last 20)" + "PowerShell($env:JAVA_HOME=\"D:\\\\Development\\\\app\\\\JDK\\\\jdk-21.0.5\"; .\\\\gradlew.bat clean compileJava --console=plain 2>&1 | Select-Object -Last 20)", + "Bash(git rm *)", + "Bash(git add *)", + "Bash(git -c user.name=hehih -c user.email=hehihoho86@gmail.com commit -q -m 'Retire Opal/YtVideo pipeline; ChannelVideo is the single video master *)" ] } } diff --git a/src/main/java/com/hlab/yanalyst/domain/opal/OpalDraft.java b/src/main/java/com/hlab/yanalyst/domain/opal/OpalDraft.java deleted file mode 100644 index a7965f5..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/opal/OpalDraft.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.hlab.yanalyst.domain.opal; - -import com.hlab.yanalyst.domain.video.YtVideo; -import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.Type; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "opal_draft") -@Getter -@Setter -@NoArgsConstructor -public class OpalDraft { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "draft_id") - private Long draftId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "video_id", nullable = false) - private YtVideo video; - - @Type(JsonBinaryType.class) - @Column(name = "request_payload_json", columnDefinition = "jsonb") - private String requestPayloadJson; - - @Column(name = "response_text", columnDefinition = "TEXT") - private String responseText; - - @Column(name = "old_script_summary", columnDefinition = "TEXT") - private String oldScriptSummary; - - @Column(name = "new_script_summary", columnDefinition = "TEXT") - private String newScriptSummary; - - @Column(name = "user_feedback", columnDefinition = "TEXT") - private String userFeedback; - - @Column(name = "version_no", nullable = false) - private Integer versionNo; - - @Column(name = "is_accepted", nullable = false) - private Boolean isAccepted = false; - - @Column(name = "accepted_at") - private LocalDateTime acceptedAt; - - @Column(name = "status", nullable = false, length = 20) - private String status = "SUCCESS"; - - @Column(name = "error_msg", columnDefinition = "TEXT") - private String errorMsg; - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; -} diff --git a/src/main/java/com/hlab/yanalyst/domain/opal/OpalDraftRepository.java b/src/main/java/com/hlab/yanalyst/domain/opal/OpalDraftRepository.java deleted file mode 100644 index f738de4..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/opal/OpalDraftRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.hlab.yanalyst.domain.opal; - -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface OpalDraftRepository extends JpaRepository { - List findByVideo_VideoIdOrderByVersionNoDesc(Long videoId); - Integer countByVideo_VideoId(Long videoId); -} diff --git a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinal.java b/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinal.java deleted file mode 100644 index 8db79a3..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinal.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.hlab.yanalyst.domain.opal; - -import com.hlab.yanalyst.domain.video.YtVideo; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "opal_final") -@Getter -@Setter -@NoArgsConstructor -public class OpalFinal { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "final_id") - private Long finalId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "video_id", nullable = false) - private YtVideo video; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "draft_id", nullable = false) - private OpalDraft draft; - - @Column(name = "final_script_text", nullable = false, columnDefinition = "TEXT") - private String finalScriptText; - - @Column(name = "is_active", nullable = false) - private Boolean isActive = false; - - @CreationTimestamp - @Column(name = "finalized_at", nullable = false) - private LocalDateTime finalizedAt; - - @Column(name = "status", nullable = false, length = 20) - private String status = "FINALIZED"; -} diff --git a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalAsset.java b/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalAsset.java deleted file mode 100644 index 65f29d9..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalAsset.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.hlab.yanalyst.domain.opal; - -import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.Type; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "opal_final_asset") -@Getter -@Setter -@NoArgsConstructor -public class OpalFinalAsset { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "asset_id") - private Long assetId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "final_id", nullable = false) - private OpalFinal opalFinal; - - @Type(JsonBinaryType.class) - @Column(name = "asset_json", nullable = false, columnDefinition = "jsonb") - private String assetJson; - - @Column(name = "title", columnDefinition = "TEXT") - private String title; - - @Column(name = "summary", columnDefinition = "TEXT") - private String summary; - - @Type(JsonBinaryType.class) - @Column(name = "timeline", columnDefinition = "jsonb") - private String timeline; - - @Column(name = "video_prompt", columnDefinition = "TEXT") - private String videoPrompt; - - @Type(JsonBinaryType.class) - @Column(name = "image_urls", columnDefinition = "jsonb") - private String imageUrls; - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; -} diff --git a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalAssetRepository.java b/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalAssetRepository.java deleted file mode 100644 index c7919b4..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalAssetRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.hlab.yanalyst.domain.opal; - -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; - -public interface OpalFinalAssetRepository extends JpaRepository { - Optional findByOpalFinal_FinalId(Long finalId); -} diff --git a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalRepository.java b/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalRepository.java deleted file mode 100644 index 4010352..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/opal/OpalFinalRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.hlab.yanalyst.domain.opal; - -import org.springframework.data.jpa.repository.JpaRepository; -import java.util.Optional; -import java.util.List; - -public interface OpalFinalRepository extends JpaRepository { - Optional findByVideo_VideoIdAndIsActiveTrue(Long videoId); - List findByVideo_VideoId(Long videoId); -} diff --git a/src/main/java/com/hlab/yanalyst/domain/opal/dto/OpalDraftResponseDto.java b/src/main/java/com/hlab/yanalyst/domain/opal/dto/OpalDraftResponseDto.java deleted file mode 100644 index 7f99130..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/opal/dto/OpalDraftResponseDto.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.hlab.yanalyst.domain.opal.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class OpalDraftResponseDto { - private String oldScriptSummary; - private String newScriptSummary; -} diff --git a/src/main/java/com/hlab/yanalyst/domain/script/ScriptGen.java b/src/main/java/com/hlab/yanalyst/domain/script/ScriptGen.java deleted file mode 100644 index de84cad..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/script/ScriptGen.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.hlab.yanalyst.domain.script; - -import com.hlab.yanalyst.domain.video.YtVideo; -import io.hypersistence.utils.hibernate.type.json.JsonBinaryType; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.Type; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "script_gen") -@Getter -@Setter -@NoArgsConstructor -public class ScriptGen { - - @Id - @Column(name = "video_id") - private Long videoId; - - @OneToOne(fetch = FetchType.LAZY) - @MapsId - @JoinColumn(name = "video_id") - private YtVideo video; - - @Type(JsonBinaryType.class) - @Column(name = "request_payload_json", columnDefinition = "jsonb") - private String requestPayloadJson; - - @Column(name = "response_text", nullable = false, columnDefinition = "TEXT") - private String responseText; - - @Column(name = "model_name", length = 100) - private String modelName; - - @Column(name = "latency_ms") - private Integer latencyMs; - - @Column(name = "status", nullable = false, length = 20) - private String status = "SUCCESS"; - - @Column(name = "error_msg", columnDefinition = "TEXT") - private String errorMsg; - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; -} diff --git a/src/main/java/com/hlab/yanalyst/domain/script/ScriptGenRepository.java b/src/main/java/com/hlab/yanalyst/domain/script/ScriptGenRepository.java deleted file mode 100644 index c3d7a35..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/script/ScriptGenRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.hlab.yanalyst.domain.script; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ScriptGenRepository extends JpaRepository { -} diff --git a/src/main/java/com/hlab/yanalyst/domain/video/YtVideo.java b/src/main/java/com/hlab/yanalyst/domain/video/YtVideo.java deleted file mode 100644 index 582352c..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/video/YtVideo.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.hlab.yanalyst.domain.video; - -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -@Entity -@Table(name = "yt_video") -@Getter -@Setter -@NoArgsConstructor -public class YtVideo { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "video_id") - private Long videoId; - - @Column(name = "youtube_video_id", nullable = false, unique = true, length = 32) - private String youtubeVideoId; - - @Column(name = "video_url", nullable = false, columnDefinition = "TEXT") - private String videoUrl; - - @Column(name = "opening_script", columnDefinition = "TEXT") - private String openingScript; - - @Column(name = "title", columnDefinition = "TEXT") - private String title; - - @Column(name = "channel_title", columnDefinition = "TEXT") - private String channelTitle; - - @Column(name = "published_at") - private LocalDateTime publishedAt; - - @Column(name = "duration_sec") - private Integer durationSec; - - @Column(name = "view_count") - private Long viewCount; - - @Column(name = "like_count") - private Long likeCount; - - @Column(name = "comment_count") - private Long commentCount; - - @Column(name = "subscriber_count") - private Long subscriberCount; - - @Column(name = "views_per_hour", precision = 18, scale = 2) - private BigDecimal viewsPerHour; - - @Column(name = "views_per_sub_ratio", precision = 18, scale = 2) - private BigDecimal viewsPerSubRatio; - - @Column(name = "thumbnail_url", columnDefinition = "TEXT") - private String thumbnailUrl; - - @Column(name = "status", nullable = false, length = 30) - private String status = "CRAWLED"; - - @Column(name = "last_crawled_at", nullable = false) - private LocalDateTime lastCrawledAt = LocalDateTime.now(); - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; - - @Column(name = "is_completed") - private Boolean isCompleted = false; -} diff --git a/src/main/java/com/hlab/yanalyst/domain/video/YtVideoRepository.java b/src/main/java/com/hlab/yanalyst/domain/video/YtVideoRepository.java deleted file mode 100644 index 512167b..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/video/YtVideoRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.hlab.yanalyst.domain.video; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface YtVideoRepository extends JpaRepository, org.springframework.data.jpa.repository.JpaSpecificationExecutor { - Page findAllByOrderByLastCrawledAtDesc(Pageable pageable); - - java.util.Optional findByYoutubeVideoId(String youtubeVideoId); -} diff --git a/src/main/java/com/hlab/yanalyst/domain/video/dto/VideoDetailResponse.java b/src/main/java/com/hlab/yanalyst/domain/video/dto/VideoDetailResponse.java deleted file mode 100644 index 7e69b7f..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/video/dto/VideoDetailResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.hlab.yanalyst.domain.video.dto; - -import com.hlab.yanalyst.domain.video.YtVideo; -import lombok.Data; -import java.time.LocalDateTime; - -@Data -public class VideoDetailResponse { - private VideoListResponse metadata; - private String videoUrl; - private Integer durationSec; - private Long likeCount; - private Long commentCount; - private LocalDateTime publishedAt; - - public static VideoDetailResponse from(YtVideo video) { - VideoDetailResponse response = new VideoDetailResponse(); - response.setMetadata(VideoListResponse.from(video)); - response.setVideoUrl(video.getVideoUrl()); - response.setDurationSec(video.getDurationSec()); - response.setLikeCount(video.getLikeCount()); - response.setCommentCount(video.getCommentCount()); - response.setPublishedAt(video.getPublishedAt()); - return response; - } -} diff --git a/src/main/java/com/hlab/yanalyst/domain/video/dto/VideoListResponse.java b/src/main/java/com/hlab/yanalyst/domain/video/dto/VideoListResponse.java deleted file mode 100644 index b969b91..0000000 --- a/src/main/java/com/hlab/yanalyst/domain/video/dto/VideoListResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.hlab.yanalyst.domain.video.dto; - -import com.hlab.yanalyst.domain.video.YtVideo; -import lombok.Data; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -@Data -public class VideoListResponse { - private Long videoId; - private String youtubeVideoId; - private String title; - private String channelTitle; - private String thumbnailUrl; - private Long viewCount; - private Long subscriberCount; - private BigDecimal viewsPerSubRatio; - private BigDecimal viewsPerHour; - private String status; - private LocalDateTime lastCrawledAt; - - public static VideoListResponse from(YtVideo video) { - VideoListResponse response = new VideoListResponse(); - response.setVideoId(video.getVideoId()); - response.setYoutubeVideoId(video.getYoutubeVideoId()); - response.setTitle(video.getTitle()); - response.setChannelTitle(video.getChannelTitle()); - response.setThumbnailUrl(video.getThumbnailUrl()); - response.setViewCount(video.getViewCount()); - response.setSubscriberCount(video.getSubscriberCount()); - response.setViewsPerSubRatio(video.getViewsPerSubRatio()); - response.setViewsPerHour(video.getViewsPerHour()); - response.setStatus(video.getStatus()); - response.setLastCrawledAt(video.getLastCrawledAt()); - return response; - } -} diff --git a/src/main/java/com/hlab/yanalyst/service/AnalysisWorkflowService.java b/src/main/java/com/hlab/yanalyst/service/AnalysisWorkflowService.java deleted file mode 100644 index 308b838..0000000 --- a/src/main/java/com/hlab/yanalyst/service/AnalysisWorkflowService.java +++ /dev/null @@ -1,180 +0,0 @@ -package com.hlab.yanalyst.service; - -import com.hlab.yanalyst.domain.opal.*; -import com.hlab.yanalyst.domain.script.ScriptGen; -import com.hlab.yanalyst.domain.script.ScriptGenRepository; -import com.hlab.yanalyst.domain.video.YtVideo; -import com.hlab.yanalyst.domain.video.YtVideoRepository; -import com.hlab.yanalyst.service.external.ExternalApiService; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; - -@Service -@RequiredArgsConstructor -@Transactional -public class AnalysisWorkflowService { - - private final YtVideoRepository ytVideoRepository; - private final ScriptGenRepository scriptGenRepository; - private final OpalDraftRepository opalDraftRepository; - private final OpalFinalRepository opalFinalRepository; - private final OpalFinalAssetRepository opalFinalAssetRepository; - private final ExternalApiService externalApiService; - private final ObjectMapper objectMapper; - - public ScriptGen generateScript(Long videoId) { - YtVideo video = ytVideoRepository.findById(videoId) - .orElseThrow(() -> new IllegalArgumentException("Video not found")); - - String scriptContent = externalApiService.generateScript(video.getVideoUrl()); - - ScriptGen scriptGen = scriptGenRepository.findById(videoId).orElse(new ScriptGen()); - if (scriptGen.getVideoId() == null) { - scriptGen.setVideo(video); - } - scriptGen.setResponseText(scriptContent); - scriptGen.setStatus("SUCCESS"); - // Update video status - video.setStatus("SCRIPT_READY"); - - return scriptGenRepository.save(scriptGen); - } - - public OpalDraft generateDraft(Long videoId, String feedback, String mode) { - YtVideo video = ytVideoRepository.findById(videoId) - .orElseThrow(() -> new IllegalArgumentException("Video not found")); - - ScriptGen scriptGen = scriptGenRepository.findById(videoId) - .orElseThrow(() -> new IllegalArgumentException("Script not found")); - - var draftDto = externalApiService.generateOpalDraft(scriptGen.getResponseText(), feedback, mode); - - OpalDraft draft = new OpalDraft(); - draft.setVideo(video); - draft.setOldScriptSummary(draftDto.getOldScriptSummary()); - draft.setNewScriptSummary(draftDto.getNewScriptSummary()); - // For compatibility, we can concatenate or just leave responseText empty/null, or use New Summary as main response - draft.setResponseText(draftDto.getNewScriptSummary()); - draft.setUserFeedback(feedback); - - Integer maxVersion = opalDraftRepository.findByVideo_VideoIdOrderByVersionNoDesc(videoId) - .stream().findFirst().map(OpalDraft::getVersionNo).orElse(0); - draft.setVersionNo(maxVersion + 1); - draft.setStatus("SUCCESS"); - - video.setStatus("DRAFTING"); - - return opalDraftRepository.save(draft); - } - - public OpalFinal acceptDraft(Long videoId, Long draftId) { - YtVideo video = ytVideoRepository.findById(videoId).orElseThrow(); - OpalDraft draft = opalDraftRepository.findById(draftId).orElseThrow(); - - if (!draft.getVideo().getVideoId().equals(videoId)) { - throw new IllegalArgumentException("Draft does not match video"); - } - - // Deactivate existing active final - opalFinalRepository.findByVideo_VideoIdAndIsActiveTrue(videoId) - .ifPresent(existing -> { - existing.setIsActive(false); - opalFinalRepository.save(existing); - }); - - // Mark draft accepted - draft.setIsAccepted(true); - draft.setAcceptedAt(LocalDateTime.now()); - opalDraftRepository.save(draft); - - OpalFinal opalFinal = new OpalFinal(); - opalFinal.setVideo(video); - opalFinal.setDraft(draft); - opalFinal.setFinalScriptText(draft.getResponseText()); - opalFinal.setIsActive(true); - - return opalFinalRepository.save(opalFinal); - } - - public OpalFinalAsset generateFinalAsset(Long videoId) { - YtVideo video = ytVideoRepository.findById(videoId).orElseThrow(); - OpalFinal activeFinal = opalFinalRepository.findByVideo_VideoIdAndIsActiveTrue(videoId) - .orElse(null); - - if (activeFinal == null) { - OpalDraft latestDraft = opalDraftRepository.findByVideo_VideoIdOrderByVersionNoDesc(videoId) - .stream().findFirst() - .orElseThrow(() -> new IllegalArgumentException("No active final script and no drafts found. Please generate a draft first.")); - - activeFinal = new OpalFinal(); - activeFinal.setVideo(video); - activeFinal.setDraft(latestDraft); - activeFinal.setIsActive(true); - activeFinal.setFinalScriptText(""); - activeFinal = opalFinalRepository.save(activeFinal); - } - - // 1. Fetch updated final script from external source (GDoc) - String updatedFinalScript = externalApiService.fetchFinalScript(); - - // 2. Update and save active final script - activeFinal.setFinalScriptText(updatedFinalScript); - opalFinalRepository.save(activeFinal); - - // 3. Generate asset metadata - var assetMap = externalApiService.generateFinalAsset(activeFinal.getFinalScriptText()); - - OpalFinalAsset asset = new OpalFinalAsset(); - asset.setOpalFinal(activeFinal); - try { - asset.setAssetJson(objectMapper.writeValueAsString(assetMap)); - asset.setTitle((String) assetMap.get("title")); - asset.setSummary((String) assetMap.get("summary")); - asset.setTimeline(objectMapper.writeValueAsString(assetMap.get("timeline"))); - asset.setVideoPrompt((String) assetMap.get("video_prompt")); - asset.setImageUrls(objectMapper.writeValueAsString(assetMap.get("image_urls"))); - } catch (Exception e) { - throw new RuntimeException("JSON processing error", e); - } - - video.setStatus("FINALIZED"); - return opalFinalAssetRepository.save(asset); - } - public boolean toggleComplete(Long videoId) { - YtVideo video = ytVideoRepository.findById(videoId) - .orElseThrow(() -> new IllegalArgumentException("Video not found")); - - boolean newState = video.getIsCompleted() == null ? true : !video.getIsCompleted(); - video.setIsCompleted(newState); - ytVideoRepository.save(video); - return newState; - } - - public void updateFinalAsset(Long videoId, String newText) { - OpalFinal opalFinal = opalFinalRepository.findByVideo_VideoIdAndIsActiveTrue(videoId) - .orElseThrow(() -> new IllegalArgumentException("No active final asset found for videoId: " + videoId)); - opalFinal.setFinalScriptText(newText); - opalFinalRepository.save(opalFinal); - } - - public void generateOpening(Long videoId, String docId) { - YtVideo video = ytVideoRepository.findById(videoId) - .orElseThrow(() -> new IllegalArgumentException("Video not found")); - - String openingScript = externalApiService.fetchOpeningScript(docId); - video.setOpeningScript(openingScript); - ytVideoRepository.save(video); - } - - public void resetOpening(Long videoId) { - YtVideo video = ytVideoRepository.findById(videoId) - .orElseThrow(() -> new IllegalArgumentException("Video not found")); - video.setOpeningScript(null); - ytVideoRepository.save(video); - } -} diff --git a/src/main/java/com/hlab/yanalyst/service/YtVideoService.java b/src/main/java/com/hlab/yanalyst/service/YoutubeSearchService.java similarity index 55% rename from src/main/java/com/hlab/yanalyst/service/YtVideoService.java rename to src/main/java/com/hlab/yanalyst/service/YoutubeSearchService.java index 885f314..1f728ac 100644 --- a/src/main/java/com/hlab/yanalyst/service/YtVideoService.java +++ b/src/main/java/com/hlab/yanalyst/service/YoutubeSearchService.java @@ -1,210 +1,53 @@ package com.hlab.yanalyst.service; -import com.hlab.yanalyst.domain.opal.OpalDraftRepository; -import com.hlab.yanalyst.domain.opal.OpalFinal; -import com.hlab.yanalyst.domain.opal.OpalFinalAssetRepository; -import com.hlab.yanalyst.domain.opal.OpalFinalRepository; -import com.hlab.yanalyst.domain.script.ScriptGenRepository; -import com.hlab.yanalyst.domain.video.YtVideo; -import com.hlab.yanalyst.domain.video.YtVideoRepository; -import com.hlab.yanalyst.web.dto.*; -import jakarta.persistence.criteria.Predicate; +import com.fasterxml.jackson.databind.JsonNode; +import com.hlab.yanalyst.web.dto.YoutubeSearchCondition; +import com.hlab.yanalyst.web.dto.YoutubeSearchPageDto; +import com.hlab.yanalyst.web.dto.YoutubeSearchResultDto; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +/** + * YouTube Data API 검색 — 지역/채널/키워드/포맷(Shorts·롱폼)으로 영상을 찾아 + * 조회수·구독자·길이·해시태그까지 보강한 결과를 반환한다. + * 결과는 {@link com.hlab.yanalyst.domain.channel.SearchCollectionService} 를 통해 수집함(ChannelVideo)에 저장된다. + */ @Service @RequiredArgsConstructor -@Transactional(readOnly = true) -public class YtVideoService { +public class YoutubeSearchService { - private final YtVideoRepository ytVideoRepository; - private final ScriptGenRepository scriptGenRepository; - private final OpalDraftRepository opalDraftRepository; - private final OpalFinalRepository opalFinalRepository; - private final OpalFinalAssetRepository opalFinalAssetRepository; - private final org.springframework.web.client.RestTemplate restTemplate; - private final com.fasterxml.jackson.databind.ObjectMapper objectMapper; + private final RestTemplate restTemplate; - @org.springframework.beans.factory.annotation.Value("${youtube.api.key}") + @Value("${youtube.api.key}") private String youtubeApiKey; - @Transactional - public YtVideo saveVideoFromUrl(String url) { - String videoId = extractVideoId(url); - - // Check if exists - java.util.Optional existing = ytVideoRepository.findByYoutubeVideoId(videoId); - if (existing.isPresent()) { - throw new IllegalArgumentException("Video already exists with ID: " + videoId); - } - - String apiUrl = "https://www.googleapis.com/youtube/v3/videos"; - org.springframework.web.util.UriComponentsBuilder builder = org.springframework.web.util.UriComponentsBuilder.fromHttpUrl(apiUrl) - .queryParam("part", "snippet,statistics,contentDetails") - .queryParam("id", videoId) - .queryParam("key", youtubeApiKey); - - try { - com.fasterxml.jackson.databind.JsonNode root = restTemplate.getForObject(builder.toUriString(), com.fasterxml.jackson.databind.JsonNode.class); - com.fasterxml.jackson.databind.JsonNode items = root.path("items"); - - if (items.isEmpty()) { - throw new IllegalArgumentException("Video not found for ID: " + videoId); - } - - com.fasterxml.jackson.databind.JsonNode item = items.get(0); - com.fasterxml.jackson.databind.JsonNode snippet = item.get("snippet"); - com.fasterxml.jackson.databind.JsonNode statistics = item.get("statistics"); - com.fasterxml.jackson.databind.JsonNode contentDetails = item.get("contentDetails"); - - YtVideo video = new YtVideo(); - video.setYoutubeVideoId(videoId); - video.setVideoUrl("https://www.youtube.com/watch?v=" + videoId); - video.setTitle(snippet.get("title").asText()); - video.setChannelTitle(snippet.get("channelTitle").asText()); - video.setThumbnailUrl(snippet.get("thumbnails").has("maxres") - ? snippet.get("thumbnails").get("maxres").get("url").asText() - : snippet.get("thumbnails").get("high").get("url").asText()); - - video.setPublishedAt(LocalDateTime.parse(snippet.get("publishedAt").asText(), java.time.format.DateTimeFormatter.ISO_DATE_TIME)); - - if (statistics.has("viewCount")) video.setViewCount(Long.parseLong(statistics.get("viewCount").asText())); - if (statistics.has("likeCount")) video.setLikeCount(Long.parseLong(statistics.get("likeCount").asText())); - if (statistics.has("commentCount")) video.setCommentCount(Long.parseLong(statistics.get("commentCount").asText())); - - String duration = contentDetails.get("duration").asText(); - video.setDurationSec((int) java.time.Duration.parse(duration).getSeconds()); - - // Set simple defaults for calculated fields - video.setViewsPerHour(java.math.BigDecimal.ZERO); - video.setViewsPerSubRatio(java.math.BigDecimal.ZERO); - video.setSubscriberCount(0L); // We don't have channel stats here in one call - - video.setStatus("CRAWLED"); - - return ytVideoRepository.save(video); - - } catch (Exception e) { - throw new RuntimeException("Failed to fetch video details", e); - } - } - - private String extractVideoId(String url) { - // Simple extraction for v= parameter or short url - if (url.contains("v=")) { - String[] parts = url.split("v="); - if (parts.length > 1) { - String id = parts[1]; - int ampIndex = id.indexOf("&"); - if (ampIndex != -1) { - return id.substring(0, ampIndex); - } - return id; - } - } else if (url.contains("youtu.be/")) { - String[] parts = url.split("youtu.be/"); - if (parts.length > 1) { - return parts[1].split("\\?")[0]; - } - } - return url; // Assume ID if not URL format, or fail later - } - - public Page getVideos(VideoSearchCondition condition, Pageable pageable) { - Specification spec = (root, query, cb) -> { - List predicates = new ArrayList<>(); - - if (StringUtils.hasText(condition.getStatus())) { - predicates.add(cb.equal(root.get("status"), condition.getStatus())); - } - - if (StringUtils.hasText(condition.getKeyword())) { - String likePattern = "%" + condition.getKeyword() + "%"; - predicates.add(cb.or( - cb.like(root.get("title"), likePattern), - cb.like(root.get("channelTitle"), likePattern) - )); - } - - if (condition.getStartDate() != null) { - predicates.add(cb.greaterThanOrEqualTo(root.get("lastCrawledAt"), condition.getStartDate().atStartOfDay())); - } - - if (condition.getEndDate() != null) { - predicates.add(cb.lessThanOrEqualTo(root.get("lastCrawledAt"), condition.getEndDate().atTime(23, 59, 59))); - } - - return cb.and(predicates.toArray(new Predicate[0])); - }; - - return ytVideoRepository.findAll(spec, pageable).map(VideoResponse::new); - } - - public VideoDetailResponse getVideoDetail(Long videoId) { - YtVideo video = ytVideoRepository.findById(videoId) - .orElseThrow(() -> new IllegalArgumentException("Video not found")); - - VideoDetailResponse response = new VideoDetailResponse(video); - - scriptGenRepository.findById(videoId).ifPresent(scriptGen -> { - response.setScriptContent(scriptGen.getResponseText()); - response.setScriptStatus(scriptGen.getStatus()); - }); - - return response; - } - - public List getDrafts(Long videoId) { - return opalDraftRepository.findByVideo_VideoIdOrderByVersionNoDesc(videoId) - .stream() - .map(OpalDraftResponse::new) - .collect(Collectors.toList()); - } - - public FinalAssetResponse getFinalAsset(Long videoId) { - return opalFinalRepository.findByVideo_VideoIdAndIsActiveTrue(videoId) - .map(opalFinal -> { - var asset = opalFinalAssetRepository.findById(opalFinal.getFinalId()).orElse(null); - return new FinalAssetResponse(opalFinal, asset); - }) - .orElse(null); - } - - @Transactional - public void updateFinalAsset(Long videoId, String newText) { - OpalFinal opalFinal = opalFinalRepository.findByVideo_VideoIdAndIsActiveTrue(videoId) - .orElseThrow(() -> new IllegalArgumentException("No active final asset found for videoId: " + videoId)); - opalFinal.setFinalScriptText(newText); - // opalFinalRepository.save(opalFinal); // Transactional handles save - } - - public com.hlab.yanalyst.web.dto.YoutubeSearchPageDto searchYoutubeVideos(YoutubeSearchCondition condition) { + public YoutubeSearchPageDto searchYoutubeVideos(YoutubeSearchCondition condition) { List results = new ArrayList<>(); java.util.Map nextTokens = new java.util.HashMap<>(); - - List regions = condition.getRegions() != null && !condition.getRegions().isEmpty() - ? condition.getRegions() + + List regions = condition.getRegions() != null && !condition.getRegions().isEmpty() + ? condition.getRegions() : List.of("JP", "US"); - + List channelIds = condition.getChannelIds(); boolean isChannelSearch = channelIds != null && !channelIds.isEmpty(); - + List searchTargets = isChannelSearch ? channelIds : regions; String publishedAfter = null; if (condition.getPeriodDays() != null && condition.getPeriodDays() > 0) { java.time.ZonedDateTime date = java.time.ZonedDateTime.now(java.time.ZoneOffset.UTC).minusDays(condition.getPeriodDays()); - publishedAfter = date.format(java.time.format.DateTimeFormatter.ISO_INSTANT); + publishedAfter = date.format(DateTimeFormatter.ISO_INSTANT); } String duration = "short"; @@ -213,12 +56,12 @@ public class YtVideoService { } String searchApiUrl = "https://www.googleapis.com/youtube/v3/search"; - List allItems = new ArrayList<>(); + List allItems = new ArrayList<>(); for (String target : searchTargets) { String order = StringUtils.hasText(condition.getKeyword()) ? "relevance" : "date"; - org.springframework.web.util.UriComponentsBuilder builder = org.springframework.web.util.UriComponentsBuilder.fromHttpUrl(searchApiUrl) + UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(searchApiUrl) .queryParam("part", "snippet") .queryParam("maxResults", 50) .queryParam("type", "video") @@ -251,13 +94,13 @@ public class YtVideoService { } try { - com.fasterxml.jackson.databind.JsonNode root = restTemplate.getForObject(builder.build().encode().toUri(), com.fasterxml.jackson.databind.JsonNode.class); + JsonNode root = restTemplate.getForObject(builder.build().encode().toUri(), JsonNode.class); if (root != null) { if (root.has("nextPageToken")) { nextTokens.put(target, root.get("nextPageToken").asText()); } if (root.has("items")) { - for (com.fasterxml.jackson.databind.JsonNode item : root.get("items")) { + for (JsonNode item : root.get("items")) { allItems.add(item); } } @@ -268,20 +111,20 @@ public class YtVideoService { } java.util.Map uniqueVideos = new java.util.HashMap<>(); - - for (com.fasterxml.jackson.databind.JsonNode item : allItems) { + + for (JsonNode item : allItems) { String videoId = item.get("id").get("videoId").asText(); if (!uniqueVideos.containsKey(videoId)) { - com.fasterxml.jackson.databind.JsonNode snippet = item.get("snippet"); + JsonNode snippet = item.get("snippet"); YoutubeSearchResultDto dto = new YoutubeSearchResultDto(); dto.setVideoId(videoId); dto.setTitle(snippet.get("title").asText()); dto.setChannelId(snippet.get("channelId").asText()); dto.setChannelTitle(snippet.get("channelTitle").asText()); - + String publishedAtStr = snippet.get("publishedAt").asText(); - dto.setPublishedAt(LocalDateTime.parse(publishedAtStr, java.time.format.DateTimeFormatter.ISO_DATE_TIME)); - + dto.setPublishedAt(LocalDateTime.parse(publishedAtStr, DateTimeFormatter.ISO_DATE_TIME)); + if (snippet.has("thumbnails") && snippet.get("thumbnails").has("high")) { dto.setThumbnailUrl(snippet.get("thumbnails").get("high").get("url").asText()); } else if (snippet.has("thumbnails") && snippet.get("thumbnails").has("default")) { @@ -294,7 +137,7 @@ public class YtVideoService { results = new ArrayList<>(uniqueVideos.values()); if (results.isEmpty()) { - com.hlab.yanalyst.web.dto.YoutubeSearchPageDto pageDto = new com.hlab.yanalyst.web.dto.YoutubeSearchPageDto(); + YoutubeSearchPageDto pageDto = new YoutubeSearchPageDto(); pageDto.setItems(results); pageDto.setNextTokens(nextTokens); return pageDto; @@ -309,17 +152,17 @@ public class YtVideoService { String videoApiUrl = "https://www.googleapis.com/youtube/v3/videos"; java.util.Set idsToRemove = new java.util.HashSet<>(); - + for (List batch : partitionedResults) { String videoIds = batch.stream().map(YoutubeSearchResultDto::getVideoId).collect(Collectors.joining(",")); try { - org.springframework.web.util.UriComponentsBuilder vBuilder = org.springframework.web.util.UriComponentsBuilder.fromHttpUrl(videoApiUrl) + UriComponentsBuilder vBuilder = UriComponentsBuilder.fromHttpUrl(videoApiUrl) .queryParam("part", "statistics,contentDetails,snippet") .queryParam("id", videoIds) .queryParam("key", youtubeApiKey); - com.fasterxml.jackson.databind.JsonNode vRoot = restTemplate.getForObject(vBuilder.build().encode().toUri(), com.fasterxml.jackson.databind.JsonNode.class); + JsonNode vRoot = restTemplate.getForObject(vBuilder.build().encode().toUri(), JsonNode.class); if (vRoot != null && vRoot.has("items")) { - for (com.fasterxml.jackson.databind.JsonNode item : vRoot.get("items")) { + for (JsonNode item : vRoot.get("items")) { String vId = item.get("id").asText(); long viewCount = 0; if (item.has("statistics") && item.get("statistics").has("viewCount")) { @@ -330,14 +173,14 @@ public class YtVideoService { String durStr = item.get("contentDetails").get("duration").asText(); durSec = (int) java.time.Duration.parse(durStr).getSeconds(); } - + String titleForTags = ""; String descForTags = ""; if (item.has("snippet")) { if (item.get("snippet").has("title")) titleForTags = item.get("snippet").get("title").asText(); if (item.get("snippet").has("description")) descForTags = item.get("snippet").get("description").asText(); } - + java.util.List tags = new java.util.ArrayList<>(); java.util.regex.Matcher m = java.util.regex.Pattern.compile("#[^\\s#]+").matcher(titleForTags + " " + descForTags); while (m.find()) { @@ -346,7 +189,7 @@ public class YtVideoService { tags.add(tag); } } - + long finalViewCount = viewCount; int finalDurSec = durSec; results.stream().filter(r -> r.getVideoId().equals(vId)).forEach(r -> { @@ -354,7 +197,7 @@ public class YtVideoService { r.setDurationSec(finalDurSec); r.setHashtags(tags); }); - + // Strict filter based on format (65 seconds for true Shorts) boolean isShorts = "SHORTS".equalsIgnoreCase(condition.getFormat()); if (isShorts && durSec > 65) { @@ -368,7 +211,7 @@ public class YtVideoService { // Ignore } } - + results.removeIf(r -> idsToRemove.contains(r.getVideoId())); // 3. 채널 구독자 수 및 국가 정보 가져오기 (channels API) @@ -381,39 +224,39 @@ public class YtVideoService { String channelApiUrl = "https://www.googleapis.com/youtube/v3/channels"; java.util.Set channelIdsToRemove = new java.util.HashSet<>(); - + for (List batch : partitionedChannels) { String cIdsStr = String.join(",", batch); try { - org.springframework.web.util.UriComponentsBuilder cBuilder = org.springframework.web.util.UriComponentsBuilder.fromHttpUrl(channelApiUrl) + UriComponentsBuilder cBuilder = UriComponentsBuilder.fromHttpUrl(channelApiUrl) .queryParam("part", "statistics,snippet") .queryParam("id", cIdsStr) .queryParam("key", youtubeApiKey); - com.fasterxml.jackson.databind.JsonNode cRoot = restTemplate.getForObject(cBuilder.build().encode().toUri(), com.fasterxml.jackson.databind.JsonNode.class); + JsonNode cRoot = restTemplate.getForObject(cBuilder.build().encode().toUri(), JsonNode.class); if (cRoot != null && cRoot.has("items")) { - for (com.fasterxml.jackson.databind.JsonNode item : cRoot.get("items")) { + for (JsonNode item : cRoot.get("items")) { String cId = item.get("id").asText(); long subCount = 0; if (item.has("statistics") && item.get("statistics").has("subscriberCount")) { subCount = item.get("statistics").get("subscriberCount").asLong(); } - + String cCountry = "UNKNOWN"; if (item.has("snippet") && item.get("snippet").has("country")) { cCountry = item.get("snippet").get("country").asText(); } - + long finalSubCount = subCount; String finalCountry = cCountry; results.stream().filter(r -> r.getChannelId().equals(cId)).forEach(r -> { r.setSubscriberCount(finalSubCount); r.setChannelCountry(finalCountry); }); - + // 지역 기반 필터링 boolean isSearchEmpty = !StringUtils.hasText(condition.getKeyword()); boolean keep = false; - + if (isChannelSearch) { keep = true; } else if (!"UNKNOWN".equals(cCountry)) { @@ -435,11 +278,11 @@ public class YtVideoService { break; } } - + boolean matched = false; if (regions.contains("KR") && title.matches(".*[가-힣].*")) matched = true; if (regions.contains("JP") && title.matches(".*[ぁ-んァ-ン一-龥].*")) matched = true; - + if (!matched && (regions.contains("US") || regions.isEmpty())) { // US is selected (or no region selected). Allow if it DOES NOT contain obvious non-English scripts. boolean hasForeign = false; @@ -450,16 +293,16 @@ public class YtVideoService { } if (!regions.contains("KR") && title.matches(".*[가-힣].*")) hasForeign = true; if (!regions.contains("JP") && title.matches(".*[ぁ-んァ-ン一-龥].*")) hasForeign = true; - + if (!hasForeign) { matched = true; } } - + keep = matched; } } - + if (!keep) { channelIdsToRemove.add(cId); } @@ -469,19 +312,18 @@ public class YtVideoService { // Ignore } } - + results.removeIf(r -> channelIdsToRemove.contains(r.getChannelId())); // 조회수 내림차순 정렬 results.sort((v1, v2) -> Long.compare( - v2.getViewCount() != null ? v2.getViewCount() : 0L, - v1.getViewCount() != null ? v1.getViewCount() : 0L + v2.getViewCount() != null ? v2.getViewCount() : 0L, + v1.getViewCount() != null ? v1.getViewCount() : 0L )); - com.hlab.yanalyst.web.dto.YoutubeSearchPageDto pageDto = new com.hlab.yanalyst.web.dto.YoutubeSearchPageDto(); + YoutubeSearchPageDto pageDto = new YoutubeSearchPageDto(); pageDto.setItems(results); pageDto.setNextTokens(nextTokens); return pageDto; } } - \ No newline at end of file diff --git a/src/main/java/com/hlab/yanalyst/service/external/ExternalApiService.java b/src/main/java/com/hlab/yanalyst/service/external/ExternalApiService.java deleted file mode 100644 index 9dc3c22..0000000 --- a/src/main/java/com/hlab/yanalyst/service/external/ExternalApiService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.hlab.yanalyst.service.external; - -import org.springframework.stereotype.Service; - -import com.hlab.yanalyst.domain.opal.dto.OpalDraftResponseDto; -import java.util.Map; - -public interface ExternalApiService { - String generateScript(String videoUrl); - OpalDraftResponseDto generateOpalDraft(String script, String feedback, String mode); - String fetchFinalScript(); - String fetchOpeningScript(String docId); - Map generateFinalAsset(String finalScript); -} diff --git a/src/main/java/com/hlab/yanalyst/service/external/ExternalApiServiceImpl.java b/src/main/java/com/hlab/yanalyst/service/external/ExternalApiServiceImpl.java deleted file mode 100644 index 469f39e..0000000 --- a/src/main/java/com/hlab/yanalyst/service/external/ExternalApiServiceImpl.java +++ /dev/null @@ -1,262 +0,0 @@ -package com.hlab.yanalyst.service.external; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.hlab.yanalyst.domain.production.dto.ScriptResponseDto; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Primary; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -import com.google.api.client.auth.oauth2.Credential; -import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; -import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; -import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; -import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; -import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.json.gson.GsonFactory; -import com.google.api.client.util.store.FileDataStoreFactory; -import com.google.api.services.docs.v1.Docs; -import com.google.api.services.docs.v1.DocsScopes; -import com.google.api.services.docs.v1.model.*; - -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Slf4j -@Service -@Primary -@RequiredArgsConstructor -public class ExternalApiServiceImpl implements ExternalApiService { - - private final RestTemplate restTemplate; - private final ObjectMapper objectMapper; - - @Override - public String generateScript(String videoUrl) { - String apiUrl = "http://h-python.tolag.shop/transcript"; - log.info("Requesting transcript for URL: {}", videoUrl); - - try { - Map requestBody = Collections.singletonMap("url", videoUrl); - ResponseEntity response = restTemplate.postForEntity(apiUrl, requestBody, String.class); - - if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) { - ScriptResponseDto scriptDto = objectMapper.readValue(response.getBody(), ScriptResponseDto.class); - return scriptDto.getTranscript(); - } else { - log.error("Failed to fetch script from Python API. Status: {}", response.getStatusCode()); - return generateFallbackScript(videoUrl, "API returned status: " + response.getStatusCode()); - } - } catch (Exception e) { - log.error("Error calling Python API", e); - // Fallback to prevent blocking the workflow - return generateFallbackScript(videoUrl, e.getMessage()); - } - } - - private String generateFallbackScript(String url, String errorMsg) { - return String.format(""" - [SYSTEM NOTICE: Failed to retrieve real transcript due to external API error.] - [Error Detail: %s] - - [Fallback Script for %s] - - 00:00 - Introduction - Hello, this is a placeholder transcript generated by the system because the external Python API (YouTube Transcript) is currently blocked or unavailable. - - 00:15 - Main Topic - Usually, real content would appear here. Since YouTube blocks known cloud IPs, this is a common issue with scraping services. - - 00:30 - Conclusion - You can proceed to generate Opal Drafts and Final Assets using this placeholder text to test the rest of the intended workflow. - """, errorMsg, url); - } - - @Override - public com.hlab.yanalyst.domain.opal.dto.OpalDraftResponseDto generateOpalDraft(String script, String feedback, String mode) { - // Determine Doc ID based on mode - String docId; - if ("STRUCTURE_CHANGE".equals(mode)) { - docId = "11eFwYXm1Ld2vZUrOHvJZEmN69KDXvQygEysdXWr8-W4"; - } else { - // "TRUE_STORY" (Default) - docId = "1EHpn1GNregte6jTs43ycUUVqy6oljIPj6dm-uIXkcZc"; - } - - log.info("Fetching Opal draft (summary) using Google Docs API from Doc ID: {} (Mode: {})", docId, mode); - - try { - // 1. Fetch content using API - String text = readGoogleDoc(docId); - - String oldSummaryMarker = "old_script_summary"; - String newSummaryMarker = "new_script_summary"; - - int oldStart = text.indexOf(oldSummaryMarker); - int newStart = text.indexOf(newSummaryMarker); - - String oldSum = ""; - String newSum = ""; - - if (oldStart != -1 && newStart != -1) { - oldSum = text.substring(oldStart + oldSummaryMarker.length(), newStart).trim(); - newSum = text.substring(newStart + newSummaryMarker.length()).trim(); - - // Clear the doc if valid - clearGoogleDoc(docId); - log.info("Cleared content of Google Doc: {}", docId); - - } else { - // Fallback if markers missing - log.warn("Markers not found in doc. NOT clearing doc."); - oldSum = "Markers not found in doc."; - newSum = text.substring(0, Math.min(text.length(), 200)) + "..."; - } - - if (feedback != null && !feedback.isEmpty()) { - newSum += "\n\n[Reflecting Feedback: " + feedback + "]"; - } - - return new com.hlab.yanalyst.domain.opal.dto.OpalDraftResponseDto(oldSum, newSum); - - } catch (Exception e) { - log.error("Error fetching Opal draft from GDoc", e); - throw new RuntimeException("Error fetching Opal draft", e); - } - } - - @Override - public String fetchFinalScript() { -// String docId = "1tThnN2-OdYS-RuAWUWFsW9W238TPqZssaww5_wULOg4"; - String docId = "1jiSEwFuWeggIFln08j15pXw-8iUsFaDVjVKl3RbVdsw"; - log.info("Fetching final script using Google Docs API from Doc ID: {}", docId); - - try { - String text = readGoogleDoc(docId); - - // Clear content - clearGoogleDoc(docId); - log.info("Cleared content of Google Doc: {}", docId); - - return text; - } catch (Exception e) { - log.error("Error fetching final script", e); - throw new RuntimeException("Error fetching final script", e); - } - } - - @Override - public String fetchOpeningScript(String docId) { - log.info("Fetching opening script using Google Docs API from Doc ID: {}", docId); - - try { - String text = readGoogleDoc(docId); - - // Clear content - clearGoogleDoc(docId); - log.info("Cleared content of Google Doc: {}", docId); - - return text; - } catch (Exception e) { - log.error("Error fetching opening script", e); - throw new RuntimeException("Error fetching opening script", e); - } - } - - // --- Google Docs Helper Methods --- - - private static final String CREDENTIALS_FILE_PATH = "/credentials.json"; - private static final java.util.List SCOPES = Collections.singletonList(DocsScopes.DOCUMENTS); - private static final String TOKENS_DIRECTORY_PATH = "tokens"; - - private Docs getDocsService() throws Exception { - final NetHttpTransport HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport(); - GsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); - - InputStream in = ExternalApiServiceImpl.class.getResourceAsStream(CREDENTIALS_FILE_PATH); - if (in == null) { - throw new FileNotFoundException("Resource not found: " + CREDENTIALS_FILE_PATH); - } - GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in)); - - // Build flow and trigger user authorization request. - GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder( - HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES) - .setDataStoreFactory(new FileDataStoreFactory(new java.io.File(TOKENS_DIRECTORY_PATH))) - .setAccessType("offline") - .build(); - - LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build(); - // This authorize call will open browser if token is missing - Credential credential = new AuthorizationCodeInstalledApp(flow, receiver).authorize("user"); - - return new Docs.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential) - .setApplicationName("HLAB-Backend") - .build(); - } - - private String readGoogleDoc(String docId) throws Exception { - Docs service = getDocsService(); - Document doc = service.documents().get(docId).execute(); - StringBuilder sb = new StringBuilder(); - doc.getBody().getContent().forEach(c -> { - if (c.getParagraph() != null) { - c.getParagraph().getElements().forEach(e -> { - if (e.getTextRun() != null) { - sb.append(e.getTextRun().getContent()); - } - }); - sb.append("\n"); // Add line break for paragraphs - } - }); - return sb.toString(); - } - - private void clearGoogleDoc(String docId) throws Exception { - Docs service = getDocsService(); - Document doc = service.documents().get(docId).execute(); - - // Calculate the end index. Content usually ends with a newline. - int lastContentIndex = doc.getBody().getContent().size() - 1; - int docEnd = doc.getBody().getContent().get(lastContentIndex).getEndIndex(); - - log.info("Attempting to clear doc {}. Last content index: {}, Doc End Index: {}", docId, lastContentIndex, docEnd); - - // Safety check: if doc is already practically empty - if (docEnd > 2) { - int deleteEndIndex = docEnd - 1; - log.info("Deleting range: 1 to {}", deleteEndIndex); - - Request request = new Request() - .setDeleteContentRange(new DeleteContentRangeRequest() - .setRange(new Range().setStartIndex(1).setEndIndex(deleteEndIndex))); - - BatchUpdateDocumentRequest body = new BatchUpdateDocumentRequest().setRequests(Collections.singletonList(request)); - BatchUpdateDocumentResponse response = service.documents().batchUpdate(docId, body).execute(); - log.info("Clear doc response: {}", response); - } else { - log.info("Doc appears empty or too small to clear (endIndex <= 2). Skipping delete."); - } - } - - @Override - public Map generateFinalAsset(String finalScript) { - // Stub for now - try { Thread.sleep(2000); } catch (InterruptedException e) {} - Map asset = new HashMap<>(); - asset.put("title", "Generated Title (Real)"); - asset.put("summary", "Generated Summary (Real)"); - asset.put("timeline", List.of(Map.of("time", "00:00", "desc", "Intro"), Map.of("time", "01:00", "desc", "Main Content"))); - asset.put("video_prompt", "Cinematic prompt for " + finalScript.substring(0, 10)); - asset.put("image_urls", List.of("https://via.placeholder.com/150", "https://via.placeholder.com/150")); - return asset; - } -} diff --git a/src/main/java/com/hlab/yanalyst/service/external/ExternalApiServiceStub.java b/src/main/java/com/hlab/yanalyst/service/external/ExternalApiServiceStub.java deleted file mode 100644 index ab6aad0..0000000 --- a/src/main/java/com/hlab/yanalyst/service/external/ExternalApiServiceStub.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.hlab.yanalyst.service.external; - -import org.springframework.stereotype.Service; -import java.util.HashMap; -import java.util.Map; -import java.util.List; - -@Service -public class ExternalApiServiceStub implements ExternalApiService { - - @Override - public String generateScript(String videoUrl) { - // Stub: simulate python script generation - try { Thread.sleep(1000); } catch (InterruptedException e) {} - return "Stub Script Content for " + videoUrl + "\n\n[Intro]\nHello everyone, today we are analyzing..."; - } - - @Override - public com.hlab.yanalyst.domain.opal.dto.OpalDraftResponseDto generateOpalDraft(String script, String feedback, String mode) { - // Stub: simulate opal generation - try { Thread.sleep(1500); } catch (InterruptedException e) {} - String prefix = feedback != null ? "[Reflecting Feedback: " + feedback + "]\n" : ""; - return new com.hlab.yanalyst.domain.opal.dto.OpalDraftResponseDto( - "Stub Old Summary from " + script.substring(0, Math.min(script.length(), 20)), - prefix + "Stub New Summary: " + script.substring(0, Math.min(script.length(), 20)) - ); - } - - @Override - public String fetchFinalScript() { - return "Stub Final Script Content"; - } - - @Override - public String fetchOpeningScript(String docId) { - return "Stub Opening Script Content from " + docId; - } - - @Override - public Map generateFinalAsset(String finalScript) { - // Stub: simulate final asset generation - try { Thread.sleep(2000); } catch (InterruptedException e) {} - Map asset = new HashMap<>(); - asset.put("title", "Generated Title"); - asset.put("summary", "Generated Summary"); - asset.put("timeline", List.of(Map.of("time", "00:00", "desc", "Intro"), Map.of("time", "01:00", "desc", "Main Topic"))); - asset.put("video_prompt", "A cinematic shot of..."); - asset.put("image_urls", List.of("http://example.com/img1.jpg", "http://example.com/img2.jpg")); - return asset; - } -} diff --git a/src/main/java/com/hlab/yanalyst/web/VideoActionController.java b/src/main/java/com/hlab/yanalyst/web/VideoActionController.java deleted file mode 100644 index 5835232..0000000 --- a/src/main/java/com/hlab/yanalyst/web/VideoActionController.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.hlab.yanalyst.web; - -import com.hlab.yanalyst.domain.opal.OpalDraft; -import com.hlab.yanalyst.domain.opal.OpalFinal; -import com.hlab.yanalyst.domain.opal.OpalFinalAsset; -import com.hlab.yanalyst.domain.script.ScriptGen; -import com.hlab.yanalyst.global.common.ApiResponse; -import com.hlab.yanalyst.service.AnalysisWorkflowService; -import com.hlab.yanalyst.web.dto.DraftGenerateRequest; -import com.hlab.yanalyst.web.dto.FinalAssetResponse; -import com.hlab.yanalyst.web.dto.OpalDraftResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/api/videos") -@RequiredArgsConstructor -public class VideoActionController { - - private final AnalysisWorkflowService workflowService; - - @PostMapping("/{videoId}/script") - public ApiResponse generateScript(@PathVariable Long videoId) { - workflowService.generateScript(videoId); - return ApiResponse.ok(null); - } - - @PostMapping("/{videoId}/drafts") - public ApiResponse generateDraft( - @PathVariable Long videoId, - @RequestBody(required = false) DraftGenerateRequest request) { - String feedback = request != null ? request.getFeedback() : null; - String mode = request != null ? request.getMode() : null; - OpalDraft draft = workflowService.generateDraft(videoId, feedback, mode); - return ApiResponse.created(new OpalDraftResponse(draft)); - } - - @PostMapping("/{videoId}/drafts/{draftId}/accept") - public ApiResponse acceptDraft( - @PathVariable Long videoId, - @PathVariable Long draftId) { - workflowService.acceptDraft(videoId, draftId); - return ApiResponse.ok(null); - } - - @PostMapping("/{videoId}/final-asset") - public ApiResponse generateFinalAsset(@PathVariable Long videoId) { - OpalFinalAsset asset = workflowService.generateFinalAsset(videoId); - // Need to refetch opalFinal to pass to DTO, strictly strictly speaking asset has reference - return ApiResponse.created(new FinalAssetResponse(asset.getOpalFinal(), asset)); - } - @PostMapping("/{videoId}/complete") - public ApiResponse toggleComplete(@PathVariable Long videoId) { - boolean isCompleted = workflowService.toggleComplete(videoId); - return ApiResponse.ok(isCompleted); - } - - @PutMapping("/{videoId}/final-asset") - public ApiResponse updateFinalAsset( - @PathVariable Long videoId, - @RequestBody java.util.Map body) { - String newText = body.get("finalScriptText"); - workflowService.updateFinalAsset(videoId, newText); - return ApiResponse.ok(null); - } - - @PostMapping("/{videoId}/opening") - public ApiResponse generateOpening( - @PathVariable Long videoId, - @RequestBody java.util.Map body) { - String docId = body.get("docId"); - workflowService.generateOpening(videoId, docId); - return ApiResponse.ok(null); - } - - @DeleteMapping("/{videoId}/opening") - public ApiResponse resetOpening(@PathVariable Long videoId) { - workflowService.resetOpening(videoId); - return ApiResponse.ok(null); - } -} diff --git a/src/main/java/com/hlab/yanalyst/web/WebController.java b/src/main/java/com/hlab/yanalyst/web/WebController.java index 20f6b3c..e64e008 100644 --- a/src/main/java/com/hlab/yanalyst/web/WebController.java +++ b/src/main/java/com/hlab/yanalyst/web/WebController.java @@ -7,12 +7,10 @@ import org.springframework.web.bind.annotation.GetMapping; @Controller public class WebController { - private final com.hlab.yanalyst.service.YtVideoService ytVideoService; private final com.hlab.yanalyst.domain.production.ProductionService productionService; private final com.hlab.yanalyst.domain.channel.ChannelService channelService; - public WebController(com.hlab.yanalyst.service.YtVideoService ytVideoService, com.hlab.yanalyst.domain.production.ProductionService productionService, com.hlab.yanalyst.domain.channel.ChannelService channelService) { - this.ytVideoService = ytVideoService; + public WebController(com.hlab.yanalyst.domain.production.ProductionService productionService, com.hlab.yanalyst.domain.channel.ChannelService channelService) { this.productionService = productionService; this.channelService = channelService; } @@ -36,18 +34,6 @@ public class WebController { return "videos"; } - @GetMapping("/videos/{videoId}") - public String videoDetail(@org.springframework.web.bind.annotation.PathVariable Long videoId, Model model) { - model.addAttribute("currentPage", "videos"); - model.addAttribute("video", ytVideoService.getVideoDetail(videoId)); - // Also fetch drafts and final asset initially to render server-side if needed, - // or let the view fetch via AJAX. - // For 'Draft History' and 'Final', let's pass them to model for initial load to avoid layout shift. - model.addAttribute("drafts", ytVideoService.getDrafts(videoId)); - model.addAttribute("finalAsset", ytVideoService.getFinalAsset(videoId)); - return "video_detail"; - } - @GetMapping("/collection") public String collection(Model model) { model.addAttribute("currentPage", "collection"); diff --git a/src/main/java/com/hlab/yanalyst/web/YoutubeSearchApiController.java b/src/main/java/com/hlab/yanalyst/web/YoutubeSearchApiController.java index 436e0a4..7e54d83 100644 --- a/src/main/java/com/hlab/yanalyst/web/YoutubeSearchApiController.java +++ b/src/main/java/com/hlab/yanalyst/web/YoutubeSearchApiController.java @@ -2,7 +2,7 @@ package com.hlab.yanalyst.web; import com.hlab.yanalyst.domain.channel.SearchCollectionService; import com.hlab.yanalyst.global.common.ApiResponse; -import com.hlab.yanalyst.service.YtVideoService; +import com.hlab.yanalyst.service.YoutubeSearchService; import com.hlab.yanalyst.web.dto.YoutubeSearchCondition; import com.hlab.yanalyst.web.dto.YoutubeSearchResultDto; import io.swagger.v3.oas.annotations.Operation; @@ -19,12 +19,12 @@ import java.util.List; @RequiredArgsConstructor public class YoutubeSearchApiController { - private final YtVideoService ytVideoService; + private final YoutubeSearchService youtubeSearchService; private final SearchCollectionService searchCollectionService; @PostMapping("/search") public com.hlab.yanalyst.web.dto.YoutubeSearchPageDto search(@RequestBody YoutubeSearchCondition condition) { - return ytVideoService.searchYoutubeVideos(condition); + return youtubeSearchService.searchYoutubeVideos(condition); } @PostMapping("/collect") diff --git a/src/main/java/com/hlab/yanalyst/web/YtVideoController.java b/src/main/java/com/hlab/yanalyst/web/YtVideoController.java deleted file mode 100644 index 808749e..0000000 --- a/src/main/java/com/hlab/yanalyst/web/YtVideoController.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.hlab.yanalyst.web; - -import com.hlab.yanalyst.global.common.ApiResponse; -import com.hlab.yanalyst.service.YtVideoService; -import com.hlab.yanalyst.web.dto.FinalAssetResponse; -import com.hlab.yanalyst.web.dto.OpalDraftResponse; -import com.hlab.yanalyst.web.dto.VideoDetailResponse; -import com.hlab.yanalyst.web.dto.VideoResponse; -import com.hlab.yanalyst.web.dto.VideoSearchCondition; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.PageableDefault; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -@RestController -@RequestMapping("/api/videos") -@RequiredArgsConstructor -public class YtVideoController { - - private final YtVideoService ytVideoService; - - @org.springframework.web.bind.annotation.PostMapping - public ApiResponse addVideo(@org.springframework.web.bind.annotation.RequestBody com.hlab.yanalyst.web.dto.VideoAddRequest request) { - try { - ytVideoService.saveVideoFromUrl(request.getUrl()); - return ApiResponse.ok("Video added successfully"); - } catch (IllegalArgumentException e) { - return ApiResponse.error(e.getMessage()); - } catch (Exception e) { - return ApiResponse.error("Failed to add video: " + e.getMessage()); - } - } - - @GetMapping - public ApiResponse> getVideos( - VideoSearchCondition condition, - @PageableDefault(size = 20, sort = "lastCrawledAt", direction = Sort.Direction.DESC) Pageable pageable) { - return ApiResponse.ok(ytVideoService.getVideos(condition, pageable)); - } - - @GetMapping("/{videoId}") - public ApiResponse getVideoDetail(@PathVariable Long videoId) { - return ApiResponse.ok(ytVideoService.getVideoDetail(videoId)); - } - - @GetMapping("/{videoId}/drafts") - public ApiResponse> getDrafts(@PathVariable Long videoId) { - return ApiResponse.ok(ytVideoService.getDrafts(videoId)); - } - - @GetMapping("/{videoId}/final-asset") - public ApiResponse getFinalAsset(@PathVariable Long videoId) { - return ApiResponse.ok(ytVideoService.getFinalAsset(videoId)); - } -} diff --git a/src/main/java/com/hlab/yanalyst/web/dto/DraftGenerateRequest.java b/src/main/java/com/hlab/yanalyst/web/dto/DraftGenerateRequest.java deleted file mode 100644 index 3c448b1..0000000 --- a/src/main/java/com/hlab/yanalyst/web/dto/DraftGenerateRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.hlab.yanalyst.web.dto; - -import lombok.Data; - -@Data -public class DraftGenerateRequest { - private String feedback; - private String mode; // "TRUE_STORY" or "STRUCTURE_CHANGE" -} diff --git a/src/main/java/com/hlab/yanalyst/web/dto/FinalAssetResponse.java b/src/main/java/com/hlab/yanalyst/web/dto/FinalAssetResponse.java deleted file mode 100644 index 9e6ef2d..0000000 --- a/src/main/java/com/hlab/yanalyst/web/dto/FinalAssetResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.hlab.yanalyst.web.dto; - -import com.hlab.yanalyst.domain.opal.OpalFinal; -import com.hlab.yanalyst.domain.opal.OpalFinalAsset; -import lombok.Data; - -@Data -public class FinalAssetResponse { - private Long finalId; - private String finalScriptText; - private String assetJson; - private Boolean isActive; - - public FinalAssetResponse(OpalFinal opalFinal, OpalFinalAsset asset) { - this.finalId = opalFinal.getFinalId(); - this.finalScriptText = opalFinal.getFinalScriptText(); - this.isActive = opalFinal.getIsActive(); - if (asset != null) { - this.assetJson = asset.getAssetJson(); - } - } -} diff --git a/src/main/java/com/hlab/yanalyst/web/dto/OpalDraftResponse.java b/src/main/java/com/hlab/yanalyst/web/dto/OpalDraftResponse.java deleted file mode 100644 index af01ed6..0000000 --- a/src/main/java/com/hlab/yanalyst/web/dto/OpalDraftResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.hlab.yanalyst.web.dto; - -import com.hlab.yanalyst.domain.opal.OpalDraft; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -public class OpalDraftResponse { - private Long draftId; - private Integer versionNo; - private String responseText; - private String userFeedback; - private Boolean isAccepted; - private LocalDateTime createdAt; - - private String oldScriptSummary; - private String newScriptSummary; - - public OpalDraftResponse(OpalDraft draft) { - this.draftId = draft.getDraftId(); - this.versionNo = draft.getVersionNo(); - this.responseText = draft.getResponseText(); - this.userFeedback = draft.getUserFeedback(); - this.isAccepted = draft.getIsAccepted(); - this.createdAt = draft.getCreatedAt(); - this.oldScriptSummary = draft.getOldScriptSummary(); - this.newScriptSummary = draft.getNewScriptSummary(); - } -} diff --git a/src/main/java/com/hlab/yanalyst/web/dto/VideoAddRequest.java b/src/main/java/com/hlab/yanalyst/web/dto/VideoAddRequest.java deleted file mode 100644 index 3ec556c..0000000 --- a/src/main/java/com/hlab/yanalyst/web/dto/VideoAddRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.hlab.yanalyst.web.dto; - -import lombok.Data; - -@Data -public class VideoAddRequest { - private String url; -} diff --git a/src/main/java/com/hlab/yanalyst/web/dto/VideoDetailResponse.java b/src/main/java/com/hlab/yanalyst/web/dto/VideoDetailResponse.java deleted file mode 100644 index c42877d..0000000 --- a/src/main/java/com/hlab/yanalyst/web/dto/VideoDetailResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.hlab.yanalyst.web.dto; - -import com.hlab.yanalyst.domain.video.YtVideo; -import lombok.Data; - -@Data -public class VideoDetailResponse { - private VideoResponse metadata; - private String videoUrl; - private String youtubeVideoId; - - // We will load Script, Draft, Final via separate API or include them here? - // Requirement says "Center Area: 3 Tabs". - // It's better to fetch detail data including status, but maybe load heavy text data lazy or in this response. - // Given the requirement "Show script_gen.response_text", it might be large. - // Let's include everything for simplicity as per common SPA patterns unless it's huge. - - private String scriptContent; // ScriptGen.responseText - private String scriptStatus; - private String openingScript; - - // We can return list of drafts separately or here. - // Let's create a separate API for drafts history to keep this clean, - // or just include basic info. - // Requirement: "Opal Draft History - list newest first". - // Let's fetch the detailed lists via separate calls to keep payload light and responsive. - - // But for "Script" tab, if it's 1:1, we can include it. - - public VideoDetailResponse(YtVideo video) { - this.metadata = new VideoResponse(video); - this.videoUrl = video.getVideoUrl(); - this.youtubeVideoId = video.getYoutubeVideoId(); - this.openingScript = video.getOpeningScript(); - } -} diff --git a/src/main/java/com/hlab/yanalyst/web/dto/VideoResponse.java b/src/main/java/com/hlab/yanalyst/web/dto/VideoResponse.java deleted file mode 100644 index 2edb634..0000000 --- a/src/main/java/com/hlab/yanalyst/web/dto/VideoResponse.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.hlab.yanalyst.web.dto; - -import com.hlab.yanalyst.domain.video.YtVideo; -import lombok.Data; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -@Data -public class VideoResponse { - private Long videoId; - private String thumbnailUrl; - private String title; - private String channelTitle; - private Long viewCount; - private Long subscriberCount; - private BigDecimal viewsPerSubRatio; - private BigDecimal viewsPerHour; - private String status; - private LocalDateTime lastCrawledAt; - private Boolean isCompleted; - - public VideoResponse(YtVideo video) { - this.videoId = video.getVideoId(); - this.thumbnailUrl = video.getThumbnailUrl(); - this.title = video.getTitle(); - this.channelTitle = video.getChannelTitle(); - this.viewCount = video.getViewCount(); - this.subscriberCount = video.getSubscriberCount(); - this.viewsPerSubRatio = video.getViewsPerSubRatio(); - this.viewsPerHour = video.getViewsPerHour(); - this.status = video.getStatus(); - this.lastCrawledAt = video.getLastCrawledAt(); - this.isCompleted = video.getIsCompleted() != null && video.getIsCompleted(); - } -} diff --git a/src/main/java/com/hlab/yanalyst/web/dto/VideoSearchCondition.java b/src/main/java/com/hlab/yanalyst/web/dto/VideoSearchCondition.java deleted file mode 100644 index 4f884e6..0000000 --- a/src/main/java/com/hlab/yanalyst/web/dto/VideoSearchCondition.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.hlab.yanalyst.web.dto; - -import lombok.Data; -import org.springframework.format.annotation.DateTimeFormat; - -import java.time.LocalDate; - -@Data -public class VideoSearchCondition { - private String status; - private String keyword; // title or channelTitle - - @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate startDate; - - @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate endDate; -} diff --git a/src/main/resources/templates/video_detail.html b/src/main/resources/templates/video_detail.html deleted file mode 100644 index f129a14..0000000 --- a/src/main/resources/templates/video_detail.html +++ /dev/null @@ -1,622 +0,0 @@ - - - - -
- - - - -
- Thumb - -
-
-
-

Video Title

- Channel Name -
-
- - - Open YouTube - -
-
- -
-
- 10K - Views -
-
- 5K - Subs -
-
- 120 - Views/Hr -
-
- CRAWLED - Status -
-
-
-
- - -
-
- -
- -
- -
- -
-
- - -
- - -
-
-

Python Script (Transcript)

- -
- -
-
- -
-

-                
-
- - - - - - - -
- - - -
- - - - - - - - - - - - \ No newline at end of file