From 5e66261f0ccd63e6ceda46dc908661a748c16325 Mon Sep 17 00:00:00 2001 From: Brian Doherty Date: Wed, 20 Jul 2022 23:16:13 +0100 Subject: [PATCH] feat(subsonic): add avatar support closes: #228 --- db/migrations.go | 8 ++ db/model.go | 9 +- go.mod | 36 ++++--- go.sum | 49 ++++++++++ server/assets/pages/change_avatar.tmpl | 26 ++++++ server/assets/pages/change_own_avatar.tmpl | 26 ++++++ server/assets/pages/home.tmpl | 9 +- server/assets/partials/head.tmpl | 1 + server/assets/static/main.css | 6 ++ server/assets/static/main.js | 5 + server/assets/static/playlist-upload.js | 3 - server/ctrladmin/ctrl.go | 11 ++- server/ctrladmin/handlers.go | 104 ++++++++++++++++++++- server/ctrlsubsonic/handlers_raw.go | 16 ++++ server/server.go | 7 ++ 15 files changed, 288 insertions(+), 28 deletions(-) create mode 100644 server/assets/pages/change_avatar.tmpl create mode 100644 server/assets/pages/change_own_avatar.tmpl create mode 100644 server/assets/static/main.js delete mode 100644 server/assets/static/playlist-upload.js diff --git a/db/migrations.go b/db/migrations.go index b0008e34..6e8a485b 100644 --- a/db/migrations.go +++ b/db/migrations.go @@ -43,6 +43,7 @@ func (db *DB) Migrate(ctx MigrationContext) error { construct(ctx, "202202241218", migratePublicPlaylist), construct(ctx, "202204270903", migratePodcastDropUserID), construct(ctx, "202206011628", migrateInternetRadioStations), + construct(ctx, "202206101425", migrateUser), } return gormigrate. @@ -363,3 +364,10 @@ func migrateInternetRadioStations(tx *gorm.DB, _ MigrationContext) error { ). Error } + +func migrateUser(tx *gorm.DB, _ MigrationContext) error { + return tx.AutoMigrate( + User{}, + ). + Error +} diff --git a/db/model.go b/db/model.go index de45a871..ed0d2fb3 100644 --- a/db/model.go +++ b/db/model.go @@ -172,6 +172,7 @@ type User struct { ListenBrainzURL string `sql:"default: null"` ListenBrainzToken string `sql:"default: null"` IsAdmin bool `sql:"default: null"` + Avatar []byte `sql:"default: null"` } type Setting struct { @@ -398,10 +399,10 @@ type Bookmark struct { } type InternetRadioStation struct { - ID int `gorm:"primary_key"` - StreamURL string - Name string - HomepageURL string + ID int `gorm:"primary_key"` + StreamURL string + Name string + HomepageURL string } func (ir *InternetRadioStation) SID() *specid.ID { diff --git a/go.mod b/go.mod index 3873d78e..70c7de3b 100644 --- a/go.mod +++ b/go.mod @@ -7,44 +7,58 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible github.com/PuerkitoBio/goquery v1.8.0 // indirect - github.com/cespare/xxhash v1.1.0 + github.com/andybalholm/cascadia v1.3.1 // indirect + github.com/cespare/xxhash v1.1.0 // indirect github.com/disintegration/imaging v1.6.2 github.com/dustin/go-humanize v1.0.0 github.com/faiface/beep v1.1.0 + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.21.1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.8.0 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.1 - github.com/hajimehoshi/go-mp3 v0.3.2 // indirect + github.com/hajimehoshi/go-mp3 v0.3.3 // indirect github.com/hajimehoshi/oto v1.0.1 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/icza/bitio v1.1.0 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/imdario/mergo v0.3.13 // indirect github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414 + github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.2 // indirect - github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f + github.com/josephburnett/jd v1.5.2 + 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/mailru/easyjson v0.7.7 // indirect github.com/matryer/is v1.4.0 - github.com/mattn/go-sqlite3 v1.14.11 + github.com/mattn/go-sqlite3 v1.14.13 + github.com/mewkiz/flac v1.0.7 // indirect github.com/mewkiz/pkg v0.0.0-20211102230744-16a6ce8f1b77 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mmcdole/gofeed v1.1.3 github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nicksellen/audiotags v0.0.0-20160226222119-94015fa599bd github.com/oklog/run v1.1.0 github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c github.com/peterbourgon/ff v1.7.1 + github.com/pkg/errors v0.9.1 // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 github.com/stretchr/testify v1.7.0 // indirect - golang.org/x/crypto v0.0.0-20220209155544-dad33157f4bf // indirect - golang.org/x/exp/shiny v0.0.0-20220407100705-7b9b53b0aca4 // indirect - golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect - golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 // indirect - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect - golang.org/x/sys v0.0.0-20220207234003-57398862261d // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/exp/shiny v0.0.0-20220609121020-a51bd0440498 // indirect + golang.org/x/image v0.0.0-20220601225756-64ec528b34cd // indirect + golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd // indirect + golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect + golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d // indirect + golang.org/x/text v0.3.7 // indirect gopkg.in/gormigrate.v1 v1.6.0 + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index c1410d33..4e563f4b 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,7 @@ github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEq github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -40,6 +41,11 @@ github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9 github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -64,6 +70,8 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/go-mp3 v0.3.2 h1:xSYNE2F3lxtOu9BRjCWHHceg7S91IHfXfXp5+LYQI7s= github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/go-mp3 v0.3.3 h1:cWnfRdpye2m9ElSoVqneYRcpt/l3ijttgjMeQh+r+FE= +github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4= @@ -77,6 +85,8 @@ github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lTo github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/jezek/xgb v1.0.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= @@ -94,15 +104,28 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f h1:ijUonnyvDekPD7lUF4oQ1LV+dKaTnchEzmenMFa6NL4= github.com/josephburnett/jd v0.0.0-20191228205456-aa1a7c66b42f/go.mod h1:aeV+6oc13ogwzcRNHBe4vbyLmoQxMfEDoqyqCU9oE30= +github.com/josephburnett/jd v1.5.2 h1:RzE34nVV4kdTmFY5Cl7Wrwwo7J3AKlFMQk9x89GvIHE= +github.com/josephburnett/jd v1.5.2/go.mod h1:2pSZGHitQCumXDDTxmJehndlsltrTeVAhrzP8WfFeuc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= @@ -110,6 +133,8 @@ github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/mattn/go-sqlite3 v1.14.11 h1:gt+cp9c0XGqe9S/wAHTL3n/7MqY+siPWgWJgqdsFrzQ= github.com/mattn/go-sqlite3 v1.14.11/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I= +github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8= github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= @@ -131,8 +156,11 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 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= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= @@ -155,6 +183,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -167,6 +196,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220209155544-dad33157f4bf h1:gdgmgieTI2lLaGI2N+xEiaCMUgo2XFmAS0rlF8HZoso= golang.org/x/crypto v0.0.0-20220209155544-dad33157f4bf/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= @@ -174,17 +205,23 @@ golang.org/x/exp/shiny v0.0.0-20220209042442-160e291fcf24 h1:jn6Q9FOmCn1Kk7ec3Qm golang.org/x/exp/shiny v0.0.0-20220209042442-160e291fcf24/go.mod h1:NtXcNtv5Wu0zUbBl574y/D5MMZvnQnV3sgjZxbs64Jo= golang.org/x/exp/shiny v0.0.0-20220407100705-7b9b53b0aca4 h1:ywNGLBFk8tKaiu+GYZeoXWzrFoJ/a1LHYKy1lb3R9cM= golang.org/x/exp/shiny v0.0.0-20220407100705-7b9b53b0aca4/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= +golang.org/x/exp/shiny v0.0.0-20220609121020-a51bd0440498 h1:mJjyic/dxHcz1W6IUE8zf6+RltuO8+9mS45tTtb4F6k= +golang.org/x/exp/shiny v0.0.0-20220609121020-a51bd0440498/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220601225756-64ec528b34cd h1:9NbNcTg//wfC5JskFW4Z3sqwVnjmJKHxLAol1bW2qgw= +golang.org/x/image v0.0.0-20220601225756-64ec528b34cd/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 h1:jhDgkcu3yQ4tasBZ+1YwDmK7eFmuVf1w1k+NGGGxfmE= golang.org/x/mobile v0.0.0-20220112015953-858099ff7816/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= +golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd h1:x1GptNaTtxPAlTVIAJk61fuXg0y17h09DTxyb+VNC/k= +golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -203,6 +240,8 @@ golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -219,6 +258,9 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220207234003-57398862261d h1:Bm7BNOQt2Qv7ZqysjeLjgCBanX+88Z/OtdvsrEv1Djc= golang.org/x/sys v0.0.0-20220207234003-57398862261d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -237,11 +279,18 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/gormigrate.v1 v1.6.0 h1:XpYM6RHQPmzwY7Uyu+t+xxMXc86JYFJn4nEc9HzQjsI= gopkg.in/gormigrate.v1 v1.6.0/go.mod h1:Lf00lQrHqfSYWiTtPcyQabsDdM6ejZaMgV0OU6JMSlw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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 h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/assets/pages/change_avatar.tmpl b/server/assets/pages/change_avatar.tmpl new file mode 100644 index 00000000..2f42a2ce --- /dev/null +++ b/server/assets/pages/change_avatar.tmpl @@ -0,0 +1,26 @@ +{{ define "user" }} +
+
+ changing {{ .SelectedUser.Name }}'s avatar +
+ {{ if ne (len .SelectedUser.Avatar) 0 }} +
+ +
+ {{ end }} +
+
+ + +
+ {{ if ne (len .SelectedUser.Avatar) 0 }} +

