Initial commit: docker compose config
Release Docker multi arch / docker (push) Has been cancelled
Test Install Script / Test Script Syntax (push) Has been cancelled
Test Install Script / Test on almalinux-10 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-10 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-8 (root) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (default) (push) Has been cancelled
Test Install Script / Test on almalinux-9 (root) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (default) (push) Has been cancelled
Test Install Script / Test on amazonlinux-2 (root) (push) Has been cancelled
Test Install Script / Test on debian-11 (default) (push) Has been cancelled
Test Install Script / Test on debian-11 (root) (push) Has been cancelled
Test Install Script / Test on debian-12 (default) (push) Has been cancelled
Test Install Script / Test on debian-12 (root) (push) Has been cancelled
Test Install Script / Test on debian-13 (default) (push) Has been cancelled
Test Install Script / Test on debian-13 (root) (push) Has been cancelled
Test Install Script / Test on fedora-latest (default) (push) Has been cancelled
Test Install Script / Test on fedora-latest (root) (push) Has been cancelled
Test Install Script / Test on rocky-10 (default) (push) Has been cancelled
Test Install Script / Test on rocky-10 (root) (push) Has been cancelled
Test Install Script / Test on rocky-8 (default) (push) Has been cancelled
Test Install Script / Test on rocky-8 (root) (push) Has been cancelled
Test Install Script / Test on rocky-9 (default) (push) Has been cancelled
Test Install Script / Test on rocky-9 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-22.04 (root) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (default) (push) Has been cancelled
Test Install Script / Test on ubuntu-24.04 (root) (push) Has been cancelled

