Compare commits

...

6 Commits

Author SHA1 Message Date
d23888c68d Add last_posted date to /api/channel-list from Elasticsearch
Some checks failed
docker-build / build (push) Has been cancelled
Queries the latest video date per channel and includes it in the
channel-list JSON response.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:14:53 -04:00
c019730666 Fix remaining placeholder channel names
Some checks failed
docker-build / build (push) Has been cancelled
- UCCebR16tXbv5Ykk9_WtCCug -> Christian T. Golden
- UC4YwC5zA9S_2EwthE27Xlew -> CMA

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:04:50 -04:00
bb2850ef98 Add /channels HTML page and fix placeholder channel names
Some checks failed
docker-build / build (push) Has been cancelled
- Add /channels route serving a simple HTML page with channel names
  linked to their YouTube pages
- Fix names for UCehAungJpAeC (Wholly Unfocused) and UCiJmdXTb76i
  (Bridges of Meaning Hub) from Elasticsearch data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:01:45 -04:00
7fdb31bf18 Add 3 missing channels from jet-alone to channels.yml source of truth
Some checks failed
docker-build / build (push) Has been cancelled
Syncs channels.yml (canonical) and urls.txt with channels that existed
only on jet-alone: LeviathanForPlay, UCehAungJpAeC-F3R5FwvvCQ,
UC4YwC5zA9S_2EwthE27Xlew.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 11:39:06 -04:00
Ubuntu
090f5943c3 Add notes page
Some checks failed
docker-build / build (push) Has been cancelled
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 20:40:53 +00:00
d168287636 Add Rigel Windsong Thurston
Some checks failed
docker-build / build (push) Has been cancelled
2026-01-10 13:36:10 -05:00
5 changed files with 164 additions and 3 deletions

View File

@@ -3,7 +3,7 @@
channels:
- id: UCCebR16tXbv5Ykk9_WtCCug
name: Channel UCCebR16tXbv
name: Christian T. Golden
url: https://www.youtube.com/channel/UCCebR16tXbv5Ykk9_WtCCug/videos
- id: UC6vg0HkKKlgsWk-3HfV-vnw
name: A Quality Existence
@@ -81,7 +81,7 @@ channels:
name: TheCommonToad
url: https://www.youtube.com/channel/UC-QiBn6GsM3JZJAeAQpaGAA/videos
- id: UCiJmdXTb76i8eIPXdJyf8ZQ
name: Channel UCiJmdXTb76i
name: Bridges of Meaning Hub
url: https://www.youtube.com/channel/UCiJmdXTb76i8eIPXdJyf8ZQ/videos
- id: UCM9Z05vuQhMEwsV03u6DrLA
name: Cassidy van der Kamp
@@ -244,6 +244,10 @@ channels:
handle: theplebistocrat
name: the plebistocrat
url: https://www.youtube.com/@theplebistocrat/videos
- id: UCWehDXDEdUpB58P7-Bg1cHg
handle: rigelwindsongthurston
name: Rigel Windsong Thurston
url: https://www.youtube.com/@rigelwindsongthurston/videos
- id: UCZA5mUAyYcCL1kYgxbeMNrA
handle: RightInChrist
name: Rightinchrist
@@ -256,3 +260,12 @@ channels:
handle: WavesOfObsession
name: Wavesofobsession
url: https://www.youtube.com/@WavesOfObsession/videos
- handle: LeviathanForPlay
name: LeviathanForPlay
url: https://www.youtube.com/@LeviathanForPlay/videos
- id: UCehAungJpAeC-F3R5FwvvCQ
name: Wholly Unfocused
url: https://www.youtube.com/channel/UCehAungJpAeC-F3R5FwvvCQ/videos
- id: UC4YwC5zA9S_2EwthE27Xlew
name: CMA
url: https://www.youtube.com/channel/UC4YwC5zA9S_2EwthE27Xlew/videos

View File

@@ -123,6 +123,8 @@ feeds:
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCFQ6Gptuq-sLflbJ4YY3Umw&format=Mrss
- name: Restoring Meaning
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCzX6R3ZLQh5Zma_5AsPcqPA&format=Mrss
- name: Rigel Windsong Thurston
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCWehDXDEdUpB58P7-Bg1cHg&format=Mrss
- name: Rightinchrist
url: http://rss-bridge/?action=display&bridge=YoutubeBridge&context=By+channel+id&c=UCZA5mUAyYcCL1kYgxbeMNrA&format=Mrss
- name: Ron Copperman

View File

