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
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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user