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 install -y -qq build-essential git sqlite libtag1-dev ffmpeg mpv zlib1g-dev
- name: Lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
version: v1.54
version: v1.60
args: --timeout=5m
install-mode: "goinstall"
- name: Test
run: go test ./...
build-release:

View File

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

View File

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

View File

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

View File

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

View File

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

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_MPV_EXTRA_ARGS` | `-jukebox-mpv-extra-args` | **optional** extra command line arguments to pass to the jukebox mpv daemon |
| `GONIC_PODCAST_PURGE_AGE` | `-podcast-purge-age` | **optional** age (in days) to purge podcast episodes if not accessed |
| `GONIC_EXCLUDE_PATTERN` | `-exclude-pattern` | **optional** files matching this regex pattern will not be imported |
| `GONIC_EXCLUDE_PATTERN` | `-exclude-pattern` | **optional** files matching this regex pattern will not be imported. eg <code>@eaDir\|[aA]rtwork\|[cC]overs\|[sS]cans\|[sS]pectrals</code> |
| `GONIC_MULTI_VALUE_GENRE` | `-multi-value-genre` | **optional** setting for multi-valued genre tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_MULTI_VALUE_ARTIST` | `-multi-value-artist` | **optional** setting for multi-valued artist tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_MULTI_VALUE_ALBUM_ARTIST` | `-multi-value-album-artist` | **optional** setting for multi-valued album artist tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_TRANSCODE_CACHE_SIZE` | `-transcode-cache-size` | **optional** size of the transcode cache in MB (0 = no limit) |
| `GONIC_TRANSCODE_EJECT_INTERVAL` | `-transcode-eject-interval` | **optional** interval (in minutes) to eject transcode cache (0 = never) |
| `GONIC_EXPVAR` | `-expvar` | **optional** enable the /debug/vars endpoint (exposes useful debugging attributes as well as database stats) |
## multi valued tags (v0.16+)
@@ -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.
on top of that - if you don't decide your folder names, or simply do not want the same name in your subsonic client,
on top of that - if you don't decide your folder names, or simply do not want the same name in your subsonic client,
gonic can parse aliases for the folder names with the optional `ALIAS->PATH` syntax
if you're running gonic with the command line, stack the `-music-path` arg

View File

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

View File

