Add support for channels

This commit is contained in:
Tim Van Baak 2023-06-02 18:30:06 -07:00
parent 70ee60bb65
commit ad6da14387
5 changed files with 133 additions and 22 deletions

View File

@ -1,5 +1,6 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import List
import json import json
import os import os
import time import time
@ -41,29 +42,62 @@ def root():
Navigation home page. Navigation home page.
""" """
data_path = intake_data_dir() data_path = intake_data_dir()
sources = [] sources = []
for child in data_path.iterdir(): for child in data_path.iterdir():
if (child / "intake.json").exists(): if (child / "intake.json").exists():
sources.append(LocalSource(data_path, child.name)) sources.append(LocalSource(data_path, child.name))
sources.sort(key=lambda s: s.source_name) sources.sort(key=lambda s: s.source_name)
channels = {}
channels_config_path = data_path / "channels.json"
if channels_config_path.exists():
channels = json.loads(channels_config_path.read_text(encoding="utf8"))
return render_template( return render_template(
"home.jinja2", "home.jinja2",
sources=sources, sources=sources,
channels=channels,
) )
@app.get("/source/<string:source_name>") @app.get("/source/<string:name>")
def source_feed(source_name): def source_feed(name):
""" """
Feed view for a single source. Feed view for a single source.
""" """
source = LocalSource(intake_data_dir(), source_name) source = LocalSource(intake_data_dir(), name)
if not source.source_path.exists(): if not source.source_path.exists():
abort(404) abort(404)
return _sources_feed(name, [source])
@app.get("/channel/<string:name>")
def channel_feed(name):
"""
Feed view for a channel.
"""
channels_config_path = intake_data_dir() / "channels.json"
if not channels_config_path.exists():
abort(404)
channels = json.loads(channels_config_path.read_text(encoding="utf8"))
if name not in channels:
abort(404)
sources = [LocalSource(intake_data_dir(), name) for name in channels[name]]
return _sources_feed(name, sources)
def _sources_feed(name: str, sources: List[LocalSource]):
"""
Feed view for multiple sources.
"""
# Get all items # Get all items
all_items = sorted(source.get_all_items(), key=item_sort_key) all_items = sorted(
[item for source in sources for item in source.get_all_items()],
key=item_sort_key,
)
# Apply paging parameters # Apply paging parameters
count = int(request.args.get("count", "100")) count = int(request.args.get("count", "100"))
@ -73,14 +107,14 @@ def source_feed(source_name):
None None
if page <= 0 if page <= 0
else url_for( else url_for(
request.endpoint, source_name=source_name, count=count, page=page - 1 request.endpoint, name=name, count=count, page=page - 1
) )
) )
pager_next = ( pager_next = (
None None
if (count * page + count) > len(all_items) if (count * page + count) > len(all_items)
else url_for( else url_for(
request.endpoint, source_name=source_name, count=count, page=page + 1 request.endpoint, name=name, count=count, page=page + 1
) )
) )
@ -147,12 +181,12 @@ def action(source_name, item_id, action):
return jsonify(item) return jsonify(item)
@app.route("/edit/source/<string:source_name>", methods=["GET", "POST"]) @app.route("/edit/source/<string:name>", methods=["GET", "POST"])
def source_edit(source_name): def source_edit(name):
""" """
Config editor for a source Config editor for a source
""" """
source = LocalSource(intake_data_dir(), source_name) source = LocalSource(intake_data_dir(), name)
if not source.source_path.exists(): if not source.source_path.exists():
abort(404) abort(404)
@ -160,10 +194,7 @@ def source_edit(source_name):
error_message: str = None error_message: str = None
if request.method == "POST": if request.method == "POST":
config_str = request.form.get("config", "") config_str = request.form.get("config", "")
error_message, config = try_parse_config(config_str) error_message, config = _parse_source_config(config_str)
print(config_str)
print(error_message)
print(config)
if not error_message: if not error_message:
source.save_config(config) source.save_config(config)
return redirect(url_for("root")) return redirect(url_for("root"))
@ -175,13 +206,13 @@ def source_edit(source_name):
return render_template( return render_template(
"edit.jinja2", "edit.jinja2",
source=source, subtitle=source.source_name,
config=config_str, config=config_str,
error_message=error_message, error_message=error_message,
) )
def try_parse_config(config_str: str): def _parse_source_config(config_str: str):
if not config_str: if not config_str:
return ("Config required", {}) return ("Config required", {})
try: try:
@ -198,14 +229,62 @@ def try_parse_config(config_str: str):
fetch = action["fetch"] fetch = action["fetch"]
if "exe" not in fetch: if "exe" not in fetch:
return ("No fetch exe", {}) return ("No fetch exe", {})
config = { config = {"action": parsed["action"]}
"action": parsed["action"]
}
if "env" in parsed: if "env" in parsed:
config["env"] = parsed["env"] config["env"] = parsed["env"]
return (None, config) return (None, config)
@app.route("/edit/channels", methods=["GET", "POST"])
def channels_edit():
"""
Config editor for channels
"""
config_path = intake_data_dir() / "channels.json"
# For POST, check if the config is valid
error_message: str = None
if request.method == "POST":
config_str = request.form.get("config", "")
error_message, config = _parse_channels_config(config_str)
if not error_message:
config_path.write_text(json.dumps(config, indent=2), encoding="utf8")
return redirect(url_for("root"))
# For GET, load the config
if request.method == "GET":
if config_path.exists():
config = json.loads(config_path.read_text(encoding="utf8"))
else:
config = {}
config_str = json.dumps(config, indent=2)
return render_template(
"edit.jinja2",
subtitle="Channels",
config=config_str,
error_message=error_message,
)
def _parse_channels_config(config_str: str):
if not config_str:
return ("Config required", {})
try:
parsed = json.loads(config_str)
except json.JSONDecodeError:
return ("Invalid JSON", {})
if not isinstance(parsed, dict):
return ("Invalid config format", {})
for key in parsed:
if not isinstance(parsed[key], list):
return (f"{key} must map to a list", {})
for val in parsed[key]:
if not isinstance(val, str):
return f"{key} source {val} must be a string"
return (None, parsed)
def wsgi(): def wsgi():
# init_default_logging() # init_default_logging()
return app return app

