fix: a Discord source takes only messages with an image attached, from the posters it names, and knows its channel's name (#4481)
CI and images / lint (push) Successful in 4s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 21s
CI and images / extension-test (push) Successful in 20s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Successful in 2m24s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 7s
CI and images / build-web (push) Successful in 1m42s
CI and images / smoke-web (push) Successful in 59s
CI and images / promote (push) Successful in 2s
CI and images / lint (push) Successful in 4s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 21s
CI and images / extension-test (push) Successful in 20s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Successful in 2m24s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 7s
CI and images / build-web (push) Successful in 1m42s
CI and images / smoke-web (push) Successful in 59s
CI and images / promote (push) Successful in 2s
- A message with no image or video attachment is chat: extract_media returns nothing for it, so nothing downloads and no post record is written. A message that passes keeps every file, numbered as gallery-dl numbers them. - `discord_authors` in a source's config limits the walk to those posters (id, username or display name); the edit dialog has a field for it and no longer drops config keys it has no field for. - source.display_name (migration 0115), refreshed by every Discord walk, shows on Subscriptions in place of the two-id URL; a Discord post card names its channel from the record's `channel`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
:to="{ name: 'artist', params: { slug: post.artist.slug } }"
|
||||
class="fc-post-card__artist"
|
||||
>{{ post.artist.name }}</RouterLink>
|
||||
<span v-if="post.channel" class="fc-post-card__meta">#{{ post.channel }}</span>
|
||||
<span class="fc-post-card__date" :title="absoluteDate">{{ relativeDate }}</span>
|
||||
<span v-if="totalImages" class="fc-post-card__meta">
|
||||
· {{ totalImages }} image{{ totalImages === 1 ? '' : 's' }}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<a
|
||||
:href="source.url" target="_blank" rel="noopener"
|
||||
class="fc-source-card__url" @click.stop
|
||||
>{{ source.url }}</a>
|
||||
>{{ source.display_name || source.url }}</a>
|
||||
<v-btn
|
||||
icon="mdi-pencil" size="x-small" variant="text"
|
||||
@click.stop="$emit('edit', source)"
|
||||
|
||||
@@ -44,6 +44,14 @@
|
||||
v-model="structuredSince" label="Skip posts older than (YYYY-MM-DD)"
|
||||
placeholder="2024-01-01" hide-details class="mt-2"
|
||||
/>
|
||||
<!-- A creator's server is full of other members posting their own
|
||||
pictures; the source is subscribed to the creator (#4481). -->
|
||||
<v-text-field
|
||||
v-if="platform === 'discord'"
|
||||
v-model="structuredAuthors" label="Only posts by (Discord names or ids)"
|
||||
placeholder="Todding" hint="Comma-separated. Empty takes everyone's."
|
||||
persistent-hint class="mt-2"
|
||||
/>
|
||||
<p class="text-caption mt-2" style="opacity: 0.75">
|
||||
More per-platform fields land here over time. Use Advanced JSON for everything else.
|
||||
</p>
|
||||
@@ -98,6 +106,24 @@ const urlError = ref('')
|
||||
const configTab = ref('structured')
|
||||
const structuredVideos = ref(true)
|
||||
const structuredSince = ref('')
|
||||
const structuredAuthors = ref('')
|
||||
// Keys the structured view has no field for, carried through its saves so
|
||||
// switching tabs never drops what the JSON view set.
|
||||
const otherConfig = ref({})
|
||||
const STRUCTURED_KEYS = ['videos', 'since', 'discord_authors']
|
||||
|
||||
function splitAuthors(txt) {
|
||||
return (txt || '').split(',').map(s => s.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
function takeConfig(co) {
|
||||
structuredVideos.value = co.videos !== false
|
||||
structuredSince.value = co.since ?? ''
|
||||
structuredAuthors.value = Array.isArray(co.discord_authors) ? co.discord_authors.join(', ') : ''
|
||||
otherConfig.value = Object.fromEntries(
|
||||
Object.entries(co).filter(([k]) => !STRUCTURED_KEYS.includes(k)),
|
||||
)
|
||||
}
|
||||
const jsonText = ref('{}')
|
||||
const jsonError = ref('')
|
||||
|
||||
@@ -105,13 +131,15 @@ const busy = ref(false)
|
||||
|
||||
// Sync config_overrides between the two views.
|
||||
const config = computed(() => {
|
||||
const out = {}
|
||||
const out = { ...otherConfig.value }
|
||||
if (!structuredVideos.value) out.videos = false
|
||||
if (structuredSince.value) out.since = structuredSince.value
|
||||
const authors = splitAuthors(structuredAuthors.value)
|
||||
if (authors.length) out.discord_authors = authors
|
||||
return out
|
||||
})
|
||||
|
||||
watch([structuredVideos, structuredSince], () => {
|
||||
watch([structuredVideos, structuredSince, structuredAuthors], () => {
|
||||
if (configTab.value === 'structured') {
|
||||
jsonText.value = JSON.stringify(config.value, null, 2)
|
||||
jsonError.value = ''
|
||||
@@ -128,8 +156,7 @@ watch(jsonText, (txt) => {
|
||||
}
|
||||
jsonError.value = ''
|
||||
// Reflect recognized keys into the structured view.
|
||||
structuredVideos.value = parsed.videos !== false
|
||||
structuredSince.value = parsed.since ?? ''
|
||||
takeConfig(parsed)
|
||||
} catch {
|
||||
jsonError.value = 'Invalid JSON'
|
||||
}
|
||||
@@ -146,14 +173,13 @@ watch(() => props.modelValue, async (open) => {
|
||||
url.value = props.source.url
|
||||
enabled.value = props.source.enabled
|
||||
const co = props.source.config_overrides || {}
|
||||
structuredVideos.value = co.videos !== false
|
||||
structuredSince.value = co.since ?? ''
|
||||
takeConfig(co)
|
||||
jsonText.value = JSON.stringify(co, null, 2)
|
||||
artistChoice.value = { id: props.source.artist_id, name: props.source.artist_name }
|
||||
} else {
|
||||
platform.value = platformsStore.list[0]?.key || 'patreon'
|
||||
url.value = ''; enabled.value = true
|
||||
structuredVideos.value = true; structuredSince.value = ''
|
||||
takeConfig({})
|
||||
jsonText.value = '{}'
|
||||
artistChoice.value = props.initialArtist
|
||||
? { id: props.initialArtist.id, name: props.initialArtist.name }
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="fc-source-row__url-wrap">
|
||||
<a :href="source.url" target="_blank" rel="noopener" class="fc-source-row__url"
|
||||
@click.stop>
|
||||
{{ source.url }}
|
||||
{{ source.display_name || source.url }}
|
||||
</a>
|
||||
<!-- Edit sits next to the source identity (operator-requested), not in
|
||||
the action cluster where it was easy to fat-finger Remove. -->
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
v-if="item.singleSource"
|
||||
:href="item.singleSource.url" target="_blank" rel="noopener"
|
||||
class="fc-subs__sub-url" @click.stop
|
||||
>{{ item.singleSource.url }}</a>
|
||||
>{{ item.singleSource.display_name || item.singleSource.url }}</a>
|
||||
</template>
|
||||
|
||||
<template #item.platforms="{ item }">
|
||||
@@ -517,6 +517,7 @@ const filteredGroups = computed(() => {
|
||||
g.sources.some(
|
||||
(s) =>
|
||||
(s.url || '').toLowerCase().includes(q) ||
|
||||
(s.display_name || '').toLowerCase().includes(q) ||
|
||||
(s.platform || '').toLowerCase().includes(q),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user