From 1fad49ee35f1600bf00d7d8edfa4b9274fcb7a3e Mon Sep 17 00:00:00 2001
From: Florian Klink <flokli@flokli.de>
Date: Fri, 24 Nov 2023 15:23:14 +0200
Subject: [PATCH] dlna: also look at "Subs" subdirectory

Apparently it seems pretty common for subtitles to be put in a
subdirectory called "Subs", rather than in the same directory as the
media file itself.

This covers that usecase, by checking the returned listing for a
directory called "Subs" to exist.

If it does, its child nodes are added to the list before they're being
passed to mediaWithResources, allowing these subtitles to be discovered
automatically.
---
 cmd/serve/dlna/cds.go       | 14 +++++++++
 cmd/serve/dlna/dlna_test.go | 57 +++++++++++++++++++++++++++++--------
 2 files changed, 59 insertions(+), 12 deletions(-)

diff --git a/cmd/serve/dlna/cds.go b/cmd/serve/dlna/cds.go
index 98ad42799..8678ad5cb 100644
--- a/cmd/serve/dlna/cds.go
+++ b/cmd/serve/dlna/cds.go
@@ -130,6 +130,20 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
 		return
 	}
 
+	// if there's a "Subs" child directory, add its children to the list as well,
+	// so mediaWithResources is able to find them.
+	for _, node := range dirEntries {
+		if strings.EqualFold(node.Name(), "Subs") && node.IsDir() {
+			subtitleDir := node.(*vfs.Dir)
+			subtitleEntries, err := subtitleDir.ReadDirAll()
+			if err != nil {
+				err = errors.New("failed to list subtitle directory")
+				return nil, err
+			}
+			dirEntries = append(dirEntries, subtitleEntries...)
+		}
+	}
+
 	dirEntries, mediaResources := mediaWithResources(dirEntries)
 	for _, de := range dirEntries {
 		child := object{
diff --git a/cmd/serve/dlna/dlna_test.go b/cmd/serve/dlna/dlna_test.go
index 0270f27ba..5b090acd3 100644
--- a/cmd/serve/dlna/dlna_test.go
+++ b/cmd/serve/dlna/dlna_test.go
@@ -180,8 +180,9 @@ func TestContentDirectoryBrowseDirectChildren(t *testing.T) {
 	require.Contains(t, string(body), "/r/video.srt")
 	require.Contains(t, string(body), "/r/video.en.srt")
 
-	// Then a subdirectory
-	req, err = http.NewRequest("POST", baseURL+serviceControlURL, strings.NewReader(`
+	// Then a subdirectory (subdir)
+	{
+		req, err = http.NewRequest("POST", baseURL+serviceControlURL, strings.NewReader(`
 <?xml version="1.0" encoding="utf-8"?>
 <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
             s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
@@ -196,14 +197,46 @@ func TestContentDirectoryBrowseDirectChildren(t *testing.T) {
         </u:Browse>
     </s:Body>
 </s:Envelope>`))
-	require.NoError(t, err)
-	req.Header.Set("SOAPACTION", `"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"`)
-	resp, err = http.DefaultClient.Do(req)
-	require.NoError(t, err)
-	assert.Equal(t, http.StatusOK, resp.StatusCode)
-	body, err = io.ReadAll(resp.Body)
-	require.NoError(t, err)
-	// expect video.mp4, video.srt, URLs to be in the DIDL
-	require.Contains(t, string(body), "/r/subdir/video.mp4")
-	require.Contains(t, string(body), "/r/subdir/video.srt")
+		require.NoError(t, err)
+		req.Header.Set("SOAPACTION", `"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"`)
+		resp, err = http.DefaultClient.Do(req)
+		require.NoError(t, err)
+		assert.Equal(t, http.StatusOK, resp.StatusCode)
+		body, err = io.ReadAll(resp.Body)
+		require.NoError(t, err)
+		// expect video.mp4, video.srt, URLs to be in the DIDL
+		require.Contains(t, string(body), "/r/subdir/video.mp4")
+		require.Contains(t, string(body), "/r/subdir/video.srt")
+
+	}
+
+	// Then a subdirectory with subtitles separately (subdir2)
+	{
+		req, err = http.NewRequest("POST", baseURL+serviceControlURL, strings.NewReader(`
+<?xml version="1.0" encoding="utf-8"?>
+<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
+            s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
+    <s:Body>
+        <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
+            <ObjectID>%2Fsubdir2</ObjectID>
+            <BrowseFlag>BrowseDirectChildren</BrowseFlag>
+            <Filter>*</Filter>
+            <StartingIndex>0</StartingIndex>
+            <RequestedCount>0</RequestedCount>
+            <SortCriteria></SortCriteria>
+        </u:Browse>
+    </s:Body>
+</s:Envelope>`))
+		require.NoError(t, err)
+		req.Header.Set("SOAPACTION", `"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"`)
+		resp, err = http.DefaultClient.Do(req)
+		require.NoError(t, err)
+		assert.Equal(t, http.StatusOK, resp.StatusCode)
+		body, err = io.ReadAll(resp.Body)
+		require.NoError(t, err)
+		// expect video.mp4, Subs/video.srt, URLs to be in the DIDL
+		require.Contains(t, string(body), "/r/subdir2/video.mp4")
+		require.Contains(t, string(body), "/r/subdir2/Subs/video.srt")
+
+	}
 }