From f3e7d2b3cb9871093f199443644da7c6bb13d5a2 Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:25:16 +0000 Subject: [PATCH 1/3] Include historical events in TV and Sports feeds Removed time-based filtering that previously excluded past events from the calendar feeds and UI. Core library functions were renamed from "Upcoming" variants to more general names (e.g., getEpisodes, getEvents) to reflect the inclusion of both past and future data. Updated the frontend UI and ICS metadata to remove "Upcoming" labeling. --- api/episodes.js | 6 +++--- api/sports-events.js | 4 ++-- lib/sports.js | 15 ++++--------- lib/tvEpisodes.js | 15 ++++++------- public/app.js | 6 +++--- server_output.log | 1 - test/sports.test.js | 26 +++++++++++------------ test/tvEpisodes.test.js | 47 ++++++++++++++++++----------------------- 8 files changed, 52 insertions(+), 68 deletions(-) delete mode 100644 server_output.log diff --git a/api/episodes.js b/api/episodes.js index 39c7eb0..7e75ba0 100644 --- a/api/episodes.js +++ b/api/episodes.js @@ -1,4 +1,4 @@ -import { getUpcomingEpisodes, toIcs } from '../lib/tvEpisodes.js'; +import { getEpisodes, toIcs } from '../lib/tvEpisodes.js'; function sendJson(res, statusCode, payload) { res.statusCode = statusCode; @@ -19,7 +19,7 @@ export default async function handler(req, res) { const timezone = requestUrl.searchParams.get('tz') || 'UTC'; try { - const result = await getUpcomingEpisodes({ query }); + const result = await getEpisodes({ query }); if (format === 'ics') { res.statusCode = 200; @@ -32,7 +32,7 @@ export default async function handler(req, res) { } catch (error) { const statusCode = error.statusCode || (error.message?.includes('404') ? 404 : 500); return sendJson(res, statusCode, { - error: statusCode === 404 ? 'Show not found.' : error.message || 'Unable to fetch upcoming episodes.' + error: statusCode === 404 ? 'Show not found.' : error.message || 'Unable to fetch episodes.' }); } } diff --git a/api/sports-events.js b/api/sports-events.js index 226bd39..a3962f9 100644 --- a/api/sports-events.js +++ b/api/sports-events.js @@ -1,4 +1,4 @@ -import { getUpcomingEvents, toIcs } from '../lib/sports.js'; +import { getEvents, toIcs } from '../lib/sports.js'; function sendJson(res, statusCode, payload) { res.statusCode = statusCode; @@ -23,7 +23,7 @@ export default async function handler(req, res) { } try { - const result = await getUpcomingEvents({ teamId }); + const result = await getEvents({ teamId }); if (format === 'ics') { res.statusCode = 200; diff --git a/lib/sports.js b/lib/sports.js index 1f8df82..7fcf249 100644 --- a/lib/sports.js +++ b/lib/sports.js @@ -246,7 +246,7 @@ async function loadSupplementalTeamEvents(teamId) { } } -export async function getUpcomingEvents({ teamId, fetchImpl = globalThis.fetch } = {}) { +export async function getEvents({ teamId, now = new Date(), fetchImpl = globalThis.fetch } = {}) { if (!teamId) { throw new Error('A team ID is required.'); } @@ -319,14 +319,7 @@ export async function getUpcomingEvents({ teamId, fetchImpl = globalThis.fetch } } } - // Filter for upcoming events (include ongoing events started in the last 3 hours) - const now = new Date(); - const threeHoursAgo = new Date(now.getTime() - 180 * 60 * 1000); - const upcomingEvents = uniqueEvents - .filter((event) => { - const eventDate = parseApiTimestamp(event.timestamp, event.date, event.time); - return eventDate >= threeHoursAgo; - }) + const sortedEvents = uniqueEvents .sort((a, b) => { const dateA = parseApiTimestamp(a.timestamp, a.date, a.time); const dateB = parseApiTimestamp(b.timestamp, b.date, b.time); @@ -335,7 +328,7 @@ export async function getUpcomingEvents({ teamId, fetchImpl = globalThis.fetch } return { team: normalizeTeamSuggestion(team), - events: upcomingEvents, + events: sortedEvents, generatedAt: now.toISOString() }; } @@ -354,7 +347,7 @@ export function toIcs(result, { timezone = 'UTC' } = {}) { const lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', - 'PRODID:-//MakeICS//Sports Upcoming Events//EN', + 'PRODID:-//MakeICS//Sports Events//EN', 'CALSCALE:GREGORIAN', `X-PUBLISHED-TTL:${FEED_REFRESH_INTERVAL}`, `REFRESH-INTERVAL;VALUE=DURATION:${FEED_REFRESH_INTERVAL}` diff --git a/lib/tvEpisodes.js b/lib/tvEpisodes.js index 905596e..c6eeea0 100644 --- a/lib/tvEpisodes.js +++ b/lib/tvEpisodes.js @@ -97,11 +97,8 @@ function normalizeEpisode(episode, show) { }; } -function filterUpcomingEpisodes(episodes, show, now) { - const from = now.getTime(); - +function normalizeEpisodes(episodes, show) { return episodes - .filter((episode) => getEpisodeTimestamp(episode) >= from) .sort((left, right) => getEpisodeTimestamp(left) - getEpisodeTimestamp(right)) .map((episode) => normalizeEpisode(episode, show)); } @@ -320,7 +317,7 @@ async function fetchImdbDetails(imdbId, env, fetchImpl) { } } -export async function getUpcomingEpisodes({ query, now = new Date(), fetchImpl = globalThis.fetch, env = process.env } = {}) { +export async function getEpisodes({ query, now = new Date(), fetchImpl = globalThis.fetch, env = process.env } = {}) { const trimmedQuery = typeof query === 'string' ? query.trim() : ''; if (!trimmedQuery) { const error = new Error('A show name is required.'); @@ -334,7 +331,7 @@ export async function getUpcomingEpisodes({ query, now = new Date(), fetchImpl = const show = await searchShow(trimmedQuery, fetchImpl); const episodes = await fetchShowEpisodes(show.id, fetchImpl); - const upcomingEpisodes = filterUpcomingEpisodes(episodes, show, now); + const allEpisodes = normalizeEpisodes(episodes, show); const imdbId = show.externals?.imdb || null; const imdb = await fetchImdbDetails(imdbId, env, fetchImpl); @@ -361,7 +358,7 @@ export async function getUpcomingEpisodes({ query, now = new Date(), fetchImpl = network: show.network?.name || show.webChannel?.name || null }, imdb, - episodes: upcomingEpisodes + episodes: allEpisodes }; } @@ -383,7 +380,7 @@ export function toIcs(result, { timezone = 'UTC' } = {}) { details.push(`Time: ${episodeTime}.`); } - details.push(episode.summary || `Upcoming episode of ${episode.showName}`); + details.push(episode.summary || `Episode of ${episode.showName}`); return details.join(' '); }; @@ -394,7 +391,7 @@ export function toIcs(result, { timezone = 'UTC' } = {}) { const lines = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', - 'PRODID:-//MakeICS//TV Upcoming Episodes//EN', + 'PRODID:-//MakeICS//TV Episodes//EN', 'CALSCALE:GREGORIAN', `X-PUBLISHED-TTL:${FEED_REFRESH_INTERVAL}`, `REFRESH-INTERVAL;VALUE=DURATION:${FEED_REFRESH_INTERVAL}` diff --git a/public/app.js b/public/app.js index adf12ef..7c9d406 100644 --- a/public/app.js +++ b/public/app.js @@ -18,7 +18,7 @@ let activeSuggestionIndex = -1; const CATEGORY_HINTS = { tv: 'Start typing to see TV show suggestions below the search bar. The all-time feed uses TVMaze for show and episode schedules.', - sports: 'Search for sports teams from TheSportsDB. Copy the ICS URL to track upcoming matches.' + sports: 'Search for sports teams from TheSportsDB. Copy the ICS URL to track matches.' }; // --- Tab Logic --- @@ -389,8 +389,8 @@ function renderList(items, type, context, timezone = 'UTC') { const count = document.createElement('p'); count.className = 'count'; count.textContent = items.length - ? `${items.length} upcoming event${items.length === 1 ? '' : 's'} found.` - : `No upcoming events found.`; + ? `${items.length} event${items.length === 1 ? '' : 's'} found.` + : `No events found.`; resultEl.append(count); const list = document.createElement('div'); diff --git a/server_output.log b/server_output.log deleted file mode 100644 index fb7c8b7..0000000 --- a/server_output.log +++ /dev/null @@ -1 +0,0 @@ -MakeICS TV Episodes running at http://localhost:3000 diff --git a/test/sports.test.js b/test/sports.test.js index d1e1215..7ef83ca 100644 --- a/test/sports.test.js +++ b/test/sports.test.js @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { searchTeamSuggestions, getUpcomingEvents, toIcs } from '../lib/sports.js'; +import { searchTeamSuggestions, getEvents, toIcs } from '../lib/sports.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const CACHE_DIR = path.join(__dirname, '../lib/data/sports'); @@ -128,19 +128,19 @@ test('searchTeamSuggestions returns team results', async () => { assert.equal(suggestions[0].name, 'Arsenal'); }); -test('getUpcomingEvents returns merged and deduplicated events for a team', (t) => { +test('getEvents returns merged and deduplicated events for a team, including past ones', (t) => { const clock = t.mock.timers; - clock.enable({ names: ['Date'], now: new Date('2026-01-01T00:00:00Z') }); + clock.enable({ names: ['Date'], now: new Date('2027-01-01T00:00:00Z') }); return t.test('merging logic', async () => { - const result = await getUpcomingEvents({ + const result = await getEvents({ teamId: '133604', fetchImpl: createFetchMock() }); assert.equal(result.team.name, 'Arsenal'); // Should have: Arsenal vs Real Betis (4), Arsenal vs Everton (1), Chelsea vs Arsenal (3) - // Deduplicated and sorted by date + // Deduplicated and sorted by date, all are in the past now but should be included assert.equal(result.events.length, 3); assert.equal(result.events[0].id, '4'); assert.equal(result.events[1].id, '1'); @@ -148,7 +148,7 @@ test('getUpcomingEvents returns merged and deduplicated events for a team', (t) }); }); -test('getUpcomingEvents utilizes local cache if available', async (t) => { +test('getEvents utilizes local cache if available', async (t) => { const leagueId = '4328'; const cacheFilePath = path.join(CACHE_DIR, `${leagueId}.json`); const cachedData = { @@ -180,7 +180,7 @@ test('getUpcomingEvents utilizes local cache if available', async (t) => { const onCall = (url) => calls.push(url); try { - const result = await getUpcomingEvents({ + const result = await getEvents({ teamId: '133604', fetchImpl: createFetchMock(onCall) }); @@ -197,7 +197,7 @@ test('getUpcomingEvents utilizes local cache if available', async (t) => { } }); -test('getUpcomingEvents merges supplemental (scraped) data', async (t) => { +test('getEvents merges supplemental (scraped) data', async (t) => { const clock = t.mock.timers; clock.enable({ names: ['Date'], now: new Date('2026-01-01T00:00:00Z') }); @@ -229,7 +229,7 @@ test('getUpcomingEvents merges supplemental (scraped) data', async (t) => { await fs.writeFile(supplementalFilePath, JSON.stringify(scrapedData)); try { - const result = await getUpcomingEvents({ + const result = await getEvents({ teamId, fetchImpl: createFetchMock() }); @@ -247,7 +247,7 @@ test('toIcs creates ICS for sports events', async (t) => { const clock = t.mock.timers; clock.enable({ names: ['Date'], now: new Date('2026-01-01T00:00:00Z') }); - const result = await getUpcomingEvents({ + const result = await getEvents({ teamId: '133604', fetchImpl: createFetchMock() }); @@ -260,7 +260,7 @@ test('toIcs creates ICS for sports events', async (t) => { assert.match(ics, /LOCATION:Emirates Stadium/); }); -test('getUpcomingEvents handles strTimestamp with offset correctly', async (t) => { +test('getEvents handles strTimestamp with offset correctly', async (t) => { const clock = t.mock.timers; clock.enable({ names: ['Date'], now: new Date('2026-01-01T00:00:00Z') }); @@ -284,12 +284,12 @@ test('getUpcomingEvents handles strTimestamp with offset correctly', async (t) = const fetchImpl = async (url) => { if (url.includes('lookupteam.php')) return Response.json(teamPayload); if (url.includes('eventsnext.php')) return Response.json(customEventsPayload); - // Return empty for other calls to avoid errors in getUpcomingEvents + // Return empty for other calls to avoid errors in getEvents if (url.includes('lookupleague.php')) return Response.json({ leagues: [] }); return Response.json({}); }; - const result = await getUpcomingEvents({ + const result = await getEvents({ teamId: '133604', fetchImpl }); diff --git a/test/tvEpisodes.test.js b/test/tvEpisodes.test.js index d3e6d8f..b014f38 100644 --- a/test/tvEpisodes.test.js +++ b/test/tvEpisodes.test.js @@ -1,7 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; -import { getUpcomingEpisodes, searchShowSuggestions, toIcs } from '../lib/tvEpisodes.js'; +import { getEpisodes, searchShowSuggestions, toIcs } from '../lib/tvEpisodes.js'; const showPayload = { id: 1, @@ -129,10 +129,9 @@ test('searchShowSuggestions returns Google-style typeahead TV show results', asy assert.ok(requests.some((request) => request.url === 'https://api.tvmaze.com/search/shows?q=exa')); }); -test('getUpcomingEpisodes returns upcoming TVMaze episodes and custom IMDb enrichment', async () => { - const result = await getUpcomingEpisodes({ +test('getEpisodes returns TVMaze episodes and custom IMDb enrichment', async () => { + const result = await getEpisodes({ query: 'Example Show', - now: new Date('2026-05-29T00:00:00Z'), fetchImpl: createFetchMock(), env: { IMDB_API_URL: 'https://imdb.test/title/{imdbId}' } }); @@ -141,17 +140,16 @@ test('getUpcomingEpisodes returns upcoming TVMaze episodes and custom IMDb enric assert.equal(result.show.imdbId, 'tt1234567'); assert.equal(result.imdb.title, 'IMDb Example Show'); assert.equal(result.window.mode, 'all-time'); - assert.equal(result.episodes.length, 2); - assert.deepEqual(result.episodes.map((episode) => episode.name), ['The Future', 'Too Far Away']); - assert.equal(result.episodes[0].summary, 'Future episode.'); + assert.equal(result.episodes.length, 3); + assert.deepEqual(result.episodes.map((episode) => episode.name), ['Already Aired', 'The Future', 'Too Far Away']); + assert.equal(result.episodes[1].summary, 'Future episode.'); assert.equal(result.episodes[0].network, 'Test Network'); }); -test('getUpcomingEpisodes uses the free public IMDb endpoint without an API key by default', async () => { +test('getEpisodes uses the free public IMDb endpoint without an API key by default', async () => { const requests = []; - const result = await getUpcomingEpisodes({ + const result = await getEpisodes({ query: 'Example Show', - now: new Date('2026-05-29T00:00:00Z'), fetchImpl: createFetchMock(requests), env: {} }); @@ -163,7 +161,7 @@ test('getUpcomingEpisodes uses the free public IMDb endpoint without an API key assert.ok(requests.every((request) => !request.options.headers?.Authorization)); }); -test('getUpcomingEpisodes quietly skips unavailable default IMDb enrichment', async () => { +test('getEpisodes quietly skips unavailable default IMDb enrichment', async () => { const requests = []; const fetchImpl = async (url, options = {}) => { requests.push({ url: String(url), options }); @@ -179,9 +177,8 @@ test('getUpcomingEpisodes quietly skips unavailable default IMDb enrichment', as throw new Error(`Unexpected URL: ${url}`); }; - const result = await getUpcomingEpisodes({ + const result = await getEpisodes({ query: 'Example Show', - now: new Date('2026-05-29T00:00:00Z'), fetchImpl, env: {} }); @@ -190,11 +187,11 @@ test('getUpcomingEpisodes quietly skips unavailable default IMDb enrichment', as assert.equal(result.imdb.sourceConfigured, false); assert.equal(result.imdb.error, undefined); assert.match(result.imdb.warning, /HTTP 500/); - assert.equal(result.episodes.length, 2); + assert.equal(result.episodes.length, 3); assert.ok(requests.some((request) => request.url === 'https://imdb.iamidiotareyoutoo.com/search?tt=tt1234567')); }); -test('getUpcomingEpisodes quietly skips default IMDb network failures', async () => { +test('getEpisodes quietly skips default IMDb network failures', async () => { const fetchImpl = async (url) => { if (String(url).includes('/singlesearch/shows')) { return Response.json(showPayload); @@ -208,9 +205,8 @@ test('getUpcomingEpisodes quietly skips default IMDb network failures', async () throw new Error(`Unexpected URL: ${url}`); }; - const result = await getUpcomingEpisodes({ + const result = await getEpisodes({ query: 'Example Show', - now: new Date('2026-05-29T00:00:00Z'), fetchImpl, env: {} }); @@ -218,14 +214,13 @@ test('getUpcomingEpisodes quietly skips default IMDb network failures', async () assert.equal(result.imdb.sourceConfigured, false); assert.equal(result.imdb.error, undefined); assert.match(result.imdb.warning, /request failed/); - assert.equal(result.episodes.length, 2); + assert.equal(result.episodes.length, 3); }); -test('getUpcomingEpisodes uses FIRECRAWL_API_KEY for IMDb scraping when configured', async () => { +test('getEpisodes uses FIRECRAWL_API_KEY for IMDb scraping when configured', async () => { const requests = []; - const result = await getUpcomingEpisodes({ + const result = await getEpisodes({ query: 'Example Show', - now: new Date('2026-05-29T00:00:00Z'), fetchImpl: createFetchMock(requests), env: { FIRECRAWL_API_KEY: 'fc-test', FIRECRAWL_API_URL: 'https://firecrawl.test/v2/scrape' } }); @@ -239,17 +234,16 @@ test('getUpcomingEpisodes uses FIRECRAWL_API_KEY for IMDb scraping when configur assert.match(firecrawlRequest.options.body, /https:\/\/www\.imdb\.com\/title\/tt1234567\//); }); -test('getUpcomingEpisodes validates missing show names', async () => { +test('getEpisodes validates missing show names', async () => { await assert.rejects( - () => getUpcomingEpisodes({ query: ' ', fetchImpl: createFetchMock() }), + () => getEpisodes({ query: ' ', fetchImpl: createFetchMock() }), /show name is required/ ); }); -test('toIcs creates a daily-refreshing calendar event feed for each upcoming episode', async () => { - const result = await getUpcomingEpisodes({ +test('toIcs creates a daily-refreshing calendar event feed for episodes', async () => { + const result = await getEpisodes({ query: 'Example Show', - now: new Date('2026-05-29T00:00:00Z'), fetchImpl: createFetchMock(), env: {} }); @@ -258,6 +252,7 @@ test('toIcs creates a daily-refreshing calendar event feed for each upcoming epi assert.match(ics, /BEGIN:VCALENDAR/); assert.match(ics, /X-PUBLISHED-TTL:PT24H/); assert.match(ics, /REFRESH-INTERVAL;VALUE=DURATION:PT24H/); + assert.match(ics, /SUMMARY:Example Show S01E01 Already Aired/); assert.match(ics, /SUMMARY:Example Show S02E03 The Future/); assert.match(ics, /SUMMARY:Example Show S02E04 Too Far Away/); assert.match(ics, /DESCRIPTION:.*Time: 9:00 PM EDT \/ 6:00 PM PDT.*/); From 382e5426d638beba0155466737c42e030daa6177 Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:37:42 +0000 Subject: [PATCH 2/3] Include historical events and fix episode filtering - Modified `lib/tvEpisodes.js` and `lib/sports.js` to include past events by removing time-based filters. - Renamed `getUpcomingEpisodes` to `getEpisodes` and `getUpcomingEvents` to `getEvents`. - Added a filter in `lib/tvEpisodes.js` to exclude episodes without scheduling info (`airstamp` or `airdate`) to prevent ICS generation errors. - Updated frontend UI in `public/app.js` and metadata in `lib/tvEpisodes.js` and `lib/sports.js` to reflect that feeds now include all events, not just upcoming ones. - Updated tests to verify inclusion of past events and new filtering logic. --- lib/tvEpisodes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tvEpisodes.js b/lib/tvEpisodes.js index c6eeea0..41e8b12 100644 --- a/lib/tvEpisodes.js +++ b/lib/tvEpisodes.js @@ -331,7 +331,7 @@ export async function getEpisodes({ query, now = new Date(), fetchImpl = globalT const show = await searchShow(trimmedQuery, fetchImpl); const episodes = await fetchShowEpisodes(show.id, fetchImpl); - const allEpisodes = normalizeEpisodes(episodes, show); + const allEpisodes = normalizeEpisodes(episodes, show).filter((e) => e.airstamp || e.airdate); const imdbId = show.externals?.imdb || null; const imdb = await fetchImdbDetails(imdbId, env, fetchImpl); From 3f36aeac50bced2b002b6e86c57986b07ff146aa Mon Sep 17 00:00:00 2001 From: DisabledAbel <196466003+DisabledAbel@users.noreply.github.com> Date: Fri, 12 Jun 2026 20:53:41 +0000 Subject: [PATCH 3/3] Retain past events in feeds using a 'since' parameter - Modified `lib/tvEpisodes.js` and `lib/sports.js` to include historical events by removing internal time-based filters. - Introduced an optional `since` parameter to `getEpisodes` and `getEvents` to allow clients to specify a start date for the feed. - Updated `api/episodes.js` and `api/sports-events.js` to pass the `since` query parameter. - Updated `public/app.js` to automatically include the current date as the `since` parameter in generated ICS URLs, ensuring that events are "cached" in the feed from the day the user subscribes. - Added a safety check in `lib/tvEpisodes.js` to filter out episodes missing both `airstamp` and `airdate`. - Updated frontend UI hints and metadata to reflect the new persistent event tracking behavior. - Updated unit tests to verify the `since` filtering and inclusion of past events. --- api/episodes.js | 3 ++- api/sports-events.js | 3 ++- lib/sports.js | 14 ++++++++++++-- lib/tvEpisodes.js | 12 ++++++++++-- public/app.js | 14 ++++++++------ 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/api/episodes.js b/api/episodes.js index 7e75ba0..5831288 100644 --- a/api/episodes.js +++ b/api/episodes.js @@ -17,9 +17,10 @@ export default async function handler(req, res) { const query = requestUrl.searchParams.get('show') || requestUrl.searchParams.get('q'); const format = requestUrl.searchParams.get('format'); const timezone = requestUrl.searchParams.get('tz') || 'UTC'; + const since = requestUrl.searchParams.get('since'); try { - const result = await getEpisodes({ query }); + const result = await getEpisodes({ query, since }); if (format === 'ics') { res.statusCode = 200; diff --git a/api/sports-events.js b/api/sports-events.js index a3962f9..58fdc69 100644 --- a/api/sports-events.js +++ b/api/sports-events.js @@ -17,13 +17,14 @@ export default async function handler(req, res) { const teamId = requestUrl.searchParams.get('teamId'); const format = requestUrl.searchParams.get('format'); const timezone = requestUrl.searchParams.get('tz') || 'UTC'; + const since = requestUrl.searchParams.get('since'); if (!teamId) { return sendJson(res, 400, { error: 'teamId is required.' }); } try { - const result = await getEvents({ teamId }); + const result = await getEvents({ teamId, since }); if (format === 'ics') { res.statusCode = 200; diff --git a/lib/sports.js b/lib/sports.js index 7fcf249..b3e9bdf 100644 --- a/lib/sports.js +++ b/lib/sports.js @@ -246,7 +246,7 @@ async function loadSupplementalTeamEvents(teamId) { } } -export async function getEvents({ teamId, now = new Date(), fetchImpl = globalThis.fetch } = {}) { +export async function getEvents({ teamId, now = new Date(), since = null, fetchImpl = globalThis.fetch } = {}) { if (!teamId) { throw new Error('A team ID is required.'); } @@ -319,13 +319,23 @@ export async function getEvents({ teamId, now = new Date(), fetchImpl = globalTh } } - const sortedEvents = uniqueEvents + let sortedEvents = uniqueEvents .sort((a, b) => { const dateA = parseApiTimestamp(a.timestamp, a.date, a.time); const dateB = parseApiTimestamp(b.timestamp, b.date, b.time); return dateA - dateB; }); + if (since) { + const sinceTime = Date.parse(since); + if (!Number.isNaN(sinceTime)) { + sortedEvents = sortedEvents.filter((e) => { + const timestamp = parseApiTimestamp(e.timestamp, e.date, e.time); + return timestamp.getTime() >= sinceTime; + }); + } + } + return { team: normalizeTeamSuggestion(team), events: sortedEvents, diff --git a/lib/tvEpisodes.js b/lib/tvEpisodes.js index 41e8b12..9ff35f3 100644 --- a/lib/tvEpisodes.js +++ b/lib/tvEpisodes.js @@ -317,7 +317,7 @@ async function fetchImdbDetails(imdbId, env, fetchImpl) { } } -export async function getEpisodes({ query, now = new Date(), fetchImpl = globalThis.fetch, env = process.env } = {}) { +export async function getEpisodes({ query, now = new Date(), since = null, fetchImpl = globalThis.fetch, env = process.env } = {}) { const trimmedQuery = typeof query === 'string' ? query.trim() : ''; if (!trimmedQuery) { const error = new Error('A show name is required.'); @@ -331,7 +331,15 @@ export async function getEpisodes({ query, now = new Date(), fetchImpl = globalT const show = await searchShow(trimmedQuery, fetchImpl); const episodes = await fetchShowEpisodes(show.id, fetchImpl); - const allEpisodes = normalizeEpisodes(episodes, show).filter((e) => e.airstamp || e.airdate); + let allEpisodes = normalizeEpisodes(episodes, show).filter((e) => e.airstamp || e.airdate); + + if (since) { + const sinceTime = Date.parse(since); + if (!Number.isNaN(sinceTime)) { + allEpisodes = allEpisodes.filter((e) => getEpisodeTimestamp(e) >= sinceTime); + } + } + const imdbId = show.externals?.imdb || null; const imdb = await fetchImdbDetails(imdbId, env, fetchImpl); diff --git a/public/app.js b/public/app.js index 7c9d406..afd4025 100644 --- a/public/app.js +++ b/public/app.js @@ -17,8 +17,8 @@ let suggestionAbortController; let activeSuggestionIndex = -1; const CATEGORY_HINTS = { - tv: 'Start typing to see TV show suggestions below the search bar. The all-time feed uses TVMaze for show and episode schedules.', - sports: 'Search for sports teams from TheSportsDB. Copy the ICS URL to track matches.' + tv: 'Start typing to see TV show suggestions below the search bar. The feed includes all episodes from today onwards once added.', + sports: 'Search for sports teams from TheSportsDB. Copy the ICS URL to track matches from today onwards once added.' }; // --- Tab Logic --- @@ -225,10 +225,11 @@ function formatAirDate(dateStr, timeStr, timestamp, includeZones = false, timezo function icsUrlForCurrent() { let path = ''; const tz = timezoneInput?.value || 'UTC'; + const since = new Date().toISOString().split('T')[0]; if (currentCategory === 'tv') { - path = `/api/episodes?show=${encodeURIComponent(showInput.value)}&format=ics&tz=${encodeURIComponent(tz)}`; + path = `/api/episodes?show=${encodeURIComponent(showInput.value)}&format=ics&tz=${encodeURIComponent(tz)}&since=${since}`; } else if (currentCategory === 'sports') { - path = `/api/sports-events?teamId=${encodeURIComponent(sportsInput.dataset.teamId)}&format=ics&tz=${encodeURIComponent(tz)}`; + path = `/api/sports-events?teamId=${encodeURIComponent(sportsInput.dataset.teamId)}&format=ics&tz=${encodeURIComponent(tz)}&since=${since}`; } return new URL(path, window.location.origin).href; } @@ -494,9 +495,10 @@ form.addEventListener('submit', async (event) => { let label = ''; const tz = timezoneInput?.value || 'UTC'; + const since = new Date().toISOString().split('T')[0]; if (currentCategory === 'tv') { label = showInput.value; - url = `/api/episodes?show=${encodeURIComponent(label)}&tz=${encodeURIComponent(tz)}`; + url = `/api/episodes?show=${encodeURIComponent(label)}&tz=${encodeURIComponent(tz)}&since=${since}`; } else if (currentCategory === 'sports') { const teamId = sportsInput.dataset.teamId; if (!teamId || teamId === 'undefined') { @@ -504,7 +506,7 @@ form.addEventListener('submit', async (event) => { return; } label = sportsInput.value; - url = `/api/sports-events?teamId=${encodeURIComponent(teamId)}&tz=${encodeURIComponent(tz)}`; + url = `/api/sports-events?teamId=${encodeURIComponent(teamId)}&tz=${encodeURIComponent(tz)}&since=${since}`; } setStatus(`Fetching for ${label}...`);