secubox-openwrt/package/secubox/secubox-app-cyberfeed/files/usr/bin/cyberfeed
CyberMind-FR 814a85754d style(cyberfeed): Amber & green CRT color scheme for timeline
- Amber (#ffb000) for titles, borders, hover effects
- Green (#33ff33) for dates, sources, navigation
- Gradient timeline line (amber → green → amber)
- Glow effects on text and borders
- Audio items highlighted in green
- Status bar with item count and sync time
- Emojified content from AWK parser preserved

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 03:58:12 +01:00

989 lines
27 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# ╔═══════════════════════════════════════════════════════════════════╗
# ║ ⚡ CYBERFEED v0.2 - RSS Aggregator for OpenWrt/SecuBox ⚡ ║
# ║ Cyberpunk Feed Analyzer with Timeline & Audio Preview ║
# ║ Author: CyberMind.FR | License: MIT ║
# ╚═══════════════════════════════════════════════════════════════════╝
. /lib/functions.sh
# === CONFIGURATION ===
CYBERFEED_DIR="/tmp/cyberfeed"
CACHE_DIR="${CYBERFEED_DIR}/cache"
OUTPUT_DIR="${CYBERFEED_DIR}/output"
CONFIG_FILE="/etc/cyberfeed/feeds.conf"
HISTORY_FILE="/var/lib/cyberfeed/history.json"
MEDIA_DIR="/srv/cyberfeed/media"
MAX_ITEMS=20
CACHE_TTL=300
DOWNLOAD_MEDIA=0
GENERATE_TIMELINE=1
# Load UCI config
load_config() {
config_load cyberfeed
config_get MAX_ITEMS main max_items 20
config_get CACHE_TTL main cache_ttl 300
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 (applied inside AWK) ===
# Emojification is done inside the AWK parser to avoid corrupting JSON keys
# === RSS FETCHER ===
fetch_feed() {
local url="$1"
local name="$2"
local cache_file="${CACHE_DIR}/${name}.xml"
if [ -f "$cache_file" ]; then
local file_time=$(stat -c %Y "$cache_file" 2>/dev/null || echo 0)
local now=$(date +%s)
local age=$((now - file_time))
if [ "$age" -lt "$CACHE_TTL" ]; then
cat "$cache_file"
return 0
fi
fi
wget -q -T 15 -O "$cache_file" "$url" 2>/dev/null
if [ -f "$cache_file" ] && [ -s "$cache_file" ]; then
cat "$cache_file"
else
rm -f "$cache_file"
return 1
fi
}
# === ENHANCED RSS PARSER (BusyBox AWK compatible) ===
parse_rss() {
local xml="$1"
local source="$2"
local category="$3"
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 ""
}
# Helper: add cyberpunk emojis to text (case-insensitive)
function emojify(text) {
gsub(/[Hh]ack/, "🔓hack", text)
gsub(/[Bb]reach/, "🔓breach", text)
gsub(/[Ee]xploit/, "🔓exploit", text)
gsub(/[Vv]ulnerab/, "🔓vulnerab", text)
gsub(/[Ss]ecur/, "🛡secur", text)
gsub(/[Pp]rotect/, "🛡protect", text)
gsub(/[Ff]irewall/, "🛡firewall", text)
gsub(/[Cc]yber/, "⚡cyber", text)
gsub(/[Ee]ncrypt/, "🔐encrypt", text)
gsub(/[Cc]rypto/, "🔐crypto", text)
gsub(/[Mm]alware/, "☠malware", text)
gsub(/[Vv]irus/, "☠virus", text)
gsub(/[Aa]ttack/, "💀attack", text)
gsub(/[Tt]hreat/, "💀threat", text)
gsub(/[Nn]etwork/, "🌐network", text)
gsub(/[Ss]erver/, "💾server", text)
gsub(/[Cc]loud/, "💾cloud", text)
gsub(/[Cc]ode/, "💻code", text)
gsub(/[Ll]inux/, "🐧linux", text)
gsub(/[Gg]ithub/, "🐧github", text)
gsub(/AI/, "🤖AI", text)
gsub(/[Uu]pdate/, "📡update", text)
gsub(/[Ll]aunch/, "🚀launch", text)
gsub(/[Rr]elease/, "🚀release", text)
gsub(/[Pp]odcast/, "🎧podcast", text)
gsub(/[Rr]adio/, "🎧radio", text)
gsub(/[Vv]ideo/, "📺video", text)
return text
}
BEGIN {
RS="</item>|</entry>"
item_count=0
}
{
if (item_count >= max) next
title = extract_tag($0, "title")
link = extract_tag($0, "link")
# Atom links use href attribute
if (link == "") {
link = extract_attr($0, "link", "href")
}
# Extract date (multiple formats)
date = extract_tag($0, "pubDate")
if (date == "") date = extract_tag($0, "published")
if (date == "") date = extract_tag($0, "updated")
if (date == "") date = extract_tag($0, "dc:date")
# Extract description
desc = extract_tag($0, "description")
if (desc == "") desc = extract_tag($0, "summary")
if (desc == "") desc = extract_tag($0, "content")
# Extract enclosure URL and type (podcasts)
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 != "") {
# Apply emojification to title and desc only
title = emojify(title)
desc = emojify(desc)
# Clean and escape for JSON
gsub(/\\/, "\\\\", title)
gsub(/"/, "\\\"", title)
gsub(/[\r\n\t]/, " ", title)
gsub(/\\/, "\\\\", desc)
gsub(/"/, "\\\"", desc)
gsub(/[\r\n\t]/, " ", desc)
gsub(/<[^>]+>/, "", desc)
desc = substr(desc, 1, 280)
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++
}
}
'
}
# === TIMELINE HTML GENERATOR (Amber & Green CRT Colors) ===
generate_timeline() {
local json_file="$1"
local output_file="${OUTPUT_DIR}/timeline.html"
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 {
--amber: #ffb000;
--amber-dim: #cc8800;
--amber-glow: rgba(255,176,0,0.4);
--green: #33ff33;
--green-dim: #22aa22;
--green-glow: rgba(51,255,51,0.4);
--dark-bg: #0a0a0a;
--darker-bg: #050505;
--text-dim: #555;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Share Tech Mono', monospace;
background: var(--dark-bg);
color: var(--amber);
min-height: 100vh;
padding: 20px;
}
.header {
text-align: center;
padding: 20px;
margin-bottom: 30px;
border-bottom: 2px solid var(--amber);
box-shadow: 0 2px 20px var(--amber-glow);
}
.header h1 {
font-family: 'Orbitron', sans-serif;
font-size: 2rem;
color: var(--amber);
text-shadow: 0 0 10px var(--amber), 0 0 20px var(--amber-glow);
letter-spacing: 0.1em;
}
.header .subtitle {
color: var(--green);
margin-top: 8px;
font-size: 0.85rem;
text-shadow: 0 0 8px var(--green-glow);
}
.nav-links {
text-align: center;
margin-bottom: 25px;
}
.nav-links a {
color: var(--green);
margin: 0 15px;
text-decoration: none;
padding: 8px 16px;
border: 1px solid var(--green-dim);
transition: all 0.3s;
}
.nav-links a:hover {
background: var(--green);
color: var(--dark-bg);
box-shadow: 0 0 15px var(--green-glow);
}
.timeline {
max-width: 800px;
margin: 0 auto;
position: relative;
padding-left: 40px;
}
.timeline::before {
content: '';
position: absolute;
left: 15px;
top: 0;
bottom: 0;
width: 3px;
background: linear-gradient(180deg, var(--amber), var(--green), var(--amber));
box-shadow: 0 0 15px var(--amber-glow);
}
.timeline-date {
font-family: 'Orbitron', sans-serif;
font-size: 1.1rem;
color: var(--green);
margin: 30px 0 15px -40px;
padding-left: 40px;
text-shadow: 0 0 8px var(--green-glow);
letter-spacing: 0.05em;
}
.timeline-item {
position: relative;
margin-bottom: 20px;
padding: 15px;
background: rgba(255,176,0,0.03);
border: 1px solid rgba(255,176,0,0.2);
border-left: 3px solid var(--amber);
border-radius: 4px;
transition: all 0.3s;
}
.timeline-item:hover {
background: rgba(255,176,0,0.08);
border-color: var(--amber);
box-shadow: 0 0 20px var(--amber-glow);
}
.timeline-item::before {
content: '●';
position: absolute;
left: -32px;
top: 18px;
color: var(--amber);
font-size: 12px;
text-shadow: 0 0 10px var(--amber);
}
.timeline-item.has-audio {
border-left-color: var(--green);
}
.timeline-item.has-audio::before {
content: '♪';
color: var(--green);
text-shadow: 0 0 10px var(--green);
}
.timeline-item.has-video::before {
content: '▶';
color: var(--green);
text-shadow: 0 0 10px var(--green);
}
.item-time {
font-size: 0.75rem;
color: var(--green-dim);
margin-bottom: 6px;
letter-spacing: 0.05em;
}
.item-title {
font-family: 'Orbitron', sans-serif;
font-size: 1rem;
color: var(--amber);
margin-bottom: 8px;
line-height: 1.4;
}
.item-title a {
color: inherit;
text-decoration: none;
transition: text-shadow 0.3s;
}
.item-title a:hover {
text-shadow: 0 0 15px var(--amber);
}
.item-desc {
font-size: 0.85rem;
color: var(--amber-dim);
opacity: 0.85;
margin-bottom: 10px;
line-height: 1.5;
}
.item-source {
display: inline-block;
background: rgba(51,255,51,0.1);
border: 1px solid var(--green-dim);
color: var(--green);
padding: 3px 10px;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.item-source.duration {
background: rgba(255,176,0,0.1);
border-color: var(--amber-dim);
color: var(--amber);
}
.audio-player {
margin-top: 12px;
}
.audio-player audio {
width: 100%;
height: 40px;
border-radius: 20px;
filter: sepia(100%) hue-rotate(-10deg) saturate(200%);
}
.status-bar {
text-align: center;
padding: 15px;
margin-top: 30px;
border-top: 1px solid var(--green-dim);
font-size: 0.7rem;
color: var(--text-dim);
}
.status-bar span {
color: var(--green);
margin: 0 10px;
}
</style>
</head>
<body>
<header class="header">
<h1>⚡ TIMELINE ⚡</h1>
<p class="subtitle">[ NEURAL FEED MATRIX ]</p>
</header>
<nav class="nav-links">
<a href="/cyberfeed/">← DASHBOARD</a>
<a href="/cyberfeed/timeline.html">↻ REFRESH</a>
</nav>
<div class="timeline" id="timeline">
<p style="text-align:center; color: var(--amber-dim);">LOADING DATA STREAM...</p>
</div>
<div class="status-bar">
<span id="item-count">-- ITEMS</span> |
<span id="last-update">SYNCING...</span> |
CYBERFEED v0.2
</div>
<script>
async function loadTimeline() {
try {
const resp = await fetch('/cyberfeed/feeds.json?' + Date.now());
const items = await resp.json();
document.getElementById('item-count').textContent = items.length + ' ITEMS';
document.getElementById('last-update').textContent = new Date().toLocaleTimeString('fr-FR', {hour12: false});
// 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'
}).toUpperCase() : '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></div>';
}
html += '<span class="item-source">' + (item.source || 'RSS') + '</span>';
if (item.duration) html += ' <span class="item-source duration">◷ ' + item.duration + '</span>';
html += '</div>';
});
});
document.getElementById('timeline').innerHTML = html || '<p style="text-align:center; color: var(--amber);">NO DATA</p>';
} catch(e) {
document.getElementById('timeline').innerHTML = '<p style="text-align:center; color: #ff3333;">ERROR: CONNECTION LOST</p>';
}
}
loadTimeline();
setInterval(loadTimeline, 180000);
</script>
</body>
</html>
TIMELINEHTML
}
# === MAIN HTML GENERATOR (with audio player) ===
generate_html() {
local json_file="$1"
local output_file="${OUTPUT_DIR}/index.html"
cat > "$output_file" << 'HTMLEOF'
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚡ CYBERFEED ⚡ Neural RSS Matrix</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;
--neon-yellow: #ff0;
--dark-bg: #0a0a0f;
--grid-color: rgba(0, 255, 255, 0.03);
--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;
}
body::before {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 50px 50px;
animation: gridScroll 20s linear infinite;
pointer-events: none;
z-index: -1;
}
@keyframes gridScroll {
0% { transform: translateY(0); }
100% { transform: translateY(50px); }
}
.cyber-header {
text-align: center;
padding: 2rem;
border-bottom: 1px solid var(--neon-cyan);
background: linear-gradient(180deg, rgba(0,255,255,0.1) 0%, transparent 100%);
}
.cyber-header h1 {
font-family: 'Orbitron', sans-serif;
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.2em;
color: var(--neon-cyan);
text-shadow: 0 0 10px var(--neon-cyan), 0 0 20px var(--neon-cyan), 0 0 40px var(--neon-cyan);
}
.cyber-header .subtitle {
font-size: 0.85rem;
color: var(--neon-magenta);
margin-top: 0.5rem;
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 {
display: flex;
justify-content: center;
gap: 2rem;
margin-top: 1rem;
font-size: 0.75rem;
color: var(--text-dim);
flex-wrap: wrap;
}
.status-bar span::before { content: '▸ '; color: var(--neon-cyan); }
.category-filter {
display: flex;
justify-content: center;
gap: 0.5rem;
padding: 1rem;
flex-wrap: wrap;
}
.category-btn {
background: rgba(0,255,255,0.1);
border: 1px solid var(--neon-cyan);
color: var(--neon-cyan);
padding: 0.4rem 1rem;
font-family: 'Orbitron', sans-serif;
font-size: 0.7rem;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
}
.category-btn:hover, .category-btn.active {
background: var(--neon-cyan);
color: var(--dark-bg);
box-shadow: 0 0 15px var(--neon-cyan);
}
.feed-container {
max-width: 900px;
margin: 0 auto;
padding: 1.5rem;
}
.feed-item {
background: linear-gradient(135deg, rgba(0,255,255,0.05) 0%, rgba(255,0,255,0.02) 100%);
border: 1px solid rgba(0,255,255,0.2);
border-left: 3px solid var(--neon-cyan);
margin-bottom: 1.2rem;
padding: 1.2rem;
transition: all 0.3s ease;
position: relative;
}
.feed-item.has-audio { border-left-color: var(--neon-magenta); }
.feed-item.has-video { border-left-color: var(--neon-yellow); }
.feed-item:hover {
border-color: var(--neon-magenta);
box-shadow: 0 0 20px rgba(0,255,255,0.2);
}
.feed-item .meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.feed-item .timestamp {
font-size: 0.7rem;
color: var(--neon-magenta);
text-transform: uppercase;
letter-spacing: 0.15em;
}
.feed-item .source-tag {
background: rgba(255,0,255,0.2);
border: 1px solid var(--neon-magenta);
padding: 0.15rem 0.5rem;
font-size: 0.6rem;
text-transform: uppercase;
}
.feed-item .media-tag {
background: rgba(255,255,0,0.2);
border: 1px solid var(--neon-yellow);
padding: 0.15rem 0.5rem;
font-size: 0.6rem;
text-transform: uppercase;
}
.feed-item .title {
font-family: 'Orbitron', sans-serif;
font-size: 1rem;
font-weight: 700;
color: var(--neon-cyan);
margin-bottom: 0.6rem;
line-height: 1.4;
}
.feed-item .title a {
color: inherit;
text-decoration: none;
}
.feed-item .title a:hover { text-shadow: 0 0 10px var(--neon-cyan); }
.feed-item .description {
font-size: 0.85rem;
color: var(--text-primary);
line-height: 1.5;
opacity: 0.85;
}
.audio-player {
margin-top: 12px;
}
.audio-player audio {
width: 100%;
height: 40px;
border-radius: 20px;
}
.cyber-footer {
text-align: center;
padding: 2rem;
border-top: 1px solid rgba(0,255,255,0.2);
font-size: 0.7rem;
color: var(--text-dim);
}
.cyber-footer a { color: var(--neon-cyan); text-decoration: none; }
</style>
</head>
<body>
<header class="cyber-header">
<h1>⚡ CYBERFEED ⚡</h1>
<p class="subtitle">NEURAL RSS MATRIX INTERFACE</p>
<div class="status-bar">
<span id="feed-count">FEEDS: --</span>
<span id="last-update">SYNC: --:--:--</span>
<span>STATUS: ONLINE</span>
</div>
</header>
<nav class="nav-bar">
<a href="/cyberfeed/">Dashboard</a>
<a href="/cyberfeed/timeline.html">📅 Timeline</a>
</nav>
<div class="category-filter">
<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('tech')">TECH</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>
</div>
<main class="feed-container" id="feed-items">
<div style="text-align:center; padding:40px; color: var(--text-dim);">
<p style="font-size:3rem;">🔮</p>
<p>Awaiting Neural Feed Connection...</p>
</div>
</main>
<footer class="cyber-footer">
<p>⚡ CYBERFEED v0.2 | Powered by <a href="https://cybermind.fr">CyberMind.FR</a> | SecuBox Module ⚡</p>
<p>░▒▓ JACK IN TO THE MATRIX ▓▒░</p>
</footer>
<script>
let feedData = [];
let currentFilter = 'all';
async function loadFeeds() {
try {
const resp = await fetch('/cyberfeed/feeds.json?' + Date.now());
feedData = await resp.json();
renderFeeds();
document.getElementById('feed-count').textContent = 'FEEDS: ' + feedData.length;
document.getElementById('last-update').textContent = 'SYNC: ' + new Date().toLocaleTimeString('fr-FR', {hour12: false});
} catch(e) {
console.error('Feed load error:', e);
}
}
function filterCategory(cat) {
currentFilter = cat;
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
event.target.classList.add('active');
renderFeeds();
}
function renderFeeds() {
const container = document.getElementById('feed-items');
const filtered = currentFilter === 'all' ? feedData : feedData.filter(f => f.category === currentFilter);
if (filtered.length === 0) {
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;
}
container.innerHTML = filtered.map(item => {
const hasMedia = item.media_type ? ' has-' + item.media_type : '';
let audioPlayer = '';
if (item.enclosure && item.media_type === 'audio') {
audioPlayer = '<div class="audio-player"><audio controls preload="none"><source src="' + item.enclosure + '" type="audio/mpeg">Audio non supporté</audio></div>';
}
let mediaTag = '';
if (item.media_type === 'audio') mediaTag = '<span class="media-tag">🎧 AUDIO</span>';
else if (item.media_type === 'video') mediaTag = '<span class="media-tag">📺 VIDEO</span>';
return '<article class="feed-item' + hasMedia + '" data-category="' + (item.category || 'custom') + '">' +
'<div class="meta">' +
'<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();
setInterval(loadFeeds, 300000);
</script>
</body>
</html>
HTMLEOF
}
# === STATUS ===
get_status() {
local enabled=$(uci -q get cyberfeed.main.enabled || echo 0)
local feed_count=0
local item_count=0
local last_sync=0
if [ -f "${OUTPUT_DIR}/feeds.json" ]; then
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)
fi
if [ -f "$CONFIG_FILE" ]; then
feed_count=$(grep -v "^#" "$CONFIG_FILE" 2>/dev/null | grep -c "|" || echo 0)
fi
cat << EOF
{
"enabled": $enabled,
"feed_count": $feed_count,
"item_count": $item_count,
"last_sync": $last_sync,
"has_timeline": $([ -f "${OUTPUT_DIR}/timeline.html" ] && echo "true" || echo "false")
}
EOF
}
# === SYNC FEEDS ===
sync_feeds() {
load_config
init_history
mkdir -p "$CACHE_DIR" "$OUTPUT_DIR"
if [ ! -f "$CONFIG_FILE" ]; then
echo '[]' > "${OUTPUT_DIR}/feeds.json"
generate_html "${OUTPUT_DIR}/feeds.json"
return 0
fi
local json_items="["
local feed_count=0
while IFS='|' read -r name url type category || [ -n "$name" ]; do
case "$name" in
''|\#*) continue ;;
esac
[ -z "$category" ] && category="custom"
echo "📡 Fetching: $name" >&2
raw_xml=$(fetch_feed "$url" "$name")
if [ -n "$raw_xml" ]; then
parsed=$(parse_rss "$raw_xml" "$name" "$category")
if [ -n "$parsed" ]; then
json_items="${json_items}${parsed}"
feed_count=$((feed_count + 1))
fi
fi
done < "$CONFIG_FILE"
json_items=$(echo "$json_items" | sed 's/,$//')
json_items="${json_items}]"
echo "$json_items" > "${OUTPUT_DIR}/feeds.json"
generate_html "${OUTPUT_DIR}/feeds.json"
# 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/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 "║ ⚡ CYBERFEED v0.2 SYNC COMPLETE ⚡ ║"
echo "╠═══════════════════════════════════════════════════════════════╣"
printf "║ 📊 Feeds processed: %-36s ║\n" "$feed_count"
echo "║ 📁 Output: /www/cyberfeed/ ║"
echo "║ 📅 Timeline: /www/cyberfeed/timeline.html ║"
echo "╚═══════════════════════════════════════════════════════════════╝"
}
# === LIST FEEDS ===
list_feeds() {
if [ ! -f "$CONFIG_FILE" ]; then
echo "[]"
return
fi
echo "["
local first=1
while IFS='|' read -r name url type category || [ -n "$name" ]; do
case "$name" in
''|\#*) continue ;;
esac
[ "$first" = "1" ] || echo ","
first=0
printf '{"name":"%s","url":"%s","type":"%s","category":"%s"}' \
"$name" "$url" "${type:-rss}" "${category:-custom}"
done < "$CONFIG_FILE"
echo "]"
}
# === ADD FEED ===
add_feed() {
local name="$1"
local url="$2"
local type="${3:-rss}"
local category="${4:-custom}"
[ -z "$name" ] || [ -z "$url" ] && {
echo '{"success":false,"error":"Name and URL required"}'
return 1
}
if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then
echo '{"success":false,"error":"Feed already exists"}'
return 1
fi
echo "${name}|${url}|${type}|${category}" >> "$CONFIG_FILE"
echo '{"success":true}'
}
# === DELETE FEED ===
delete_feed() {
local name="$1"
[ -z "$name" ] && {
echo '{"success":false,"error":"Name required"}'
return 1
}
if grep -q "^${name}|" "$CONFIG_FILE" 2>/dev/null; then
sed -i "/^${name}|/d" "$CONFIG_FILE"
rm -f "${CACHE_DIR}/${name}.xml"
echo '{"success":true}'
else
echo '{"success":false,"error":"Feed not found"}'
return 1
fi
}
# === MAIN ===
case "$1" in
sync)
sync_feeds
;;
status)
get_status
;;
list)
list_feeds
;;
add)
add_feed "$2" "$3" "$4" "$5"
;;
delete)
delete_feed "$2"
;;
*)
echo "Usage: $0 {sync|status|list|add|delete}"
echo ""
echo "CyberFeed v0.2 - Enhanced RSS Aggregator"
echo ""
echo "Commands:"
echo " sync Fetch feeds, generate HTML + Timeline"
echo " status Show service status (JSON)"
echo " list List configured feeds (JSON)"
echo " add NAME URL [TYPE] [CATEGORY]"
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