Skip to content

Commit 8dff3b6

Browse files
authored
Merge pull request #15 from HanClinto/copilot/clean-up-hacker-news-youtube-integration
HN/YouTube sidebar cleanup + auto-discover integrations from GitHub profile social links
2 parents 285424c + 6fc7658 commit 8dff3b6

9 files changed

Lines changed: 831 additions & 44 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ _site/
66
# Personal ingestor configuration — keep these local, never commit real values.
77
# Copy the corresponding .example file and fill in your own IDs/usernames.
88
config/youtube_playlists.txt
9+
config/youtube_channels.txt
910
config/hackernews.txt

blog/generate.py

Lines changed: 100 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from jinja2 import Environment, FileSystemLoader # noqa: E402
4444
import urllib.parse # noqa: E402
4545

46-
from blog.ingestors import github_issues, hackernews, youtube # noqa: E402
46+
from blog.ingestors import github_issues, github_profile, hackernews, youtube # noqa: E402
4747

4848
# ---------------------------------------------------------------------------
4949
# Paths
@@ -102,41 +102,84 @@ def generate_site(
102102
token: str | None,
103103
output_dir: Path,
104104
youtube_playlist_ids: str | None = None,
105+
youtube_channel_ids: str | None = None,
105106
hn_usernames: list[str] | None = None,
106107
) -> None:
107108
_start = time.monotonic()
108109

110+
repo_owner = repo.split("/")[0]
109111
repo_name = repo.split("/")[-1]
110112
repo_url = f"https://github.com/{repo}"
111113

114+
# Build GitHub API request headers
115+
gh_headers: dict[str, str] = {"Accept": "application/vnd.github+json"}
116+
if token:
117+
gh_headers["Authorization"] = f"Bearer {token}"
118+
112119
# Load config files early so we can pass them to the config page
113120
hidden_labels = github_issues._load_hidden_labels(CONFIG_DIR)
114121
blocked_users = github_issues._load_blocked_users(CONFIG_DIR)
115122

123+
# --- GitHub owner profile & social links ---
124+
print(f"Fetching GitHub profile for: {repo_owner}…")
125+
owner_profile = github_profile.fetch_owner_profile(repo_owner, gh_headers)
126+
if owner_profile:
127+
print(f" Profile: {owner_profile.name or owner_profile.login}")
128+
print(f" Social links: {len(owner_profile.social_links)} found.")
129+
else:
130+
print(" Could not fetch GitHub profile — social link auto-discovery disabled.")
131+
116132
# --- GitHub Issues (My Writing) — always runs ---
117133
print("Fetching GitHub Issues (My Writing)…")
118134
writing_posts = github_issues.ingest(repo, token, CONFIG_DIR)
119135
print(f" {len(writing_posts)} post(s) ingested from GitHub Issues.")
120136

121-
# --- YouTube playlists (My Watching) — uses free public RSS feeds, no API key ---
137+
# --- YouTube playlists & channels (My Watching) — uses free public RSS feeds, no API key ---
122138
watching_posts: list[dict] = []
123139
playlist_ids = youtube.load_playlist_ids(CONFIG_DIR, youtube_playlist_ids)
124-
if playlist_ids:
125-
print("Fetching YouTube playlists (My Watching)…")
126-
watching_posts = youtube.ingest(CONFIG_DIR, youtube_playlist_ids)
140+
channel_ids = youtube.load_channel_ids(CONFIG_DIR, youtube_channel_ids)
141+
142+
# Auto-discover YouTube channel from GitHub social links when not explicitly configured
143+
profile_youtube_handles: list[str] = []
144+
if owner_profile:
145+
profile_youtube_handles = github_profile.extract_youtube_handles(owner_profile.social_links)
146+
auto_discovered_channels = (
147+
profile_youtube_handles
148+
if (not channel_ids and profile_youtube_handles)
149+
else []
150+
)
151+
effective_channel_ids_str = youtube_channel_ids
152+
if auto_discovered_channels and not channel_ids:
153+
print(f" Auto-discovered YouTube channel(s) from GitHub profile: {auto_discovered_channels}")
154+
effective_channel_ids_str = ",".join(auto_discovered_channels)
155+
channel_ids = youtube.load_channel_ids(CONFIG_DIR, effective_channel_ids_str)
156+
157+
if playlist_ids or channel_ids:
158+
print("Fetching YouTube content (My Watching)…")
159+
watching_posts = youtube.ingest(CONFIG_DIR, youtube_playlist_ids, effective_channel_ids_str)
127160
print(f" {len(watching_posts)} post(s) ingested from YouTube.")
128161
else:
129-
print("YOUTUBE_PLAYLIST_IDS not configured — skipping YouTube ingestor.")
162+
print("YOUTUBE_PLAYLIST_IDS / YOUTUBE_CHANNEL_IDS not configured and none found in GitHub profile — skipping YouTube ingestor.")
130163

