From 7e9be02de644fc9e2b5b3332bf99a30c004dbdbb Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 16:06:58 +0000 Subject: [PATCH 01/43] added optional STEAM_WEBAPI_KEY --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 622360c..a06013d 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ OMGLOL_KEY= OMGLOL_USERNAME=melanie TRAKT_SLURM= + +# optional - only needed if using steam service +STEAM_WEBAPI_KEY= From b3b8658b720ac23190763559ce8bb338e550cc83 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 16:52:26 +0000 Subject: [PATCH 02/43] moved services from config.js into their own services.js --- config.js | 61 ++-------------------- services.js | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 57 deletions(-) create mode 100644 services.js diff --git a/config.js b/config.js index d5cd3f3..f440cca 100644 --- a/config.js +++ b/config.js @@ -1,60 +1,7 @@ -import { - getJsonFeedItemContent, - getJsonFeedItemTitle, - getLetterboxdActivity, - getMalojaScrobble, - getMastodonPost, - getReadingBooklogr, - getRSSItemTitle, - getTraktEpisode, - getTraktMovie, - htmlLinkRegex, -} from './utils.js' +import { services } from './services.js' -// Set to 'html' for HTML links or 'markdown' for markdown links +// set to 'html' for HTML links or 'markdown' for markdown links export const linkFormat = 'html' -export const items = [ - { - id: 'blog', - regex: htmlLinkRegex('dylan.weblog.lol'), - getLatest: async () => - getJsonFeedItemTitle('https://dylan.weblog.lol/feed.json'), - }, - { - id: 'pics', - regex: htmlLinkRegex('some.pics'), - getLatest: async () => - getRSSItemTitle('https://dylan.some.pics/rss'), - }, - { - id: 'lastfm', - regex: htmlLinkRegex('www.last.fm/music'), - getLatest: async () => - getRSSItemTitle('https://lfm.xiffy.nl/lookathimthere'), - }, - { - id: 'mastodon', - regex: htmlLinkRegex('social.lol/@dylan'), - getLatest: async () => - getMastodonPost('https://social.lol/@dylan.rss'), - }, - { - id: 'letterboxd', - regex: htmlLinkRegex('letterboxd.com/STFUDonny'), - getLatest: async () => - getLetterboxdActivity('stfudonny', false), - }, - { - id: 'trakt-episode', - regex: htmlLinkRegex('trakt.tv/episodes'), - getLatest: async () => - getTraktEpisode('crankle', '78e1e87b446901f7e4f0883dd4995cec', false), - }, - { - id: 'trakt-movie', - regex: htmlLinkRegex('trakt.tv/movies'), - getLatest: async () => - getTraktMovie('crankle', '78e1e87b446901f7e4f0883dd4995cec', false), - }, -] +// filter to only active services - edit services.js to toggle isActive or customise +export const items = services.filter((s) => s.isActive) diff --git a/services.js b/services.js new file mode 100644 index 0000000..34aab01 --- /dev/null +++ b/services.js @@ -0,0 +1,145 @@ +// just flip isActive to true/false to enable/disable services +// customise usernames/URLs/IDs for each service + +export const services = [ + // ======================================================================== + // omg.lol services + // ======================================================================== + { + id: 'weblog', + isActive: true, + userId: 'dylan', // your omg.lol username + feedUrlTemplate: 'https://{userId}.weblog.lol/feed.json', + feedType: 'json', + }, + { + id: 'some.pics', + isActive: true, + userId: 'dylan', // your omg.lol username + feedUrlTemplate: 'https://{userId}.some.pics/rss', + feedType: 'rss', + }, + + // ======================================================================== + // music + // ======================================================================== + { + id: 'lastfm', + isActive: true, + userId: 'lookathimthere', // your last.fm username + feedUrlTemplate: 'https://lfm.xiffy.nl/{userId}', + feedType: 'rss', + }, + { + id: 'listenbrainz', + isActive: false, + userId: 'your-username', // your listenbrainz username + feedUrlTemplate: 'https://api.listenbrainz.org/1/user/{userId}/listens?count=1', + feedType: 'listenbrainz', + }, + + // ======================================================================== + // social media + // ======================================================================== + { + id: 'mastodon', + isActive: true, + userId: 'dylan', // your mastodon username + instance: 'social.lol', // your mastodon instance + feedUrlTemplate: 'https://{instance}/@{userId}.rss', + feedType: 'mastodon', + }, + { + id: 'pixelfed', + isActive: false, + userId: 'your-username', // your pixelfed username + instance: 'pixelfed.instance', // your pixelfed instance + feedUrlTemplate: 'https://{instance}/@{userId}.atom', + feedType: 'atom', + }, + { + id: 'bluesky', + isActive: false, + userId: 'your-handle.bsky.social', // your bluesky handle + feedUrlTemplate: 'https://bsky.app/profile/{userId}/rss', + feedType: 'rss', + }, + + // ======================================================================== + // movies & tv + // ======================================================================== + { + id: 'letterboxd', + isActive: true, + userId: 'stfudonny', // your letterboxd username + feedUrlTemplate: 'https://letterboxd.com/{userId}/rss/', + feedType: 'letterboxd', + showImage: false, + }, + { + id: 'trakt-episode', + isActive: true, + userId: 'crankle', // your trakt username + traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID + feedType: 'trakt-episode', + showImage: false, + }, + { + id: 'trakt-movie', + isActive: true, + userId: 'crankle', // your trakt username + traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID + feedType: 'trakt-movie', + showImage: false, + }, + + // ======================================================================== + // books + // ======================================================================== + { + id: 'hardcover', + isActive: true, + userId: 'itsdylan', // your hardcover username + feedUrl: 'https://hardcover.app/api/graphql', + feedType: 'hardcover', + }, + + // ======================================================================== + // gaming + // ======================================================================== + { + id: 'steam', + isActive: true, + userId: '76561198022952207', // your steam ID 64 (not username) - convert at steamid.io + feedType: 'steam', + }, + + // ======================================================================== + // code & development + // ======================================================================== + { + id: 'source.tube', + isActive: true, + userId: 'dylan', // your source.tube username + feedUrlTemplate: 'https://source.tube/{userId}.rss', + feedType: 'source.tube', + }, + + // ======================================================================== + // content creation + // ======================================================================== + { + id: 'youtube', + isActive: false, + userId: 'UC_your_channel_id', // your youtube channel ID (starts with UC, not your username) + feedUrlTemplate: 'https://www.youtube.com/feeds/videos.xml?channel_id={userId}', + feedType: 'atom', + }, + { + id: 'twitch', + isActive: false, + userId: 'your-channel', // your twitch channel name + feedUrlTemplate: 'https://twitchrss.appspot.com/vod/{userId}', + feedType: 'rss', + }, +] From 385f24a28a9b87a0dd8179a4c9d4f4d231f8d349 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 16:56:40 +0000 Subject: [PATCH 03/43] updated feed handling to use dedicated handlers for each feed type --- index.js | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 5253352..d2ce2fe 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,36 @@ import 'dotenv/config' import { items, linkFormat } from './config.js' -import { markdownCharEscape } from './utils.js' +import { + getAtomFeed, + getHardcoverActivity, + getJsonFeedItemTitle, + getLetterboxdActivity, + getListenBrainzScrobble, + getMastodonPost, + getRSSItemTitle, + getSourceTubeActivity, + getSteamRecentlyPlayed, + getTraktEpisode, + getTraktMovie, + htmlServiceLinkRegex, + markdownCharEscape, + markdownServiceLinkRegex, +} from './utils.js' + +const feedHandlers = { + rss: getRSSItemTitle, + atom: getAtomFeed, + json: getJsonFeedItemTitle, + 'source.tube': getSourceTubeActivity, + mastodon: getMastodonPost, + letterboxd: getLetterboxdActivity, + 'trakt-episode': getTraktEpisode, + 'trakt-movie': getTraktMovie, + steam: getSteamRecentlyPlayed, + listenbrainz: getListenBrainzScrobble, + hardcover: getHardcoverActivity, +} const OMGLOL_API = `https://api.omg.lol/address/${process.env.OMGLOL_USERNAME}/now` @@ -32,11 +61,24 @@ export default async function now() { let newNow = now await Promise.all( - items.map(async ({ id, regex, getLatest }) => { + items.map(async (item) => { try { - const latest = await getLatest() - console.log(`${id}: ${latest.text}`) - console.log(`${id} URL: ${latest.url}`) + const handler = feedHandlers[item.feedType] + if (!handler) { + throw new Error(`Unknown feedType: ${item.feedType}`) + } + + // build feedUrl from template if provided + let feedUrl = item.feedUrl + if (item.feedUrlTemplate) { + feedUrl = item.feedUrlTemplate + .replace('{userId}', item.userId || '') + .replace('{instance}', item.instance || '') + } + + const latest = await handler(feedUrl || { ...item, feedUrl }) + console.log(`${item.id}: ${latest.text}`) + console.log(`${item.id} URL: ${latest.url}`) // Apply markdown escaping only for markdown links const displayText = @@ -45,7 +87,13 @@ export default async function now() { : markdownCharEscape(latest.text) if (latest && latest.text && latest.url) { - newNow = newNow.replace(regex, (match, openTag, closeTag) => { + // generate regex based on id and linkFormat + const regex = + linkFormat === 'html' + ? htmlServiceLinkRegex(item.id) + : markdownServiceLinkRegex(item.id) + + newNow = newNow.replace(regex, (match, openTag, closeTag) => { // Replace href in opening tag const updatedOpenTag = openTag.replace( /href=["'][^"']*["']/, From 8f807cb47a8d57d16245bd76bd181ee0a1f4f333 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 16:57:45 +0000 Subject: [PATCH 04/43] removed local development files from .gitignore --- .gitignore | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 302d61e..ea3925c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,3 @@ .DS_Store node_modules/* -.env -# Local development files -fetch-now.js -now-local.html -preview-now.sh \ No newline at end of file +.env \ No newline at end of file From cc64e56b77d8fffe323f8e09044637be81ba896a Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 16:57:57 +0000 Subject: [PATCH 05/43] removed biome.json and lefthook.yml configuration files --- biome.json | 36 ------------------------------------ lefthook.yml | 6 ------ 2 files changed, 42 deletions(-) delete mode 100644 biome.json delete mode 100644 lefthook.yml diff --git a/biome.json b/biome.json deleted file mode 100644 index 720c7db..0000000 --- a/biome.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", - "vcs": { - "enabled": true, - "clientKind": "git", - "useIgnoreFile": true, - "defaultBranch": "main" - }, - "files": { - "ignoreUnknown": false - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "single", - "semicolons": "asNeeded" - } - }, - "assist": { - "enabled": true, - "actions": { - "source": { - "organizeImports": "on" - } - } - } -} diff --git a/lefthook.yml b/lefthook.yml deleted file mode 100644 index e068607..0000000 --- a/lefthook.yml +++ /dev/null @@ -1,6 +0,0 @@ -pre-commit: - commands: - check: - glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" - run: npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} - stage_fixed: true From db25612241c2c548c23e9be45bbf0126cfc1cf3b Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 16:58:45 +0000 Subject: [PATCH 06/43] updated now-page-template.md layout --- now-page-template.md | 89 +++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index 16c021f..b48eeb6 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -1,65 +1,70 @@ # What I've been up to -This page automatically updates with my latest activity. +This page _should_ update automatically with my latest activity.
-
- I wrote this
tktk -
- I posted this
- tktk + I posted this
+ tktk
I shared this photo
tktk
- I listened to this
- tktk -
+
+ I wrote this
tktk +
+ I listened to this
+ tktk +
I rated this
tktk
- I watched this episode
- tktk
- and this movie
- tktk -
+ I watched this episode
+ tktk
+ and this movie
+ tktk + ##### {last-updated} -

