This commit is contained in:
+12
-21
@@ -321,27 +321,11 @@ func (a *App) searchMedia(c *gin.Context) {
|
|||||||
scored := services.RankSearchResults(rankQuery, results)
|
scored := services.RankSearchResults(rankQuery, results)
|
||||||
a.debug("search ranked summary", summarizeSearchResults(scored, time.Since(started), services.GeminiCandidateLimit(len(scored)), ""))
|
a.debug("search ranked summary", summarizeSearchResults(scored, time.Since(started), services.GeminiCandidateLimit(len(scored)), ""))
|
||||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
|
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "analyzing all candidate visuals with Gemini Vision", "progress": 75})
|
||||||
recommended, geminiStats := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
recommended, geminiStats, geminiErr := services.EvaluateAllCandidatesWithGemini(a.GeminiService, req.Query, scored)
|
||||||
a.debug("search gemini evaluation", geminiStats)
|
a.debug("search gemini evaluation", geminiStats)
|
||||||
err = nil
|
if geminiErr != nil && len(recommended) == 0 {
|
||||||
if len(recommended) == 0 {
|
warning := geminiErr.Error()
|
||||||
err = fmt.Errorf("gemini vision returned no recommended items across all candidate batches")
|
fallback := services.BuildFallbackRecommendations(scored, 20, "")
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
fallback := make([]services.AIRecommendation, 0, min(20, len(scored)))
|
|
||||||
for _, result := range scored[:min(20, len(scored))] {
|
|
||||||
fallback = append(fallback, services.AIRecommendation{
|
|
||||||
Title: result.Title,
|
|
||||||
Link: result.Link,
|
|
||||||
Snippet: result.Snippet,
|
|
||||||
ThumbnailURL: result.ThumbnailURL,
|
|
||||||
PreviewVideoURL: result.PreviewVideoURL,
|
|
||||||
Source: result.Source,
|
|
||||||
Reason: "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다.",
|
|
||||||
Recommended: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
warning := err.Error()
|
|
||||||
a.debug("search fallback summary", summarizeRecommendationResults(fallback, time.Since(started), warning))
|
a.debug("search fallback summary", summarizeRecommendationResults(fallback, time.Since(started), warning))
|
||||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini Vision fallback to ranked results", "progress": 90, "message": warning})
|
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "Gemini Vision fallback to ranked results", "progress": 90, "message": warning})
|
||||||
c.JSON(http.StatusOK, gin.H{"results": fallback, "warning": warning, "queries": queryVariants})
|
c.JSON(http.StatusOK, gin.H{"results": fallback, "warning": warning, "queries": queryVariants})
|
||||||
@@ -349,8 +333,15 @@ func (a *App) searchMedia(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
merged := services.MergeRecommendations(recommended, scored, 20)
|
merged := services.MergeRecommendations(recommended, scored, 20)
|
||||||
a.debug("search complete summary", summarizeRecommendationResults(merged, time.Since(started), ""))
|
warning := ""
|
||||||
|
if geminiErr != nil {
|
||||||
|
warning = geminiErr.Error()
|
||||||
|
}
|
||||||
|
a.debug("search complete summary", summarizeRecommendationResults(merged, time.Since(started), warning))
|
||||||
response := gin.H{"results": merged, "queries": queryVariants}
|
response := gin.H{"results": merged, "queries": queryVariants}
|
||||||
|
if warning != "" {
|
||||||
|
response["warning"] = warning
|
||||||
|
}
|
||||||
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "search complete", "progress": 100})
|
a.Hub.Broadcast("progress", gin.H{"type": "search", "status": "search complete", "progress": 100})
|
||||||
c.JSON(http.StatusOK, response)
|
c.JSON(http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,16 +164,21 @@ User query: ` + query,
|
|||||||
}
|
}
|
||||||
|
|
||||||
maxImages := min(len(candidates), 10)
|
maxImages := min(len(candidates), 10)
|
||||||
|
visualCount := 0
|
||||||
for idx := 0; idx < maxImages; idx++ {
|
for idx := 0; idx < maxImages; idx++ {
|
||||||
img, mimeType, err := fetchCandidateVisualInlineData(g.Client, candidates[idx])
|
img, mimeType, err := fetchCandidateVisualInlineData(g.Client, candidates[idx])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
visualCount++
|
||||||
parts = append(parts,
|
parts = append(parts,
|
||||||
geminiPart{"text": fmt.Sprintf("Candidate %d: title=%s source=%s link=%s", idx, candidates[idx].Title, candidates[idx].Source, candidates[idx].Link)},
|
geminiPart{"text": fmt.Sprintf("Candidate %d: title=%s source=%s link=%s", idx, candidates[idx].Title, candidates[idx].Source, candidates[idx].Link)},
|
||||||
geminiPart{"inlineData": map[string]string{"mimeType": mimeType, "data": img}},
|
geminiPart{"inlineData": map[string]string{"mimeType": mimeType, "data": img}},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if visualCount == 0 {
|
||||||
|
return nil, fmt.Errorf("no candidate thumbnails or preview frames could be fetched for gemini vision")
|
||||||
|
}
|
||||||
|
|
||||||
body := map[string]any{
|
body := map[string]any{
|
||||||
"contents": []map[string]any{
|
"contents": []map[string]any{
|
||||||
@@ -248,18 +253,7 @@ User query: ` + query,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(recommendations) == 0 {
|
if len(recommendations) == 0 {
|
||||||
for _, candidate := range candidates[:min(8, len(candidates))] {
|
recommendations = BuildFallbackRecommendations(candidates, 8, "Gemini Vision 평가를 받지 못해 키워드 기준으로 보강된 결과입니다.")
|
||||||
recommendations = append(recommendations, AIRecommendation{
|
|
||||||
Title: candidate.Title,
|
|
||||||
Link: candidate.Link,
|
|
||||||
Snippet: candidate.Snippet,
|
|
||||||
ThumbnailURL: candidate.ThumbnailURL,
|
|
||||||
PreviewVideoURL: candidate.PreviewVideoURL,
|
|
||||||
Source: candidate.Source,
|
|
||||||
Reason: "Gemini Vision 평가를 받지 못해 키워드 기준으로 보강된 결과입니다.",
|
|
||||||
Recommended: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return recommendations, nil
|
return recommendations, nil
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const GeminiFallbackReason = "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다."
|
||||||
|
|
||||||
type GeminiBatchStats struct {
|
type GeminiBatchStats struct {
|
||||||
CandidateCap int `json:"candidateCap"`
|
CandidateCap int `json:"candidateCap"`
|
||||||
Requested int `json:"requested"`
|
Requested int `json:"requested"`
|
||||||
@@ -84,9 +87,13 @@ func GeminiCandidateLimit(total int) int {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats) {
|
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats, error) {
|
||||||
const chunkSize = 8
|
const chunkSize = 8
|
||||||
const maxConcurrentBatches = 2
|
const maxConcurrentBatches = 2
|
||||||
|
if service == nil {
|
||||||
|
return nil, GeminiBatchStats{}, fmt.Errorf("gemini service is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
limit := GeminiCandidateLimit(len(ranked))
|
limit := GeminiCandidateLimit(len(ranked))
|
||||||
stats := GeminiBatchStats{
|
stats := GeminiBatchStats{
|
||||||
CandidateCap: limit,
|
CandidateCap: limit,
|
||||||
@@ -106,6 +113,9 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
|||||||
batches = append(batches, ranked[start:end])
|
batches = append(batches, ranked[start:end])
|
||||||
}
|
}
|
||||||
stats.Batches = len(batches)
|
stats.Batches = len(batches)
|
||||||
|
if len(batches) == 0 {
|
||||||
|
return []AIRecommendation{}, stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
results := make([]batchResult, len(batches))
|
results := make([]batchResult, len(batches))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -146,7 +156,41 @@ func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranke
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
stats.RecommendedCount = len(merged)
|
stats.RecommendedCount = len(merged)
|
||||||
return merged, stats
|
|
||||||
|
switch {
|
||||||
|
case len(merged) > 0 && stats.Failed == 0:
|
||||||
|
return merged, stats, nil
|
||||||
|
case len(merged) > 0 && stats.Failed > 0:
|
||||||
|
return merged, stats, fmt.Errorf("gemini vision partially failed on %d of %d batches", stats.Failed, stats.Batches)
|
||||||
|
case stats.Failed == stats.Batches:
|
||||||
|
if len(stats.Errors) > 0 {
|
||||||
|
return nil, stats, fmt.Errorf("gemini vision failed for all batches: %s", strings.Join(stats.Errors, "; "))
|
||||||
|
}
|
||||||
|
return nil, stats, fmt.Errorf("gemini vision failed for all batches")
|
||||||
|
default:
|
||||||
|
return nil, stats, fmt.Errorf("gemini vision returned no candidate evaluations")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildFallbackRecommendations(ranked []SearchResult, limit int, reason string) []AIRecommendation {
|
||||||
|
if strings.TrimSpace(reason) == "" {
|
||||||
|
reason = GeminiFallbackReason
|
||||||
|
}
|
||||||
|
|
||||||
|
fallback := make([]AIRecommendation, 0, min(limit, len(ranked)))
|
||||||
|
for _, item := range ranked[:min(limit, len(ranked))] {
|
||||||
|
fallback = append(fallback, AIRecommendation{
|
||||||
|
Title: item.Title,
|
||||||
|
Link: item.Link,
|
||||||
|
Snippet: item.Snippet,
|
||||||
|
ThumbnailURL: item.ThumbnailURL,
|
||||||
|
PreviewVideoURL: item.PreviewVideoURL,
|
||||||
|
Source: item.Source,
|
||||||
|
Reason: reason,
|
||||||
|
Recommended: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
|
func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult, limit int) []AIRecommendation {
|
||||||
@@ -184,7 +228,7 @@ func MergeRecommendations(recommended []AIRecommendation, ranked []SearchResult,
|
|||||||
ThumbnailURL: item.ThumbnailURL,
|
ThumbnailURL: item.ThumbnailURL,
|
||||||
PreviewVideoURL: item.PreviewVideoURL,
|
PreviewVideoURL: item.PreviewVideoURL,
|
||||||
Source: item.Source,
|
Source: item.Source,
|
||||||
Reason: "Gemini Vision 응답이 부족해 키워드 기준으로 보강된 결과입니다.",
|
Reason: GeminiFallbackReason,
|
||||||
Recommended: false,
|
Recommended: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-27
@@ -56,6 +56,7 @@ let activeResultItem = null;
|
|||||||
const activePlatforms = new Set(["envato", "artgrid", "google video"]);
|
const activePlatforms = new Set(["envato", "artgrid", "google video"]);
|
||||||
const hlsInstances = new WeakMap();
|
const hlsInstances = new WeakMap();
|
||||||
const debugEntries = [];
|
const debugEntries = [];
|
||||||
|
const PREVIEW_PLACEHOLDER = "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview";
|
||||||
|
|
||||||
function setStatus(label, progress) {
|
function setStatus(label, progress) {
|
||||||
statusLabel.textContent = label;
|
statusLabel.textContent = label;
|
||||||
@@ -63,6 +64,18 @@ function setStatus(label, progress) {
|
|||||||
logEvent("status", { label, progress });
|
logEvent("status", { label, progress });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setHidden(element, hidden, visibleDisplayClass = "flex") {
|
||||||
|
element.classList.toggle("hidden", hidden);
|
||||||
|
if (visibleDisplayClass) {
|
||||||
|
element.classList.toggle(visibleDisplayClass, !hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWarning(message) {
|
||||||
|
searchWarning.textContent = message;
|
||||||
|
setHidden(searchWarning, !message, "");
|
||||||
|
}
|
||||||
|
|
||||||
function logEvent(type, payload) {
|
function logEvent(type, payload) {
|
||||||
const entry = {
|
const entry = {
|
||||||
ts: new Date().toISOString(),
|
ts: new Date().toISOString(),
|
||||||
@@ -318,6 +331,20 @@ function detachVideoSource(video) {
|
|||||||
video.load();
|
video.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetPreviewPlayer() {
|
||||||
|
previewVideo.pause();
|
||||||
|
detachVideoSource(previewVideo);
|
||||||
|
previewMediaFrame.style.aspectRatio = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function showModal(element) {
|
||||||
|
setHidden(element, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideModal(element) {
|
||||||
|
setHidden(element, true);
|
||||||
|
}
|
||||||
|
|
||||||
function renderResults(results) {
|
function renderResults(results) {
|
||||||
searchResults.innerHTML = "";
|
searchResults.innerHTML = "";
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
@@ -329,7 +356,7 @@ function renderResults(results) {
|
|||||||
const image = node.querySelector("img");
|
const image = node.querySelector("img");
|
||||||
const previewVideo = node.querySelector(".preview-hover");
|
const previewVideo = node.querySelector(".preview-hover");
|
||||||
const overlays = node.querySelectorAll(".preview-overlay");
|
const overlays = node.querySelectorAll(".preview-overlay");
|
||||||
image.src = item.thumbnailUrl || "https://placehold.co/1280x720/0a0a0a/ffffff?text=Preview";
|
image.src = item.thumbnailUrl || PREVIEW_PLACEHOLDER;
|
||||||
image.alt = item.title;
|
image.alt = item.title;
|
||||||
node.querySelector("h3").textContent = item.title;
|
node.querySelector("h3").textContent = item.title;
|
||||||
node.querySelector(".result-snippet").textContent = item.snippet || item.reason || item.source || "";
|
node.querySelector(".result-snippet").textContent = item.snippet || item.reason || item.source || "";
|
||||||
@@ -389,8 +416,7 @@ function openResultModal(item) {
|
|||||||
resultModalOpenExternal.href = item.link || "#";
|
resultModalOpenExternal.href = item.link || "#";
|
||||||
const canDirectDownload = item.source === "Google Video" && item.link;
|
const canDirectDownload = item.source === "Google Video" && item.link;
|
||||||
resultModalDownload.classList.toggle("hidden", !canDirectDownload);
|
resultModalDownload.classList.toggle("hidden", !canDirectDownload);
|
||||||
resultModal.classList.remove("hidden");
|
showModal(resultModal);
|
||||||
resultModal.classList.add("flex");
|
|
||||||
logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link });
|
logEvent("result:modal:open", { title: item.title, source: item.source, link: item.link });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,14 +426,13 @@ function closeResultViewer() {
|
|||||||
}
|
}
|
||||||
activeResultItem = null;
|
activeResultItem = null;
|
||||||
resultModalFrame.src = "about:blank";
|
resultModalFrame.src = "about:blank";
|
||||||
resultModal.classList.add("hidden");
|
hideModal(resultModal);
|
||||||
resultModal.classList.remove("flex");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
searchForm.addEventListener("submit", async (event) => {
|
searchForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setStatus("preparing search", 5);
|
setStatus("preparing search", 5);
|
||||||
searchWarning.classList.add("hidden");
|
showWarning("");
|
||||||
try {
|
try {
|
||||||
const data = await api("/api/search", {
|
const data = await api("/api/search", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -416,15 +441,11 @@ searchForm.addEventListener("submit", async (event) => {
|
|||||||
});
|
});
|
||||||
renderResults(data.results || []);
|
renderResults(data.results || []);
|
||||||
renderQueryVariants(data.queries || []);
|
renderQueryVariants(data.queries || []);
|
||||||
if (data.warning) {
|
showWarning(data.warning || "");
|
||||||
searchWarning.textContent = data.warning;
|
|
||||||
searchWarning.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
logEvent("search:completed", { results: data.results?.length || 0, queries: data.queries || [] });
|
logEvent("search:completed", { results: data.results?.length || 0, queries: data.queries || [] });
|
||||||
setStatus("search complete", 100);
|
setStatus("search complete", 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
searchWarning.textContent = error.message;
|
showWarning(error.message);
|
||||||
searchWarning.classList.remove("hidden");
|
|
||||||
renderQueryVariants([]);
|
renderQueryVariants([]);
|
||||||
setStatus("search failed", 100);
|
setStatus("search failed", 100);
|
||||||
}
|
}
|
||||||
@@ -445,18 +466,16 @@ async function uploadFile(file) {
|
|||||||
function openPreviewModal(preview) {
|
function openPreviewModal(preview) {
|
||||||
logEvent("preview:modal:open", preview);
|
logEvent("preview:modal:open", preview);
|
||||||
previewTitle.textContent = preview.title;
|
previewTitle.textContent = preview.title;
|
||||||
previewThumbnail.src = preview.thumbnail;
|
previewThumbnail.src = preview.thumbnail || PREVIEW_PLACEHOLDER;
|
||||||
previewThumbnail.alt = preview.title;
|
previewThumbnail.alt = preview.title;
|
||||||
previewVideo.pause();
|
resetPreviewPlayer();
|
||||||
detachVideoSource(previewVideo);
|
|
||||||
previewMediaFrame.style.aspectRatio = "";
|
|
||||||
if (preview.previewStreamUrl) {
|
if (preview.previewStreamUrl) {
|
||||||
attachVideoSource(previewVideo, preview.previewStreamUrl);
|
attachVideoSource(previewVideo, preview.previewStreamUrl);
|
||||||
previewVideo.classList.remove("hidden");
|
setHidden(previewVideo, false, "");
|
||||||
previewThumbnail.classList.add("hidden");
|
setHidden(previewThumbnail, true, "");
|
||||||
} else {
|
} else {
|
||||||
previewVideo.classList.add("hidden");
|
setHidden(previewVideo, true, "");
|
||||||
previewThumbnail.classList.remove("hidden");
|
setHidden(previewThumbnail, false, "");
|
||||||
}
|
}
|
||||||
previewDuration.textContent = preview.duration;
|
previewDuration.textContent = preview.duration;
|
||||||
qualitySelect.innerHTML = "";
|
qualitySelect.innerHTML = "";
|
||||||
@@ -470,17 +489,13 @@ function openPreviewModal(preview) {
|
|||||||
cropStart = 0;
|
cropStart = 0;
|
||||||
cropEnd = cropMax;
|
cropEnd = cropMax;
|
||||||
syncRanges();
|
syncRanges();
|
||||||
previewModal.classList.remove("hidden");
|
showModal(previewModal);
|
||||||
previewModal.classList.add("flex");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
logEvent("preview:modal:close", { title: previewTitle.textContent });
|
logEvent("preview:modal:close", { title: previewTitle.textContent });
|
||||||
previewVideo.pause();
|
resetPreviewPlayer();
|
||||||
detachVideoSource(previewVideo);
|
hideModal(previewModal);
|
||||||
previewMediaFrame.style.aspectRatio = "";
|
|
||||||
previewModal.classList.add("hidden");
|
|
||||||
previewModal.classList.remove("flex");
|
|
||||||
cropStart = 0;
|
cropStart = 0;
|
||||||
cropEnd = 0;
|
cropEnd = 0;
|
||||||
cropMax = 0;
|
cropMax = 0;
|
||||||
|
|||||||
+43
-25
@@ -36,12 +36,6 @@ def parse_duration(value):
|
|||||||
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||||
|
|
||||||
|
|
||||||
def format_label(height, ext):
|
|
||||||
if height:
|
|
||||||
return f"{height}p ({ext})"
|
|
||||||
return f"Best ({ext})"
|
|
||||||
|
|
||||||
|
|
||||||
def build_quality_options(formats: List[dict]):
|
def build_quality_options(formats: List[dict]):
|
||||||
heights = []
|
heights = []
|
||||||
for item in formats:
|
for item in formats:
|
||||||
@@ -99,6 +93,45 @@ def probe(url):
|
|||||||
print(json.dumps(preview), flush=True)
|
print(json.dumps(preview), flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_timestamp(value: str) -> int:
|
||||||
|
text = (value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return 0
|
||||||
|
parts = text.split(":")
|
||||||
|
try:
|
||||||
|
if len(parts) == 3:
|
||||||
|
hours, minutes, seconds = parts
|
||||||
|
return int(hours) * 3600 + int(minutes) * 60 + int(float(seconds))
|
||||||
|
if len(parts) == 2:
|
||||||
|
minutes, seconds = parts
|
||||||
|
return int(minutes) * 60 + int(float(seconds))
|
||||||
|
return int(float(text))
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_source_file(tmpdir: str) -> str:
|
||||||
|
files = [os.path.join(tmpdir, name) for name in os.listdir(tmpdir)]
|
||||||
|
media_files = [path for path in files if os.path.isfile(path)]
|
||||||
|
if not media_files:
|
||||||
|
raise RuntimeError("yt-dlp did not produce an output file")
|
||||||
|
return sorted(media_files)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def should_trim(start: str, end: str) -> bool:
|
||||||
|
start_seconds = parse_timestamp(start)
|
||||||
|
end_seconds = parse_timestamp(end)
|
||||||
|
return end_seconds > start_seconds
|
||||||
|
|
||||||
|
|
||||||
|
def build_ffmpeg_cmd(source_file: str, output_path: str, start: str, end: str) -> List[str]:
|
||||||
|
cmd = ["ffmpeg", "-y"]
|
||||||
|
if should_trim(start, end):
|
||||||
|
cmd.extend(["-ss", start, "-to", end])
|
||||||
|
cmd.extend(["-i", source_file, "-c", "copy", output_path])
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--mode", choices=["probe", "download"], default="download")
|
parser.add_argument("--mode", choices=["probe", "download"], default="download")
|
||||||
@@ -132,25 +165,10 @@ def main():
|
|||||||
run(download_cmd)
|
run(download_cmd)
|
||||||
emit("downloaded", 55, "Source downloaded")
|
emit("downloaded", 55, "Source downloaded")
|
||||||
|
|
||||||
files = [os.path.join(tmpdir, name) for name in os.listdir(tmpdir)]
|
source_file = resolve_source_file(tmpdir)
|
||||||
if not files:
|
ffmpeg_cmd = build_ffmpeg_cmd(source_file, args.output, args.start, args.end)
|
||||||
raise RuntimeError("yt-dlp did not produce an output file")
|
message = "Cropping requested segment" if should_trim(args.start, args.end) else "Saving downloaded media"
|
||||||
source_file = sorted(files)[0]
|
emit("cropping", 75, message)
|
||||||
|
|
||||||
ffmpeg_cmd = [
|
|
||||||
"ffmpeg",
|
|
||||||
"-y",
|
|
||||||
"-ss",
|
|
||||||
args.start,
|
|
||||||
"-to",
|
|
||||||
args.end,
|
|
||||||
"-i",
|
|
||||||
source_file,
|
|
||||||
"-c",
|
|
||||||
"copy",
|
|
||||||
args.output,
|
|
||||||
]
|
|
||||||
emit("cropping", 75, "Cropping requested segment")
|
|
||||||
run(ffmpeg_cmd)
|
run(ffmpeg_cmd)
|
||||||
|
|
||||||
emit("completed", 100, "Download complete", args.output)
|
emit("completed", 100, "Download complete", args.output)
|
||||||
|
|||||||
Reference in New Issue
Block a user