4343from jinja2 import Environment , FileSystemLoader # noqa: E402
4444import 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