Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 58d54a0338 | |||
| c177bae59e | |||
| f131cee6de |
@@ -33,7 +33,8 @@
|
||||
- Result modal sizing is now being constrained to the viewport, and modal-only source-summary translation is now part of the active implementation path.
|
||||
- Card summaries now also translate lazily to Korean, and Gemini negative-assessment handling now drives stronger follow-up search behavior than before.
|
||||
- Search preview delivery is now moving away from persistent on-disk preview caching toward live proxy / live transcode behavior, with Google Video preview reuse added to result cards and modal playback.
|
||||
- The latest search-breadth / modal-fitting experiment from `5ca7aef` has been rolled back after live regression was confirmed.
|
||||
- Search breadth and Gemini review budget were widened again because the latest user feedback still reported a thin visible result count even after smarter filtering.
|
||||
- The codebase is now back on the broader-search / modal-fitting direction associated with `5ca7aef`, with an added Gemini deadline guard to reduce reverse-proxy `504` risk.
|
||||
|
||||
## Current Architecture
|
||||
- `backend/main.go`
|
||||
@@ -232,8 +233,8 @@
|
||||
- Source Summary translation now depends on Google Translate HTTP availability; frontend silently falls back to original summary text if translation fails.
|
||||
- The result modal should now stay within viewport height, but this still needs real browser confirmation on multiple short-height displays because CSS-only constraints were the source of the latest user-visible regression.
|
||||
- Artgrid preview playback now has a server-side ffmpeg transcode path for `.m3u8` style preview URLs, but this trades storage savings for runtime CPU cost.
|
||||
- The reverted `5ca7aef` experiment showed that simply widening collector caps and Gemini candidate count can backfire when the added candidates are weak: final visible count fell sharply even though backend raw candidate count increased.
|
||||
- Gemini Vision batch execution can still see mixed failure modes: true model/output-format failures and ignorable low-value/no-visual candidate skips. These should not be conflated in user-facing warnings.
|
||||
- The provided Artgrid HTML sample still does not expose a direct preview `m3u8` or `mp4` URL by itself, and `yt-dlp` probe on the sample clip URL returned `Unsupported URL`, so fully reliable Artgrid playback still depends on live-page/runtime preview discovery succeeding elsewhere in the pipeline.
|
||||
- Docker CLI is not installed in this environment, so container-image build verification still cannot be performed locally from this machine.
|
||||
- The local self-test script is better than before, but it is still a smoke test, not full integration coverage.
|
||||
|
||||
## Current Risks Around Search Quality
|
||||
@@ -555,12 +556,13 @@
|
||||
## Highest-Value Next Steps
|
||||
- [ ] Reduce `/api/search` latency further without collapsing result count
|
||||
- [ ] Rebuild the reverted search-expansion work from the previous stable baseline, but only after measuring where candidate quality collapses between ranked pool and final merge
|
||||
- [ ] Continue hardening Gemini response parsing so truncated JSON or mixed-quality candidate batches degrade more gracefully without surfacing alarming warnings
|
||||
- [ ] Validate the reopened `5ca7aef` search-breadth direction against real proxy timeouts and visible result count before widening it any further
|
||||
- [ ] Build a repeatable repo-local bootstrap script or documented setup command set for non-root machines so fresh PC setup does not depend on shell history
|
||||
- [ ] Improve Envato / Artgrid preview acquisition reliability so Gemini Vision sees real frames more often
|
||||
- [ ] Browser-verify the new result modal at multiple viewport heights and confirm translated Source Summary readability on real long descriptions
|
||||
- [ ] Evaluate whether the new Gemini supplemental-query generation is reducing irrelevant results on a small fixed benchmark query set
|
||||
- [ ] Measure runtime cost of live Artgrid preview transcoding and decide whether bounded in-memory throttling or concurrency caps are needed
|
||||
- [ ] If Artgrid playback is still required for previewless clips, investigate a browser-rendered fetch path or clip-page asset bundle analysis because static HTML + `yt-dlp` probe were both insufficient on the supplied sample
|
||||
- [ ] Revisit Google Video UX:
|
||||
- current YouTube embed was abandoned due error `153`
|
||||
- current in-app panel is more reliable but less rich than a true embedded watch page
|
||||
@@ -628,41 +630,39 @@
|
||||
## Recent Change Log
|
||||
- Date: `2026-03-17`
|
||||
- What changed:
|
||||
- Reduced Gemini Vision batch size from `8` to `6` candidates to lower the chance of oversized/truncated JSON responses.
|
||||
- Added an explicit Gemini output token cap for the vision JSON response path.
|
||||
- Changed Gemini batch failure accounting so ignorable per-candidate skips such as low-value thumbnails or missing visuals no longer count as a user-facing partial batch failure when useful recommendations were still recovered.
|
||||
- Added unit coverage for the hard-error filtering behavior.
|
||||
- Reapplied the broader-search / modal-fitting codepath from `5ca7aef` as requested by the user.
|
||||
- Added a targeted 504 mitigation by making Gemini batch recovery stop at the request deadline instead of continuing sequential single-candidate retries indefinitely.
|
||||
- Kept an explicit Gemini vision JSON output token cap to reduce the chance of truncated model responses during larger structured batches.
|
||||
- Why it changed:
|
||||
- The user reported the warning `gemini vision partially failed on 2 of 2 batches`.
|
||||
- The provided log `ai-media-hub-2026-03-17T04-35-33-028Z.log` showed two distinct failure classes during the second repeated search:
|
||||
- a real batch-level JSON extraction failure caused by truncated Gemini output
|
||||
- multiple ignorable sequential-retry skips caused by low-value thumbnails or missing visuals
|
||||
- Those were being combined into an alarming partial-failure warning even though useful recommendations were still recovered.
|
||||
- The user explicitly asked to return to the `5ca7aef` direction and then fix the live `504 Gateway Time-out`.
|
||||
- The provided log `ai-media-hub-2026-03-17T04-59-24-566Z.log` showed backend search collection finishing in about `22s`, then Gemini batch failure plus sequential recovery continuing until the reverse proxy returned `504`.
|
||||
- The direct cause was deadline-unaware Gemini recovery work, not initial search collection itself.
|
||||
- How it was verified:
|
||||
- `go test ./...`
|
||||
- `bash scripts/selftest.sh`
|
||||
- attempted `docker build -t ai-media-hub:test .` but `docker` is not installed in this environment
|
||||
- What is still risky or incomplete:
|
||||
- This reduces false-positive partial-failure warnings, but true Gemini output truncation can still happen occasionally if model behavior changes again.
|
||||
- Further hardening may still be needed if Gemini starts returning different malformed JSON shapes.
|
||||
- This reduces one concrete `504` path, but it does not guarantee that all reverse-proxy timeout cases are eliminated under worse upstream conditions.
|
||||
- Container-image build verification still needs to happen on a machine that actually has Docker available.
|
||||
|
||||
- Date: `2026-03-17`
|
||||
- What changed:
|
||||
- Reverted commit `5ca7aef` (`Strengthen search breadth and modal fitting`) to restore the previous stable search/modal baseline.
|
||||
- Revalidated the rollback state locally.
|
||||
- Raised search breadth again by widening per-source collector caps, increasing the number of base queries considered, increasing per-collector query budgets, and expanding the Gemini candidate review budget.
|
||||
- Added another browser-resolution-aware modal fitting pass: the result popup now recalculates shell width, shell height, media max height, and compact-mode behavior from `window.innerWidth` / `window.innerHeight` when opened or when the viewport changes.
|
||||
- Removed the fixed minimum heights on the lower modal panels so they can shrink with the viewport instead of forcing the popup past the visible browser area.
|
||||
- Checked the supplied Artgrid clip HTML and a direct `yt-dlp` probe against `https://artgrid.io/clip/355470/...`; the HTML still exposes metadata and thumbnail signals only, and the direct probe returned `Unsupported URL`.
|
||||
- Why it changed:
|
||||
- The user reported that search results became too sparse again and that `search returned partial results to avoid gateway timeout` reappeared.
|
||||
- The provided log `ai-media-hub-2026-03-17T04-19-08-889Z.log` showed:
|
||||
- backend candidate pool still reaching `30`
|
||||
- Gemini candidate cap widened to `24`
|
||||
- `visualRejectCount: 12`
|
||||
- final visible result count collapsing to `5`
|
||||
- That indicates the widened experiment increased weak/low-visual candidates and pushed the request back toward deadline pressure without improving visible result count.
|
||||
- The user reported that the popup still overflowed the browser viewport and that the visible result set still felt too thin despite the earlier search-expansion work.
|
||||
- The attached Artgrid HTML sample was intended to verify whether a stable preview URL could be extracted directly from static clip HTML.
|
||||
- How it was verified:
|
||||
- `go test ./...`
|
||||
- `bash scripts/selftest.sh`
|
||||
- `python3 -m py_compile worker/downloader.py scripts/mock_searxng.py`
|
||||
- `python3 worker/downloader.py --mode probe --url 'https://artgrid.io/clip/355470/couple-woman-man-holding-hand'`
|
||||
- What is still risky or incomplete:
|
||||
- This rollback restores the earlier baseline but does not solve the underlying request to improve visible result count.
|
||||
- The next attempt should start from the reverted baseline and target the actual bottleneck between ranked pool and final merge, instead of only widening raw query breadth.
|
||||
- The new modal layout now adapts to actual browser viewport dimensions in code, but a live browser confirmation on the exact user display is still needed.
|
||||
- Widening search breadth and Gemini review budget should improve visible count, but it can also increase latency and token cost under poor upstream conditions.
|
||||
- The supplied Artgrid HTML sample alone was not enough to derive a directly playable preview URL, so previewless Artgrid clips may still fall back to thumbnail-only behavior.
|
||||
|
||||
- Date: `2026-03-17`
|
||||
- What changed:
|
||||
|
||||
+27
-24
@@ -554,7 +554,7 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
targetCount := 16
|
||||
targetCount := 18
|
||||
merged := services.MergeRecommendations(recommended, scored, targetCount)
|
||||
if geminiErr != nil {
|
||||
merged = services.BackfillRecommendations(
|
||||
@@ -564,31 +564,34 @@ func (a *App) searchMedia(c *gin.Context) {
|
||||
"Gemini 배치 일부가 실패해 미리보기 가능한 상위 후보를 보강했습니다.",
|
||||
)
|
||||
}
|
||||
if len(merged) < targetCount && time.Now().Before(deadline.Add(-5*time.Second)) {
|
||||
for pass := 0; pass < 2 && len(merged) < targetCount && time.Now().Before(deadline.Add(-4*time.Second)); pass++ {
|
||||
coverageQueries := buildCoverageQueries(req.Query, queryVariants, recommended, merged)
|
||||
if len(coverageQueries) > 0 {
|
||||
a.debug("search coverage query variants", gin.H{"variants": coverageQueries, "variantCount": len(coverageQueries), "existingCount": len(merged)})
|
||||
extraResults, extraMeta, extraErr := a.SearchService.SearchMediaWithDeadline(coverageQueries, enabledPlatforms, deadline.Add(-5*time.Second))
|
||||
supplementalDeadlineLimited = supplementalDeadlineLimited || extraMeta.PartialDueToDeadline
|
||||
if extraErr == nil && len(extraResults) > 0 {
|
||||
results = mergeSearchResults(results, extraResults)
|
||||
scored = services.RankSearchResults(strings.Join(coverageQueries[:min(len(coverageQueries), 3)], " "), results)
|
||||
reviewedLinks := services.ReviewedRecommendationLinks(recommended)
|
||||
supplementalCandidates := services.SelectUnevaluatedCandidates(scored, reviewedLinks, services.RemainingGeminiCapacity(recommended))
|
||||
if len(supplementalCandidates) > 0 {
|
||||
extraRecommended, extraStats, extraGeminiErr := services.EvaluateAllCandidatesWithGeminiWithDeadline(
|
||||
a.GeminiService,
|
||||
req.Query,
|
||||
supplementalCandidates,
|
||||
deadline.Add(-2*time.Second),
|
||||
)
|
||||
recommended = services.MergeUniqueRecommendations(recommended, extraRecommended)
|
||||
geminiStats = services.MergeGeminiBatchStats(geminiStats, extraStats)
|
||||
geminiErr = combineSearchWarnings(geminiErr, extraGeminiErr)
|
||||
}
|
||||
merged = services.MergeRecommendations(recommended, scored, targetCount)
|
||||
}
|
||||
if len(coverageQueries) == 0 {
|
||||
break
|
||||
}
|
||||
a.debug("search coverage query variants", gin.H{"variants": coverageQueries, "variantCount": len(coverageQueries), "existingCount": len(merged), "pass": pass + 1})
|
||||
extraResults, extraMeta, extraErr := a.SearchService.SearchMediaWithDeadline(coverageQueries, enabledPlatforms, deadline.Add(-4*time.Second))
|
||||
supplementalDeadlineLimited = supplementalDeadlineLimited || extraMeta.PartialDueToDeadline
|
||||
if extraErr != nil || len(extraResults) == 0 {
|
||||
break
|
||||
}
|
||||
results = mergeSearchResults(results, extraResults)
|
||||
scored = services.RankSearchResults(strings.Join(coverageQueries[:min(len(coverageQueries), 3)], " "), results)
|
||||
reviewedLinks := services.ReviewedRecommendationLinks(recommended)
|
||||
supplementalCandidates := services.SelectUnevaluatedCandidates(scored, reviewedLinks, services.RemainingGeminiCapacity(recommended))
|
||||
if len(supplementalCandidates) == 0 {
|
||||
break
|
||||
}
|
||||
extraRecommended, extraStats, extraGeminiErr := services.EvaluateAllCandidatesWithGeminiWithDeadline(
|
||||
a.GeminiService,
|
||||
req.Query,
|
||||
supplementalCandidates,
|
||||
deadline.Add(-2*time.Second),
|
||||
)
|
||||
recommended = services.MergeUniqueRecommendations(recommended, extraRecommended)
|
||||
geminiStats = services.MergeGeminiBatchStats(geminiStats, extraStats)
|
||||
geminiErr = combineSearchWarnings(geminiErr, extraGeminiErr)
|
||||
merged = services.MergeRecommendations(recommended, scored, targetCount)
|
||||
}
|
||||
if len(merged) < targetCount {
|
||||
merged = services.BackfillRecommendations(merged, scored, targetCount, "추가 검색 후에도 충분한 결과가 부족해 시각 자산이 있는 후보를 제한적으로 보강했습니다.")
|
||||
|
||||
@@ -94,9 +94,9 @@ func (s *SearchService) SearchMediaWithDeadline(queries []string, enabledPlatfor
|
||||
results := make([]SearchResult, 0, 90)
|
||||
var lastErr error
|
||||
|
||||
baseQueries := limitQueries(queries, 8)
|
||||
baseQueries := limitQueries(queries, 10)
|
||||
shuffleStrings(baseQueries)
|
||||
primaryQueries := baseQueries[:minInt(len(baseQueries), 3)]
|
||||
primaryQueries := baseQueries[:minInt(len(baseQueries), 4)]
|
||||
runSearchPass := func(bases []string, onlyMissing bool) {
|
||||
for _, base := range bases {
|
||||
if !deadline.IsZero() && time.Now().After(deadline) {
|
||||
@@ -1432,9 +1432,9 @@ func limitCollectorQueries(collector string, queries []string, onlyMissing bool)
|
||||
limit := 2
|
||||
switch collector {
|
||||
case "Envato", "Artgrid":
|
||||
limit = 4
|
||||
limit = 5
|
||||
case "Google Video":
|
||||
limit = 3
|
||||
limit = 4
|
||||
}
|
||||
if onlyMissing {
|
||||
limit--
|
||||
|
||||
@@ -159,13 +159,13 @@ func TestLimitCollectorQueriesUsesSmallerBudgetForMissingPass(t *testing.T) {
|
||||
queries := []string{"a", "b", "c", "d"}
|
||||
|
||||
got := limitCollectorQueries("Artgrid", queries, true)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("expected 3 queries for missing-pass Artgrid collector, got %d", len(got))
|
||||
if len(got) != 4 {
|
||||
t.Fatalf("expected 4 queries for missing-pass Artgrid collector, got %d", len(got))
|
||||
}
|
||||
|
||||
got = limitCollectorQueries("Google Video", queries, false)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("expected 3 queries for Google Video collector, got %d", len(got))
|
||||
if len(got) != 4 {
|
||||
t.Fatalf("expected 4 queries for Google Video collector, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,8 +123,8 @@ func TestRemainingGeminiCapacityShrinksWithReviewedItems(t *testing.T) {
|
||||
{Link: "https://a.example"},
|
||||
{Link: "https://b.example"},
|
||||
}
|
||||
if got := RemainingGeminiCapacity(reviewed); got != 14 {
|
||||
t.Fatalf("expected 14 remaining slots, got %d", got)
|
||||
if got := RemainingGeminiCapacity(reviewed); got != 22 {
|
||||
t.Fatalf("expected 22 remaining slots, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,18 +213,3 @@ func TestMergeRecommendationsExcludesIrrelevantAndPendingFiller(t *testing.T) {
|
||||
t.Fatalf("unexpected merged result: %#v", merged)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterHardGeminiErrorsIgnoresLowValueVisualFailures(t *testing.T) {
|
||||
errs := []string{
|
||||
"candidate thumbnail is low value",
|
||||
"no candidate thumbnails or preview frames could be fetched for gemini vision",
|
||||
"gemini vision JSON extraction failed: no complete JSON object found",
|
||||
}
|
||||
filtered := filterHardGeminiErrors(errs)
|
||||
if len(filtered) != 1 {
|
||||
t.Fatalf("expected only hard errors to remain, got %#v", filtered)
|
||||
}
|
||||
if !strings.Contains(filtered[0], "JSON extraction failed") {
|
||||
t.Fatalf("unexpected filtered errors: %#v", filtered)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-41
@@ -97,7 +97,7 @@ func RankSearchResults(query string, results []SearchResult) []SearchResult {
|
||||
}
|
||||
|
||||
func GeminiCandidateLimit(total int) int {
|
||||
return min(total, 16)
|
||||
return min(total, 24)
|
||||
}
|
||||
|
||||
func EvaluateAllCandidatesWithGemini(service *GeminiService, query string, ranked []SearchResult) ([]AIRecommendation, GeminiBatchStats, error) {
|
||||
@@ -186,8 +186,7 @@ func EvaluateAllCandidatesWithGeminiWithDeadline(service *GeminiService, query s
|
||||
"error": batch.err.Error(),
|
||||
})
|
||||
}
|
||||
recovered, recoveredErrs := recoverGeminiBatchSequentially(service, query, ranked, batch.index*chunkSize)
|
||||
hardErrs := filterHardGeminiErrors(recoveredErrs)
|
||||
recovered, recoveredErrs := recoverGeminiBatchSequentially(service, query, ranked, batch.index*chunkSize, chunkSize, deadline)
|
||||
if len(recovered) > 0 {
|
||||
stats.SequentialRetried++
|
||||
stats.Succeeded++
|
||||
@@ -198,9 +197,9 @@ func EvaluateAllCandidatesWithGeminiWithDeadline(service *GeminiService, query s
|
||||
seen[item.Link] = true
|
||||
merged = append(merged, item)
|
||||
}
|
||||
if len(hardErrs) > 0 {
|
||||
if len(recoveredErrs) > 0 {
|
||||
stats.Failed++
|
||||
for _, recoveredErr := range hardErrs {
|
||||
for _, recoveredErr := range recoveredErrs {
|
||||
if len(stats.Errors) < 5 {
|
||||
stats.Errors = append(stats.Errors, recoveredErr)
|
||||
}
|
||||
@@ -208,9 +207,6 @@ func EvaluateAllCandidatesWithGeminiWithDeadline(service *GeminiService, query s
|
||||
}
|
||||
continue
|
||||
}
|
||||
if len(hardErrs) == 0 {
|
||||
continue
|
||||
}
|
||||
stats.Failed++
|
||||
if len(stats.Errors) < 5 {
|
||||
stats.Errors = append(stats.Errors, batch.err.Error())
|
||||
@@ -295,7 +291,7 @@ func ReviewedRecommendationLinks(items []AIRecommendation) map[string]bool {
|
||||
}
|
||||
|
||||
func RemainingGeminiCapacity(reviewed []AIRecommendation) int {
|
||||
remaining := GeminiCandidateLimit(16) - len(ReviewedRecommendationLinks(reviewed))
|
||||
remaining := GeminiCandidateLimit(24) - len(ReviewedRecommendationLinks(reviewed))
|
||||
if remaining < 0 {
|
||||
return 0
|
||||
}
|
||||
@@ -333,35 +329,6 @@ func MergeUniqueRecommendations(base, extra []AIRecommendation) []AIRecommendati
|
||||
return merged
|
||||
}
|
||||
|
||||
func filterHardGeminiErrors(errs []string) []string {
|
||||
filtered := make([]string, 0, len(errs))
|
||||
for _, item := range errs {
|
||||
if isIgnorableGeminiError(item) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func isIgnorableGeminiError(message string) bool {
|
||||
lower := strings.ToLower(strings.TrimSpace(message))
|
||||
if lower == "" {
|
||||
return false
|
||||
}
|
||||
for _, token := range []string{
|
||||
"no candidate thumbnails or preview frames could be fetched for gemini vision",
|
||||
"candidate thumbnail is low value",
|
||||
"candidate has no thumbnail or preview video",
|
||||
"image url is empty",
|
||||
} {
|
||||
if strings.Contains(lower, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func MergeGeminiBatchStats(base, extra GeminiBatchStats) GeminiBatchStats {
|
||||
merged := base
|
||||
merged.CandidateCap += extra.CandidateCap
|
||||
@@ -378,11 +345,17 @@ func MergeGeminiBatchStats(base, extra GeminiBatchStats) GeminiBatchStats {
|
||||
return merged
|
||||
}
|
||||
|
||||
func recoverGeminiBatchSequentially(service *GeminiService, query string, ranked []SearchResult, startIndex int) ([]AIRecommendation, []string) {
|
||||
recovered := make([]AIRecommendation, 0, 8)
|
||||
func recoverGeminiBatchSequentially(service *GeminiService, query string, ranked []SearchResult, startIndex, chunkSize int, deadline time.Time) ([]AIRecommendation, []string) {
|
||||
recovered := make([]AIRecommendation, 0, chunkSize)
|
||||
errs := make([]string, 0, 4)
|
||||
endIndex := min(startIndex+8, len(ranked))
|
||||
endIndex := min(startIndex+chunkSize, len(ranked))
|
||||
for idx := startIndex; idx < endIndex; idx++ {
|
||||
if !deadline.IsZero() && time.Now().After(deadline) {
|
||||
if len(errs) < 4 {
|
||||
errs = append(errs, "sequential gemini recovery stopped at deadline")
|
||||
}
|
||||
break
|
||||
}
|
||||
recs, err := service.Recommend(query, []SearchResult{ranked[idx]})
|
||||
if err != nil {
|
||||
if len(errs) < 4 {
|
||||
|
||||
@@ -15,7 +15,7 @@ type searchCollector interface {
|
||||
type envatoCollector struct{}
|
||||
|
||||
func (envatoCollector) Name() string { return "Envato" }
|
||||
func (envatoCollector) MaxResults() int { return 12 }
|
||||
func (envatoCollector) MaxResults() int { return 16 }
|
||||
func (envatoCollector) Enabled(enabledPlatforms map[string]bool) bool {
|
||||
return len(enabledPlatforms) == 0 || enabledPlatforms["envato"]
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func (envatoCollector) Enrich(searcher *SearchService, result SearchResult) Sear
|
||||
type artgridCollector struct{}
|
||||
|
||||
func (artgridCollector) Name() string { return "Artgrid" }
|
||||
func (artgridCollector) MaxResults() int { return 12 }
|
||||
func (artgridCollector) MaxResults() int { return 16 }
|
||||
func (artgridCollector) Enabled(enabledPlatforms map[string]bool) bool {
|
||||
return len(enabledPlatforms) == 0 || enabledPlatforms["artgrid"]
|
||||
}
|
||||
@@ -47,7 +47,7 @@ func (artgridCollector) Enrich(searcher *SearchService, result SearchResult) Sea
|
||||
type googleVideoCollector struct{}
|
||||
|
||||
func (googleVideoCollector) Name() string { return "Google Video" }
|
||||
func (googleVideoCollector) MaxResults() int { return 8 }
|
||||
func (googleVideoCollector) MaxResults() int { return 12 }
|
||||
func (googleVideoCollector) Enabled(enabledPlatforms map[string]bool) bool {
|
||||
return len(enabledPlatforms) == 0 || enabledPlatforms["google video"]
|
||||
}
|
||||
|
||||
+29
-4
@@ -38,6 +38,7 @@ const downloadLogs = document.getElementById("downloadLogs");
|
||||
const debugLogList = document.getElementById("debugLogList");
|
||||
const debugSummary = document.getElementById("debugSummary");
|
||||
const resultModal = document.getElementById("resultModal");
|
||||
const resultModalShell = document.getElementById("resultModalShell");
|
||||
const resultModalTitle = document.getElementById("resultModalTitle");
|
||||
const resultModalSource = document.getElementById("resultModalSource");
|
||||
const resultModalSnippet = document.getElementById("resultModalSnippet");
|
||||
@@ -56,6 +57,7 @@ const resultModalSecondaryAction = document.getElementById("resultModalSecondary
|
||||
const closeResultModal = document.getElementById("closeResultModal");
|
||||
const resultModalReady = Boolean(
|
||||
resultModal &&
|
||||
resultModalShell &&
|
||||
resultModalTitle &&
|
||||
resultModalSource &&
|
||||
resultModalSnippet &&
|
||||
@@ -436,12 +438,30 @@ function resetPreviewPlayer() {
|
||||
|
||||
function showModal(element) {
|
||||
setHidden(element, false);
|
||||
if (element === resultModal) {
|
||||
syncResultModalLayout();
|
||||
}
|
||||
}
|
||||
|
||||
function hideModal(element) {
|
||||
setHidden(element, true);
|
||||
}
|
||||
|
||||
function syncResultModalLayout() {
|
||||
if (!resultModalReady) {
|
||||
return;
|
||||
}
|
||||
const viewportWidth = Math.max(320, window.innerWidth || document.documentElement.clientWidth || 0);
|
||||
const viewportHeight = Math.max(320, window.innerHeight || document.documentElement.clientHeight || 0);
|
||||
const shellWidth = Math.min(1150, viewportWidth - 16);
|
||||
const shellHeight = Math.min(840, viewportHeight - 12);
|
||||
const mediaHeight = Math.max(150, Math.min(Math.floor(viewportHeight * 0.32), 320));
|
||||
resultModalShell.style.setProperty("--result-modal-shell-width", `${shellWidth}px`);
|
||||
resultModalShell.style.setProperty("--result-modal-shell-height", `${shellHeight}px`);
|
||||
resultModalShell.style.setProperty("--result-modal-media-max-height", `${mediaHeight}px`);
|
||||
resultModalShell.classList.toggle("result-modal-compact", viewportHeight < 900 || viewportWidth < 1280);
|
||||
}
|
||||
|
||||
function buildResultModalEmbedURL(item) {
|
||||
if (item?.embedUrl) {
|
||||
if (item.source === "Google Video" && item.embedUrl.includes("youtube-nocookie.com/embed/")) {
|
||||
@@ -589,7 +609,7 @@ async function fetchResultPreview(item) {
|
||||
}
|
||||
const request = (async () => {
|
||||
try {
|
||||
const preview = await api("/api/download/preview", {
|
||||
const preview = await api("/api/download/preview", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ url: key }),
|
||||
@@ -671,10 +691,10 @@ function renderResults(results) {
|
||||
node.addEventListener("click", () => openResultModal(item));
|
||||
previewVideo.poster = usableThumbnail ? item.thumbnailUrl : "";
|
||||
const mediaArea = node.querySelector(".relative");
|
||||
if (item.previewVideoUrl || item.source === "Google Video") {
|
||||
if (item.previewVideoUrl || item.source === "Google Video" || item.source === "Artgrid") {
|
||||
mediaArea.addEventListener("mouseenter", async () => {
|
||||
let previewURL = item.previewVideoUrl || "";
|
||||
if (!previewURL && item.source === "Google Video") {
|
||||
if (!previewURL && (item.source === "Google Video" || item.source === "Artgrid")) {
|
||||
const preview = await fetchResultPreview(item);
|
||||
previewURL = preview?.previewStreamUrl || "";
|
||||
}
|
||||
@@ -749,7 +769,7 @@ async function openResultModal(item) {
|
||||
const embedURL = buildResultModalEmbedURL(item);
|
||||
const fallbackReason = item.previewBlockedReason || "Embedded view was unavailable, switched to fallback preview.";
|
||||
let resolvedPreviewURL = item.previewVideoUrl || "";
|
||||
if (!resolvedPreviewURL && item.source === "Google Video") {
|
||||
if (!resolvedPreviewURL && (item.source === "Google Video" || item.source === "Artgrid")) {
|
||||
const preview = await fetchResultPreview(item);
|
||||
resolvedPreviewURL = preview?.previewStreamUrl || "";
|
||||
}
|
||||
@@ -1030,6 +1050,11 @@ if (resultModalReady) {
|
||||
}
|
||||
});
|
||||
}
|
||||
window.addEventListener("resize", () => {
|
||||
if (resultModalReady && !resultModal.classList.contains("hidden")) {
|
||||
syncResultModalLayout();
|
||||
}
|
||||
});
|
||||
for (const button of platformToggles) {
|
||||
button.addEventListener("click", () => {
|
||||
const platform = button.dataset.platformToggle;
|
||||
|
||||
+4
-4
@@ -150,7 +150,7 @@
|
||||
</div>
|
||||
|
||||
<div id="resultModal" class="fixed inset-0 z-50 hidden items-start justify-center overflow-y-auto bg-black/80 px-2 py-2 sm:px-4 sm:py-4">
|
||||
<div class="result-modal-shell flex w-full max-w-6xl min-h-0 flex-col overflow-hidden rounded-3xl border border-white/10 bg-zinc-950 shadow-2xl">
|
||||
<div id="resultModalShell" class="result-modal-shell flex w-full max-w-6xl min-h-0 flex-col overflow-hidden rounded-3xl border border-white/10 bg-zinc-950 shadow-2xl">
|
||||
<div class="flex items-center justify-between border-b border-white/10 px-4 py-3 sm:px-5 sm:py-4">
|
||||
<div class="min-w-0">
|
||||
<p id="resultModalSource" class="text-xs uppercase tracking-[0.25em] text-zinc-500"></p>
|
||||
@@ -180,13 +180,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-modal-details grid min-h-0 gap-3 px-3 py-3 sm:gap-4 sm:px-4 sm:py-4 lg:grid-cols-[1.5fr_0.8fr]">
|
||||
<div class="flex min-h-[180px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div class="flex min-h-0 min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<p class="text-xs uppercase tracking-[0.25em] text-zinc-500">AI Note</p>
|
||||
<div class="result-panel-scroll mt-3 min-h-0 flex-1 overflow-y-auto pr-2">
|
||||
<p id="resultModalReason" class="whitespace-pre-wrap text-xs leading-6 text-zinc-200 sm:text-sm"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-h-[200px] min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div class="flex min-h-0 min-w-0 flex-col rounded-2xl border border-white/10 bg-white/[0.03] p-4">
|
||||
<div class="mb-3 flex flex-col gap-2">
|
||||
<button id="resultModalDownload" type="button" class="hidden w-full rounded-2xl border border-white bg-white px-4 py-3 text-sm font-medium text-black transition hover:bg-zinc-200">
|
||||
Primary Action
|
||||
@@ -224,6 +224,6 @@
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script src="/app.js?v=20260317c" defer></script>
|
||||
<script src="/app.js?v=20260317d" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+13
-2
@@ -60,7 +60,8 @@ body {
|
||||
}
|
||||
|
||||
.result-modal-shell {
|
||||
height: min(calc(100dvh - 0.5rem), 860px);
|
||||
height: var(--result-modal-shell-height, min(calc(100dvh - 0.5rem), 860px));
|
||||
max-width: var(--result-modal-shell-width, 72rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ body {
|
||||
}
|
||||
|
||||
.result-modal-media-frame {
|
||||
max-height: min(34dvh, 22rem);
|
||||
max-height: var(--result-modal-media-max-height, min(34dvh, 22rem));
|
||||
}
|
||||
|
||||
.result-modal-details {
|
||||
@@ -79,6 +80,16 @@ body {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.result-modal-shell.result-modal-compact .result-modal-details {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.result-modal-shell.result-modal-compact #resultModalReason,
|
||||
.result-modal-shell.result-modal-compact #resultModalSnippet {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.35rem;
|
||||
}
|
||||
|
||||
#resultModalSnippet,
|
||||
#resultModalReason {
|
||||
white-space: pre-wrap;
|
||||
|
||||
Reference in New Issue
Block a user