mirror of
https://github.com/rclone/rclone
synced 2024-11-28 06:41:41 +01:00
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.
This commit is contained in:
parent
d210fecf3b
commit
6337cc70d3
@ -46,6 +46,7 @@ func init() {
|
|||||||
{"video/webm", ".webm"},
|
{"video/webm", ".webm"},
|
||||||
{"video/x-msvideo", ".avi"},
|
{"video/x-msvideo", ".avi"},
|
||||||
{"video/x-matroska", ".mpv,.mkv"},
|
{"video/x-matroska", ".mpv,.mkv"},
|
||||||
|
{"text/srt", ".srt"},
|
||||||
} {
|
} {
|
||||||
for _, ext := range strings.Split(t.extensions, ",") {
|
for _, ext := range strings.Split(t.extensions, ",") {
|
||||||
err := mime.AddExtensionType(ext, t.mimeType)
|
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
|
// Turns the given entry and DMS host into a UPnP object. A nil object is
|
||||||
// returned if the entry is not of interest.
|
// 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{
|
obj := upnpav.Object{
|
||||||
ID: cdsObject.ID(),
|
ID: cdsObject.ID(),
|
||||||
Restricted: 1,
|
Restricted: 1,
|
||||||
@ -128,12 +129,25 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi
|
|||||||
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{
|
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{
|
||||||
SupportRange: true,
|
SupportRange: true,
|
||||||
}.String()),
|
}.String()),
|
||||||
Bitrate: 0,
|
Size: uint64(fileInfo.Size()),
|
||||||
Duration: "",
|
|
||||||
Size: uint64(fileInfo.Size()),
|
|
||||||
Resolution: "",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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
|
ret = item
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -157,13 +171,13 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(dirEntries)
|
dirEntries, extraResources := partitionExtraResources(dirEntries)
|
||||||
|
|
||||||
for _, de := range dirEntries {
|
for _, de := range dirEntries {
|
||||||
child := object{
|
child := object{
|
||||||
path.Join(o.Path, de.Name()),
|
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 {
|
if err != nil {
|
||||||
fs.Errorf(cds, "error with %s: %s", child.FilePath(), err)
|
fs.Errorf(cds, "error with %s: %s", child.FilePath(), err)
|
||||||
continue
|
continue
|
||||||
@ -178,6 +192,53 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
|
|||||||
return
|
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 {
|
type browse struct {
|
||||||
ObjectID string
|
ObjectID string
|
||||||
BrowseFlag string
|
BrowseFlag string
|
||||||
@ -256,7 +317,8 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -279,9 +341,9 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt
|
|||||||
return map[string]string{
|
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">
|
"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">
|
<Feature name="samsung.com_BASICVIEW" version="1">
|
||||||
<container id="/" type="object.item.imageItem"/>
|
<container id="0" type="object.item.imageItem"/>
|
||||||
<container id="/" type="object.item.audioItem"/>
|
<container id="0" type="object.item.audioItem"/>
|
||||||
<container id="/" type="object.item.videoItem"/>
|
<container id="0" type="object.item.videoItem"/>
|
||||||
</Feature>
|
</Feature>
|
||||||
</Features>`}, nil
|
</Features>`}, nil
|
||||||
case "X_SetBookmark":
|
case "X_SetBookmark":
|
||||||
|
@ -134,7 +134,11 @@ func (lrw *loggingResponseWriter) logRequest(code int, err interface{}) {
|
|||||||
level = fs.LogLevelError
|
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.RemoteAddr, lrw.request.Method, code,
|
||||||
lrw.request.Header.Get("SOAPACTION"), err)
|
lrw.request.Header.Get("SOAPACTION"), err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user