View File

@ -1,7 +1,7 @@
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Intake - {{ source.source_name }}</title> <title>Intake - {{ subtitle }}</title>
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS41ZEdYUgAAAGFJREFUOE+lkFEKwDAIxXrzXXB3ckMm9EnAV/YRCxFCcUXEL3Jc77NDjpDA/VGL3RFWYEICfeGC8oQc9IPuCAnQDcoRVmBCAn3hgvKEHPSD7ggJ0A3KEVZgQgJ94YLSJ9YDUzNGDXGZ/JEAAAAASUVORK5CYII="> <link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMS41ZEdYUgAAAGFJREFUOE+lkFEKwDAIxXrzXXB3ckMm9EnAV/YRCxFCcUXEL3Jc77NDjpDA/VGL3RFWYEICfeGC8oQc9IPuCAnQDcoRVmBCAn3hgvKEHPSD7ggJ0A3KEVZgQgJ94YLSJ9YDUzNGDXGZ/JEAAAAASUVORK5CYII=">
<style> <style>
div#wrapper { div#wrapper {
@ -61,6 +61,9 @@ pre {
table.feed-control td { table.feed-control td {
font-family: monospace; padding: 5px 10px; font-family: monospace; padding: 5px 10px;
} }
span.error-message {
color: red;
}
</style> </style>
</head> </head>
<body> <body>
@ -68,9 +71,13 @@ table.feed-control td {
<div class="readable-item"> <div class="readable-item">
<form method="post"> <form method="post">
<label for="config" class="item-title">Source Editor</label> <label for="config" class="item-title">Config Editor</label>
<textarea autofocus id="config" name="config" rows=20>{{config}}</textarea> <textarea autofocus id="config" name="config" rows=20>{{config}}</textarea>
<p><input type="submit" value="Submit"></p> <p><input type="submit" value="Submit">
{% if error_message %}
<span class="error-message">{{ error_message }}</span>
{% endif %}
</p>
</form> </form>
</div> </div>

View File

@ -121,6 +121,9 @@ var doAction = function (source, itemid, action) {
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div class="readable-item center">
<span class="item-title"><a href="{{url_for('root')}}">Home</a></span>
</div>
{% if items %} {% if items %}
{% for item in items %} {% for item in items %}
<div class="readable-item {%- if not item.active %} strikethru fade{% endif %}{%- if item.active and item.tts and item.created + item.tts > now %} fade{% endif %}" id="{{item.source}}-{{item.id}}"> <div class="readable-item {%- if not item.active %} strikethru fade{% endif %}{%- if item.active and item.tts and item.created + item.tts > now %} fade{% endif %}" id="{{item.source}}-{{item.id}}">

View File

@ -33,6 +33,21 @@ summary:focus {
</head> </head>
<body> <body>
<div id="wrapper"> <div id="wrapper">
<div class="readable-item">
<details open>
<summary><span class="item-title">Channels</span></summary>
{% if not channels %}
<p>No channels found.</p>
{% else %}
{% for channel in channels %}
<p><a href="{{ url_for('channel_feed', name=channel) }}">{{ channel }}</a></p>
{% endfor %}
{% endif %}
<p><a href="{{ url_for('channels_edit') }}">Edit channels</a></p>
</details>
</div>
<div class="readable-item"> <div class="readable-item">
<details open> <details open>
<summary><span class="item-title">Sources</span></summary> <summary><span class="item-title">Sources</span></summary>
@ -40,7 +55,7 @@ summary:focus {
<p>No sources found.</p> <p>No sources found.</p>
{% else %} {% else %}
{% for source in sources %} {% for source in sources %}
<p><a href="{{ url_for('source_feed', source_name=source.source_name) }}">{{ source.source_name|safe }}</a> (<a href="{{ url_for('source_edit', source_name=source.source_name) }}">edit</a>)</p> <p><a href="{{ url_for('source_feed', name=source.source_name) }}">{{ source.source_name|safe }}</a> (<a href="{{ url_for('source_edit', name=source.source_name) }}">edit</a>)</p>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</details> </details>

7
tests/channels.json Normal file
View File

@ -0,0 +1,7 @@
{
"demo": [
"demo_basic_callback",
"demo_logging",
"demo_raw_sh"
]
}