feat(posts): extract + record external file-host links (Phase 3)
Capture off-platform links (mega/gdrive/mediafire/dropbox/pixeldrain) embedded in post bodies so they're never silently dropped, and surface them in the post view. The download worker (Phase 4) walks these rows. - link_extract.py: pure extractor — <a href> + bare URLs, unwraps Patreon redirect shims, PRESERVES the full url incl. #fragment (mega's key), dedups. Reusable by every platform (runs off Post.description). - external_link model + migration 0049: post_id/artist_id/host/url/label/status /attempts/last_error/attachment_id/timing; CHECK whitelists (full enum incl. worker statuses up front) + (post_id,url) unique. - importer._sync_external_links: insert-missing on both import paths (_apply_sidecar + upsert_post_record) so a re-import never resets a link's status; runs for all platforms. - post_feed_service.get_post: returns external_links (detail-only). - PostCard: renders the links (host chip + label + status) once expanded. - tests: extractor (5 hosts, fragment, shim unwrap, dedup), importer (record + no-dup on reimport), serializer. Refs FC #830. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,25 @@
|
||||
@click="toggleDesc"
|
||||
>{{ descExpanded ? 'Show less' : 'Show more' }}</button>
|
||||
|
||||
<!-- Off-platform file-host links (mega/gdrive/…) found in the body.
|
||||
Shown once expanded; clickable now, auto-downloaded by the worker
|
||||
slice. -->
|
||||
<div v-if="descExpanded && externalLinks.length" class="fc-post-card__links">
|
||||
<div class="fc-post-card__links-label">External files</div>
|
||||
<a
|
||||
v-for="lnk in externalLinks" :key="lnk.id"
|
||||
:href="lnk.url" target="_blank" rel="noopener noreferrer"
|
||||
class="fc-post-card__link"
|
||||
>
|
||||
<span class="fc-post-card__link-host">{{ lnk.host }}</span>
|
||||
<span class="fc-post-card__link-text">{{ lnk.label || lnk.url }}</span>
|
||||
<span
|
||||
v-if="lnk.status && lnk.status !== 'pending'"
|
||||
class="fc-post-card__link-status" :data-status="lnk.status"
|
||||
>{{ lnk.status }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="attachments.length" class="fc-post-card__atts">
|
||||
<a
|
||||
v-for="att in attachments" :key="att.id"
|
||||
@@ -201,6 +220,8 @@ const fullDescription = computed(() => detail.value?.description_full || null)
|
||||
// Backend-sanitized HTML body (detail-only). Drives the faithful render once
|
||||
// expanded; falls back to plain text when detail isn't loaded.
|
||||
const descHtml = computed(() => detail.value?.description_html || null)
|
||||
// Off-platform file-host links (detail-only), surfaced once expanded.
|
||||
const externalLinks = computed(() => detail.value?.external_links || [])
|
||||
const descText = computed(() =>
|
||||
descExpanded.value
|
||||
? (fullDescription.value || props.post.description_plain)
|
||||
@@ -469,4 +490,46 @@ function formatBytes (n) {
|
||||
}
|
||||
.fc-post-card__att-icon { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
.fc-post-card__att-size { color: rgb(var(--v-theme-on-surface-variant)); }
|
||||
|
||||
.fc-post-card__links {
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.fc-post-card__links-label {
|
||||
font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
.fc-post-card__link {
|
||||
display: inline-flex; align-items: center; gap: 8px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid rgb(var(--v-theme-on-surface-variant));
|
||||
border-radius: 8px;
|
||||
color: rgb(var(--v-theme-on-surface));
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.fc-post-card__link:hover {
|
||||
color: rgb(var(--v-theme-accent));
|
||||
border-color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
.fc-post-card__link-host {
|
||||
flex: 0 0 auto;
|
||||
font-weight: 700; text-transform: uppercase; font-size: 0.7rem;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 2px 6px; border-radius: 4px;
|
||||
background: rgba(var(--v-theme-accent), 0.16);
|
||||
color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
.fc-post-card__link-text {
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.fc-post-card__link-status {
|
||||
flex: 0 0 auto; margin-left: auto;
|
||||
font-size: 0.7rem; text-transform: uppercase;
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
}
|
||||
.fc-post-card__link-status[data-status="downloaded"] {
|
||||
color: rgb(var(--v-theme-accent));
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user