Merge pull request 'add direct linking to individual items from feeds' (#1) from update/direct-links into main

Reviewed-on: https://source.tube/dylan/now-updater/pulls/1
This commit is contained in:
dylan 2025-12-25 19:27:25 +00:00
commit a3ed5a4a82
4 changed files with 59 additions and 26 deletions

View file

@ -5,7 +5,8 @@ import {
getMalojaScrobble, getMalojaScrobble,
getReadingBooklogr, getReadingBooklogr,
getRSSItemTitle, getRSSItemTitle,
getTraktEpisodeAndMovie, getTraktEpisode,
getTraktMovie,
htmlLinkRegex, htmlLinkRegex,
} from './utils.js' } from './utils.js'
@ -28,9 +29,14 @@ export const items = [
getLatest: async () => getLetterboxdActivity('stfudonny', false), getLatest: async () => getLetterboxdActivity('stfudonny', false),
}, },
{ {
id: 'trakt', id: 'trakt-episode',
regex: htmlLinkRegex('trakt.tv/users/crankle'), regex: htmlLinkRegex('trakt.tv/users/crankle'),
getLatest: async () => getLatest: async () =>
getTraktEpisodeAndMovie('crankle', '78e1e87b446901f7e4f0883dd4995cec', false), getTraktEpisode('crankle', '78e1e87b446901f7e4f0883dd4995cec', false),
},
{
id: 'trakt-movie',
regex: htmlLinkRegex('trakt.tv/users/crankle'),
getLatest: async () => getTraktMovie('crankle', false),
}, },
] ]

View file

