Compare commits

..

16 commits

Author SHA1 Message Date
agnes
bafb19d339 Update services.js 2026-08-13 14:24:04 +00:00
agnes
20bec36c74 Update services.js 2026-08-13 14:18:45 +00:00
agnes
18a2a65458 Delete .env 2026-08-13 08:58:00 +00:00
agnes
3b290d44b6 Update .env 2026-08-13 08:57:05 +00:00
agnes
5abeb31f06 Update services.js 2026-08-13 08:56:05 +00:00
agnes
bb88932bde Update services.js 2026-08-13 08:54:42 +00:00
agnes
896919d0c3 Update services.js 2026-08-13 08:52:25 +00:00
dylan
bc18d5beca Merge pull request 'Fix regex, again!' (#5) from fix/image-updates into main
Reviewed-on: https://source.tube/dylan/now-updater/pulls/5
2025-12-31 09:30:12 +00:00
dylan
05904356b9 updated now-page-template with new image handling and layout adjustments 2025-12-31 00:19:37 +00:00
dylan
8d2c808c68 updated regex replacement to include old text in debug logging 2025-12-31 00:19:26 +00:00
dylan
7740822918 updated regex to match service by data-service attribute in html anchor tags 2025-12-31 00:19:02 +00:00
dylan
147445df66 added debug logging for regex matching and replacement in now function 2025-12-31 00:03:59 +00:00
dylan
973a774442 Merge pull request 'Update image handling' (#4) from fix/image-updates into main
Reviewed-on: https://source.tube/dylan/now-updater/pulls/4
2025-12-29 19:36:17 +00:00
dylan
185014493c updated image tags 2025-12-29 19:30:31 +00:00
dylan
61bb4d4d9e updated README to include details on new image handling 2025-12-29 19:30:19 +00:00
dylan
84878d3c17 Merge pull request 'Add Steam, Bluesky, Hardcover and more' (#3) from update/new-services into main
Reviewed-on: https://source.tube/dylan/now-updater/pulls/3
2025-12-28 22:27:17 +00:00
6 changed files with 65 additions and 46 deletions

View file

@ -1,9 +0,0 @@
OMGLOL_KEY=
OMGLOL_USERNAME=melanie
TRAKT_SLURM=
# optional - only needed if using steam service
STEAM_WEBAPI_KEY=
# optional - only needed if using hardcover service
HARDCOVER_API_KEY=

View file

@ -10,6 +10,8 @@ Demo: [dylan.omg.lol/now](https://dylan.omg.lol/now)
Fetches content from RSS feeds, JSON feeds, and different services (Letterboxd, Trakt, Last.fm, Steam, Hardcover, etc.) and updates HTML or markdown links on your Now page. Supports optional images and videos for services that provide them (These are... iffy so if something breaks it's probably this). Runs every 3 hours via Forgejo Actions, only updating your page when changes are detected. Fetches content from RSS feeds, JSON feeds, and different services (Letterboxd, Trakt, Last.fm, Steam, Hardcover, etc.) and updates HTML or markdown links on your Now page. Supports optional images and videos for services that provide them (These are... iffy so if something breaks it's probably this). Runs every 3 hours via Forgejo Actions, only updating your page when changes are detected.
Images are matched using a `data-service` attribute on the img tag, which allows them to be updated repeatedly even after the initial replacement.
### Example ### Example
Your Now page links will be automatically updated from this: Your Now page links will be automatically updated from this:
@ -54,6 +56,7 @@ For local development, copy `.env.example` to `.env` with your credentials.
```html ```html
I wrote this: <a href="https://your-blog.com">blog</a> I wrote this: <a href="https://your-blog.com">blog</a>
I watched this: <a href="https://letterboxd.com/username">letterboxd</a> I watched this: <a href="https://letterboxd.com/username">letterboxd</a>
<img data-service="letterboxd" src="letterboxd-image" style="max-width: 120px;">
I listened to: <a href="https://www.last.fm/user/username">lastfm</a> I listened to: <a href="https://www.last.fm/user/username">lastfm</a>
``` ```
@ -161,6 +164,15 @@ Many services support optional images and videos:
Control image inclusion with the `showImage` flag in your service config. Images are returned separately from text so you can control placement on your Now page. Control image inclusion with the `showImage` flag in your service config. Images are returned separately from text so you can control placement on your Now page.
To use images, your img tags must include a `data-service` attribute matching the service `id` from services.js. For example:
```html
<img data-service="letterboxd" src="letterboxd-image" style="max-width: 120px;">
<img data-service="steam" src="steam-image" style="max-width: 120px;">
```
The `src` attribute can start with a placeholder (like `letterboxd-image`) or an existing URL - the updater will replace it either way as long as the `data-service` attribute is present.
## Available Services ## Available Services
All handlers now use a config object with consistent parameters. The `feedType` in services.js maps to these handlers. All handlers now use a config object with consistent parameters. The `feedType` in services.js maps to these handlers.

View file

@ -96,7 +96,7 @@ export default async function now() {
if (latest.image) console.log(`${item.id} Image: ${latest.image}`) if (latest.image) console.log(`${item.id} Image: ${latest.image}`)
if (latest.video) console.log(`${item.id} Video: ${latest.video}`) if (latest.video) console.log(`${item.id} Video: ${latest.video}`)
// Apply markdown escaping only for markdown links // apply markdown escaping only for markdown links
const displayText = const displayText =
linkFormat === 'html' linkFormat === 'html'
? latest.text ? latest.text
@ -109,13 +109,23 @@ export default async function now() {
? htmlServiceLinkRegex(item.id) ? htmlServiceLinkRegex(item.id)
: markdownServiceLinkRegex(item.id) : markdownServiceLinkRegex(item.id)
newNow = newNow.replace(regex, (match, openTag, closeTag) => { console.log(`\nDEBUG: Testing regex for ${item.id}`)
// Replace href in opening tag console.log(`Regex: ${regex}`)
const matched = regex.test(newNow)
console.log(`Regex match found: ${matched}`)
newNow = newNow.replace(regex, (match, openTag, oldText, closeTag) => {
console.log(`DEBUG: Replacing match for ${item.id}`)
console.log(`Old match: ${match}`)
console.log(`Old text: ${oldText}`)
// replace href in opening tag
const updatedOpenTag = openTag.replace( const updatedOpenTag = openTag.replace(
/href=["'][^"']*["']/, /href=["'][^"']*["']/,
`href="${latest.url}"`, `href="${latest.url}"`,
) )
return `${updatedOpenTag}${displayText}${closeTag}` const result = `${updatedOpenTag}${displayText}${closeTag}`
console.log(`New replacement: ${result}`)
return result
}) })
// replace image/video placeholders if available, otherwise remove tags // replace image/video placeholders if available, otherwise remove tags
@ -165,9 +175,15 @@ export default async function now() {
}), }),
) )
console.log('\n=== FINAL COMPARISON ===')
console.log(`Original length: ${now.length}`)
console.log(`New length: ${newNow.length}`)
console.log(`Strings are equal: ${now === newNow}`)
if (now === newNow) { if (now === newNow) {
console.log('\nNow page has no changes') console.log('\nNow page has no changes')
} else { } else {
console.log('\nChanges detected! Updating Now page...')
await setNow(newNow) await setNow(newNow)
} }
} }

