Compare commits

...

20 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
28 changed files with 340 additions and 250 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

@@ -81,6 +81,9 @@ issues:
- text: "weak random number generator"
linters:
- gosec
- text: "integer overflow conversion"
linters:
- gosec
- text: "at least one file in a package should have a package comment"
linters:
- stylecheck

View File

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

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

@@ -71,6 +71,8 @@ password can then be changed from the web interface
| `GONIC_MULTI_VALUE_GENRE` | `-multi-value-genre` | **optional** setting for multi-valued genre tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_MULTI_VALUE_ARTIST` | `-multi-value-artist` | **optional** setting for multi-valued artist tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_MULTI_VALUE_ALBUM_ARTIST` | `-multi-value-album-artist` | **optional** setting for multi-valued album artist tags when scanning ([see more](#multi-valued-tags-v016)) |
| `GONIC_TRANSCODE_CACHE_SIZE` | `-transcode-cache-size` | **optional** size of the transcode cache in MB (0 = no limit) |
| `GONIC_TRANSCODE_EJECT_INTERVAL` | `-transcode-eject-interval` | **optional** interval (in minutes) to eject transcode cache (0 = never) |
| `GONIC_EXPVAR` | `-expvar` | **optional** enable the /debug/vars endpoint (exposes useful debugging attributes as well as database stats) |
## multi valued tags (v0.16+)

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) {
@@ -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

29
go.mod
View File

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

58
go.sum
View File

@@ -6,8 +6,8 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
@@ -57,10 +57,10 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/jinzhu/gorm v1.9.2/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
@@ -92,12 +92,12 @@ github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
@@ -128,8 +128,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sentriz/audiotags v0.0.0-20240401192409-5a8ac2a2974f h1:Yio3vmGw+yf+gzjYLf1plSGEf/1IUTVY45n+qcGJEmk=
github.com/sentriz/audiotags v0.0.0-20240401192409-5a8ac2a2974f/go.mod h1:+pmkMFDEXJuu/u4h2OYJVfYF2qIhXJD7kqvWq6q5Zo0=
github.com/sentriz/audiotags v0.0.0-20240713161505-a6bb82b19f54 h1:JaJaWCUDLGUbU8AIO+YhQ+Nq4ByCCaApLuHi868uWQw=
github.com/sentriz/audiotags v0.0.0-20240713161505-a6bb82b19f54/go.mod h1:+pmkMFDEXJuu/u4h2OYJVfYF2qIhXJD7kqvWq6q5Zo0=
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981 h1:sLILANWN76ja66/K4k/mBqJuCjDZaM67w+Ru6rEB0s0=
github.com/sentriz/gormstore v0.0.0-20220105134332-64e31f7f6981/go.mod h1:Rx8XB1ck+so+41uu9VY1gMKs1CPQ2NTq0pzf+OCCQHo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
@@ -139,18 +139,20 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.senan.xyz/flagconf v0.1.8 h1:0HadvAEXHYJOGGdO6cHz2Ok4vWawaM64m5ldSjLoVUw=
go.senan.xyz/flagconf v0.1.8/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
go.senan.xyz/flagconf v0.1.9 h1:LBDmqiVFgijfqFXDzH97gPn0qDbg1Dq6/vxsxS/TzC4=
go.senan.xyz/flagconf v0.1.9/go.mod h1:NqOFfSwJvNWXOTUabcRZ8mPK9+sJmhStJhqtEt74wNQ=
go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230 h1:Peeh4dn9T9YD3wFR4ocef74/2GRCRCqRz/Mx8sO9VPw=
go.senan.xyz/wrtag v0.0.0-20240913105114-298b03ad8230/go.mod h1:bnHbnDhLgt0ckjAzT/YNJmzHFXf0hHI4BVVS4w4V7S8=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -163,13 +165,13 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -180,8 +182,8 @@ golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -191,14 +193,14 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

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)
})
}

View File

@@ -371,6 +371,8 @@ 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)
func firstInt(or int, ints ...int) int {

View File

@@ -94,6 +94,10 @@ func (s *Store) Read(relPath string) (*Playlist, error) {
return nil, fmt.Errorf("stat m3u: %w", err)
}
if stat.IsDir() {
return nil, errors.New("path is a directory")
}
var playlist Playlist
playlist.UpdatedAt = stat.ModTime()

View File

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

View File

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

View File

@@ -24,8 +24,8 @@ import (
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/fileutil"
"go.senan.xyz/gonic/scanner/coverresolve"
"go.senan.xyz/gonic/tags/tagcommon"
"go.senan.xyz/wrtag/coverparse"
)
var (
@@ -267,7 +267,7 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
}
var tracks []string
var covers []string
var cover string
for _, item := range items {
absPath := filepath.Join(absPath, item.Name())
if s.excludePattern != nil && s.excludePattern.MatchString(absPath) {
@@ -278,8 +278,8 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
continue
}
if coverresolve.IsCover(item.Name()) {
covers = append(covers, item.Name())
if coverparse.IsCover(item.Name()) {
coverparse.BestBetween(&cover, item.Name())
continue
}
if s.tagReader.CanRead(absPath) {
@@ -288,8 +288,6 @@ func (s *Scanner) scanDir(tx *db.DB, st *State, absPath string) error {
}
}
cover := coverresolve.SelectCover(covers)
pdir, pbasename := filepath.Split(filepath.Dir(relPath))
var parent db.Album
if err := tx.Where("root_dir=? AND left_path=? AND right_path=?", musicDir, pdir, pbasename).Assign(db.Album{RootDir: musicDir, LeftPath: pdir, RightPath: pbasename}).FirstOrCreate(&parent).Error; err != nil {
@@ -465,8 +463,9 @@ 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()

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

@@ -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

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")
@@ -33,19 +34,31 @@ type Info interface {
Length() int
Bitrate() 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
@@ -60,6 +61,8 @@ func (i *info) ReplayGainAlbumPeak() float32 { return flt(first(find(i.raw, "rep
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
for _, i := range is {

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