Compare commits

...

10 Commits

Author SHA1 Message Date
cf5e87e62b feat: improve cover selection algorithm
Some checks failed
Release / Run Release Please (push) Blocked by required conditions
Release / Build, tag, and publish Docker image (push) Blocked by required conditions
Release / Notify IRC (push) Blocked by required conditions
Release / Lint and test (push) Has been cancelled
2024-06-11 18:26:44 +08:00
sentriz
0e45f5e84c feat(subsonic): expose replaygain tags 2024-05-30 11:43:45 +01:00
garfieldairlines.net
259be0edde docs: add example GONIC_EXCLUDE_PATTERN usage (#505)
+ regex examples for GONIC_EXCLUDE_PATTERN
2024-05-23 10:19:19 +00:00
Artem Tarasov
0d7d92d545 chore(docker): update to Alpine 3.19 (#502)
* update to Alpine 3.19

check out utfcpp (taglib2 dependency) from git, because Alpine 3.19
packages incompatible utfcpp 4.0, resulting in build failure

* use utfcpp package and specify the include path
2024-05-20 16:21:04 +00:00
sentriz
14c34c6052 2024-05-16 13:24:12 +02:00
sentriz
86fd590556 scanner: use create time if we have it 2024-05-15 17:07:44 +02:00
xxxserxxx
f5893ea5ea feat(playlist): assume playlists in the root dir without a user dir belong to admin (#499) 2024-05-01 15:32:51 +00:00
sentriz
559c9106b0 remove redundant handlerutil.Redirect 2024-04-28 13:32:41 +01:00
sentriz
6ba342c770 chore: bump deps 2024-04-28 13:32:40 +01:00
brian-doherty
93ce039963 feat(scanner): support full scan cleanups in watcher (#496)
* Added code to trigger rescan of entire tree upon file removal to clean as needed.

* Simplified ScanOptions initialization. Removed broken linter.

* gofmt fix
2024-04-21 00:04:53 +00:00
29 changed files with 557 additions and 277 deletions

View File

@@ -45,7 +45,6 @@ linters:
- makezero
- mirror
- misspell
- musttag
- nakedret
- nestif
- nilerr

View File

@@ -1,4 +1,4 @@
FROM alpine:3.18 AS builder-taglib
FROM alpine:3.19 AS builder-taglib
WORKDIR /tmp
COPY alpine/taglib/APKBUILD .
RUN apk update && \
@@ -26,7 +26,7 @@ RUN go mod download
COPY . .
RUN GOOS=linux go build -o gonic cmd/gonic/gonic.go
FROM alpine:3.18
FROM alpine:3.19
LABEL org.opencontainers.image.source https://github.com/sentriz/gonic
RUN apk add -U --no-cache \
ffmpeg \

View File

@@ -67,7 +67,7 @@ password can then be changed from the web interface
| `GONIC_JUKEBOX_ENABLED` | `-jukebox-enabled` | **optional** whether the subsonic [jukebox api](https://airsonic.github.io/docs/jukebox/) should be enabled |
| `GONIC_JUKEBOX_MPV_EXTRA_ARGS` | `-jukebox-mpv-extra-args` | **optional** extra command line arguments to pass to the jukebox mpv daemon |
| `GONIC_PODCAST_PURGE_AGE` | `-podcast-purge-age` | **optional** age (in days) to purge podcast episodes if not accessed |
| `GONIC_EXCLUDE_PATTERN` | `-exclude-pattern` | **optional** files matching this regex pattern will not be imported |
| `GONIC_EXCLUDE_PATTERN` | `-exclude-pattern` | **optional** files matching this regex pattern will not be imported. eg <code>@eaDir\|[aA]rtwork\|[cC]overs\|[sS]cans\|[sS]pectrals</code> |
| `GONIC_MULTI_VALUE_GENRE` | `-multi-value-genre` | **optional** setting for multi-valued genre tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_MULTI_VALUE_ARTIST` | `-multi-value-artist` | **optional** setting for multi-valued artist tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_MULTI_VALUE_ALBUM_ARTIST` | `-multi-value-album-artist` | **optional** setting for multi-valued album artist tags when scanning ([see more](#multi-valued-tags-v016)) |
@@ -97,7 +97,7 @@ the available modes are:
gonic supports multiple music folders. this can be handy if you have your music separated by albums, compilations, singles. or maybe 70s, 80s, 90s. whatever.
on top of that - if you don't decide your folder names, or simply do not want the same name in your subsonic client,
on top of that - if you don't decide your folder names, or simply do not want the same name in your subsonic client,
gonic can parse aliases for the folder names with the optional `ALIAS->PATH` syntax
if you're running gonic with the command line, stack the `-music-path` arg

View File

@@ -1,7 +1,7 @@
# Contributor: Leo <thinkabit.ukim@gmail.com>
# Maintainer: Natanael Copa <ncopa@alpinelinux.org>
pkgname=taglib2
pkgver=2.0
pkgver=2.0.1
pkgrel=0
pkgdesc="Library for reading and editing metadata of several popular audio formats"
url="https://taglib.github.io/"
@@ -30,10 +30,11 @@ build() {
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DWITH_ZLIB=ON \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_EXAMPLES=ON \
-DBUILD_EXAMPLES=OFF \
-DBUILD_TESTING="$(want_check && echo ON || echo OFF)" \
-DVISIBILITY_HIDDEN=ON
cmake --build build
CPLUS_INCLUDE_PATH="/usr/include/utf8cpp" \
cmake --build build
}
check() {
@@ -51,5 +52,5 @@ _lib() {
}
sha512sums="
099d02b2eab033f5702a8cb03e70752d7523c6f8c2f3eebdd0bcd939eafbdca3f2a6c82452983904b5822cfa45f2707ed866c3419508df9d43bf5c0b3a476f6c taglib-2.0.tar.gz
25ee89293a96d7f8dca6276f822bdaef01fd98503b78c20ffeac8e1d9821de7273a5127146aa798d304c6a995cb2b7229a205aff1cc261b5d4fa9e499dda0439 taglib-2.0.1.tar.gz
"

View File

@@ -273,7 +273,7 @@ func main() {
mux.Handle("/admin/", http.StripPrefix("/admin", chain(ctrlAdmin)))
mux.Handle("/rest/", http.StripPrefix("/rest", chain(trim(ctrlSubsonic))))
mux.Handle("/ping", chain(handlerutil.Message("ok")))
mux.Handle("/", chain(handlerutil.Redirect(resolveProxyPath("/admin/home"))))
mux.Handle("/", chain(http.RedirectHandler(resolveProxyPath("/admin/home"), http.StatusSeeOther)))
if *confExpvar {
mux.Handle("/debug/vars", expvar.Handler())

View File

@@ -235,9 +235,15 @@ type Track struct {
TagTrackNumber int `sql:"default: null"`
TagDiscNumber int `sql:"default: null"`
TagBrainzID string `sql:"default: null"`
TrackStar *TrackStar
TrackRating *TrackRating
AverageRating float64 `sql:"default: null"`
ReplayGainTrackGain float32
ReplayGainTrackPeak float32
ReplayGainAlbumGain float32
ReplayGainAlbumPeak float32
TrackStar *TrackStar
TrackRating *TrackRating
AverageRating float64 `sql:"default: null"`
}
func (t *Track) AudioLength() int { return t.Length }

View File

@@ -72,6 +72,7 @@ func (db *DB) Migrate(ctx MigrationContext) error {
construct(ctx, "202311072309", migrateAlbumInfo),
construct(ctx, "202311082304", migrateTemporaryDisplayAlbumArtist),
construct(ctx, "202312110003", migrateAddExtraIndexes),
construct(ctx, "202405301140", migrateAddReplayGainFields),
}
return gormigrate.
@@ -813,3 +814,7 @@ func migrateAddExtraIndexes(tx *gorm.DB, _ MigrationContext) error {
CREATE INDEX idx_artist_appearances_album_id ON "artist_appearances" (album_id);
`).Error
}
func migrateAddReplayGainFields(tx *gorm.DB, _ MigrationContext) error {
return tx.AutoMigrate(Track{}).Error
}

22
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/andybalholm/cascadia v1.3.2
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37
github.com/disintegration/imaging v1.6.2
github.com/djherbis/times v1.6.0
github.com/dustin/go-humanize v1.0.1
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.7.0
@@ -15,20 +16,20 @@ require (
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.2.2
github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414
github.com/josephburnett/jd v1.7.1
github.com/josephburnett/jd v1.8.1
github.com/mattn/go-sqlite3 v1.14.22
github.com/mitchellh/mapstructure v1.5.0
github.com/mmcdole/gofeed v1.3.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be
github.com/sentriz/audiotags v0.0.0-20240305214804-7a32981c18f8
github.com/sentriz/audiotags v0.0.0-20240401192409-5a8ac2a2974f
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
github.com/stretchr/testify v1.8.4
go.senan.xyz/flagconf v0.1.7
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
golang.org/x/net v0.22.0
golang.org/x/sync v0.6.0
github.com/stretchr/testify v1.9.0
go.senan.xyz/flagconf v0.1.8
golang.org/x/net v0.24.0
golang.org/x/sync v0.7.0
golang.org/x/sys v0.19.0
gopkg.in/gormigrate.v1 v1.6.0
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
)
@@ -38,8 +39,8 @@ 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/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/swag v0.22.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
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
@@ -59,9 +60,8 @@ require (
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.21.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/sys v0.18.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

67
go.sum
View File

@@ -6,8 +6,6 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.9.0 h1:zgjKkdpRY9T97Q5DCtcXwfqkcylSFIVCocZmn2huTp8=
github.com/PuerkitoBio/goquery v1.9.0/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
@@ -23,6 +21,8 @@ github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37 h1:s+qNFsO3VsdsKro
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37/go.mod h1:CXCwawNJCtFDip7gvbaQVgw0cGjldpyHDIp7oA5prOg=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
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/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/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
@@ -31,10 +31,10 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
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/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
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=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
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=
@@ -75,8 +75,8 @@ github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
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 v1.7.1 h1:oXBPMS+SNnILTMGj1fWLK9pexpeJUXtbVFfRku/PjBU=
github.com/josephburnett/jd v1.7.1/go.mod h1:R8ZnZnLt2D4rhW4NvBc/USTo6mzyNT6fYNIIWOJA9GY=
github.com/josephburnett/jd v1.8.1 h1:U4wae4kEvduCmf5mlXJ3uKnfHFmGhwttEFkQ6rsoDMk=
github.com/josephburnett/jd v1.8.1/go.mod h1:d9nEP87VBIx8SxhIVraVdEU/IwZ7JH6kHWjZMByRq2M=
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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -104,12 +104,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4=
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=
github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -132,36 +128,26 @@ 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=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sentriz/audiotags v0.0.0-20240202193907-618ae39d7743 h1:aecPwcrY8mYmZmd9XgQcG8aILRuhRxeQMSunnr6DQ3U=
github.com/sentriz/audiotags v0.0.0-20240202193907-618ae39d7743/go.mod h1:Zoo4UP5t2ySbPwScJfoydAlLLBonoqntv4ovA1T91Z8=
github.com/sentriz/audiotags v0.0.0-20240305214804-7a32981c18f8 h1:WBzwq2r567WlnfYravpwUdsAzaXedbWLypXyArLGgI4=
github.com/sentriz/audiotags v0.0.0-20240305214804-7a32981c18f8/go.mod h1:+pmkMFDEXJuu/u4h2OYJVfYF2qIhXJD7kqvWq6q5Zo0=
github.com/sentriz/audiotags v0.0.0-20240401192409-5a8ac2a2974f h1:Yio3vmGw+yf+gzjYLf1plSGEf/1IUTVY45n+qcGJEmk=
github.com/sentriz/audiotags v0.0.0-20240401192409-5a8ac2a2974f/go.mod h1:+pmkMFDEXJuu/u4h2OYJVfYF2qIhXJD7kqvWq6q5Zo0=
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 h1:sLILANWN76ja66/K4k/mBqJuCjDZaM67w+Ru6rEB0s0=
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981/go.mod h1:Rx8XB1ck+so+41uu9VY1gMKs1CPQ2NTq0pzf+OCCQHo=
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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
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=
go.senan.xyz/flagconf v0.1.5 h1:5HTNpA5jzH1XnsyR79pClXf9T+V+6OL/IsESORMrExs=
go.senan.xyz/flagconf v0.1.5/go.mod h1:CGD/sgYWiTacz1ojgsQRwErqLxtShWMpBxxnsJI6yaE=
go.senan.xyz/flagconf v0.1.7 h1:+o9Cg3WyzCG+KSfZAwOP61dTWSzGhfH3W+zz9mbNJOA=
go.senan.xyz/flagconf v0.1.7/go.mod h1:CGD/sgYWiTacz1ojgsQRwErqLxtShWMpBxxnsJI6yaE=
go.senan.xyz/flagconf v0.1.8 h1:0HadvAEXHYJOGGdO6cHz2Ok4vWawaM64m5ldSjLoVUw=
go.senan.xyz/flagconf v0.1.8/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
@@ -177,28 +163,25 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.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=
@@ -214,8 +197,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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=

View File

@@ -56,12 +56,6 @@ func BasicCORS(next http.Handler) http.Handler {
})
}
func Redirect(to string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, to, http.StatusSeeOther)
})
}
func Message(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, message)

View File

@@ -11,6 +11,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"slices"
"strconv"
"strings"
"sync"
@@ -18,7 +19,6 @@ import (
"github.com/dexterlb/mpvipc"
"github.com/mitchellh/mapstructure"
"golang.org/x/exp/slices"
)
var (

View File

@@ -362,8 +362,14 @@ 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) Length() int { return firstInt(100, i.RawLength) }
func (i *TagInfo) Bitrate() int { return firstInt(100, i.RawBitrate) }
func (i *TagInfo) ReplayGainTrackGain() float32 { return 0 }
func (i *TagInfo) ReplayGainTrackPeak() float32 { return 0 }
func (i *TagInfo) ReplayGainAlbumGain() float32 { return 0 }
func (i *TagInfo) ReplayGainAlbumPeak() float32 { return 0 }
func (i *TagInfo) Length() int { return firstInt(100, i.RawLength) }
func (i *TagInfo) Bitrate() int { return firstInt(100, i.RawBitrate) }
var _ tagcommon.Reader = (*tagReader)(nil)

View File

@@ -99,7 +99,7 @@ func (s *Store) Read(relPath string) (*Playlist, error) {
playlist.UserID, err = userIDFromPath(relPath)
if err != nil {
return nil, fmt.Errorf("convert id to str: %w", err)
playlist.UserID = 1
}
playlist.Name = strings.TrimSuffix(filepath.Base(relPath), filepath.Ext(relPath))

View File

@@ -0,0 +1,83 @@
package coverresolve
import (
"regexp"
"sort"
"strconv"
"strings"
)
var DefaultKeywords = []string{
"cover",
"folder",
"front",
"albumart",
"album",
"artist",
"scan",
}
// Helper function to extract the number from the filename
func extractNumber(filename string) int {
re := regexp.MustCompile(`\d+`)
matches := re.FindAllString(filename, -1)
if len(matches) == 0 {
return 0
}
num, _ := strconv.Atoi(matches[0])
return num
}
type CoverAlternative struct {
Name string
Score int
}
func SelectCover(covers []string) string {
if len(covers) == 0 {
return ""
}
coverAlternatives := make([]CoverAlternative, 0)
for _, keyword := range DefaultKeywords {
if len(coverAlternatives) > 0 {
break
}
for _, cover := range covers {
if strings.Contains(strings.ToLower(cover), keyword) {
coverAlternatives = append(coverAlternatives, CoverAlternative{
Name: cover,
Score: 0,
})
}
}
}
// parse the integer from the filename
// eg. cover(1).jpg will have higher score than cover(114514).jpg
for i := range coverAlternatives {
coverAlternatives[i].Score -= extractNumber(coverAlternatives[i].Name)
}
// sort by score
sort.Slice(coverAlternatives, func(i, j int) bool {
return coverAlternatives[i].Score > coverAlternatives[j].Score
})
if len(coverAlternatives) == 0 {
return covers[0]
}
return coverAlternatives[0].Name
}
func IsCover(name string) bool {
for _, ext := range []string{"jpg", "jpeg", "png", "bmp", "gif"} {
if strings.HasSuffix(strings.ToLower(name), "."+ext) {
return true
}
}
return false
}

View File

@@ -0,0 +1,85 @@
package coverresolve
import (
"testing"
)
func TestIsCover(t *testing.T) {
tests := []struct {
name string
filename string
expected bool
}{
{"JPEG file", "Image.jpg", true},
{"JPEG file", "image.jpg", true},
{"PNG file", "picture.png", true},
{"BMP file", "photo.bmp", true},
{"GIF file", "animation.gif", true},
{"Non-image file", "document.pdf", false},
{"Empty file name", "", false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := IsCover(test.filename)
if result != test.expected {
t.Errorf("Expected IsCover(%q) to be %v, but got %v", test.filename, test.expected, result)
}
})
}
}
func TestSelectCover(t *testing.T) {
tests := []struct {
name string
covers []string
expected string
}{
{
name: "Empty covers slice",
covers: []string{},
expected: "",
},
{
name: "Covers without keywords or numbers case sensitive",
covers: []string{"Cover1.jpg", "cover2.png"},
expected: "Cover1.jpg",
},
{
name: "Covers without keywords or numbers",
covers: []string{"cover1.jpg", "cover2.png"},
expected: "cover1.jpg",
},
{
name: "Covers with keywords and numbers",
covers: []string{"cover12.jpg", "cover2.png", "special_cover1.jpg"},
expected: "special_cover1.jpg",
},
{
name: "Covers with keywords but without numbers",
covers: []string{"cover12.jpg", "cover_keyword.png"},
expected: "cover_keyword.png",
},
{
name: "Covers without keywords but with numbers",
covers: []string{"cover1.jpg", "cover12.png"},
expected: "cover1.jpg",
},
{
name: "Covers with same highest score",
covers: []string{"cover1.jpg", "cover2.jpg", "cover_special.jpg"},
expected: "cover_special.jpg",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Mock the DefaultScoreRules
result := SelectCover(test.covers)
if result != test.expected {
t.Errorf("Expected SelectCover(%v) to be %q, but got %q", test.covers, test.expected, result)
}
})
}
}

View File

@@ -17,12 +17,14 @@ import (
"sync/atomic"
"time"
"github.com/djherbis/times"
"github.com/fsnotify/fsnotify"
"github.com/jinzhu/gorm"
"github.com/rainycape/unidecode"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/fileutil"
"go.senan.xyz/gonic/scanner/coverresolve"
"go.senan.xyz/gonic/tags/tagcommon"
)
@@ -134,9 +136,18 @@ func (s *Scanner) ExecuteWatch(ctx context.Context) error {
}
batchSeen := map[string]struct{}{}
batchClean := false
for {
select {
case <-batchT.C:
if batchClean {
if _, err := s.ScanAndClean(ScanOptions{}); err != nil {
log.Printf("error scanning: %v", err)
}
clear(batchSeen)
batchClean = false
break
}
if !s.StartScanning() {
break
}
@@ -164,6 +175,10 @@ func (s *Scanner) ExecuteWatch(ctx context.Context) error {
clear(batchSeen)
case event := <-watcher.Events:
if event.Op&(fsnotify.Remove) == fsnotify.Remove {
batchClean = true
break
}
if event.Op&(fsnotify.Create|fsnotify.Write) == 0 {
break
}
@@ -252,7 +267,7 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
}
var tracks []string
var cover string
var covers []string
for _, item := range items {
absPath := filepath.Join(absPath, item.Name())
if s.excludePattern != nil && s.excludePattern.MatchString(absPath) {
@@ -263,8 +278,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
continue
}
if isCover(item.Name()) {
cover = item.Name()
if coverresolve.IsCover(item.Name()) {
covers = append(covers, item.Name())
continue
}
if s.tagReader.CanRead(absPath) {
@@ -273,6 +288,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
}
}
cover := coverresolve.SelectCover(covers)
pdir, pbasename := filepath.Split(filepath.Dir(relPath))
var parent db.Album
if err := tx.Where("root_dir=? AND left_path=? AND right_path=?", musicDir, pdir, pbasename).Assign(db.Album{RootDir: musicDir, LeftPath: pdir, RightPath: pbasename}).FirstOrCreate(&parent).Error; err != nil {
@@ -301,9 +318,10 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
}
func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db.Album, basename string, absPath string) error {
stat, err := os.Stat(absPath)
// useful to get the real create/birth time for filesystems and kernels which support it
timeSpec, err := times.Stat(absPath)
if err != nil {
return fmt.Errorf("stating %q: %w", basename, err)
return fmt.Errorf("get times %q: %w", basename, err)
}
var track db.Track
@@ -311,7 +329,7 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return fmt.Errorf("query track: %w", err)
}
if !st.isFull && track.ID != 0 && stat.ModTime().Before(track.UpdatedAt) {
if !st.isFull && track.ID != 0 && timeSpec.ModTime().Before(track.UpdatedAt) {
st.seenTracks[track.ID] = struct{}{}
return nil
}
@@ -350,7 +368,11 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return fmt.Errorf("populate track artists: %w", err)
}
if err := populateAlbum(tx, album, trags, stat.ModTime()); err != nil {
modTime, createTime := timeSpec.ModTime(), timeSpec.ModTime()
if timeSpec.HasBirthTime() {
createTime = timeSpec.BirthTime()
}
if err := populateAlbum(tx, album, trags, modTime, createTime); err != nil {
return fmt.Errorf("populate album: %w", err)
}
@@ -359,6 +381,10 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
}
}
stat, err := os.Stat(absPath)
if err != nil {
return fmt.Errorf("stating %q: %w", basename, err)
}
if err := populateTrack(tx, album, &track, trags, basename, int(stat.Size())); err != nil {
return fmt.Errorf("process %q: %w", basename, err)
}
@@ -389,7 +415,7 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return nil
}
func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime time.Time) error {
func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime, createTime time.Time) error {
albumName := tagcommon.MustAlbum(trags)
album.TagTitle = albumName
album.TagTitleUDec = decoded(albumName)
@@ -398,8 +424,8 @@ func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime tim
album.TagYear = trags.Year()
album.ModifiedAt = modTime
if album.CreatedAt.After(modTime) {
album.CreatedAt = modTime // reset created at to match filesytem for new albums
if album.CreatedAt.After(createTime) {
album.CreatedAt = createTime // reset created at to match filesytem for new albums
}
if err := tx.Save(&album).Error; err != nil {
@@ -446,8 +472,14 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tagcommon.
track.TagDiscNumber = trags.DiscNumber()
track.TagBrainzID = trags.BrainzID()
track.Length = trags.Length() // these two should be calculated
track.Bitrate = trags.Bitrate() // ...from the file instead of tags
track.ReplayGainTrackGain = trags.ReplayGainTrackGain()
track.ReplayGainTrackPeak = trags.ReplayGainTrackPeak()
track.ReplayGainAlbumGain = trags.ReplayGainAlbumGain()
track.ReplayGainAlbumPeak = trags.ReplayGainAlbumPeak()
// these two are calculated from the file instead of tags
track.Length = trags.Length()
track.Bitrate = trags.Bitrate()
if err := tx.Save(&track).Error; err != nil {
return fmt.Errorf("saving track: %w", err)
@@ -642,26 +674,6 @@ func (s *Scanner) cleanGenres(st *State) error { //nolint:unparam
return nil
}
//nolint:gochecknoglobals
var coverNames = map[string]struct{}{}
//nolint:gochecknoinits
func init() {
for _, name := range []string{"cover", "folder", "front", "albumart", "album", "artist"} {
for _, ext := range []string{"jpg", "jpeg", "png", "bmp", "gif"} {
coverNames[fmt.Sprintf("%s.%s", name, ext)] = struct{}{}
for i := 0; i < 3; i++ {
coverNames[fmt.Sprintf("%s.%d.%s", name, i, ext)] = struct{}{} // support beets extras
}
}
}
}
func isCover(name string) bool {
_, ok := coverNames[strings.ToLower(name)]
return ok
}
// decoded converts a string to it's latin equivalent.
// it will be used by the model's *UDec fields, and is only set if it
// differs from the original. the fields are used for searching.

View File

@@ -102,6 +102,14 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
for _, a := range t.Artists {
trCh.Artists = append(trCh.Artists, &ArtistRef{ID: a.SID(), Name: a.Name})
}
if t.ReplayGainTrackGain != 0 || t.ReplayGainAlbumGain != 0 {
trCh.ReplayGain = &ReplayGain{
TrackGain: t.ReplayGainTrackGain,
TrackPeak: t.ReplayGainTrackPeak,
AlbumGain: t.ReplayGainAlbumGain,
AlbumPeak: t.ReplayGainAlbumPeak,
}
}
return trCh
}

View File

@@ -112,6 +112,14 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
for _, a := range album.Artists {
ret.AlbumArtists = append(ret.AlbumArtists, &ArtistRef{ID: a.SID(), Name: a.Name})
}
if t.ReplayGainTrackGain != 0 || t.ReplayGainAlbumGain != 0 {
ret.ReplayGain = &ReplayGain{
TrackGain: t.ReplayGainTrackGain,
TrackPeak: t.ReplayGainTrackPeak,
AlbumGain: t.ReplayGainAlbumGain,
AlbumPeak: t.ReplayGainAlbumPeak,
}
}
return ret
}

View File

@@ -169,6 +169,13 @@ type TranscodeMeta struct {
TranscodedSuffix string `xml:"transcodedSuffix,attr,omitempty" json:"transcodedSuffix,omitempty"`
}
type ReplayGain struct {
TrackGain float32 `xml:"trackGain,attr" json:"trackGain"`
TrackPeak float32 `xml:"trackPeak,attr" json:"trackPeak"`
AlbumGain float32 `xml:"albumGain,attr" json:"albumGain"`
AlbumPeak float32 `xml:"albumPeak,attr" json:"albumPeak"`
}
// https://opensubsonic.netlify.app/docs/responses/child/
type TrackChild struct {
ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"`
@@ -211,6 +218,8 @@ type TrackChild struct {
UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
AverageRating string `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
ReplayGain *ReplayGain `xml:"replayGain" json:"replayGain"`
TranscodeMeta
}

View File

@@ -8,16 +8,16 @@
"albumList": {
"album": [
{
"id": "al-9",
"id": "al-5",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-1",
"artist": "artist-0",
"artists": null,
"displayArtist": "",
"title": "album-2",
"album": "album-2",
"parent": "al-6",
"parent": "al-2",
"isDir": true,
"coverArt": "al-9",
"coverArt": "al-5",
"name": "album-2",
"songCount": 3,
"duration": 300,
@@ -40,48 +40,16 @@
"playCount": 0
},
{
"id": "al-4",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-0",
"artists": null,
"displayArtist": "",
"title": "album-1",
"album": "album-1",
"parent": "al-2",
"isDir": true,
"coverArt": "al-4",
"name": "album-1",
"songCount": 3,
"duration": 300,
"playCount": 0
},
{
"id": "al-12",
"id": "al-13",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-2",
"artists": null,
"displayArtist": "",
"title": "album-1",
"album": "album-1",
"parent": "al-10",
"isDir": true,
"coverArt": "al-12",
"name": "album-1",
"songCount": 3,
"duration": 300,
"playCount": 0
},
{
"id": "al-5",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-0",
"artists": null,
"displayArtist": "",
"title": "album-2",
"album": "album-2",
"parent": "al-2",
"parent": "al-10",
"isDir": true,
"coverArt": "al-5",
"coverArt": "al-13",
"name": "album-2",
"songCount": 3,
"duration": 300,
@@ -103,6 +71,54 @@
"duration": 300,
"playCount": 0
},
{
"id": "al-4",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-0",
"artists": null,
"displayArtist": "",
"title": "album-1",
"album": "album-1",
"parent": "al-2",
"isDir": true,
"coverArt": "al-4",
"name": "album-1",
"songCount": 3,
"duration": 300,
"playCount": 0
},
{
"id": "al-9",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-1",
"artists": null,
"displayArtist": "",
"title": "album-2",
"album": "album-2",
"parent": "al-6",
"isDir": true,
"coverArt": "al-9",
"name": "album-2",
"songCount": 3,
"duration": 300,
"playCount": 0
},
{
"id": "al-12",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-2",
"artists": null,
"displayArtist": "",
"title": "album-1",
"album": "album-1",
"parent": "al-10",
"isDir": true,
"coverArt": "al-12",
"name": "album-1",
"songCount": 3,
"duration": 300,
"playCount": 0
},
{
"id": "al-8",
"created": "2019-11-30T00:00:00Z",
@@ -119,22 +135,6 @@
"duration": 300,
"playCount": 0
},
{
"id": "al-13",
"created": "2019-11-30T00:00:00Z",
"artist": "artist-2",
"artists": null,
"displayArtist": "",
"title": "album-2",
"album": "album-2",
"parent": "al-10",
"isDir": true,
"coverArt": "al-13",
"name": "album-2",
"songCount": 3,
"duration": 300,
"playCount": 0
},
{
"id": "al-11",
"created": "2019-11-30T00:00:00Z",

View File

@@ -24,32 +24,16 @@
"year": 2021
},
{
"id": "al-5",
"created": "2019-11-30T00:00:00Z",
"artistId": "ar-1",
"artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"displayArtist": "artist-0",
"title": "album-2",
"album": "album-2",
"coverArt": "al-5",
"name": "album-2",
"songCount": 3,
"duration": 300,
"playCount": 0,
"year": 2021
},
{
"id": "al-8",
"id": "al-7",
"created": "2019-11-30T00:00:00Z",
"artistId": "ar-2",
"artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"displayArtist": "artist-1",
"title": "album-1",
"album": "album-1",
"coverArt": "al-8",
"name": "album-1",
"title": "album-0",
"album": "album-0",
"coverArt": "al-7",
"name": "album-0",
"songCount": 3,
"duration": 300,
"playCount": 0,
@@ -71,38 +55,6 @@
"playCount": 0,
"year": 2021
},
{
"id": "al-12",
"created": "2019-11-30T00:00:00Z",
"artistId": "ar-3",
"artist": "artist-2",
"artists": [{ "id": "ar-3", "name": "artist-2" }],
"displayArtist": "artist-2",
"title": "album-1",
"album": "album-1",
"coverArt": "al-12",
"name": "album-1",
"songCount": 3,
"duration": 300,
"playCount": 0,
"year": 2021
},
{
"id": "al-7",
"created": "2019-11-30T00:00:00Z",
"artistId": "ar-2",
"artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"displayArtist": "artist-1",
"title": "album-0",
"album": "album-0",
"coverArt": "al-7",
"name": "album-0",
"songCount": 3,
"duration": 300,
"playCount": 0,
"year": 2021
},
{
"id": "al-13",
"created": "2019-11-30T00:00:00Z",
@@ -135,6 +87,54 @@
"playCount": 0,
"year": 2021
},
{
"id": "al-12",
"created": "2019-11-30T00:00:00Z",
"artistId": "ar-3",
"artist": "artist-2",
"artists": [{ "id": "ar-3", "name": "artist-2" }],
"displayArtist": "artist-2",
"title": "album-1",
"album": "album-1",
"coverArt": "al-12",
"name": "album-1",
"songCount": 3,
"duration": 300,
"playCount": 0,
"year": 2021
},
{
"id": "al-5",
"created": "2019-11-30T00:00:00Z",
"artistId": "ar-1",
"artist": "artist-0",
"artists": [{ "id": "ar-1", "name": "artist-0" }],
"displayArtist": "artist-0",
"title": "album-2",
"album": "album-2",
"coverArt": "al-5",
"name": "album-2",
"songCount": 3,
"duration": 300,
"playCount": 0,
"year": 2021
},
{
"id": "al-8",
"created": "2019-11-30T00:00:00Z",
"artistId": "ar-2",
"artist": "artist-1",
"artists": [{ "id": "ar-2", "name": "artist-1" }],
"displayArtist": "artist-1",
"title": "album-1",
"album": "album-1",
"coverArt": "al-8",
"name": "album-1",
"songCount": 3,
"duration": 300,
"playCount": 0,
"year": 2021
},
{
"id": "al-9",
"created": "2019-11-30T00:00:00Z",

View File

@@ -48,7 +48,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-2",
@@ -75,7 +76,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-3",
@@ -102,7 +104,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
}
]
}

View File

@@ -33,7 +33,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-2",
@@ -58,7 +59,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-3",
@@ -83,7 +85,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
}
]
}

View File

@@ -23,7 +23,8 @@
"isVideo": false,
"parent": "al-2",
"title": "album-0",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-4",
@@ -38,7 +39,8 @@
"isVideo": false,
"parent": "al-2",
"title": "album-1",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-5",
@@ -53,7 +55,8 @@
"isVideo": false,
"parent": "al-2",
"title": "album-2",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
}
]
}

View File

@@ -34,7 +34,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-2",
@@ -63,7 +64,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-3",
@@ -92,7 +94,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-4",
@@ -121,7 +124,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-5",
@@ -150,7 +154,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-6",
@@ -179,7 +184,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-7",
@@ -208,7 +214,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-8",
@@ -237,7 +244,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-9",
@@ -266,7 +274,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-10",
@@ -295,7 +304,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-11",
@@ -324,7 +334,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-12",
@@ -353,7 +364,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-13",
@@ -382,7 +394,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-14",
@@ -411,7 +424,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-15",
@@ -440,7 +454,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-16",
@@ -469,7 +484,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-17",
@@ -498,7 +514,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-18",
@@ -527,7 +544,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-19",
@@ -556,7 +574,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-20",
@@ -585,7 +604,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
}
]
}

View File

@@ -20,7 +20,8 @@
"isVideo": false,
"parent": "al-2",
"title": "album-0",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-4",
@@ -35,7 +36,8 @@
"isVideo": false,
"parent": "al-2",
"title": "album-1",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-5",
@@ -50,7 +52,8 @@
"isVideo": false,
"parent": "al-2",
"title": "album-2",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-7",
@@ -65,7 +68,8 @@
"isVideo": false,
"parent": "al-6",
"title": "album-0",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-8",
@@ -80,7 +84,8 @@
"isVideo": false,
"parent": "al-6",
"title": "album-1",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-9",
@@ -95,7 +100,8 @@
"isVideo": false,
"parent": "al-6",
"title": "album-2",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-11",
@@ -110,7 +116,8 @@
"isVideo": false,
"parent": "al-10",
"title": "album-0",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-12",
@@ -125,7 +132,8 @@
"isVideo": false,
"parent": "al-10",
"title": "album-1",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "al-13",
@@ -140,7 +148,8 @@
"isVideo": false,
"parent": "al-10",
"title": "album-2",
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
}
]
}

View File

@@ -30,7 +30,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-2",
@@ -55,7 +56,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-3",
@@ -80,7 +82,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-4",
@@ -105,7 +108,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-5",
@@ -130,7 +134,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-6",
@@ -155,7 +160,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-7",
@@ -180,7 +186,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-8",
@@ -205,7 +212,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-9",
@@ -230,7 +238,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-10",
@@ -255,7 +264,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-11",
@@ -280,7 +290,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-12",
@@ -305,7 +316,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-13",
@@ -330,7 +342,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-14",
@@ -355,7 +368,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-15",
@@ -380,7 +394,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-16",
@@ -405,7 +420,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-17",
@@ -430,7 +446,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-18",
@@ -455,7 +472,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-19",
@@ -480,7 +498,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
},
{
"id": "tr-20",
@@ -505,7 +524,8 @@
"discNumber": 1,
"type": "music",
"year": 2021,
"musicBrainzId": ""
"musicBrainzId": "",
"replayGain": null
}
]
}

View File

@@ -24,9 +24,15 @@ type Info interface {
Genres() []string
TrackNumber() int
DiscNumber() int
Year() int
ReplayGainTrackGain() float32
ReplayGainTrackPeak() float32
ReplayGainAlbumGain() float32
ReplayGainAlbumPeak() float32
Length() int
Bitrate() int
Year() int
}
const (

View File

@@ -51,8 +51,14 @@ 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) Length() int { return i.props.Length }
func (i *info) Bitrate() int { return i.props.Bitrate }
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"))) }
func (i *info) ReplayGainAlbumGain() float32 { return dB(first(find(i.raw, "replaygain_album_gain"))) }
func (i *info) ReplayGainAlbumPeak() float32 { return flt(first(find(i.raw, "replaygain_album_peak"))) }
func (i *info) Length() int { return i.props.Length }
func (i *info) Bitrate() int { return i.props.Bitrate }
func first[T comparable](is []T) T {
var z T
@@ -83,6 +89,17 @@ func filterStr(ss []string) []string {
return r
}
func flt(in string) float32 {
f, _ := strconv.ParseFloat(in, 32)
return float32(f)
}
func dB(in string) float32 {
in = strings.ToLower(in)
in = strings.TrimSuffix(in, " db")
in = strings.TrimSuffix(in, "db")
return flt(in)
}
func intSep(sep, in string) int {
start, _, _ := strings.Cut(in, sep)
out, _ := strconv.Atoi(start)