Compare commits
6 Commits
6534db6f64
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d23888c68d | |||
| c019730666 | |||
| bb2850ef98 | |||
| 7fdb31bf18 | |||
|
|
090f5943c3 | ||
| d168287636 |
17
channels.yml
17
channels.yml
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
61
static/notes.html
Normal 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>
|
||||
4
urls.txt
4
urls.txt
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user