Phase 0: remove auto-updater, pin Go 1.27.1, fix vet warnings, tidy ignores
continuous-integration/drone/push Build encountered an error

- Delete BinaryUpdate, internal/up2date, GitHub/Update structs and the
  xteveAutoUpdate / update.url settings (UI rows, en.json, defaults).
  Settings-schema migrations kept and moved to src/migrate.go.
- Drop kardianos/osext dependency.
- xteve.go version 0200 -> 0201 to match changelog; Drone now fails on drift.
- go.mod go 1.27.1; Dockerfile and Drone golang images pinned to 1.27.1.
- Fix four go vet unreachable-code warnings.
- .gitignore: .gocache/, agent.md, skill.md. .dockerignore: build context
  no longer includes caches, ts/, tasks/ or markdown except the changelog.
- Drone: publish :latest only from master; other branches publish a
  branch-named tag so a feature push cannot replace the deployed image.
- Add tasks/improvement-plan.md and tasks/todo.md.
- Regenerate src/webUI.go.
This commit is contained in:
2026-09-26 12:38:02 +10:00
parent f558a855ae
commit 51c7830067
28 changed files with 504 additions and 887 deletions
+8
View File
@@ -8,3 +8,11 @@ README-DEV.md
*.log
*.tmp
.gocache/
ts/
tasks/
agent.md
skill.md
*.md
!changelog-beta.md
+34 -2
View File
@@ -4,16 +4,18 @@ name: default
steps:
- name: go-test
image: cache.coadcorp.com/library/golang
image: cache.coadcorp.com/library/golang:1.27.1
commands:
- go mod download
- go test ./...
- name: go-build
image: cache.coadcorp.com/library/golang
image: cache.coadcorp.com/library/golang:1.27.1
commands:
- XTEVE_VERSION="$(grep -m1 '^#### ' changelog-beta.md | cut -d' ' -f2 | sed 's/-beta$//')"
- test -n "$XTEVE_VERSION" || (echo "Could not parse version from changelog-beta.md" && exit 1)
- SRC_VERSION="$(grep -m1 '^var Version = ' xteve.go | cut -d'"' -f2)"
- test "$SRC_VERSION" = "$XTEVE_VERSION" || (echo "Version drift: xteve.go has $SRC_VERSION, changelog-beta.md has $XTEVE_VERSION" && exit 1)
- echo "Building xTeVe version $XTEVE_VERSION from changelog-beta.md"
- go build -v -ldflags "-X main.Version=$XTEVE_VERSION" ./...
@@ -61,3 +63,33 @@ steps:
when:
event:
- push
branch:
- master
- name: docker-publish-branch
image: plugins/docker
environment:
XTEVE_UID: "1000"
XTEVE_GID: "1000"
settings:
registry: registry.coadcorp.com
repo: registry.coadcorp.com/nathan/xteve
dockerfile: Dockerfile
username: nathan
password:
from_secret: registry_password
tags:
- ${DRONE_BRANCH}
- ${DRONE_COMMIT_SHA}
build_args:
- TARGETOS=linux
- TARGETARCH=amd64
build_args_from_env:
- XTEVE_UID
- XTEVE_GID
when:
event:
- push
branch:
exclude:
- master
+4
View File
@@ -7,3 +7,7 @@ update_xteve*.sh
xteve
xteve.exe
de.json
.gocache/
agent.md
skill.md
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1.7
FROM golang:1.26-alpine AS builder
FROM golang:1.27.1-alpine AS builder
WORKDIR /src
RUN apk add --no-cache ca-certificates tzdata
+1 -2
View File
@@ -1,10 +1,9 @@
module xteve
go 1.25
go 1.27.1
require (
github.com/gorilla/websocket v1.5.3
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/koron/go-ssdp v0.1.0
)
-20
View File
@@ -1,28 +1,8 @@
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/koron/go-ssdp v0.0.2 h1:fL3wAoyT6hXHQlORyXUW4Q23kkQpJRgEAYcZB5BR71o=
github.com/koron/go-ssdp v0.0.2/go.mod h1:XoLfkAiA2KeZsYh4DbHxD7h3nR2AZNqVQOa+LJuqPYs=
github.com/koron/go-ssdp v0.1.0 h1:ckl5x5H6qSNFmi+wCuROvvGUu2FQnMbQrU95IHCcv3Y=
github.com/koron/go-ssdp v0.1.0/go.mod h1:GltaDBjtK1kemZOusWYLGotV0kBeEf59Bp0wtSB0uyU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+1 -1
View File
@@ -21,7 +21,7 @@ menuItems.push(new MainMenuItem("log", "{{.mainMenu.item.log}}", "log.png", "{{.
menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.png", "{{.mainMenu.headline.logout}}"));
// Kategorien für die Einstellungen
var settingsCategory = new Array();
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"));
-14
View File
@@ -241,17 +241,6 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "xteveAutoUpdate":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.xteveAutoUpdate.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createCheckbox(settingsKey);
input.checked = data;
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
case "api":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.api.title}}" + ":";
@@ -386,9 +375,6 @@ var SettingsCategory = /** @class */ (function () {
text = "{{.settings.authenticationAPI.description}}";
}
break;
case "xteveAutoUpdate":
text = "{{.settings.xteveAutoUpdate.description}}";
break;
case "backup.keep":
text = "{{.settings.backupKeep.description}}";
break;
-5
View File
@@ -406,11 +406,6 @@
"info_strict": "Strict (deactivate channels)",
"info_relaxed": "Relaxed (keep active / dummy guide)"
},
"xteveAutoUpdate":
{
"title": "Automatic update of xTeVe",
"description": "If a new version of xTeVe is available, it will be automatically installed. The updates are downloaded from GitHub."
},
"streamBuffering":
{
"title": "Stream Buffer",
-2
View File
@@ -730,8 +730,6 @@ func connectToStreamingServer(streamID int, playlistID string) {
}
defer resp.Body.Close()
}
defer resp.Body.Close()
+1 -7
View File
@@ -57,11 +57,6 @@ func Init() (err error) {
// Default Logeinträge, wird später von denen aus der settings.json überschrieben. Muss gemacht werden, damit die ersten Einträge auch im Log (webUI aangezeigt werden)
Settings.LogEntriesRAM = 500
// Variablen für den Update Prozess
//System.Update.Git = "https://github.com/xteve-project/xTeVe-Downloads/blob"
System.Update.Git = fmt.Sprintf("https://github.com/%s/%s/blob", System.GitHub.User, System.GitHub.Repo)
System.Update.Name = "xteve_2"
// Ordnerpfade festlegen
var tempFolder = os.TempDir() + string(os.PathSeparator) + System.AppName + string(os.PathSeparator)
tempFolder = getPlatformPath(strings.Replace(tempFolder, "//", "/", -1))
@@ -186,8 +181,7 @@ func Init() (err error) {
System.Branch = "master"
}
showInfo(fmt.Sprintf("GitHub:https://github.com/%s", System.GitHub.User))
showInfo(fmt.Sprintf("Git Branch:%s [%s]", System.Branch, System.GitHub.User))
showInfo(fmt.Sprintf("Branch:%s", System.Branch))
// Domainnamen setzten
setGlobalDomain(fmt.Sprintf("%s:%s", System.IPAddress, Settings.Port))
-2
View File
@@ -22,7 +22,6 @@ func ShowSystemInfo() {
fmt.Println(fmt.Sprintf("Version: %s %s.%s", System.Name, System.Version, System.Build))
fmt.Println(fmt.Sprintf("Branch: %s", System.Branch))
fmt.Println(fmt.Sprintf("GitHub: %s/%s | Git update = %t", System.GitHub.User, System.GitHub.Repo, System.GitHub.Update))
fmt.Println(fmt.Sprintf("Folder (config): %s", System.Folder.Config))
fmt.Println(fmt.Sprintf("Streams: %d / %d", len(Data.Streams.Active), len(Data.Streams.All)))
@@ -68,7 +67,6 @@ func ShowSystemInfo() {
println("---")
fmt.Println("Settings [General]")
fmt.Println(fmt.Sprintf("xTeVe Update: %t", Settings.XteveAutoUpdate))
fmt.Println(fmt.Sprintf("UUID: %s", Settings.UUID))
fmt.Println(fmt.Sprintf("Tuner (Plex / Emby): %d", Settings.Tuner))
fmt.Println(fmt.Sprintf("EPG Source: %s", Settings.EpgSource))
-129
View File
@@ -1,129 +0,0 @@
package up2date
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"runtime"
"time"
)
// ClientInfo : Information about the key (NAME OS, ARCH, UUID, KEY)
type ClientInfo struct {
Arch string `json:"arch,required"`
Branch string `json:"branch,required"`
CMD string `json:"cmd,omitempty"`
Name string `json:"name,required"`
OS string `json:"os,required"`
URL string `json:"url,required"`
Response ServerResponse `json:"response"`
}
// ServerResponse : Response from server after client request
type ServerResponse struct {
Status bool `json:"status,omitempty"`
Reason string `json:"reason,omitempty"`
Version string `json:"version,omitempty"`
UpdateBIN string `json:"update.url.bin,omitempty"`
UpdateZIP string `json:"update.url.zip,omitempty"`
Filename string `json:"filename.bin,omitempty"`
}
// Updater : Client infos
var Updater ClientInfo
// UpdateURL : URL for the new binary
var UpdateURL string
// Init : Init
func Init() {
Updater.OS = runtime.GOOS
Updater.Arch = runtime.GOARCH
}
// GetVersion : Information about the latest version
func GetVersion() (err error) {
Updater.CMD = "getVersion"
err = serverRequest()
return
}
func serverRequest() (err error) {
var serverResponse ServerResponse
jsonByte, err := json.MarshalIndent(Updater, "", " ")
if err == nil {
// Serververbindung prüfen
u, err := url.Parse(Updater.URL)
if err != nil {
return err
}
var server = u.Host
timeout := time.Duration(1 * time.Second)
_, err = net.DialTimeout("tcp", server, timeout)
if err != nil {
return err
}
// Check redirect 301 <---> 308
redirect, err := http.NewRequest("POST", Updater.URL, nil)
client := &http.Client{}
client.CheckRedirect = func(redirect *http.Request, via []*http.Request) error {
return errors.New("Redirect")
}
resp, err := client.Do(redirect)
if err != nil {
// Redirect
if resp.StatusCode >= 301 && resp.StatusCode <= 308 { //status code 301 <---> 308
Updater.URL = resp.Header.Get("Location")
} else {
return err
}
}
// ---
req, err := http.NewRequest("POST", Updater.URL, bytes.NewBuffer(jsonByte))
req.Header.Set("Content-Type", "application/json")
client = &http.Client{}
resp, err = client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
//fmt.Println(resp.StatusCode, Updater.URL, Updater.CMD)
err = fmt.Errorf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), Updater.URL)
return err
}
Updater.CMD = ""
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &serverResponse)
if err != nil {
return err
}
Updater.Response = serverResponse
}
return
}
-271
View File
@@ -1,271 +0,0 @@
package up2date
import (
"archive/zip"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"syscall"
"github.com/kardianos/osext"
)
// DoUpdate : Update binary
func DoUpdate(fileType, filenameBIN string) (err error) {
var url string
switch fileType {
case "bin":
url = Updater.Response.UpdateBIN
case "zip":
url = Updater.Response.UpdateZIP
}
switch runtime.GOOS {
case "windows":
filenameBIN = filenameBIN + ".exe"
}
if len(url) > 0 {
log.Println("["+strings.ToUpper(fileType)+"]", "New version ("+Updater.Name+"):", Updater.Response.Version)
// Download new binary
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
log.Println("["+strings.ToUpper(fileType)+"]", "Download new version...")
if resp.StatusCode != http.StatusOK {
log.Println("["+strings.ToUpper(fileType)+"]", "Download new version...OK")
return fmt.Errorf("bad status: %s", resp.Status)
}
// Change binary filename to .filename
binary, err := osext.Executable()
var filename = getFilenameFromPath(binary)
var path = getPlatformPath(binary)
var oldBinary = path + "_old_" + filename
var newBinary = binary
// ZIP
var tmpFolder = path + "tmp"
var tmpFile = tmpFolder + string(os.PathSeparator) + filenameBIN
//fmt.Println(binary, path+"."+filename)
os.Rename(newBinary, oldBinary)
// Save the new binary with the old file name
out, err := os.Create(binary)
if err != nil {
restorOldBinary(oldBinary, newBinary)
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
restorOldBinary(oldBinary, newBinary)
return err
}
// Update as a ZIP file
if fileType == "zip" {
log.Println("["+strings.ToUpper(fileType)+"]", "Update file:", filenameBIN)
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...")
err = extractZIP(binary, tmpFolder)
binary = newBinary
if err != nil {
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...ERROR")
restorOldBinary(oldBinary, newBinary)
return err
} else {
log.Println("["+strings.ToUpper(fileType)+"]", "Unzip ZIP file...OK")
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...")
err = copyFile(tmpFile, binary)
if err == nil {
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...OK")
} else {
log.Println("["+strings.ToUpper(fileType)+"]", "Copy binary file...ERROR")
restorOldBinary(oldBinary, newBinary)
return err
}
os.RemoveAll(tmpFolder)
}
}
// Set the permission
err = os.Chmod(binary, 0755)
// Close the new file !Windows
out.Close()
log.Println("["+strings.ToUpper(fileType)+"]", "Update Successful")
// Restart binary (Windows)
if runtime.GOOS == "windows" {
bin, err := os.Executable()
if err != nil {
restorOldBinary(oldBinary, newBinary)
return err
}
var pid = os.Getpid()
var process, _ = os.FindProcess(pid)
if proc, err := start(bin); err == nil {
os.RemoveAll(oldBinary)
process.Kill()
proc.Wait()
} else {
restorOldBinary(oldBinary, newBinary)
}
} else {
// Restart binary (Linux and UNIX)
file, _ := osext.Executable()
os.RemoveAll(oldBinary)
err = syscall.Exec(file, os.Args, os.Environ())
if err != nil {
restorOldBinary(oldBinary, newBinary)
log.Fatal(err)
return err
}
}
}
return
}
func start(args ...string) (p *os.Process, err error) {
if args[0], err = exec.LookPath(args[0]); err == nil {
//fmt.Println(args[0])
var procAttr os.ProcAttr
procAttr.Files = []*os.File{os.Stdin, os.Stdout, os.Stderr}
p, err := os.StartProcess(args[0], args, &procAttr)
if err == nil {
return p, nil
}
}
return nil, err
}
func restorOldBinary(oldBinary, newBinary string) {
os.RemoveAll(newBinary)
os.Rename(oldBinary, newBinary)
}
func getPlatformFile(filename string) string {
path, file := filepath.Split(filename)
var newPath = filepath.Dir(path)
var newFileName = newPath + string(os.PathSeparator) + file
return newFileName
}
func getFilenameFromPath(path string) string {
file := filepath.Base(path)
return file
}
func getPlatformPath(path string) string {
var newPath = filepath.Dir(path) + string(os.PathSeparator)
return newPath
}
func copyFile(src, dst string) (err error) {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Close()
}
func extractZIP(archive, target string) (err error) {
reader, err := zip.OpenReader(archive)
if err != nil {
return err
}
if err := os.MkdirAll(target, 0755); err != nil {
return err
}
for _, file := range reader.File {
path := filepath.Join(target, file.Name)
if file.FileInfo().IsDir() {
os.MkdirAll(path, file.Mode())
continue
}
fileReader, err := file.Open()
if err != nil {
return err
}
defer fileReader.Close()
targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
defer targetFile.Close()
if _, err := io.Copy(targetFile, fileReader); err != nil {
return err
}
}
return
}
-9
View File
@@ -1,7 +1,6 @@
package src
import (
"fmt"
"math/rand"
"time"
)
@@ -10,7 +9,6 @@ import (
func InitMaintenance() (err error) {
rand.Seed(time.Now().Unix())
System.TimeForAutoUpdate = fmt.Sprintf("0%d%d", randomTime(0, 2), randomTime(10, 59))
go maintenance()
@@ -64,18 +62,11 @@ func maintenance() {
}
// Update xTeVe (Binary)
if System.TimeForAutoUpdate == t.Format("1504") {
BinaryUpdate()
}
}
time.Sleep(60 * time.Second)
}
return
}
func randomTime(min, max int) int {
+156
View File
@@ -0,0 +1,156 @@
package src
import (
"errors"
"fmt"
"reflect"
)
// conditionalUpdateChanges : Settings-schema migrations applied at start-up.
// (Binary self-update was removed; the Docker image is the only distribution channel.)
func conditionalUpdateChanges() (err error) {
checkVersion:
settingsMap, err := loadJSONFileToMap(System.File.Settings)
if err != nil || len(settingsMap) == 0 {
return
}
if settingsVersion, ok := settingsMap["version"].(string); ok {
if settingsVersion > System.DBVersion {
showInfo("Settings DB Version:" + settingsVersion)
showInfo("System DB Version:" + System.DBVersion)
err = errors.New(getErrMsg(1031))
return
}
// Letzte Kompatible Version (1.4.4)
if settingsVersion < System.Compatibility {
err = errors.New(getErrMsg(1013))
return
}
switch settingsVersion {
case "1.4.4":
// UUID Wert in xepg.json setzen
err = setValueForUUID()
if err != nil {
return
}
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert
if oldFilter, ok := settingsMap["filter"].([]any); ok {
var newFilterMap = convertToNewFilter(oldFilter)
settingsMap["filter"] = newFilterMap
settingsMap["version"] = "2.0.0"
err = saveMapToJSONFile(System.File.Settings, settingsMap)
if err != nil {
return
}
goto checkVersion
} else {
err = errors.New(getErrMsg(1030))
return
}
case "2.0.0":
if oldBuffer, ok := settingsMap["buffer"].(bool); ok {
var newBuffer string
switch oldBuffer {
case true:
newBuffer = "xteve"
case false:
newBuffer = "-"
}
settingsMap["buffer"] = newBuffer
settingsMap["version"] = "2.1.0"
err = saveMapToJSONFile(System.File.Settings, settingsMap)
if err != nil {
return
}
goto checkVersion
} else {
err = errors.New(getErrMsg(1030))
return
}
case "2.1.0":
// Falls es in einem späteren Update Änderungen an der Datenbank gibt, geht es hier weiter
break
}
} else {
// settings.json ist zu alt (älter als Version 1.4.4)
err = errors.New(getErrMsg(1013))
}
return
}
func convertToNewFilter(oldFilter []any) (newFilterMap map[int]any) {
newFilterMap = make(map[int]any)
switch reflect.TypeFor[[]any]().Kind() {
case reflect.Slice:
s := reflect.ValueOf(oldFilter)
for i := 0; i < s.Len(); i++ {
var newFilter FilterStruct
newFilter.Active = true
newFilter.Name = fmt.Sprintf("Custom filter %d", i+1)
newFilter.Filter = s.Index(i).Interface().(string)
newFilter.Type = "custom-filter"
newFilter.CaseSensitive = false
newFilterMap[i] = newFilter
}
}
return
}
func setValueForUUID() (err error) {
xepg, err := loadJSONFileToMap(System.File.XEPG)
for _, c := range xepg {
var xepgChannel = c.(map[string]any)
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok {
if value, ok := xepgChannel[uuidKey].(string); ok {
if len(value) > 0 {
xepgChannel["_uuid.value"] = value
}
}
}
}
err = saveMapToJSONFile(System.File.XEPG, xepg)
return
}
-21
View File
@@ -77,7 +77,6 @@ type SystemStruct struct {
Name string
OS string
ScanInProgress int
TimeForAutoUpdate string
Notification map[string]Notification
@@ -89,18 +88,6 @@ type SystemStruct struct {
XML string
}
GitHub struct {
Branch string
Repo string
Update bool
User string
}
Update struct {
Git string
Name string
}
URLBase string
UDPxy string
Version string
@@ -109,12 +96,6 @@ type SystemStruct struct {
}
}
// GitStruct : Updateinformationen von GitHub
type GitStruct struct {
Filename string `json:"filename"`
Version string `json:"version"`
}
// DataStruct : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
type DataStruct struct {
Cache struct {
@@ -295,14 +276,12 @@ type SettingsStruct struct {
TempPath string `json:"temp.path"`
Tuner int `json:"tuner"`
Update []string `json:"update"`
UpdateURL string `json:"update.url,omitempty"`
UserAgent string `json:"user.agent"`
UUID string `json:"uuid"`
UDPxy string `json:"udpxy"`
Version string `json:"version"`
XepgMissingEPGMode string `json:"xepg.missing.epg.mode"`
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
XteveAutoUpdate bool `json:"xteveAutoUpdate"`
WizardCompleted bool `json:"wizard.completed"`
}
-1
View File
@@ -45,7 +45,6 @@ type RequestStruct struct {
UserAgent *string `json:"user.agent,omitempty"`
XepgMissingEPGMode *string `json:"xepg.missing.epg.mode,omitempty"`
XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"`
XteveAutoUpdate *bool `json:"xteveAutoUpdate,omitempty"`
SchemeM3U *string `json:"scheme.m3u,omitempty"`
SchemeXML *string `json:"scheme.xml,omitempty"`
} `json:"settings"`
-1
View File
@@ -152,7 +152,6 @@ func loadSettings() (settings SettingsStruct, err error) {
defaults["uuid"] = createUUID()
defaults["udpxy"] = ""
defaults["version"] = System.DBVersion
defaults["xteveAutoUpdate"] = true
defaults["wizard.completed"] = !freshInstall
var defaultTempPath = System.Folder.Temp
if len(os.Getenv("XTEVE_CONFIG")) > 0 {
-307
View File
@@ -1,307 +0,0 @@
package src
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
up2date "xteve/src/internal/up2date/client"
"reflect"
)
// BinaryUpdate : Binary Update Prozess. Git Branch master und beta wird von GitHub geladen.
func BinaryUpdate() (err error) {
if System.GitHub.Update == false {
showWarning(2099)
return
}
var debug string
var updater = &up2date.Updater
updater.Name = System.Update.Name
updater.Branch = System.Branch
up2date.Init()
switch System.Branch {
// Update von GitHub
case "master", "beta":
var gitInfo = fmt.Sprintf("%s/%s/info.json?raw=true", System.Update.Git, System.Branch)
var zipFile = fmt.Sprintf("%s/%s/%s_%s_%s.zip?raw=true", System.Update.Git, System.Branch, System.AppName, System.OS, System.ARCH)
var body []byte
var git GitStruct
resp, err := http.Get(gitInfo)
if err != nil {
ShowError(err, 6003)
return nil
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode == 404 {
err = fmt.Errorf("Update Server: %s (%s)", http.StatusText(resp.StatusCode), gitInfo)
ShowError(err, 6003)
return nil
}
err = fmt.Errorf("%d: %s (%s)", resp.StatusCode, http.StatusText(resp.StatusCode), gitInfo)
return err
}
body, err = ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &git)
if err != nil {
return err
}
updater.Response.Status = true
updater.Response.UpdateZIP = zipFile
updater.Response.Version = git.Version
updater.Response.Filename = git.Filename
// Update vom eigenen Server
default:
updater.URL = Settings.UpdateURL
if len(updater.URL) == 0 {
showInfo(fmt.Sprintf("Update URL:No server URL specified, update will not be performed. Branch: %s", System.Branch))
return
}
showInfo("Update URL:" + updater.URL)
fmt.Println("-----------------")
// Versionsinformationen vom Server laden
err = up2date.GetVersion()
if err != nil {
debug = err.Error()
showDebug(debug, 1)
return nil
}
if len(updater.Response.Reason) > 0 {
err = fmt.Errorf("Update Server: %s", updater.Response.Reason)
ShowError(err, 6002)
return nil
}
}
var currentVersion = System.Version + "." + System.Build
// Versionsnummer überprüfen
if updater.Response.Version > currentVersion && updater.Response.Status == true {
if Settings.XteveAutoUpdate == true {
// Update durchführen
var fileType, url string
showInfo(fmt.Sprintf("Update Available:Version: %s", updater.Response.Version))
switch System.Branch {
// Update von GitHub
case "master", "beta":
showInfo(fmt.Sprintf("Update Server:GitHub"))
// Update vom eigenen Server
default:
showInfo(fmt.Sprintf("Update Server:%s", Settings.UpdateURL))
}
showInfo(fmt.Sprintf("Start Update:Branch: %s", updater.Branch))
// Neue Version als BIN Datei herunterladen
if len(updater.Response.UpdateBIN) > 0 {
url = updater.Response.UpdateBIN
fileType = "bin"
}
// Neue Version als ZIP Datei herunterladen
if len(updater.Response.UpdateZIP) > 0 {
url = updater.Response.UpdateZIP
fileType = "zip"
}
if len(url) > 0 {
err = up2date.DoUpdate(fileType, updater.Response.Filename)
if err != nil {
ShowError(err, 6002)
}
}
} else {
// Hinweis ausgeben
showWarning(6004)
}
}
return nil
}
func conditionalUpdateChanges() (err error) {
checkVersion:
settingsMap, err := loadJSONFileToMap(System.File.Settings)
if err != nil || len(settingsMap) == 0 {
return
}
if settingsVersion, ok := settingsMap["version"].(string); ok {
if settingsVersion > System.DBVersion {
showInfo("Settings DB Version:" + settingsVersion)
showInfo("System DB Version:" + System.DBVersion)
err = errors.New(getErrMsg(1031))
return
}
// Letzte Kompatible Version (1.4.4)
if settingsVersion < System.Compatibility {
err = errors.New(getErrMsg(1013))
return
}
switch settingsVersion {
case "1.4.4":
// UUID Wert in xepg.json setzen
err = setValueForUUID()
if err != nil {
return
}
// Neuer Filter (WebUI). Alte Filtereinstellungen werden konvertiert
if oldFilter, ok := settingsMap["filter"].([]any); ok {
var newFilterMap = convertToNewFilter(oldFilter)
settingsMap["filter"] = newFilterMap
settingsMap["version"] = "2.0.0"
err = saveMapToJSONFile(System.File.Settings, settingsMap)
if err != nil {
return
}
goto checkVersion
} else {
err = errors.New(getErrMsg(1030))
return
}
case "2.0.0":
if oldBuffer, ok := settingsMap["buffer"].(bool); ok {
var newBuffer string
switch oldBuffer {
case true:
newBuffer = "xteve"
case false:
newBuffer = "-"
}
settingsMap["buffer"] = newBuffer
settingsMap["version"] = "2.1.0"
err = saveMapToJSONFile(System.File.Settings, settingsMap)
if err != nil {
return
}
goto checkVersion
} else {
err = errors.New(getErrMsg(1030))
return
}
case "2.1.0":
// Falls es in einem späteren Update Änderungen an der Datenbank gibt, geht es hier weiter
break
}
} else {
// settings.json ist zu alt (älter als Version 1.4.4)
err = errors.New(getErrMsg(1013))
}
return
}
func convertToNewFilter(oldFilter []any) (newFilterMap map[int]any) {
newFilterMap = make(map[int]any)
switch reflect.TypeFor[[]any]().Kind() {
case reflect.Slice:
s := reflect.ValueOf(oldFilter)
for i := 0; i < s.Len(); i++ {
var newFilter FilterStruct
newFilter.Active = true
newFilter.Name = fmt.Sprintf("Custom filter %d", i+1)
newFilter.Filter = s.Index(i).Interface().(string)
newFilter.Type = "custom-filter"
newFilter.CaseSensitive = false
newFilterMap[i] = newFilter
}
}
return
}
func setValueForUUID() (err error) {
xepg, err := loadJSONFileToMap(System.File.XEPG)
for _, c := range xepg {
var xepgChannel = c.(map[string]any)
if uuidKey, ok := xepgChannel["_uuid.key"].(string); ok {
if value, ok := xepgChannel[uuidKey].(string); ok {
if len(value) > 0 {
xepgChannel["_uuid.value"] = value
}
}
}
}
err = saveMapToJSONFile(System.File.XEPG, xepg)
return
}
+33 -32
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -410,7 +410,6 @@ func WS(w http.ResponseWriter, r *http.Request) {
ShowError(err, 1022)
} else {
return
break
}
return
-2
View File
@@ -352,8 +352,6 @@ func createXEPGDatabase() (err error) {
firstFreeNumber++
}
return
}
var generateHashForChannel = func(m3uID string, groupTitle string, tvgID string, tvgName string, uuidKey string, uuidValue string) string {
+169
View File
@@ -0,0 +1,169 @@
# xTeVe fork: review and improvement plan
Date: 2026-09-25
Scope: whole repository (Go backend, TypeScript UI, Docker, CI, docs)
Status: reviewed with maintainer 2026-09-25/26, all open questions resolved, ready to start Phase 0. Checklist lives in `tasks/todo.md`.
## 1. Where the fork stands
- Upstream (`xteve-project/xTeVe`) has not moved since commit `0e999b8` (March 2021). There is nothing to merge. This fork is effectively its own project now, so decisions can be made without worrying about upstream compatibility.
- The fork adds 26 commits, 57 files, roughly +4800/-1600 lines: UI redesign, Plex API refresh (`src/plex_api.go`), relaxed/strict XEPG mapping with auto-remap (`findXEPGReplacementChannel`), wizard-completed flag, config-dir migration in `docker/entrypoint.sh`, Drone CI, Go 1.25/1.26 modernisation.
- `go build ./...` passes. `go vet ./...` reports four "unreachable code" warnings. One test file exists (`src/internal/m3u-parser`). staticcheck cannot run locally because the installed binary predates Go 1.25.
- The README already lists four next steps (go:embed, CI, `/healthz`, websocket integration tests). This plan absorbs them.
The codebase works, and the fork's additions are sensible. The problems are mostly inherited from upstream: a security model that assumes a trusted LAN and a trusted playlist, data races in the stream buffer, and a build pipeline that depends on committed generated files. None of them show up in daily use until they do.
## 2. Guiding principles
1. **Security and crash bugs before features.** Several findings allow code execution or process crashes from data you do not control (playlists, EPG feeds, any web page open on the LAN).
2. **Tests before refactors.** Every phase below starts by adding tests around the code it touches, so the refactor can be checked against current behaviour.
3. **Small, reviewable PRs.** Each checklist item should be one PR. Avoid a "big bang" rewrite.
4. **Keep vanilla TypeScript for now.** A framework migration is a 3 to 6 week rewrite and would block everything else. Revisit after Phase 5.
5. **Every phase ends green:** `go vet`, `go test -race`, `tsc --noEmit`, Docker build.
## 3. Phases
### Phase 0: Hygiene (about half a day, no risk)
- Commit the pending `.gitignore` change (`.gocache/`), delete `.gocache/` from the working tree, and add `.gocache/`, `ts/`, `tasks/`, `*.md` (except what the Dockerfile needs) to `.dockerignore`.
- Decide where `agent.md` and `skill.md` live. Recommendation: move to `.claude/` or `tasks/` and ignore them, so the build context and repo root stay clean.
- Fix version drift: `xteve.go` says `2.2.0.0200`, `changelog-beta.md` says `0201`. Add the drift check that the changelog claims exists to `.drone.yml` (compare `grep` of both and fail on mismatch), or drop the hardcoded default and make the changelog the single source.
- **Delete the auto-updater** (decided 2026-09-25: Docker is the only distribution channel). As shipped, a fork build will download and install an upstream binary over itself if upstream ever tags a higher build. Remove `BinaryUpdate` and the `internal/up2date` package, the calls at `xteve.go:191` and `src/maintenance.go:69`, the `GitHub` struct and `-X`-style branch/user plumbing in `xteve.go`, `config.go:62,189-190` and `info.go:25`, and the `xteveAutoUpdate` setting from `struct-system.go:305`, `ts/base_ts.ts:26`, `ts/settings_ts.ts:290,484` and `html/lang/en.json`. **Keep** `conditionalUpdateChanges`, `convertToNewFilter` and `setValueForUUID` in `src/update.go`; they are settings-schema migrations, not binary updates. Rename that file to `migrate.go` so the distinction is obvious. This also removes the `kardianos/osext` dependency.
- Pin Go to 1.27.1 in `go.mod`, the Dockerfile and the Drone golang image (decided 2026-09-26).
- Fix the four `go vet` unreachable-code warnings (`buffer.go:733`, `maintenance.go:78`, `webserver.go:413`, `xepg.go:356`).
- Install a current staticcheck (or use golangci-lint) and record the baseline.
### Phase 1: Security (1 to 2 weeks, must do)
The threat model to design for: an attacker who can serve you a playlist or EPG feed, or who can get a browser on your LAN to load a page. Both are realistic for an IPTV proxy.
**1a. Browser-side (small, do first)**
- Stored XSS: provider-controlled strings (channel name, group, file name, log lines, descriptions) are written with `innerHTML`. Replace with `textContent` at `ts/menu_ts.ts:534, 589, 2362-2390`, `ts/logs_ts.ts:19`, `ts/configuration_ts.ts:91`, `ts/settings_ts.ts:591`. The unauthenticated `/stream/` endpoint logs the caller's User-Agent, so this is reachable without a malicious playlist.
- Websocket origin: `wsUpgrader.CheckOrigin` in `src/webserver.go:21` returns true. Compare `Origin` host to `r.Host` and reject mismatches. Move the token out of the query string into the cookie, and stop logging requests and responses (with tokens) to the console in `ts/network_ts.ts`.
- Cookie flags: set `HttpOnly`, `SameSite=Strict` on the Token cookie (`src/internal/authentication/authentication.go:601`), and stop writing it from JS.
- Serving the wizard page sets `Settings.AuthenticationWEB = false` (`src/webserver.go:650`). A GET must not mutate global settings. Gate the wizard on `wizard.completed` only.
**1b. Server-side input handling (small to medium)**
- `uploadLogo` writes to a client-supplied filename with no sanitisation (`src/images.go:19`). Apply `filepath.Base` and an extension allow-list.
- Backup restore has no zip-slip check (`src/compression.go:96`). Reject entries whose cleaned path escapes the target directory.
- `ffmpeg.path` / `vlc.path` accept any existing file, and `ffmpeg.options` is free text. Combined with the open websocket this is remote code execution. Options: restrict paths to a configured allow-list (Docker image ships a fixed ffmpeg), or require authentication to change them, or both.
- Put `/download/` (backups containing `authentication.json` and the Plex token) behind authentication.
- Stop deriving `System.Domain` from the request Host header on every request (`setGlobalDomain`). Use a configured base URL with Host as a fallback only when unset.
- Provider URLs reach ffmpeg's `-i` unchanged; reject non-http(s) schemes so `file:` and `concat:` sources are not possible.
**1c. Authentication (small to medium, needs migration)**
- `SHA256(secret, salt)` ignores the salt and computes an unsalted HMAC with the password as key. Replace with bcrypt (`golang.org/x/crypto/bcrypt`), migrate each user on next successful login, and use constant-time comparison.
- Tokens minted for URL/Basic auth calls are never evicted (`authentication.go:243, 575-585`). Plex polls constantly, so memory grows for the life of the process. Add expiry and a periodic sweep.
- Any authenticated web user can change any other user's credentials (`src/data.go:606`). Enforce that only admins can edit other users.
- Plex token is stored in plaintext in `settings.json` (mode 0644) and pushed to every websocket client. Write settings with 0600 and redact the token in the config payload sent to the UI.
- Web authentication stays off by default (LAN-only deployment, decided 2026-09-25). Document this in the README security note.
### Phase 2: Stability and correctness in the streaming path (2 to 3 weeks)
This is where the "works well until it doesn't" risk sits. Start with `go test -race` tests that spin up two concurrent tuners against a local `httptest` server that streams a fake TS file, then fix:
- **Shared maps without locks.** `Playlist` is stored by value in a `sync.Map` but contains maps, so every copy shares them. `bufferingStream`, `connectToStreamingServer` and `killClientConnection` write those maps from different goroutines, some under `Lock`, some not (`src/buffer.go:101, 125, 189, 208, 462, 480, 575, 929`). Give `Playlist` its own mutex, store `*Playlist`, and remove the store-back of stale copies at lines 756 and 930.
- **Tuner limit is a check-then-act race** (`buffer.go:152`). Reserve the slot under the playlist lock.
- **`force=true` in `killClientConnection`** deletes the stream but never cleans `BufferClients`.
- **`Data.Cache.StreamingURLS`** is written by `/lineup.json` and `/m3u` while `/stream/` reads it (`src/system.go:338-390`). Guard with an RWMutex.
- **Deferred closes inside read loops** (`buffer.go:880, 891`, also 316, 707-737; `compression.go:106, 112`; `imgcache/cache.go:106, 118`). A multi-hour stream accumulates millions of deferred calls. Close explicitly or move the loop body into a function.
- **ffmpeg/VLC process management** (`buffer.go:1345-1641`): timeout goroutine can block forever, the 20 s timeout cannot fire while `Read` blocks, `cmd.Start()` error ignored, `panic` inside a goroutine kills the process, double-open of the segment file leaks an fd. Rewrite around `exec.CommandContext` with the request context, which also replaces the deprecated `http.CloseNotifier`. **Decided 2026-09-25: keep VLC.** Structure the rewrite as one external-process buffer that takes a binary path plus an argument builder, with ffmpeg and VLC as two small builders. VLC then costs nothing to keep, the process-management fixes apply to both, and tests can cover the builders without VLC installed. The Docker image continues to ship ffmpeg only.
- **Fake mutexes** in `src/screen.go:55, 115, 130` (a new `sync.RWMutex{}` per call). Use one package-level mutex for the log.
- **Timeouts everywhere.** Replace `http.ListenAndServe` with an `http.Server` with read/write/idle timeouts, and give every outbound `http.Client` a timeout (provider fetch, logo download, update check). A hung provider currently leaves `ScanInProgress` stuck.
- **imgcache** holds its lock during every logo download, blocking `/m3u/` and the XMLTV build. Download outside the lock.
- **Atomic file writes.** Every save is a truncate-then-write. Write to a temp file in the same directory, fsync, rename. One helper, used by `writeByteToFile` and friends.
- **Concrete bugs to fix while there:** `xepg.go:249` appends instead of removing (`Files[:i]`); `data.go:882` mutates a slice while ranging; `screen.go:193` drops newest log lines when full; `buffer.go:161-163` sets headers after `WriteHeader` and names one `"Content-Length:"`; `screen.go:412` evicts random notifications; unchecked type assertions and indexing in `data.go`, `backup.go`, `provider.go`, `authentication.go:89`, `screen.go:104`; API double-write after `responseAPIError` (`webserver.go:974-978`); websocket `request`/`response` structs reused across iterations so error state and stale fields leak (`webserver.go:334-335`).
- **Error hygiene:** the `authenticationErr` closures return from the closure not the caller; remove `os.Exit`/`log.Fatal`/`panic` outside `main`; exit non-zero on fatal errors; check ignored results of `getProviderData`, `saveSettings`, `writeByteToFile`.
### Phase 3: Build, embed, CI, Docker (1 week, low risk, big quality-of-life win)
- **Replace `src/webUI.go` with `//go:embed html`.** The generated file is 783 KB, map-ordered so it changes on every run, and appears in most UI commits. Serve via `fs.Sub` and `http.FileServer`; keep template substitution for HTML only. In `-dev` mode use `os.DirFS("html")`. Delete `src/html-build.go` and `cmd/webui-gen`. Add cache headers and ETags.
- **Drop i18n** (decided 2026-09-26). The JS currently contains Go template placeholders (`{{.settings.x.title}}`) so it cannot be bundled, cached, or run outside the server. Only English exists. Inline the English strings from `html/lang/en.json` into the TypeScript, delete `en.json` and the template pass for JS and CSS, and keep `parseTemplate` only for the HTML pages that need server values. This is a prerequisite for the next item. Mechanical approach: a one-off script that reads `en.json` and rewrites every `{{.path}}` in `ts/*.ts` with the string literal, then a review pass.
- **Pinned TypeScript toolchain.** Add `package.json` and `tsconfig.json` (strict, ES2020 target), bundle with esbuild to one unminified `html/js/app.js`, and run `tsc --noEmit` in CI. **The bundle is committed** (decided 2026-09-26) so `go build` works from a bare clone and the Dockerfile needs no Node stage; CI rebuilds it and fails on a diff. Delete the ten unreferenced legacy files in `html/js/` (`mapping-editor.js` alone is 1465 lines) which are currently embedded in every binary.
- **CI hardening in `.drone.yml`:** add `go vet`, golangci-lint, `gofmt -l`, `go test -race`, `tsc --noEmit`, and a check that committed build outputs are current. Publish images tagged with the version as well as `latest` and SHA. amd64 only (decided 2026-09-26); no multi-arch step.
- **Docker runtime:** UID/GID are fixed at build time so prebuilt images cannot change them. Switch to the PUID/PGID + su-exec pattern in the entrypoint. Pin `mwader/static-ffmpeg` to a tag. Add `VOLUME`. Verify the healthcheck still passes with web auth enabled. Remove `/xteve` from `LEGACY_CONFIG_DIRS` (it is the parent of the default dir). Note in the README that SSDP needs host networking.
- **Add `/healthz`** as the README suggests, so the healthcheck and monitoring do not depend on HDHomeRun endpoints.
- **Docs:** write `README-DEV.md` (build, dev mode, UI toolchain, release and versioning process) and add a fork section to `README.md` (registry, compose files, env vars `XTEVE_CONFIG`/`XTEVE_PORT`/PUID/PGID, differences from upstream, security notes). Remove the upstream donation block.
- **Code cleanup (moved from Phase 4):** deduplicate the image-caching goroutine (`xepg.go:78-93` and `123-138`), `addErrorToStream` (`buffer.go:581`, `1379`), `extractZIP`/`mapToJSON`/`randomString`; delete dead code (`Auto`, `getStreamByChannelID`, `updateXEPG`, `indexOfInt`, commented-out blocks in `struct-buffer.go` and `authentication.go`); drop `io/ioutil` (12 files), `kardianos/osext`, `rand.Seed`. Do this after go:embed lands so the diffs stay readable.
- Note: `agent.md` references `docs/design-system/` and `tasks/design-foundation.md`, neither of which exist in this repo. Either add them or remove that section.
### Phase 4: Data model and performance (NOT PLANNED)
**Decided 2026-09-25: skipped.** The real deployment is one M3U with 174 streams and 167 XEPG channels, and that is not expected to change. None of the quadratic passes matter at that size. The list below is kept for reference only; the non-performance cleanup items (dead code, duplicates, deprecated packages) have moved to Phase 3. Revisit only if a rebuild ever takes more than a few seconds.
- `Data.XEPG.Channels` is `map[string]any` and every pass does `json.MarshalIndent` then `Unmarshal` per channel (`xepg.go:370, 389, 405, 462, 659, 889, 1265`). Type it as `map[string]*XEPGChannelStruct`.
- `getProgramData` scans every programme for every channel (`xepg.go:961`). Index programmes by channel ID once per build.
- `xepg.json` is written three times per rebuild; `settings.json` once per provider; `urls.json` on every `/lineup.json` request. Debounce and write once.
- `filterThisStream` compiles regexps per stream per filter (`m3u.go:65, 77`). Precompile.
- `buildM3U` uses string concatenation; use `strings.Builder`. The XMLTV output is built entirely in memory then copied; stream it to the gzip writer.
- `Web()` re-parses the language JSON per request (`webserver.go:614-634`). Parse once.
- Parsed XMLTV is cached forever (`xepg.go:1221`). Drop it after the build.
- Deduplicate: image-caching goroutine (`xepg.go:78-93` and `123-138`), `addErrorToStream` (`buffer.go:581`, `1379`), `extractZIP`/`mapToJSON`/`randomString` across packages. Delete dead code (`Auto`, `getStreamByChannelID`, `updateXEPG`, `indexOfInt`, commented-out blocks in `struct-buffer.go` and `authentication.go`).
- Modernise: drop `io/ioutil` (12 files), `kardianos/osext` (use `os.Executable`), `rand.Seed`, `http.CloseNotifier`; adopt `context`, `slog`, `errors.Is`, `slices`, `maps`.
### Phase 5: Frontend architecture (2 to 4 weeks, medium risk)
The UI redesign left a consistent visual layer. The problems are underneath it.
- **Websocket client:** each request opens a new socket, and a global flag silently drops any request made while one is in flight (`ts/network_ts.ts:9-11`). Replace with one persistent socket, request IDs, a promise per request, a queue, and exponential-backoff reconnect. This is what "reconnection" should mean instead of a failure counter.
- **Stop rebuilding the whole UI on every response.** `createLayout()` re-renders everything, losing focus and scroll position. Diff by section, or at minimum re-render only the active tab.
- **Mapping table:** renders every channel with no virtualisation or pagination. Add windowed rendering; this is the single largest UX win for big lineups.
- **Code structure:** `menu_ts.ts` is 2472 lines with 34 string `onclick` handlers (one typo'd `javscript:` at line 551). Split into modules, use `addEventListener`, make the settings table data-driven (about 20 copy-pasted blocks in `settings_ts.ts:24-437`).
- **Accessibility follow-ups:** buttons instead of clickable `<td>`, `<button>` instead of `<input type=button>`, replace `alert()` with the existing announcer, drop deprecated `language="javascript"`. Add a light theme via `prefers-color-scheme` using the existing `:root` tokens.
- Merge `base.css` and `screen.css` into a layered structure (tokens, base, components, layout) so the boundary is clear.
### Phase 6: Features worth borrowing (optional, after 1 to 3)
Threadfin is the most active successor fork. Its additions that fit this codebase without a rewrite:
- Regex include/exclude filters (the current filter is substring based).
- Per-playlist tuner limits and per-playlist buffer choice.
- Bulk channel editing in the mapping table (the fork already has range-select checkboxes; this is the next step).
- Working backup/restore with schema versioning (ties into the atomic-write work in Phase 2).
- Dummy EPG with selectable durations.
## 4. Test strategy
There is one test today. Aim for the following minimum before each phase's refactor:
| Area | Test type | Phase |
|---|---|---|
| Websocket command dispatch | `httptest` + gorilla client, one test per `cmd` | 1 |
| Auth: login, token expiry, bcrypt migration, user edit permissions | unit | 1 |
| Upload/restore path traversal | unit with crafted names and zips | 1 |
| Buffer: two concurrent tuners, tuner limit, client disconnect cleanup | integration with `-race`, fake TS server | 2 |
| Atomic write helper: crash simulation leaves old file intact | unit | 2 |
| XEPG mapping: strict vs relaxed, auto-remap with 0/1/2 candidates | table-driven unit on fixtures | 2, 4 |
| M3U/XMLTV parsing and filtering | extend existing parser tests | 4 |
| XEPG build benchmark | `go test -bench` on 2000-channel fixture | 4 |
| TypeScript | `tsc --noEmit` in CI; consider Vitest for the websocket client once it is a module | 3, 5 |
## 5. What this plan deliberately does not do
- **No framework migration now.** A Preact/Svelte rewrite is the largest item on the list and blocks security and stability fixes. Phase 5 makes the vanilla code modular enough that a later migration can be incremental.
- **No upstream merge.** There is nothing to merge; upstream is inactive.
- **No new features before Phase 3.** The value is in stopping the process from crashing or being hijacked, and in making the next change cheap to ship.
## 6. Suggested order and rough timeline
| Order | Phase | Effort | Why this position |
|---|---|---|---|
| 1 | 0 Hygiene | 0.5 day | Free, removes foot-guns (auto-update from upstream) |
| 2 | 1a Browser security | 1 to 2 days | Smallest fix for the worst exposure |
| 3 | 3 go:embed + TS toolchain + CI | 1 week | Every later PR gets smaller and safer to review |
| 4 | 1b, 1c Server security + auth | 1 week | Needs the test harness from step 3's CI |
| 5 | 2 Streaming stability | 2 to 3 weeks | Highest complexity; do with race tests in place |
| 6 | 5 Frontend architecture | 2 to 4 weeks | Builds on the bundled TS from step 3 |
| 7 | 6 Features | as desired | Only once the base is solid |
## 7. Open questions
These change the shape of the work and should be settled before starting:
1. ~~Is the deployment always Docker on a trusted LAN, or is xTeVe ever exposed beyond it?~~ **Decided 2026-09-25: trusted LAN only.** Consequences: keep all of Phase 1a and 1b (a LAN browser or a hostile playlist is still in scope), keep bcrypt and token eviction in 1c, but do not change the auth-off default for fresh installs. Restrict `ffmpeg.path` to an allow-list rather than requiring auth to change it.
2. ~~How large are your playlists and EPG files?~~ **Decided 2026-09-25: 174 streams / 167 channels, one M3U, stable. Phase 4 dropped.**
3. ~~Do you use the VLC buffer?~~ **Decided 2026-09-25: ffmpeg in practice, but keep VLC as an option.** Implemented as a shared external-process buffer with per-tool argument builders (see Phase 2).
4. ~~Is the auto-updater used?~~ **Decided 2026-09-25: Docker only. Updater deleted in Phase 0; settings migrations in `update.go` are kept.**
5. ~~Is multi-arch (arm64) needed for the image?~~ **Decided 2026-09-26: amd64 only.**
6. ~~Keep the language layer?~~ **Decided 2026-09-26: drop it. English strings inlined in the TypeScript.**
7. ~~Where does the TypeScript build run?~~ **Decided 2026-09-26: commit the esbuild bundle; CI verifies freshness; no Node in Docker.**
+83
View File
@@ -0,0 +1,83 @@
# xTeVe improvement checklist
Detailed rationale, file references and effort estimates: `tasks/improvement-plan.md`.
Status: Phase 0 committed on branch `improvements` 2026-09-26.
## Phase 0: Hygiene
- [x] `.gitignore` (`.gocache/`), deleted `.gocache/`, extended `.dockerignore`
- [x] `agent.md` / `skill.md` git-ignored and docker-ignored (left in place)
- [x] Version drift fixed (`xteve.go` now 0201) and Drone drift check added
- [x] Auto-updater deleted (`BinaryUpdate`, `internal/up2date`, `GitHub`/`Update` structs, `xteveAutoUpdate` + `update.url` settings, UI rows, `en.json`); migrations kept in `migrate.go`; `kardianos/osext` gone
- [x] `go 1.27.1`; Dockerfile and Drone golang images pinned to 1.27.1
- [x] Four `go vet` unreachable-code warnings fixed (vet clean)
- [x] staticcheck baseline via `go run honnef.co/go/tools/cmd/staticcheck@latest ./...`: 408 findings (S1002 113, SA5008 79, S1039 67, S1038 40, S1023 35, ST1005 20, SA1019 14, SA4006 9, misc 11). Mostly style; SA5008/SA4006/SA1019 worth a pass in Phase 3 cleanup
## Phase 1: Security
- [ ] Replace `innerHTML` with `textContent` for provider-controlled strings
- [ ] Enforce websocket Origin check; move token from query string to cookie; stop console-logging tokens
- [ ] Cookie `HttpOnly` + `SameSite`
- [ ] Wizard GET must not set `AuthenticationWEB = false`
- [ ] Sanitise `uploadLogo` filename
- [ ] Zip-slip guard in backup restore
- [ ] Restrict `ffmpeg.path` / `vlc.path` and reject non-http(s) stream URLs
- [ ] Put `/download/` behind auth
- [ ] Configured base URL instead of Host-header-derived domain
- [ ] bcrypt with migrate-on-login; constant-time compare
- [ ] Token expiry and eviction
- [ ] Only admins may edit other users
- [ ] Settings file mode 0600; redact Plex token in UI payload
- [x] Decide default for web auth on fresh installs (keep off; LAN only, decided 2026-09-25)
## Phase 2: Streaming stability
- [ ] Race tests: two concurrent tuners against a fake TS server, run with `-race`
- [ ] `*Playlist` with own mutex; remove stale store-backs
- [ ] Atomic tuner reservation; clean `BufferClients` on force kill
- [ ] RWMutex around `StreamingURLS`
- [ ] Remove `defer` inside read loops (buffer, compression, imgcache)
- [ ] Single external-process buffer (`exec.CommandContext` + request context) with ffmpeg and VLC argument builders; drop `CloseNotifier`
- [ ] Real mutex for screen log
- [ ] `http.Server` with timeouts; timeouts on all outbound clients
- [ ] imgcache: download outside the lock
- [ ] Atomic write helper (temp + fsync + rename)
- [ ] Fix listed concrete bugs (xepg append, range mutation, log overflow, headers after WriteHeader, random notification eviction, unchecked assertions, API double write, WS struct reuse)
- [ ] Error hygiene: no-op closures, `os.Exit`/`panic` outside main, ignored results
## Phase 3: Build, embed, CI, Docker
- [ ] `//go:embed html`, delete `webUI.go`, `html-build.go`, `cmd/webui-gen`; cache headers
- [ ] Drop i18n: inline English strings into TS, delete `en.json`, template only HTML pages
- [ ] `package.json` + `tsconfig.json` + esbuild bundle (committed, CI-verified); delete 10 dead JS files
- [ ] CI: vet, lint, gofmt, `test -race`, `tsc --noEmit`, generated-output check
- [ ] Version-tagged images (amd64 only)
- [ ] PUID/PGID via su-exec; pin static-ffmpeg; `VOLUME`; fix `LEGACY_CONFIG_DIRS`
- [ ] `/healthz` endpoint
- [ ] `README-DEV.md` and fork section in `README.md`
- [ ] Deduplicate and delete dead code; drop `ioutil`, `osext`, `rand.Seed` (after go:embed)
- [ ] Resolve missing `docs/design-system/` referenced by `agent.md`
## Phase 4: Data model and performance
Not planned (lineup is ~170 channels, decided 2026-09-25). See plan for the reference list.
## Phase 5: Frontend architecture
- [ ] Persistent websocket with request IDs, queue, backoff reconnect
- [ ] Section-level re-render instead of full `createLayout()`
- [ ] Virtualised mapping table
- [ ] Split `menu_ts.ts`; `addEventListener`; data-driven settings rows
- [ ] Accessibility follow-ups; light theme via `prefers-color-scheme`
- [ ] Consolidate CSS layers
## Phase 6: Optional features
- [ ] Regex filters
- [ ] Per-playlist tuner limit and buffer choice
- [ ] Bulk channel edit
- [ ] Versioned backup/restore
- [ ] Dummy EPG durations
## Review
### Phase 0 (2026-09-26, branch `improvements`)
- `go build`, `go vet`, `go test ./...` all clean. `src/webUI.go` regenerated.
- Smoke run against an empty config dir on port 34499: `/web/`, `/lineup_status.json`, `/discover.json` all 200, no errors logged.
- Not verified locally: Docker image build (no daemon on this machine); Drone will cover it.
- Compiled JS in `html/js/` was hand-edited to mirror the TS change because a current `tsc` does not reproduce the committed output (different shim, 5 type errors in `menu_ts.ts`). Phase 3 replaces this with a pinned toolchain.
- `html/js/menu.js` still mentions `xteveAutoUpdate`; it is one of the ten dead legacy files scheduled for deletion in Phase 3.
- `Settings.Branch` (`git.branch`) was kept: it still drives whether the build number is shown in the UI and XMLTV header.
+1 -1
View File
@@ -23,7 +23,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
// Kategorien für die Einstellungen
var settingsCategory = new Array()
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "tuner,epgSource,api,use_plexAPI,plex.url,plex.token"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.missing.epg.mode,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"))
-18
View File
@@ -287,20 +287,6 @@ class SettingsCategory {
setting.appendChild(tdRight)
break
case "xteveAutoUpdate":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.xteveAutoUpdate.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createCheckbox(settingsKey)
input.checked = data
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
case "api":
var tdLeft = document.createElement("TD")
tdLeft.innerHTML = "{{.settings.api.title}}" + ":"
@@ -481,10 +467,6 @@ class SettingsCategory {
}
break
case "xteveAutoUpdate":
text = "{{.settings.xteveAutoUpdate.description}}"
break
case "backup.keep":
text = "{{.settings.backupKeep.description}}"
break
+1 -27
View File
@@ -16,31 +16,12 @@ import (
"xteve/src"
)
// GitHubStruct : GitHub Account. Über diesen Account werden die Updates veröffentlicht
type GitHubStruct struct {
Branch string
Repo string
Update bool
User string
}
// GitHub : GitHub Account
// If you want to fork this project, enter your Github account here. This prevents a newer version of xTeVe from updating your version.
var GitHub = GitHubStruct{Branch: "master", User: "xteve-project", Repo: "xTeVe-Downloads", Update: true}
/*
Branch: GitHub Branch
User: GitHub Username
Repo: GitHub Repository
Update: Automatic updates from the GitHub repository [true|false]
*/
// Name : Programmname
const Name = "xTeVe"
// Version : Version, die Build Nummer wird in der main func geparst.
// Can be overwritten at build time: -ldflags "-X main.Version=..."
var Version = "2.2.0.0200"
var Version = "2.2.0.0201"
// DBVersion : Datanbank Version
const DBVersion = "2.1.0"
@@ -71,10 +52,8 @@ func main() {
var system = &src.System
system.APIVersion = APIVersion
system.Branch = GitHub.Branch
system.Build = build[len(build)-1:][0]
system.DBVersion = DBVersion
system.GitHub = GitHub
system.Name = Name
system.Version = strings.Join(build[0:len(build)-1], ".")
@@ -188,11 +167,6 @@ func main() {
os.Exit(0)
}
err = src.BinaryUpdate()
if err != nil {
src.ShowError(err, 0)
}
err = src.StartSystem(false)
if err != nil {
src.ShowError(err, 0)