From 6337cc70d320868f47995fdf89290dd845f9b47d Mon Sep 17 00:00:00 2001 From: Dan Walters Date: Sun, 22 Sep 2019 18:58:24 -0500 Subject: [PATCH] dlna: support for external srt subtitles Allows for filename.srt, filename.en.srt, etc., to be automatically associated with video.mp4 (or whatever) when playing over dlna. This is the "modern" method, which I've verified to work on VLC and in LG webOS 2. There is a vendor specific mechanism for Samsung that I havn't been able to get working on my F series. Also made some minor corrections to logging and container IDs. --- cmd/serve/dlna/cds.go | 84 ++++++++++++++++++++++++++++++++----- cmd/serve/dlna/dlna_util.go | 6 ++- 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/cmd/serve/dlna/cds.go b/cmd/serve/dlna/cds.go index 9b7d1ad59..716d3140e 100644 --- a/cmd/serve/dlna/cds.go +++ b/cmd/serve/dlna/cds.go @@ -46,6 +46,7 @@ func init() { {"video/webm", ".webm"}, {"video/x-msvideo", ".avi"}, {"video/x-matroska", ".mpv,.mkv"}, + {"text/srt", ".srt"}, } { for _, ext := range strings.Split(t.extensions, ",") { err := mime.AddExtensionType(ext, t.mimeType) @@ -69,7 +70,7 @@ var mediaMimeTypeRegexp = regexp.MustCompile("^(video|audio|image)/") // Turns the given entry and DMS host into a UPnP object. A nil object is // returned if the entry is not of interest. -func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo vfs.Node, host string) (ret interface{}, err error) { +func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo vfs.Node, resources vfs.Nodes, host string) (ret interface{}, err error) { obj := upnpav.Object{ ID: cdsObject.ID(), Restricted: 1, @@ -128,12 +129,25 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ SupportRange: true, }.String()), - Bitrate: 0, - Duration: "", - Size: uint64(fileInfo.Size()), - Resolution: "", + Size: uint64(fileInfo.Size()), }) + basePath, _ := path.Split(cdsObject.Path) + for _, resource := range resources { + subtitleURL := (&url.URL{ + Scheme: "http", + Host: host, + Path: resPath, + RawQuery: url.Values{ + "path": {basePath + resource.Path()}, + }.Encode(), + }).String() + item.Res = append(item.Res, upnpav.Resource{ + URL: subtitleURL, + ProtocolInfo: fmt.Sprintf("http-get:*:%s:*", "text/srt"), + }) + } + ret = item return } @@ -157,13 +171,13 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return } - sort.Sort(dirEntries) + dirEntries, extraResources := partitionExtraResources(dirEntries) for _, de := range dirEntries { child := object{ path.Join(o.Path, de.Name()), } - obj, err := cds.cdsObjectToUpnpavObject(child, de, host) + obj, err := cds.cdsObjectToUpnpavObject(child, de, extraResources[de], host) if err != nil { fs.Errorf(cds, "error with %s: %s", child.FilePath(), err) continue @@ -178,6 +192,53 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret [] return } +// Given a list of nodes, separate them into potential media items and any associated resources (external subtitles, +// thumbnails, metadata, etc.) +func partitionExtraResources(nodes vfs.Nodes) (vfs.Nodes, map[vfs.Node]vfs.Nodes) { + // First, separate out the subtitles into a separate list from the media + media, subtitles := make(vfs.Nodes, 0), make(vfs.Nodes, 0) + for _, node := range nodes { + name := strings.ToLower(node.Name()) // case insensitive + switch path.Ext(name) { + case ".srt": + subtitles = append(subtitles, node) + default: + media = append(media, node) + } + } + + // Find the associated media file for each subtitle + extraResources := make(map[vfs.Node]vfs.Nodes) + for _, node := range subtitles { + subtitleName := strings.ToLower(node.Name()) + + // For a media file named "My Video.mp4", we want to associated any subtitles named like + // "My Video.srt", "My Video.en.srt", "My Video.es.srt", "My Video.forced.srt" + // note: nodes must be sorted! vfs.dir.ReadDirAll() results are already sorted .. + mediaIdx := sort.Search(len(media), func(idx int) bool { + mediaName := strings.ToLower(media[idx].Name()) + basename := strings.SplitN(mediaName, ".", 2)[0] + if strings.Compare(subtitleName, basename) <= 0 { + return true + } + if strings.HasPrefix(subtitleName, basename) { + return subtitleName[len(basename)] == '.' + } + return false + }) + if mediaIdx == -1 { + fs.Infof(node, "could not find associated media for subtitle: %s", node.Name()) + continue + } + + mediaNode := media[mediaIdx] + fs.Debugf(mediaNode, "associating subtitle: %s", node.Name()) + extraResources[mediaNode] = append(extraResources[mediaNode], node) + } + + return media, extraResources +} + type browse struct { ObjectID string BrowseFlag string @@ -256,7 +317,8 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt if err != nil { return nil, err } - upnpObject, err := cds.cdsObjectToUpnpavObject(obj, node, host) + // TODO: External subtitles won't appear in the metadata here, but probably should. + upnpObject, err := cds.cdsObjectToUpnpavObject(obj, node, vfs.Nodes{}, host) if err != nil { return nil, err } @@ -279,9 +341,9 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt return map[string]string{ "FeatureList": ` - - - + + + `}, nil case "X_SetBookmark": diff --git a/cmd/serve/dlna/dlna_util.go b/cmd/serve/dlna/dlna_util.go index 78b85aeaf..c46654d48 100644 --- a/cmd/serve/dlna/dlna_util.go +++ b/cmd/serve/dlna/dlna_util.go @@ -134,7 +134,11 @@ func (lrw *loggingResponseWriter) logRequest(code int, err interface{}) { level = fs.LogLevelError } - fs.LogPrintf(level, lrw.request.URL.Path, "%s %s %d %s %s", + if err == nil { + err = "" + } + + fs.LogPrintf(level, lrw.request.URL, "%s %s %d %s %s", lrw.request.RemoteAddr, lrw.request.Method, code, lrw.request.Header.Get("SOAPACTION"), err) }