diff --git a/db/db.go b/db/db.go index ab5a9485..4a2c0544 100644 --- a/db/db.go +++ b/db/db.go @@ -225,6 +225,7 @@ type Track struct { Album *Album AlbumID int `gorm:"not null; unique_index:idx_folder_filename" sql:"default: null; type:int REFERENCES albums(id) ON DELETE CASCADE"` Artists []*Artist `gorm:"many2many:track_artists"` + Lyrics string `sql:"default:null"` Genres []*Genre `gorm:"many2many:track_genres"` Size int `sql:"default: null"` Length int `sql:"default: null"` diff --git a/db/migrations.go b/db/migrations.go index 447bfdf6..16e8fb99 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -73,6 +73,7 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202311082304", migrateTemporaryDisplayAlbumArtist), construct(ctx, "202312110003", migrateAddExtraIndexes), construct(ctx, "202405301140", migrateAddReplayGainFields), + construct(ctx, "202406191355", migrateAddLyrics), } return gormigrate. @@ -818,3 +819,7 @@ func migrateAddExtraIndexes(tx *gorm.DB, _ MigrationContext) error { func migrateAddReplayGainFields(tx *gorm.DB, _ MigrationContext) error { return tx.AutoMigrate(Track{}).Error } + +func migrateAddLyrics(tx *gorm.DB, _ MigrationContext) error { + return tx.AutoMigrate(Track{}).Error +} diff --git a/go.mod b/go.mod index d9f6ac2b..32a41107 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,11 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/PuerkitoBio/goquery v1.9.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 // indirect + github.com/env25/mpdlrc v0.7.4 // indirect + github.com/fhs/gompd/v2 v2.3.1-0.20221204164802-46d3f48f8632 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.7.4 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gorilla/context v1.1.2 // indirect @@ -49,6 +54,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/lib/pq v1.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -57,11 +63,13 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect golang.org/x/crypto v0.22.0 // indirect golang.org/x/image v0.15.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 4f423af3..6b8ddeb5 100644 --- a/go.sum +++ b/go.sum @@ -23,14 +23,24 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/env25/mpdlrc v0.7.4 h1:A9If5JKVjgJldaIjJztqkXpUKXlsoRIwXqGK3qS38pY= +github.com/env25/mpdlrc v0.7.4/go.mod h1:Zyhq2WfcYeSJUCtx7mhbXwlh4Xt6mR7HDw0qQh7LvyY= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fhs/gompd/v2 v2.3.1-0.20221204164802-46d3f48f8632 h1:1wiBisyui/BLSk/+W8+ey1gDU3O2PJZdy+Ft/eO/7hw= +github.com/fhs/gompd/v2 v2.3.1-0.20221204164802-46d3f48f8632/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= +github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= @@ -89,6 +99,8 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -117,6 +129,8 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4 h1:SvVjZyjXBOjqjCdMiC9ndyWb709aP5qU4Qbun40GCxA= github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4/go.mod h1:Mpa6Hci7lO3vybfdYlWXmH5gEq2vyOmYYjhrlwCTW3w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -124,6 +138,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -135,7 +150,13 @@ github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981/go.mod h1:Rx8XB1 github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -180,12 +201,16 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -210,6 +235,7 @@ gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g= diff --git a/mockfs/mockfs.go b/mockfs/mockfs.go index a109f80c..2c9d1383 100644 --- a/mockfs/mockfs.go +++ b/mockfs/mockfs.go @@ -362,6 +362,7 @@ func (i *TagInfo) Genres() []string { return []string{i.RawGenre} } func (i *TagInfo) TrackNumber() int { return 1 } func (i *TagInfo) DiscNumber() int { return 1 } func (i *TagInfo) Year() int { return 2021 } +func (i *TagInfo) Lyrics() string { return "[00:01.58] Line one\n[00:03.44] Line two\n" } func (i *TagInfo) ReplayGainTrackGain() float32 { return 0 } func (i *TagInfo) ReplayGainTrackPeak() float32 { return 0 } diff --git a/scanner/scanner.go b/scanner/scanner.go index adbd8d4d..77137dbe 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -461,6 +461,7 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tagcommon. track.FilenameUDec = decoded(basename) track.Size = size track.AlbumID = album.ID + track.Lyrics = trags.Lyrics() track.TagTitle = trags.Title() track.TagTitleUDec = decoded(trags.Title()) diff --git a/server/ctrlsubsonic/ctrl.go b/server/ctrlsubsonic/ctrl.go index 12d0bab2..308cf0c6 100644 --- a/server/ctrlsubsonic/ctrl.go +++ b/server/ctrlsubsonic/ctrl.go @@ -127,6 +127,7 @@ func New(dbc *db.DB, scannr *scanner.Scanner, musicPaths []MusicPath, podcastsPa c.Handle("/getSimilarSongs", chain(resp(c.ServeGetSimilarSongs))) c.Handle("/getSimilarSongs2", chain(resp(c.ServeGetSimilarSongsTwo))) c.Handle("/getLyrics", chain(resp(c.ServeGetLyrics))) + c.Handle("/getLyricsBySongId", chain(resp(c.ServeGetLyricsBySongID))) // raw c.Handle("/getCoverArt", chainRaw(respRaw(c.ServeGetCoverArt))) diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index 90ef5df6..933587a9 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -8,10 +8,12 @@ import ( "net/http" "os" "path/filepath" + "strings" "sync" "time" "unicode" + "github.com/env25/mpdlrc/lrc" "github.com/google/uuid" "github.com/jinzhu/gorm" @@ -41,6 +43,7 @@ func (c *Controller) ServeGetOpenSubsonicExtensions(_ *http.Request) *spec.Respo sub.OpenSubsonicExtensions = &spec.OpenSubsonicExtensions{ {Name: "transcodeOffset", Versions: []int{1}}, {Name: "formPost", Versions: []int{1}}, + {Name: "songLyrics", Versions: []int{1}}, } return sub } @@ -468,12 +471,154 @@ func (c *Controller) ServeJukebox(r *http.Request) *spec.Response { // nolint:go return sub } -func (c *Controller) ServeGetLyrics(_ *http.Request) *spec.Response { +func (c *Controller) ServeGetLyrics(r *http.Request) *spec.Response { + params := r.Context().Value(CtxParams).(params.Params) + artist, _ := params.Get("artist") + title, _ := params.Get("title") + + var track db.Track + q := c.dbc. + Preload("Album"). + Joins("JOIN track_artists ON track_artists.track_id = tracks.id"). + Joins("JOIN artists ON artists.id = track_artists.artist_id"). + Where("tracks.tag_title LIKE ? AND artists.name LIKE ?", title, artist). + First(&track) + if err := q.Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return spec.NewError(70, "couldn't find a track with that id") + } else { + return spec.NewError(0, "lyrics: %v", err) + } + } + + sub := spec.NewResponse() + + if track.Lyrics != "" { + sub.Lyrics = &spec.Lyrics{ + Value: track.Lyrics, + Artist: track.TagTrackArtist, + Title: track.TagTitle, + } + return sub + } + + _, text, err := lyricsFile(&track) + if err != nil { + if os.IsNotExist(err) { + return sub + } + return spec.NewError(0, fmt.Sprintf("lyricsFile: %v", err.Error())) + } + + contents := strings.Join(text, "\n") + + sub.Lyrics = &spec.Lyrics{ + Value: contents, + Artist: track.TagTrackArtist, + Title: track.TagTitle, + } + return sub +} + +func (c *Controller) ServeGetLyricsBySongID(r *http.Request) *spec.Response { + params := r.Context().Value(CtxParams).(params.Params) + id, err := params.GetID("id") + if err != nil { + return spec.NewError(10, "provide an `id` parameter") + } + + var track db.Track + q := c.dbc. + Preload("Album"). + Preload("Album.Artists"). + Preload("Artists"). + Where("id=?", id.Value). + First(&track) + if err := q.Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return spec.NewError(70, "couldn't find a track with that id") + } else { + return spec.NewError(0, "lyrics: %v", err) + } + } + sub := spec.NewResponse() - sub.Lyrics = &spec.Lyrics{} + + if track.Lyrics != "" { + times, lrc, err := lrc.ParseString(track.Lyrics) + if err != nil { + return spec.NewError(0, fmt.Sprintf("lyricsFile: %v", err.Error())) + } + + lines := make([]spec.Lyric, len(times)) + for i, time := range times { + lines[i] = spec.Lyric{ + Start: time.Milliseconds(), + Value: lrc[i], + } + } + + structured := spec.StructuredLyrics{ + Lang: "xxx", + Synced: true, + Lines: lines, + DisplayArtist: track.TagTrackArtist, + DisplayTitle: track.TagTitle, + Offset: 0, + } + + sub.LyricsList = &spec.LyricsList{ + StructuredLyrics: []spec.StructuredLyrics{structured}, + } + return sub + } + + times, lrc, err := lyricsFile(&track) + if err != nil { + if os.IsNotExist(err) { + sub.LyricsList = &spec.LyricsList{ + StructuredLyrics: []spec.StructuredLyrics{}, + } + return sub + } + return spec.NewError(0, fmt.Sprintf("lyricsFile: %v", err.Error())) + } + + lines := make([]spec.Lyric, len(times)) + for i, time := range times { + lines[i] = spec.Lyric{ + Start: time.Milliseconds(), + Value: lrc[i], + } + } + + structured := spec.StructuredLyrics{ + Lang: "xxx", + Synced: true, + Lines: lines, + DisplayArtist: track.TagTrackArtist, + DisplayTitle: track.TagTitle, + Offset: 0, + } + + sub.LyricsList = &spec.LyricsList{ + StructuredLyrics: []spec.StructuredLyrics{structured}, + } return sub } +func lyricsFile(file *db.Track) ([]lrc.Duration, []lrc.Text, error) { + dir := filepath.Dir(file.AbsPath()) + filename := strings.TrimSuffix(filepath.Base(file.AbsPath()), filepath.Ext(file.AbsPath())) + + lrcContent, err := os.ReadFile(filepath.Join(dir, filename+".lrc")) + if err != nil { + return []lrc.Duration{}, []lrc.Text{}, err + } + + return lrc.Parse(lrcContent) +} + func scrobbleStatsUpdateTrack(dbc *db.DB, track *db.Track, userID int, playTime time.Time) error { var play db.Play if err := dbc.Where("album_id=? AND user_id=?", track.AlbumID, userID).First(&play).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index 48055b08..b63e148a 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -69,6 +69,7 @@ type Response struct { SimilarSongsTwo *SimilarSongsTwo `xml:"similarSongs2" json:"similarSongs2,omitempty"` InternetRadioStations *InternetRadioStations `xml:"internetRadioStations" json:"internetRadioStations,omitempty"` Lyrics *Lyrics `xml:"lyrics" json:"lyrics,omitempty"` + LyricsList *LyricsList `xml:"lyricsList" json:"lyricsList,omitempty"` } func NewResponse() *Response { @@ -468,6 +469,24 @@ type Lyrics struct { Title string `xml:"title,attr,omitempty" json:"title,omitempty"` } +type Lyric struct { + Start int64 `xml:"start,attr" json:"start"` + Value string `xml:",chardata" json:"value"` +} + +type LyricsList struct { + StructuredLyrics []StructuredLyrics `xml:"structuredLyrics" json:"structuredLyrics"` +} + +type StructuredLyrics struct { + Lang string `xml:"lang,attr" json:"lang"` // ISO 639 (or und, xxx if unknown) + Synced bool `xml:"synced,attr" json:"synced"` + Lines []Lyric `xml:"line" json:"line"` + DisplayArtist string `xml:"displayArtist,attr,omitempty" json:"displayArtist,omitempty"` + DisplayTitle string `xml:"displayTitle,attr,omitempty" json:"displayTitle,omitempty"` + Offset int `xml:"offset,attr,omitempty" json:"offset,omitempty"` +} + type OpenSubsonicExtension struct { Name string `xml:"name,attr" json:"name"` Versions []int `xml:"versions" json:"versions"` diff --git a/tags/tagcommon/tagcommmon.go b/tags/tagcommon/tagcommmon.go index 10da58d1..e93a2898 100644 --- a/tags/tagcommon/tagcommmon.go +++ b/tags/tagcommon/tagcommmon.go @@ -25,6 +25,7 @@ type Info interface { TrackNumber() int DiscNumber() int Year() int + Lyrics() string ReplayGainTrackGain() float32 ReplayGainTrackPeak() float32 diff --git a/tags/taglib/taglib.go b/tags/taglib/taglib.go index ba2ab827..e2422cc0 100644 --- a/tags/taglib/taglib.go +++ b/tags/taglib/taglib.go @@ -51,6 +51,9 @@ func (i *info) Genres() []string { return find(i.raw, "genres") } func (i *info) TrackNumber() int { return intSep("/", first(find(i.raw, "tracknumber"))) } // eg. 5/12 func (i *info) DiscNumber() int { return intSep("/", first(find(i.raw, "discnumber"))) } // eg. 1/2 func (i *info) Year() int { return intSep("-", first(find(i.raw, "originaldate", "date", "year"))) } // eg. 2023-12-01 +func (i *info) Lyrics() string { + return first(find(i.raw, "lyrics", "lyrics:description", "USLT:description", "LYRICS", "Lyrics", "©lyr")) +} func (i *info) ReplayGainTrackGain() float32 { return dB(first(find(i.raw, "replaygain_track_gain"))) } func (i *info) ReplayGainTrackPeak() float32 { return flt(first(find(i.raw, "replaygain_track_peak"))) }