@ -34,9 +34,17 @@ export default async function now() {
items.map(async ({ id, regex, getLatest }) => { items.map(async ({ id, regex, getLatest }) => {
try { try {
const latest = await getLatest() const latest = await getLatest()
console.log(`${id}: ${latest}`) console.log(`${id}: ${latest.text}`)
if (latest) { console.log(`${id} URL: ${latest.url}`)
newNow = newNow.replace(regex, `$1${latest}$2`) if (latest && latest.text && latest.url) {
newNow = newNow.replace(regex, (match, openTag, closeTag) => {
// Replace href in opening tag
const updatedOpenTag = openTag.replace(
/href=["'][^"']*["']/,
`href="${latest.url}"`,
)
return `${updatedOpenTag}${latest.text}${closeTag}`
})
} }
} catch (e) { } catch (e) {
console.warn(`⚠️ Failed to fetch ${id}`, e) console.warn(`⚠️ Failed to fetch ${id}`, e)

View file

@ -15,7 +15,10 @@
I rated this<br>{letterboxd} <a href="https://letterboxd.com/STFUDonny" style="color: #f38ba8 !important;">tktk</a> I rated this<br>{letterboxd} <a href="https://letterboxd.com/STFUDonny" style="color: #f38ba8 !important;">tktk</a>
</div> </div>
<div style="background: rgba(250, 179, 135, 0.15); padding: 1rem; border: 4px solid #fab387; color:white;"> <div style="background: rgba(250, 179, 135, 0.15); padding: 1rem; border: 4px solid #fab387; color:white;">
I watched this<br>{trakt} <a href="https://trakt.tv/users/crankle" style="color: #fab387 !important;">tktk</a> I watched this episode<br>
<i class="fa-solid fa-tv"></i> <a href="https://trakt.tv/users/crankle" style="color: #fab387 !important;">tktk</a><br>
and this movie<br>
<i class="fa-solid fa-film"></i> <a href="https://trakt.tv/users/crankle" style="color: #fab387 !important;">tktk</a>
</div> </div>
</div> </div>

View file

@ -38,27 +38,31 @@ export function markdownCharEscape(text) {
export async function getRSSItemTitle(feedUrl) { export async function getRSSItemTitle(feedUrl) {
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) const dom = new JSDOM(data, { contentType: 'text/xml' })
const item = dom.window.document.querySelector('item') const item = dom.window.document.querySelector('item')
const title = item.querySelector('title').textContent const title = item.querySelector('title').textContent
return markdownCharEscape( const link = item.querySelector('link').textContent.trim()
const cleanTitle = markdownCharEscape(
sanitizeHtml(title, { sanitizeHtml(title, {
allowedTags: [], allowedTags: [],
allowedAttributes: {}, allowedAttributes: {},
})?.trim(), })?.trim(),
) )
return { text: cleanTitle, url: link }
} }
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 } = post const { title, image, url } = post
const cleanTitle = markdownCharEscape(title) const cleanTitle = markdownCharEscape(title)
if (!showImage) { const text = !showImage
return cleanTitle ? cleanTitle
} : image
return image ? `${cleanTitle} ![${cleanTitle}](${image})` : cleanTitle ? `${cleanTitle} ![${cleanTitle}](${image})`
: cleanTitle
return { text, url }
} }
export async function getJsonFeedItemContent(feedUrl, showImage = true) { export async function getJsonFeedItemContent(feedUrl, showImage = true) {
@ -93,15 +97,22 @@ export async function getMalojaScrobble(malojaUrl) {
export async function getLetterboxdActivity(username, showImage = true) { export async function getLetterboxdActivity(username, showImage = true) {
const res = await fetch(`https://letterboxd.com/${username}/rss/`) const res = await fetch(`https://letterboxd.com/${username}/rss/`)
const data = await res.text() const data = await res.text()
const dom = new JSDOM(data) const dom = new JSDOM(data, { contentType: 'text/xml' })
const item = dom.window.document.querySelector('item') const item = dom.window.document.querySelector('item')
const title = item.querySelector('title').textContent const title = item.querySelector('title').textContent
const link = item.querySelector('link').textContent.trim()
const cleanTitle = markdownCharEscape(title) const cleanTitle = markdownCharEscape(title)
const image = item.querySelector('description > img').src
if (!showImage) { let text = cleanTitle
return cleanTitle if (showImage) {
// Parse CDATA content as HTML to extract image
const description = item.querySelector('description').textContent
const imgMatch = description.match(/<img[^>]+src="([^"]+)"/)
const image = imgMatch ? imgMatch[1] : null
text = image ? `${cleanTitle} ![${cleanTitle}](${image})` : cleanTitle
} }
return `${cleanTitle} ![${cleanTitle}](${image})`
return { text, url: link }
} }
export async function getTraktEpisode(username, id, showImage = true) { export async function getTraktEpisode(username, id, showImage = true) {
@ -112,11 +123,12 @@ export async function getTraktEpisode(username, id, showImage = true) {
const dom = new JSDOM(data) const dom = new JSDOM(data)
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 cleanTitle = markdownCharEscape(title) const cleanTitle = markdownCharEscape(title)
if (!showImage) { const text = !showImage
return cleanTitle ? cleanTitle
} : `${cleanTitle} ![${cleanTitle}](https://widgets.trakt.tv/users/${id}/watched/thumb@2x.jpg?type=episode&image_only=1)`
return `${cleanTitle} ![${cleanTitle}](https://widgets.trakt.tv/users/${id}/watched/thumb@2x.jpg?type=episode&image_only=1)` return { text, url: link }
} }
export async function getTraktMovie(username, showImage = true) { export async function getTraktMovie(username, showImage = true) {
@ -127,13 +139,17 @@ export async function getTraktMovie(username, showImage = true) {
const dom = new JSDOM(data) const dom = new JSDOM(data)
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
return markdownCharEscape(title) const link = entry.querySelector('link').getAttribute('href')
return { text: markdownCharEscape(title), url: link }
} }
export async function getTraktEpisodeAndMovie(username, id, showImage = true) { export async function getTraktEpisodeAndMovie(username, id, showImage = true) {
const episode = await getTraktEpisode(username, id, showImage) const episode = await getTraktEpisode(username, id, showImage)
const movie = await getTraktMovie(username, showImage) const movie = await getTraktMovie(username, showImage)
return `${episode} <br>and<br> ${movie}` return {
text: `${episode.text} <br>and<br> ${movie.text}`,
url: `https://trakt.tv/users/${username}`,
}
} }
export async function getReadingBooklogr(booklogrUrl, booklogrUser) { export async function getReadingBooklogr(booklogrUrl, booklogrUser) {