Skip to content

Commit 0a482c8

Browse files
committed
fix: per-episode log rows with human-readable messages and air dates
docs: update activity log table to reflect per-episode rows and plain-text details
1 parent f9144cd commit 0a482c8

3 files changed

Lines changed: 46 additions & 52 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ You can also point Tautulli webhooks at Episode Guard so it reacts the moment yo
2525
| Event | What it means |
2626
|---|---|
2727
| **Webhook** | Tautulli sent a play event — Episode Guard woke up immediately |
28-
| **Confirmed** | Current episode is playing — on disk by definition, no check needed |
29-
| **Grabbed** | Triggered a search in Sonarr for a missing episode |
30-
| **On Disk** | Upcoming episode(s) already on disk, no action needed |
31-
| **Monitored** | Upcoming episode wasn't monitored — fixed it so Sonarr grabs it when it airs |
32-
| **Season Monitored** | Near season end — next season marked monitored so Sonarr picks it up on release |
28+
| **Confirmed** | Currently playing — on disk by definition |
29+
| **Grabbed** | Search triggered in Sonarr for a missing episode |
30+
| **On Disk** | Upcoming episode already on disk, or not aired yet — one row per episode |
31+
| **Monitored** | Upcoming episode wasn't monitored — fixed so Sonarr grabs it when it airs |
32+
| **Season Monitored** | Near season end — next season marked monitored |
3333
| **Error** | Something went wrong — check the details column |
3434

35-
The details column shows the trigger (`via webhook` or `via poll`), reason, and episode count where relevant. Episodes marked `not yet aired` are left alone until their air date.
35+
Each upcoming episode gets its own row so you can see exactly what was checked and why. The details column shows a plain-English summary — for unaired episodes it includes the expected air date.
3636

3737
---
3838

src/public/index.html

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -309,25 +309,29 @@ <h1>Episode<span>Guard</span></h1>
309309
function detailSummary(row) {
310310
const d = row.details ? JSON.parse(row.details) : {};
311311
const parts = [];
312-
if (row.episode_count > 1) parts.push('x'+row.episode_count);
313-
if (d.reason) parts.push(d.reason.replace(/_/g,' '));
314-
if (d.status) parts.push(d.status.replace(/_/g,' '));
315-
if (d.message) parts.push(String(d.message).slice(0,60));
312+
if (d.message) {
313+
parts.push(d.message);
314+
} else {
315+
if (row.episode_count > 1) parts.push('x'+row.episode_count);
316+
if (d.reason) parts.push(d.reason.replace(/_/g,' '));
317+
if (d.status) parts.push(d.status.replace(/_/g,' '));
318+
}
316319
if (d.apiStatus && d.apiStatus !== 'ok') parts.push('api: '+String(d.apiStatus).slice(0,40));
317-
if (d.trigger && d.trigger !== 'poll') parts.push('via '+d.trigger);
318320
return esc(parts.join(' - '));
319321
}
320322

321323
function detailPills(row) {
322324
const d = row.details ? JSON.parse(row.details) : {};
323325
const pills = [];
324-
if (row.episode_count > 1) pills.push('x'+row.episode_count+' eps');
325-
if (d.reason) pills.push(d.reason.replace(/_/g,' '));
326-
if (d.status) pills.push(d.status.replace(/_/g,' '));
327-
if (d.trigger && d.trigger !== 'poll') pills.push('via '+d.trigger);
328-
if (d.episodes) pills.push(d.episodes.slice(0,3).join(', ')+(d.episodes.length>3?'...':''));
326+
if (d.message) {
327+
pills.push(d.message);
328+
} else {
329+
if (row.episode_count > 1) pills.push('x'+row.episode_count+' eps');
330+
if (d.reason) pills.push(d.reason.replace(/_/g,' '));
331+
if (d.status) pills.push(d.status.replace(/_/g,' '));
332+
if (d.episodes) pills.push(d.episodes.slice(0,3).join(', ')+(d.episodes.length>3?'...':''));
333+
}
329334
if (d.apiStatus && d.apiStatus !== 'ok') pills.push('! '+String(d.apiStatus).slice(0,40));
330-
if (d.message) pills.push(String(d.message).slice(0,80));
331335
return pills.map(p=>'<span class="detail-pill">'+esc(p)+'</span>').join('');
332336
}
333337

