From 15e1394fa337fbb6d54f51d5358507bfba41a615 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Deluan=20Quint=C3=A3o?= <deluan@navidrome.org>
Date: Fri, 22 Dec 2023 21:03:55 -0500
Subject: [PATCH] Implement `originalReleaseDate` in OpenSubsonic responses.
 (#2733)

See https://github.com/opensubsonic/open-subsonic-api/pull/80
---
 server/subsonic/helpers.go                    | 20 ++++++++++++++++++
 server/subsonic/helpers_test.go               | 11 ++++++++++
 ...mWithSongsID3 with data should match .JSON |  5 +++++
 ...umWithSongsID3 with data should match .XML |  1 +
 ...thSongsID3 without data should match .JSON |  3 ++-
 ...ithSongsID3 without data should match .XML |  4 +++-
 server/subsonic/responses/responses.go        | 21 ++++++++++++-------
 server/subsonic/responses/responses_test.go   |  3 ++-
 8 files changed, 58 insertions(+), 10 deletions(-)

diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go
index 706a12c8b..46d1ab310 100644
--- a/server/subsonic/helpers.go
+++ b/server/subsonic/helpers.go
@@ -7,6 +7,7 @@ import (
 	"mime"
 	"net/http"
 	"sort"
+	"strconv"
 	"strings"
 
 	"github.com/navidrome/navidrome/consts"
@@ -244,6 +245,24 @@ func childrenFromAlbums(ctx context.Context, als model.Albums) []responses.Child
 	return children
 }
 
+// toItemDate converts a string date in the formats 'YYYY-MM-DD', 'YYYY-MM' or 'YYYY' to an OS ItemDate
+func toItemDate(date string) responses.ItemDate {
+	itemDate := responses.ItemDate{}
+	if date == "" {
+		return itemDate
+	}
+	parts := strings.Split(date, "-")
+	if len(parts) > 2 {
+		itemDate.Day, _ = strconv.Atoi(parts[2])
+	}
+	if len(parts) > 1 {
+		itemDate.Month, _ = strconv.Atoi(parts[1])
+	}
+	itemDate.Year, _ = strconv.Atoi(parts[0])
+
+	return itemDate
+}
+
 func buildItemGenres(genres model.Genres) []responses.ItemGenre {
 	itemGenres := make([]responses.ItemGenre, len(genres))
 	for i, g := range genres {
@@ -301,5 +320,6 @@ func buildAlbumID3(ctx context.Context, album model.Album) responses.AlbumID3 {
 	dir.MusicBrainzId = album.MbzAlbumID
 	dir.IsCompilation = album.Compilation
 	dir.SortName = album.SortAlbumName
+	dir.OriginalReleaseDate = toItemDate(album.OriginalDate)
 	return dir
 }
diff --git a/server/subsonic/helpers_test.go b/server/subsonic/helpers_test.go
index 898ca2a6c..8b33ebda4 100644
--- a/server/subsonic/helpers_test.go
+++ b/server/subsonic/helpers_test.go
@@ -57,4 +57,15 @@ var _ = Describe("helpers", func() {
 			Expect(buildDiscSubtitles(context.Background(), album)).To(Equal(expected))
 		})
 	})
+
+	DescribeTable("toItemDate",
+		func(date string, expected responses.ItemDate) {
+			Expect(toItemDate(date)).To(Equal(expected))
+		},
+		Entry("1994-02-04", "1994-02-04", responses.ItemDate{Year: 1994, Month: 2, Day: 4}),
+		Entry("1994-02", "1994-02", responses.ItemDate{Year: 1994, Month: 2}),
+		Entry("1994", "1994", responses.ItemDate{Year: 1994}),
+		Entry("19940201", "", responses.ItemDate{}),
+		Entry("", "", responses.ItemDate{}),
+	)
 })
diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON
index 15ddbbcea..b7ba2d1dd 100644
--- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON	
+++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .JSON	
@@ -31,6 +31,11 @@
         "title": "disc 2"
       }
     ],