@@ -1150,6 +1150,10 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
def graph_page():
return send_from_directory(app.static_folder, "graph.html")
@app.route("/notes")
def notes_page():
return send_from_directory(app.static_folder, "notes.html")
@app.route("/static/<path:filename>")
def static_files(filename: str):
return send_from_directory(app.static_folder, filename)
@@ -1273,6 +1277,29 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
data.sort(key=lambda item: item["Name"].lower())
return jsonify(data)
def _channel_latest_dates() -> Dict[str, Optional[str]]:
"""Return {channel_id: latest_date_str} from Elasticsearch."""
try:
resp = client.search(index=index, body={
"size": 0,
"aggs": {
"by_channel": {
"terms": {"field": "channel_id.keyword", "size": 500},
"aggs": {"latest": {"max": {"field": "date"}}},
}
},
}, request_timeout=15)
except Exception as exc:
LOGGER.warning("Failed to fetch latest dates: %s", exc)
return {}
result: Dict[str, Optional[str]] = {}
for bucket in resp.get("aggregations", {}).get("by_channel", {}).get("buckets", []):
cid = bucket.get("key")
val = bucket.get("latest", {}).get("value_as_string")
if cid and val:
result[cid] = val[:10] if len(val) > 10 else val
return result
@app.route("/api/channel-list")
def channel_list():
payload = {
@@ -1281,13 +1308,20 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
"source": str(config.channels_path),
}
try:
payload["channels"] = load_channel_entries(config.channels_path)
entries = load_channel_entries(config.channels_path)
except FileNotFoundError:
LOGGER.warning("Channel list not found: %s", config.channels_path)
payload["error"] = "channels_not_found"
return jsonify(payload)
except Exception as exc:
LOGGER.exception("Failed to load channel list: %s", exc)
payload["error"] = "channels_load_failed"
return jsonify(payload)
latest_dates = _channel_latest_dates()
for entry in entries:
entry["last_posted"] = latest_dates.get(entry.get("id")) or None
payload["channels"] = entries
return jsonify(payload)
@app.route("/channels.txt")
@@ -1305,6 +1339,53 @@ def create_app(config: AppConfig = CONFIG) -> Flask:
body = "\n".join(urls) + ("\n" if urls else "")
return (body, 200, {"Content-Type": "text/plain; charset=utf-8"})
@app.route("/channels")
def channels_page():
try:
entries = load_channel_entries(config.channels_path)
except FileNotFoundError:
return "Channel list not found.", 404
except Exception:
return "Failed to load channel list.", 500
rows = ""
for ch in entries:
name = ch.get("name") or ch.get("handle") or ch.get("id") or "Unknown"
url = ch.get("url", "")
# Link to the channel page, not the /videos tab
channel_url = url.replace("/videos", "") if url.endswith("/videos") else url
name_html = (
f'<a href="{channel_url}" target="_blank" rel="noopener">{name}</a>'
if channel_url
else name
)
rows += f"<tr><td>{name_html}</td></tr>\n"
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Channels - This Little Corner</title>
<style>
body {{ font-family: system-ui, -apple-system, sans-serif; margin: 2rem auto; max-width: 640px; color: #222; }}
h1 {{ font-size: 1.4rem; margin-bottom: 0.25rem; }}
p.sub {{ color: #666; margin-top: 0; font-size: 0.9rem; }}
table {{ width: 100%; border-collapse: collapse; }}
td {{ padding: 0.45rem 0; border-bottom: 1px solid #eee; }}
a {{ color: #1a6fb5; text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
</style>
</head>
<body>
<h1>Channels</h1>
<p class="sub">{len(entries)} channels tracked</p>
<table>
{rows}</table>
</body>
</html>"""
return (html, 200, {"Content-Type": "text/html; charset=utf-8"})
def _rss_target(feed_name: str) -> str:
name = (feed_name or "").strip("/")
if not name:

61
static/notes.html Normal file
View File

@@ -0,0 +1,61 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Notes</title>
<link rel="icon" href="/static/favicon.png" type="image/png" />
<link rel="stylesheet" href="https://unpkg.com/xp.css" integrity="sha384-isKk8ZXKlU28/m3uIrnyTfuPaamQIF4ONLeGSfsWGEe3qBvaeLU5wkS4J7cTIwxI" crossorigin="anonymous" />
<link rel="stylesheet" href="/static/style.css" />
<style>
.notes-content {
line-height: 1.6;
}
.notes-content h2 {
margin-top: 1.5em;
margin-bottom: 0.5em;
border-bottom: 1px solid #ccc;
padding-bottom: 0.25em;
}
.notes-content h2:first-child {
margin-top: 0;
}
.notes-content p {
margin: 0.75em 0;
}
.notes-content ul, .notes-content ol {
margin: 0.75em 0;
padding-left: 1.5em;
}
.notes-content li {
margin: 0.25em 0;
}
</style>
</head>
<body>
<div class="window" style="max-width: 800px; margin: 20px auto;">
<div class="title-bar">
<div class="title-bar-text">Notes</div>
<div class="title-bar-controls">
<button aria-label="Minimize"></button>
<button aria-label="Maximize"></button>
<button aria-label="Close"></button>
</div>
</div>
<div class="window-body">
<p style="margin-bottom: 16px;"><a href="/">← Back to search</a></p>
<div class="notes-content">
<h2>Welcome</h2>
<p>This is a space for thoughts, observations, and notes related to this project and beyond.</p>
<!-- Add your notes below -->
</div>
</div>
<div class="status-bar">
<p class="status-bar-field">Last updated: January 2026</p>
</div>
</div>
</body>
</html>

View File

@@ -69,6 +69,10 @@ https://www.youtube.com/@davidbusuttil9086/videos
https://www.youtube.com/@matthewparlato5626/videos
https://www.youtube.com/@lancecleaver227/videos
https://www.youtube.com/@theplebistocrat/videos
https://www.youtube.com/@rigelwindsongthurston/videos
https://www.youtube.com/@RightInChrist/videos
https://www.youtube.com/@RafeKelley/videos
https://www.youtube.com/@WavesOfObsession/videos
https://www.youtube.com/@LeviathanForPlay/videos
https://www.youtube.com/channel/UCehAungJpAeC-F3R5FwvvCQ/videos
https://www.youtube.com/channel/UC4YwC5zA9S_2EwthE27Xlew/videos