View file

@ -2,15 +2,15 @@
<p>This page <em>should</em> update automatically with my latest activity.</p> <p>This page <em>should</em> update automatically with my latest activity.</p>
<div style="display: flex; flex-direction: column; gap: 0.75rem;"> <div style="display: flex; flex-direction: column; gap: 0.75rem;">
<div style="background: rgba(166, 227, 161, 0.15); padding: 1rem; border: 4px solid #a6e3a1; color:white;text-align: left;"><div><i class="fa-brands fa-lastfm"></i> I listened to this<br><a href="https://www.last.fm/user/lookathimthere" style="color: #a6e3a1 !important;">lastfm</a></div></div> <div style="background: rgba(166, 227, 161, 0.15); padding: 1rem; border: 4px solid #a6e3a1; color:white;text-align: left;"><div><i class="fa-brands fa-lastfm"></i> I listened to this<br><a data-service="lastfm" href="https://www.last.fm/user/lookathimthere" style="color: #a6e3a1 !important;">lastfm</a></div></div>
<div style="background: rgba(250, 179, 135, 0.15); padding: 1rem; border: 4px solid #fab387; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid fa-tv"></i> I watched this episode<br><a href="https://trakt.tv/episodes" style="color: #fab387 !important;">trakt-episode</a></div><img src="trakt-episode-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div> <div style="background: rgba(250, 179, 135, 0.15); padding: 1rem; border: 4px solid #fab387; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid fa-tv"></i> I watched this episode<br><a data-service="trakt-episode" href="https://trakt.tv/episodes" style="color: #fab387 !important;">trakt-episode</a></div><img data-service="trakt-episode" src="trakt-episode-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div>
<div style="background: rgba(250, 179, 135, 0.15); padding: 1rem; border: 4px solid #fab387; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid fa-film"></i> I watched this movie<br><a href="https://trakt.tv/movies/" style="color: #fab387 !important;">trakt-movie</a></div><img src="trakt-movie-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div> <div style="background: rgba(250, 179, 135, 0.15); padding: 1rem; border: 4px solid #fab387; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid fa-film"></i> I watched this movie<br><a data-service="trakt-movie" href="https://trakt.tv/movies/" style="color: #fab387 !important;">trakt-movie</a></div><img data-service="trakt-movie" src="trakt-movie-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div>
<div style="background: rgba(243, 139, 168, 0.15); padding: 1rem; border: 4px solid #f38ba8; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid omg-icon omg-letterboxd"></i> I rated this<br><a href="https://letterboxd.com/STFUDonny" style="color: #f38ba8 !important;">letterboxd</a></div><img src="letterboxd-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div> <div style="background: rgba(243, 139, 168, 0.15); padding: 1rem; border: 4px solid #f38ba8; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid omg-icon omg-letterboxd"></i> I rated this<br><a data-service="letterboxd" href="https://letterboxd.com/STFUDonny" style="color: #f38ba8 !important;">letterboxd</a></div><img data-service="letterboxd" src="letterboxd-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div>
<div style="background: rgba(180, 190, 254, 0.15); padding: 1rem; border: 4px solid #b4befe; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid fa-book"></i> I'm reading this<br><a href="https://hardcover.app" style="color: #b4befe !important;">hardcover</a></div><img src="hardcover-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div> <div style="background: rgba(180, 190, 254, 0.15); padding: 1rem; border: 4px solid #b4befe; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-solid fa-book"></i> I'm reading this<br><a data-service="hardcover" href="https://hardcover.app" style="color: #b4befe !important;">hardcover</a></div><img data-service="hardcover" src="hardcover-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div>
<div style="background: rgba(137, 180, 250, 0.15); padding: 1rem; border: 4px solid #89b4fa; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-brands fa-steam"></i> I played this<br><a href="https://store.steampowered.com" style="color: #89b4fa !important;">steam</a></div><img src="steam-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div> <div style="background: rgba(137, 180, 250, 0.15); padding: 1rem; border: 4px solid #89b4fa; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-brands fa-steam"></i> I played this<br><a data-service="steam" href="https://store.steampowered.com" style="color: #89b4fa !important;">steam</a></div><img data-service="steam" src="steam-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div>
<div style="background: rgba(148, 226, 213, 0.15); padding: 1rem; border: 4px solid #94e2d5; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-regular fa-images"></i> I shared this photo<br><a href="https://dylan.some.pics" style="color: #94e2d5 !important;">some.pics</a></div><img src="some.pics-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div> <div style="background: rgba(148, 226, 213, 0.15); padding: 1rem; border: 4px solid #94e2d5; color:white; display: flex; justify-content: space-between; align-items: center;"><div style="text-align: left;"><i class="fa-regular fa-images"></i> I shared this photo<br><a data-service="some.pics" href="https://dylan.some.pics" style="color: #94e2d5 !important;">some.pics</a></div><img data-service="some.pics" src="some.pics-image" style="max-width: 120px; max-height: 120px; object-fit: cover; border-radius: 4px;"></div>
<div style="background: rgba(249, 226, 175, 0.15); padding: 1rem; border: 4px solid #f9e2af; color:white;text-align: left;"><i class="fa-regular fa-newspaper"></i>I wrote this<br><a href="https://dylan.weblog.lol" style="color: #f9e2af !important;">weblog</a></div> <div style="background: rgba(249, 226, 175, 0.15); padding: 1rem; border: 4px solid #f9e2af; color:white;text-align: left;"><i class="fa-regular fa-newspaper"></i>I wrote this<br><a data-service="weblog" href="https://dylan.weblog.lol" style="color: #f9e2af !important;">weblog</a></div>
<div style="background: rgba(137, 220, 235, 0.15); padding: 1rem; border: 4px solid #89dceb; color:white;text-align: left;"><i class="fa-solid fa-code"></i> I pushed some (bad) code<br><a href="https://source.tube/dylan" style="color: #89dceb !important;">source.tube</a></div> <div style="background: rgba(137, 220, 235, 0.15); padding: 1rem; border: 4px solid #89dceb; color:white;text-align: left;"><i class="fa-solid fa-code"></i> I pushed some (bad) code<br><a data-service="source.tube" href="https://source.tube/dylan" style="color: #89dceb !important;">source.tube</a></div>
</div> </div>
<h5>{last-updated}</h5> <h5>{last-updated}</h5>

