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 }