github.com/ncw/rclone@v1.48.1-0.20190724201158-a35aa1360e3e/cmd/serve/dlna/cds.go (about)

     1  package dlna
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"regexp"
    13  	"sort"
    14  
    15  	"github.com/anacrolix/dms/dlna"
    16  	"github.com/anacrolix/dms/upnp"
    17  	"github.com/anacrolix/dms/upnpav"
    18  	"github.com/ncw/rclone/fs"
    19  	"github.com/ncw/rclone/vfs"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  type contentDirectoryService struct {
    24  	*server
    25  	upnp.Eventing
    26  }
    27  
    28  func (cds *contentDirectoryService) updateIDString() string {
    29  	return fmt.Sprintf("%d", uint32(os.Getpid()))
    30  }
    31  
    32  var mediaMimeTypeRegexp = regexp.MustCompile("^(video|audio|image)/")
    33  
    34  // Turns the given entry and DMS host into a UPnP object. A nil object is
    35  // returned if the entry is not of interest.
    36  func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host string) (ret interface{}, err error) {
    37  	obj := upnpav.Object{
    38  		ID:         cdsObject.ID(),
    39  		Restricted: 1,
    40  		ParentID:   cdsObject.ParentID(),
    41  	}
    42  
    43  	if fileInfo.IsDir() {
    44  		obj.Class = "object.container.storageFolder"
    45  		obj.Title = fileInfo.Name()
    46  		ret = upnpav.Container{Object: obj}
    47  		return
    48  	}
    49  
    50  	if !fileInfo.Mode().IsRegular() {
    51  		return
    52  	}
    53  
    54  	mimeType := fs.MimeTypeFromName(fileInfo.Name())
    55  	mediaType := mediaMimeTypeRegexp.FindStringSubmatch(mimeType)
    56  	if mediaType == nil {
    57  		return
    58  	}
    59  
    60  	obj.Class = "object.item." + mediaType[1] + "Item"
    61  	obj.Title = fileInfo.Name()
    62  
    63  	item := upnpav.Item{
    64  		Object: obj,
    65  		Res:    make([]upnpav.Resource, 0, 1),
    66  	}
    67  
    68  	item.Res = append(item.Res, upnpav.Resource{
    69  		URL: (&url.URL{
    70  			Scheme: "http",
    71  			Host:   host,
    72  			Path:   resPath,
    73  			RawQuery: url.Values{
    74  				"path": {cdsObject.Path},
    75  			}.Encode(),
    76  		}).String(),
    77  		ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{
    78  			SupportRange: true,
    79  		}.String()),
    80  		Bitrate:    0,
    81  		Duration:   "",
    82  		Size:       uint64(fileInfo.Size()),
    83  		Resolution: "",
    84  	})
    85  
    86  	ret = item
    87  	return
    88  }
    89  
    90  // Returns all the upnpav objects in a directory.
    91  func (cds *contentDirectoryService) readContainer(o object, host string) (ret []interface{}, err error) {
    92  	node, err := cds.vfs.Stat(o.Path)
    93  	if err != nil {
    94  		return
    95  	}
    96  
    97  	if !node.IsDir() {
    98  		err = errors.New("not a directory")
    99  		return
   100  	}
   101  
   102  	dir := node.(*vfs.Dir)
   103  	dirEntries, err := dir.ReadDirAll()
   104  	if err != nil {
   105  		err = errors.New("failed to list directory")
   106  		return
   107  	}
   108  
   109  	sort.Sort(dirEntries)
   110  
   111  	for _, de := range dirEntries {
   112  		child := object{
   113  			path.Join(o.Path, de.Name()),
   114  		}
   115  		obj, err := cds.cdsObjectToUpnpavObject(child, de, host)
   116  		if err != nil {
   117  			fs.Errorf(cds, "error with %s: %s", child.FilePath(), err)
   118  			continue
   119  		}
   120  		if obj == nil {
   121  			fs.Debugf(cds, "unrecognized file type: %s", de)
   122  			continue
   123  		}
   124  		ret = append(ret, obj)
   125  	}
   126  
   127  	return
   128  }
   129  
   130  type browse struct {
   131  	ObjectID       string
   132  	BrowseFlag     string
   133  	Filter         string
   134  	StartingIndex  int
   135  	RequestedCount int
   136  }
   137  
   138  // ContentDirectory object from ObjectID.
   139  func (cds *contentDirectoryService) objectFromID(id string) (o object, err error) {
   140  	o.Path, err = url.QueryUnescape(id)
   141  	if err != nil {
   142  		return
   143  	}
   144  	if o.Path == "0" {
   145  		o.Path = "/"
   146  	}
   147  	o.Path = path.Clean(o.Path)
   148  	if !path.IsAbs(o.Path) {
   149  		err = fmt.Errorf("bad ObjectID %v", o.Path)
   150  		return
   151  	}
   152  	return
   153  }
   154  
   155  func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *http.Request) (map[string]string, error) {
   156  	host := r.Host
   157  
   158  	switch action {
   159  	case "GetSystemUpdateID":
   160  		return map[string]string{
   161  			"Id": cds.updateIDString(),
   162  		}, nil
   163  	case "GetSortCapabilities":
   164  		return map[string]string{
   165  			"SortCaps": "dc:title",
   166  		}, nil
   167  	case "Browse":
   168  		var browse browse
   169  		if err := xml.Unmarshal(argsXML, &browse); err != nil {
   170  			return nil, err
   171  		}
   172  		obj, err := cds.objectFromID(browse.ObjectID)
   173  		if err != nil {
   174  			return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
   175  		}
   176  		switch browse.BrowseFlag {
   177  		case "BrowseDirectChildren":
   178  			objs, err := cds.readContainer(obj, host)
   179  			if err != nil {
   180  				return nil, upnp.Errorf(upnpav.NoSuchObjectErrorCode, err.Error())
   181  			}
   182  			totalMatches := len(objs)
   183  			objs = objs[func() (low int) {
   184  				low = browse.StartingIndex
   185  				if low > len(objs) {
   186  					low = len(objs)
   187  				}
   188  				return
   189  			}():]
   190  			if browse.RequestedCount != 0 && browse.RequestedCount < len(objs) {
   191  				objs = objs[:browse.RequestedCount]
   192  			}
   193  			result, err := xml.Marshal(objs)
   194  			if err != nil {
   195  				return nil, err
   196  			}
   197  			return map[string]string{
   198  				"TotalMatches":   fmt.Sprint(totalMatches),
   199  				"NumberReturned": fmt.Sprint(len(objs)),
   200  				"Result":         didlLite(string(result)),
   201  				"UpdateID":       cds.updateIDString(),
   202  			}, nil
   203  		case "BrowseMetadata":
   204  			result, err := xml.Marshal(obj)
   205  			if err != nil {
   206  				return nil, err
   207  			}
   208  			return map[string]string{
   209  				"Result": didlLite(string(result)),
   210  			}, nil
   211  		default:
   212  			return nil, upnp.Errorf(upnp.ArgumentValueInvalidErrorCode, "unhandled browse flag: %v", browse.BrowseFlag)
   213  		}
   214  	case "GetSearchCapabilities":
   215  		return map[string]string{
   216  			"SearchCaps": "",
   217  		}, nil
   218  	// Samsung Extensions
   219  	case "X_GetFeatureList":
   220  		return map[string]string{
   221  			"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">
   222  	<Feature name="samsung.com_BASICVIEW" version="1">
   223  		<container id="/" type="object.item.imageItem"/>
   224  		<container id="/" type="object.item.audioItem"/>
   225  		<container id="/" type="object.item.videoItem"/>
   226  	</Feature>
   227  </Features>`}, nil
   228  	case "X_SetBookmark":
   229  		// just ignore
   230  		return map[string]string{}, nil
   231  	default:
   232  		return nil, upnp.InvalidActionError
   233  	}
   234  }
   235  
   236  // Represents a ContentDirectory object.
   237  type object struct {
   238  	Path string // The cleaned, absolute path for the object relative to the server.
   239  }
   240  
   241  // Returns the actual local filesystem path for the object.
   242  func (o *object) FilePath() string {
   243  	return filepath.FromSlash(o.Path)
   244  }
   245  
   246  // Returns the ObjectID for the object. This is used in various ContentDirectory actions.
   247  func (o object) ID() string {
   248  	if !path.IsAbs(o.Path) {
   249  		log.Panicf("Relative object path: %s", o.Path)
   250  	}
   251  	if len(o.Path) == 1 {
   252  		return "0"
   253  	}
   254  	return url.QueryEscape(o.Path)
   255  }
   256  
   257  func (o *object) IsRoot() bool {
   258  	return o.Path == "/"
   259  }
   260  
   261  // Returns the object's parent ObjectID. Fortunately it can be deduced from the
   262  // ObjectID (for now).
   263  func (o object) ParentID() string {
   264  	if o.IsRoot() {
   265  		return "-1"
   266  	}
   267  	o.Path = path.Dir(o.Path)
   268  	return o.ID()
   269  }