Skip to content

Commit 8b28dc0

Browse files
committed
fix: extracción de contenido erróneo cuando Jina sirve el perfil del usuario
Cuando Jina devuelve el perfil/feed de @usuario en vez del post específico, el contenido del primer post visible (el más reciente del usuario) se extraía erróneamente en lugar del post objetivo. Causa raíz: extractPostSectionMedia tomaba los primeros 3000 chars del markdown sin anclar al postId solicitado; extractFallbackTextFromSource empezaba desde línea 0 cuando el postId no se encontraba en la fuente. Fixes: - extractPostSectionMedia(jinaMarkdown, postId): si postId disponible, busca su primera aparición en el markdown y extrae media desde ese punto; si no aparece → Jina sirvió otra página → retorna [] en vez de media ajena - extractFallbackTextFromSource: si postId proporcionado pero no encontrado en la fuente, retorna undefined en vez de texto del post equivocado - Botón "Refrescar" en PostDetailScreen para re-extraer texto y metadatos de posts guardados con contenido incorrecto (sobreescribe el texto actual) - onpaste en ShareScreen: limpia ?xmt= y otros parámetros de tracking automáticamente al pegar la URL, sin que el usuario deba hacerlo a mano
1 parent 1c5c618 commit 8b28dc0

3 files changed

Lines changed: 89 additions & 13 deletions

File tree