@@ -45,6 +45,7 @@ import (
"go.senan.xyz/gonic/scrobble"
"go.senan.xyz/gonic/server/ctrladmin"
"go.senan.xyz/gonic/server/ctrlsubsonic"
"go.senan.xyz/gonic/tags/ffprobe"
"go.senan.xyz/gonic/tags/tagcommon"
"go.senan.xyz/gonic/tags/taglib"
"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)")
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")
@@ -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")
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)")
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)")
confTranscodeCacheSize := flag.Int("transcode-cache-size", 0, "size of the transcode cache in MB (0 = no limit) (optional)")
confTranscodeEjectInterval := flag.Int("transcode-eject-interval", 0, "interval (in minutes) to eject transcode cache (0 = never) (optional)")
flag.Parse()
flagconf.ParseEnv()
flagconf.ParseConfig(*confConfigPath)
@@ -140,6 +146,7 @@ func main() {
if err != nil {
log.Fatalf("error opening database: %v\n", err)
}
dbc.LogMode(*confDBLog)
defer dbc.Close()
err = dbc.Migrate(db.MigrationContext{
@@ -182,7 +189,7 @@ func main() {
tagReader := tagcommon.ChainReader{
taglib.TagLib{},
// ffprobe reader?
ffprobe.FFProbe{},
// nfo reader?
}
@@ -201,6 +208,7 @@ func main() {
transcoder := transcode.NewCachingTranscoder(
transcode.NewFFmpegTranscoder(),
cacheDirAudio,
*confTranscodeCacheSize,
)
lastfmClientKeySecretFunc := func() (string, string, error) {
@@ -273,7 +281,7 @@ func main() {
mux.Handle("/admin/", http.StripPrefix("/admin", chain(ctrlAdmin)))
mux.Handle("/rest/", http.StripPrefix("/rest", chain(trim(ctrlSubsonic))))
mux.Handle("/ping", chain(handlerutil.Message("ok")))
mux.Handle("/", chain(handlerutil.Redirect(resolveProxyPath("/admin/home"))))
mux.Handle("/", chain(http.RedirectHandler(resolveProxyPath("/admin/home"), http.StatusSeeOther)))
if *confExpvar {
mux.Handle("/debug/vars", expvar.Handler())
@@ -379,6 +387,10 @@ func main() {
})
errgrp.Go(func() error {
if !*confPodcastDownload {
return nil
}
defer logJob("podcast download")()
ctxTick(ctx, 5*time.Second, func() {
@@ -404,6 +416,21 @@ func main() {
return nil
})
errgrp.Go(func() error {
if *confTranscodeEjectInterval == 0 || *confTranscodeCacheSize == 0 {
return nil
}
defer logJob("transcode cache eject")()
ctxTick(ctx, time.Duration(*confTranscodeEjectInterval)*time.Minute, func() {
if err := transcoder.CacheEject(); err != nil {
log.Printf("error ejecting transcode cache: %v", err)
}
})
return nil
})
errgrp.Go(func() error {
if *confScanIntervalMins == 0 {
return nil

View File

@@ -49,6 +49,10 @@ playlists-path <path to your m3u playlist dir>
# regenerated.
cache-path /var/cache/gonic
# Option to eject least recently used items from transcode cache.
#transcode-cache-size 5000 # in Mb (0 = no limit)
#transcode-eject-interval 1440 # in minutes (0 = never eject)
# Interval (in minutes) to check for new music. Default: don't scan
#scan-interval 0
#scan-at-start-enabled false

View File

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

View File

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

39
go.mod
View File

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

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/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.9.0 h1:zgjKkdpRY9T97Q5DCtcXwfqkcylSFIVCocZmn2huTp8=
github.com/PuerkitoBio/goquery v1.9.0/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
@@ -23,6 +21,8 @@ github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37 h1:s+qNFsO3VsdsKro
github.com/dexterlb/mpvipc v0.0.0-20230829142118-145d6eabdc37/go.mod h1:CXCwawNJCtFDip7gvbaQVgw0cGjldpyHDIp7oA5prOg=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
@@ -31,10 +31,10 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -57,10 +57,10 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
@@ -75,8 +75,8 @@ github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josephburnett/jd v1.7.1 h1:oXBPMS+SNnILTMGj1fWLK9pexpeJUXtbVFfRku/PjBU=
github.com/josephburnett/jd v1.7.1/go.mod h1:R8ZnZnLt2D4rhW4NvBc/USTo6mzyNT6fYNIIWOJA9GY=
github.com/josephburnett/jd v1.8.1 h1:U4wae4kEvduCmf5mlXJ3uKnfHFmGhwttEFkQ6rsoDMk=
github.com/josephburnett/jd v1.8.1/go.mod h1:d9nEP87VBIx8SxhIVraVdEU/IwZ7JH6kHWjZMByRq2M=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -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/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mmcdole/gofeed v1.2.1 h1:tPbFN+mfOLcM1kDF1x2c/N68ChbdBatkppdzf/vDe1s=
github.com/mmcdole/gofeed v1.2.1/go.mod h1:2wVInNpgmC85q16QTTuwbuKxtKkHLCDDtf0dCmnrNr4=
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
github.com/mmcdole/goxpp v1.1.0 h1:WwslZNF7KNAXTFuzRtn/OKZxFLJAAyOA9w82mDz2ZGI=
github.com/mmcdole/goxpp v1.1.0/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -132,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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sentriz/audiotags v0.0.0-20240202193907-618ae39d7743 h1:aecPwcrY8mYmZmd9XgQcG8aILRuhRxeQMSunnr6DQ3U=
github.com/sentriz/audiotags v0.0.0-20240202193907-618ae39d7743/go.mod h1:Zoo4UP5t2ySbPwScJfoydAlLLBonoqntv4ovA1T91Z8=
github.com/sentriz/audiotags v0.0.0-20240305214804-7a32981c18f8 h1:WBzwq2r567WlnfYravpwUdsAzaXedbWLypXyArLGgI4=
github.com/sentriz/audiotags v0.0.0-20240305214804-7a32981c18f8/go.mod h1:+pmkMFDEXJuu/u4h2OYJVfYF2qIhXJD7kqvWq6q5Zo0=
github.com/sentriz/audiotags v0.0.0-20240713161505-a6bb82b19f54 h1:JaJaWCUDLGUbU8AIO+YhQ+Nq4ByCCaApLuHi868uWQw=
github.com/sentriz/audiotags v0.0.0-20240713161505-a6bb82b19f54/go.mod h1:+pmkMFDEXJuu/u4h2OYJVfYF2qIhXJD7kqvWq6q5Zo0=
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 h1:sLILANWN76ja66/K4k/mBqJuCjDZaM67w+Ru6rEB0s0=
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981/go.mod h1:Rx8XB1ck+so+41uu9VY1gMKs1CPQ2NTq0pzf+OCCQHo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.senan.xyz/flagconf v0.1.5 h1:5HTNpA5jzH1XnsyR79pClXf9T+V+6OL/IsESORMrExs=
go.senan.xyz/flagconf v0.1.5/go.mod h1:CGD/sgYWiTacz1ojgsQRwErqLxtShWMpBxxnsJI6yaE=
go.senan.xyz/flagconf v0.1.7 h1:+o9Cg3WyzCG+KSfZAwOP61dTWSzGhfH3W+zz9mbNJOA=
go.senan.xyz/flagconf v0.1.7/go.mod h1:CGD/sgYWiTacz1ojgsQRwErqLxtShWMpBxxnsJI6yaE=
go.senan.xyz/flagconf v0.1.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4=
go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230 h1:Peeh4dn9T9YD3wFR4ocef74/2GRCRCqRz/Mx8sO9VPw=
go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230/go.mod h1:bnHbnDhLgt0ckjAzT/YNJmzHFXf0hHI4BVVS4w4V7S8=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"strings"
"time"
)
type Middleware func(http.Handler) http.Handler
@@ -29,9 +30,15 @@ func TrimPathSuffix(suffix string) Middleware {
func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
begin := time.Now()
sw := &statusWriter{ResponseWriter: w}
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 {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, message)

View File

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

View File

@@ -362,8 +362,16 @@ func (i *TagInfo) Genres() []string { return []string{i.RawGenre} }
func (i *TagInfo) TrackNumber() int { return 1 }
func (i *TagInfo) DiscNumber() int { return 1 }
func (i *TagInfo) Year() int { return 2021 }
func (i *TagInfo) Length() int { return firstInt(100, i.RawLength) }
func (i *TagInfo) Bitrate() int { return firstInt(100, i.RawBitrate) }
func (i *TagInfo) ReplayGainTrackGain() float32 { return 0 }
func (i *TagInfo) ReplayGainTrackPeak() float32 { return 0 }
func (i *TagInfo) ReplayGainAlbumGain() float32 { return 0 }
func (i *TagInfo) ReplayGainAlbumPeak() float32 { return 0 }
func (i *TagInfo) Length() int { return firstInt(100, i.RawLength) }
func (i *TagInfo) Bitrate() int { return firstInt(100, i.RawBitrate) }
func (i *TagInfo) AbsPath() string { return "" }
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)
}
if stat.IsDir() {
return nil, errors.New("path is a directory")
}
var playlist Playlist
playlist.UpdatedAt = stat.ModTime()
playlist.UserID, err = userIDFromPath(relPath)
if err != nil {
return nil, fmt.Errorf("convert id to str: %w", err)
playlist.UserID = 1
}
playlist.Name = strings.TrimSuffix(filepath.Base(relPath), filepath.Ext(relPath))

View File

@@ -17,6 +17,7 @@ import (
"sync/atomic"
"time"
"github.com/djherbis/times"
"github.com/fsnotify/fsnotify"
"github.com/jinzhu/gorm"
"github.com/rainycape/unidecode"
@@ -24,6 +25,7 @@ import (
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/fileutil"
"go.senan.xyz/gonic/tags/tagcommon"
"go.senan.xyz/wrtag/coverparse"
)
var (
@@ -134,9 +136,18 @@ func (s *Scanner) ExecuteWatch(ctx context.Context) error {
}
batchSeen := map[string]struct{}{}
batchClean := false
for {
select {
case <-batchT.C:
if batchClean {
if _, err := s.ScanAndClean(ScanOptions{}); err != nil {
log.Printf("error scanning: %v", err)
}
clear(batchSeen)
batchClean = false
break
}
if !s.StartScanning() {
break
}
@@ -164,6 +175,10 @@ func (s *Scanner) ExecuteWatch(ctx context.Context) error {
clear(batchSeen)
case event := <-watcher.Events:
if event.Op&(fsnotify.Remove) == fsnotify.Remove {
batchClean = true
break
}
if event.Op&(fsnotify.Create|fsnotify.Write) == 0 {
break
}
@@ -263,8 +278,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
continue
}
if isCover(item.Name()) {
cover = item.Name()
if coverparse.IsCover(item.Name()) {
coverparse.BestBetween(&cover, item.Name())
continue
}
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 {
stat, err := os.Stat(absPath)
// useful to get the real create/birth time for filesystems and kernels which support it
timeSpec, err := times.Stat(absPath)
if err != nil {
return fmt.Errorf("stating %q: %w", basename, err)
return fmt.Errorf("get times %q: %w", basename, err)
}
var track db.Track
@@ -311,7 +327,7 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return fmt.Errorf("query track: %w", err)
}
if !st.isFull && track.ID != 0 && stat.ModTime().Before(track.UpdatedAt) {
if !st.isFull && track.ID != 0 && timeSpec.ModTime().Before(track.UpdatedAt) {
st.seenTracks[track.ID] = struct{}{}
return nil
}
@@ -350,7 +366,11 @@ func (s *Scanner) populateTrackAndArtists(tx *db.DB, st *State, i int, album *db
return fmt.Errorf("populate track artists: %w", err)
}
if err := populateAlbum(tx, album, trags, stat.ModTime()); err != nil {
modTime, createTime := timeSpec.ModTime(), timeSpec.ModTime()
if timeSpec.HasBirthTime() {
createTime = timeSpec.BirthTime()
}
if err := populateAlbum(tx, album, trags, modTime, createTime); err != nil {
return fmt.Errorf("populate album: %w", err)
}
@@ -359,6 +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 {
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
}
func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime time.Time) error {
func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime, createTime time.Time) error {
albumName := tagcommon.MustAlbum(trags)
album.TagTitle = albumName
album.TagTitleUDec = decoded(albumName)
@@ -398,8 +422,8 @@ func populateAlbum(tx *db.DB, album *db.Album, trags tagcommon.Info, modTime tim
album.TagYear = trags.Year()
album.ModifiedAt = modTime
if album.CreatedAt.After(modTime) {
album.CreatedAt = modTime // reset created at to match filesytem for new albums
if album.CreatedAt.After(createTime) {
album.CreatedAt = createTime // reset created at to match filesytem for new albums
}
if err := tx.Save(&album).Error; err != nil {
@@ -439,15 +463,22 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tagcommon.
track.Size = size
track.AlbumID = album.ID
track.TagTitle = trags.Title()
track.TagTitleUDec = decoded(trags.Title())
tagTitle := tagcommon.MustTitle(trags)
track.TagTitle = tagTitle
track.TagTitleUDec = decoded(tagTitle)
track.TagTrackArtist = tagcommon.MustArtist(trags)
track.TagTrackNumber = trags.TrackNumber()
track.TagDiscNumber = trags.DiscNumber()
track.TagBrainzID = trags.BrainzID()
track.Length = trags.Length() // these two should be calculated
track.Bitrate = trags.Bitrate() // ...from the file instead of tags
track.ReplayGainTrackGain = trags.ReplayGainTrackGain()
track.ReplayGainTrackPeak = trags.ReplayGainTrackPeak()
track.ReplayGainAlbumGain = trags.ReplayGainAlbumGain()
track.ReplayGainAlbumPeak = trags.ReplayGainAlbumPeak()
// these two are calculated from the file instead of tags
track.Length = trags.Length()
track.Bitrate = trags.Bitrate()
if err := tx.Save(&track).Error; err != nil {
return fmt.Errorf("saving track: %w", err)
@@ -642,26 +673,6 @@ func (s *Scanner) cleanGenres(st *State) error { //nolint:unparam
return nil
}
//nolint:gochecknoglobals
var coverNames = map[string]struct{}{}
//nolint:gochecknoinits
func init() {
for _, name := range []string{"cover", "folder", "front", "albumart", "album", "artist"} {
for _, ext := range []string{"jpg", "jpeg", "png", "bmp", "gif"} {
coverNames[fmt.Sprintf("%s.%s", name, ext)] = struct{}{}
for i := 0; i < 3; i++ {
coverNames[fmt.Sprintf("%s.%d.%s", name, i, ext)] = struct{}{} // support beets extras
}
}
}
}
func isCover(name string) bool {
_, ok := coverNames[strings.ToLower(name)]
return ok
}
// decoded converts a string to it's latin equivalent.
// it will be used by the model's *UDec fields, and is only set if it
// differs from the original. the fields are used for searching.

View File

@@ -63,9 +63,9 @@
{{ slot }}
<div class="px-5 text-right whitespace-nowrap">
<span class="text-gray-500">v{{ .Version }}</span>
senan kelly, 2020
senan kelly (heimoshuiyu forked), 2020
<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>
</body>

View File

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

View File

@@ -32,7 +32,7 @@ import (
const (
coverDefaultSize = 600
coverCacheFormat = "png"
coverCacheFormat = "jpg"
)
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
}
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Header().Set("Cache-Control", "public, max-age=1209600")
http.ServeFile(w, r, cachePath)
return nil
@@ -160,7 +160,7 @@ func coverScaleAndSave(reader io.Reader, cachePath string, size int) error {
// don't upscale images
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 nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 (
"errors"
"path"
)
var ErrUnsupported = errors.New("filetype unsupported")
@@ -24,22 +25,40 @@ type Info interface {
Genres() []string
TrackNumber() int
DiscNumber() int
Year() int
ReplayGainTrackGain() float32
ReplayGainTrackPeak() float32
ReplayGainAlbumGain() float32
ReplayGainAlbumPeak() float32
Length() int
Bitrate() int
Year() int
AbsPath() string
}
const (
FallbackAlbum = "Unknown Album"
FallbackArtist = "Unknown Artist"
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 {
if r := p.Album(); r != "" {
return r
}
return FallbackAlbum
// return the dir name for album name
return path.Base(path.Dir(p.AbsPath()))
}
func MustArtist(p Info) string {

View File

@@ -28,12 +28,13 @@ func (TagLib) Read(absPath string) (tagcommon.Info, error) {
defer f.Close()
props := f.ReadAudioProperties()
raw := f.ReadTags()
return &info{raw, props}, nil
return &info{raw, props, absPath}, nil
}
type info struct {
raw map[string][]string
props *audiotags.AudioProperties
raw map[string][]string
props *audiotags.AudioProperties
abspath string
}
// 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) DiscNumber() int { return intSep("/", first(find(i.raw, "discnumber"))) } // eg. 1/2
func (i *info) Year() int { return intSep("-", first(find(i.raw, "originaldate", "date", "year"))) } // eg. 2023-12-01
func (i *info) Length() int { return i.props.Length }
func (i *info) Bitrate() int { return i.props.Bitrate }
func (i *info) ReplayGainTrackGain() float32 { return dB(first(find(i.raw, "replaygain_track_gain"))) }
func (i *info) ReplayGainTrackPeak() float32 { return flt(first(find(i.raw, "replaygain_track_peak"))) }
func (i *info) ReplayGainAlbumGain() float32 { return dB(first(find(i.raw, "replaygain_album_gain"))) }
func (i *info) ReplayGainAlbumPeak() float32 { return flt(first(find(i.raw, "replaygain_album_peak"))) }
func (i *info) Length() int { return i.props.Length }
func (i *info) Bitrate() int { return i.props.Bitrate }
func (i *info) AbsPath() string { return i.abspath }
func first[T comparable](is []T) T {
var z T
@@ -83,6 +92,17 @@ func filterStr(ss []string) []string {
return r
}
func flt(in string) float32 {
f, _ := strconv.ParseFloat(in, 32)
return float32(f)
}
func dB(in string) float32 {
in = strings.ToLower(in)
in = strings.TrimSuffix(in, " db")
in = strings.TrimSuffix(in, "db")
return flt(in)
}
func intSep(sep, in string) int {
start, _, _ := strings.Cut(in, sep)
out, _ := strconv.Atoi(start)

View File

@@ -29,6 +29,8 @@ var UserProfiles = map[string]Profile{
"opus_128": Opus128,
"opus_128_rg": Opus128RG,
"opus_192": Opus192,
"opus_320": Opus320,
"opus_512": Opus512,
}
// 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 -`)
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 -`)
)

View File

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

View File

@@ -5,9 +5,12 @@ import (
"crypto/md5"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"sync"
"time"
)
const perm = 0o644
@@ -15,16 +18,21 @@ const perm = 0o644
type CachingTranscoder struct {
cachePath string
transcoder Transcoder
limitMB int
locks keyedMutex
cleanLock sync.RWMutex
}
var _ Transcoder = (*CachingTranscoder)(nil)
func NewCachingTranscoder(t Transcoder, cachePath string) *CachingTranscoder {
return &CachingTranscoder{transcoder: t, cachePath: cachePath}
func NewCachingTranscoder(t Transcoder, cachePath string, limitMB int) *CachingTranscoder {
return &CachingTranscoder{transcoder: t, cachePath: cachePath, limitMB: limitMB}
}
func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error {
t.cleanLock.RLock()
defer t.cleanLock.RUnlock()
// don't try cache partial transcodes
if profile.Seek() > 0 {
return t.transcoder.Transcode(ctx, profile, in, out)
@@ -52,6 +60,7 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s
if i, err := cf.Stat(); err == nil && i.Size() > 0 {
_, _ = io.Copy(out, cf)
_ = os.Chtimes(path, time.Now(), time.Now()) // Touch for LRU cache purposes
return nil
}
@@ -64,6 +73,55 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s
return nil
}
func (t *CachingTranscoder) CacheEject() error {
t.cleanLock.Lock()
defer t.cleanLock.Unlock()
// Delete LRU cache files that exceed size limit. Use last modified time.
type file struct {
path string
info os.FileInfo
}
var files []file
var total int64 = 0
err := filepath.WalkDir(t.cachePath, func(path string, de fs.DirEntry, err error) error {
if err != nil {
return err
}
if !de.IsDir() {
info, err := de.Info()
if err != nil {
return fmt.Errorf("walk cache path for eject: %w", err)
}
files = append(files, file{path, info})
total += info.Size()
}
return nil
})
if err != nil {
return fmt.Errorf("walk cache path for eject: %w", err)
}
sort.Slice(files, func(i, j int) bool {
return files[i].info.ModTime().Before(files[j].info.ModTime())
})
for total > int64(t.limitMB)*1024*1024 {
curFile := files[0]
files = files[1:]
total -= curFile.info.Size()
err = os.Remove(curFile.path)
if err != nil {
return fmt.Errorf("remove cache file: %w", err)
}
}
return nil
}
func cacheKey(cmd string, args []string) string {
// the cache is invalid whenever transcode command (which includes the
// absolute filepath, bit rate args, replay gain args, etc.) changes