Want your own automatically updated Now page? Check out now-updater.

+

Want your own automatically updated Now page? Check out now-updater.

---
- - People Pledge 88x31 Badge - - Follow me on Mastodon - @dylan@social.lol 88x31 Badge - - Badly Hand-Coded and Proud 88x31 Badge - - omg.lol 88x31 Badge - - Anna's Archive 88x31 Badge - - Internet Privacy 88x31 Badge - - omg.lol 88x31 Badge + + People Pledge 88x31 Badge + + Follow me on Mastodon - @dylan@social.lol 88x31 Badge + + Badly Hand-Coded and Proud 88x31 Badge + + omg.lol 88x31 Badge + + Anna's Archive 88x31 Badge + + Internet Privacy 88x31 Badge + + omg.lol 88x31 Badge
### [Back to my omg.lol page!](https://dylan.omg.lol) \ No newline at end of file From 64488f88edc5325905d1d2113cdc667d212ef45d Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:22:14 +0000 Subject: [PATCH 07/43] added some examples --- services.js | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/services.js b/services.js index 34aab01..7d8c344 100644 --- a/services.js +++ b/services.js @@ -20,6 +20,16 @@ export const services = [ feedType: 'rss', }, + // ======================================================================== + // blogging & writing + // ======================================================================== + { + id: 'blog', + isActive: false, + feedUrl: 'https://your-blog.com/feed.xml', // direct url to your blog's rss feed + feedType: 'rss', + }, + // ======================================================================== // music // ======================================================================== @@ -37,6 +47,12 @@ export const services = [ feedUrlTemplate: 'https://api.listenbrainz.org/1/user/{userId}/listens?count=1', feedType: 'listenbrainz', }, + { + id: 'maloja', + isActive: false, + feedUrl: 'https://your-maloja-instance.com', // your maloja instance url + feedType: 'maloja', + }, // ======================================================================== // social media @@ -103,6 +119,20 @@ export const services = [ feedUrl: 'https://hardcover.app/api/graphql', feedType: 'hardcover', }, + { + id: 'storygraph', + isActive: false, + userId: 'your-username', // your storygraph username + feedUrlTemplate: 'https://app.thestorygraph.com/profile/{userId}.rss', + feedType: 'rss', + }, + { + id: 'goodreads', + isActive: false, + userId: 'your-user-id', // your goodreads user id (numbers from your profile url) + feedUrlTemplate: 'https://www.goodreads.com/user/updates_rss/{userId}', + feedType: 'rss', + }, // ======================================================================== // gaming @@ -124,9 +154,23 @@ export const services = [ feedUrlTemplate: 'https://source.tube/{userId}.rss', feedType: 'source.tube', }, + { + id: 'gitlab', + isActive: false, + userId: 'your-username', // your gitlab username + feedUrlTemplate: 'https://gitlab.com/users/{userId}/activity.atom', + feedType: 'atom', + }, + { + id: 'codeberg', + isActive: false, + userId: 'your-username', // your codeberg username + feedUrlTemplate: 'https://codeberg.org/{userId}.rss', + feedType: 'rss', + }, // ======================================================================== - // content creation + // video & streaming // ======================================================================== { id: 'youtube', From e971dbf0ba1dc5d8c1d5561b52ac284f38ab0449 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:23:18 +0000 Subject: [PATCH 08/43] updated regex and reorganised and added headings to match services.js --- utils.js | 279 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 210 insertions(+), 69 deletions(-) diff --git a/utils.js b/utils.js index 39aceec..0a59f69 100644 --- a/utils.js +++ b/utils.js @@ -2,21 +2,26 @@ import 'dotenv/config' import { JSDOM } from 'jsdom' import sanitizeHtml from 'sanitize-html' -export function markdownLinkRegex(url) { +// ======================================================================== +// helper functions +// ======================================================================== + +// regex to match service name within an html anchor tag +// used when linkFormat is 'html' in config.js +export function htmlServiceLinkRegex(serviceName) { return new RegExp( - `(\\[).*?(\\]\\(https:\\/\\/${url.replaceAll('.', '\\.')}\\))`, + `(]+href=["'][^"']*["'][^>]*>)${serviceName}(<\\/a>)`, 'i', ) } -export function htmlLinkRegex(url) { - const escapedUrl = url.replaceAll('.', '\\.') - return new RegExp( - `(]+href=["']https?:\\/\\/${escapedUrl}[^"']*["'][^>]*>).*?(<\\/a>)`, - 'i', - ) +// regex to match service name within a markdown link +// used when linkFormat is 'markdown' in config.js +export function markdownServiceLinkRegex(serviceName) { + return new RegExp(`(\\[)${serviceName}(\\]\\([^)]*\\))`, 'i') } +// escapes special markdown characters so they display literally export function markdownCharEscape(text) { return text .replaceAll('`', '\\`') @@ -35,6 +40,11 @@ export function markdownCharEscape(text) { .replaceAll('!', '\\!') } +// ======================================================================== +// generic feed parsers +// ======================================================================== + +// rss feed parser - grabs the latest item from any standard rss feed export async function getRSSItemTitle(feedUrl) { const res = await fetch(feedUrl) const data = await res.text() @@ -49,6 +59,80 @@ export async function getRSSItemTitle(feedUrl) { return { text: cleanTitle, url: link } } +// atom feed parser - grabs the latest entry from any standard atom feed +export async function getAtomFeed(url) { + const res = await fetch(url) + const data = await res.text() + const dom = new JSDOM(data, { contentType: 'text/xml' }) + const entry = dom.window.document.querySelector('entry') + if (!entry) { + throw new Error('No recent items found') + } + const title = entry.querySelector('title').textContent + const link = entry.querySelector('link').getAttribute('href') + const cleanTitle = sanitizeHtml(title, { + allowedTags: [], + allowedAttributes: {}, + })?.trim() + return { text: cleanTitle, url: link } +} + +// json feed parser - grabs the latest item title +// optionally includes images in markdown format +export async function getJsonFeedItemTitle(feedUrl, showImage = true) { + const res = await fetch(feedUrl) + const data = await res.json() + const post = data.items[0] + const { title, image, url } = post + const text = !showImage + ? title + : image + ? `${title} ![${title}](${image})` + : title + return { text, url } +} + +// ======================================================================== +// music +// ======================================================================== + +// listenbrainz json api - grabs your most recent listen +// returns "track name
by artist name" +export async function getListenBrainzScrobble(config) { + const { feedUrl, userId } = config + + const res = await fetch(feedUrl) + const data = await res.json() + if (!data.payload.listens || data.payload.listens.length === 0) { + throw new Error('No recent listens found') + } + const listen = data.payload.listens[0] + const { track_name, artist_name } = listen.track_metadata + const text = `${track_name}
by ${artist_name}` + const url = `https://listenbrainz.org/user/${userId}/` + return { text, url } +} + +// maloja music scrobbling api - grabs your most recent listen +// returns "track name
by artist name" +// not configured in services.js by default but kept from upstream +export async function getMalojaScrobble(malojaUrl) { + const res = await fetch(`${malojaUrl}/apis/mlj_1/scrobbles?perpage=1`) + const data = await res.json() + const scrobble = data.list[0] + const { + title, + album: { artists }, + } = scrobble.track + return `${title}
by ${artists.join(', ')}` +} + +// ======================================================================== +// social media +// ======================================================================== + +// mastodon rss feed parser - filters out replies (posts starting with @) +// also replaces urls with 🔗 emoji to keep things tidy export async function getMastodonPost(feedUrl) { const res = await fetch(feedUrl) const data = await res.text() @@ -61,7 +145,7 @@ export async function getMastodonPost(feedUrl) { for (const item of items) { const description = item.querySelector('description')?.textContent || '' - // skip if it's a reply (starts with @ mention) + // skip if it is a reply if (description.trim().startsWith('@')) { continue } @@ -74,7 +158,7 @@ export async function getMastodonPost(feedUrl) { allowedAttributes: {}, })?.trim() - // replace URLs with link emoji + // replace URLs with a link emoji cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim() return { text: cleanText, url: link } @@ -93,49 +177,15 @@ export async function getMastodonPost(feedUrl) { return { text: cleanText, url: link } } -export async function getJsonFeedItemTitle(feedUrl, showImage = true) { +// ======================================================================== +// movies & tv +// ======================================================================== + +// letterboxd rss parser - can optionally extract poster images from cdata +// images are embedded in the description field as html +export async function getLetterboxdActivity(config) { + const { feedUrl, showImage = false } = config const res = await fetch(feedUrl) - const data = await res.json() - const post = data.items[0] - const { title, image, url } = post - const text = !showImage - ? title - : image - ? `${title} ![${title}](${image})` - : title - return { text, url } -} - -export async function getJsonFeedItemContent(feedUrl, showImage = true) { - const res = await fetch(feedUrl) - const data = await res.json() - const post = data.items[0] - const { content_html, image, url } = post - const title = sanitizeHtml(content_html, { - allowedTags: [], - allowedAttributes: {}, - })?.trim() - const text = !showImage - ? title - : image - ? `${title} ![${title}](${image})` - : title - return { text, url } -} - -export async function getMalojaScrobble(malojaUrl) { - const res = await fetch(`${malojaUrl}/apis/mlj_1/scrobbles?perpage=1`) - const data = await res.json() - const scrobble = data.list[0] - const { - title, - album: { artists }, - } = scrobble.track - return `${title}
by ${artists.join(', ')}` -} - -export async function getLetterboxdActivity(username, showImage = true) { - const res = await fetch(`https://letterboxd.com/${username}/rss/`) const data = await res.text() const dom = new JSDOM(data, { contentType: 'text/xml' }) const item = dom.window.document.querySelector('item') @@ -154,9 +204,12 @@ export async function getLetterboxdActivity(username, showImage = true) { return { text, url: link } } -export async function getTraktEpisode(username, id, showImage = true) { +// trakt episode history - requires TRAKT_SLURM variable (see readme for info) +// can optionally include trakt widget thumbnail image +export async function getTraktEpisode(config) { + const { userId, traktId, showImage = false } = config const res = await fetch( - `https://trakt.tv/users/${username}/history/episodes/added/asc.atom?slurm=${process.env.TRAKT_SLURM}`, + `https://trakt.tv/users/${userId}/history/episodes/added/asc.atom?slurm=${process.env.TRAKT_SLURM}`, ) const data = await res.text() const dom = new JSDOM(data) @@ -165,13 +218,16 @@ export async function getTraktEpisode(username, id, showImage = true) { const link = entry.querySelector('link').getAttribute('href') const text = !showImage ? title - : `${title} ![${title}](https://widgets.trakt.tv/users/${id}/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)` return { text, url: link } } -export async function getTraktMovie(username, id, showImage = true) { +// trakt movie history - requires TRAKT_SLURM variable (see readme for info) +// can optionally include trakt widget thumbnail image +export async function getTraktMovie(config) { + const { userId, traktId, showImage = false } = config const res = await fetch( - `https://trakt.tv/users/${username}/history/movies/added/asc.atom?slurm=${process.env.TRAKT_SLURM}`, + `https://trakt.tv/users/${userId}/history/movies/added/asc.atom?slurm=${process.env.TRAKT_SLURM}`, ) const data = await res.text() const dom = new JSDOM(data) @@ -180,24 +236,109 @@ export async function getTraktMovie(username, id, showImage = true) { const link = entry.querySelector('link').getAttribute('href') const text = !showImage ? title - : `${title} ![${title}](https://widgets.trakt.tv/users/${id}/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)` return { text, url: link } } -export async function getTraktEpisodeAndMovie(username, id, showImage = true) { - const episode = await getTraktEpisode(username, id, showImage) - const movie = await getTraktMovie(username, id, showImage) +// ======================================================================== +// books +// ======================================================================== + +// hardcover graphql api - grabs your currently reading book +export async function getHardcoverActivity(config) { + const { userId, feedUrl } = config + const query = ` + query { + user(username: "${userId}") { + currently_reading_books(limit: 1) { + title + id + } + } + } + ` + + const res = await fetch(feedUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query }), + }) + + const data = await res.json() + if ( + !data.data?.user?.currently_reading_books || + data.data.user.currently_reading_books.length === 0 + ) { + throw new Error('Nothing!') + } + + const book = data.data.user.currently_reading_books[0] return { - text: `${episode.text}
and
${movie.text}`, - url: `https://trakt.tv/users/${username}`, + text: book.title, + url: `https://hardcover.app/books/${book.id}`, } } -export async function getReadingBooklogr(booklogrUrl, booklogrUser) { - const res = await fetch(`${booklogrUrl}/v1/profiles/${booklogrUser}`) - const data = await res.json() - const books = data.books.filter( - ({ reading_status }) => reading_status === 'Currently reading', +// ======================================================================== +// gaming +// ======================================================================== + +// steam web api - grabs your most recently played game +// requires STEAM_WEBAPI_KEY see readme for more info +// needs your steam id 64 (not username) - convert at steamid.io +export async function getSteamRecentlyPlayed(config) { + const { steamId } = config + const apiKey = process.env.STEAM_WEBAPI_KEY + if (!apiKey) { + throw new Error('STEAM_WEBAPI_KEY environment variable not set') + } + + const res = await fetch( + `https://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v1/?key=${process.env.STEAM_WEBAPI_KEY}&steamid=${steamId}&format=json`, ) - return books.map(({ title }) => title).join(',
') + const data = await res.json() + + if (!data.response.games || data.response.games.length === 0) { + throw new Error('No recent games found') + } + + const game = data.response.games[0] + const { name, appid } = game + + return { + text: name, + url: `https://store.steampowered.com/app/${appid}`, + } +} + +// ======================================================================== +// code & development +// ======================================================================== + +// source.tube (forgejo) rss parser - strips username from the beginning +// turns "dylan pushed an update to..." into "pushed an update to..." +export async function getSourceTubeActivity(config) { + const { feedUrl, userId } = config + + const res = await fetch(feedUrl) + const data = await res.text() + const dom = new JSDOM(data, { contentType: 'text/xml' }) + const item = dom.window.document.querySelector('item') + if (!item) { + throw new Error('No recent activity found') + } + const title = item.querySelector('title').textContent + const link = item.querySelector('link').textContent.trim() + let cleanTitle = sanitizeHtml(title, { + allowedTags: [], + allowedAttributes: {}, + })?.trim() + + // remove username prefix (e.g., "dylan pushed..." -> "pushed...") + if (userId) { + const usernamePattern = new RegExp(`^${userId}\\s+`, 'i') + cleanTitle = cleanTitle.replace(usernamePattern, '') + } + + return { text: cleanTitle, url: link } } From 001834fe64f2b7a35efbac95e993133af5542fb9 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:23:27 +0000 Subject: [PATCH 09/43] added Maloja scrobble handler to feedHandlers --- README.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++------ index.js | 2 ++ 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5a0f3d8..bd3ec9f 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,13 @@ Your Now page links will be automatically updated from this: **HTML links** (before): ```html -tktk +blog +gitlab ``` **Markdown links** (before): ```markdown -[tktk](https://your-blog.com) +[blog](service-link) +[gitlab](service-link) ``` to this: @@ -27,12 +29,16 @@ to this: **HTML links** (after): ```html My Latest Post +Pushed to repository ``` **Markdown links** (after): ```markdown [My Latest Post](https://your-blog.com/2025/12/my-latest-post) +[Pushed to repository](https://gitlab.com/username/project) ``` +**New in this fork**: Use service names as link text instead of URLs! Just write `servicename` and the script fills in both the text and URL. + ## Setup 1. Fork this repo @@ -76,15 +82,78 @@ export const items = [ ## Configuration -Edit `config.js` to define your content sources. Each item needs: -- `id`: identifier for logs -- `regex`: pattern to match your link -- `getLatest`: async function that fetches and returns latest content +### Quick Start -Set `linkFormat` to `'html'` or `'markdown'` depending on your Now page format. +1. **In your Now page**, use service names as link text: +```html +blog +gitlab +``` + +2. **In services.js**, find the service and flip `isActive` to `true`: +```javascript +{ + id: 'gitlab', + isActive: true, // just change this! + regex: htmlServiceLinkRegex('gitlab'), + feedUrl: 'https://gitlab.com/users/your-username/activity.atom', + feedType: 'atom', +} +``` + +That's it! No importing, no commenting/uncommenting. + +### Files + +- **config.js** - Link format setting and filter logic (rarely needs editing) +- **services.js** - All available services with `isActive` flags (edit this to enable/disable services) +- **utils.js** - Generic feed handlers and special service logic + +### Service Structure + +Each service has: +- `id`: identifier for logs +- `isActive`: `true` to enable, `false` to disable +- `regex`: matches service name in your Now page +- `feedType`: Handler to use (`rss`, `atom`, `json`, or special handler name like `mastodon`) +- `feedUrl`: Direct feed URL (use this for custom feeds without usernames) +- `feedUrlTemplate`: URL template with `{userId}` placeholder (use this for standard services) +- `userId`: Your username/ID (only needed with feedUrlTemplate) +- Extra params as needed (e.g., `instance`, `steamId`, `traktId`, `showImage`) + +**Note**: Use either `feedUrl` (direct) OR `feedUrlTemplate` + `userId` (template). Both work! + +Set `linkFormat` in config.js to `'html'` or `'markdown'`. ## Available Functions +### Code Activity +- `getSourceTubeActivity(username)` - Forgejo activity on source.tube +- `getGitLabActivity(username)` - GitLab activity feed +- `getCodebergActivity(username)` - Codeberg activity feed + +### Content Creation +- `getYouTubeLatestVideo(channelId)` - Latest YouTube video (requires channel ID starting with "UC") +- `getVimeoLatestVideo(username)` - Latest Vimeo video +- `getTwitchLatestStream(channel)` - Latest Twitch stream/VOD (via TwitchRSS, no API key needed) + +### Gaming +- `getBackloggdActivity(username, showImage)` - Backloggd game activity with optional cover images +- `getExophaseActivity(username, showImage)` - Multi-platform gaming achievements +- `getSteamRecentlyPlayed(steamId)` - Recently played Steam game (requires STEAM_WEBAPI_KEY) + +### Books +- `getGoodreadsActivity(userId, showImage)` - Goodreads activity (requires numeric user ID, not username) +- `getHardcoverActivity(username)` - Currently reading books via GraphQL API + +### Music +- `getListenBrainzScrobble(username)` - Latest music scrobble (open source Last.fm alternative) + +### Social Media +- `getPixelfedPost(instance, username)` - Latest Pixelfed post from federated instance +- `getBlueskyPost(handle)` - Latest Bluesky post (note: links not clickable in RSS) + +### General/Legacy - `getRSSItemTitle(feedUrl)` - Standard RSS feeds - `getJsonFeedItemTitle(feedUrl, showImage)` - JSON feeds - `getLetterboxdActivity(username, showImage)` - Letterboxd activity @@ -94,11 +163,17 @@ Set `linkFormat` to `'html'` or `'markdown'` depending on your Now page format. - `getMastodonPost(feedUrl)` - Latest non-reply Mastodon post - `getMalojaScrobble(url)` - Latest music scrobble from [maloja](https://github.com/krateng/maloja) -**Notes:** +**Important Notes:** - `showImage` (defaults to `true`) controls whether images/posters are included in the output. Set it to `false` to show text only. - For Last.fm, use `getRSSItemTitle('https://lfm.xiffy.nl/your-username')` (via [lfm.xiffy.nl](https://lfm.xiffy.nl)) +- **YouTube** requires channel ID (not username), find it in your channel's page source or URL +- **Goodreads** requires numeric user ID (found in profile URL after `/user/show/`) +- **Steam** requires Steam ID 64 (convert at https://steamid.io/) +- **Pixelfed** requires instance domain and username (e.g., `pixelfed.social`, `username`) -## Trakt +## API Keys Setup + +### Trakt To get your Trakt slurm key, follow these steps: @@ -107,7 +182,15 @@ To get your Trakt slurm key, follow these steps: 3. The URL it shows will look something like this: `https://trakt.tv/users/crankle/history.atom?slurm=your-slurm-key` 4. Copy the `slurm` value from the URL and add it as a secret in your Actions settings named `TRAKT_SLURM`. -There may be a better way to get this key, but this is how I found it. +### Steam + +To get your Steam Web API key: + +1. Visit https://steamcommunity.com/dev/apikey +2. Enter a domain name (can be localhost for personal use) +3. Agree to the terms and get your API key +4. Add it to your `.env` file as `STEAM_WEBAPI_KEY` or to Actions secrets +5. Find your Steam ID 64 at https://steamid.io/ (enter your profile URL) ## Credits diff --git a/index.js b/index.js index d2ce2fe..d39ba99 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ import { getJsonFeedItemTitle, getLetterboxdActivity, getListenBrainzScrobble, + getMalojaScrobble, getMastodonPost, getRSSItemTitle, getSourceTubeActivity, @@ -29,6 +30,7 @@ const feedHandlers = { 'trakt-movie': getTraktMovie, steam: getSteamRecentlyPlayed, listenbrainz: getListenBrainzScrobble, + maloja: getMalojaScrobble, hardcover: getHardcoverActivity, } From c196e8f328ef85d2524fb900bd32d8a66d6b7e70 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:27:20 +0000 Subject: [PATCH 10/43] updated now-page-template.md with new things --- now-page-template.md | 46 ++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index b48eeb6..da49d17 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -2,31 +2,43 @@ This page _should_ update automatically with my latest activity.
-
- I posted this
- tktk -
-
- I shared this photo
tktk -
-
- I wrote this
tktk -
I listened to this
tktk -
-
- I rated this
tktk + style="color: #a6e3a1 !important;">lastfm
I watched this episode
tktk
+ style="color: #fab387 !important;">trakt-episode
and this movie
- tktk + trakt-movie +
+
+ I rated this
letterboxd +
+
+ I'm reading this
+ hardcover +
+
+ I played this
+ steam +
+
+ I tooted this
+ mastodon +
+
+ I shared this photo
some.pics +
+
+ I wrote this
weblog +
+
+ I pushed some (bad) code
+ source.tube
From df031eaa4a21bdd5de129d2f7630f7ec4326e827 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:29:09 +0000 Subject: [PATCH 11/43] fixed an error with the error --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index d39ba99..9b287ba 100644 --- a/index.js +++ b/index.js @@ -105,7 +105,7 @@ export default async function now() { }) } } catch (e) { - console.warn(`⚠️ Failed to fetch ${id}`, e) + console.warn(`⚠️ Failed to fetch ${item.id}`, e) } }), ) From acdb9ff8d7644459e693f47ae08e55f37bc73fda Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:32:17 +0000 Subject: [PATCH 12/43] updated to differentiate between generic and special feeds --- index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 9b287ba..c387272 100644 --- a/index.js +++ b/index.js @@ -78,7 +78,13 @@ export default async function now() { .replace('{instance}', item.instance || '') } - const latest = await handler(feedUrl || { ...item, feedUrl }) + // generic handlers expect just feedUrl string, special handlers expect config object + const handlerParam = ['rss', 'atom', 'json', 'mastodon', 'maloja'].includes( + item.feedType, + ) + ? feedUrl + : { ...item, feedUrl } + const latest = await handler(handlerParam) console.log(`${item.id}: ${latest.text}`) console.log(`${item.id} URL: ${latest.url}`) From 689079cab4b5c5c71e032825484f65b360494fab Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:45:59 +0000 Subject: [PATCH 13/43] updated to include image extraction and separate image urls --- index.js | 17 ++++++++++++++++- services.js | 7 ++++--- utils.js | 51 +++++++++++++++++++++++++++++---------------------- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index c387272..15d15c4 100644 --- a/index.js +++ b/index.js @@ -79,7 +79,7 @@ export default async function now() { } // 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, ) ? feedUrl @@ -109,6 +109,21 @@ export default async function now() { ) 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(`(]+src=["'])${imageId}(["'][^>]*>)`, 'gi'), + `$1${latest.image}$2`, + ) + // markdown image replacement + newNow = newNow.replace( + new RegExp(`(!\\[[^\\]]*\\]\\()${imageId}(\\))`, 'gi'), + `$1${latest.image}$2`, + ) + } } } catch (e) { console.warn(`⚠️ Failed to fetch ${item.id}`, e) diff --git a/services.js b/services.js index 7d8c344..a52273a 100644 --- a/services.js +++ b/services.js @@ -64,6 +64,7 @@ export const services = [ instance: 'social.lol', // your mastodon instance feedUrlTemplate: 'https://{instance}/@{userId}.rss', feedType: 'mastodon', + showImage: false, }, { id: 'pixelfed', @@ -90,7 +91,7 @@ export const services = [ userId: 'stfudonny', // your letterboxd username feedUrlTemplate: 'https://letterboxd.com/{userId}/rss/', feedType: 'letterboxd', - showImage: false, + showImage: true, }, { id: 'trakt-episode', @@ -98,7 +99,7 @@ export const services = [ userId: 'crankle', // your trakt username traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID feedType: 'trakt-episode', - showImage: false, + showImage: true, }, { id: 'trakt-movie', @@ -106,7 +107,7 @@ export const services = [ userId: 'crankle', // your trakt username traktId: '78e1e87b446901f7e4f0883dd4995cec', // your trakt ID feedType: 'trakt-movie', - showImage: false, + showImage: true, }, // ======================================================================== diff --git a/utils.js b/utils.js index 0a59f69..bb76488 100644 --- a/utils.js +++ b/utils.js @@ -78,18 +78,13 @@ export async function getAtomFeed(url) { } // 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) { const res = await fetch(feedUrl) const data = await res.json() const post = data.items[0] const { title, image, url } = post - const text = !showImage - ? title - : image - ? `${title} ![${title}](${image})` - : title - return { text, url } + return { text: title, url, image: showImage ? image : null } } // ======================================================================== @@ -133,7 +128,10 @@ export async function getMalojaScrobble(malojaUrl) { // mastodon rss feed parser - filters out replies (posts starting with @) // 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 data = await res.text() const dom = new JSDOM(data, { contentType: 'text/xml' }) @@ -152,6 +150,10 @@ export async function getMastodonPost(feedUrl) { const link = item.querySelector('link')?.textContent.trim() + // extract image from description html if available + const imgMatch = description.match(/]+src="([^"]+)"/) + const image = imgMatch ? imgMatch[1] : null + // use description as the text, stripping HTML let cleanText = sanitizeHtml(description, { allowedTags: [], @@ -161,20 +163,26 @@ export async function getMastodonPost(feedUrl) { // replace URLs with a link emoji 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 const firstItem = items[0] const description = firstItem.querySelector('description')?.textContent || '' const link = firstItem.querySelector('link')?.textContent.trim() + + // extract image from description html if available + const imgMatch = description.match(/]+src="([^"]+)"/) + const image = imgMatch ? imgMatch[1] : null + let cleanText = sanitizeHtml(description, { allowedTags: [], allowedAttributes: {}, })?.trim() // replace URLs with link emoji 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 link = item.querySelector('link').textContent.trim() - let text = title + let image = null if (showImage) { // Parse CDATA content as HTML to extract image const description = item.querySelector('description').textContent const imgMatch = description.match(/]+src="([^"]+)"/) - const image = imgMatch ? imgMatch[1] : null - text = image ? `${title} ![${title}](${image})` : title + image = imgMatch ? imgMatch[1] : null } - return { text, url: link } + return { text: title, url: link, image } } // 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 title = entry.querySelector('title').textContent const link = entry.querySelector('link').getAttribute('href') - const text = !showImage - ? title - : `${title} ![${title}](https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=episode&image_only=1)` - return { text, url: link } + const image = showImage + ? `https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=episode&image_only=1` + : null + return { text: title, url: link, image } } // 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 title = entry.querySelector('title').textContent const link = entry.querySelector('link').getAttribute('href') - const text = !showImage - ? title - : `${title} ![${title}](https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=movie&image_only=1)` - return { text, url: link } + const image = showImage + ? `https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=movie&image_only=1` + : null + return { text: title, url: link, image } } // ======================================================================== From 13ebc9f4e3076e56a688b0355452d4d6048697f9 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:46:16 +0000 Subject: [PATCH 14/43] updated to include images --- now-page-template.md | 68 ++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index da49d17..e1d1d0c 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -2,20 +2,35 @@ This page _should_ update automatically with my latest activity.
-
- I listened to this
- lastfm +
+
+ I listened to this
+ lastfm +
+
-
- I watched this episode
- trakt-episode
- and this movie
- trakt-movie +
+
+ I watched this episode
+ trakt-episode +
+
-
- I rated this
letterboxd +
+
+ I watched this movie
+ trakt-movie +
+ +
+
+
+ I rated this
+ letterboxd +
+
I'm reading this
@@ -25,16 +40,27 @@ This page _should_ update automatically with my latest activity. I played this
steam
-
- I tooted this
- mastodon +
+
+ I tooted this
+ mastodon +
+
-
- I shared this photo
some.pics +
+
+ I shared this photo
+ some.pics +
+
-
- I wrote this
weblog +
+
+ I wrote this
+ weblog +
+
I pushed some (bad) code
@@ -79,4 +105,4 @@ This page _should_ update automatically with my latest activity. alt="omg.lol 88x31 Badge">
-### [Back to my omg.lol page!](https://dylan.omg.lol) \ No newline at end of file +### [Back to my omg.lol page!](https://dylan.omg.lol) From e41e3f51d84fd2d89daa6eeee27500331d337da2 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 17:59:47 +0000 Subject: [PATCH 15/43] added STEAM_WEBAPI_KEY to environment variables in update.yml --- .forgejo/workflows/update.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/update.yml b/.forgejo/workflows/update.yml index 55be46b..e7cc72d 100644 --- a/.forgejo/workflows/update.yml +++ b/.forgejo/workflows/update.yml @@ -24,3 +24,4 @@ jobs: OMGLOL_KEY: ${{ secrets.OMGLOL_KEY }} OMGLOL_USERNAME: ${{ vars.OMGLOL_USERNAME }} TRAKT_SLURM: ${{ secrets.TRAKT_SLURM }} + STEAM_WEBAPI_KEY: ${{ secrets.STEAM_WEBAPI_KEY }} From 221e96b7c194a7c035b59c1f281428d87475935b Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 18:13:32 +0000 Subject: [PATCH 16/43] added HARDCOVER_API_KEY to .env.example for hardcover service --- .env.example | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.env.example b/.env.example index a06013d..4b575e4 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,6 @@ TRAKT_SLURM= # optional - only needed if using steam service STEAM_WEBAPI_KEY= + +# optional - only needed if using hardcover service +HARDCOVER_API_KEY= From 936a8437b7e53d3287ff01941829195477fdee77 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 18:14:00 +0000 Subject: [PATCH 17/43] added hardcover activity function with API key validation --- utils.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/utils.js b/utils.js index bb76488..2e783a8 100644 --- a/utils.js +++ b/utils.js @@ -252,8 +252,14 @@ export async function getTraktMovie(config) { // ======================================================================== // hardcover graphql api - grabs your currently reading book +// requires HARDCOVER_API_KEY see readme for more info export async function getHardcoverActivity(config) { const { userId, feedUrl } = config + const apiKey = process.env.HARDCOVER_API_KEY + if (!apiKey) { + throw new Error('HARDCOVER_API_KEY environment variable not set') + } + const query = ` query { user(username: "${userId}") { @@ -267,7 +273,10 @@ export async function getHardcoverActivity(config) { const res = await fetch(feedUrl, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + authorization: apiKey, + }, body: JSON.stringify({ query }), }) From 7dfff74ea07c439e880472fc27794231bb79f208 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 18:14:05 +0000 Subject: [PATCH 18/43] added HARDCOVER_API_KEY to environment variables in update.yml --- .forgejo/workflows/update.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/update.yml b/.forgejo/workflows/update.yml index e7cc72d..83c8471 100644 --- a/.forgejo/workflows/update.yml +++ b/.forgejo/workflows/update.yml @@ -25,3 +25,4 @@ jobs: OMGLOL_USERNAME: ${{ vars.OMGLOL_USERNAME }} TRAKT_SLURM: ${{ secrets.TRAKT_SLURM }} STEAM_WEBAPI_KEY: ${{ secrets.STEAM_WEBAPI_KEY }} + HARDCOVER_API_KEY: ${{ secrets.HARDCOVER_API_KEY }} From 48a5aab1c289b3e81e2a9f4cd7d060d9ab864745 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 18:40:03 +0000 Subject: [PATCH 19/43] updated hardcover and steam apis to actually work after testing --- services.js | 5 +++-- utils.js | 46 ++++++++++++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/services.js b/services.js index a52273a..240539c 100644 --- a/services.js +++ b/services.js @@ -116,9 +116,10 @@ export const services = [ { id: 'hardcover', isActive: true, - userId: 'itsdylan', // your hardcover username - feedUrl: 'https://hardcover.app/api/graphql', + userId: '62036', // your hardcover user ID (find in your API token or profile URL) + feedUrl: 'https://api.hardcover.app/v1/graphql', feedType: 'hardcover', + showImage: true, }, { id: 'storygraph', diff --git a/utils.js b/utils.js index 2e783a8..640ba02 100644 --- a/utils.js +++ b/utils.js @@ -262,10 +262,20 @@ export async function getHardcoverActivity(config) { const query = ` query { - user(username: "${userId}") { - currently_reading_books(limit: 1) { + user_books( + where: {user_id: {_eq: ${userId}}, status_id: {_eq: 2}} + ) { + book { title id + image { + url + } + contributions { + author { + name + } + } } } } @@ -275,23 +285,24 @@ export async function getHardcoverActivity(config) { method: 'POST', headers: { 'Content-Type': 'application/json', - authorization: apiKey, + authorization: `Bearer ${apiKey}`, + 'User-Agent': 'now-updater/1.0 (omg.lol now page updater)', }, body: JSON.stringify({ query }), }) const data = await res.json() - if ( - !data.data?.user?.currently_reading_books || - data.data.user.currently_reading_books.length === 0 - ) { + if (!data.data?.user_books || data.data.user_books.length === 0) { throw new Error('Nothing!') } - const book = data.data.user.currently_reading_books[0] + const userBook = data.data.user_books[0] + const book = userBook.book + const author = book.contributions?.[0]?.author?.name || 'Unknown Author' return { - text: book.title, + text: `${book.title} by ${author}`, url: `https://hardcover.app/books/${book.id}`, + image: book.image?.url || null, } } @@ -310,20 +321,31 @@ export async function getSteamRecentlyPlayed(config) { } const res = await fetch( - `https://api.steampowered.com/IPlayerService/GetRecentlyPlayedGames/v1/?key=${process.env.STEAM_WEBAPI_KEY}&steamid=${steamId}&format=json`, + `https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=${apiKey}&steamid=${steamId}&format=json&include_appinfo=1&include_played_free_games=1`, ) const data = await res.json() if (!data.response.games || data.response.games.length === 0) { - throw new Error('No recent games found') + throw new Error('No games found') } - const game = data.response.games[0] + // sort by most recently played + const games = data.response.games + const sortedGames = games + .filter((g) => g.rtime_last_played > 0) + .sort((a, b) => b.rtime_last_played - a.rtime_last_played) + + if (sortedGames.length === 0) { + throw new Error('Nothing yet!') + } + + const game = sortedGames[0] const { name, appid } = game return { text: name, url: `https://store.steampowered.com/app/${appid}`, + image: `https://cdn.cloudflare.steamstatic.com/steam/apps/${appid}/library_600x900.jpg`, } } From b549858bc00291de4dfdad0b1bc7f9d824dab416 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 18:40:12 +0000 Subject: [PATCH 20/43] updated layout for hardcover and steam sections to include images --- now-page-template.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index e1d1d0c..490c343 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -32,13 +32,19 @@ This page _should_ update automatically with my latest activity.
-
- I'm reading this
- hardcover +
+
+ I'm reading this
+ hardcover +
+
-
- I played this
- steam +
+
+ I played this
+ steam +
+
From 3b8a1edcb43b6beb88ab8a41f88e12d10f23f8a5 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 18:49:21 +0000 Subject: [PATCH 21/43] fixed steam api by passing the right thing lol --- utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils.js b/utils.js index 640ba02..b5fa88a 100644 --- a/utils.js +++ b/utils.js @@ -314,14 +314,14 @@ export async function getHardcoverActivity(config) { // requires STEAM_WEBAPI_KEY see readme for more info // needs your steam id 64 (not username) - convert at steamid.io export async function getSteamRecentlyPlayed(config) { - const { steamId } = config + const { userId } = config const apiKey = process.env.STEAM_WEBAPI_KEY if (!apiKey) { throw new Error('STEAM_WEBAPI_KEY environment variable not set') } const res = await fetch( - `https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=${apiKey}&steamid=${steamId}&format=json&include_appinfo=1&include_played_free_games=1`, + `https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=${apiKey}&steamid=${userId}&format=json&include_appinfo=1&include_played_free_games=1`, ) const data = await res.json() From 02f122d69082ca6e65749dafcae8cceb6d4d567b Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 19:08:27 +0000 Subject: [PATCH 22/43] updated layout to left align and show images nicely --- now-page-template.md | 82 ++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index 490c343..2c8f993 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -2,75 +2,75 @@ This page _should_ update automatically with my latest activity.
-
-
- I listened to this
- + - +
-
-
- I watched this episode
- + - +
-
-
- I watched this movie
- trakt-movie +
+
+ I watched this movie
+ trakt-movie
- +
-
-
+
+
I rated this
letterboxd
- +
-
-
- I'm reading this
- hardcover +
+
+ I'm reading this
+ hardcover
- +
-
-
- I played this
- steam +
+
+ I played this
+ steam
- +
-
-
- I tooted this
- + - +
-
-
+
+
I shared this photo
some.pics
- +
-
-
+
+
I wrote this
weblog
- +
- I pushed some (bad) code
- source.tube + I pushed some (bad) code
+ source.tube
From 3a8683204cb35adf87ed900688bb4eedeac92a69 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 19:08:32 +0000 Subject: [PATCH 23/43] updated image handling to remove img tags if no image is available --- index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 15d15c4..f1e2094 100644 --- a/index.js +++ b/index.js @@ -110,9 +110,9 @@ export default async function now() { return `${updatedOpenTag}${displayText}${closeTag}` }) - // replace image placeholders if image is available + // replace image placeholders if image is available, otherwise remove img tags + const imageId = `${item.id}-image` if (latest.image) { - const imageId = `${item.id}-image` // html image replacement newNow = newNow.replace( new RegExp(`(]+src=["'])${imageId}(["'][^>]*>)`, 'gi'), @@ -123,6 +123,17 @@ export default async function now() { new RegExp(`(!\\[[^\\]]*\\]\\()${imageId}(\\))`, 'gi'), `$1${latest.image}$2`, ) + } else { + // remove img tags if no image available + newNow = newNow.replace( + new RegExp(`]+src=["']${imageId}["'][^>]*>`, 'gi'), + '', + ) + // remove markdown image + newNow = newNow.replace( + new RegExp(`!\\[[^\\]]*\\]\\(${imageId}\\)`, 'gi'), + '', + ) } } } catch (e) { From 469dddd539af81cbff4425d7d1e1b27392f7087f Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 19:08:37 +0000 Subject: [PATCH 24/43] updated mastodon post image extraction to check for media:content first --- utils.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/utils.js b/utils.js index b5fa88a..b78ded9 100644 --- a/utils.js +++ b/utils.js @@ -150,9 +150,14 @@ export async function getMastodonPost(config) { const link = item.querySelector('link')?.textContent.trim() - // extract image from description html if available - const imgMatch = description.match(/]+src="([^"]+)"/) - const image = imgMatch ? imgMatch[1] : null + // extract image from media:content or description html + // check for media:content (mastodon uses this for attachments) + const mediaContent = item.getElementsByTagName('media:content')[0] + let image = mediaContent?.getAttribute('url') || null + if (!image) { + const imgMatch = description.match(/]+src="([^"]+)"/) + image = imgMatch ? imgMatch[1] : null + } // use description as the text, stripping HTML let cleanText = sanitizeHtml(description, { @@ -171,9 +176,14 @@ export async function getMastodonPost(config) { const description = firstItem.querySelector('description')?.textContent || '' const link = firstItem.querySelector('link')?.textContent.trim() - // extract image from description html if available - const imgMatch = description.match(/]+src="([^"]+)"/) - const image = imgMatch ? imgMatch[1] : null + // extract image from media:content or description html + // check for media:content (mastodon uses this for attachments) + const mediaContent = firstItem.getElementsByTagName('media:content')[0] + let image = mediaContent?.getAttribute('url') || null + if (!image) { + const imgMatch = description.match(/]+src="([^"]+)"/) + image = imgMatch ? imgMatch[1] : null + } let cleanText = sanitizeHtml(description, { allowedTags: [], From 85fcc462dae387aa7ab1f3e13a71b695211389eb Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 19:11:54 +0000 Subject: [PATCH 25/43] updated mastodon post handling to extract video and image media types --- index.js | 17 ++++++++++++++++- utils.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index f1e2094..fa6e228 100644 --- a/index.js +++ b/index.js @@ -110,8 +110,9 @@ export default async function now() { return `${updatedOpenTag}${displayText}${closeTag}` }) - // replace image placeholders if image is available, otherwise remove img tags + // replace image/video placeholders if available, otherwise remove tags const imageId = `${item.id}-image` + const videoId = `${item.id}-video` if (latest.image) { // html image replacement newNow = newNow.replace( @@ -135,6 +136,20 @@ export default async function now() { '', ) } + + if (latest.video) { + // html video replacement + newNow = newNow.replace( + new RegExp(`(]+src=["'])${videoId}(["'][^>]*>)`, 'gi'), + `$1${latest.video}$2`, + ) + } else { + // remove video tags if no video available + newNow = newNow.replace( + new RegExp(`]*src=["']${videoId}["'][^>]*>[\\s\\S]*?`, 'gi'), + '', + ) + } } } catch (e) { console.warn(`⚠️ Failed to fetch ${item.id}`, e) diff --git a/utils.js b/utils.js index b78ded9..d12d1e8 100644 --- a/utils.js +++ b/utils.js @@ -150,11 +150,25 @@ export async function getMastodonPost(config) { const link = item.querySelector('link')?.textContent.trim() - // extract image from media:content or description html + // extract media from media:content or description html // check for media:content (mastodon uses this for attachments) const mediaContent = item.getElementsByTagName('media:content')[0] - let image = mediaContent?.getAttribute('url') || null - if (!image) { + let image = null + let video = null + + if (mediaContent) { + const mediaUrl = mediaContent.getAttribute('url') + const mediaType = mediaContent.getAttribute('type') + // check if it's a video or image + if (mediaType?.startsWith('video/')) { + video = mediaUrl + } else if (mediaType?.startsWith('image/')) { + image = mediaUrl + } + } + + // fallback to img tags in description html + if (!image && !video) { const imgMatch = description.match(/]+src="([^"]+)"/) image = imgMatch ? imgMatch[1] : null } @@ -168,7 +182,12 @@ export async function getMastodonPost(config) { // replace URLs with a link emoji cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim() - return { text: cleanText, url: link, image: showImage ? image : null } + return { + text: cleanText, + url: link, + image: showImage ? image : null, + video: showImage ? video : null, + } } // fallback to first item if no non-reply found @@ -176,11 +195,25 @@ export async function getMastodonPost(config) { const description = firstItem.querySelector('description')?.textContent || '' const link = firstItem.querySelector('link')?.textContent.trim() - // extract image from media:content or description html + // extract media from media:content or description html // check for media:content (mastodon uses this for attachments) const mediaContent = firstItem.getElementsByTagName('media:content')[0] - let image = mediaContent?.getAttribute('url') || null - if (!image) { + let image = null + let video = null + + if (mediaContent) { + const mediaUrl = mediaContent.getAttribute('url') + const mediaType = mediaContent.getAttribute('type') + // check if it's a video or image + if (mediaType?.startsWith('video/')) { + video = mediaUrl + } else if (mediaType?.startsWith('image/')) { + image = mediaUrl + } + } + + // fallback to img tags in description html + if (!image && !video) { const imgMatch = description.match(/]+src="([^"]+)"/) image = imgMatch ? imgMatch[1] : null } @@ -192,7 +225,12 @@ export async function getMastodonPost(config) { // replace URLs with link emoji cleanText = cleanText.replace(/https?:\/\/\S+/g, '🔗').trim() - return { text: cleanText, url: link, image: showImage ? image : null } + return { + text: cleanText, + url: link, + image: showImage ? image : null, + video: showImage ? video : null, + } } // ======================================================================== From 5a431287d68b6005689a967fdcba45a577cd2391 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 19:11:57 +0000 Subject: [PATCH 26/43] added video support for mastodon posts --- now-page-template.md | 1 + 1 file changed, 1 insertion(+) diff --git a/now-page-template.md b/now-page-template.md index 2c8f993..7f6c734 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -53,6 +53,7 @@ This page _should_ update automatically with my latest activity. style="color: #cba6f7 !important;">mastodon
+
From 0e9efce6fb06d3d419267d0261173bb603d9c813 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 19:34:17 +0000 Subject: [PATCH 27/43] updated to show images for mastodon posts --- services.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services.js b/services.js index 240539c..e01f309 100644 --- a/services.js +++ b/services.js @@ -64,7 +64,7 @@ export const services = [ instance: 'social.lol', // your mastodon instance feedUrlTemplate: 'https://{instance}/@{userId}.rss', feedType: 'mastodon', - showImage: false, + showImage: true, }, { id: 'pixelfed', From 2fbf9e809b72960b027b0052b39abf022c04ff01 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 19:37:49 +0000 Subject: [PATCH 28/43] updated to just html --- now-page-template.md | 77 +++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 51 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index 7f6c734..8c49ee0 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -1,68 +1,65 @@ -# What I've been up to -This page _should_ update automatically with my latest activity. +

What I've been up to

+

This page should update automatically with my latest activity.

-
+
I listened to this
- lastfm + lastfm
-
+
I watched this episode
- trakt-episode + trakt-episode
-
+
I watched this movie
trakt-movie
-
+
- I rated this
+ I rated this
letterboxd
-
+
I'm reading this
hardcover
-
+
I played this
steam
-
+
I tooted this
- mastodon + mastodon
-
+
I shared this photo
some.pics
-
+
I wrote this
weblog @@ -75,41 +72,19 @@ This page _should_ update automatically with my latest activity.
-##### {last-updated} -

Want your own automatically updated Now page? Check out now-updater.

+
{last-updated}
+

Want your own automatically updated Now page? Check out now-updater.

---- +
- - People Pledge 88x31 Badge - - Follow me on Mastodon - @dylan@social.lol 88x31 Badge - - Badly Hand-Coded and Proud 88x31 Badge - - omg.lol 88x31 Badge - - Anna's Archive 88x31 Badge - - Internet Privacy 88x31 Badge - - omg.lol 88x31 Badge + People Pledge 88x31 Badge + Follow me on Mastodon - @dylan@social.lol 88x31 Badge + Badly Hand-Coded and Proud 88x31 Badge + omg.lol 88x31 Badge + Anna's Archive 88x31 Badge + Internet Privacy 88x31 Badge + omg.lol 88x31 Badge
-### [Back to my omg.lol page!](https://dylan.omg.lol) +

Back to my omg.lol page!

From 7ded8d55c643363c5e51160758da53914e175e31 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 20:40:55 +0000 Subject: [PATCH 29/43] removed mastodon post section from now-page-template --- now-page-template.md | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index 8c49ee0..9ae62fe 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -2,12 +2,9 @@

This page should update automatically with my latest activity.

-
-
- I listened to this
- lastfm -
- +
+ I listened to this
+ lastfm
@@ -44,14 +41,6 @@
-
-
- I tooted this
- mastodon -
- - -
I shared this photo
From 88720c7269a408e5bbe788c1e0aab50da38e4d53 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 20:41:01 +0000 Subject: [PATCH 30/43] updated mastodon service to set isActive to false --- services.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services.js b/services.js index e01f309..22cb984 100644 --- a/services.js +++ b/services.js @@ -59,7 +59,7 @@ export const services = [ // ======================================================================== { id: 'mastodon', - isActive: true, + isActive: false, userId: 'dylan', // your mastodon username instance: 'social.lol', // your mastodon instance feedUrlTemplate: 'https://{instance}/@{userId}.rss', From c712c387ae84048f4add5c4c16ede00aaeec2fbc Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 20:51:19 +0000 Subject: [PATCH 31/43] updated some.pics integration --- index.js | 12 ++++++--- now-page-template.md | 60 +++++++++++++++++++++++++++++++------------- services.js | 2 +- utils.js | 21 ++++++++++++++++ 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index fa6e228..3f5e762 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ import { getMalojaScrobble, getMastodonPost, getRSSItemTitle, + getSomePicsPost, getSourceTubeActivity, getSteamRecentlyPlayed, getTraktEpisode, @@ -23,6 +24,7 @@ const feedHandlers = { rss: getRSSItemTitle, atom: getAtomFeed, json: getJsonFeedItemTitle, + 'some.pics': getSomePicsPost, 'source.tube': getSourceTubeActivity, mastodon: getMastodonPost, letterboxd: getLetterboxdActivity, @@ -79,9 +81,13 @@ export default async function now() { } // generic handlers expect just feedUrl string, special handlers expect config object - const handlerParam = ['rss', 'atom', 'json', 'maloja'].includes( - item.feedType, - ) + const handlerParam = [ + 'rss', + 'atom', + 'json', + 'maloja', + 'some.pics', + ].includes(item.feedType) ? feedUrl : { ...item, feedUrl } const latest = await handler(handlerParam) diff --git a/now-page-template.md b/now-page-template.md index 9ae62fe..6e7db73 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -6,49 +6,58 @@ I listened to this
lastfm
-
+
I watched this episode
trakt-episode
- +
-
+
I watched this movie
trakt-movie
- +
-
+
I rated this
letterboxd
-
+
I'm reading this
hardcover
-
+
I played this
steam
-
+
I shared this photo
some.pics
-
+
I wrote this
weblog @@ -62,18 +71,33 @@
{last-updated}
-

Want your own automatically updated Now page? Check out now-updater.

+

Want your own automatically updated Now page? Check out now-updater.


- People Pledge 88x31 Badge - Follow me on Mastodon - @dylan@social.lol 88x31 Badge - Badly Hand-Coded and Proud 88x31 Badge - omg.lol 88x31 Badge - Anna's Archive 88x31 Badge - Internet Privacy 88x31 Badge - omg.lol 88x31 Badge + People Pledge 88x31 Badge + Follow me on Mastodon - @dylan@social.lol 88x31 Badge + Badly Hand-Coded and Proud 88x31 Badge + omg.lol 88x31 Badge + Anna's Archive 88x31 Badge + Internet Privacy 88x31 Badge + omg.lol 88x31 Badge
-

Back to my omg.lol page!

+

Back to my omg.lol page!

\ No newline at end of file diff --git a/services.js b/services.js index 22cb984..f74146d 100644 --- a/services.js +++ b/services.js @@ -17,7 +17,7 @@ export const services = [ isActive: true, userId: 'dylan', // your omg.lol username feedUrlTemplate: 'https://{userId}.some.pics/rss', - feedType: 'rss', + feedType: 'some.pics', }, // ======================================================================== diff --git a/utils.js b/utils.js index d12d1e8..4d02a71 100644 --- a/utils.js +++ b/utils.js @@ -87,6 +87,27 @@ export async function getJsonFeedItemTitle(feedUrl, showImage = true) { return { text: title, url, image: showImage ? image : null } } +// ======================================================================== +// omg.lol services +// ======================================================================== + +// some.pics rss parser - extracts image from cdata description +export async function getSomePicsPost(feedUrl) { + const res = await fetch(feedUrl) + const data = await res.text() + const dom = new JSDOM(data, { contentType: 'text/xml' }) + const item = dom.window.document.querySelector('item') + const title = item.querySelector('title').textContent + const link = item.querySelector('link').textContent.trim() + + // extract image from cdata description + const description = item.querySelector('description').textContent + const imgMatch = description.match(/]+src="([^"]+)"/) + const image = imgMatch ? imgMatch[1] : null + + return { text: title, url: link, image } +} + // ======================================================================== // music // ======================================================================== From 37c167fc3812c44feb445bf56047c814ab358df9 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 20:53:09 +0000 Subject: [PATCH 32/43] updated trakt episode and movie image URLs to use poster instead of thumb --- utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils.js b/utils.js index 4d02a71..5a1dfb2 100644 --- a/utils.js +++ b/utils.js @@ -293,7 +293,7 @@ export async function getTraktEpisode(config) { const title = entry.querySelector('title').textContent const link = entry.querySelector('link').getAttribute('href') const image = showImage - ? `https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=episode&image_only=1` + ? `https://widgets.trakt.tv/users/${traktId}/watched/poster@2x.jpg?type=episode&image_only=1` : null return { text: title, url: link, image } } @@ -311,7 +311,7 @@ export async function getTraktMovie(config) { const title = entry.querySelector('title').textContent const link = entry.querySelector('link').getAttribute('href') const image = showImage - ? `https://widgets.trakt.tv/users/${traktId}/watched/thumb@2x.jpg?type=movie&image_only=1` + ? `https://widgets.trakt.tv/users/${traktId}/watched/poster@2x.jpg?type=movie&image_only=1` : null return { text: title, url: link, image } } From 79a27ae3680a8b7ee6a8e9b2eb9f26412b15d5d8 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:10:08 +0000 Subject: [PATCH 33/43] added logging for latest image and video in feed handler --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index 3f5e762..5774ee1 100644 --- a/index.js +++ b/index.js @@ -93,6 +93,8 @@ export default async function now() { const latest = await handler(handlerParam) console.log(`${item.id}: ${latest.text}`) console.log(`${item.id} URL: ${latest.url}`) + if (latest.image) console.log(`${item.id} Image: ${latest.image}`) + if (latest.video) console.log(`${item.id} Video: ${latest.video}`) // Apply markdown escaping only for markdown links const displayText = From 6819d100d3e04a3603c9137df92acf90ad3ba5b9 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:14:30 +0000 Subject: [PATCH 34/43] updated image tag handling to support multi-line tags --- index.js | 4 ++-- now-page-template.md | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 5774ee1..09fcb0f 100644 --- a/index.js +++ b/index.js @@ -133,9 +133,9 @@ export default async function now() { `$1${latest.image}$2`, ) } else { - // remove img tags if no image available + // remove img tags if no image available (handle multi-line tags) newNow = newNow.replace( - new RegExp(`]+src=["']${imageId}["'][^>]*>`, 'gi'), + new RegExp(`]*src=["']${imageId}["'][^>]*>`, 'gis'), '', ) // remove markdown image diff --git a/now-page-template.md b/now-page-template.md index 6e7db73..f6f394e 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -3,8 +3,10 @@
- I listened to this
- lastfm +
+ I listened to this
+ lastfm +
@@ -65,8 +67,10 @@
- I pushed some (bad) code
- source.tube +
+ I pushed some (bad) code
+ source.tube +
From 5c78a8a491de48ce7465648a7bc279e62942a753 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:18:27 +0000 Subject: [PATCH 35/43] updated regex for img and video tag removal to fix broken cards (fingers crossed!) --- index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 09fcb0f..9c2fb02 100644 --- a/index.js +++ b/index.js @@ -133,9 +133,9 @@ export default async function now() { `$1${latest.image}$2`, ) } else { - // remove img tags if no image available (handle multi-line tags) + // remove img tags if no image available (handle multi-line tags and any attribute order) newNow = newNow.replace( - new RegExp(`]*src=["']${imageId}["'][^>]*>`, 'gis'), + new RegExp(`]*\\bsrc=["']${imageId}["'][^>]*>`, 'gis'), '', ) // remove markdown image @@ -154,7 +154,7 @@ export default async function now() { } else { // remove video tags if no video available newNow = newNow.replace( - new RegExp(`]*src=["']${videoId}["'][^>]*>[\\s\\S]*?`, 'gi'), + new RegExp(`]*\\bsrc=["']${videoId}["'][^>]*>[\\s\\S]*?`, 'gis'), '', ) } From ad10100c025fabde31ad46d0249bd2ad9e3cf9f1 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:22:42 +0000 Subject: [PATCH 36/43] updated regex for img and video tag removal one more time... --- index.js | 4 ++-- now-page-template.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 9c2fb02..6200e07 100644 --- a/index.js +++ b/index.js @@ -135,7 +135,7 @@ export default async function now() { } else { // remove img tags if no image available (handle multi-line tags and any attribute order) newNow = newNow.replace( - new RegExp(`]*\\bsrc=["']${imageId}["'][^>]*>`, 'gis'), + new RegExp(`\\s*]*\\bsrc=["']${imageId}["'][^>]*>\\s*`, 'gis'), '', ) // remove markdown image @@ -154,7 +154,7 @@ export default async function now() { } else { // remove video tags if no video available newNow = newNow.replace( - new RegExp(`]*\\bsrc=["']${videoId}["'][^>]*>[\\s\\S]*?`, 'gis'), + new RegExp(`\\s*]*\\bsrc=["']${videoId}["'][^>]*>[\\s\\S]*?\\s*`, 'gis'), '', ) } diff --git a/now-page-template.md b/now-page-template.md index f6f394e..8ce0dc9 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -53,7 +53,7 @@
- I shared this photo
+ I shared this photo
some.pics
From 44839870735b78997a8c6b9988859d6b93c5a7c5 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:30:37 +0000 Subject: [PATCH 37/43] updated closing div tag with no indentation to fix breaking when no images are found --- now-page-template.md | 67 +++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 38 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index 8ce0dc9..4f07927 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -6,72 +6,61 @@
I listened to this
lastfm -
-
-
+
+
I watched this episode
trakt-episode -
- -
-
+
+ +
+
I watched this movie
trakt-movie -
- -
-
+
+ +
+
I rated this
letterboxd -
+
-
-
+
+
I'm reading this
hardcover -
+
-
-
+
+
I played this
steam -
+
-
-
+
+
- I shared this photo
+ I shared this photo
some.pics -
+
-
-
+
+
I wrote this
weblog -
+
-
+
I pushed some (bad) code
source.tube -
-
+
{last-updated}
@@ -104,4 +93,6 @@ alt="omg.lol 88x31 Badge">
-

Back to my omg.lol page!

\ No newline at end of file +

Back to my omg.lol page!

+ +Perfect! Put closing `
` tags at column 0 (no indentation). Now when img tags are removed, there's no indented closing tag to trigger code blocks. This should fix the weblog breaking issue! \ No newline at end of file From 9e4d661c8a3d594611e266fbacc6536407c1a18d Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:35:13 +0000 Subject: [PATCH 38/43] removed img tag! --- now-page-template.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index 4f07927..427c20e 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -49,13 +49,11 @@
-
+
I wrote this
weblog -
- -
+
I pushed some (bad) code
From ec2117aa137b6b3fa965e12441e43e6005498662 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:39:54 +0000 Subject: [PATCH 39/43] updated to remove images from weblog and minify html to avoid parsing issues --- now-page-template.md | 99 ++++++++------------------------------------ 1 file changed, 17 insertions(+), 82 deletions(-) diff --git a/now-page-template.md b/now-page-template.md index 427c20e..c8c25ec 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -2,95 +2,30 @@

This page should update automatically with my latest activity.

-
-
- I listened to this
- lastfm -
-
-
- I watched this episode
- trakt-episode -
- -
-
-
- I watched this movie
- trakt-movie -
- -
-
-
- I rated this
- letterboxd -
- -
-
-
- I'm reading this
- hardcover -
- -
-
-
- I played this
- steam -
- -
-
-
- I shared this photo
- some.pics -
- -
-
-
- I wrote this
- weblog -
-
-
- I pushed some (bad) code
- source.tube -
+
I listened to this
lastfm
+
I watched this episode
trakt-episode
+
I watched this movie
trakt-movie
+
I rated this
letterboxd
+
I'm reading this
hardcover
+
I played this
steam
+
I shared this photo
some.pics
+
I wrote this
weblog
+
I pushed some (bad) code
source.tube
{last-updated}
-

Want your own automatically updated Now page? Check out now-updater.

+

Want your own automatically updated Now page? Check out now-updater.


- People Pledge 88x31 Badge - Follow me on Mastodon - @dylan@social.lol 88x31 Badge - Badly Hand-Coded and Proud 88x31 Badge - omg.lol 88x31 Badge - Anna's Archive 88x31 Badge - Internet Privacy 88x31 Badge - omg.lol 88x31 Badge +People Pledge 88x31 Badge +Follow me on Mastodon - @dylan@social.lol 88x31 Badge +Badly Hand-Coded and Proud 88x31 Badge +omg.lol 88x31 Badge +Anna's Archive 88x31 Badge +Internet Privacy 88x31 Badge +omg.lol 88x31 Badge

Back to my omg.lol page!

- -Perfect! Put closing `
` tags at column 0 (no indentation). Now when img tags are removed, there's no indented closing tag to trigger code blocks. This should fix the weblog breaking issue! \ No newline at end of file From 692259b0c3bf958d7ac8d224e1bd7452f9b5c73f Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:41:17 +0000 Subject: [PATCH 40/43] updated icon for weblog --- now-page-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/now-page-template.md b/now-page-template.md index c8c25ec..3968eaf 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -9,7 +9,7 @@
I'm reading this
hardcover
I played this
steam
I shared this photo
some.pics
-
I wrote this
weblog
+
I wrote this
weblog
I pushed some (bad) code
source.tube
From 83a1ea97318845f5293d3e951a6fcfb40ac9167f Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:44:26 +0000 Subject: [PATCH 41/43] updated link style --- now-page-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/now-page-template.md b/now-page-template.md index 3968eaf..7b38c73 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -28,4 +28,4 @@ omg.lol 88x31 Badge
-

Back to my omg.lol page!

+

Back to my omg.lol page!

From 3faa24b2838b637898bb76acc44f17cc4bb1e08e Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 21:45:34 +0000 Subject: [PATCH 42/43] updated font size --- now-page-template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/now-page-template.md b/now-page-template.md index 7b38c73..e7d75a9 100644 --- a/now-page-template.md +++ b/now-page-template.md @@ -14,7 +14,7 @@
{last-updated}
-

Want your own automatically updated Now page? Check out now-updater.

+

Want your own automatically updated Now page? Check out now-updater.


From 2165ce5bd37b290e865c8453dbc318a471f33773 Mon Sep 17 00:00:00 2001 From: dylan Date: Sun, 28 Dec 2025 22:18:27 +0000 Subject: [PATCH 43/43] updated README with support for new services --- README.md | 195 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 127 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index bd3ec9f..d2e666b 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ Automatically updates your omg.lol Now page with your latest activity. -> Fork of [melanie/now-updater](https://source.tube/melanie/now-updater) with added support for HTML output links, Mastodon, Last.fm, and dynamic URLs. +> Fork of [melanie/now-updater](https://source.tube/melanie/now-updater) with added support for HTML links, images/videos, Steam, Hardcover, some.pics, and simplified service configuration. Demo: [dylan.omg.lol/now](https://dylan.omg.lol/now) ## How It Works -Fetches content from RSS feeds, JSON feeds, and different services (Letterboxd, Trakt, Mastodon, Last.fm) and updates HTML or markdown links on your Now page. 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. ### Example Your Now page links will be automatically updated from this: @@ -37,15 +37,14 @@ to this: [Pushed to repository](https://gitlab.com/username/project) ``` -**New in this fork**: Use service names as link text instead of URLs! Just write `servicename` and the script fills in both the text and URL. - ## Setup 1. Fork this repo 2. Enable Actions in repo settings 3. Add Actions variables: `OMGLOL_USERNAME` (your omg.lol username) -4. Add Actions secrets: `OMGLOL_KEY` (your API key), `TRAKT_SLURM` (optional, see [Trakt](#trakt) for details) -5. Add your sources in `config.js` +4. Add Actions secrets: `OMGLOL_KEY` (your API key) +5. Add optional secrets for services you want to use: `TRAKT_SLURM`, `STEAM_WEBAPI_KEY`, `HARDCOVER_API_KEY` +6. Activate your services in `services.js` by setting `isActive: true` For local development, copy `.env.example` to `.env` with your credentials. @@ -53,29 +52,34 @@ For local development, copy `.env.example` to `.env` with your credentials. **Now page template:** ```html -I wrote this: [tktk](https://your-blog.com) -I watched this: [tktk](trakt.tv/users/username) +I wrote this: blog +I watched this: letterboxd +I listened to: lastfm ``` -**config.js:** +**services.js:** ```js -export const linkFormat = 'markdown' - -export const items = [ +export const services = [ { id: 'blog', - regex: htmlLinkRegex('your-blog.com'), - getLatest: async () => getJsonFeedItemTitle('https://your-blog.com/feed.json'), + isActive: true, + feedUrl: 'https://your-blog.com/feed.json', + feedType: 'json', }, { id: 'letterboxd', - regex: htmlLinkRegex('letterboxd.com/username'), - getLatest: async () => getLetterboxdActivity('username', true), // with image + isActive: true, + userId: 'your-username', + feedUrlTemplate: 'https://letterboxd.com/{userId}/rss/', + feedType: 'letterboxd', + showImage: true, // optional: include movie posters }, { - id: 'trakt-movie', - regex: htmlLinkRegex('trakt.tv/users/username'), - getLatest: async () => getTraktMovie('username', 'your-trakt-id', false), // text only + id: 'lastfm', + isActive: true, + userId: 'your-username', + feedUrlTemplate: 'https://lfm.xiffy.nl/{userId}', + feedType: 'rss', }, ] ``` @@ -95,81 +99,124 @@ export const items = [ { id: 'gitlab', isActive: true, // just change this! - regex: htmlServiceLinkRegex('gitlab'), - feedUrl: 'https://gitlab.com/users/your-username/activity.atom', + userId: 'your-username', + feedUrlTemplate: 'https://gitlab.com/users/{userId}/activity.atom', feedType: 'atom', } ``` -That's it! No importing, no commenting/uncommenting. +The service `id` is used to match the link text in your Now page (e.g., `blog` matches `id: 'blog'`). If you need multiple instances of the same service just create separate entries in `services.js` with unique `id`s and matching link texts. Like so: + +```js + { + id: 'mastodon-personal', + isActive: false, + userId: 'dylan', + instance: 'social.lol', // your mastodon instance + feedUrlTemplate: 'https://{instance}/@{userId}.rss', + feedType: 'mastodon', + showImage: true, + }, + { + id: 'mastodon-work', + isActive: false, + userId: 'mrdylan', + instance: 'mastodon.instance', // your mastodon instance + feedUrlTemplate: 'https://{instance}/@{userId}.rss', + feedType: 'mastodon', + showImage: true, + }, +``` +Then you can have multiple Mastodon accounts on your Now page with different link texts: `[mastodon-personal](service-link)` and `[mastodon-work](service-link)`. ### Files -- **config.js** - Link format setting and filter logic (rarely needs editing) +- **config.js** - Link format setting (`html` or `markdown`) and filter logic - **services.js** - All available services with `isActive` flags (edit this to enable/disable services) -- **utils.js** - Generic feed handlers and special service logic +- **utils.js** - Generic feed handlers and service-specific logic ### Service Structure Each service has: -- `id`: identifier for logs +- `id`: Identifier that matches the link text in your Now page (e.g., `id: 'blog-1'` matches `blog-1`) - `isActive`: `true` to enable, `false` to disable -- `regex`: matches service name in your Now page -- `feedType`: Handler to use (`rss`, `atom`, `json`, or special handler name like `mastodon`) +- `feedType`: Handler to use (`rss`, `atom`, `json`, or special handler name like `letterboxd`, `steam`, `hardcover`) - `feedUrl`: Direct feed URL (use this for custom feeds without usernames) -- `feedUrlTemplate`: URL template with `{userId}` placeholder (use this for standard services) +- `feedUrlTemplate`: URL template with `{userId}` or `{instance}` placeholders - `userId`: Your username/ID (only needed with feedUrlTemplate) -- Extra params as needed (e.g., `instance`, `steamId`, `traktId`, `showImage`) +- `showImage`: Optional. Controls whether image/videos are included (defaults based on service) +- Extra params as needed (e.g., `instance` for federated services, `traktId` for Trakt widgets) -**Note**: Use either `feedUrl` (direct) OR `feedUrlTemplate` + `userId` (template). Both work! +Set `linkFormat` in config.js to `'html'` or `'markdown'` depending on your Now page format. -Set `linkFormat` in config.js to `'html'` or `'markdown'`. +### Image and Video Support -## Available Functions +Many services support optional images and videos: +- **Letterboxd**: Movie posters from CDATA descriptions +- **Trakt**: Episode/movie poster widgets (portrait orientation, change the URL from `poster` to `thumb` for landscape) +- **Hardcover**: Book cover images +- **Steam**: Game library posters +- **Mastodon**: Attached images and videos from `media:content` tags (iffy, may not always work) +- **some.pics**: Photo images from RSS CDATA -### Code Activity -- `getSourceTubeActivity(username)` - Forgejo activity on source.tube -- `getGitLabActivity(username)` - GitLab activity feed -- `getCodebergActivity(username)` - Codeberg activity feed +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. -### Content Creation -- `getYouTubeLatestVideo(channelId)` - Latest YouTube video (requires channel ID starting with "UC") -- `getVimeoLatestVideo(username)` - Latest Vimeo video -- `getTwitchLatestStream(channel)` - Latest Twitch stream/VOD (via TwitchRSS, no API key needed) +## Available Services -### Gaming -- `getBackloggdActivity(username, showImage)` - Backloggd game activity with optional cover images -- `getExophaseActivity(username, showImage)` - Multi-platform gaming achievements -- `getSteamRecentlyPlayed(steamId)` - Recently played Steam game (requires STEAM_WEBAPI_KEY) +All handlers now use a config object with consistent parameters. The `feedType` in services.js maps to these handlers. -### Books -- `getGoodreadsActivity(userId, showImage)` - Goodreads activity (requires numeric user ID, not username) -- `getHardcoverActivity(username)` - Currently reading books via GraphQL API +### Generic Feed Handlers +- `getRSSItemTitle(feedUrl)` - Standard RSS feeds +- `getAtomFeed(url)` - Standard Atom feeds +- `getJsonFeedItemTitle(feedUrl, showImage)` - JSON feeds with optional images + +### omg.lol Services +- `getSomePicsPost(feedUrl)` - some.pics photo posts with images extracted from CDATA ### Music -- `getListenBrainzScrobble(username)` - Latest music scrobble (open source Last.fm alternative) +- `getListenBrainzScrobble(config)` - ListenBrainz scrobbles (not tested, let me know!) + - Config: `{ feedUrl, userId }` +- For Last.fm, use generic RSS handler with `https://lfm.xiffy.nl/{userId}` ### Social Media -- `getPixelfedPost(instance, username)` - Latest Pixelfed post from federated instance -- `getBlueskyPost(handle)` - Latest Bluesky post (note: links not clickable in RSS) +- `getMastodonPost(config)` - Latest non-reply Mastodon post with optional images/videos + - Config: `{ feedUrl, showImage }` + - Extracts media from `media:content` tags and filters out replies -### General/Legacy -- `getRSSItemTitle(feedUrl)` - Standard RSS feeds -- `getJsonFeedItemTitle(feedUrl, showImage)` - JSON feeds -- `getLetterboxdActivity(username, showImage)` - Letterboxd activity -- `getTraktEpisode(username, id, showImage)` - Latest TV episode -- `getTraktMovie(username, id, showImage)` - Latest movie -- `getTraktEpisodeAndMovie(username, id, showImage)` - Latest episode and movie combined -- `getMastodonPost(feedUrl)` - Latest non-reply Mastodon post -- `getMalojaScrobble(url)` - Latest music scrobble from [maloja](https://github.com/krateng/maloja) +### Movies & TV +- `getLetterboxdActivity(config)` - Letterboxd activity with optional poster images + - Config: `{ feedUrl, showImage }` + - Extracts images from CDATA descriptions +- `getTraktEpisode(config)` - Latest TV episode with optional poster + - Config: `{ userId, traktId, showImage }` + - Requires `TRAKT_SLURM` environment variable + - Uses portrait poster widgets +- `getTraktMovie(config)` - Latest movie with optional poster + - Config: `{ userId, traktId, showImage }` + - Requires `TRAKT_SLURM` environment variable + - Uses portrait poster widgets + +### Books +- `getHardcoverActivity(config)` - Currently reading books with cover images + - Config: `{ userId, feedUrl }` + - Uses GraphQL API, requires `HARDCOVER_API_KEY` + - `userId` must be numeric user ID (not username) + +### Gaming +- `getSteamRecentlyPlayed(config)` - Recently played Steam game with library poster + - Config: `{ userId }` + - Requires `STEAM_WEBAPI_KEY` environment variable + - `userId` must be Steam ID 64 (convert at https://steamid.io/) + +### Code & Development +- `getSourceTubeActivity(config)` - Forgejo activity on source.tube + - Config: `{ feedUrl, userId }` + - Strips username prefix from activity descriptions (e.g., "dylan pushed to repository" becomes "pushed to repository") **Important Notes:** -- `showImage` (defaults to `true`) controls whether images/posters are included in the output. Set it to `false` to show text only. -- For Last.fm, use `getRSSItemTitle('https://lfm.xiffy.nl/your-username')` (via [lfm.xiffy.nl](https://lfm.xiffy.nl)) -- **YouTube** requires channel ID (not username), find it in your channel's page source or URL -- **Goodreads** requires numeric user ID (found in profile URL after `/user/show/`) -- **Steam** requires Steam ID 64 (convert at https://steamid.io/) -- **Pixelfed** requires instance domain and username (e.g., `pixelfed.social`, `username`) +- `showImage` controls whether images/videos are included in the response +- Images and videos are returned separately from text (as `{ text, url, image, video }`) +- Generic RSS/Atom/JSON handlers work with most standard feeds ## API Keys Setup @@ -179,19 +226,31 @@ To get your Trakt slurm key, follow these steps: 1. Go to your History page on Trakt: `https://trakt.tv/users/your-username/history` 2. There should be an RSS feed icon on the top right of the page. Click it. -3. The URL it shows will look something like this: `https://trakt.tv/users/crankle/history.atom?slurm=your-slurm-key` +3. The URL it shows will look something like this: `https://trakt.tv/users/username/history.atom?slurm=your-slurm-key` 4. Copy the `slurm` value from the URL and add it as a secret in your Actions settings named `TRAKT_SLURM`. +5. Add it to your `.env` file for local testing. ### Steam To get your Steam Web API key: 1. Visit https://steamcommunity.com/dev/apikey -2. Enter a domain name (can be localhost for personal use) +2. Enter a domain name (I used omg.lol, I don't know if that will cause me issues later) 3. Agree to the terms and get your API key -4. Add it to your `.env` file as `STEAM_WEBAPI_KEY` or to Actions secrets +4. Add it to your `.env` as `STEAM_WEBAPI_KEY` and to Actions secrets 5. Find your Steam ID 64 at https://steamid.io/ (enter your profile URL) +**Note**: The Steam handler uses the GetOwnedGames API endpoint sorted by `rtime_last_played`, which has no time limit. GetRecentlyPlayedGames can be used too, but it only shows anything played in the last 14 days which may be limiting. + +### Hardcover + +To get your Hardcover API key: + +1. Visit https://hardcover.app/account/api (Make sure you're logged in) +2. Find the authorisation header token section +3. **Important**: The token shown will include "Bearer " at the beginning - do not include this prefix when adding it (Note the space after "Bearer", that needs to be removed too) +4. Add the token to your `.env` file as `HARDCOVER_API_KEY` and to Actions secrets + ## Credits Original project by [melanie kat](https://source.tube/melanie/now-updater). Without her work, this fork would not exist.