+ {{ end }} +
+
+{{ end }} diff --git a/server/assets/pages/change_own_avatar.tmpl b/server/assets/pages/change_own_avatar.tmpl new file mode 100644 index 00000000..07803346 --- /dev/null +++ b/server/assets/pages/change_own_avatar.tmpl @@ -0,0 +1,26 @@ +{{ define "user" }} +
+
+ changing account avatar +
+ {{ if ne (len .SelectedUser.Avatar) 0 }} +
+ +
+ {{ end }} +
+
+ + +
+ {{ if ne (len .SelectedUser.Avatar) 0 }} +

+ {{ end }} +
+
+{{ end }} diff --git a/server/assets/pages/home.tmpl b/server/assets/pages/home.tmpl index 3303e909..73ce083b 100644 --- a/server/assets/pages/home.tmpl +++ b/server/assets/pages/home.tmpl @@ -82,6 +82,8 @@ | password… | + change avatar… + | {{ if $user.IsAdmin }} delete… {{ else }} @@ -100,6 +102,8 @@ change username… | change password… + | + change avatar… {{ end }} @@ -260,17 +264,16 @@ {{ end }}
- +
- {{ end }} diff --git a/server/assets/partials/head.tmpl b/server/assets/partials/head.tmpl index 7fe8e8f3..e6efdb2e 100644 --- a/server/assets/partials/head.tmpl +++ b/server/assets/partials/head.tmpl @@ -5,4 +5,5 @@ + {{ end }} diff --git a/server/assets/static/main.css b/server/assets/static/main.css index c335d92c..072b29fd 100644 --- a/server/assets/static/main.css +++ b/server/assets/static/main.css @@ -178,3 +178,9 @@ a:hover { .angry { background-color: #f4433669; } + +.avatar-preview { + width: 64px; + height: 64px; + object-fit: cover; +} diff --git a/server/assets/static/main.js b/server/assets/static/main.js new file mode 100644 index 00000000..cd49ca3e --- /dev/null +++ b/server/assets/static/main.js @@ -0,0 +1,5 @@ +for (const form of document.querySelectorAll("form.file-upload") || []) { + const input = form.querySelector("input[type=file]"); + if (!input) continue; + input.onchange = (e) => form.submit(); +} diff --git a/server/assets/static/playlist-upload.js b/server/assets/static/playlist-upload.js deleted file mode 100644 index a1a698e0..00000000 --- a/server/assets/static/playlist-upload.js +++ /dev/null @@ -1,3 +0,0 @@ -document.getElementById("playlist-upload-input").onchange = e => { - document.getElementById("playlist-upload-form").submit(); -}; diff --git a/server/ctrladmin/ctrl.go b/server/ctrladmin/ctrl.go index d86d0a19..fb81861d 100644 --- a/server/ctrladmin/ctrl.go +++ b/server/ctrladmin/ctrl.go @@ -2,6 +2,7 @@ package ctrladmin import ( + "encoding/base64" "encoding/gob" "errors" "fmt" @@ -21,10 +22,10 @@ import ( "github.com/sentriz/gormstore" "go.senan.xyz/gonic" - "go.senan.xyz/gonic/server/assets" - "go.senan.xyz/gonic/server/ctrlbase" "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/podcasts" + "go.senan.xyz/gonic/server/assets" + "go.senan.xyz/gonic/server/ctrlbase" ) type CtxKey int @@ -47,6 +48,7 @@ func funcMap() template.FuncMap { return strings.ToLower(in.Format("Jan 02, 2006")) }, "dateHuman": humanize.Time, + "base64": base64.StdEncoding.EncodeToString, } } @@ -122,8 +124,11 @@ type templateData struct { DefaultListenBrainzURL string SelectedUser *db.User - Podcasts []*db.Podcast + Podcasts []*db.Podcast InternetRadioStations []*db.InternetRadioStation + + // avatar + Avatar []byte } type Response struct { diff --git a/server/ctrladmin/handlers.go b/server/ctrladmin/handlers.go index 269e1e86..3f6d9f51 100644 --- a/server/ctrladmin/handlers.go +++ b/server/ctrladmin/handlers.go @@ -1,7 +1,12 @@ package ctrladmin import ( + "bytes" "fmt" + "image" + _ "image/gif" // to decode uploaded GIF avatars + "image/jpeg" + _ "image/png" // to decode uploaded PNG avatars "log" "net/http" "net/url" @@ -9,6 +14,7 @@ import ( "time" "github.com/mmcdole/gofeed" + "github.com/nfnt/resize" "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/scanner" @@ -120,6 +126,37 @@ func (c *Controller) ServeChangeOwnPasswordDo(r *http.Request) *Response { return &Response{redirect: "/admin/home"} } +func (c *Controller) ServeChangeOwnAvatar(r *http.Request) *Response { + data := &templateData{} + user := r.Context().Value(CtxUser).(*db.User) + data.SelectedUser = user + return &Response{ + template: "change_own_avatar.tmpl", + data: data, + } +} + +func (c *Controller) ServeChangeOwnAvatarDo(r *http.Request) *Response { + user := r.Context().Value(CtxUser).(*db.User) + avatar, err := getAvatarFile(r) + if err != nil { + return &Response{ + redirect: r.Referer(), + flashW: []string{err.Error()}, + } + } + user.Avatar = avatar + c.DB.Save(user) + return &Response{redirect: "/admin/home"} +} + +func (c *Controller) ServeDeleteOwnAvatarDo(r *http.Request) *Response { + user := r.Context().Value(CtxUser).(*db.User) + user.Avatar = nil + c.DB.Save(user) + return &Response{redirect: "/admin/home"} +} + func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response { token := r.URL.Query().Get("token") if token == "" { @@ -242,6 +279,46 @@ func (c *Controller) ServeChangePasswordDo(r *http.Request) *Response { return &Response{redirect: "/admin/home"} } +func (c *Controller) ServeChangeAvatar(r *http.Request) *Response { + username := r.URL.Query().Get("user") + if username == "" { + return &Response{code: 400, err: "please provide a username"} + } + user := c.DB.GetUserByName(username) + if user == nil { + return &Response{code: 400, err: "couldn't find a user with that name"} + } + data := &templateData{} + data.SelectedUser = user + return &Response{ + template: "change_avatar.tmpl", + data: data, + } +} + +func (c *Controller) ServeChangeAvatarDo(r *http.Request) *Response { + username := r.URL.Query().Get("user") + user := c.DB.GetUserByName(username) + avatar, err := getAvatarFile(r) + if err != nil { + return &Response{ + redirect: r.Referer(), + flashW: []string{err.Error()}, + } + } + user.Avatar = avatar + c.DB.Save(user) + return &Response{redirect: "/admin/home"} +} + +func (c *Controller) ServeDeleteAvatarDo(r *http.Request) *Response { + username := r.URL.Query().Get("user") + user := c.DB.GetUserByName(username) + user.Avatar = nil + c.DB.Save(user) + return &Response{redirect: "/admin/home"} +} + func (c *Controller) ServeDeleteUser(r *http.Request) *Response { username := r.URL.Query().Get("user") if username == "" { @@ -278,8 +355,7 @@ func (c *Controller) ServeCreateUser(r *http.Request) *Response { func (c *Controller) ServeCreateUserDo(r *http.Request) *Response { username := r.FormValue("username") - err := validateUsername(username) - if err != nil { + if err := validateUsername(username); err != nil { return &Response{ redirect: r.Referer(), flashW: []string{err.Error()}, @@ -287,8 +363,7 @@ func (c *Controller) ServeCreateUserDo(r *http.Request) *Response { } passwordOne := r.FormValue("password_one") passwordTwo := r.FormValue("password_two") - err = validatePasswords(passwordOne, passwordTwo) - if err != nil { + if err := validatePasswords(passwordOne, passwordTwo); err != nil { return &Response{ redirect: r.Referer(), flashW: []string{err.Error()}, @@ -570,3 +645,24 @@ func (c *Controller) ServeInternetRadioStationDeleteDo(r *http.Request) *Respons redirect: "/admin/home", } } + +func getAvatarFile(r *http.Request) ([]byte, error) { + err := r.ParseMultipartForm(10 << 20) // keep up to 10MB in memory + if err != nil { + return nil, err + } + file, _, err := r.FormFile("avatar") + if err != nil { + return nil, fmt.Errorf("read form file: %w", err) + } + i, _, err := image.Decode(file) + if err != nil { + return nil, fmt.Errorf("decode image: %w", err) + } + resized := resize.Resize(64, 64, i, resize.Lanczos3) + var buff bytes.Buffer + if err := jpeg.Encode(&buff, resized, nil); err != nil { + return nil, err + } + return buff.Bytes(), nil +} diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index c79be8c2..0ab34557 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -1,6 +1,7 @@ package ctrlsubsonic import ( + "bytes" "errors" "fmt" "log" @@ -297,3 +298,18 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R } return nil } + +func (c *Controller) ServeGetAvatar(w http.ResponseWriter, r *http.Request) *spec.Response { + params := r.Context().Value(CtxParams).(params.Params) + user := r.Context().Value(CtxUser).(*db.User) + username, err := params.Get("username") + if err != nil { + return spec.NewError(10, "please provide an `username` parameter") + } + reqUser := c.DB.GetUserByName(username) + if (user != reqUser) && !user.IsAdmin { + return spec.NewError(50, "user not admin") + } + http.ServeContent(w, r, "", time.Now(), bytes.NewReader(reqUser.Avatar)) + return nil +} diff --git a/server/server.go b/server/server.go index 863aafec..08b8c333 100644 --- a/server/server.go +++ b/server/server.go @@ -161,6 +161,9 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) { routUser.Handle("/change_own_username_do", ctrl.H(ctrl.ServeChangeOwnUsernameDo)) routUser.Handle("/change_own_password", ctrl.H(ctrl.ServeChangeOwnPassword)) routUser.Handle("/change_own_password_do", ctrl.H(ctrl.ServeChangeOwnPasswordDo)) + routUser.Handle("/change_own_avatar", ctrl.H(ctrl.ServeChangeOwnAvatar)) + routUser.Handle("/change_own_avatar_do", ctrl.H(ctrl.ServeChangeOwnAvatarDo)) + routUser.Handle("/delete_own_avatar_do", ctrl.H(ctrl.ServeDeleteOwnAvatarDo)) routUser.Handle("/link_lastfm_do", ctrl.H(ctrl.ServeLinkLastFMDo)) routUser.Handle("/unlink_lastfm_do", ctrl.H(ctrl.ServeUnlinkLastFMDo)) routUser.Handle("/link_listenbrainz_do", ctrl.H(ctrl.ServeLinkListenBrainzDo)) @@ -177,6 +180,9 @@ func setupAdmin(r *mux.Router, ctrl *ctrladmin.Controller) { routAdmin.Handle("/change_username_do", ctrl.H(ctrl.ServeChangeUsernameDo)) routAdmin.Handle("/change_password", ctrl.H(ctrl.ServeChangePassword)) routAdmin.Handle("/change_password_do", ctrl.H(ctrl.ServeChangePasswordDo)) + routAdmin.Handle("/change_avatar", ctrl.H(ctrl.ServeChangeAvatar)) + routAdmin.Handle("/change_avatar_do", ctrl.H(ctrl.ServeChangeAvatarDo)) + routAdmin.Handle("/delete_avatar_do", ctrl.H(ctrl.ServeDeleteAvatarDo)) routAdmin.Handle("/delete_user", ctrl.H(ctrl.ServeDeleteUser)) routAdmin.Handle("/delete_user_do", ctrl.H(ctrl.ServeDeleteUserDo)) routAdmin.Handle("/create_user", ctrl.H(ctrl.ServeCreateUser)) @@ -235,6 +241,7 @@ func setupSubsonic(r *mux.Router, ctrl *ctrlsubsonic.Controller) { r.Handle("/getCoverArt{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetCoverArt)) r.Handle("/stream{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream)) r.Handle("/download{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeStream)) + r.Handle("/getAvatar{_:(?:\\.view)?}", ctrl.HR(ctrl.ServeGetAvatar)) // browse by tag r.Handle("/getAlbum{_:(?:\\.view)?}", ctrl.H(ctrl.ServeGetAlbum))