src/lib/utils/post-extractor.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,32 @@ function extractEscapedMediaFromText(text: string): string[] {
118118

119119
/*
120120
PBL: Extrae media SOLO de la sección del post en la respuesta markdown de Jina.
121-
Jina devuelve la página completa (post + posts relacionados/sugeridos).
122-
Limitamos a los primeros 3000 chars del bloque "Markdown Content:" para
123-
quedarnos con el post objetivo y evitar imágenes de otros posts.
121+
Jina puede devolver la página completa (post + posts relacionados/perfil del usuario).
122+
123+
BUG CORREGIDO: cuando Jina sirve el perfil de @usuario en vez del post específico,
124+
el primer post visible es el más reciente del usuario (que puede ser otro post distinto).
125+
Sin anclaje al postId, cogemos media del post equivocado.
126+
127+
Fix: si tenemos postId, buscamos su primera aparición en el markdown y extraemos
128+
media desde ese punto. Si el postId no aparece → Jina sirvió otra página → retornamos [].
124129
*/
125-
function extractPostSectionMedia(jinaMarkdown: string): string[] {
130+
function extractPostSectionMedia(jinaMarkdown: string, postId: string | null): string[] {
126131
if (!jinaMarkdown) return []
132+
133+
if (postId) {
134+
const postMatch = new RegExp(`/post/${postId}\\b`, 'i').exec(jinaMarkdown)
135+
if (postMatch) {
136+
const postSection = jinaMarkdown.slice(postMatch.index, postMatch.index + 2000)
137+
return [
138+
...extractMediaFromText(postSection),
139+
...extractEscapedMediaFromText(postSection),
140+
]
141+
}
142+
// postId no encontrado → Jina sirvió perfil/feed en vez del post → no extraer media
143+
return []
144+
}
145+
146+
// Sin postId: comportamiento original (primeros 3000 chars tras "Markdown Content:")
127147
const contentMatch = /Markdown Content:\s*/i.exec(jinaMarkdown)
128148
const start = contentMatch ? contentMatch.index + contentMatch[0].length : 0
129149
const postSection = jinaMarkdown.slice(start, start + 3000)
@@ -275,6 +295,11 @@ function extractFallbackTextFromSource(source: string, postId: string | null): s
275295
? lines.findIndex((line) => new RegExp(`/post/${postId}\\b`, 'i').test(line))
276296
: -1
277297

298+
// BUG CORREGIDO: si tenemos postId pero no aparece en la fuente, Jina devolvió
299+
// otra página (perfil/feed del usuario). Es más seguro retornar undefined que
300+
// devolver texto del primer post visible, que puede ser un post diferente.
301+
if (postId && postLineIndex === -1) return undefined
302+
278303
const start = postLineIndex >= 0 ? postLineIndex + 1 : 0
279304
for (let index = start; index < Math.min(lines.length, start + 28); index += 1) {
280305
const line = lines[index]
@@ -366,7 +391,8 @@ export async function extractPostData(rawUrl: string): Promise<ExtractedPostData
366391
addMedia(forceMediaEntries([ogImage, twitterImage], 'image'))
367392
if (oembed?.html) addMedia(toMediaEntries(extractMediaFromText(oembed.html)))
368393
// PBL: jinaHtml (markdown limpio del post) en vez de source (HTML completo con otros posts)
369-
addMedia(toMediaEntries(extractPostSectionMedia(jinaHtml ?? '')))
394+
// postId ancla la extracción al bloque correcto — evita coger media de otros posts del feed
395+
addMedia(toMediaEntries(extractPostSectionMedia(jinaHtml ?? '', postId)))
370396

371397
/*
372398
PBL: Detección de vídeo por thumbnail CDN.

src/routes/PostDetailScreen.svelte

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
let savingNote = $state(false)
2222
let refreshingMedia = $state(false)
2323
let mediaRefreshError = $state('')
24+
let refreshingContent = $state(false)
2425
let failedMediaIds = $state<Set<string>>(new Set())
2526
let mediaSourceIndex = $state<Record<string, number>>({})
2627
let inlineVideoState = $state<Record<string, {
@@ -185,6 +186,33 @@
185186
].join('')
186187
}
187188
189+
// Re-extrae texto y metadatos del post desde Threads/Jina.
190+
// A diferencia de refreshMedia (que preserva el texto existente), éste lo sobreescribe.
191+
// Útil cuando la extracción inicial cogió contenido del post equivocado.
192+
async function refreshContent() {
193+
if (!post || refreshingContent) return
194+
refreshingContent = true
195+
try {
196+
const extracted = await extractPostData(post.canonicalUrl ?? post.url)
197+
const updated: Post = {
198+
...post,
199+
author: extracted.author || post.author,
200+
previewTitle: extracted.title ?? post.previewTitle,
201+
extractedText: extracted.text !== undefined ? extracted.text : post.extractedText,
202+
previewImage: extracted.previewImage ?? post.previewImage,
203+
previewVideo: extracted.previewVideo ?? post.previewVideo,
204+
}
205+
const storage = await getStorage()
206+
await storage.savePost(updated)
207+
post = updated
208+
await loadVault()
209+
} catch {
210+
// silencioso — el usuario puede reintentar
211+
} finally {
212+
refreshingContent = false
213+
}
214+
}
215+
188216
async function loadInlineVideo(media: PostMedia) {
189217
if (!post) return
190218
const current = getInlineVideoState(media)
@@ -688,21 +716,38 @@
688716
{/if}
689717
</div>
690718

691-
{#if post.extractedText}
692-
<div class="rounded-xl p-4 mb-4" style="
693-
background: rgba(0,188,212,0.08);
694-
border: 1px solid rgba(0,188,212,0.24);
695-
">
696-
<p class="text-xs font-semibold uppercase mb-1.5" style="
719+
<div class="rounded-xl p-4 mb-4" style="
720+
background: rgba(0,188,212,0.08);
721+
border: 1px solid rgba(0,188,212,0.24);
722+
">
723+
<div class="flex items-center justify-between gap-2 mb-2">
724+
<p class="text-xs font-semibold uppercase" style="
697725
color: rgba(188,248,255,0.85);
698726
font-family: var(--font-display);
699727
letter-spacing: 0.08em;
700728
">Texto extraído</p>
729+
<button
730+
onclick={refreshContent}
731+
disabled={refreshingContent}
732+
class="px-2.5 py-1 rounded-lg text-xs font-semibold transition-all duration-200 disabled:opacity-50"
733+
style="
734+
background: rgba(0,188,212,0.14);
735+
border: 1px solid rgba(0,188,212,0.32);
736+
color: #baf5ff;
737+
font-family: var(--font-display);
738+
"
739+
>{refreshingContent ? 'Extrayendo...' : 'Refrescar'}</button>
740+
</div>
741+
{#if post.extractedText}
701742
<p class="text-sm leading-relaxed" style="color: var(--vault-on-bg); opacity: 0.9">
702743
{post.extractedText}
703744
</p>
704-
</div>
705-
{/if}
745+
{:else}
746+
<p class="text-xs" style="color: var(--vault-on-bg-muted); font-style: italic">
747+
No se extrajo texto. Pulsa Refrescar para intentar de nuevo.
748+
</p>
749+
{/if}
750+
</div>
706751

707752
{#if post.media?.length}
708753
<div class="mb-4">

src/routes/ShareScreen.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@
160160
id="url-input"
161161
type="url"
162162
bind:value={url}
163+
onpaste={(e) => {
164+
e.preventDefault()
165+
const pasted = e.clipboardData?.getData('text') ?? ''
166+
url = cleanThreadsUrl(pasted.trim())
167+
}}
163168
placeholder="https://www.threads.net/@usuario/post/..."
164169
class="w-full px-4 py-3 rounded-xl text-sm outline-none transition-all duration-200"
165170
style="

0 commit comments

Comments
 (0)