131-
# --- Hacker News (My Reading) — requires HN_USERNAME ---
164+
# --- Hacker News (My Reading) — HN_USERNAME env var, or auto-discovered from GitHub profile ---
132165
reading_posts: list[dict] = []
133-
if hn_usernames:
134-
names_str = ", ".join(hn_usernames)
166+
auto_discovered_hn_username: str | None = None
167+
effective_hn_usernames = hn_usernames # start with whatever was explicitly configured
168+
169+
if not effective_hn_usernames and owner_profile:
170+
discovered = github_profile.extract_hn_username(owner_profile.social_links)
171+
if discovered:
172+
auto_discovered_hn_username = discovered
173+
effective_hn_usernames = [discovered]
174+
print(f" Auto-discovered HN username from GitHub profile: {discovered}")
175+
176+
if effective_hn_usernames:
177+
names_str = ", ".join(effective_hn_usernames)
135178
print(f"Fetching Hacker News (My Reading) for: {names_str}…")
136-
reading_posts = hackernews.ingest(hn_usernames)
179+
reading_posts = hackernews.ingest(effective_hn_usernames)
137180
print(f" {len(reading_posts)} post(s) ingested from Hacker News.")
138181
else:
139-
print("HN_USERNAME not configured — skipping Hacker News ingestor.")
182+
print("HN_USERNAME not configured and none found in GitHub profile — skipping Hacker News ingestor.")
140183

141184
# Build active sections (skip sections that produced no posts)
142185
section_posts = {
@@ -157,6 +200,44 @@ def generate_site(
157200
reverse=True,
158201
)
159202

203+
# --- Sidebar data ---
204+
# Split HN posts into stories vs. comments for separate sidebar panels
205+
_SIDEBAR_LIMIT = 5
206+
hn_stories = [p for p in reading_posts if p.get("metadata", {}).get("hn_type") == "story"]
207+
hn_comments = [p for p in reading_posts if p.get("metadata", {}).get("hn_type") == "comment"]
208+
# Build per-username HN profile links (use first effective username if multiple)
209+
_hn_user = (effective_hn_usernames or [None])[0]
210+
hn_submitted_url = (
211+
f"https://news.ycombinator.com/submitted?id={_hn_user}" if _hn_user else None
212+
)
213+
hn_threads_url = (
214+
f"https://news.ycombinator.com/threads?id={_hn_user}" if _hn_user else None
215+
)
216+
hn_profile_url = (
217+
f"https://news.ycombinator.com/user?id={_hn_user}" if _hn_user else None
218+
)
219+
220+
# Collect unique YouTube "view more" URLs (one per playlist/channel)
221+
seen_view_more: set[str] = set()
222+
youtube_view_more_urls: list[dict] = []
223+
for p in watching_posts:
224+
vmu = p.get("metadata", {}).get("view_more_url")
225+
stype = p.get("metadata", {}).get("source_type", "playlist")
226+
if vmu and vmu not in seen_view_more:
227+
seen_view_more.add(vmu)
228+
youtube_view_more_urls.append({"url": vmu, "source_type": stype})
229+
230+
sidebar = {
231+
"hn_stories": hn_stories[:_SIDEBAR_LIMIT],
232+
"hn_comments": hn_comments[:_SIDEBAR_LIMIT],
233+
"hn_submitted_url": hn_submitted_url,
234+
"hn_threads_url": hn_threads_url,
235+
"hn_profile_url": hn_profile_url,
236+
"hn_username": _hn_user,
237+
"watching": watching_posts[:_SIDEBAR_LIMIT],
238+
"youtube_view_more_urls": youtube_view_more_urls,
239+
}
240+
160241
# --- Jinja2 setup ---
161242
env = Environment(
162243
loader=FileSystemLoader(str(TEMPLATES_DIR)),
@@ -204,7 +285,7 @@ def generate_site(
204285

205286
# Render index page
206287
index_tmpl = env.get_template("index.html")
207-
index_html = index_tmpl.render(sections=active_sections)
288+
index_html = index_tmpl.render(sections=active_sections, sidebar=sidebar)
208289
(output_dir / "index.html").write_text(index_html, encoding="utf-8")
209290
print("Wrote index.html")
210291

@@ -233,8 +314,12 @@ def generate_site(
233314

234315
# Render config page
235316
config_ctx = {
236-
"hn_usernames": hn_usernames or [],
317+
"hn_usernames": effective_hn_usernames or [],
318+
"auto_discovered_hn_username": auto_discovered_hn_username,
237319
"playlist_ids": playlist_ids,
320+
"channel_ids": channel_ids,
321+
"auto_discovered_channels": auto_discovered_channels,
322+
"owner_profile": owner_profile,
238323
"hidden_labels": sorted(hidden_labels),
239324
"blocked_user_count": len(blocked_users),
240325
"writing_post_count": len(writing_posts),
@@ -270,6 +355,7 @@ def main() -> None:
270355
output_dir = Path(os.environ.get("OUTPUT_DIR", "_site")).resolve()
271356

272357
youtube_playlist_ids = os.environ.get("YOUTUBE_PLAYLIST_IDS") or None
358+
youtube_channel_ids = os.environ.get("YOUTUBE_CHANNEL_IDS") or None
273359

274360
# HN usernames: from HN_USERNAME env var and/or local config file (gitignored)
275361
hn_usernames = hackernews.load_usernames(CONFIG_DIR, os.environ.get("HN_USERNAME") or None)
@@ -279,6 +365,7 @@ def main() -> None:
279365
token=token,
280366
output_dir=output_dir,
281367
youtube_playlist_ids=youtube_playlist_ids,
368+
youtube_channel_ids=youtube_channel_ids,
282369
hn_usernames=hn_usernames or None,
283370
)
284371

0 commit comments

Comments
 (0)