DLNA refactor and support browse folder objects (#1517)

This commit is contained in:
WithoutPants
2021-06-22 18:56:16 +10:00
committed by GitHub
parent 5fdfbaa7f1
commit ae3400a9b1
2 changed files with 210 additions and 157 deletions

View File

@@ -180,7 +180,7 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http
case "Browse":
var browse browse
if err := xml.Unmarshal([]byte(argsXML), &browse); err != nil {
return nil, err
return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "cannot unmarshal browse argument: %s", err.Error())
}
obj, err := me.objectFromID(browse.ObjectID)
@@ -190,6 +190,36 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http
switch browse.BrowseFlag {
case "BrowseDirectChildren":
return me.handleBrowseDirectChildren(obj, host)
case "BrowseMetadata":
return me.handleBrowseMetadata(obj, host)
default:
return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "unhandled browse flag: %v", browse.BrowseFlag)
}
case "GetSearchCapabilities":
return map[string]string{
"SearchCaps": "",
}, nil
// from https://github.com/rclone/rclone/blob/master/cmd/serve/dlna/cds.go
// Samsung Extensions
case "X_GetFeatureList":
return map[string]string{
"FeatureList": `<Features xmlns="urn:schemas-upnp-org:av:avs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd">
<Feature name="samsung.com_BASICVIEW" version="1">
<container id="0" type="object.item.imageItem"/>
<container id="0" type="object.item.audioItem"/>
<container id="0" type="object.item.videoItem"/>
</Feature>
</Features>`}, nil
case "X_SetBookmark":
// just ignore
return map[string]string{}, nil
default:
return nil, upnp.InvalidActionError
}
}
func (me *contentDirectoryService) handleBrowseDirectChildren(obj object, host string) (map[string]string, error) {
// Read folder and return children
// TODO: check if obj == 0 and return root objects
// TODO: check if special path and return files
@@ -297,26 +327,31 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http
objs = me.getRatingScenes(childPath(paths), host)
}
result, err := xml.Marshal(objs)
return makeBrowseResult(objs, me.updateIDString())
}
func (me *contentDirectoryService) handleBrowseMetadata(obj object, host string) (map[string]string, error) {
var objs []interface{}
var updateID string
// if numeric, then must be scene, otherwise handle as if path
sceneID, err := strconv.Atoi(obj.Path)
if err != nil {
return nil, err
// #1465 - handle root object
if obj.IsRoot() {
objs = getRootObject()
} else {
// HACK: just create a fake storage folder to return. The name won't
// be correct, but hopefully the names returned from handleBrowseDirectChildren
// will be used instead.
objs = []interface{}{makeStorageFolder(obj.ID(), obj.ID(), obj.ParentID())}
}
return map[string]string{
"TotalMatches": fmt.Sprint(len(objs)),
"NumberReturned": fmt.Sprint(len(objs)),
"Result": didl_lite(string(result)),
"UpdateID": me.updateIDString(),
}, nil
case "BrowseMetadata":
updateID = me.updateIDString()
} else {
var scene *models.Scene
if err := me.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
sceneID, err := strconv.Atoi(obj.Path)
if err != nil {
return err
}
scene, err = r.Scene().Find(sceneID)
if err != nil {
return err
@@ -329,48 +364,32 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http
if scene != nil {
upnpObject := sceneToContainer(scene, "-1", host)
result, err := xml.Marshal(upnpObject)
if err != nil {
return nil, err
}
objs = []interface{}{upnpObject}
// http://upnp.org/specs/av/UPnP-av-ContentDirectory-v1-Service.pdf
// maximum update ID is 2**32, then rolls back to 0
const maxUpdateID int64 = 1 << 32
updateID := scene.UpdatedAt.Timestamp.Unix() % maxUpdateID
return map[string]string{
"Result": didl_lite(string(result)),
"NumberReturned": "1",
"TotalMatches": "1",
"UpdateID": fmt.Sprint(updateID),
}, nil
updateID = fmt.Sprint(scene.UpdatedAt.Timestamp.Unix() % maxUpdateID)
} else {
return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, "scene not found")
}
default:
return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "unhandled browse flag: %v", browse.BrowseFlag)
}
case "GetSearchCapabilities":
return makeBrowseResult(objs, updateID)
}
func makeBrowseResult(objs []interface{}, updateID string) (map[string]string, error) {
result, err := xml.Marshal(objs)
if err != nil {
return nil, upnp.Errorf(upnp.ActionFailedErrorCode, "could not marshal objects: %s", err.Error())
}
return map[string]string{
"SearchCaps": "",
"TotalMatches": fmt.Sprint(len(objs)),
"NumberReturned": fmt.Sprint(len(objs)),
"Result": didl_lite(string(result)),
"UpdateID": updateID,
}, nil
// from https://github.com/rclone/rclone/blob/master/cmd/serve/dlna/cds.go
// Samsung Extensions
case "X_GetFeatureList":
return map[string]string{
"FeatureList": `<Features xmlns="urn:schemas-upnp-org:av:avs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd">
<Feature name="samsung.com_BASICVIEW" version="1">
<container id="0" type="object.item.imageItem"/>
<container id="0" type="object.item.audioItem"/>
<container id="0" type="object.item.videoItem"/>
</Feature>
</Features>`}, nil
case "X_SetBookmark":
// just ignore
return map[string]string{}, nil
default:
return nil, upnp.InvalidActionError
}
}
func makeStorageFolder(id, title, parentID string) upnpav.Container {
@@ -387,6 +406,12 @@ func makeStorageFolder(id, title, parentID string) upnpav.Container {
}
}
func getRootObject() []interface{} {
const rootID = "0"
return []interface{}{makeStorageFolder(rootID, "stash", "-1")}
}
func getRootObjects() []interface{} {
const rootID = "0"

View File

@@ -27,8 +27,12 @@ package dlna
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import (
"net/http"
"strings"
"testing"
"github.com/stashapp/stash/pkg/models/mocks"
"github.com/stretchr/testify/assert"
)
func TestEscapeObjectID(t *testing.T) {
@@ -52,3 +56,27 @@ func TestRootParentObjectID(t *testing.T) {
t.FailNow()
}
}
func testHandleBrowse(argsXML string) (map[string]string, error) {
cds := contentDirectoryService{
Server: &Server{},
txnManager: mocks.NewTransactionManager(),
}
r := &http.Request{}
return cds.Handle("Browse", []byte(argsXML), r)
}
func TestBrowseMetadataRoot(t *testing.T) {
argsXML := `<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1"><ObjectID>0</ObjectID><BrowseFlag>BrowseMetadata</BrowseFlag><Filter>*</Filter><StartingIndex>0</StartingIndex><RequestedCount>0</RequestedCount><SortCriteria></SortCriteria></u:Browse>`
_, err := testHandleBrowse(argsXML)
assert.Nil(t, err)
}
func TestBrowseMetadataTags(t *testing.T) {
argsXML := `<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1"><ObjectID>tags</ObjectID><BrowseFlag>BrowseMetadata</BrowseFlag><Filter>*</Filter><StartingIndex>0</StartingIndex><RequestedCount>0</RequestedCount><SortCriteria></SortCriteria></u:Browse>`
_, err := testHandleBrowse(argsXML)
assert.Nil(t, err)
}