diff --git a/.github/workflows/nightly-release.yaml b/.github/workflows/nightly-release.yaml index d5bf63d..acf7cd9 100644 --- a/.github/workflows/nightly-release.yaml +++ b/.github/workflows/nightly-release.yaml @@ -33,11 +33,10 @@ jobs: sudo apt update -qq sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev - name: Lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.54 + version: v1.60 args: --timeout=5m - install-mode: "goinstall" - name: Test run: go test ./... build-release: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5edd1ad..8a5987b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -19,11 +19,10 @@ jobs: sudo apt update -qq sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev - name: Lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.54 + version: v1.60 args: --timeout=5m - install-mode: "goinstall" - name: Test run: go test ./... release-please: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index bb25da4..a186d19 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -20,10 +20,9 @@ jobs: sudo apt update -qq sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev - name: Lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.54 + version: v1.60 args: --timeout=5m - install-mode: "goinstall" - name: Test run: go test ./... diff --git a/.golangci.yml b/.golangci.yml index d01804f..770413e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -81,6 +81,9 @@ issues: - text: "weak random number generator" linters: - gosec + - text: "integer overflow conversion" + linters: + - gosec - text: "at least one file in a package should have a package comment" linters: - stylecheck diff --git a/Dockerfile b/Dockerfile index e1204ca..ba3b1b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ -FROM alpine:3.19 AS builder-taglib +FROM alpine:3.20 AS builder-taglib WORKDIR /tmp COPY alpine/taglib/APKBUILD . RUN apk update && \ - apk add --no-cache abuild && \ - abuild-keygen -a -n && \ + apk add --no-cache abuild doas && \ + echo "permit nopass root" > /etc/doas.conf && \ + abuild-keygen -a -n -i && \ REPODEST=/pkgs abuild -F -r -FROM golang:1.21-alpine AS builder +FROM golang:1.23-alpine AS builder RUN apk add -U --no-cache \ build-base \ ca-certificates \ @@ -26,7 +27,7 @@ RUN go mod download COPY . . RUN GOOS=linux go build -o gonic cmd/gonic/gonic.go -FROM alpine:3.19 +FROM alpine:3.20 LABEL org.opencontainers.image.source https://github.com/sentriz/gonic RUN apk add -U --no-cache \ ffmpeg \ diff --git a/Dockerfile.debug b/Dockerfile.debug index 39f94ea..d1530b8 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -1,4 +1,4 @@ -FROM golang:1.21-alpine AS builder +FROM golang:1.23-alpine AS builder RUN apk add -U --no-cache \ build-base \ ca-certificates \ diff --git a/Dockerfile.dev b/Dockerfile.dev index e9d915f..71659a9 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:experimental -FROM golang:1.21-alpine AS builder +FROM golang:1.23-alpine AS builder RUN apk add -U --no-cache \ build-base \ ca-certificates \ @@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ GOOS=linux go build -o gonic cmd/gonic/gonic.go -FROM alpine:3.18 +FROM alpine:3.20 RUN apk add -U --no-cache \ ffmpeg \ mpv \ diff --git a/README.md b/README.md index af2a117..6cafaca 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ password can then be changed from the web interface | `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)) | +| `GONIC_TRANSCODE_CACHE_SIZE` | `-transcode-cache-size` | **optional** size of the transcode cache in MB (0 = no limit) | +| `GONIC_TRANSCODE_EJECT_INTERVAL` | `-transcode-eject-interval` | **optional** interval (in minutes) to eject transcode cache (0 = never) | | `GONIC_EXPVAR` | `-expvar` | **optional** enable the /debug/vars endpoint (exposes useful debugging attributes as well as database stats) | ## multi valued tags (v0.16+) diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index 74e5d79..6074080 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -96,6 +96,9 @@ func main() { deprecatedConfGenreSplit := flag.String("genre-split", "", "(deprecated, see multi-value settings)") + confTranscodeCacheSize := flag.Int("transcode-cache-size", 0, "size of the transcode cache in MB (0 = no limit) (optional)") + confTranscodeEjectInterval := flag.Int("transcode-eject-interval", 0, "interval (in minutes) to eject transcode cache (0 = never) (optional)") + flag.Parse() flagconf.ParseEnv() flagconf.ParseConfig(*confConfigPath) @@ -205,6 +208,7 @@ func main() { transcoder := transcode.NewCachingTranscoder( transcode.NewFFmpegTranscoder(), cacheDirAudio, + *confTranscodeCacheSize, ) lastfmClientKeySecretFunc := func() (string, string, error) { @@ -412,6 +416,21 @@ func main() { return nil }) + errgrp.Go(func() error { + if *confTranscodeEjectInterval == 0 || *confTranscodeCacheSize == 0 { + return nil + } + + defer logJob("transcode cache eject")() + + ctxTick(ctx, time.Duration(*confTranscodeEjectInterval)*time.Minute, func() { + if err := transcoder.CacheEject(); err != nil { + log.Printf("error ejecting transcode cache: %v", err) + } + }) + return nil + }) + errgrp.Go(func() error { if *confScanIntervalMins == 0 { return nil diff --git a/contrib/config b/contrib/config index 9031113..f50ed64 100644 --- a/contrib/config +++ b/contrib/config @@ -49,6 +49,10 @@ playlists-path # regenerated. cache-path /var/cache/gonic +# Option to eject least recently used items from transcode cache. +#transcode-cache-size 5000 # in Mb (0 = no limit) +#transcode-eject-interval 1440 # in minutes (0 = never eject) + # Interval (in minutes) to check for new music. Default: don't scan #scan-interval 0 #scan-at-start-enabled false diff --git a/go.mod b/go.mod index d9f6ac2..b98219d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module go.senan.xyz/gonic -go 1.21 +go 1.23.0 require ( github.com/Masterminds/sprig v2.22.0+incompatible @@ -14,22 +14,22 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 - github.com/gorilla/sessions v1.2.2 + github.com/gorilla/sessions v1.4.0 github.com/jinzhu/gorm v1.9.17-0.20211120011537-5c235b72a414 github.com/josephburnett/jd v1.8.1 - github.com/mattn/go-sqlite3 v1.14.22 + github.com/mattn/go-sqlite3 v1.14.23 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-20240401192409-5a8ac2a2974f + github.com/sentriz/audiotags v0.0.0-20240713161505-a6bb82b19f54 github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 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 + go.senan.xyz/flagconf v0.1.9 + go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230 + golang.org/x/net v0.29.0 + golang.org/x/sync v0.8.0 gopkg.in/gormigrate.v1 v1.6.0 jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 ) @@ -37,12 +37,12 @@ require ( require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect - github.com/PuerkitoBio/goquery v1.9.1 // indirect + github.com/PuerkitoBio/goquery v1.10.0 // indirect github.com/davecgh/go-spew v1.1.1 // 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/huandu/xstrings v1.5.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.2 // indirect @@ -50,7 +50,7 @@ require ( 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/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mmcdole/goxpp v1.1.1 // indirect @@ -60,9 +60,10 @@ 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.22.0 // indirect - golang.org/x/image v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/image v0.20.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 4f423af..33a3045 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ 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.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= -github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= +github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= @@ -57,10 +57,10 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= -github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= @@ -92,12 +92,12 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= -github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= -github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0= +github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -128,8 +128,8 @@ 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-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/audiotags v0.0.0-20240713161505-a6bb82b19f54 h1:JaJaWCUDLGUbU8AIO+YhQ+Nq4ByCCaApLuHi868uWQw= +github.com/sentriz/audiotags v0.0.0-20240713161505-a6bb82b19f54/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= @@ -139,18 +139,20 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV 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.8 h1:0HadvAEXHYJOGGdO6cHz2Ok4vWawaM64m5ldSjLoVUw= -go.senan.xyz/flagconf v0.1.8/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ= +go.senan.xyz/flagconf v0.1.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4= +go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ= +go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230 h1:Peeh4dn9T9YD3wFR4ocef74/2GRCRCqRz/Mx8sO9VPw= +go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230/go.mod h1:bnHbnDhLgt0ckjAzT/YNJmzHFXf0hHI4BVVS4w4V7S8= 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.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 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= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -163,13 +165,13 @@ 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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.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= @@ -180,8 +182,8 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.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= @@ -191,14 +193,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 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= diff --git a/playlist/playlist.go b/playlist/playlist.go index e51dbc1..c3bec6a 100644 --- a/playlist/playlist.go +++ b/playlist/playlist.go @@ -94,6 +94,10 @@ func (s *Store) Read(relPath string) (*Playlist, error) { return nil, fmt.Errorf("stat m3u: %w", err) } + if stat.IsDir() { + return nil, errors.New("path is a directory") + } + var playlist Playlist playlist.UpdatedAt = stat.ModTime() diff --git a/scanner/coverresolve/cover.go b/scanner/coverresolve/cover.go deleted file mode 100644 index 4806925..0000000 --- a/scanner/coverresolve/cover.go +++ /dev/null @@ -1,83 +0,0 @@ -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 -} diff --git a/scanner/coverresolve/cover_test.go b/scanner/coverresolve/cover_test.go deleted file mode 100644 index 27327ca..0000000 --- a/scanner/coverresolve/cover_test.go +++ /dev/null @@ -1,85 +0,0 @@ -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) - } - }) - } -} diff --git a/scanner/scanner.go b/scanner/scanner.go index c9bd646..287d571 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -24,8 +24,8 @@ import ( "go.senan.xyz/gonic/db" "go.senan.xyz/gonic/fileutil" - "go.senan.xyz/gonic/scanner/coverresolve" "go.senan.xyz/gonic/tags/tagcommon" + "go.senan.xyz/wrtag/coverparse" ) var ( @@ -267,7 +267,7 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error { } var tracks []string - var covers []string + var cover string for _, item := range items { absPath := filepath.Join(absPath, item.Name()) if s.excludePattern != nil && s.excludePattern.MatchString(absPath) { @@ -278,8 +278,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error { continue } - if coverresolve.IsCover(item.Name()) { - covers = append(covers, item.Name()) + if coverparse.IsCover(item.Name()) { + coverparse.BestBetween(&cover, item.Name()) continue } if s.tagReader.CanRead(absPath) { @@ -288,8 +288,6 @@ 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 { diff --git a/server/ctrlsubsonic/handlers_playlist.go b/server/ctrlsubsonic/handlers_playlist.go index ca0c6e5..8426f8a 100644 --- a/server/ctrlsubsonic/handlers_playlist.go +++ b/server/ctrlsubsonic/handlers_playlist.go @@ -75,8 +75,10 @@ func (c *Controller) ServeCreateOrUpdatePlaylist(r *http.Request) *spec.Response playlistPath := playlistIDDecode(playlistID) var playlist playlistp.Playlist - if pl, _ := c.playlistStore.Read(playlistPath); pl != nil { - playlist = *pl + if playlistPath != "" { + if pl, err := c.playlistStore.Read(playlistPath); err != nil && pl != nil { + playlist = *pl + } } if playlist.UserID != 0 && playlist.UserID != user.ID { diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 90b75e6..0366448 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -64,7 +64,7 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s return nil } - w.Header().Set("Cache-Control", "public, max-age=3600") + w.Header().Set("Cache-Control", "public, max-age=1209600") http.ServeFile(w, r, cachePath) return nil diff --git a/transcode/transcode_test.go b/transcode/transcode_test.go index eecced2..b28b7b6 100644 --- a/transcode/transcode_test.go +++ b/transcode/transcode_test.go @@ -113,7 +113,7 @@ func TestCachingParallelism(t *testing.T) { callback: func() { realTranscodeCount.Add(1) }, } - cacheTranscoder := transcode.NewCachingTranscoder(transcoder, t.TempDir()) + cacheTranscoder := transcode.NewCachingTranscoder(transcoder, t.TempDir(), 1024) var wg sync.WaitGroup for i := 0; i < 5; i++ { diff --git a/transcode/transcoder_caching.go b/transcode/transcoder_caching.go index d402652..b7021e0 100644 --- a/transcode/transcoder_caching.go +++ b/transcode/transcoder_caching.go @@ -5,9 +5,12 @@ import ( "crypto/md5" "fmt" "io" + "io/fs" "os" "path/filepath" + "sort" "sync" + "time" ) const perm = 0o644 @@ -15,16 +18,21 @@ const perm = 0o644 type CachingTranscoder struct { cachePath string transcoder Transcoder + limitMB int locks keyedMutex + cleanLock sync.RWMutex } var _ Transcoder = (*CachingTranscoder)(nil) -func NewCachingTranscoder(t Transcoder, cachePath string) *CachingTranscoder { - return &CachingTranscoder{transcoder: t, cachePath: cachePath} +func NewCachingTranscoder(t Transcoder, cachePath string, limitMB int) *CachingTranscoder { + return &CachingTranscoder{transcoder: t, cachePath: cachePath, limitMB: limitMB} } func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error { + t.cleanLock.RLock() + defer t.cleanLock.RUnlock() + // don't try cache partial transcodes if profile.Seek() > 0 { return t.transcoder.Transcode(ctx, profile, in, out) @@ -52,6 +60,7 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s if i, err := cf.Stat(); err == nil && i.Size() > 0 { _, _ = io.Copy(out, cf) + _ = os.Chtimes(path, time.Now(), time.Now()) // Touch for LRU cache purposes return nil } @@ -64,6 +73,55 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s return nil } +func (t *CachingTranscoder) CacheEject() error { + t.cleanLock.Lock() + defer t.cleanLock.Unlock() + + // Delete LRU cache files that exceed size limit. Use last modified time. + type file struct { + path string + info os.FileInfo + } + + var files []file + var total int64 = 0 + + err := filepath.WalkDir(t.cachePath, func(path string, de fs.DirEntry, err error) error { + if err != nil { + return err + } + if !de.IsDir() { + info, err := de.Info() + if err != nil { + return fmt.Errorf("walk cache path for eject: %w", err) + } + files = append(files, file{path, info}) + total += info.Size() + } + return nil + }) + + if err != nil { + return fmt.Errorf("walk cache path for eject: %w", err) + } + + sort.Slice(files, func(i, j int) bool { + return files[i].info.ModTime().Before(files[j].info.ModTime()) + }) + + for total > int64(t.limitMB)*1024*1024 { + curFile := files[0] + files = files[1:] + total -= curFile.info.Size() + err = os.Remove(curFile.path) + if err != nil { + return fmt.Errorf("remove cache file: %w", err) + } + } + + return nil +} + func cacheKey(cmd string, args []string) string { // the cache is invalid whenever transcode command (which includes the // absolute filepath, bit rate args, replay gain args, etc.) changes