commits
tags
import re
import urllib.request as _urlreq
import xml.etree.ElementTree as _ET
from datetime import datetime as _dt, timedelta as _td
from flask import Blueprint, jsonify, request, abort
import db
import extensions
from auth import login_required
bp = Blueprint('ticker', __name__, url_prefix='/api')
_HEX_COLOR = re.compile(r'^#[0-9a-fA-F]{6}$')
_rss_cache = {'url': None, 'items': [], 'fetched_at': None}
def _fetch_rss(url, max_items=30):
global _rss_cache
now = _dt.now()
if (_rss_cache['url'] == url and _rss_cache['fetched_at']
and now - _rss_cache['fetched_at'] < _td(minutes=2)):
return _rss_cache['items']
try:
req = _urlreq.Request(url, headers={'User-Agent': 'KioskRSS/1.0'})
with _urlreq.urlopen(req, timeout=10) as r:
raw = r.read()
root = _ET.fromstring(raw)
items = []
# RSS 2.0
for item in root.findall('.//item'):
t = (item.findtext('title') or '').strip()
if t:
items.append(t)
# Atom
if not items:
ns = {'a': 'http://www.w3.org/2005/Atom'}
for entry in root.findall('.//a:entry', ns):
t = (entry.findtext('a:title', namespaces=ns) or '').strip()
if t:
items.append(t)
_rss_cache = {'url': url, 'items': items[:max_items], 'fetched_at': now}
except Exception:
pass # keep stale cache on error
return _rss_cache['items']
@bp.route('/ticker/rss')
def get_ticker_rss():
state = db.get_state()
url = state.get('ticker_rss_url', '').strip()
if not url:
return jsonify({'items': [], 'error': 'Ingen RSS-URL konfigurerad'})
items = _fetch_rss(url)
return jsonify({'items': items})
@bp.route('/ticker', methods=['GET'])
def get_ticker():
state = db.get_state()
return jsonify({
'enabled': state.get('ticker_enabled', '0') == '1',
'source': state.get('ticker_source', 'text'),
'text': state.get('ticker_text', ''),
'rss_url': state.get('ticker_rss_url', ''),
'rss_interval': int(state.get('ticker_rss_interval', '10')),
'pps': int(state.get('ticker_pps', '150')),
'mode': state.get('ticker_mode', 'sequential'),
'separator': state.get('ticker_separator', '◆'),
'position': state.get('ticker_position', 'bottom'),
'text_color': state.get('ticker_text_color', '#ffffff'),
'font_size': float(state.get('ticker_font_size', '2.0')),
'font_family': state.get('ticker_font_family', 'sans-serif'),
'bar_color': state.get('ticker_bar_color', '#000000'),
'bar_opacity': int(state.get('ticker_bar_opacity', '72')),
'bar_padding': float(state.get('ticker_bar_padding', '0.45')),
})
@bp.route('/ticker', methods=['POST'])
@login_required
def set_ticker():
data = request.get_json()
if not data:
abort(400)
enabled = bool(data.get('enabled', False))
source = 'rss' if data.get('source') == 'rss' else 'text'
text = str(data.get('text', ''))
rss_url = str(data.get('rss_url', ''))
rss_interval = max(1, min(1440, int(data.get('rss_interval', 10))))
pps = max(30, min(800, int(data.get('pps', 150))))
mode = 'continuous' if data.get('mode') == 'continuous' else 'sequential'
separator = str(data.get('separator', '◆'))[:20]
position = 'top' if data.get('position') == 'top' else 'bottom'
text_color = str(data.get('text_color', '#ffffff'))
font_size = max(0.5, min(6.0, float(data.get('font_size', 2.0))))
font_family = str(data.get('font_family', 'sans-serif'))
bar_color = str(data.get('bar_color', '#000000'))
bar_opacity = max(0, min(100, int(data.get('bar_opacity', 72))))
bar_padding = max(0.1, min(2.0, float(data.get('bar_padding', 0.45))))
if not _HEX_COLOR.match(text_color): text_color = '#ffffff'
if not _HEX_COLOR.match(bar_color): bar_color = '#000000'
db.set_state('ticker_enabled', '1' if enabled else '0')
db.set_state('ticker_source', source)
db.set_state('ticker_text', text)
db.set_state('ticker_rss_url', rss_url)
db.set_state('ticker_rss_interval', str(rss_interval))
db.set_state('ticker_pps', str(pps))
db.set_state('ticker_mode', mode)
db.set_state('ticker_separator', separator)
db.set_state('ticker_position', position)
db.set_state('ticker_text_color', text_color)
db.set_state('ticker_font_size', str(font_size))
db.set_state('ticker_font_family', font_family)
db.set_state('ticker_bar_color', bar_color)
db.set_state('ticker_bar_opacity', str(bar_opacity))
db.set_state('ticker_bar_padding', str(bar_padding))
# Invalidate RSS cache when URL changes
if source == 'rss':
_rss_cache['fetched_at'] = None
payload = {
'enabled': enabled, 'source': source,
'text': text, 'rss_url': rss_url, 'rss_interval': rss_interval,
'pps': pps, 'mode': mode, 'separator': separator, 'position': position,
'text_color': text_color, 'font_size': font_size, 'font_family': font_family,
'bar_color': bar_color, 'bar_opacity': bar_opacity, 'bar_padding': bar_padding,
}
extensions.socketio.emit('ticker', payload)
return '', 204