Compare commits

...

32 Commits

Author SHA1 Message Date
612d43be11 fix: mockfs.go AbsPath()
Some checks failed
Release / Lint and test (push) Failing after 16m50s
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Release / Notify IRC (push) Has been skipped
Nightly Release / Check latest commit (push) Has been cancelled
Nightly Release / Lint and test (push) Has been cancelled
Nightly Release / Build and release Docker image (push) Has been cancelled
2024-09-25 15:22:38 +08:00
a9b565f948 Merge remote-tracking branch 'origin/master'
Some checks failed
Release / Lint and test (push) Failing after 17m48s
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Release / Notify IRC (push) Has been skipped
Nightly Release / Check latest commit (push) Successful in 9s
Nightly Release / Build and release Docker image (push) Has been skipped
Nightly Release / Lint and test (push) Has been skipped
Conflicts:
	scanner/scanner.go
2024-09-18 17:41:40 +08:00
Nadia Santalla
ac798ac2d2 fix(playlist): fix non-admin users not being able to create playlists (#524)
* fix(playlist): fail early if playlist path is a directory

* fix(playlist): check error before assuming playlist loaded
2024-09-15 14:57:23 +00:00
brian-doherty
bcb613c79c feat(transcode): add cache pruning and config options
* Added config option to set size of transcode cache and cadence to enforce that sizing via ejection.

* Added cache eject to contrib/config.

* Added error return for CacheEject(). Changed to use WalkDir() instead of Walk().

* Lint fix.

* Added universal lock for cache eject.

* Removed accidentally committed binary.
2024-09-15 14:04:28 +00:00
sentriz
bfa0e130d4 chore(deps): bump 2024-09-13 11:52:43 +01:00
sentriz
640d872f4c fix(ci): bump golangci-lint 2024-09-12 18:41:10 +01:00
sentriz
453639ee34 feat(scanner): use wrtag/coverparse for cover selection
later, gonic will also use wrtag/tags for tag parsing

closes #338
closes #516
2024-09-12 18:36:57 +01:00
sentriz
120fd7959a fix(ci): ignore gosec integer overflow conversion 2024-09-12 18:36:57 +01:00
sentriz
875a83ad4f chore: bump to go1.23 2024-09-12 18:30:40 +01:00
Nadia Santalla
fb36dbf719 fix(dockerfile): install abuild key (#526)
Without this, abuild refuses to run correctly.
2024-09-12 17:24:09 +00:00
sentriz
bbe16b7555 feat(subsonic): bump image cache expiration 2024-09-03 12:43:10 +01:00
sentriz
a1d929e486 chore(deps): bump 2024-07-13 17:19:45 +01:00
211e104535 typo
Some checks failed
Release / Lint and test (push) Failing after 17m37s
Release / Notify IRC (push) Has been skipped
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Nightly Release / Check latest commit (push) Successful in 17s
Nightly Release / Build and release Docker image (push) Has been skipped
Nightly Release / Lint and test (push) Has been skipped
2024-07-01 17:15:06 +08:00
15a13a149b fix: ffprobe length in unit (k)
Some checks failed
Release / Lint and test (push) Failing after 17m35s
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Release / Notify IRC (push) Has been skipped
2024-07-01 16:08:16 +08:00
02be9219b1 add ffprobe tag reader for webm, mp4, mkv
Some checks failed
Release / Lint and test (push) Failing after 17m42s
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Release / Notify IRC (push) Has been skipped
This is not well-tested tag reader. So FFProbe.Read() only success in
following condition:
- FFProbeScore == 100
2024-07-01 15:27:00 +08:00
e8e478f2aa feat: print request duration 2024-06-25 10:43:43 +08:00
8abe131f28 mark heimoshuiyu fork version string 2024-06-25 00:34:57 +08:00
8914c59978 Use file name for unknown tag title retrieval.
When taglib get empty or null title from file/album, use file name or
folder name as the tag_title field. It could be confuse to user, but at
leaset it is better than tracks/albums can't be search in ID3 mode API.
2024-06-24 16:21:04 +08:00
332f00ff7a feat: add cli args -db-log
I add a command-line arguments "-db-log" default to false. This help me
debug with the gorm SQL.
2024-06-24 14:54:43 +08:00
71ae1029e8 fead: cli args: -podcast-download default to false
This commit turns off the podcast download ability.
Because the podcast download routine will execute a DB query every 5
sec. To temporary fix the db pressure I add this command-line flag to
turn it off. I will be looking for a way to reduce DB pressure and
manually trigger eposide downloads later on.
2024-06-24 14:46:28 +08:00
b70979d2e0 transcode profile: add opus320, opus512
Some checks failed
Release / Lint and test (push) Failing after 18m13s
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Release / Notify IRC (push) Has been skipped
Nightly Release / Check latest commit (push) Successful in 15s
Nightly Release / Build and release Docker image (push) Has been skipped
Nightly Release / Lint and test (push) Has been skipped
2024-06-15 14:52:24 +08:00
853107fca6 cache cover in jpeg format
Some checks failed
Release / Lint and test (push) Failing after 26m31s
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Release / Notify IRC (push) Has been skipped
Nightly Release / Check latest commit (push) Successful in 14s
Nightly Release / Build and release Docker image (push) Has been skipped
Nightly Release / Lint and test (push) Has been skipped
2024-06-12 17:41:16 +08:00
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
42 changed files with 706 additions and 332 deletions

View File

@@ -33,11 +33,10 @@ jobs:
sudo apt update -qq sudo apt update -qq
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.54 version: v1.60
args: --timeout=5m args: --timeout=5m
install-mode: "goinstall"
- name: Test - name: Test
run: go test ./... run: go test ./...
build-release: build-release:

View File

@@ -19,11 +19,10 @@ jobs:
sudo apt update -qq sudo apt update -qq
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.54 version: v1.60
args: --timeout=5m args: --timeout=5m
install-mode: "goinstall"
- name: Test - name: Test
run: go test ./... run: go test ./...
release-please: release-please:

View File

@@ -20,10 +20,9 @@ jobs:
sudo apt update -qq sudo apt update -qq
sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev sudo apt install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev
- name: Lint - name: Lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v6
with: with:
version: v1.54 version: v1.60
args: --timeout=5m args: --timeout=5m
install-mode: "goinstall"
- name: Test - name: Test
run: go test ./... run: go test ./...

View File

@@ -45,7 +45,6 @@ linters:
- makezero - makezero
- mirror - mirror
- misspell - misspell
- musttag
- nakedret - nakedret
- nestif - nestif
- nilerr - nilerr
@@ -82,6 +81,9 @@ issues:
- text: "weak random number generator" - text: "weak random number generator"
linters: linters:
- gosec - gosec
- text: "integer overflow conversion"
linters:
- gosec
- text: "at least one file in a package should have a package comment" - text: "at least one file in a package should have a package comment"
linters: linters:
- stylecheck - stylecheck

View File

@@ -1,12 +1,13 @@
FROM alpine:3.18 AS builder-taglib FROM alpine:3.20 AS builder-taglib
WORKDIR /tmp WORKDIR /tmp
COPY alpine/taglib/APKBUILD . COPY alpine/taglib/APKBUILD .
RUN apk update && \ RUN apk update && \
apk add --no-cache abuild && \ apk add --no-cache abuild doas && \
abuild-keygen -a -n && \ echo "permit nopass root" > /etc/doas.conf && \
abuild-keygen -a -n -i && \
REPODEST=/pkgs abuild -F -r 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 \ RUN apk add -U --no-cache \
build-base \ build-base \
ca-certificates \ ca-certificates \
@@ -26,7 +27,7 @@ RUN go mod download
COPY . . COPY . .
RUN GOOS=linux go build -o gonic cmd/gonic/gonic.go RUN GOOS=linux go build -o gonic cmd/gonic/gonic.go
FROM alpine:3.18 FROM alpine:3.20
LABEL org.opencontainers.image.source https://github.com/sentriz/gonic LABEL org.opencontainers.image.source https://github.com/sentriz/gonic
RUN apk add -U --no-cache \ RUN apk add -U --no-cache \
ffmpeg \ ffmpeg \

View File

@@ -1,4 +1,4 @@
FROM golang:1.21-alpine AS builder FROM golang:1.23-alpine AS builder
RUN apk add -U --no-cache \ RUN apk add -U --no-cache \
build-base \ build-base \
ca-certificates \ ca-certificates \

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:experimental # syntax=docker/dockerfile:experimental
FROM golang:1.21-alpine AS builder FROM golang:1.23-alpine AS builder
RUN apk add -U --no-cache \ RUN apk add -U --no-cache \
build-base \ build-base \
ca-certificates \ ca-certificates \
@@ -14,7 +14,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/root/.cache/go-build \
GOOS=linux go build -o gonic cmd/gonic/gonic.go GOOS=linux go build -o gonic cmd/gonic/gonic.go
FROM alpine:3.18 FROM alpine:3.20
RUN apk add -U --no-cache \ RUN apk add -U --no-cache \
ffmpeg \ ffmpeg \
mpv \ mpv \

View File

@@ -67,10 +67,12 @@ 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_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_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_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_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_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_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) | | `GONIC_EXPVAR` | `-expvar` | **optional** enable the /debug/vars endpoint (exposes useful debugging attributes as well as database stats) |
## multi valued tags (v0.16+) ## multi valued tags (v0.16+)
@@ -97,7 +99,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. 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 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 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> # Contributor: Leo <thinkabit.ukim@gmail.com>
# Maintainer: Natanael Copa <ncopa@alpinelinux.org> # Maintainer: Natanael Copa <ncopa@alpinelinux.org>
pkgname=taglib2 pkgname=taglib2
pkgver=2.0 pkgver=2.0.1
pkgrel=0 pkgrel=0
pkgdesc="Library for reading and editing metadata of several popular audio formats" pkgdesc="Library for reading and editing metadata of several popular audio formats"
url="https://taglib.github.io/" url="https://taglib.github.io/"
@@ -30,10 +30,11 @@ build() {
-DCMAKE_BUILD_TYPE=MinSizeRel \ -DCMAKE_BUILD_TYPE=MinSizeRel \
-DWITH_ZLIB=ON \ -DWITH_ZLIB=ON \
-DBUILD_SHARED_LIBS=ON \ -DBUILD_SHARED_LIBS=ON \
-DBUILD_EXAMPLES=ON \ -DBUILD_EXAMPLES=OFF \
-DBUILD_TESTING="$(want_check && echo ON || echo OFF)" \ -DBUILD_TESTING="$(want_check && echo ON || echo OFF)" \
-DVISIBILITY_HIDDEN=ON -DVISIBILITY_HIDDEN=ON
cmake --build build CPLUS_INCLUDE_PATH="/usr/include/utf8cpp" \
cmake --build build
} }
check() { check() {
@@ -51,5 +52,5 @@ _lib() {
} }
sha512sums=" sha512sums="
099d02b2eab033f5702a8cb03e70752d7523c6f8c2f3eebdd0bcd939eafbdca3f2a6c82452983904b5822cfa45f2707ed866c3419508df9d43bf5c0b3a476f6c taglib-2.0.tar.gz 25ee89293a96d7f8dca6276f822bdaef01fd98503b78c20ffeac8e1d9821de7273a5127146aa798d304c6a995cb2b7229a205aff1cc261b5d4fa9e499dda0439 taglib-2.0.1.tar.gz
" "

View File

@@ -45,6 +45,7 @@ import (
"go.senan.xyz/gonic/scrobble" "go.senan.xyz/gonic/scrobble"
"go.senan.xyz/gonic/server/ctrladmin" "go.senan.xyz/gonic/server/ctrladmin"
"go.senan.xyz/gonic/server/ctrlsubsonic" "go.senan.xyz/gonic/server/ctrlsubsonic"
"go.senan.xyz/gonic/tags/ffprobe"
"go.senan.xyz/gonic/tags/tagcommon" "go.senan.xyz/gonic/tags/tagcommon"
"go.senan.xyz/gonic/tags/taglib" "go.senan.xyz/gonic/tags/taglib"
"go.senan.xyz/gonic/transcode" "go.senan.xyz/gonic/transcode"
@@ -58,6 +59,7 @@ func main() {
confPodcastPurgeAgeDays := flag.Uint("podcast-purge-age", 0, "age (in days) to purge podcast episodes if not accessed (optional)") confPodcastPurgeAgeDays := flag.Uint("podcast-purge-age", 0, "age (in days) to purge podcast episodes if not accessed (optional)")
confPodcastPath := flag.String("podcast-path", "", "path to podcasts") confPodcastPath := flag.String("podcast-path", "", "path to podcasts")
confPodcastDownload := flag.Bool("podcast-download", false, "whether to download podcasts (optional, default false)")
confCachePath := flag.String("cache-path", "", "path to cache") confCachePath := flag.String("cache-path", "", "path to cache")
@@ -67,6 +69,7 @@ func main() {
confPlaylistsPath := flag.String("playlists-path", "", "path to your list of new or existing m3u playlists that gonic can manage") confPlaylistsPath := flag.String("playlists-path", "", "path to your list of new or existing m3u playlists that gonic can manage")
confDBPath := flag.String("db-path", "gonic.db", "path to database (optional)") confDBPath := flag.String("db-path", "gonic.db", "path to database (optional)")
confDBLog := flag.Bool("db-log", false, "database logging (optional)")
confScanIntervalMins := flag.Uint("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)") confScanIntervalMins := flag.Uint("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)")
confScanAtStart := flag.Bool("scan-at-start-enabled", false, "whether to perform an initial scan at startup (optional)") confScanAtStart := flag.Bool("scan-at-start-enabled", false, "whether to perform an initial scan at startup (optional)")
@@ -93,6 +96,9 @@ func main() {
deprecatedConfGenreSplit := flag.String("genre-split", "", "(deprecated, see multi-value settings)") 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() flag.Parse()
flagconf.ParseEnv() flagconf.ParseEnv()
flagconf.ParseConfig(*confConfigPath) flagconf.ParseConfig(*confConfigPath)
@@ -140,6 +146,7 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("error opening database: %v\n", err) log.Fatalf("error opening database: %v\n", err)
} }
dbc.LogMode(*confDBLog)
defer dbc.Close() defer dbc.Close()
err = dbc.Migrate(db.MigrationContext{ err = dbc.Migrate(db.MigrationContext{
@@ -182,7 +189,7 @@ func main() {
tagReader := tagcommon.ChainReader{ tagReader := tagcommon.ChainReader{
taglib.TagLib{}, taglib.TagLib{},
// ffprobe reader? ffprobe.FFProbe{},
// nfo reader? // nfo reader?
} }
@@ -201,6 +208,7 @@ func main() {
transcoder := transcode.NewCachingTranscoder( transcoder := transcode.NewCachingTranscoder(
transcode.NewFFmpegTranscoder(), transcode.NewFFmpegTranscoder(),
cacheDirAudio, cacheDirAudio,
*confTranscodeCacheSize,
) )
lastfmClientKeySecretFunc := func() (string, string, error) { lastfmClientKeySecretFunc := func() (string, string, error) {
@@ -273,7 +281,7 @@ func main() {
mux.Handle("/admin/", http.StripPrefix("/admin", chain(ctrlAdmin))) mux.Handle("/admin/", http.StripPrefix("/admin", chain(ctrlAdmin)))
mux.Handle("/rest/", http.StripPrefix("/rest", chain(trim(ctrlSubsonic)))) mux.Handle("/rest/", http.StripPrefix("/rest", chain(trim(ctrlSubsonic))))
mux.Handle("/ping", chain(handlerutil.Message("ok"))) 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 { if *confExpvar {
mux.Handle("/debug/vars", expvar.Handler()) mux.Handle("/debug/vars", expvar.Handler())
@@ -379,6 +387,10 @@ func main() {
}) })
errgrp.Go(func() error { errgrp.Go(func() error {
if !*confPodcastDownload {
return nil
}
defer logJob("podcast download")() defer logJob("podcast download")()
ctxTick(ctx, 5*time.Second, func() { ctxTick(ctx, 5*time.Second, func() {
@@ -404,6 +416,21 @@ func main() {
return nil 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 { errgrp.Go(func() error {
if *confScanIntervalMins == 0 { if *confScanIntervalMins == 0 {
return nil return nil

View File

@@ -49,6 +49,10 @@ playlists-path <path to your m3u playlist dir>
# regenerated. # regenerated.
cache-path /var/cache/gonic 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 # Interval (in minutes) to check for new music. Default: don't scan
#scan-interval 0 #scan-interval 0
#scan-at-start-enabled false #scan-at-start-enabled false

View File

@@ -235,9 +235,15 @@ type Track struct {
TagTrackNumber int `sql:"default: null"` TagTrackNumber int `sql:"default: null"`
TagDiscNumber int `sql:"default: null"` TagDiscNumber int `sql:"default: null"`
TagBrainzID string `sql:"default: null"` TagBrainzID string `sql:"default: null"`
TrackStar *TrackStar
TrackRating *TrackRating ReplayGainTrackGain float32
AverageRating float64 `sql:"default: null"` ReplayGainTrackPeak float32
ReplayGainAlbumGain float32
ReplayGainAlbumPeak float32
TrackStar *TrackStar
TrackRating *TrackRating
AverageRating float64 `sql:"default: null"`
} }
func (t *Track) AudioLength() int { return t.Length } 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, "202311072309", migrateAlbumInfo),
construct(ctx, "202311082304", migrateTemporaryDisplayAlbumArtist), construct(ctx, "202311082304", migrateTemporaryDisplayAlbumArtist),
construct(ctx, "202312110003", migrateAddExtraIndexes), construct(ctx, "202312110003", migrateAddExtraIndexes),
construct(ctx, "202405301140", migrateAddReplayGainFields),
} }
return gormigrate. 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); CREATE INDEX idx_artist_appearances_album_id ON "artist_appearances" (album_id);
`).Error `).Error
} }
func migrateAddReplayGainFields(tx *gorm.DB, _ MigrationContext) error {
return tx.AutoMigrate(Track{}).Error
}

39
go.mod
View File

@@ -1,34 +1,35 @@
module go.senan.xyz/gonic module go.senan.xyz/gonic
go 1.21 go 1.23.0
require ( require (
github.com/Masterminds/sprig v2.22.0+incompatible github.com/Masterminds/sprig v2.22.0+incompatible
github.com/andybalholm/cascadia v1.3.2 github.com/andybalholm/cascadia v1.3.2
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37 github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/djherbis/times v1.6.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.7.0 github.com/fsnotify/fsnotify v1.7.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/securecookie v1.1.2 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/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/mattn/go-sqlite3 v1.14.23
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/mmcdole/gofeed v1.3.0 github.com/mmcdole/gofeed v1.3.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4 github.com/philippta/go-template v0.0.0-20220911145045-4556aca435e4
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be 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-20240713161505-a6bb82b19f54
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.9.0
go.senan.xyz/flagconf v0.1.7 go.senan.xyz/flagconf v0.1.9
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230
golang.org/x/net v0.22.0 golang.org/x/net v0.29.0
golang.org/x/sync v0.6.0 golang.org/x/sync v0.8.0
gopkg.in/gormigrate.v1 v1.6.0 gopkg.in/gormigrate.v1 v1.6.0
jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056
) )
@@ -36,12 +37,12 @@ require (
require ( require (
github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // 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/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.22.4 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/gorilla/context v1.1.2 // 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/imdario/mergo v0.3.16 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.2 // indirect github.com/jinzhu/now v1.1.2 // indirect
@@ -49,7 +50,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/lib/pq v1.3.0 // indirect github.com/lib/pq v1.3.0 // indirect
github.com/mailru/easyjson v0.7.7 // 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/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mmcdole/goxpp v1.1.1 // indirect github.com/mmcdole/goxpp v1.1.1 // indirect
@@ -59,10 +60,10 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.27.0 // indirect
golang.org/x/image v0.15.0 // indirect golang.org/x/image v0.20.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.18.0 // indirect
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

97
go.sum
View File

@@ -6,10 +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 h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 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.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.9.0 h1:zgjKkdpRY9T97Q5DCtcXwfqkcylSFIVCocZmn2huTp8= github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.9.0/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
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= 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 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
@@ -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/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 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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= 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/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 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 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.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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.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 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -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 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 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.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 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 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
@@ -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/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 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 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.8.1 h1:U4wae4kEvduCmf5mlXJ3uKnfHFmGhwttEFkQ6rsoDMk=
github.com/josephburnett/jd v1.7.1/go.mod h1:R8ZnZnLt2D4rhW4NvBc/USTo6mzyNT6fYNIIWOJA9GY= 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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -92,24 +92,20 @@ 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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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.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.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.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 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= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 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 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE= 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 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8= 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= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -132,39 +128,31 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 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-20240713161505-a6bb82b19f54 h1:JaJaWCUDLGUbU8AIO+YhQ+Nq4ByCCaApLuHi868uWQw=
github.com/sentriz/audiotags v0.0.0-20240202193907-618ae39d7743/go.mod h1:Zoo4UP5t2ySbPwScJfoydAlLLBonoqntv4ovA1T91Z8= github.com/sentriz/audiotags v0.0.0-20240713161505-a6bb82b19f54/go.mod h1:+pmkMFDEXJuu/u4h2OYJVfYF2qIhXJD7kqvWq6q5Zo0=
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/gormstore v0.0.0-20220105134332-64e31f7f6981 h1:sLILANWN76ja66/K4k/mBqJuCjDZaM67w+Ru6rEB0s0= 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/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 h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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= 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.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4=
go.senan.xyz/flagconf v0.1.5/go.mod h1:CGD/sgYWiTacz1ojgsQRwErqLxtShWMpBxxnsJI6yaE= go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
go.senan.xyz/flagconf v0.1.7 h1:+o9Cg3WyzCG+KSfZAwOP61dTWSzGhfH3W+zz9mbNJOA= go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230 h1:Peeh4dn9T9YD3wFR4ocef74/2GRCRCqRz/Mx8sO9VPw=
go.senan.xyz/flagconf v0.1.7/go.mod h1:CGD/sgYWiTacz1ojgsQRwErqLxtShWMpBxxnsJI6yaE= 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-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-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-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-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
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/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 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.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= 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.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/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -177,28 +165,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.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.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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-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-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-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-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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.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.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -208,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.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.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.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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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-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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 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= 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= 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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -5,6 +5,7 @@ import (
"log" "log"
"net/http" "net/http"
"strings" "strings"
"time"
) )
type Middleware func(http.Handler) http.Handler type Middleware func(http.Handler) http.Handler
@@ -29,9 +30,15 @@ func TrimPathSuffix(suffix string) Middleware {
func Log(next http.Handler) http.Handler { func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
begin := time.Now()
sw := &statusWriter{ResponseWriter: w} sw := &statusWriter{ResponseWriter: w}
next.ServeHTTP(sw, r) next.ServeHTTP(sw, r)
log.Printf("response %s %s %v", statusToBlock(sw.status), r.Method, r.URL) elasped := time.Since(begin)
log.Printf("response %s %s %s %v",
statusToBlock(sw.status),
elasped.String(),
r.Method,
r.URL)
}) })
} }
@@ -56,12 +63,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 { func Message(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, message) fmt.Fprintln(w, message)

View File

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

View File

@@ -362,8 +362,16 @@ func (i *TagInfo) Genres() []string { return []string{i.RawGenre} }
func (i *TagInfo) TrackNumber() int { return 1 } func (i *TagInfo) TrackNumber() int { return 1 }
func (i *TagInfo) DiscNumber() int { return 1 } func (i *TagInfo) DiscNumber() int { return 1 }
func (i *TagInfo) Year() int { return 2021 } 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) }
func (i *TagInfo) AbsPath() string { return "" }
var _ tagcommon.Reader = (*tagReader)(nil) var _ tagcommon.Reader = (*tagReader)(nil)

View File

@@ -94,12 +94,16 @@ func (s *Store) Read(relPath string) (*Playlist, error) {
return nil, fmt.Errorf("stat m3u: %w", err) return nil, fmt.Errorf("stat m3u: %w", err)
} }
if stat.IsDir() {
return nil, errors.New("path is a directory")
}
var playlist Playlist var playlist Playlist
playlist.UpdatedAt = stat.ModTime() playlist.UpdatedAt = stat.ModTime()
playlist.UserID, err = userIDFromPath(relPath) playlist.UserID, err = userIDFromPath(relPath)
if err != nil { 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)) playlist.Name = strings.TrimSuffix(filepath.Base(relPath), filepath.Ext(relPath))

View File

@@ -17,6 +17,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/djherbis/times"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/rainycape/unidecode" "github.com/rainycape/unidecode"
@@ -24,6 +25,7 @@ import (
"go.senan.xyz/gonic/db" "go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/fileutil" "go.senan.xyz/gonic/fileutil"
"go.senan.xyz/gonic/tags/tagcommon" "go.senan.xyz/gonic/tags/tagcommon"
"go.senan.xyz/wrtag/coverparse"
) )
var ( var (
@@ -134,9 +136,18 @@ func (s *Scanner) ExecuteWatch(ctx context.Context) error {
} }
batchSeen := map[string]struct{}{} batchSeen := map[string]struct{}{}
batchClean := false
for { for {
select { select {
case <-batchT.C: 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() { if !s.StartScanning() {
break break
} }
@@ -164,6 +175,10 @@ func (s *Scanner) ExecuteWatch(ctx context.Context) error {
clear(batchSeen) clear(batchSeen)
case event := <-watcher.Events: case event := <-watcher.Events:
if event.Op&(fsnotify.Remove) == fsnotify.Remove {
batchClean = true
break
}
if event.Op&(fsnotify.Create|fsnotify.Write) == 0 { if event.Op&(fsnotify.Create|fsnotify.Write) == 0 {
break break
} }
@@ -263,8 +278,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
continue continue
} }
if isCover(item.Name()) { if coverparse.IsCover(item.Name()) {
cover = item.Name() coverparse.BestBetween(&cover, item.Name())
continue continue
} }
if s.tagReader.CanRead(absPath) { if s.tagReader.CanRead(absPath) {
@@ -301,9 +316,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 { 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 { if err != nil {
return fmt.Errorf("stating %q: %w", basename, err) return fmt.Errorf("get times %q: %w", basename, err)
} }
var track db.Track var track db.Track
@@ -311,7 +327,7 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return fmt.Errorf("query track: %w", err) 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{}{} st.seenTracks[track.ID] = struct{}{}
return nil return nil
} }
@@ -350,7 +366,11 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return fmt.Errorf("populate track artists: %w", err) 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) return fmt.Errorf("populate album: %w", err)
} }
@@ -359,6 +379,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 { if err := populateTrack(tx, album, &track, trags, basename, int(stat.Size())); err != nil {
return fmt.Errorf("process %q: %w", basename, err) return fmt.Errorf("process %q: %w", basename, err)
} }
@@ -389,7 +413,7 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return nil 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) albumName := tagcommon.MustAlbum(trags)
album.TagTitle = albumName album.TagTitle = albumName
album.TagTitleUDec = decoded(albumName) album.TagTitleUDec = decoded(albumName)
@@ -398,8 +422,8 @@ func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime tim
album.TagYear = trags.Year() album.TagYear = trags.Year()
album.ModifiedAt = modTime album.ModifiedAt = modTime
if album.CreatedAt.After(modTime) { if album.CreatedAt.After(createTime) {
album.CreatedAt = modTime // reset created at to match filesytem for new albums album.CreatedAt = createTime // reset created at to match filesytem for new albums
} }
if err := tx.Save(&album).Error; err != nil { if err := tx.Save(&album).Error; err != nil {
@@ -439,15 +463,22 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tagcommon.
track.Size = size track.Size = size
track.AlbumID = album.ID track.AlbumID = album.ID
track.TagTitle = trags.Title() tagTitle := tagcommon.MustTitle(trags)
track.TagTitleUDec = decoded(trags.Title()) track.TagTitle = tagTitle
track.TagTitleUDec = decoded(tagTitle)
track.TagTrackArtist = tagcommon.MustArtist(trags) track.TagTrackArtist = tagcommon.MustArtist(trags)
track.TagTrackNumber = trags.TrackNumber() track.TagTrackNumber = trags.TrackNumber()
track.TagDiscNumber = trags.DiscNumber() track.TagDiscNumber = trags.DiscNumber()
track.TagBrainzID = trags.BrainzID() track.TagBrainzID = trags.BrainzID()
track.Length = trags.Length() // these two should be calculated track.ReplayGainTrackGain = trags.ReplayGainTrackGain()
track.Bitrate = trags.Bitrate() // ...from the file instead of tags 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 { if err := tx.Save(&track).Error; err != nil {
return fmt.Errorf("saving track: %w", err) return fmt.Errorf("saving track: %w", err)
@@ -642,26 +673,6 @@ func (s *Scanner) cleanGenres(st *State) error { //nolint:unparam
return nil 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. // 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 // 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. // differs from the original. the fields are used for searching.

View File

@@ -63,9 +63,9 @@
{{ slot }} {{ slot }}
<div class="px-5 text-right whitespace-nowrap"> <div class="px-5 text-right whitespace-nowrap">
<span class="text-gray-500">v{{ .Version }}</span> <span class="text-gray-500">v{{ .Version }}</span>
senan kelly, 2020 senan kelly (heimoshuiyu forked), 2020
<span class="text-gray-500">&#124;</span> <span class="text-gray-500">&#124;</span>
{{ component "ext_link" (props . "To" "https://github.com/sentriz/gonic") }}github{{ end }} {{ component "ext_link" (props . "To" "https://github.com/heimoshuiyu/gonic") }}github{{ end }}
</div> </div>
</div> </div>
</body> </body>

View File

@@ -75,8 +75,10 @@ func (c *Controller) ServeCreateOrUpdatePlaylist(r *http.Request) *spec.Response
playlistPath := playlistIDDecode(playlistID) playlistPath := playlistIDDecode(playlistID)
var playlist playlistp.Playlist var playlist playlistp.Playlist
if pl, _ := c.playlistStore.Read(playlistPath); pl != nil { if playlistPath != "" {
playlist = *pl if pl, err := c.playlistStore.Read(playlistPath); err != nil && pl != nil {
playlist = *pl
}
} }
if playlist.UserID != 0 && playlist.UserID != user.ID { if playlist.UserID != 0 && playlist.UserID != user.ID {

View File

@@ -32,7 +32,7 @@ import (
const ( const (
coverDefaultSize = 600 coverDefaultSize = 600
coverCacheFormat = "png" coverCacheFormat = "jpg"
) )
func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response { func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response {
@@ -64,7 +64,7 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s
return nil 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) http.ServeFile(w, r, cachePath)
return nil return nil
@@ -160,7 +160,7 @@ func coverScaleAndSave(reader io.Reader, cachePath string, size int) error {
// don't upscale images // don't upscale images
width = src.Bounds().Dx() width = src.Bounds().Dx()
} }
if err := imaging.Save(imaging.Resize(src, width, 0, imaging.Lanczos), cachePath); err != nil { if err := imaging.Save(imaging.Resize(src, width, 0, imaging.Lanczos), cachePath, imaging.JPEGQuality(80)); err != nil {
return fmt.Errorf("caching %q: %w", cachePath, err) return fmt.Errorf("caching %q: %w", cachePath, err)
} }
return nil return nil

View File

@@ -102,6 +102,14 @@ func NewTCTrackByFolder(t *db.Track, parent *db.Album) *TrackChild {
for _, a := range t.Artists { for _, a := range t.Artists {
trCh.Artists = append(trCh.Artists, &ArtistRef{ID: a.SID(), Name: a.Name}) 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 return trCh
} }

View File

@@ -112,6 +112,14 @@ func NewTrackByTags(t *db.Track, album *db.Album) *TrackChild {
for _, a := range album.Artists { for _, a := range album.Artists {
ret.AlbumArtists = append(ret.AlbumArtists, &ArtistRef{ID: a.SID(), Name: a.Name}) 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 return ret
} }

View File

@@ -169,6 +169,13 @@ type TranscodeMeta struct {
TranscodedSuffix string `xml:"transcodedSuffix,attr,omitempty" json:"transcodedSuffix,omitempty"` 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/ // https://opensubsonic.netlify.app/docs/responses/child/
type TrackChild struct { type TrackChild struct {
ID *specid.ID `xml:"id,attr,omitempty" json:"id,omitempty"` 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"` UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"`
AverageRating string `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"` AverageRating string `xml:"averageRating,attr,omitempty" json:"averageRating,omitempty"`
ReplayGain *ReplayGain `xml:"replayGain" json:"replayGain"`
TranscodeMeta TranscodeMeta
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

9
tags/ffprobe/errors.go Normal file
View File

@@ -0,0 +1,9 @@
package ffprobe
import "errors"
var (
ErrNoMediaStreams = errors.New("no media streams")
ErrNoMediaDuration = errors.New("no media duration")
ErrFFProbeScroeNotEnough = errors.New("ffprobe score not enough")
)

97
tags/ffprobe/ffprobe.go Normal file
View File

@@ -0,0 +1,97 @@
package ffprobe
import (
"bytes"
"encoding/json"
"os/exec"
"path/filepath"
"strconv"
"strings"
"go.senan.xyz/gonic/tags/tagcommon"
)
type FFProbe struct{}
func (FFProbe) CanRead(absPath string) bool {
switch ext := strings.ToLower(filepath.Ext(absPath)); ext {
case ".webm", ".mp4", ".mkv":
return true
}
return false
}
func (FFProbe) Read(absPath string) (tagcommon.Info, error) {
cmd := exec.Command(
"ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_format",
"-show_streams",
)
stdout := bytes.NewBuffer(make([]byte, 0, 1024))
stderr := bytes.NewBuffer(make([]byte, 0, 1024))
cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.Args = append(cmd.Args, absPath)
if err := cmd.Run(); err != nil {
return nil, err
}
var mi MediaInfo
if err := json.NewDecoder(stdout).Decode(&mi); err != nil {
return nil, err
}
if len(mi.Streams) == 0 {
return nil, ErrNoMediaStreams
}
if mi.Format.ProbeScore < 100 {
return nil, ErrFFProbeScroeNotEnough
}
if mi.Format.Duration == "" {
return nil, ErrNoMediaDuration
}
ret := &info{absPath, mi}
return ret, nil
}
type info struct {
abspath string
mediaInfo MediaInfo
}
func (i *info) Title() string { return "" }
func (i *info) BrainzID() string { return "" }
func (i *info) Artist() string { return tagcommon.FallbackArtist }
func (i *info) Artists() []string { return []string{tagcommon.FallbackArtist} }
func (i *info) Album() string { return "" }
func (i *info) AlbumArtist() string { return "" }
func (i *info) AlbumArtists() []string { return []string{} }
func (i *info) AlbumBrainzID() string { return "" }
func (i *info) Genre() string { return tagcommon.FallbackGenre }
func (i *info) Genres() []string { return []string{tagcommon.FallbackGenre} }
func (i *info) TrackNumber() int { return 0 }
func (i *info) DiscNumber() int { return 0 }
func (i *info) Year() int { return 0 }
func (i *info) ReplayGainTrackGain() float32 { return 0 }
func (i *info) ReplayGainTrackPeak() float32 { return 0 }
func (i *info) ReplayGainAlbumGain() float32 { return 0 }
func (i *info) ReplayGainAlbumPeak() float32 { return 0 }
func (i *info) Length() int {
ret, _ := strconv.ParseFloat(i.mediaInfo.Format.Duration, 64)
return int(ret)
}
func (i *info) Bitrate() int {
ret, _ := strconv.Atoi(i.mediaInfo.Format.BitRate)
return ret / 1024
}
func (i *info) AbsPath() string { return i.abspath }

View File

@@ -0,0 +1,27 @@
package ffprobe
type Stream struct {
Index int `json:"index"`
CodecName string `json:"codec_name"`
CodecLongName string `json:"codec_long_name"`
CodecType string `json:"codec_type"`
SampleRate string `json:"sample_rate"`
Channels int `json:"channels"`
ChannelLayout string `json:"channel_layout"`
}
type Format struct {
Filename string `json:"filename"`
NbStreams int `json:"nb_streams"`
FormatName string `json:"format_name"`
FormatLongName string `json:"format_long_name"`
Duration string `json:"duration"`
Size string `json:"size"`
BitRate string `json:"bit_rate"`
ProbeScore int `json:"probe_score"`
}
type MediaInfo struct {
Streams []Stream `json:"streams"`
Format Format `json:"format"`
}

View File

@@ -2,6 +2,7 @@ package tagcommon
import ( import (
"errors" "errors"
"path"
) )
var ErrUnsupported = errors.New("filetype unsupported") var ErrUnsupported = errors.New("filetype unsupported")
@@ -24,22 +25,40 @@ type Info interface {
Genres() []string Genres() []string
TrackNumber() int TrackNumber() int
DiscNumber() int DiscNumber() int
Year() int
ReplayGainTrackGain() float32
ReplayGainTrackPeak() float32
ReplayGainAlbumGain() float32
ReplayGainAlbumPeak() float32
Length() int Length() int
Bitrate() int Bitrate() int
Year() int
AbsPath() string
} }
const ( const (
FallbackAlbum = "Unknown Album"
FallbackArtist = "Unknown Artist" FallbackArtist = "Unknown Artist"
FallbackGenre = "Unknown Genre" FallbackGenre = "Unknown Genre"
) )
func MustTitle(p Info) string {
if r := p.Title(); r != "" {
return r
}
// return the file name for title name
return path.Base(p.AbsPath())
}
func MustAlbum(p Info) string { func MustAlbum(p Info) string {
if r := p.Album(); r != "" { if r := p.Album(); r != "" {
return r return r
} }
return FallbackAlbum
// return the dir name for album name
return path.Base(path.Dir(p.AbsPath()))
} }
func MustArtist(p Info) string { func MustArtist(p Info) string {

View File

@@ -28,12 +28,13 @@ func (TagLib) Read(absPath string) (tagcommon.Info, error) {
defer f.Close() defer f.Close()
props := f.ReadAudioProperties() props := f.ReadAudioProperties()
raw := f.ReadTags() raw := f.ReadTags()
return &info{raw, props}, nil return &info{raw, props, absPath}, nil
} }
type info struct { type info struct {
raw map[string][]string raw map[string][]string
props *audiotags.AudioProperties props *audiotags.AudioProperties
abspath string
} }
// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html // https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
@@ -51,8 +52,16 @@ 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) 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) 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) 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 (i *info) AbsPath() string { return i.abspath }
func first[T comparable](is []T) T { func first[T comparable](is []T) T {
var z T var z T
@@ -83,6 +92,17 @@ func filterStr(ss []string) []string {
return r 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 { func intSep(sep, in string) int {
start, _, _ := strings.Cut(in, sep) start, _, _ := strings.Cut(in, sep)
out, _ := strconv.Atoi(start) out, _ := strconv.Atoi(start)

View File

@@ -29,6 +29,8 @@ var UserProfiles = map[string]Profile{
"opus_128": Opus128, "opus_128": Opus128,
"opus_128_rg": Opus128RG, "opus_128_rg": Opus128RG,
"opus_192": Opus192, "opus_192": Opus192,
"opus_320": Opus320,
"opus_512": Opus512,
} }
// Store as simple strings, since we may let the user provide their own profiles soon // Store as simple strings, since we may let the user provide their own profiles soon
@@ -46,6 +48,8 @@ var (
Opus128RGLoud = NewProfile("audio/ogg", "opus", 128, `ffmpeg -v 0 -i <file> -ss <seek> -map 0:a:0 -vn -b:a <bitrate> -c:a libopus -vbr on -af "aresample=96000:resampler=soxr, volume=replaygain=track:replaygain_preamp=15dB:replaygain_noclip=0, alimiter=level=disabled, asidedata=mode=delete:type=REPLAYGAIN" -metadata replaygain_album_gain= -metadata replaygain_album_peak= -metadata replaygain_track_gain= -metadata replaygain_track_peak= -metadata r128_album_gain= -metadata r128_track_gain= -f opus -`) Opus128RGLoud = NewProfile("audio/ogg", "opus", 128, `ffmpeg -v 0 -i <file> -ss <seek> -map 0:a:0 -vn -b:a <bitrate> -c:a libopus -vbr on -af "aresample=96000:resampler=soxr, volume=replaygain=track:replaygain_preamp=15dB:replaygain_noclip=0, alimiter=level=disabled, asidedata=mode=delete:type=REPLAYGAIN" -metadata replaygain_album_gain= -metadata replaygain_album_peak= -metadata replaygain_track_gain= -metadata replaygain_track_peak= -metadata r128_album_gain= -metadata r128_track_gain= -f opus -`)
Opus192 = NewProfile("audio/ogg", "opus", 192, `ffmpeg -v 0 -i <file> -ss <seek> -map 0:a:0 -vn -b:a <bitrate> -c:a libopus -vbr on -f opus -`) Opus192 = NewProfile("audio/ogg", "opus", 192, `ffmpeg -v 0 -i <file> -ss <seek> -map 0:a:0 -vn -b:a <bitrate> -c:a libopus -vbr on -f opus -`)
Opus320 = NewProfile("audio/ogg", "opus", 320, `ffmpeg -v 0 -i <file> -ss <seek> -map 0:a:0 -vn -b:a <bitrate> -c:a libopus -vbr on -f opus -`)
Opus512 = NewProfile("audio/ogg", "opus", 512, `ffmpeg -v 0 -i <file> -ss <seek> -map 0:a:0 -vn -b:a <bitrate> -c:a libopus -vbr on -f opus -`)
PCM16le = NewProfile("audio/wav", "wav", 0, `ffmpeg -v 0 -i <file> -ss <seek> -c:a pcm_s16le -ac 2 -ar 48000 -f s16le -`) PCM16le = NewProfile("audio/wav", "wav", 0, `ffmpeg -v 0 -i <file> -ss <seek> -c:a pcm_s16le -ac 2 -ar 48000 -f s16le -`)
) )

View File

@@ -113,7 +113,7 @@ func TestCachingParallelism(t *testing.T) {
callback: func() { realTranscodeCount.Add(1) }, callback: func() { realTranscodeCount.Add(1) },
} }
cacheTranscoder := transcode.NewCachingTranscoder(transcoder, t.TempDir()) cacheTranscoder := transcode.NewCachingTranscoder(transcoder, t.TempDir(), 1024)
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {

View File

@@ -5,9 +5,12 @@ import (
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"sync" "sync"
"time"
) )
const perm = 0o644 const perm = 0o644
@@ -15,16 +18,21 @@ const perm = 0o644
type CachingTranscoder struct { type CachingTranscoder struct {
cachePath string cachePath string
transcoder Transcoder transcoder Transcoder
limitMB int
locks keyedMutex locks keyedMutex
cleanLock sync.RWMutex
} }
var _ Transcoder = (*CachingTranscoder)(nil) var _ Transcoder = (*CachingTranscoder)(nil)
func NewCachingTranscoder(t Transcoder, cachePath string) *CachingTranscoder { func NewCachingTranscoder(t Transcoder, cachePath string, limitMB int) *CachingTranscoder {
return &CachingTranscoder{transcoder: t, cachePath: cachePath} return &CachingTranscoder{transcoder: t, cachePath: cachePath, limitMB: limitMB}
} }
func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error { 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 // don't try cache partial transcodes
if profile.Seek() > 0 { if profile.Seek() > 0 {
return t.transcoder.Transcode(ctx, profile, in, out) 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 { if i, err := cf.Stat(); err == nil && i.Size() > 0 {
_, _ = io.Copy(out, cf) _, _ = io.Copy(out, cf)
_ = os.Chtimes(path, time.Now(), time.Now()) // Touch for LRU cache purposes
return nil return nil
} }
@@ -64,6 +73,55 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s
return nil 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 { func cacheKey(cmd string, args []string) string {
// the cache is invalid whenever transcode command (which includes the // the cache is invalid whenever transcode command (which includes the
// absolute filepath, bit rate args, replay gain args, etc.) changes // absolute filepath, bit rate args, replay gain args, etc.) changes