Track Keyword Mentions in Podcast Transcripts Every Day
Get every podcast episode that mentioned your topic yesterday, with the passages and links, as raw data you can store.
Before you start
- API token
- A topic and the words people use for it
Choose your example
Your selection applies to every step. HTTP uses built-in clients without the Pod Engine libraries.
Use Node.js 22 or newer. Save the complete script as recipe.mjs.
npm install @podengine/sdk
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
node recipe.mjs Use Node.js 22 or newer. Save the complete script as recipe.mjs. No package installation is needed.
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
node recipe.mjs Use Python 3.10 or newer. Save the complete script as recipe.py.
python -m pip install podengine
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
python recipe.py Use Python 3.10 or newer. Save the complete script as recipe.py. No package installation is needed.
# macOS / Linux shell; set the same variable in your shell on Windows
export PODENGINE_API_KEY="YOUR_API_KEY"
python recipe.py Set up the client
Start your file with this setup, then append each step in order. The complete script below includes everything.
import { PodEngine } from '@podengine/sdk';
const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const pe = new PodEngine({
apiKey,
baseUrl: process.env.PODENGINE_API_URL || 'https://api.podengine.ai',
}); const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const baseUrl = process.env.PODENGINE_API_URL || 'https://api.podengine.ai';
async function request(path, body) {
const response = await fetch(`${baseUrl}${path}`, {
method: body === undefined ? 'GET' : 'POST',
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
if (!response.ok) throw new Error(`Pod Engine request failed: ${response.status} ${response.statusText}`);
return (await response.json()).data;
} import json
import os
import re
import sys
from datetime import datetime, timedelta, timezone
from podengine import PodEngine
pe = PodEngine(
api_key=os.environ["PODENGINE_API_KEY"],
base_url=os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai"),
) import json
import os
import re
import sys
from datetime import datetime, timedelta, timezone
from urllib.request import Request, urlopen
api_key = os.environ["PODENGINE_API_KEY"]
base_url = os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai")
def request(path, body=None):
req = Request(
base_url + path,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": api_key, "Content-Type": "application/json"},
method="POST" if body is not None else "GET",
)
# urlopen raises HTTPError for non-success responses.
with urlopen(req, timeout=30) as response:
return json.load(response)["data"] Complete runnable script
import { PodEngine } from '@podengine/sdk';
const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const pe = new PodEngine({
apiKey,
baseUrl: process.env.PODENGINE_API_URL || 'https://api.podengine.ai',
});
// Quotes make each term a phrase match.
const inTranscript = (terms) =>
terms.map((term) => ({ searchTerm: `"${term}"`, searchType: 'text', searchTargets: ['transcript'] }));
// Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
const dayEnd = new Date();
dayEnd.setUTCHours(0, 0, 0, 0);
const dayStart = new Date(dayEnd.getTime() - 24 * 60 * 60 * 1000);
const phraseQuery = {
searchTermGroups: [
{
searchTerms: inTranscript([
'postpartum depression',
'post-partum depression',
'postnatal depression',
'post-natal depression',
'perinatal depression',
]),
},
],
publishedSince: dayStart.toISOString(),
publishedBefore: dayEnd.toISOString(),
transcriptHighlightLength: 300,
pageSize: 100,
};
const phrasePage = await pe.search.searchEpisodes(phraseQuery);
console.log(`Episodes matching a phrase: ${phrasePage.result.total.value}`);
console.log(phrasePage.result.hits[0]?.transcriptHighlights);
const acronymQuery = {
...phraseQuery,
searchTermGroups: [
{ searchTerms: inTranscript(['PPD', 'PND']) },
{
searchTerms: inTranscript([
'postpartum',
'postnatal',
'perinatal',
'pregnancy',
'pregnant',
'newborn',
'giving birth',
'new mom',
'new mum',
]),
},
],
};
const acronymPage = await pe.search.searchEpisodes(acronymQuery);
console.log(`Episodes matching an acronym in context: ${acronymPage.result.total.value}`);
const maxPerQuery = 5000;
const mentions = new Map();
for (const [label, query] of [
['phrase', phraseQuery],
['acronym', acronymQuery],
]) {
let cursor = null;
let fetched = 0;
do {
const page = await pe.search.searchEpisodes({ ...query, cursor });
for (const hit of page.result.hits) {
const mention = mentions.get(hit.episodeId) ?? { hit, matchedBy: [], passages: [] };
if (!mention.matchedBy.includes(label)) mention.matchedBy.push(label);
for (const passage of hit.transcriptHighlights ?? []) {
if (!mention.passages.includes(passage)) mention.passages.push(passage);
}
mentions.set(hit.episodeId, mention);
}
fetched += page.result.hits.length;
const nextCursor = page.cursor;
if (!page.result.hits.length || !nextCursor || nextCursor === cursor) break;
cursor = nextCursor;
} while (fetched < maxPerQuery);
if (fetched >= maxPerQuery) console.warn(`The ${label} search reached ${maxPerQuery} results; use a shorter window`);
}
console.log(`Episodes mentioning the topic on ${dayStart.toISOString().slice(0, 10)}: ${mentions.size}`);
const rows = [...mentions.values()].map(({ hit, matchedBy, passages }) => ({
episodeId: hit.episodeId,
published: hit.episodeRssPublishedAt,
podcast: hit.podcastRssTitle,
episode: hit.episodeRssTitle,
matchedBy: matchedBy.join('+'),
passages: passages.map((passage) => passage.replace(/<\/?em>/g, '')),
estimatedMonthlyListeners: hit.podcastAudienceEstimatedMonthlyListeners ?? null,
podEngineUrl: `https://www.podengine.ai/podcasts/${hit.podcastSlug}/${hit.episodeSlug}`,
audioUrl: hit.episodeEnclosureUrl,
applePodcastsUrl: hit.appleId ? `https://podcasts.apple.com/podcast/id${hit.appleId}` : null,
spotifyUrl: hit.spotifyId ? `https://open.spotify.com/show/${hit.spotifyId}` : null,
}));
console.log(JSON.stringify(rows, null, 2)); const apiKey = process.env.PODENGINE_API_KEY;
if (!apiKey) throw new Error('Set PODENGINE_API_KEY before running this recipe');
const baseUrl = process.env.PODENGINE_API_URL || 'https://api.podengine.ai';
async function request(path, body) {
const response = await fetch(`${baseUrl}${path}`, {
method: body === undefined ? 'GET' : 'POST',
headers: { Authorization: apiKey, 'Content-Type': 'application/json' },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
if (!response.ok) throw new Error(`Pod Engine request failed: ${response.status} ${response.statusText}`);
return (await response.json()).data;
}
// Quotes make each term a phrase match.
const inTranscript = (terms) =>
terms.map((term) => ({ searchTerm: `"${term}"`, searchType: 'text', searchTargets: ['transcript'] }));
// Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
const dayEnd = new Date();
dayEnd.setUTCHours(0, 0, 0, 0);
const dayStart = new Date(dayEnd.getTime() - 24 * 60 * 60 * 1000);
const phraseQuery = {
searchTermGroups: [
{
searchTerms: inTranscript([
'postpartum depression',
'post-partum depression',
'postnatal depression',
'post-natal depression',
'perinatal depression',
]),
},
],
publishedSince: dayStart.toISOString(),
publishedBefore: dayEnd.toISOString(),
transcriptHighlightLength: 300,
pageSize: 100,
};
const phrasePage = await request('/api/v1/search/episodes', phraseQuery);
console.log(`Episodes matching a phrase: ${phrasePage.result.total.value}`);
console.log(phrasePage.result.hits[0]?.transcriptHighlights);
const acronymQuery = {
...phraseQuery,
searchTermGroups: [
{ searchTerms: inTranscript(['PPD', 'PND']) },
{
searchTerms: inTranscript([
'postpartum',
'postnatal',
'perinatal',
'pregnancy',
'pregnant',
'newborn',
'giving birth',
'new mom',
'new mum',
]),
},
],
};
const acronymPage = await request('/api/v1/search/episodes', acronymQuery);
console.log(`Episodes matching an acronym in context: ${acronymPage.result.total.value}`);
const maxPerQuery = 5000;
const mentions = new Map();
for (const [label, query] of [
['phrase', phraseQuery],
['acronym', acronymQuery],
]) {
let cursor = null;
let fetched = 0;
do {
const page = await request('/api/v1/search/episodes', { ...query, cursor });
for (const hit of page.result.hits) {
const mention = mentions.get(hit.episodeId) ?? { hit, matchedBy: [], passages: [] };
if (!mention.matchedBy.includes(label)) mention.matchedBy.push(label);
for (const passage of hit.transcriptHighlights ?? []) {
if (!mention.passages.includes(passage)) mention.passages.push(passage);
}
mentions.set(hit.episodeId, mention);
}
fetched += page.result.hits.length;
const nextCursor = page.cursor;
if (!page.result.hits.length || !nextCursor || nextCursor === cursor) break;
cursor = nextCursor;
} while (fetched < maxPerQuery);
if (fetched >= maxPerQuery) console.warn(`The ${label} search reached ${maxPerQuery} results; use a shorter window`);
}
console.log(`Episodes mentioning the topic on ${dayStart.toISOString().slice(0, 10)}: ${mentions.size}`);
const rows = [...mentions.values()].map(({ hit, matchedBy, passages }) => ({
episodeId: hit.episodeId,
published: hit.episodeRssPublishedAt,
podcast: hit.podcastRssTitle,
episode: hit.episodeRssTitle,
matchedBy: matchedBy.join('+'),
passages: passages.map((passage) => passage.replace(/<\/?em>/g, '')),
estimatedMonthlyListeners: hit.podcastAudienceEstimatedMonthlyListeners ?? null,
podEngineUrl: `https://www.podengine.ai/podcasts/${hit.podcastSlug}/${hit.episodeSlug}`,
audioUrl: hit.episodeEnclosureUrl,
applePodcastsUrl: hit.appleId ? `https://podcasts.apple.com/podcast/id${hit.appleId}` : null,
spotifyUrl: hit.spotifyId ? `https://open.spotify.com/show/${hit.spotifyId}` : null,
}));
console.log(JSON.stringify(rows, null, 2)); import json
import os
import re
import sys
from datetime import datetime, timedelta, timezone
from podengine import PodEngine
pe = PodEngine(
api_key=os.environ["PODENGINE_API_KEY"],
base_url=os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai"),
)
def in_transcript(terms):
# Quotes make each term a phrase match.
return [{"searchTerm": f'"{term}"', "searchType": "text", "searchTargets": ["transcript"]} for term in terms]
# Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
day_end = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
day_start = day_end - timedelta(days=1)
phrase_query = {
"search_term_groups": [
{
"searchTerms": in_transcript(
[
"postpartum depression",
"post-partum depression",
"postnatal depression",
"post-natal depression",
"perinatal depression",
]
)
}
],
"published_since": day_start.isoformat(),
"published_before": day_end.isoformat(),
"transcript_highlight_length": 300,
"page_size": 100,
}
phrase_page = pe.search.search_episodes(**phrase_query)
print("Episodes matching a phrase:", phrase_page.result.total.value)
if phrase_page.result.hits:
print(phrase_page.result.hits[0].transcript_highlights)
acronym_query = {
**phrase_query,
"search_term_groups": [
{"searchTerms": in_transcript(["PPD", "PND"])},
{
"searchTerms": in_transcript(
[
"postpartum",
"postnatal",
"perinatal",
"pregnancy",
"pregnant",
"newborn",
"giving birth",
"new mom",
"new mum",
]
)
},
],
}
acronym_page = pe.search.search_episodes(**acronym_query)
print("Episodes matching an acronym in context:", acronym_page.result.total.value)
max_per_query = 5000
mentions = {}
for label, query in [("phrase", phrase_query), ("acronym", acronym_query)]:
cursor = None
fetched = 0
while fetched < max_per_query:
page = pe.search.search_episodes(**query, cursor=cursor)
# API field names, so the rows below match the HTTP examples.
hits = [hit.model_dump(by_alias=True, mode="json") for hit in page.result.hits]
for hit in hits:
mention = mentions.setdefault(hit["episodeId"], {"hit": hit, "matchedBy": [], "passages": []})
if label not in mention["matchedBy"]:
mention["matchedBy"].append(label)
for passage in hit.get("transcriptHighlights") or []:
if passage not in mention["passages"]:
mention["passages"].append(passage)
fetched += len(hits)
next_cursor = page.cursor
if not hits or not next_cursor or next_cursor == cursor:
break
cursor = next_cursor
if fetched >= max_per_query:
print(f"The {label} search reached {max_per_query} results; use a shorter window", file=sys.stderr)
print(f"Episodes mentioning the topic on {day_start.date()}: {len(mentions)}")
rows = []
for mention in mentions.values():
hit = mention["hit"]
rows.append(
{
"episodeId": hit["episodeId"],
"published": hit["episodeRssPublishedAt"],
"podcast": hit["podcastRssTitle"],
"episode": hit["episodeRssTitle"],
"matchedBy": "+".join(mention["matchedBy"]),
"passages": [re.sub(r"</?em>", "", passage) for passage in mention["passages"]],
"estimatedMonthlyListeners": hit.get("podcastAudienceEstimatedMonthlyListeners"),
"podEngineUrl": f"https://www.podengine.ai/podcasts/{hit['podcastSlug']}/{hit['episodeSlug']}",
"audioUrl": hit["episodeEnclosureUrl"],
"applePodcastsUrl": f"https://podcasts.apple.com/podcast/id{hit['appleId']}" if hit.get("appleId") else None,
"spotifyUrl": f"https://open.spotify.com/show/{hit['spotifyId']}" if hit.get("spotifyId") else None,
}
)
print(json.dumps(rows, indent=2)) import json
import os
import re
import sys
from datetime import datetime, timedelta, timezone
from urllib.request import Request, urlopen
api_key = os.environ["PODENGINE_API_KEY"]
base_url = os.environ.get("PODENGINE_API_URL", "https://api.podengine.ai")
def request(path, body=None):
req = Request(
base_url + path,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": api_key, "Content-Type": "application/json"},
method="POST" if body is not None else "GET",
)
# urlopen raises HTTPError for non-success responses.
with urlopen(req, timeout=30) as response:
return json.load(response)["data"]
def in_transcript(terms):
# Quotes make each term a phrase match.
return [{"searchTerm": f'"{term}"', "searchType": "text", "searchTargets": ["transcript"]} for term in terms]
# Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
day_end = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
day_start = day_end - timedelta(days=1)
phrase_query = {
"searchTermGroups": [
{
"searchTerms": in_transcript(
[
"postpartum depression",
"post-partum depression",
"postnatal depression",
"post-natal depression",
"perinatal depression",
]
)
}
],
"publishedSince": day_start.isoformat(),
"publishedBefore": day_end.isoformat(),
"transcriptHighlightLength": 300,
"pageSize": 100,
}
phrase_page = request("/api/v1/search/episodes", phrase_query)
print("Episodes matching a phrase:", phrase_page["result"]["total"]["value"])
if phrase_page["result"]["hits"]:
print(phrase_page["result"]["hits"][0].get("transcriptHighlights"))
acronym_query = {
**phrase_query,
"searchTermGroups": [
{"searchTerms": in_transcript(["PPD", "PND"])},
{
"searchTerms": in_transcript(
[
"postpartum",
"postnatal",
"perinatal",
"pregnancy",
"pregnant",
"newborn",
"giving birth",
"new mom",
"new mum",
]
)
},
],
}
acronym_page = request("/api/v1/search/episodes", acronym_query)
print("Episodes matching an acronym in context:", acronym_page["result"]["total"]["value"])
max_per_query = 5000
mentions = {}
for label, query in [("phrase", phrase_query), ("acronym", acronym_query)]:
cursor = None
fetched = 0
while fetched < max_per_query:
page = request("/api/v1/search/episodes", {**query, "cursor": cursor})
hits = page["result"]["hits"]
for hit in hits:
mention = mentions.setdefault(hit["episodeId"], {"hit": hit, "matchedBy": [], "passages": []})
if label not in mention["matchedBy"]:
mention["matchedBy"].append(label)
for passage in hit.get("transcriptHighlights") or []:
if passage not in mention["passages"]:
mention["passages"].append(passage)
fetched += len(hits)
next_cursor = page["cursor"]
if not hits or not next_cursor or next_cursor == cursor:
break
cursor = next_cursor
if fetched >= max_per_query:
print(f"The {label} search reached {max_per_query} results; use a shorter window", file=sys.stderr)
print(f"Episodes mentioning the topic on {day_start.date()}: {len(mentions)}")
rows = []
for mention in mentions.values():
hit = mention["hit"]
rows.append(
{
"episodeId": hit["episodeId"],
"published": hit["episodeRssPublishedAt"],
"podcast": hit["podcastRssTitle"],
"episode": hit["episodeRssTitle"],
"matchedBy": "+".join(mention["matchedBy"]),
"passages": [re.sub(r"</?em>", "", passage) for passage in mention["passages"]],
"estimatedMonthlyListeners": hit.get("podcastAudienceEstimatedMonthlyListeners"),
"podEngineUrl": f"https://www.podengine.ai/podcasts/{hit['podcastSlug']}/{hit['episodeSlug']}",
"audioUrl": hit["episodeEnclosureUrl"],
"applePodcastsUrl": f"https://podcasts.apple.com/podcast/id{hit['appleId']}" if hit.get("appleId") else None,
"spotifyUrl": f"https://open.spotify.com/show/{hit['spotifyId']}" if hit.get("spotifyId") else None,
}
)
print(json.dumps(rows, indent=2)) This recipe is for research, social-listening and PR teams who need every podcast mention of a topic, every day, as raw data. The example tracks postpartum depression. Swap in your own topic and words.
Choose your language and SDK or HTTP above. Run the setup first, then append each step to the same file.
Send the Episode Search endpoint one searchTermGroups group that lists every spelled-out way of saying the topic. An episode matches when its transcript contains any one of the terms. Quotes around a term make it a phrase, so "postpartum depression" only matches those words together.
publishedSince and publishedBefore limit the search to yesterday in UTC. transcriptHighlightLength returns up to five passages around the matches in each hit's transcriptHighlights.
// Quotes make each term a phrase match.
const inTranscript = (terms) =>
terms.map((term) => ({ searchTerm: `"${term}"`, searchType: 'text', searchTargets: ['transcript'] }));
// Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
const dayEnd = new Date();
dayEnd.setUTCHours(0, 0, 0, 0);
const dayStart = new Date(dayEnd.getTime() - 24 * 60 * 60 * 1000);
const phraseQuery = {
searchTermGroups: [
{
searchTerms: inTranscript([
'postpartum depression',
'post-partum depression',
'postnatal depression',
'post-natal depression',
'perinatal depression',
]),
},
],
publishedSince: dayStart.toISOString(),
publishedBefore: dayEnd.toISOString(),
transcriptHighlightLength: 300,
pageSize: 100,
};
const phrasePage = await pe.search.searchEpisodes(phraseQuery);
console.log(`Episodes matching a phrase: ${phrasePage.result.total.value}`);
console.log(phrasePage.result.hits[0]?.transcriptHighlights); // Quotes make each term a phrase match.
const inTranscript = (terms) =>
terms.map((term) => ({ searchTerm: `"${term}"`, searchType: 'text', searchTargets: ['transcript'] }));
// Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
const dayEnd = new Date();
dayEnd.setUTCHours(0, 0, 0, 0);
const dayStart = new Date(dayEnd.getTime() - 24 * 60 * 60 * 1000);
const phraseQuery = {
searchTermGroups: [
{
searchTerms: inTranscript([
'postpartum depression',
'post-partum depression',
'postnatal depression',
'post-natal depression',
'perinatal depression',
]),
},
],
publishedSince: dayStart.toISOString(),
publishedBefore: dayEnd.toISOString(),
transcriptHighlightLength: 300,
pageSize: 100,
};
const phrasePage = await request('/api/v1/search/episodes', phraseQuery);
console.log(`Episodes matching a phrase: ${phrasePage.result.total.value}`);
console.log(phrasePage.result.hits[0]?.transcriptHighlights); def in_transcript(terms):
# Quotes make each term a phrase match.
return [{"searchTerm": f'"{term}"', "searchType": "text", "searchTargets": ["transcript"]} for term in terms]
# Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
day_end = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
day_start = day_end - timedelta(days=1)
phrase_query = {
"search_term_groups": [
{
"searchTerms": in_transcript(
[
"postpartum depression",
"post-partum depression",
"postnatal depression",
"post-natal depression",
"perinatal depression",
]
)
}
],
"published_since": day_start.isoformat(),
"published_before": day_end.isoformat(),
"transcript_highlight_length": 300,
"page_size": 100,
}
phrase_page = pe.search.search_episodes(**phrase_query)
print("Episodes matching a phrase:", phrase_page.result.total.value)
if phrase_page.result.hits:
print(phrase_page.result.hits[0].transcript_highlights) def in_transcript(terms):
# Quotes make each term a phrase match.
return [{"searchTerm": f'"{term}"', "searchType": "text", "searchTargets": ["transcript"]} for term in terms]
# Yesterday in UTC: publishedSince is inclusive, publishedBefore is exclusive.
day_end = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
day_start = day_end - timedelta(days=1)
phrase_query = {
"searchTermGroups": [
{
"searchTerms": in_transcript(
[
"postpartum depression",
"post-partum depression",
"postnatal depression",
"post-natal depression",
"perinatal depression",
]
)
}
],
"publishedSince": day_start.isoformat(),
"publishedBefore": day_end.isoformat(),
"transcriptHighlightLength": 300,
"pageSize": 100,
}
phrase_page = request("/api/v1/search/episodes", phrase_query)
print("Episodes matching a phrase:", phrase_page["result"]["total"]["value"])
if phrase_page["result"]["hits"]:
print(phrase_page["result"]["hits"][0].get("transcriptHighlights")) - From 21 September to 21 October 2026, spelled-out phrases like these matched about 1,650 episodes.
- A group holds up to 10 terms, and a search holds up to 10 groups.
- Each word of a phrase is wrapped in its own <em> tag, for example <em>postpartum</em> <em>depression</em>. The last step removes the tags.
- To get one best passage instead, set includeMatchSnippet and read matchSnippet.
- Common words are transcribed the same way almost every time, so lists of misspellings rarely find more. Brand and drug names are where sound-alike spellings are worth adding.
Short forms are noisy on their own. When we tested "PPD" and "PND", they also matched police departments, a Chilean political party, a hair dye ingredient and slang.
Keep the acronyms, but require context. Put them in one group and context words in a second group. Groups combine with AND, so an episode has to say one acronym and at least one context word, such as "pregnant" or "newborn".
const acronymQuery = {
...phraseQuery,
searchTermGroups: [
{ searchTerms: inTranscript(['PPD', 'PND']) },
{
searchTerms: inTranscript([
'postpartum',
'postnatal',
'perinatal',
'pregnancy',
'pregnant',
'newborn',
'giving birth',
'new mom',
'new mum',
]),
},
],
};
const acronymPage = await pe.search.searchEpisodes(acronymQuery);
console.log(`Episodes matching an acronym in context: ${acronymPage.result.total.value}`); const acronymQuery = {
...phraseQuery,
searchTermGroups: [
{ searchTerms: inTranscript(['PPD', 'PND']) },
{
searchTerms: inTranscript([
'postpartum',
'postnatal',
'perinatal',
'pregnancy',
'pregnant',
'newborn',
'giving birth',
'new mom',
'new mum',
]),
},
],
};
const acronymPage = await request('/api/v1/search/episodes', acronymQuery);
console.log(`Episodes matching an acronym in context: ${acronymPage.result.total.value}`); acronym_query = {
**phrase_query,
"search_term_groups": [
{"searchTerms": in_transcript(["PPD", "PND"])},
{
"searchTerms": in_transcript(
[
"postpartum",
"postnatal",
"perinatal",
"pregnancy",
"pregnant",
"newborn",
"giving birth",
"new mom",
"new mum",
]
)
},
],
}
acronym_page = pe.search.search_episodes(**acronym_query)
print("Episodes matching an acronym in context:", acronym_page.result.total.value) acronym_query = {
**phrase_query,
"searchTermGroups": [
{"searchTerms": in_transcript(["PPD", "PND"])},
{
"searchTerms": in_transcript(
[
"postpartum",
"postnatal",
"perinatal",
"pregnancy",
"pregnant",
"newborn",
"giving birth",
"new mom",
"new mum",
]
)
},
],
}
acronym_page = request("/api/v1/search/episodes", acronym_query)
print("Episodes matching an acronym in context:", acronym_page["result"]["total"]["value"]) - A searchTerms entry with matchMode 'not' removes the whole episode, not one mention. If an ad says the acronym on hundreds of shows, excluding the ad's words also drops episodes where the host talks about the topic. Label those matches for review instead of excluding them.
- The context word can appear anywhere in the transcript, not only next to the acronym. Read the passages before you count an acronym-only match.
- Passages highlight the context words as well as the acronyms, so some passages show only a context word.
Run both searches for the day. Pass each response's cursor into the next request, and stop when a page has no hits or no new cursor.
A one-day window keeps each pull small, and you can rerun any day you missed. Schedule the program once a day, after midnight UTC.
const maxPerQuery = 5000;
const mentions = new Map();
for (const [label, query] of [
['phrase', phraseQuery],
['acronym', acronymQuery],
]) {
let cursor = null;
let fetched = 0;
do {
const page = await pe.search.searchEpisodes({ ...query, cursor });
for (const hit of page.result.hits) {
const mention = mentions.get(hit.episodeId) ?? { hit, matchedBy: [], passages: [] };
if (!mention.matchedBy.includes(label)) mention.matchedBy.push(label);
for (const passage of hit.transcriptHighlights ?? []) {
if (!mention.passages.includes(passage)) mention.passages.push(passage);
}
mentions.set(hit.episodeId, mention);
}
fetched += page.result.hits.length;
const nextCursor = page.cursor;
if (!page.result.hits.length || !nextCursor || nextCursor === cursor) break;
cursor = nextCursor;
} while (fetched < maxPerQuery);
if (fetched >= maxPerQuery) console.warn(`The ${label} search reached ${maxPerQuery} results; use a shorter window`);
}
console.log(`Episodes mentioning the topic on ${dayStart.toISOString().slice(0, 10)}: ${mentions.size}`); const maxPerQuery = 5000;
const mentions = new Map();
for (const [label, query] of [
['phrase', phraseQuery],
['acronym', acronymQuery],
]) {
let cursor = null;
let fetched = 0;
do {
const page = await request('/api/v1/search/episodes', { ...query, cursor });
for (const hit of page.result.hits) {
const mention = mentions.get(hit.episodeId) ?? { hit, matchedBy: [], passages: [] };
if (!mention.matchedBy.includes(label)) mention.matchedBy.push(label);
for (const passage of hit.transcriptHighlights ?? []) {
if (!mention.passages.includes(passage)) mention.passages.push(passage);
}
mentions.set(hit.episodeId, mention);
}
fetched += page.result.hits.length;
const nextCursor = page.cursor;
if (!page.result.hits.length || !nextCursor || nextCursor === cursor) break;
cursor = nextCursor;
} while (fetched < maxPerQuery);
if (fetched >= maxPerQuery) console.warn(`The ${label} search reached ${maxPerQuery} results; use a shorter window`);
}
console.log(`Episodes mentioning the topic on ${dayStart.toISOString().slice(0, 10)}: ${mentions.size}`); max_per_query = 5000
mentions = {}
for label, query in [("phrase", phrase_query), ("acronym", acronym_query)]:
cursor = None
fetched = 0
while fetched < max_per_query:
page = pe.search.search_episodes(**query, cursor=cursor)
# API field names, so the rows below match the HTTP examples.
hits = [hit.model_dump(by_alias=True, mode="json") for hit in page.result.hits]
for hit in hits:
mention = mentions.setdefault(hit["episodeId"], {"hit": hit, "matchedBy": [], "passages": []})
if label not in mention["matchedBy"]:
mention["matchedBy"].append(label)
for passage in hit.get("transcriptHighlights") or []:
if passage not in mention["passages"]:
mention["passages"].append(passage)
fetched += len(hits)
next_cursor = page.cursor
if not hits or not next_cursor or next_cursor == cursor:
break
cursor = next_cursor
if fetched >= max_per_query:
print(f"The {label} search reached {max_per_query} results; use a shorter window", file=sys.stderr)
print(f"Episodes mentioning the topic on {day_start.date()}: {len(mentions)}") max_per_query = 5000
mentions = {}
for label, query in [("phrase", phrase_query), ("acronym", acronym_query)]:
cursor = None
fetched = 0
while fetched < max_per_query:
page = request("/api/v1/search/episodes", {**query, "cursor": cursor})
hits = page["result"]["hits"]
for hit in hits:
mention = mentions.setdefault(hit["episodeId"], {"hit": hit, "matchedBy": [], "passages": []})
if label not in mention["matchedBy"]:
mention["matchedBy"].append(label)
for passage in hit.get("transcriptHighlights") or []:
if passage not in mention["passages"]:
mention["passages"].append(passage)
fetched += len(hits)
next_cursor = page["cursor"]
if not hits or not next_cursor or next_cursor == cursor:
break
cursor = next_cursor
if fetched >= max_per_query:
print(f"The {label} search reached {max_per_query} results; use a shorter window", file=sys.stderr)
print(f"Episodes mentioning the topic on {day_start.date()}: {len(mentions)}") - pageSize accepts 1–1000. These examples use 100 results per request.
- Transcripts are added after an episode is published, so a day's count can grow. Rerun the previous few days too; merging by episodeId keeps reruns safe.
- Each search stops at 5,000 results as a safety limit and prints a warning. If a busy topic reaches it, pull shorter windows.
- SDK requests retry transient failures automatically. The HTTP examples stop on errors; add bounded retries with backoff for scheduled jobs.
An episode can match both searches. Keep one row per episodeId, record which searches found it in matchedBy, and keep each distinct passage. Rows found only by the acronym search are the ones to review.
Each row keeps the links you need to check a mention: the Pod Engine episode page built from podcastSlug and episodeSlug, the audio file from episodeEnclosureUrl, and the show on Apple Podcasts and Spotify from appleId and spotifyId. Write the rows to your database or a daily file.
const rows = [...mentions.values()].map(({ hit, matchedBy, passages }) => ({
episodeId: hit.episodeId,
published: hit.episodeRssPublishedAt,
podcast: hit.podcastRssTitle,
episode: hit.episodeRssTitle,
matchedBy: matchedBy.join('+'),
passages: passages.map((passage) => passage.replace(/<\/?em>/g, '')),
estimatedMonthlyListeners: hit.podcastAudienceEstimatedMonthlyListeners ?? null,
podEngineUrl: `https://www.podengine.ai/podcasts/${hit.podcastSlug}/${hit.episodeSlug}`,
audioUrl: hit.episodeEnclosureUrl,
applePodcastsUrl: hit.appleId ? `https://podcasts.apple.com/podcast/id${hit.appleId}` : null,
spotifyUrl: hit.spotifyId ? `https://open.spotify.com/show/${hit.spotifyId}` : null,
}));
console.log(JSON.stringify(rows, null, 2)); const rows = [...mentions.values()].map(({ hit, matchedBy, passages }) => ({
episodeId: hit.episodeId,
published: hit.episodeRssPublishedAt,
podcast: hit.podcastRssTitle,
episode: hit.episodeRssTitle,
matchedBy: matchedBy.join('+'),
passages: passages.map((passage) => passage.replace(/<\/?em>/g, '')),
estimatedMonthlyListeners: hit.podcastAudienceEstimatedMonthlyListeners ?? null,
podEngineUrl: `https://www.podengine.ai/podcasts/${hit.podcastSlug}/${hit.episodeSlug}`,
audioUrl: hit.episodeEnclosureUrl,
applePodcastsUrl: hit.appleId ? `https://podcasts.apple.com/podcast/id${hit.appleId}` : null,
spotifyUrl: hit.spotifyId ? `https://open.spotify.com/show/${hit.spotifyId}` : null,
}));
console.log(JSON.stringify(rows, null, 2)); rows = []
for mention in mentions.values():
hit = mention["hit"]
rows.append(
{
"episodeId": hit["episodeId"],
"published": hit["episodeRssPublishedAt"],
"podcast": hit["podcastRssTitle"],
"episode": hit["episodeRssTitle"],
"matchedBy": "+".join(mention["matchedBy"]),
"passages": [re.sub(r"</?em>", "", passage) for passage in mention["passages"]],
"estimatedMonthlyListeners": hit.get("podcastAudienceEstimatedMonthlyListeners"),
"podEngineUrl": f"https://www.podengine.ai/podcasts/{hit['podcastSlug']}/{hit['episodeSlug']}",
"audioUrl": hit["episodeEnclosureUrl"],
"applePodcastsUrl": f"https://podcasts.apple.com/podcast/id{hit['appleId']}" if hit.get("appleId") else None,
"spotifyUrl": f"https://open.spotify.com/show/{hit['spotifyId']}" if hit.get("spotifyId") else None,
}
)
print(json.dumps(rows, indent=2)) rows = []
for mention in mentions.values():
hit = mention["hit"]
rows.append(
{
"episodeId": hit["episodeId"],
"published": hit["episodeRssPublishedAt"],
"podcast": hit["podcastRssTitle"],
"episode": hit["episodeRssTitle"],
"matchedBy": "+".join(mention["matchedBy"]),
"passages": [re.sub(r"</?em>", "", passage) for passage in mention["passages"]],
"estimatedMonthlyListeners": hit.get("podcastAudienceEstimatedMonthlyListeners"),
"podEngineUrl": f"https://www.podengine.ai/podcasts/{hit['podcastSlug']}/{hit['episodeSlug']}",
"audioUrl": hit["episodeEnclosureUrl"],
"applePodcastsUrl": f"https://podcasts.apple.com/podcast/id{hit['appleId']}" if hit.get("appleId") else None,
"spotifyUrl": f"https://open.spotify.com/show/{hit['spotifyId']}" if hit.get("spotifyId") else None,
}
)
print(json.dumps(rows, indent=2)) - appleId and spotifyId identify the show, not the episode.
- podcastAudienceEstimatedMonthlyListeners is an estimate for the whole show. It is null when we have no estimate.
- The Python SDK returns typed models. The example converts each hit with model_dump(by_alias=True, mode="json") so the rows use the same field names as the HTTP API.