+    "originalReleaseDate": {
+      "year": 1994,
+      "month": 2,
+      "day": 4
+    },
     "song": [
       {
         "id": "1",
diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML
index cfe3da99d..fd37f83f9 100644
--- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML	
+++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 with data should match .XML	
@@ -4,6 +4,7 @@
     <genres name="progressive"></genres>
     <discTitles disc="1" title="disc 1"></discTitles>
     <discTitles disc="2" title="disc 2"></discTitles>
+    <originalReleaseDate year="1994" month="2" day="4"></originalReleaseDate>
     <song id="1" isDir="true" title="title" album="album" artist="artist" track="1" year="1985" genre="Rock" coverArt="1" size="8421341" contentType="audio/flac" suffix="flac" starred="2016-03-02T20:30:00Z" transcodedContentType="audio/mpeg" transcodedSuffix="mp3" duration="146" bitRate="320" isVideo="false" bpm="127" comment="a comment" sortName="sorted song" mediaType="song" musicBrainzId="4321">
       <genres name="rock"></genres>
       <genres name="progressive"></genres>
diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON
index 9508e14ba..8a5b87f24 100644
--- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON	
+++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .JSON	
@@ -12,6 +12,7 @@
     "musicBrainzId": "",
     "isCompilation": false,
     "sortName": "",
-    "discTitles": []
+    "discTitles": [],
+    "originalReleaseDate": {}
   }
 }
diff --git a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML
index 58e03c04b..2b748953a 100644
--- a/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML	
+++ b/server/subsonic/responses/.snapshots/Responses AlbumWithSongsID3 without data should match .XML	
@@ -1,3 +1,5 @@
 <subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.8.0" type="navidrome" serverVersion="v0.0.0" openSubsonic="true">
-  <album id="" name="" userRating="0" musicBrainzId="" isCompilation="false" sortName=""></album>
+  <album id="" name="" userRating="0" musicBrainzId="" isCompilation="false" sortName="">
+    <originalReleaseDate></originalReleaseDate>
+  </album>
 </subsonic-response>
diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go
index f93a5967b..eb5a8c883 100644
--- a/server/subsonic/responses/responses.go
+++ b/server/subsonic/responses/responses.go
@@ -218,13 +218,14 @@ type AlbumID3 struct {
 	Genre     string     `xml:"genre,attr,omitempty"               json:"genre,omitempty"`
 
 	// OpenSubsonic extensions
-	Played        *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
-	UserRating    int32      `xml:"userRating,attr"       json:"userRating"`
-	Genres        ItemGenres `xml:"genres"                json:"genres"`
-	MusicBrainzId string     `xml:"musicBrainzId,attr"    json:"musicBrainzId"`
-	IsCompilation bool       `xml:"isCompilation,attr"    json:"isCompilation"`
-	SortName      string     `xml:"sortName,attr"         json:"sortName"`
-	DiscTitles    DiscTitles `xml:"discTitles"            json:"discTitles"`
+	Played              *time.Time `xml:"played,attr,omitempty" json:"played,omitempty"`
+	UserRating          int32      `xml:"userRating,attr"       json:"userRating"`
+	Genres              ItemGenres `xml:"genres"                json:"genres"`
+	MusicBrainzId       string     `xml:"musicBrainzId,attr"    json:"musicBrainzId"`
+	IsCompilation       bool       `xml:"isCompilation,attr"    json:"isCompilation"`
+	SortName            string     `xml:"sortName,attr"         json:"sortName"`
+	DiscTitles          DiscTitles `xml:"discTitles"            json:"discTitles"`
+	OriginalReleaseDate ItemDate   `xml:"originalReleaseDate"   json:"originalReleaseDate"`
 }
 
 type ArtistWithAlbumsID3 struct {
@@ -492,3 +493,9 @@ func marshalJSONArray[T any](v []T) ([]byte, error) {
 	a := v
 	return json.Marshal(a)
 }
+
+type ItemDate struct {
+	Year  int `xml:"year,attr,omitempty" json:"year,omitempty"`
+	Month int `xml:"month,attr,omitempty" json:"month,omitempty"`
+	Day   int `xml:"day,attr,omitempty" json:"day,omitempty"`
+}
diff --git a/server/subsonic/responses/responses_test.go b/server/subsonic/responses/responses_test.go
index 17ea9ec3d..47804d6ba 100644
--- a/server/subsonic/responses/responses_test.go
+++ b/server/subsonic/responses/responses_test.go
@@ -176,7 +176,8 @@ var _ = Describe("Responses", func() {
 					Id: "1", Name: "album", Artist: "artist", Genre: "rock",
 					Genres:        []ItemGenre{{Name: "rock"}, {Name: "progressive"}},
 					MusicBrainzId: "1234", IsCompilation: true, SortName: "sorted album",
-					DiscTitles: DiscTitles{{Disc: 1, Title: "disc 1"}, {Disc: 2, Title: "disc 2"}},
+					DiscTitles:          DiscTitles{{Disc: 1, Title: "disc 1"}, {Disc: 2, Title: "disc 2"}},
+					OriginalReleaseDate: ItemDate{Year: 1994, Month: 2, Day: 4},
 				}
 				t := time.Date(2016, 03, 2, 20, 30, 0, 0, time.UTC)
 				songs := []Child{{