updated to include image extraction and separate image urls

This commit is contained in:
dylan 2025-12-28 17:45:59 +00:00
parent acdb9ff8d7
commit 689079cab4
3 changed files with 49 additions and 26 deletions

View file

@ -79,7 +79,7 @@ export default async function now() {
} }
// generic handlers expect just feedUrl string, special handlers expect config object // generic handlers expect just feedUrl string, special handlers expect config object
const handlerParam = ['rss', 'atom', 'json', 'mastodon', 'maloja'].includes( const handlerParam = ['rss', 'atom', 'json', 'maloja'].includes(
item.feedType, item.feedType,
) )
? feedUrl ? feedUrl
@ -109,6 +109,21 @@ export default async function now() {
) )
return `${updatedOpenTag}${displayText}${closeTag}` return `${updatedOpenTag}${displayText}${closeTag}`
}) })
// replace image placeholders if image is available
if (latest.image) {
const imageId = `${item.id}-image`
// html image replacement
newNow = newNow.replace(
new RegExp(`(<img[^>]+src=["'])${imageId}(["'][^>]*>)`, 'gi'),
`$1${latest.image}$2`,
)
// markdown image replacement
newNow = newNow.replace(
new RegExp(`(!\\[[^\\]]*\\]\\()${imageId}(\\))`, 'gi'),
`$1${latest.image}$2`,
)
}
} }
} catch (e) { } catch (e) {
console.warn(`⚠️ Failed to fetch ${item.id}`, e) console.warn(`⚠️ Failed to fetch ${item.id}`, e)

View file

@ -64,6 +64,7 @@ export const services = [
instance: 'social.lol', // your mastodon instance instance: 'social.lol', // your mastodon instance
feedUrlTemplate: 'https://{instance}/@{userId}.rss', feedUrlTemplate: 'https://{instance}/@{userId}.rss',
feedType: 'mastodon', feedType: 'mastodon',
showImage: false,
}, },
{ {
id: 'pixelfed', id: 'pixelfed',
@ -90,7 +91,7 @@ export const services = [
userId: 'stfudonny', // your letterboxd username userId: 'stfudonny', // your letterboxd username
feedUrlTemplate: 'https://letterboxd.com/{userId}/rss/', feedUrlTemplate: 'https://letterboxd.com/{userId}/rss/',
feedType: 'letterboxd', feedType: 'letterboxd',
showImage: false, showImage: true,
}, },
{ {
id: 'trakt-episode', id: 'trakt-episode',
@ -98,7 +99,7 @@ export const services = [
userId: 'crankle', // your trakt username userId: 'crankle', // your trakt username
traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID
feedType: 'trakt-episode', feedType: 'trakt-episode',
showImage: false, showImage: true,
}, },
{ {
id: 'trakt-movie', id: 'trakt-movie',
@ -106,7 +107,7 @@ export const services = [
userId: 'crankle', // your trakt username userId: 'crankle', // your trakt username
traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID
feedType: 'trakt-movie', feedType: 'trakt-movie',
showImage: false, showImage: true,
}, },
// ======================================================================== // ========================================================================

View file

@ -78,18 +78,13 @@ export async function getAtomFeed(url) {
} }
// json feed parser - grabs the latest item title // json feed parser - grabs the latest item title
// optionally includes images in markdown format // optionally includes images separately
export async function getJsonFeedItemTitle(feedUrl, showImage = true) { export async function getJsonFeedItemTitle(feedUrl, showImage = true) {
const res = await fetch(feedUrl) const res = await fetch(feedUrl)
const data = await res.json() const data = await res.json()
const post = data.items[0] const post = data.items[0]
const { title, image, url } = post const { title, image, url } = post
const text = !showImage return { text: title, url, image: showImage ? image : null }
? title
: image
? `${title} ![${title}](${image})`
: title
return { text, url }
} }
// ======================================================================== // ========================================================================
@ -133,7 +128,10 @@ export async function getMalojaScrobble(malojaUrl) {
// mastodon rss feed parser - filters out replies (posts starting with @) // mastodon rss feed parser - filters out replies (posts starting with @)
// also replaces urls with 🔗 emoji to keep things tidy // also replaces urls with 🔗 emoji to keep things tidy
export async function getMastodonPost(feedUrl) { // extracts images from posts if available
export async function getMastodonPost(config) {
const { feedUrl, showImage = false } =
typeof config === 'string' ? { feedUrl: config } : config
const res = await fetch(feedUrl) const res = await fetch(feedUrl)
const data = await res.text() const data = await res.text()
const dom = new JSDOM(data, { contentType: 'text/xml' }) const dom = new JSDOM(data, { contentType: 'text/xml' })
@ -152,6 +150,10 @@ export async function getMastodonPost(feedUrl) {
const link = item.querySelector('link')?.textContent.trim() const link = item.querySelector('link')?.textContent.trim()
// extract image from description html if available
const imgMatch = description.match(/<img[^>]+src="([^"]+)"/)
const image = imgMatch ? imgMatch[1] : null
// use description as the text, stripping HTML // use description as the text, stripping HTML
let cleanText = sanitizeHtml(description, { let cleanText = sanitizeHtml(description, {
allowedTags: [], allowedTags: [],
@ -161,20 +163,26 @@ export async function getMastodonPost(feedUrl) {
// replace URLs with a link emoji // replace URLs with a link emoji
cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim() cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim()
return { text: cleanText, url: link } return { text: cleanText, url: link, image: showImage ? image : null }
} }
// fallback to first item if no non-reply found // fallback to first item if no non-reply found
const firstItem = items[0] const firstItem = items[0]
const description = firstItem.querySelector('description')?.textContent || '' const description = firstItem.querySelector('description')?.textContent || ''
const link = firstItem.querySelector('link')?.textContent.trim() const link = firstItem.querySelector('link')?.textContent.trim()
// extract image from description html if available
const imgMatch = description.match(/<img[^>]+src="([^"]+)"/)
const image = imgMatch ? imgMatch[1] : null
let cleanText = sanitizeHtml(description, { let cleanText = sanitizeHtml(description, {
allowedTags: [], allowedTags: [],
allowedAttributes: {}, allowedAttributes: {},
})?.trim() })?.trim()
// replace URLs with link emoji // replace URLs with link emoji
cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim() cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim()
return { text: cleanText, url: link }
return { text: cleanText, url: link, image: showImage ? image : null }
} }
// ======================================================================== // ========================================================================
@ -192,16 +200,15 @@ export async function getLetterboxdActivity(config) {
const title = item.querySelector('title').textContent const title = item.querySelector('title').textContent
const link = item.querySelector('link').textContent.trim() const link = item.querySelector('link').textContent.trim()
let text = title let image = null
if (showImage) { if (showImage) {
// Parse CDATA content as HTML to extract image // Parse CDATA content as HTML to extract image
const description = item.querySelector('description').textContent const description = item.querySelector('description').textContent
const imgMatch = description.match(/<img[^>]+src="([^"]+)"/) const imgMatch = description.match(/<img[^>]+src="([^"]+)"/)
const image = imgMatch ? imgMatch[1] : null image = imgMatch ? imgMatch[1] : null
text = image ? `${title} ![${title}](${image})` : title
} }
return { text, url: link } return { text: title, url: link, image }
} }
// trakt episode history - requires TRAKT_SLURM variable (see readme for info) // trakt episode history - requires TRAKT_SLURM variable (see readme for info)
@ -216,10 +223,10 @@ export async function getTraktEpisode(config) {
const entry = dom.window.document.querySelector('entry') const entry = dom.window.document.querySelector('entry')
const title = entry.querySelector('title').textContent const title = entry.querySelector('title').textContent
const link = entry.querySelector('link').getAttribute('href') const link = entry.querySelector('link').getAttribute('href')
const text = !showImage const image = showImage
? title ? `https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=episode&image_only=1`
: `${title} ![${title}](https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=episode&image_only=1)` : null
return { text, url: link } return { text: title, url: link, image }
} }
// trakt movie history - requires TRAKT_SLURM variable (see readme for info) // trakt movie history - requires TRAKT_SLURM variable (see readme for info)
@ -234,10 +241,10 @@ export async function getTraktMovie(config) {
const entry = dom.window.document.querySelector('entry') const entry = dom.window.document.querySelector('entry')
const title = entry.querySelector('title').textContent const title = entry.querySelector('title').textContent
const link = entry.querySelector('link').getAttribute('href') const link = entry.querySelector('link').getAttribute('href')
const text = !showImage const image = showImage
? title ? `https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=movie&image_only=1`
: `${title} ![${title}](https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=movie&image_only=1)` : null
return { text, url: link } return { text: title, url: link, image }
} }
// ======================================================================== // ========================================================================