This commit is contained in:
2026-05-30 12:07:11 +00:00
commit 616c6b1c62
381 changed files with 55145 additions and 0 deletions
+205
View File
@@ -0,0 +1,205 @@
package dlna
import (
"fmt"
"net"
"os"
"os/user"
"path/filepath"
"runtime"
"sort"
"strconv"
"time"
"github.com/anacrolix/dms/dlna/dms"
"github.com/anacrolix/log"
"github.com/wlynxg/anet"
"server/settings"
"server/web/pages/template"
)
var dmsServer *dms.Server
func Start() {
logger := log.Default.WithNames("dlna")
dmsServer = &dms.Server{
Logger: logger.WithNames("dms", "server"),
Interfaces: func() (ifs []net.Interface) {
var err error
ifaces, err := anet.Interfaces()
if err != nil {
logger.Levelf(log.Error, "%v", err)
return
// os.Exit(1) // avoid start on Android 13+
}
for _, i := range ifaces {
// interface flags seem to always be 0 on Windows
if runtime.GOOS != "windows" && (i.Flags&net.FlagLoopback != 0 || i.Flags&net.FlagUp == 0 || i.Flags&net.FlagMulticast == 0) {
continue
}
ifs = append(ifs, i)
}
return
}(),
HTTPConn: func() net.Listener {
port := 9080
for {
logger.Levelf(log.Info, "Check dlna port %d", port)
m, err := net.Listen("tcp", settings.IP+":"+strconv.Itoa(port))
if m != nil {
m.Close()
}
if err == nil {
break
}
port++
}
logger.Levelf(log.Info, "Set dlna port %d", port)
conn, err := net.Listen("tcp", settings.IP+":"+strconv.Itoa(port))
if err != nil {
logger.Levelf(log.Error, "%v", err)
os.Exit(1)
}
return conn
}(),
FriendlyName: getDefaultFriendlyName(),
NoTranscode: true,
NoProbe: true,
StallEventSubscribe: false,
Icons: []dms.Icon{
{
Width: 48,
Height: 48,
Depth: 24,
Mimetype: "image/png",
Bytes: template.Dlnaicon48png,
},
{
Width: 120,
Height: 120,
Depth: 24,
Mimetype: "image/png",
Bytes: template.Dlnaicon120png,
},
},
LogHeaders: settings.BTsets.EnableDebug,
NotifyInterval: 30 * time.Second,
AllowedIpNets: func() []*net.IPNet {
var nets []*net.IPNet
_, ipnet, _ := net.ParseCIDR("0.0.0.0/0")
nets = append(nets, ipnet)
_, ipnet, _ = net.ParseCIDR("::/0")
nets = append(nets, ipnet)
return nets
}(),
OnBrowseDirectChildren: onBrowse,
OnBrowseMetadata: onBrowseMeta,
}
if err := dmsServer.Init(); err != nil {
logger.Levelf(log.Error, "error initing dms server: %v", err)
os.Exit(1)
}
go func() {
if err := dmsServer.Run(); err != nil {
logger.Levelf(log.Error, "%v", err)
os.Exit(1)
}
}()
}
func Stop() {
if dmsServer != nil {
dmsServer.Close()
dmsServer = nil
}
}
func onBrowse(path, rootObjectPath, host, userAgent string) (ret []interface{}, err error) {
if path == "/" {
ret = getRoot()
return
} else if path == "/TR" {
ret = getTorrents()
return
} else if isHashPath(path) {
ret = getTorrent(path, host)
return
} else if filepath.Base(path) == "LD" {
ret = loadTorrent(path, host)
}
return
}
func onBrowseMeta(path string, rootObjectPath string, host, userAgent string) (ret interface{}, err error) {
ret = getTorrentMeta(path, host)
if ret == nil {
err = fmt.Errorf("meta not found")
}
return
}
func getDefaultFriendlyName() string {
logger := log.Default.WithNames("dlna")
if settings.BTsets.FriendlyName != "" {
return settings.BTsets.FriendlyName
}
ret := "TorrServer"
userName := ""
user, err := user.Current()
if err != nil {
logger.Printf("getDefaultFriendlyName could not get username: %s", err)
} else {
userName = user.Name
}
host, err := os.Hostname()
if err != nil {
logger.Printf("getDefaultFriendlyName could not get hostname: %s", err)
}
if userName == "" && host == "" {
return ret
}
if userName != "" && host != "" {
if userName == host {
return ret + ": " + userName
}
return ret + ": " + userName + " on " + host
}
if host == "localhost" { // useless host, use 1st IP
ifaces, err := anet.Interfaces()
if err != nil {
return ret + ": " + userName + "@" + host
}
var list []string
for _, i := range ifaces {
// interface flags seem to always be 0 on Windows
if runtime.GOOS != "windows" && (i.Flags&net.FlagLoopback != 0 || i.Flags&net.FlagUp == 0 || i.Flags&net.FlagMulticast == 0) {
continue
}
addrs, _ := anet.InterfaceAddrsByInterface(&i)
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if !ip.IsLoopback() && ip.To4() != nil {
list = append(list, ip.String())
}
}
}
if len(list) > 0 {
sort.Strings(list)
return ret + " " + list[0]
}
}
return ret + ": " + userName + "@" + host
}
+288
View File
@@ -0,0 +1,288 @@
package dlna
import (
"fmt"
"net/url"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/anacrolix/dms/dlna"
"github.com/anacrolix/dms/upnpav"
"server/log"
mt "server/mimetype"
"server/settings"
"server/torr"
"server/torr/state"
)
func getRoot() (ret []interface{}) {
// Torrents Object (ROOT)
tObj := upnpav.Object{
ID: "%2FTR",
ParentID: "0",
Restricted: 1,
Title: "Torrents",
Class: "object.container.storageFolder",
Date: upnpav.Timestamp{Time: time.Now()},
}
// add Torrents Object
vol := len(torr.ListTorrent())
cnt := upnpav.Container{Object: tObj, ChildCount: vol}
ret = append(ret, cnt)
return
}
func getTorrents() (ret []interface{}) {
torrs := torr.ListTorrent()
// sort by title as in cds SortCaps
sort.Slice(torrs, func(i, j int) bool {
return torrs[i].Title < torrs[j].Title
})
vol := 0
for _, t := range torrs {
vol++
obj := upnpav.Object{
ID: "%2F" + t.TorrentSpec.InfoHash.HexString(),
ParentID: "%2FTR",
Restricted: 1,
Title: strings.ReplaceAll(t.Title, "/", "|"),
Class: "object.container.storageFolder",
Icon: t.Poster,
AlbumArtURI: t.Poster,
Date: upnpav.Timestamp{Time: time.Unix(t.Timestamp, 0)}, // time.Now()
}
cnt := upnpav.Container{Object: obj, ChildCount: 1}
ret = append(ret, cnt)
}
if vol == 0 {
obj := upnpav.Object{
ID: "%2FNT",
ParentID: "%2FTR",
Restricted: 1,
Title: "No Torrents",
Class: "object.container.storageFolder",
Date: upnpav.Timestamp{Time: time.Now()},
}
cnt := upnpav.Container{Object: obj, ChildCount: 0}
ret = append(ret, cnt)
}
return
}
func getTorrent(path, host string) (ret []interface{}) {
// find torrent without load
torrs := torr.ListTorrent()
var torr *torr.Torrent
for _, t := range torrs {
if strings.Contains(path, t.TorrentSpec.InfoHash.HexString()) {
torr = t
break
}
}
if torr == nil {
return nil
}
// get content from torrent
parent := "%2F" + torr.TorrentSpec.InfoHash.HexString()
// if torrent not loaded, get button for load
if torr.Files() == nil {
obj := upnpav.Object{
ID: parent + "%2FLD",
ParentID: parent,
Restricted: 1,
Title: "Load Torrent",
Class: "object.container.storageFolder",
Date: upnpav.Timestamp{Time: time.Now()},
}
cnt := upnpav.Container{Object: obj, ChildCount: 1}
ret = append(ret, cnt)
return
}
ret = loadTorrent(path, host)
return
}
func getTorrentMeta(path, host string) (ret interface{}) {
// Meta object
if path == "/" {
// root object meta
rootObj := upnpav.Object{
ID: "0",
ParentID: "-1",
Restricted: 1,
Searchable: 1,
Title: "TorrServer",
Date: upnpav.Timestamp{Time: time.Now()},
Class: "object.container.storageFolder",
}
meta := upnpav.Container{Object: rootObj, ChildCount: 1}
return meta
} else if filepath.Base(path) == "TR" {
// TR Object Meta
trObj := upnpav.Object{
ID: "%2FTR",
ParentID: "0",
Restricted: 1,
Searchable: 1,
Title: "Torrents",
Date: upnpav.Timestamp{Time: time.Now()},
Class: "object.container.storageFolder",
}
torrs := torr.ListTorrent()
vol := len(torrs)
meta := upnpav.Container{Object: trObj, ChildCount: vol}
return meta
} else if isHashPath(path) {
// find torrent without load
torrs := torr.ListTorrent()
var torr *torr.Torrent
for _, t := range torrs {
if strings.Contains(path, t.TorrentSpec.InfoHash.HexString()) {
torr = t
break
}
}
if torr == nil {
return nil
}
// hash object meta
obj := upnpav.Object{
ID: "%2F" + torr.TorrentSpec.InfoHash.HexString(),
ParentID: "%2FTR",
Restricted: 1,
Title: torr.Title,
Date: upnpav.Timestamp{Time: time.Unix(torr.Timestamp, 0)}, // time.Now()
}
meta := upnpav.Container{Object: obj, ChildCount: 1}
return meta
} else if filepath.Base(path) == "LD" {
parent := url.PathEscape(filepath.Dir(path))
// LD object meta
obj := upnpav.Object{
ID: parent + "%2FLD",
ParentID: parent,
Restricted: 1,
Searchable: 1,
Title: "Load Torrents",
Date: upnpav.Timestamp{Time: time.Now()},
}
meta := upnpav.Container{Object: obj, ChildCount: 1}
return meta
} else {
file := filepath.Base(path)
id := url.PathEscape(path)
parent := url.PathEscape(filepath.Dir(path))
// file object meta
obj := upnpav.Object{
ID: id,
ParentID: parent,
Restricted: 1,
Searchable: 1,
Title: file,
Date: upnpav.Timestamp{Time: time.Now()},
}
meta := upnpav.Container{Object: obj, ChildCount: 1}
return meta
}
}
func loadTorrent(path, host string) (ret []interface{}) {
hash := filepath.Base(filepath.Dir(path))
if hash == "/" || hash == "\\" {
hash = filepath.Base(path)
}
if len(hash) != 40 {
return
}
tor := torr.GetTorrent(hash)
if tor == nil {
log.TLogln("Dlna error get info from torrent", hash)
return
}
if len(tor.Files()) == 0 {
time.Sleep(time.Millisecond * 200)
timeout := time.Now().Add(time.Second * 60)
for {
tor = torr.GetTorrent(hash)
if len(tor.Files()) > 0 {
break
}
time.Sleep(time.Millisecond * 200)
if time.Now().After(timeout) {
return
}
}
}
parent := "%2F" + tor.TorrentSpec.InfoHash.HexString()
files := tor.Status().FileStats
for _, f := range files {
obj := getObjFromTorrent(path, parent, host, tor, f)
if obj != nil {
ret = append(ret, obj)
}
}
return
}
func getLink(host, path string) string {
if !strings.HasPrefix(host, "http") {
host = "http://" + host
}
pos := strings.LastIndex(host, ":")
if pos > 7 {
host = host[:pos]
}
return host + ":" + settings.Port + "/" + path
}
func getObjFromTorrent(path, parent, host string, torr *torr.Torrent, file *state.TorrentFileStat) (ret interface{}) {
mime, err := mt.MimeTypeByPath(file.Path)
if err != nil {
if settings.BTsets.EnableDebug {
log.TLogln("Can't detect mime type", err)
}
return
}
// TODO: handle subtitles for media
if !mime.IsMedia() {
return
}
if settings.BTsets.EnableDebug {
log.TLogln("mime type", mime.String(), file.Path)
}
obj := upnpav.Object{
ID: parent + "%2F" + url.PathEscape(file.Path),
ParentID: parent,
Restricted: 1,
Title: file.Path,
Class: "object.item." + mime.Type() + "Item",
Date: upnpav.Timestamp{Time: time.Now()},
}
item := upnpav.Item{
Object: obj,
Res: make([]upnpav.Resource, 0, 1),
}
// pathPlay := "stream/" + url.PathEscape(file.Path) + "?link=" + torr.TorrentSpec.InfoHash.HexString() + "&play&index=" + strconv.Itoa(file.Id)
pathPlay := "play/" + torr.TorrentSpec.InfoHash.HexString() + "/" + strconv.Itoa(file.Id)
item.Res = append(item.Res, upnpav.Resource{
URL: getLink(host, pathPlay),
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mime, dlna.ContentFeatures{
SupportRange: true,
SupportTimeSeek: true,
}.String()),
Size: uint64(file.Length),
})
return item
}
+19
View File
@@ -0,0 +1,19 @@
package dlna
import (
"path/filepath"
)
func isHashPath(path string) bool {
base := filepath.Base(path)
if len(base) == 40 {
data := []byte(base)
for _, v := range data {
if !(v >= 48 && v <= 57 || v >= 65 && v <= 70 || v >= 97 && v <= 102) {
return false
}
}
return true
}
return false
}