View file

@ -7,14 +7,14 @@ export const services = [
// ======================================================================== // ========================================================================
{ {
id: 'weblog', id: 'weblog',
isActive: true, isActive: false,
userId: 'dylan', // your omg.lol username userId: 'dylan', // your omg.lol username
feedUrlTemplate: 'https://{userId}.weblog.lol/feed.json', feedUrlTemplate: 'https://{userId}.weblog.lol/feed.json',
feedType: 'json', feedType: 'json',
}, },
{ {
id: 'some.pics', id: 'some.pics',
isActive: true, isActive: false,
userId: 'dylan', // your omg.lol username userId: 'dylan', // your omg.lol username
feedUrlTemplate: 'https://{userId}.some.pics/rss', feedUrlTemplate: 'https://{userId}.some.pics/rss',
feedType: 'some.pics', feedType: 'some.pics',
@ -25,9 +25,9 @@ export const services = [
// ======================================================================== // ========================================================================
{ {
id: 'blog', id: 'blog',
isActive: false, isActive: true,
feedUrl: 'https://your-blog.com/feed.xml', // direct url to your blog's rss feed feedUrl: 'https://blog.agnes.love/feed/', // direct url to your blog's rss feed
feedType: 'rss', feedType: 'atom',
}, },
// ======================================================================== // ========================================================================
@ -36,7 +36,7 @@ export const services = [
{ {
id: 'lastfm', id: 'lastfm',
isActive: true, isActive: true,
userId: 'lookathimthere', // your last.fm username userId: 'keegbovo', // your last.fm username
feedUrlTemplate: 'https://lfm.xiffy.nl/{userId}', feedUrlTemplate: 'https://lfm.xiffy.nl/{userId}',
feedType: 'rss', feedType: 'rss',
}, },
@ -59,11 +59,11 @@ export const services = [
// ======================================================================== // ========================================================================
{ {
id: 'mastodon', id: 'mastodon',
isActive: false, isActive: true,
userId: 'dylan', // your mastodon username userId: 'alien', // your mastodon username
instance: 'social.lol', // your mastodon instance instance: 'lesbian.alien.dentist', // your mastodon instance
feedUrlTemplate: 'https://{instance}/@{userId}.rss', feedUrlTemplate: 'lesbian.alien.dentist/@alien.atom',
feedType: 'mastodon', feedType: 'atom',
showImage: true, showImage: true,
}, },
{ {
@ -76,9 +76,9 @@ export const services = [
}, },
{ {
id: 'bluesky', id: 'bluesky',
isActive: false, isActive: true,
userId: 'your-handle.bsky.social', // your bluesky handle userId: 'agnes.love', // your bluesky handle
feedUrlTemplate: 'https://bsky.app/profile/{userId}/rss', feedUrlTemplate: 'https://bsky.app/profile/agnes.love/rss',
feedType: 'rss', feedType: 'rss',
}, },
@ -87,7 +87,7 @@ export const services = [
// ======================================================================== // ========================================================================
{ {
id: 'letterboxd', id: 'letterboxd',
isActive: true, isActive: false,
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',
@ -95,7 +95,7 @@ export const services = [
}, },
{ {
id: 'trakt-episode', id: 'trakt-episode',
isActive: true, isActive: false,
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',
@ -103,7 +103,7 @@ export const services = [
}, },
{ {
id: 'trakt-movie', id: 'trakt-movie',
isActive: true, isActive: false,
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',
@ -115,7 +115,7 @@ export const services = [
// ======================================================================== // ========================================================================
{ {
id: 'hardcover', id: 'hardcover',
isActive: true, isActive: false,
userId: '62036', // your hardcover user ID (find in your API token or profile URL) userId: '62036', // your hardcover user ID (find in your API token or profile URL)
feedUrl: 'https://api.hardcover.app/v1/graphql', feedUrl: 'https://api.hardcover.app/v1/graphql',
feedType: 'hardcover', feedType: 'hardcover',
@ -123,8 +123,8 @@ export const services = [
}, },
{ {
id: 'storygraph', id: 'storygraph',
isActive: false, isActive: true,
userId: 'your-username', // your storygraph username userId: 'telescopefish', // your storygraph username
feedUrlTemplate: 'https://app.thestorygraph.com/profile/{userId}.rss', feedUrlTemplate: 'https://app.thestorygraph.com/profile/{userId}.rss',
feedType: 'rss', feedType: 'rss',
}, },
@ -141,7 +141,7 @@ export const services = [
// ======================================================================== // ========================================================================
{ {
id: 'steam', id: 'steam',
isActive: true, isActive: false,
userId: '76561198022952207', // your steam ID 64 (not username) - convert at steamid.io userId: '76561198022952207', // your steam ID 64 (not username) - convert at steamid.io
feedType: 'steam', feedType: 'steam',
}, },
@ -151,7 +151,7 @@ export const services = [
// ======================================================================== // ========================================================================
{ {
id: 'source.tube', id: 'source.tube',
isActive: true, isActive: false,
userId: 'dylan', // your source.tube username userId: 'dylan', // your source.tube username
feedUrlTemplate: 'https://source.tube/{userId}.rss', feedUrlTemplate: 'https://source.tube/{userId}.rss',
feedType: 'source.tube', feedType: 'source.tube',

View file

@ -6,11 +6,11 @@ import sanitizeHtml from 'sanitize-html'
// helper functions // helper functions
// ======================================================================== // ========================================================================
// regex to match service name within an html anchor tag // regex to match service by data-service attribute within an html anchor tag
// used when linkFormat is 'html' in config.js // used when linkFormat is 'html' in config.js
export function htmlServiceLinkRegex(serviceName) { export function htmlServiceLinkRegex(serviceName) {
return new RegExp( return new RegExp(
`(<a[^>]+href=["'][^"']*["'][^>]*>)${serviceName}(<\\/a>)`, `(<a[^>]*data-service=["']${serviceName}["'][^>]*>)([^<]*)(<\\/a>)`,
'i', 'i',
) )
} }