Add support for channels
This commit is contained in:
parent
70ee60bb65
commit
ad6da14387
115
intake/app.py
115
intake/app.py
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}}">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"demo": [
|
||||||
|
"demo_basic_callback",
|
||||||
|
"demo_logging",
|
||||||
|
"demo_raw_sh"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue