fix(cyberfeed): BusyBox AWK compatibility and settings form fixes
- Rewrite RSS parser to use BusyBox-compatible AWK (no capture groups) - Use extract_tag() helper with substr() instead of match() capture - Use extract_attr() helper for XML attribute extraction - Fix settings.js select elements to properly set initial value - Use sel.value = ... instead of selected attribute - Add new UCI config options for enhanced features: - download_media, media_dir, history_file, generate_timeline - Bump versions: secubox-app-cyberfeed 0.2.1, luci-app-cyberfeed 0.1.1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
19c30ddce9
commit
179565cfca
@ -10,8 +10,8 @@ LUCI_DEPENDS:=+secubox-app-cyberfeed +luci-base +luci-compat
|
|||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|
||||||
PKG_NAME:=luci-app-cyberfeed
|
PKG_NAME:=luci-app-cyberfeed
|
||||||
PKG_VERSION:=0.1.0
|
PKG_VERSION:=0.1.1
|
||||||
PKG_RELEASE:=3
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
PKG_LICENSE:=MIT
|
PKG_LICENSE:=MIT
|
||||||
|
|||||||
@ -51,10 +51,14 @@ return view.extend({
|
|||||||
E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 20px;' }, [
|
E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 20px;' }, [
|
||||||
E('div', { 'class': 'cf-form-group' }, [
|
E('div', { 'class': 'cf-form-group' }, [
|
||||||
E('label', { 'class': 'cf-form-label' }, 'Service Enabled'),
|
E('label', { 'class': 'cf-form-label' }, 'Service Enabled'),
|
||||||
E('select', { 'id': 'cfg-enabled', 'class': 'cf-form-input' }, [
|
(function() {
|
||||||
E('option', { 'value': '1', 'selected': config.enabled == 1 }, 'Enabled'),
|
var sel = E('select', { 'id': 'cfg-enabled', 'class': 'cf-form-input' }, [
|
||||||
E('option', { 'value': '0', 'selected': config.enabled == 0 }, 'Disabled')
|
E('option', { 'value': '1' }, 'Enabled'),
|
||||||
])
|
E('option', { 'value': '0' }, 'Disabled')
|
||||||
|
]);
|
||||||
|
sel.value = (config.enabled == 1) ? '1' : '0';
|
||||||
|
return sel;
|
||||||
|
})()
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'cf-form-group' }, [
|
E('div', { 'class': 'cf-form-group' }, [
|
||||||
E('label', { 'class': 'cf-form-label' }, 'Refresh Interval (minutes)'),
|
E('label', { 'class': 'cf-form-label' }, 'Refresh Interval (minutes)'),
|
||||||
@ -110,10 +114,14 @@ return view.extend({
|
|||||||
E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 20px;' }, [
|
E('div', { 'class': 'cf-grid cf-grid-2', 'style': 'gap: 20px;' }, [
|
||||||
E('div', { 'class': 'cf-form-group' }, [
|
E('div', { 'class': 'cf-form-group' }, [
|
||||||
E('label', { 'class': 'cf-form-label' }, 'RSS-Bridge Enabled'),
|
E('label', { 'class': 'cf-form-label' }, 'RSS-Bridge Enabled'),
|
||||||
E('select', { 'id': 'cfg-rssbridge-enabled', 'class': 'cf-form-input' }, [
|
(function() {
|
||||||
E('option', { 'value': '1', 'selected': config.rssbridge_enabled == 1 }, 'Enabled'),
|
var sel = E('select', { 'id': 'cfg-rssbridge-enabled', 'class': 'cf-form-input' }, [
|
||||||
E('option', { 'value': '0', 'selected': config.rssbridge_enabled == 0 }, 'Disabled')
|
E('option', { 'value': '1' }, 'Enabled'),
|
||||||
])
|
E('option', { 'value': '0' }, 'Disabled')
|
||||||
|
]);
|
||||||
|
sel.value = (config.rssbridge_enabled == 1) ? '1' : '0';
|
||||||
|
return sel;
|
||||||
|
})()
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'cf-form-group' }, [
|
E('div', { 'class': 'cf-form-group' }, [
|
||||||
E('label', { 'class': 'cf-form-label' }, 'RSS-Bridge Port'),
|
E('label', { 'class': 'cf-form-label' }, 'RSS-Bridge Port'),
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=secubox-app-cyberfeed
|
PKG_NAME:=secubox-app-cyberfeed
|
||||||
PKG_VERSION:=0.1.0
|
PKG_VERSION:=0.2.1
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|||||||
@ -5,6 +5,10 @@ config cyberfeed 'main'
|
|||||||
option cache_ttl '300'
|
option cache_ttl '300'
|
||||||
option output_dir '/tmp/cyberfeed/output'
|
option output_dir '/tmp/cyberfeed/output'
|
||||||
option theme 'cyberpunk'
|
option theme 'cyberpunk'
|
||||||
|
option download_media '0'
|
||||||
|
option media_dir '/srv/cyberfeed/media'
|
||||||
|
option history_file '/var/lib/cyberfeed/history.json'
|
||||||
|
option generate_timeline '1'
|
||||||
|
|
||||||
config rssbridge 'rssbridge'
|
config rssbridge 'rssbridge'
|
||||||
option enabled '0'
|
option enabled '0'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# ╔═══════════════════════════════════════════════════════════════════╗
|
# ╔═══════════════════════════════════════════════════════════════════╗
|
||||||
# ║ ⚡ CYBERFEED v0.1 - RSS Aggregator for OpenWrt/SecuBox ⚡ ║
|
# ║ ⚡ CYBERFEED v0.2 - RSS Aggregator for OpenWrt/SecuBox ⚡ ║
|
||||||
# ║ Cyberpunk Social Feed Analyzer with Emoji Enhancement ║
|
# ║ Cyberpunk Feed Analyzer with Timeline & Audio Preview ║
|
||||||
# ║ Author: CyberMind.FR | License: MIT ║
|
# ║ Author: CyberMind.FR | License: MIT ║
|
||||||
# ╚═══════════════════════════════════════════════════════════════════╝
|
# ╚═══════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
@ -12,9 +12,12 @@ CYBERFEED_DIR="/tmp/cyberfeed"
|
|||||||
CACHE_DIR="${CYBERFEED_DIR}/cache"
|
CACHE_DIR="${CYBERFEED_DIR}/cache"
|
||||||
OUTPUT_DIR="${CYBERFEED_DIR}/output"
|
OUTPUT_DIR="${CYBERFEED_DIR}/output"
|
||||||
CONFIG_FILE="/etc/cyberfeed/feeds.conf"
|
CONFIG_FILE="/etc/cyberfeed/feeds.conf"
|
||||||
TEMPLATE_FILE="/usr/share/cyberfeed/template.html"
|
HISTORY_FILE="/var/lib/cyberfeed/history.json"
|
||||||
|
MEDIA_DIR="/srv/cyberfeed/media"
|
||||||
MAX_ITEMS=20
|
MAX_ITEMS=20
|
||||||
CACHE_TTL=300
|
CACHE_TTL=300
|
||||||
|
DOWNLOAD_MEDIA=0
|
||||||
|
GENERATE_TIMELINE=1
|
||||||
|
|
||||||
# Load UCI config
|
# Load UCI config
|
||||||
load_config() {
|
load_config() {
|
||||||
@ -22,33 +25,55 @@ load_config() {
|
|||||||
config_get MAX_ITEMS main max_items 20
|
config_get MAX_ITEMS main max_items 20
|
||||||
config_get CACHE_TTL main cache_ttl 300
|
config_get CACHE_TTL main cache_ttl 300
|
||||||
config_get OUTPUT_DIR main output_dir "/tmp/cyberfeed/output"
|
config_get OUTPUT_DIR main output_dir "/tmp/cyberfeed/output"
|
||||||
|
config_get HISTORY_FILE main history_file "/var/lib/cyberfeed/history.json"
|
||||||
|
config_get MEDIA_DIR main media_dir "/srv/cyberfeed/media"
|
||||||
|
config_get DOWNLOAD_MEDIA main download_media 0
|
||||||
|
config_get GENERATE_TIMELINE main generate_timeline 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# === HISTORY MANAGEMENT ===
|
||||||
|
init_history() {
|
||||||
|
local dir=$(dirname "$HISTORY_FILE")
|
||||||
|
mkdir -p "$dir"
|
||||||
|
[ -f "$HISTORY_FILE" ] || echo '{"seen":[],"downloaded":[]}' > "$HISTORY_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_seen() {
|
||||||
|
local id="$1"
|
||||||
|
grep -q "\"$id\"" "$HISTORY_FILE" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
mark_seen() {
|
||||||
|
local id="$1"
|
||||||
|
if [ -f "$HISTORY_FILE" ]; then
|
||||||
|
local seen=$(jsonfilter -i "$HISTORY_FILE" -e '@.seen' 2>/dev/null || echo '[]')
|
||||||
|
# Simple append (proper JSON manipulation would need jq)
|
||||||
|
sed -i "s/\"seen\":\[/\"seen\":[\"$id\",/" "$HISTORY_FILE" 2>/dev/null
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# === CYBERPUNK EMOJI MAPPING ===
|
# === CYBERPUNK EMOJI MAPPING ===
|
||||||
cyberpunk_emojify() {
|
cyberpunk_emojify() {
|
||||||
local text="$1"
|
local text="$1"
|
||||||
|
|
||||||
# Security/Hacking themes
|
|
||||||
echo "$text" | sed -E '
|
echo "$text" | sed -E '
|
||||||
s/(hack|breach|exploit|vulnerab)/\xf0\x9f\x94\x93\1/gi
|
s/(hack|breach|exploit|vulnerab)/🔓\1/gi
|
||||||
s/(secur|protect|defense|firewall)/\xf0\x9f\x9b\xa1\xef\xb8\x8f\1/gi
|
s/(secur|protect|defense|firewall)/🛡️\1/gi
|
||||||
s/(cyber|digital|virtual)/\xe2\x9a\xa1\1/gi
|
s/(cyber|digital|virtual)/⚡\1/gi
|
||||||
s/(encrypt|crypto|cipher)/\xf0\x9f\x94\x90\1/gi
|
s/(encrypt|crypto|cipher)/🔐\1/gi
|
||||||
s/(malware|virus|trojan)/\xe2\x98\xa0\xef\xb8\x8f\1/gi
|
s/(malware|virus|trojan)/☠️\1/gi
|
||||||
s/(alert|warning|danger)/\xe2\x9a\xa0\xef\xb8\x8f\1/gi
|
s/(alert|warning|danger)/⚠️\1/gi
|
||||||
s/(attack|threat|risk)/\xf0\x9f\x92\x80\1/gi
|
s/(attack|threat|risk)/💀\1/gi
|
||||||
s/(network|connect|link)/\xf0\x9f\x8c\x90\1/gi
|
s/(network|connect|link)/🌐\1/gi
|
||||||
s/(server|cloud|data)/\xf0\x9f\x92\xbe\1/gi
|
s/(server|cloud|data)/💾\1/gi
|
||||||
s/(code|program|script)/\xf0\x9f\x92\xbb\1/gi
|
s/(code|program|script)/💻\1/gi
|
||||||
s/(linux|opensource|github)/\xf0\x9f\x90\xa7\1/gi
|
s/(linux|opensource|github)/🐧\1/gi
|
||||||
s/(robot|automat|ai|machine)/\xf0\x9f\xa4\x96\1/gi
|
s/(robot|automat|ai|machine)/🤖\1/gi
|
||||||
s/(update|upgrade|patch)/\xf0\x9f\x93\xa1\1/gi
|
s/(update|upgrade|patch)/📡\1/gi
|
||||||
s/(launch|deploy|release)/\xf0\x9f\x9a\x80\1/gi
|
s/(launch|deploy|release)/🚀\1/gi
|
||||||
s/(new|annonce|breaking)/\xe2\x9c\xa8\1/gi
|
s/(podcast|radio|audio)/🎧\1/gi
|
||||||
s/(success|win|achieve)/\xf0\x9f\x8f\x86\1/gi
|
s/(video|stream|watch)/📺\1/gi
|
||||||
s/(fail|error|bug)/\xf0\x9f\x90\x9b\1/gi
|
s/(music|song|album)/🎵\1/gi
|
||||||
s/(magic|mystiq|oracle)/\xf0\x9f\x94\xae\1/gi
|
s/(new|annonce|breaking)/✨\1/gi
|
||||||
s/(energy|power|force)/\xe2\x9a\xa1\1/gi
|
|
||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +83,6 @@ fetch_feed() {
|
|||||||
local name="$2"
|
local name="$2"
|
||||||
local cache_file="${CACHE_DIR}/${name}.xml"
|
local cache_file="${CACHE_DIR}/${name}.xml"
|
||||||
|
|
||||||
# Check cache freshness
|
|
||||||
if [ -f "$cache_file" ]; then
|
if [ -f "$cache_file" ]; then
|
||||||
local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
|
local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
|
||||||
local now=$(date +%s)
|
local now=$(date +%s)
|
||||||
@ -69,7 +93,6 @@ fetch_feed() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fetch with wget (OpenWrt standard)
|
|
||||||
wget -q -T 15 -O "$cache_file" "$url" 2>/dev/null
|
wget -q -T 15 -O "$cache_file" "$url" 2>/dev/null
|
||||||
|
|
||||||
if [ -f "$cache_file" ] && [ -s "$cache_file" ]; then
|
if [ -f "$cache_file" ] && [ -s "$cache_file" ]; then
|
||||||
@ -80,63 +103,321 @@ fetch_feed() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# === RSS PARSER (Pure AWK for OpenWrt) ===
|
# === ENHANCED RSS PARSER (BusyBox AWK compatible) ===
|
||||||
parse_rss() {
|
parse_rss() {
|
||||||
local xml="$1"
|
local xml="$1"
|
||||||
local source="$2"
|
local source="$2"
|
||||||
local category="$3"
|
local category="$3"
|
||||||
|
|
||||||
echo "$xml" | awk -v source="$source" -v category="$category" -v max="$MAX_ITEMS" '
|
echo "$xml" | awk -v source="$source" -v category="$category" -v max="$MAX_ITEMS" '
|
||||||
|
# Helper: extract content between XML tags
|
||||||
|
function extract_tag(str, tag, start, end, rest, content) {
|
||||||
|
# Try <tag>content</tag>
|
||||||
|
if (match(str, "<" tag "[^>]*>")) {
|
||||||
|
start = RSTART + RLENGTH
|
||||||
|
rest = substr(str, start)
|
||||||
|
if (match(rest, "</" tag ">")) {
|
||||||
|
content = substr(rest, 1, RSTART - 1)
|
||||||
|
# Handle CDATA
|
||||||
|
if (match(content, "^<!\\[CDATA\\[")) {
|
||||||
|
content = substr(content, 10)
|
||||||
|
sub(/\]\]>$/, "", content)
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper: extract attribute value
|
||||||
|
function extract_attr(str, tag, attr, tagstart, tagend, tagstr, attrpos, rest, val) {
|
||||||
|
if (match(str, "<" tag "[^>]*>")) {
|
||||||
|
tagstr = substr(str, RSTART, RLENGTH)
|
||||||
|
if (match(tagstr, attr "=\"[^\"]*\"")) {
|
||||||
|
val = substr(tagstr, RSTART + length(attr) + 2)
|
||||||
|
sub(/".*/, "", val)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
BEGIN {
|
BEGIN {
|
||||||
RS="</item>|</entry>"
|
RS="</item>|</entry>"
|
||||||
item_count=0
|
item_count=0
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
title=""
|
if (item_count >= max) next
|
||||||
link=""
|
|
||||||
date=""
|
|
||||||
desc=""
|
|
||||||
|
|
||||||
# Extract title
|
title = extract_tag($0, "title")
|
||||||
if (match($0, /<title[^>]*>([^<]+)<\/title>/, arr)) title=arr[1]
|
link = extract_tag($0, "link")
|
||||||
else if (match($0, /<title[^>]*><!\[CDATA\[([^\]]+)\]\]><\/title>/, arr)) title=arr[1]
|
|
||||||
|
|
||||||
# Extract link
|
# Atom links use href attribute
|
||||||
if (match($0, /<link[^>]*>([^<]+)<\/link>/, arr)) link=arr[1]
|
if (link == "") {
|
||||||
else if (match($0, /<link[^>]*href="([^"]+)"/, arr)) link=arr[1]
|
link = extract_attr($0, "link", "href")
|
||||||
|
}
|
||||||
|
|
||||||
# Extract date
|
# Extract date (multiple formats)
|
||||||
if (match($0, /<pubDate>([^<]+)</, arr)) date=arr[1]
|
date = extract_tag($0, "pubDate")
|
||||||
else if (match($0, /<published>([^<]+)</, arr)) date=arr[1]
|
if (date == "") date = extract_tag($0, "published")
|
||||||
else if (match($0, /<updated>([^<]+)</, arr)) date=arr[1]
|
if (date == "") date = extract_tag($0, "updated")
|
||||||
|
if (date == "") date = extract_tag($0, "dc:date")
|
||||||
|
|
||||||
# Extract description
|
# Extract description
|
||||||
if (match($0, /<description[^>]*>([^<]+)</, arr)) desc=arr[1]
|
desc = extract_tag($0, "description")
|
||||||
else if (match($0, /<summary[^>]*>([^<]+)</, arr)) desc=arr[1]
|
if (desc == "") desc = extract_tag($0, "summary")
|
||||||
else if (match($0, /<description[^>]*><!\[CDATA\[(.{1,500})/, arr)) desc=arr[1]
|
if (desc == "") desc = extract_tag($0, "content")
|
||||||
|
|
||||||
if (title != "" && item_count < max) {
|
# Extract enclosure URL and type (podcasts)
|
||||||
# Escape JSON special chars
|
enclosure = extract_attr($0, "enclosure", "url")
|
||||||
|
enclosure_type = extract_attr($0, "enclosure", "type")
|
||||||
|
|
||||||
|
# Fallback to media:content
|
||||||
|
if (enclosure == "") {
|
||||||
|
enclosure = extract_attr($0, "media:content", "url")
|
||||||
|
enclosure_type = extract_attr($0, "media:content", "type")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract iTunes duration
|
||||||
|
duration = extract_tag($0, "itunes:duration")
|
||||||
|
|
||||||
|
# Extract GUID
|
||||||
|
guid = extract_tag($0, "guid")
|
||||||
|
if (guid == "") guid = link
|
||||||
|
|
||||||
|
if (title != "") {
|
||||||
|
# Clean and escape for JSON
|
||||||
|
gsub(/\\/, "\\\\", title)
|
||||||
gsub(/"/, "\\\"", title)
|
gsub(/"/, "\\\"", title)
|
||||||
gsub(/"/, "\\\"", desc)
|
|
||||||
gsub(/[\r\n\t]/, " ", title)
|
gsub(/[\r\n\t]/, " ", title)
|
||||||
|
|
||||||
|
gsub(/\\/, "\\\\", desc)
|
||||||
|
gsub(/"/, "\\\"", desc)
|
||||||
gsub(/[\r\n\t]/, " ", desc)
|
gsub(/[\r\n\t]/, " ", desc)
|
||||||
gsub(/<[^>]+>/, "", desc)
|
gsub(/<[^>]+>/, "", desc)
|
||||||
desc = substr(desc, 1, 280)
|
desc = substr(desc, 1, 280)
|
||||||
|
|
||||||
printf "{\"title\":\"%s\",\"link\":\"%s\",\"date\":\"%s\",\"desc\":\"%s\",\"source\":\"%s\",\"category\":\"%s\"},", title, link, date, desc, source, category
|
gsub(/\\/, "\\\\", link)
|
||||||
|
gsub(/"/, "\\\"", link)
|
||||||
|
|
||||||
|
gsub(/\\/, "\\\\", enclosure)
|
||||||
|
gsub(/"/, "\\\"", enclosure)
|
||||||
|
|
||||||
|
# Determine media type
|
||||||
|
media_type = ""
|
||||||
|
if (enclosure_type ~ /audio/) media_type = "audio"
|
||||||
|
else if (enclosure_type ~ /video/) media_type = "video"
|
||||||
|
else if (enclosure ~ /\.mp3|\.m4a|\.ogg|\.wav/) media_type = "audio"
|
||||||
|
else if (enclosure ~ /\.mp4|\.webm|\.mkv/) media_type = "video"
|
||||||
|
|
||||||
|
printf "{\"title\":\"%s\",\"link\":\"%s\",\"date\":\"%s\",\"desc\":\"%s\",\"source\":\"%s\",\"category\":\"%s\",\"enclosure\":\"%s\",\"media_type\":\"%s\",\"duration\":\"%s\",\"guid\":\"%s\"},", title, link, date, desc, source, category, enclosure, media_type, duration, guid
|
||||||
item_count++
|
item_count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
# === HTML GENERATOR ===
|
# === TIMELINE HTML GENERATOR ===
|
||||||
|
generate_timeline() {
|
||||||
|
local json_file="$1"
|
||||||
|
local output_file="${OUTPUT_DIR}/timeline.html"
|
||||||
|
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
cat > "$output_file" << 'TIMELINEHTML'
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>⚡ CYBERFEED TIMELINE ⚡</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap');
|
||||||
|
:root {
|
||||||
|
--neon-cyan: #0ff;
|
||||||
|
--neon-magenta: #f0f;
|
||||||
|
--dark-bg: #0a0a0f;
|
||||||
|
--text-primary: #e0e0e0;
|
||||||
|
--text-dim: #606080;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Share Tech Mono', monospace;
|
||||||
|
background: var(--dark-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-bottom: 1px solid var(--neon-cyan);
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--neon-cyan);
|
||||||
|
text-shadow: 0 0 10px var(--neon-cyan);
|
||||||
|
}
|
||||||
|
.timeline {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 15px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: linear-gradient(180deg, var(--neon-cyan), var(--neon-magenta));
|
||||||
|
box-shadow: 0 0 10px var(--neon-cyan);
|
||||||
|
}
|
||||||
|
.timeline-date {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--neon-magenta);
|
||||||
|
margin: 30px 0 15px -40px;
|
||||||
|
padding-left: 40px;
|
||||||
|
text-shadow: 0 0 5px var(--neon-magenta);
|
||||||
|
}
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0,255,255,0.05);
|
||||||
|
border: 1px solid rgba(0,255,255,0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.timeline-item::before {
|
||||||
|
content: '◆';
|
||||||
|
position: absolute;
|
||||||
|
left: -33px;
|
||||||
|
top: 18px;
|
||||||
|
color: var(--neon-cyan);
|
||||||
|
text-shadow: 0 0 10px var(--neon-cyan);
|
||||||
|
}
|
||||||
|
.timeline-item.has-audio::before { content: '🎧'; }
|
||||||
|
.timeline-item.has-video::before { content: '📺'; }
|
||||||
|
.item-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--neon-magenta);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.item-title {
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--neon-cyan);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.item-title a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.item-title a:hover {
|
||||||
|
text-shadow: 0 0 10px var(--neon-cyan);
|
||||||
|
}
|
||||||
|
.item-desc {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
opacity: 0.8;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.item-source {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(255,0,255,0.2);
|
||||||
|
border: 1px solid var(--neon-magenta);
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.audio-player {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.audio-player audio {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
.nav-links {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.nav-links a {
|
||||||
|
color: var(--neon-cyan);
|
||||||
|
margin: 0 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-links a:hover {
|
||||||
|
text-shadow: 0 0 10px var(--neon-cyan);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<h1>⚡ TIMELINE ⚡</h1>
|
||||||
|
<p style="color: var(--text-dim); margin-top: 10px;">Chronological Feed History</p>
|
||||||
|
</header>
|
||||||
|
<nav class="nav-links">
|
||||||
|
<a href="/cyberfeed/">← Dashboard</a>
|
||||||
|
<a href="/cyberfeed/timeline.html">Timeline</a>
|
||||||
|
</nav>
|
||||||
|
<div class="timeline" id="timeline">
|
||||||
|
<p style="text-align:center; color: var(--text-dim);">Loading timeline...</p>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function loadTimeline() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/cyberfeed/feeds.json?' + Date.now());
|
||||||
|
const items = await resp.json();
|
||||||
|
|
||||||
|
// Group by date
|
||||||
|
const grouped = {};
|
||||||
|
items.forEach(item => {
|
||||||
|
const dateStr = item.date ? new Date(item.date).toLocaleDateString('fr-FR', {
|
||||||
|
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
|
||||||
|
}) : 'Date inconnue';
|
||||||
|
if (!grouped[dateStr]) grouped[dateStr] = [];
|
||||||
|
grouped[dateStr].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
Object.keys(grouped).forEach(date => {
|
||||||
|
html += '<div class="timeline-date">' + date + '</div>';
|
||||||
|
grouped[date].forEach(item => {
|
||||||
|
const hasMedia = item.media_type ? ' has-' + item.media_type : '';
|
||||||
|
html += '<div class="timeline-item' + hasMedia + '">';
|
||||||
|
html += '<div class="item-time">⏰ ' + (item.date || '') + '</div>';
|
||||||
|
html += '<div class="item-title"><a href="' + item.link + '" target="_blank">' + item.title + '</a></div>';
|
||||||
|
if (item.desc) html += '<div class="item-desc">' + item.desc + '</div>';
|
||||||
|
if (item.enclosure && item.media_type === 'audio') {
|
||||||
|
html += '<div class="audio-player"><audio controls preload="none"><source src="' + item.enclosure + '" type="audio/mpeg">Audio non supporté</audio></div>';
|
||||||
|
}
|
||||||
|
html += '<span class="item-source">' + (item.source || 'RSS') + '</span>';
|
||||||
|
if (item.duration) html += ' <span class="item-source">⏱ ' + item.duration + '</span>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('timeline').innerHTML = html || '<p style="text-align:center;">Aucun élément</p>';
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('timeline').innerHTML = '<p style="text-align:center; color: #f00;">Erreur de chargement</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadTimeline();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
TIMELINEHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
# === MAIN HTML GENERATOR (with audio player) ===
|
||||||
generate_html() {
|
generate_html() {
|
||||||
local json_file="$1"
|
local json_file="$1"
|
||||||
local output_file="${OUTPUT_DIR}/index.html"
|
local output_file="${OUTPUT_DIR}/index.html"
|
||||||
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
||||||
local feed_count=$(grep -c '"title"' "$json_file" 2>/dev/null || echo 0)
|
|
||||||
|
|
||||||
cat > "$output_file" << 'HTMLEOF'
|
cat > "$output_file" << 'HTMLEOF'
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -152,7 +433,6 @@ generate_html() {
|
|||||||
--neon-magenta: #f0f;
|
--neon-magenta: #f0f;
|
||||||
--neon-yellow: #ff0;
|
--neon-yellow: #ff0;
|
||||||
--dark-bg: #0a0a0f;
|
--dark-bg: #0a0a0f;
|
||||||
--darker-bg: #050508;
|
|
||||||
--grid-color: rgba(0, 255, 255, 0.03);
|
--grid-color: rgba(0, 255, 255, 0.03);
|
||||||
--text-primary: #e0e0e0;
|
--text-primary: #e0e0e0;
|
||||||
--text-dim: #606080;
|
--text-dim: #606080;
|
||||||
@ -181,15 +461,6 @@ body::before {
|
|||||||
0% { transform: translateY(0); }
|
0% { transform: translateY(0); }
|
||||||
100% { transform: translateY(50px); }
|
100% { transform: translateY(50px); }
|
||||||
}
|
}
|
||||||
body::after {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
.cyber-header {
|
.cyber-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@ -204,13 +475,6 @@ body::after {
|
|||||||
letter-spacing: 0.2em;
|
letter-spacing: 0.2em;
|
||||||
color: var(--neon-cyan);
|
color: var(--neon-cyan);
|
||||||
text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan), 0 0 40px var(--neon-cyan);
|
text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan), 0 0 40px var(--neon-cyan);
|
||||||
animation: flicker 3s infinite;
|
|
||||||
}
|
|
||||||
@keyframes flicker {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
92% { opacity: 1; }
|
|
||||||
93% { opacity: 0.8; }
|
|
||||||
94% { opacity: 1; }
|
|
||||||
}
|
}
|
||||||
.cyber-header .subtitle {
|
.cyber-header .subtitle {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@ -218,6 +482,26 @@ body::after {
|
|||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
letter-spacing: 0.4em;
|
letter-spacing: 0.4em;
|
||||||
}
|
}
|
||||||
|
.nav-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border-bottom: 1px solid rgba(0,255,255,0.2);
|
||||||
|
}
|
||||||
|
.nav-bar a {
|
||||||
|
color: var(--neon-cyan);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid var(--neon-cyan);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.nav-bar a:hover {
|
||||||
|
background: var(--neon-cyan);
|
||||||
|
color: var(--dark-bg);
|
||||||
|
box-shadow: 0 0 15px var(--neon-cyan);
|
||||||
|
}
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -264,22 +548,13 @@ body::after {
|
|||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.feed-item::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0; left: 0;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(0,255,255,0.1), transparent);
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform 0.5s ease;
|
|
||||||
}
|
}
|
||||||
|
.feed-item.has-audio { border-left-color: var(--neon-magenta); }
|
||||||
|
.feed-item.has-video { border-left-color: var(--neon-yellow); }
|
||||||
.feed-item:hover {
|
.feed-item:hover {
|
||||||
border-color: var(--neon-magenta);
|
border-color: var(--neon-magenta);
|
||||||
box-shadow: 0 0 20px rgba(0,255,255,0.2), inset 0 0 20px rgba(255,0,255,0.05);
|
box-shadow: 0 0 20px rgba(0,255,255,0.2);
|
||||||
}
|
}
|
||||||
.feed-item:hover::before { transform: translateX(100%); }
|
|
||||||
.feed-item .meta {
|
.feed-item .meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -300,11 +575,10 @@ body::after {
|
|||||||
padding: 0.15rem 0.5rem;
|
padding: 0.15rem 0.5rem;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.1em;
|
|
||||||
}
|
}
|
||||||
.feed-item .category-tag {
|
.feed-item .media-tag {
|
||||||
background: rgba(0,255,255,0.2);
|
background: rgba(255,255,0,0.2);
|
||||||
border: 1px solid var(--neon-cyan);
|
border: 1px solid var(--neon-yellow);
|
||||||
padding: 0.15rem 0.5rem;
|
padding: 0.15rem 0.5rem;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -320,7 +594,6 @@ body::after {
|
|||||||
.feed-item .title a {
|
.feed-item .title a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: text-shadow 0.3s ease;
|
|
||||||
}
|
}
|
||||||
.feed-item .title a:hover { text-shadow: 0 0 10px var(--neon-cyan); }
|
.feed-item .title a:hover { text-shadow: 0 0 10px var(--neon-cyan); }
|
||||||
.feed-item .description {
|
.feed-item .description {
|
||||||
@ -329,6 +602,14 @@ body::after {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
.audio-player {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.audio-player audio {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
.cyber-footer {
|
.cyber-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
@ -337,17 +618,6 @@ body::after {
|
|||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
}
|
}
|
||||||
.cyber-footer a { color: var(--neon-cyan); text-decoration: none; }
|
.cyber-footer a { color: var(--neon-cyan); text-decoration: none; }
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: 4rem 2rem;
|
|
||||||
color: var(--text-dim);
|
|
||||||
}
|
|
||||||
.empty-state .icon { font-size: 4rem; margin-bottom: 1rem; }
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.feed-container { padding: 1rem; }
|
|
||||||
.feed-item { padding: 1rem; }
|
|
||||||
.status-bar { gap: 1rem; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -360,21 +630,26 @@ body::after {
|
|||||||
<span>STATUS: ONLINE</span>
|
<span>STATUS: ONLINE</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<nav class="nav-bar">
|
||||||
|
<a href="/cyberfeed/">Dashboard</a>
|
||||||
|
<a href="/cyberfeed/timeline.html">📅 Timeline</a>
|
||||||
|
</nav>
|
||||||
<div class="category-filter">
|
<div class="category-filter">
|
||||||
<button class="category-btn active" onclick="filterCategory('all')">ALL</button>
|
<button class="category-btn active" onclick="filterCategory('all')">ALL</button>
|
||||||
<button class="category-btn" onclick="filterCategory('security')">SECURITY</button>
|
<button class="category-btn" onclick="filterCategory('security')">SECURITY</button>
|
||||||
<button class="category-btn" onclick="filterCategory('tech')">TECH</button>
|
<button class="category-btn" onclick="filterCategory('tech')">TECH</button>
|
||||||
<button class="category-btn" onclick="filterCategory('social')">SOCIAL</button>
|
<button class="category-btn" onclick="filterCategory('social')">SOCIAL</button>
|
||||||
|
<button class="category-btn" onclick="filterCategory('radio')">RADIO</button>
|
||||||
<button class="category-btn" onclick="filterCategory('news')">NEWS</button>
|
<button class="category-btn" onclick="filterCategory('news')">NEWS</button>
|
||||||
</div>
|
</div>
|
||||||
<main class="feed-container" id="feed-items">
|
<main class="feed-container" id="feed-items">
|
||||||
<div class="empty-state">
|
<div style="text-align:center; padding:40px; color: var(--text-dim);">
|
||||||
<div class="icon">🔮</div>
|
<p style="font-size:3rem;">🔮</p>
|
||||||
<p>Awaiting Neural Feed Connection...</p>
|
<p>Awaiting Neural Feed Connection...</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer class="cyber-footer">
|
<footer class="cyber-footer">
|
||||||
<p>⚡ CYBERFEED v0.1 | Powered by <a href="https://cybermind.fr">CyberMind.FR</a> | SecuBox Module ⚡</p>
|
<p>⚡ CYBERFEED v0.2 | Powered by <a href="https://cybermind.fr">CyberMind.FR</a> | SecuBox Module ⚡</p>
|
||||||
<p>░▒▓ JACK IN TO THE MATRIX ▓▒░</p>
|
<p>░▒▓ JACK IN TO THE MATRIX ▓▒░</p>
|
||||||
</footer>
|
</footer>
|
||||||
<script>
|
<script>
|
||||||
@ -405,23 +680,34 @@ function renderFeeds() {
|
|||||||
const filtered = currentFilter === 'all' ? feedData : feedData.filter(f => f.category === currentFilter);
|
const filtered = currentFilter === 'all' ? feedData : feedData.filter(f => f.category === currentFilter);
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
container.innerHTML = '<div class="empty-state"><div class="icon">📡</div><p>No feeds in this category</p></div>';
|
container.innerHTML = '<div style="text-align:center; padding:40px; color: var(--text-dim);"><p style="font-size:3rem;">📡</p><p>No feeds in this category</p></div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = filtered.map(item => `
|
container.innerHTML = filtered.map(item => {
|
||||||
<article class="feed-item" data-category="${item.category || 'custom'}">
|
const hasMedia = item.media_type ? ' has-' + item.media_type : '';
|
||||||
<div class="meta">
|
let audioPlayer = '';
|
||||||
<span class="timestamp">⏰ ${item.date || 'Unknown'}</span>
|
if (item.enclosure && item.media_type === 'audio') {
|
||||||
<div>
|
audioPlayer = '<div class="audio-player"><audio controls preload="none"><source src="' + item.enclosure + '" type="audio/mpeg">Audio non supporté</audio></div>';
|
||||||
<span class="source-tag">${item.source || 'RSS'}</span>
|
}
|
||||||
<span class="category-tag">${item.category || 'custom'}</span>
|
let mediaTag = '';
|
||||||
</div>
|
if (item.media_type === 'audio') mediaTag = '<span class="media-tag">🎧 AUDIO</span>';
|
||||||
</div>
|
else if (item.media_type === 'video') mediaTag = '<span class="media-tag">📺 VIDEO</span>';
|
||||||
<h2 class="title"><a href="${item.link}" target="_blank" rel="noopener">${item.title}</a></h2>
|
|
||||||
<p class="description">${item.desc || ''}</p>
|
return '<article class="feed-item' + hasMedia + '" data-category="' + (item.category || 'custom') + '">' +
|
||||||
</article>
|
'<div class="meta">' +
|
||||||
`).join('');
|
'<span class="timestamp">⏰ ' + (item.date || 'Unknown') + '</span>' +
|
||||||
|
'<div>' +
|
||||||
|
'<span class="source-tag">' + (item.source || 'RSS') + '</span> ' +
|
||||||
|
mediaTag +
|
||||||
|
(item.duration ? ' <span class="source-tag">⏱ ' + item.duration + '</span>' : '') +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<h2 class="title"><a href="' + item.link + '" target="_blank" rel="noopener">' + item.title + '</a></h2>' +
|
||||||
|
'<p class="description">' + (item.desc || '') + '</p>' +
|
||||||
|
audioPlayer +
|
||||||
|
'</article>';
|
||||||
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFeeds();
|
loadFeeds();
|
||||||
@ -430,38 +716,31 @@ setInterval(loadFeeds, 300000);
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
HTMLEOF
|
HTMLEOF
|
||||||
|
|
||||||
echo "$output_file"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# === STATUS ===
|
# === STATUS ===
|
||||||
get_status() {
|
get_status() {
|
||||||
local enabled=$(uci -q get cyberfeed.main.enabled || echo 0)
|
local enabled=$(uci -q get cyberfeed.main.enabled || echo 0)
|
||||||
local feed_count=0
|
local feed_count=0
|
||||||
local last_sync="never"
|
local item_count=0
|
||||||
local rssbridge_enabled=$(uci -q get cyberfeed.rssbridge.enabled || echo 0)
|
local last_sync=0
|
||||||
local rssbridge_status="stopped"
|
|
||||||
|
|
||||||
if [ -f "${OUTPUT_DIR}/feeds.json" ]; then
|
if [ -f "${OUTPUT_DIR}/feeds.json" ]; then
|
||||||
feed_count=$(grep -c '"title"' "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0)
|
item_count=$(grep -c '"title"' "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0)
|
||||||
last_sync=$(stat -c %Y "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0)
|
last_sync=$(stat -c %Y "${OUTPUT_DIR}/feeds.json" 2>/dev/null || echo 0)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$rssbridge_enabled" = "1" ]; then
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
if pgrep -f "rss-bridge" >/dev/null 2>&1; then
|
feed_count=$(grep -v "^#" "$CONFIG_FILE" 2>/dev/null | grep -c "|" || echo 0)
|
||||||
rssbridge_status="running"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat << EOF
|
cat << EOF
|
||||||
{
|
{
|
||||||
"enabled": $enabled,
|
"enabled": $enabled,
|
||||||
"feed_count": $feed_count,
|
"feed_count": $feed_count,
|
||||||
|
"item_count": $item_count,
|
||||||
"last_sync": $last_sync,
|
"last_sync": $last_sync,
|
||||||
"cache_ttl": $CACHE_TTL,
|
"has_timeline": $([ -f "${OUTPUT_DIR}/timeline.html" ] && echo "true" || echo "false")
|
||||||
"max_items": $MAX_ITEMS,
|
|
||||||
"rssbridge_enabled": $rssbridge_enabled,
|
|
||||||
"rssbridge_status": "$rssbridge_status"
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
@ -469,6 +748,7 @@ EOF
|
|||||||
# === SYNC FEEDS ===
|
# === SYNC FEEDS ===
|
||||||
sync_feeds() {
|
sync_feeds() {
|
||||||
load_config
|
load_config
|
||||||
|
init_history
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR" "$OUTPUT_DIR"
|
mkdir -p "$CACHE_DIR" "$OUTPUT_DIR"
|
||||||
|
|
||||||
@ -482,7 +762,6 @@ sync_feeds() {
|
|||||||
local feed_count=0
|
local feed_count=0
|
||||||
|
|
||||||
while IFS='|' read -r name url type category || [ -n "$name" ]; do
|
while IFS='|' read -r name url type category || [ -n "$name" ]; do
|
||||||
# Skip comments and empty lines
|
|
||||||
case "$name" in
|
case "$name" in
|
||||||
''|\#*) continue ;;
|
''|\#*) continue ;;
|
||||||
esac
|
esac
|
||||||
@ -502,24 +781,30 @@ sync_feeds() {
|
|||||||
fi
|
fi
|
||||||
done < "$CONFIG_FILE"
|
done < "$CONFIG_FILE"
|
||||||
|
|
||||||
# Remove trailing comma, close array
|
|
||||||
json_items=$(echo "$json_items" | sed 's/,$//')
|
json_items=$(echo "$json_items" | sed 's/,$//')
|
||||||
json_items="${json_items}]"
|
json_items="${json_items}]"
|
||||||
|
|
||||||
echo "$json_items" > "${OUTPUT_DIR}/feeds.json"
|
echo "$json_items" > "${OUTPUT_DIR}/feeds.json"
|
||||||
generate_html "${OUTPUT_DIR}/feeds.json" >/dev/null
|
generate_html "${OUTPUT_DIR}/feeds.json"
|
||||||
|
|
||||||
# Create symlink for web access
|
# Generate timeline if enabled
|
||||||
|
if [ "$GENERATE_TIMELINE" = "1" ]; then
|
||||||
|
generate_timeline "${OUTPUT_DIR}/feeds.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create symlinks
|
||||||
[ -L /www/cyberfeed/index.html ] || ln -sf "${OUTPUT_DIR}/index.html" /www/cyberfeed/index.html 2>/dev/null
|
[ -L /www/cyberfeed/index.html ] || ln -sf "${OUTPUT_DIR}/index.html" /www/cyberfeed/index.html 2>/dev/null
|
||||||
[ -L /www/cyberfeed/feeds.json ] || ln -sf "${OUTPUT_DIR}/feeds.json" /www/cyberfeed/feeds.json 2>/dev/null
|
[ -L /www/cyberfeed/feeds.json ] || ln -sf "${OUTPUT_DIR}/feeds.json" /www/cyberfeed/feeds.json 2>/dev/null
|
||||||
|
[ -L /www/cyberfeed/timeline.html ] || ln -sf "${OUTPUT_DIR}/timeline.html" /www/cyberfeed/timeline.html 2>/dev/null
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "╔═══════════════════════════════════════════════════════════╗"
|
echo "╔═══════════════════════════════════════════════════════════════╗"
|
||||||
echo "║ ⚡ CYBERFEED SYNC COMPLETE ⚡ ║"
|
echo "║ ⚡ CYBERFEED v0.2 SYNC COMPLETE ⚡ ║"
|
||||||
echo "╠═══════════════════════════════════════════════════════════╣"
|
echo "╠═══════════════════════════════════════════════════════════════╣"
|
||||||
printf "║ 📊 Feeds processed: %-36s ║\n" "$feed_count"
|
printf "║ 📊 Feeds processed: %-36s ║\n" "$feed_count"
|
||||||
echo "║ 📁 Output: /www/cyberfeed/ ║"
|
echo "║ 📁 Output: /www/cyberfeed/ ║"
|
||||||
echo "╚═══════════════════════════════════════════════════════════╝"
|
echo "║ 📅 Timeline: /www/cyberfeed/timeline.html ║"
|
||||||
|
echo "╚═══════════════════════════════════════════════════════════════╝"
|
||||||
}
|
}
|
||||||
|
|
||||||
# === LIST FEEDS ===
|
# === LIST FEEDS ===
|
||||||
@ -555,7 +840,6 @@ add_feed() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if feed already exists
|
|
||||||
if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then
|
if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then
|
||||||
echo '{"success":false,"error":"Feed already exists"}'
|
echo '{"success":false,"error":"Feed already exists"}'
|
||||||
return 1
|
return 1
|
||||||
@ -604,12 +888,18 @@ case "$1" in
|
|||||||
*)
|
*)
|
||||||
echo "Usage: $0 {sync|status|list|add|delete}"
|
echo "Usage: $0 {sync|status|list|add|delete}"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "CyberFeed v0.2 - Enhanced RSS Aggregator"
|
||||||
|
echo ""
|
||||||
echo "Commands:"
|
echo "Commands:"
|
||||||
echo " sync Fetch and process all feeds"
|
echo " sync Fetch feeds, generate HTML + Timeline"
|
||||||
echo " status Show service status (JSON)"
|
echo " status Show service status (JSON)"
|
||||||
echo " list List configured feeds (JSON)"
|
echo " list List configured feeds (JSON)"
|
||||||
echo " add NAME URL [TYPE] [CATEGORY]"
|
echo " add NAME URL [TYPE] [CATEGORY]"
|
||||||
echo " Add a new feed"
|
|
||||||
echo " delete NAME Remove a feed"
|
echo " delete NAME Remove a feed"
|
||||||
|
echo ""
|
||||||
|
echo "Output:"
|
||||||
|
echo " /www/cyberfeed/index.html - Main dashboard"
|
||||||
|
echo " /www/cyberfeed/timeline.html - Chronological timeline"
|
||||||
|
echo " /www/cyberfeed/feeds.json - Raw JSON data"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user