Compare commits
32 Commits
38ab0b0929
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
612d43be11
|
|||
|
a9b565f948
|
|||
|
|
ac798ac2d2 | ||
|
|
bcb613c79c | ||
|
|
bfa0e130d4 | ||
|
|
640d872f4c | ||
|
|
453639ee34 | ||
|
|
120fd7959a | ||
|
|
875a83ad4f | ||
|
|
fb36dbf719 | ||
|
|
bbe16b7555 | ||
|
|
a1d929e486 | ||
|
211e104535
|
|||
|
15a13a149b
|
|||
|
02be9219b1
|
|||
|
e8e478f2aa
|
|||
|
8abe131f28
|
|||
|
8914c59978
|
|||
|
332f00ff7a
|
|||
|
71ae1029e8
|
|||
|
b70979d2e0
|
|||
|
853107fca6
|
|||
|
cf5e87e62b
|
|||
|
|
0e45f5e84c | ||
|
|
259be0edde | ||
|
|
0d7d92d545 | ||
|
|
14c34c6052 | ||
|
|
86fd590556 | ||
|
|
f5893ea5ea | ||
|
|
559c9106b0 | ||
|
|
6ba342c770 | ||
|
|
93ce039963 |
5
.github/workflows/nightly-release.yaml
vendored
5
.github/workflows/nightly-release.yaml
vendored
@@ -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:
|
||||
|
||||
5
.github/workflows/release.yaml
vendored
5
.github/workflows/release.yaml
vendored
@@ -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:
|
||||
|
||||
5
.github/workflows/test.yaml
vendored
5
.github/workflows/test.yaml
vendored
@@ -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 ./...
|
||||
|
||||
@@ -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
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -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 \
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
12
db/db.go
12
db/db.go
@@ -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 }
|
||||
|
||||
@@ -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
39
go.mod
@@ -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
97
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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">|</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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
9
tags/ffprobe/errors.go
Normal 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
97
tags/ffprobe/ffprobe.go
Normal 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 }
|
||||
27
tags/ffprobe/ffprobe_output_struct.go
Normal file
27
tags/ffprobe/ffprobe_output_struct.go
Normal 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"`
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 -`)
|
||||
)
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user