src/watcher.js

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export async function handleWebhookTrigger(ep) {
5151
const dedupKey = showTitle + '-S' + pad(season) + 'E' + pad(episode);
5252
webhookDeduped.set(dedupKey, Date.now());
5353
logEvent({ event_type: 'webhook_received', show_title: showTitle, season, episode,
54-
details: { trigger: 'webhook', tvdbId } });
54+
details: { message: 'Received via webhook', trigger: 'webhook', tvdbId } });
5555
try {
5656
const series = tvdbId ? await findSeriesByTvdbId(tvdbId) : null;
5757
if (!series) {
@@ -159,7 +159,7 @@ async function processEpisode(ep, series, trigger) {
159159
// Currently playing/watched = on disk by definition — no Sonarr check needed
160160
console.log('[watcher] ' + label + ' playing - on disk');
161161
logEvent({ event_type: 'episode_confirmed', show_title: showTitle, season, episode,
162-
details: { status: 'on_disk', trigger, reason: 'playing' } });
162+
details: { message: 'Currently playing', trigger } });
163163

164164
// Fetch current + next season once, share across both checks
165165
console.log('[watcher] Fetching S' + pad(season) + '+S' + pad(season + 1) + ' episodes for ' + label);
@@ -169,46 +169,36 @@ async function processEpisode(ep, series, trigger) {
169169
console.log('[watcher] Checking upcoming for ' + label);
170170
const result = await ensureUpcomingEpisodes(series.id, season, episode, preloaded);
171171

172-
if (result.grabbed > 0 || result.monitored > 0) {
173-
const searchedActions = result.actions.filter(a => a.action === 'search_triggered');
174-
const monitoredActions = result.actions.filter(a => a.action === 'set_monitored');
175-
176-
if (monitoredActions.length) {
177-
logEvent({ event_type: 'episode_monitored', show_title: showTitle, season, episode,
178-
episode_count: monitoredActions.length,
179-
details: { reason: 'upcoming_unmonitored', trigger,
180-
episodes: monitoredActions.map(a => 'S' + pad(a.season) + 'E' + pad(a.episode)),
181-
apiCall: monitoredActions[0].apiCall, apiStatus: monitoredActions[0].apiStatus } });
182-
}
183-
if (searchedActions.length) {
184-
logEvent({ event_type: 'episode_grabbed', show_title: showTitle, season, episode,
185-
episode_count: searchedActions.length,
186-
details: { reason: 'upcoming_not_on_disk', trigger,
187-
episodes: searchedActions.map(a => 'S' + pad(a.season) + 'E' + pad(a.episode)),
188-
apiCall: searchedActions[0].apiCall, apiStatus: searchedActions[0].apiStatus } });
189-
}
190-
console.log('[watcher] ' + label + ' - monitored ' + result.monitored + ', grabbed ' + result.grabbed + ', skipped ' + result.skipped);
191-
} else if (result.skipped > 0 || result.future > 0) {
192-
if (result.skipped > 0) {
193-
logEvent({ event_type: 'episode_skipped', show_title: showTitle, season, episode,
194-
episode_count: result.skipped, details: { reason: 'upcoming_on_disk', trigger } });
195-
console.log('[watcher] ' + label + ' - next ' + result.skipped + ' episode(s) already on disk');
196-
}
197-
if (result.future > 0) {
198-
const futureEps = result.actions.filter(a => a.action === 'skipped_future_airdate');
199-
logEvent({ event_type: 'episode_skipped', show_title: showTitle, season, episode,
200-
episode_count: result.future, details: { reason: 'not_yet_aired', trigger,
201-
episodes: futureEps.map(a => 'S' + pad(a.season) + 'E' + pad(a.episode)),
202-
earliest_airdate: futureEps[0]?.airDateUtc } });
203-
console.log('[watcher] ' + label + ' - ' + result.future + ' future episode(s) skipped (not yet aired)');
172+
// Log one row per upcoming episode action
173+
for (const a of result.actions) {
174+
const epSeason = a.season;
175+
const epEpisode = a.episode;
176+
if (a.action === 'skipped_on_disk') {
177+
logEvent({ event_type: 'episode_skipped', show_title: showTitle, season: epSeason, episode: epEpisode,
178+
details: { message: 'Already on disk, no action needed', trigger } });
179+
} else if (a.action === 'skipped_future_airdate') {
180+
const airMsg = a.airDateUtc ? ' - airs ' + new Date(a.airDateUtc).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : '';
181+
logEvent({ event_type: 'episode_skipped', show_title: showTitle, season: epSeason, episode: epEpisode,
182+
details: { message: 'Not aired yet' + airMsg, trigger, airDateUtc: a.airDateUtc } });
183+
} else if (a.action === 'set_monitored') {
184+
const airMsg = a.airDateUtc ? ' - airs ' + new Date(a.airDateUtc).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : '';
185+
logEvent({ event_type: 'episode_monitored', show_title: showTitle, season: epSeason, episode: epEpisode,
186+
details: { message: 'Set monitored' + airMsg, trigger,
187+
apiCall: a.apiCall, apiStatus: a.apiStatus } });
188+
} else if (a.action === 'search_triggered') {
189+
logEvent({ event_type: 'episode_grabbed', show_title: showTitle, season: epSeason, episode: epEpisode,
190+
details: { message: 'Search triggered - missing episode', trigger,
191+
apiCall: a.apiCall, apiStatus: a.apiStatus } });
204192
}
205193
}
206194

195+
console.log('[watcher] ' + label + ' - monitored ' + result.monitored + ', grabbed ' + result.grabbed + ', skipped ' + result.skipped + ', future ' + result.future);
196+
207197
// 2. Season-end pre-load (reuses preloaded episodes — no extra API call)
208198
const seasonPreloaded = await checkSeasonEndAndPreload(series.id, season, episode, preloaded);
209199
if (seasonPreloaded) {
210200
logEvent({ event_type: 'season_monitored', show_title: showTitle, season: season + 1,
211-
details: { reason: 'season_end_preload', trigger } });
201+
details: { message: 'Near season end - S' + pad(season + 1) + ' pre-monitored', trigger } });
212202
console.log('[watcher] ' + label + ' - near season end, pre-monitoring S' + pad(season + 1));
213203
}
214204
}

0 commit comments

Comments
 (0)