From 64d0aee8dc0d5a9e6b7a9dddb09021a813b91bee Mon Sep 17 00:00:00 2001 From: Alex McGrath Date: Fri, 17 Apr 2020 13:53:45 +0100 Subject: [PATCH] Add support for the jukebox endpoint This supports most of jukeboxControl.view as far as i can tell. Things seem to be playing ok without freaking out I've also only tested it a little bit with ultrasonic but it does appear to be working pretty well --- go.mod | 1 + go.sum | 30 ++++ jukebox/jukebox.go | 192 +++++++++++++++++++++++++ server/ctrlbase/ctrl.go | 2 + server/ctrlsubsonic/handlers_common.go | 49 +++++++ server/ctrlsubsonic/spec/spec.go | 14 ++ server/server.go | 4 + 7 files changed, 292 insertions(+) create mode 100644 jukebox/jukebox.go diff --git a/go.mod b/go.mod index 257de3f..2b66c0b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/Masterminds/sprig v2.20.0+incompatible github.com/cespare/xxhash v1.1.0 github.com/dustin/go-humanize v1.0.0 + github.com/faiface/beep v1.0.2 github.com/google/uuid v1.1.1 // indirect github.com/gorilla/mux v1.7.3 github.com/gorilla/securecookie v1.1.1 diff --git a/go.sum b/go.sum index 85239a4..0190c57 100644 --- a/go.sum +++ b/go.sum @@ -38,7 +38,11 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 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/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ= +github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= @@ -66,6 +70,12 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw= +github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4= +github.com/gopherjs/gopherwasm v1.0.0 h1:32nge/RlujS1Im4HNCJPp0NbBOAeBXFuT1KonUuLl+Y= +github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -75,6 +85,11 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/hajimehoshi/go-mp3 v0.1.1 h1:Y33fAdTma70fkrxnc9u50Uq0lV6eZ+bkAlssdMmCwUc= +github.com/hajimehoshi/go-mp3 v0.1.1/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw= +github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04= +github.com/hajimehoshi/oto v0.3.1 h1:cpf/uIv4Q0oc5uf9loQn7PIehv+mZerh+0KKma6gzMk= +github.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -82,6 +97,8 @@ github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0 github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM= +github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/gorm v1.9.10 h1:HvrsqdhCW78xpJF67g1hMxS6eCToo9PZH4LDB8WKPac= github.com/jinzhu/gorm v1.9.10/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY= @@ -111,9 +128,13 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mewkiz/flac v1.0.5 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc= +github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd h1:xKn/gU8lZupoZt/HE7a/R3aH93iUO6JwyRsYelQUsRI= github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd/go.mod h1:B6icauz2l4tkYQxmDtCH4qmNWz/evSW5CsOqp6IE5IE= @@ -161,12 +182,18 @@ golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20180806140643-507816974b79 h1:t2JRgCWkY7Qaa1J2jal+wqC9OjbyHCHwIA9rVlRUSMo= +golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -192,10 +219,12 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= @@ -230,6 +259,7 @@ google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dT google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/jukebox/jukebox.go b/jukebox/jukebox.go new file mode 100644 index 0000000..8a7b126 --- /dev/null +++ b/jukebox/jukebox.go @@ -0,0 +1,192 @@ +package jukebox + +import ( + "os" + "path" + "sync" + "time" + + "github.com/faiface/beep" + "github.com/faiface/beep/flac" + "github.com/faiface/beep/mp3" + "github.com/faiface/beep/speaker" + "go.senan.xyz/gonic/db" + "go.senan.xyz/gonic/server/ctrlsubsonic/spec" +) + +type strmInfo struct { + ctrlStrmr beep.Ctrl + strm beep.StreamSeekCloser + format beep.Format +} + +type Jukebox struct { + playlist []*db.Track + index int + playing bool + // used to notify the player to re read the members + updates chan struct{} + done chan bool + info *strmInfo + sync.Mutex +} + +func (j *Jukebox) Init(musicPath string) error { + j.updates = make(chan struct{}) + sr := beep.SampleRate(48000) + err := speaker.Init(sr, sr.N(time.Second/2)) + if err != nil { + return err + } + j.done = make(chan bool) + go func() { + for range j.updates { + var streamer beep.Streamer + var format beep.Format + f, err := os.Open(path.Join(musicPath, j.playlist[j.index].RelPath())) + if err != nil { + j.index++ + continue + } + switch j.playlist[j.index].Ext() { + case "mp3": + streamer, format, err = mp3.Decode(f) + case "flac": + streamer, format, err = flac.Decode(f) + default: + j.index++ + continue + } + if err != nil { + j.index++ + continue + } + if j.playing { + j.Lock() + { + j.info = &strmInfo{} + j.info.strm = streamer.(beep.StreamSeekCloser) + j.info.ctrlStrmr.Streamer = beep.Resample(4, format.SampleRate, sr, j.info.strm) + j.info.format = format + } + j.Unlock() + speaker.Play(beep.Seq(&j.info.ctrlStrmr, beep.Callback(func() { + j.done <- false + }))) + if v := <-j.done; !v { + j.index++ + j.Lock() + if j.index > len(j.playlist) { + j.index = 0 + j.playing = false + } + j.Unlock() + // in a go routine as otherwise this hangs as the + go func() { + j.updates <- struct{}{} + }() + } else { + continue + } + } + } + }() + return nil +} + +func (j *Jukebox) SetTracks(tracks []*db.Track) { + j.Lock() + j.index = 0 + j.playing = true + if len(j.playlist) > 0 { + j.done <- true + j.playlist = []*db.Track{} + speaker.Clear() + } + j.playlist = tracks + j.Unlock() + j.updates <- struct{}{} +} + +func (j *Jukebox) AddTracks(tracks []*db.Track) { + j.Lock() + j.playlist = append(j.playlist, tracks...) + j.Unlock() +} + +func (j *Jukebox) ClearTracks() { + j.Lock() + j.index = 0 + j.playing = false + j.playlist = []*db.Track{} + j.Unlock() +} + +func (j *Jukebox) RemoveTrack(i int) { + j.Lock() + defer j.Unlock() + if i < 0 || i > len(j.playlist) { + return + } + j.playlist = append(j.playlist[:i], j.playlist[i+1:]...) +} + +func (j *Jukebox) Status() *spec.JukeboxStatus { + position := 0 + if j.info != nil { + length := j.info.format.SampleRate.D(j.info.strm.Position()) + position = int(length.Round(time.Millisecond).Seconds()) + } + return &spec.JukeboxStatus{ + CurrentIndex: j.index, + Playing: j.playing, + Gain: 0.9, + Position: position, + } +} + +func (j *Jukebox) GetTracks() *spec.JukeboxPlaylist { + j.Lock() + defer j.Unlock() + jb := &spec.JukeboxPlaylist{} + jb.List = make([]*spec.TrackChild, len(j.playlist)) + for i, track := range j.playlist { + jb.List[i] = spec.NewTrackByTags(track, track.Album) + } + jb.CurrentIndex = j.index + jb.Playing = j.playing + jb.Gain = 0.9 + jb.Position = 0 + if j.info != nil { + length := j.info.format.SampleRate.D(j.info.strm.Position()) + jb.Position = int(length.Round(time.Millisecond).Seconds()) + } + return jb +} + +func (j *Jukebox) Stop() { + j.Lock() + j.playing = false + j.info.ctrlStrmr.Paused = true + j.Unlock() +} + +func (j *Jukebox) Start() { + j.Lock() + j.playing = false + j.info.ctrlStrmr.Paused = false + j.Unlock() +} + +func (j *Jukebox) Skip(i int, skipCurrent bool) { + j.Lock() + if skipCurrent { + j.index++ + } else { + j.index = i + } + speaker.Clear() + j.done <- true + j.updates <- struct{}{} + j.Unlock() +} diff --git a/server/ctrlbase/ctrl.go b/server/ctrlbase/ctrl.go index 194ccc0..a886bf8 100644 --- a/server/ctrlbase/ctrl.go +++ b/server/ctrlbase/ctrl.go @@ -7,6 +7,7 @@ import ( "path" "go.senan.xyz/gonic/db" + "go.senan.xyz/gonic/jukebox" "go.senan.xyz/gonic/scanner" ) @@ -49,6 +50,7 @@ type Controller struct { MusicPath string Scanner *scanner.Scanner ProxyPrefix string + Jukebox *jukebox.Jukebox } // Path returns a URL path with the proxy prefix included diff --git a/server/ctrlsubsonic/handlers_common.go b/server/ctrlsubsonic/handlers_common.go index b64ab7a..10d9743 100644 --- a/server/ctrlsubsonic/handlers_common.go +++ b/server/ctrlsubsonic/handlers_common.go @@ -112,6 +112,7 @@ func (c *Controller) ServeGetUser(r *http.Request) *spec.Response { sub.User = &spec.User{ Username: user.Name, AdminRole: user.IsAdmin, + JukeboxRole: true, ScrobblingEnabled: user.LastFMSession != "", Folder: []int{1}, } @@ -313,3 +314,51 @@ func (c *Controller) ServeGetRandomSongs(r *http.Request) *spec.Response { } return sub } + +func (c *Controller) ServeJukebox(r *http.Request) *spec.Response { + params := r.Context().Value(CtxParams).(params.Params) + switch params.Get("action") { + case "set": + var tracks []*db.Track + ids := params.GetFirstListInt("id") + if len(ids) == 0 { + c.Jukebox.ClearTracks() + } + for _, id := range ids { + track := &db.Track{} + err := c.DB.Preload("Album").First(track, id).Error + if err != nil { + return spec.NewError(10, "couldn't find tracks with provided ids") + } + tracks = append(tracks, track) + } + c.Jukebox.SetTracks(tracks) + case "clear": + c.Jukebox.ClearTracks() + case "remove": + index, err := params.GetInt("index") + if err != nil { + return spec.NewError(10, "please provide an id for remove actions") + } + c.Jukebox.RemoveTrack(index) + case "stop": + c.Jukebox.Stop() + case "start": + c.Jukebox.Start() + case "skip": + index, err := params.GetInt("index") + var skipCurrent bool + if err != nil { + skipCurrent = true + } + c.Jukebox.Skip(index, skipCurrent) + case "get": + sub := spec.NewResponse() + sub.JukeboxPlaylist = c.Jukebox.GetTracks() + return sub + } + // All actions except get are expected to return a status + sub := spec.NewResponse() + sub.JukeboxStatus = c.Jukebox.Status() + return sub +} diff --git a/server/ctrlsubsonic/spec/spec.go b/server/ctrlsubsonic/spec/spec.go index da20b09..12cfc61 100644 --- a/server/ctrlsubsonic/spec/spec.go +++ b/server/ctrlsubsonic/spec/spec.go @@ -41,6 +41,8 @@ type Response struct { ArtistInfoTwo *ArtistInfo `xml:"artistInfo2" json:"artistInfo2,omitempty"` Genres *Genres `xml:"genres" json:"genres,omitempty"` PlayQueue *PlayQueue `xml:"playQueue" json:"playQueue,omitempty"` + JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus" json:"jukeboxStatus,omitempty"` + JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist" json:"jukeboxPlaylist,omitempty"` } func NewResponse() *Response { @@ -269,3 +271,15 @@ type PlayQueue struct { ChangedBy string `xml:"changedBy,attr" json:"changedBy"` List []*TrackChild `xml:"entry,omitempty" json:"entry,omitempty"` } + +type JukeboxStatus struct { + CurrentIndex int `xml:"currentIndex,attr" json:"currentIndex"` + Playing bool `xml:"playing,attr" json:"playing"` + Gain float64 `xml:"gain,attr" json:"gain"` + Position int `xml:"position,attr" json:"position"` +} + +type JukeboxPlaylist struct { + List []*TrackChild `xml:"entry,omitempty" json:"entry,omitempty"` + JukeboxStatus +} diff --git a/server/server.go b/server/server.go index 38b0528..8749fe9 100644 --- a/server/server.go +++ b/server/server.go @@ -11,6 +11,7 @@ import ( "github.com/gorilla/mux" "go.senan.xyz/gonic/db" + "go.senan.xyz/gonic/jukebox" "go.senan.xyz/gonic/scanner" "go.senan.xyz/gonic/server/assets" "go.senan.xyz/gonic/server/ctrladmin" @@ -46,7 +47,9 @@ func New(opts Options) *Server { MusicPath: opts.MusicPath, ProxyPrefix: opts.ProxyPrefix, Scanner: scanner, + Jukebox: &jukebox.Jukebox{}, } + base.Jukebox.Init(opts.MusicPath) // router with common wares for admin / subsonic r := mux.NewRouter() r.Use(base.WithLogging) @@ -154,6 +157,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) { r.Handle("/getSong{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSong)) r.Handle("/getRandomSongs{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetRandomSongs)) r.Handle("/getSongsByGenre{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetSongsByGenre)) + r.Handle("/jukeboxControl{_:(?:\\.view)?}", ctrl.H(ctrl.ServeJukebox)) // ** begin raw r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeDownload)) r.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt))