From 328ac017c1b3dc63a1285d6ce746c2ea40317775 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 22 Apr 2026 17:56:18 +0100 Subject: [PATCH] serve dlna: remove file extensions from titles to prevent Samsung TV duplication Samsung TVs have a bug where they duplicate file extensions when both the title contains an extension and the MIME type indicates the same file type. For example, "photo.jpg" becomes "photo.jpg.jpg". Remove extensions from while keeping them in the resource URL and MIME type. This provides a cleaner display and prevents Samsung TVs from incorrectly "fixing" what they perceive as missing extensions. --- cmd/serve/dlna/cds.go | 4 +++- cmd/serve/dlna/cds_test.go | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/cmd/serve/dlna/cds.go b/cmd/serve/dlna/cds.go index 0b3cb7063..102879b18 100644 --- a/cmd/serve/dlna/cds.go +++ b/cmd/serve/dlna/cds.go @@ -80,7 +80,9 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi } obj.Class = "object.item." + mediaType[1] + "Item" - obj.Title = fileInfo.Name() + // Remove file extension from title to prevent Samsung TVs from duplicating it + // File type is already provided via MIME type in protocolInfo + obj.Title = strings.TrimSuffix(fileInfo.Name(), filepath.Ext(fileInfo.Name())) obj.Date = upnpav.Timestamp{Time: fileInfo.ModTime()} item := upnpav.Item{ diff --git a/cmd/serve/dlna/cds_test.go b/cmd/serve/dlna/cds_test.go index cef1804d4..a0d6a9023 100644 --- a/cmd/serve/dlna/cds_test.go +++ b/cmd/serve/dlna/cds_test.go @@ -3,7 +3,9 @@ package dlna import ( "context" "encoding/xml" + "path/filepath" "sort" + "strings" "testing" "github.com/anacrolix/dms/soap" @@ -176,6 +178,49 @@ func TestSOAPResponseQuoteEscaping(t *testing.T) { assert.Contains(t, resultStr, """, "SOAP arguments should contain " entities") } +func TestTitleExtensionRemoval(t *testing.T) { + // Test that file extensions are removed from titles to prevent Samsung TV duplication + tests := []struct { + name string + filename string + expected string + }{ + { + name: "image file", + filename: "photo.jpg", + expected: "photo", + }, + { + name: "video file", + filename: "movie.mp4", + expected: "movie", + }, + { + name: "multiple dots", + filename: "file.name.with.dots.mkv", + expected: "file.name.with.dots", + }, + { + name: "no extension", + filename: "filename_no_ext", + expected: "filename_no_ext", + }, + { + name: "hidden file", + filename: ".hidden.txt", + expected: ".hidden", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Simulate the title processing logic + title := strings.TrimSuffix(tt.filename, filepath.Ext(tt.filename)) + assert.Equal(t, tt.expected, title) + }) + } +} + func TestAdjustXMLApostrophes(t *testing.T) { tests := []struct { name string