Files
xTeVe/src/screen.go
T
nathan 3bbea8e952 Phase 2b: server/client timeouts, atomic state writes, log mutex, panic and shutdown fixes
- http.Server with ReadHeaderTimeout/IdleTimeout (no read/write timeouts: /stream/ is long-lived)
- shared outbound clients: providerHTTPClient (5m), apiHTTPClient (30s), imgcache client (30s)
- imgcache: download outside the lock, per-item helper, cache URL uses the file name not the fs path
- writeFileAtomic (temp + fsync + rename) for settings/xepg/pms/urls/authentication JSON
- one package-level logMu for WebScreenLog and notifications; ring buffer keeps the newest lines
- notifications evict the oldest instead of random map entries
- xepg XMLTV file removal rebuilt after the loop; data.go range-mutation removed
- API handler returns after error body; WS request/response fresh per command
- checked type assertions in data/backup/provider/screen
- SIGINT/SIGTERM handled in main via src.Shutdown(); fatal paths exit 1
2026-09-26 13:17:07 +10:00

523 lines
12 KiB
Go

package src
import (
"fmt"
"log"
"runtime"
"strconv"
"strings"
"sync"
"time"
)
// logMu guards WebScreenLog (its slice and counters) and System.Notification.
// Log lines are appended from every goroutine (buffer, maintenance, web
// handlers) and read by the websocket handler.
var logMu sync.Mutex
// logAppend : Appends a timestamped line to the in-memory log and trims it
// to Settings.LogEntriesRAM.
func logAppend(logMsg string) {
logMu.Lock()
defer logMu.Unlock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
logCleanUpLocked()
}
// logAppendCounted : Appends a warning / error line and bumps the matching
// counter. Like the original code this does not trim the log; the next
// info / debug line does.
func logAppendCounted(logMsg string, isError bool) {
logMu.Lock()
defer logMu.Unlock()
WebScreenLog.Log = append(WebScreenLog.Log, time.Now().Format("2006-01-02 15:04:05")+" "+logMsg)
if isError {
WebScreenLog.Errors++
} else {
WebScreenLog.Warnings++
}
}
// resetWebScreenLog : Clears the in-memory log and its counters.
func resetWebScreenLog() {
logMu.Lock()
defer logMu.Unlock()
WebScreenLog.Log = make([]string, 0)
WebScreenLog.Errors = 0
WebScreenLog.Warnings = 0
}
// webScreenLogSnapshot : Copy of the log for the web client. The slice is
// copied so the JSON encoder never races with a concurrent append.
func webScreenLogSnapshot() (snapshot WebScreenLogStruct) {
logMu.Lock()
defer logMu.Unlock()
snapshot = WebScreenLog
snapshot.Log = make([]string, len(WebScreenLog.Log))
copy(snapshot.Log, WebScreenLog.Log)
return
}
// notificationsSnapshot : Copy of the notifications map for the web client.
func notificationsSnapshot() (snapshot map[string]Notification) {
logMu.Lock()
defer logMu.Unlock()
if System.Notification == nil {
return nil
}
snapshot = make(map[string]Notification, len(System.Notification))
for k, v := range System.Notification {
snapshot[k] = v
}
return
}
func showInfo(str string) {
if System.Flag.Info {
return
}
var max = 23
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
if len(msg) == 2 {
for i := length; i < max; i++ {
space = space + " "
}
msg[0] = msg[0] + ":" + space
var logMsg = fmt.Sprintf("[%s] %s%s", System.Name, msg[0], msg[1])
printLogOnScreen(logMsg, "info")
logAppend(strings.Replace(logMsg, " ", "&nbsp;", -1))
}
}
func showDebug(str string, level int) {
if System.Flag.Debug < level {
return
}
var max = 23
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
if len(msg) == 2 {
for i := length; i < max; i++ {
space = space + " "
}
msg[0] = msg[0] + ":" + space
var logMsg = fmt.Sprintf("[DEBUG] %s%s", msg[0], msg[1])
printLogOnScreen(logMsg, "debug")
logAppend(strings.Replace(logMsg, " ", "&nbsp;", -1))
}
}
func showHighlight(str string) {
var max = 23
var msg = strings.SplitN(str, ":", 2)
var length = len(msg[0])
var space string
var notification Notification
notification.Type = "info"
if len(msg) == 2 {
for i := length; i < max; i++ {
space = space + " "
}
msg[0] = msg[0] + ":" + space
var logMsg = fmt.Sprintf("[%s] %s%s", System.Name, msg[0], msg[1])
printLogOnScreen(logMsg, "highlight")
}
notification.Type = "info"
// Messages without a "key:value" prefix are shown as they are.
notification.Message = msg[len(msg)-1]
addNotification(notification)
}
func showWarning(errCode int) {
var errMsg = getErrMsg(errCode)
var logMsg = fmt.Sprintf("[%s] [WARNING] %s", System.Name, errMsg)
printLogOnScreen(logMsg, "warning")
logAppendCounted(logMsg, false)
}
// ShowError : Zeigt die Fehlermeldungen in der Konsole
func ShowError(err error, errCode int) {
var errMsg = getErrMsg(errCode)
var logMsg = fmt.Sprintf("[%s] [ERROR] %s (%s) - EC: %d", System.Name, err, errMsg, errCode)
printLogOnScreen(logMsg, "error")
logAppendCounted(logMsg, true)
}
func printLogOnScreen(logMsg string, logType string) {
var color string
switch logType {
case "info":
color = "\033[0m"
case "debug":
color = "\033[35m"
case "highlight":
color = "\033[32m"
case "warning":
color = "\033[33m"
case "error":
color = "\033[31m"
}
switch runtime.GOOS {
case "windows":
log.Println(logMsg)
default:
fmt.Print(color)
log.Println(logMsg)
fmt.Print("\033[0m")
}
}
// logCleanUpLocked : Trims the log to the newest Settings.LogEntriesRAM
// lines and recounts warnings / errors. The caller must hold logMu.
func logCleanUpLocked() {
WebScreenLog.Log = keepLastEntries(WebScreenLog.Log, Settings.LogEntriesRAM)
var logs = WebScreenLog.Log
WebScreenLog.Warnings = 0
WebScreenLog.Errors = 0
for _, log := range logs {
if strings.Contains(log, "WARNING") {
WebScreenLog.Warnings++
}
if strings.Contains(log, "ERROR") {
WebScreenLog.Errors++
}
}
}
// keepLastEntries : Returns the last max entries of logs (a copy, so the
// old backing array does not keep growing). A max <= 0 keeps nothing, which
// matches the original behaviour for an unset LogEntriesRAM.
func keepLastEntries(logs []string, max int) []string {
if max < 0 {
max = 0
}
if len(logs) <= max {
return logs
}
var tmp = make([]string, max)
copy(tmp, logs[len(logs)-max:])
return tmp
}
// Fehlercodes
func getErrMsg(errCode int) (errMsg string) {
switch errCode {
case 0:
return
// Errors
case 1001:
errMsg = "Web server could not be started."
case 1002:
errMsg = "No local IP address found."
case 1003:
errMsg = "Invalid xml"
case 1004:
errMsg = "File not found"
case 1005:
errMsg = "Invalid M3U file, an extended M3U file is required."
case 1006:
errMsg = "No playlist!"
case 1007:
errMsg = "XEPG requires an XMLTV file."
case 1010:
errMsg = "Invalid file compression"
case 1011:
errMsg = fmt.Sprintf("Data is corrupt or unavailable, %s now uses an older version of this file", System.Name)
case 1012:
errMsg = "Invalid formatting of the time"
case 1013:
errMsg = fmt.Sprintf("Invalid settings file (settings.json), file must be at least version %s", System.Compatibility)
case 1014:
errMsg = "Invalid filter rule"
case 1020:
errMsg = "Data could not be saved, invalid keyword"
// Datenbank Update
case 1030:
errMsg = fmt.Sprintf("Invalid settings file (%s)", System.File.Settings)
case 1031:
errMsg = "Database error. The database version of your settings is not compatible with this version."
// M3U Parser
case 1050:
errMsg = "Invalid duration specification in the M3U8 playlist."
case 1060:
errMsg = "Invalid characters found in the tvg parameters, streams with invalid parameters were skipped."
// Dateisystem
case 1070:
errMsg = "Folder could not be created."
case 1071:
errMsg = "File could not be created"
case 1072:
errMsg = "File not found"
// Backup
case 1090:
errMsg = "Automatic backup failed"
// Websockets
case 1100:
errMsg = "WebUI build error"
case 1101:
errMsg = "WebUI request error"
case 1102:
errMsg = "WebUI response error"
// PMS Guide Numbers
case 1200:
errMsg = "Could not create file"
// Stream URL Fehler
case 1201:
errMsg = "Plex stream error"
case 1202:
errMsg = "Steaming URL could not be found in any playlist"
case 1203:
errMsg = "Steaming URL could not be found in any playlist"
case 1204:
errMsg = "Streaming was stopped by third party transcoder (FFmpeg / VLC)"
// Warnings
case 2000:
errMsg = fmt.Sprintf("Plex can not handle more than %d streams. If you do not use Plex, you can ignore this warning.", System.PlexChannelLimit)
case 2001:
errMsg = fmt.Sprintf("%s has loaded more than %d streams. Use the filter to reduce the number of streams.", System.Name, System.UnfilteredChannelLimit)
case 2002:
errMsg = "PMS can not play m3u8 streams"
case 2003:
errMsg = "PMS can not play streams over RTSP."
case 2004:
errMsg = "Buffer is disabled for this stream."
case 2005:
errMsg = "There are no channels mapped, use the mapping menu to assign EPG data to the channels."
case 2010:
errMsg = "No valid streaming URL"
case 2020:
errMsg = "FFmpeg binary was not found. Check the FFmpeg binary path in the xTeVe settings."
case 2021:
errMsg = "VLC binary was not found. Check the VLC path binary in the xTeVe settings."
case 2099:
errMsg = "Updates have been disabled by the developer"
// Tuner
case 2105:
errMsg = "The number of tuners has changed, you have to delete " + System.Name + " in Plex / Emby HDHR and set it up again."
case 2106:
errMsg = "This function is only available with XEPG as EPG source"
case 2110:
errMsg = "Don't run this as Root!"
case 2300:
errMsg = "No channel logo found in the XMLTV or M3U file."
case 2301:
errMsg = "XMLTV file no longer available, channel has been deactivated."
case 2302:
errMsg = "Channel ID in the XMLTV file has changed. Channel has been deactivated."
// Benutzerauthentifizierung
case 3000:
errMsg = "Database for user authentication could not be initialized."
case 3001:
errMsg = "The user has no authorization to load the channels."
// Buffer
case 4000:
errMsg = "Connection to streaming source was interrupted."
case 4001:
errMsg = "Too many errors connecting to the provider. Streaming is canceled."
case 4002:
errMsg = "New URL for the redirect to the streaming server is missing"
case 4003:
errMsg = "Server sends an incompatible content-type"
case 4004:
errMsg = "This error message comes from the provider"
case 4005:
errMsg = "Temporary buffer files could not be deleted"
case 4006:
errMsg = "Server connection timeout"
// Buffer (M3U8)
case 4050:
errMsg = "Invalid M3U8 file"
case 4051:
errMsg = "#EXTM3U header is missing"
// Caching
case 4100:
errMsg = "Unknown content type for downloaded image"
case 4101:
errMsg = "Invalid URL, original URL is used for this image"
// API
case 5000:
errMsg = "Invalid API command"
// Update Server
case 6001:
errMsg = "Ivalid key"
case 6002:
errMsg = "Update failed"
case 6003:
errMsg = "Update server not available"
case 6004:
errMsg = "xTeVe update available"
default:
errMsg = fmt.Sprintf("Unknown error / warning (%d)", errCode)
}
return errMsg
}
// maxNotifications : Number of notifications kept for the web interface.
const maxNotifications = 10
func addNotification(notification Notification) (err error) {
var t = time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond))
notification.Time = strconv.FormatInt(t, 10)
insertNotification(notification)
return
}
// insertNotification : Stores the notification under its Time key and evicts
// the oldest ones once more than maxNotifications are kept.
func insertNotification(notification Notification) {
notification.New = true
if len(notification.Headline) == 0 {
notification.Headline = strings.ToUpper(notification.Type)
}
logMu.Lock()
defer logMu.Unlock()
if len(System.Notification) == 0 {
System.Notification = make(map[string]Notification)
}
System.Notification[notification.Time] = notification
for len(System.Notification) > maxNotifications {
delete(System.Notification, oldestNotificationKey(System.Notification))
}
}
// oldestNotificationKey : Key of the notification with the smallest
// timestamp. Keys are millisecond timestamps as decimal strings; anything
// that does not parse sorts before everything that does.
func oldestNotificationKey(notifications map[string]Notification) (oldest string) {
var oldestTime int64
var found bool
for key, n := range notifications {
t, err := strconv.ParseInt(n.Time, 10, 64)
if err != nil {
return key
}
if !found || t < oldestTime {
oldest = key
oldestTime = t
found = true
}
}
return
}