Direct Streams working

- Removed funscripts, they are for interactive
- updated the scanner to correctly create `audio_files` row
- Adding Audio to `paths`
- Updated sqlite to add AudioFile

Need to update mutations next
This commit is contained in:
Bob 2026-04-26 20:21:51 -07:00
parent 23c413438f
commit 169bebeaf5
24 changed files with 378 additions and 440 deletions

View file

@ -1,87 +0,0 @@
# options for analysis running
run:
timeout: 5m
linters:
disable-all: true
enable:
# Default set of linters from golangci-lint
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
# Linters added by the stash project.
# - contextcheck
- copyloopvar
- dogsled
- errchkjson
- errorlint
# - exhaustive
- gocritic
# - goerr113
- gofmt
# - gomnd
# - ifshort
- misspell
# - nakedret
- noctx
- revive
- rowserrcheck
- sqlclosecheck
# Project-specific linter overrides
linters-settings:
gofmt:
simplify: false
errorlint:
# Disable errorf because there are false positives, where you don't want to wrap
# an error.
errorf: false
asserts: true
comparison: true
revive:
ignore-generated-header: true
severity: error
confidence: 0.8
rules:
- name: blank-imports
disabled: true
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
disabled: true
- name: increment-decrement
- name: var-naming
disabled: true
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
disabled: true
- name: indent-error-flow
disabled: true
- name: errorf
- name: empty-block
disabled: true
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
rowserrcheck:
packages:
- github.com/jmoiron/sqlx

View file

@ -1,86 +1,87 @@
version: "2"
# options for analysis running
run:
timeout: 5m
linters:
default: none
disable-all: true
enable:
- copyloopvar
- dogsled
# Default set of linters from golangci-lint
- errcheck
- errchkjson
- errorlint
- gocritic
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unused
# Linters added by the stash project.
# - contextcheck
- copyloopvar
- dogsled
- errchkjson
- errorlint
# - exhaustive
- gocritic
# - goerr113
- gofmt
# - gomnd
# - ifshort
- misspell
# - nakedret
- noctx
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- unused
settings:
errorlint:
errorf: false
asserts: true
comparison: true
revive:
confidence: 0.8
severity: error
rules:
- name: blank-imports
disabled: true
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
disabled: true
- name: increment-decrement
- name: var-naming
disabled: true
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
disabled: true
- name: indent-error-flow
disabled: true
- name: errorf
- name: empty-block
disabled: true
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
settings:
gofmt:
simplify: false
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
# Project-specific linter overrides
linters-settings:
gofmt:
simplify: false
errorlint:
# Disable errorf because there are false positives, where you don't want to wrap
# an error.
errorf: false
asserts: true
comparison: true
revive:
ignore-generated-header: true
severity: error
confidence: 0.8
rules:
- name: blank-imports
disabled: true
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
disabled: true
- name: if-return
disabled: true
- name: increment-decrement
- name: var-naming
disabled: true
- name: var-declaration
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
disabled: true
- name: indent-error-flow
disabled: true
- name: errorf
- name: empty-block
disabled: true
- name: superfluous-else
- name: unused-parameter
disabled: true
- name: unreachable-code
- name: redefines-builtin-id
rowserrcheck:
packages:
- github.com/jmoiron/sqlx

View file

@ -5,6 +5,7 @@
* [Go](https://golang.org/dl/)
* [GolangCI](https://golangci-lint.run/) - A meta-linter which runs several linters in parallel
* To install, follow the [local installation instructions](https://golangci-lint.run/welcome/install/#local-installation)
* Install v1, NOT v2
* [nodejs](https://nodejs.org/en/download) - nodejs runtime
* corepack/[pnpm](https://pnpm.io/installation) - nodejs package manager (included with nodejs)

View file

@ -23,6 +23,7 @@ The `Audio` datatype is similar to `Scene` but stores audio-only media (i.e. Aud
- O History
- Play History
- Groups
- Captions
- Audio File metadata:
- duration
- audio codec
@ -60,4 +61,93 @@ The `Audio` datatype is similar to `Scene` but stores audio-only media (i.e. Aud
- Audio's could have interactive components, but removed to reduce PR complexity
## Last Steps
- [ ] Delete this file upon completion of the feature
- [ ] Delete this file upon completion of the feature
## Manual Tests
### Setup
1. Copy `.mp3` files into `.local-data`
2. `make server-clean`
3. `make server-start` OR run go debugger (VSCode F5)
4. Create new instance with library at `./.local-data/`
5. go to <http://127.0.0.1:9999/playground>
- Perform manual tests here
### Check Query
This is a manual test with all fields. The test ensures that the Querying is setup correctly.
Later you can reuse this to ensure that mutations correctly updated the database.
```graphql
query {
findAudios(filter:{sort:"title" direction:DESC}){
count
audios {
id title code details urls date rating100 organized o_counter created_at updated_at last_played_at resume_time play_duration play_count play_history o_history custom_fields
files{
id path basename mod_time size format duration audio_codec sample_rate bit_rate created_at updated_at
parent_folder{id}
zip_file{id}
fingerprints{type value}
}
captions{language_code caption_type}
paths{caption stream}
studio{id}
groups{group{id} audio_index}
tags{id}
performers{id}
audioStreams{url mime_type label}
}
}
# findScenes(filter:{sort:"title" direction:DESC}){
# count
# scenes {
# id sceneStreams{url mime_type label}
# files{id path fingerprints{type value}}
# }
# }
}
```
### Check Mutations
TODO
### Check Streams
Currently only direct streams are implemented. Use the following to get the Stream URL.
1. Execute this GraphQL
2. Paste the `Direct stream` url into the browser, ensure that the audio plays
```graphql
query {
findAudios(filter:{sort:"title" direction:DESC}){
count
audios {id audioStreams{url mime_type label}
}
}
}
```
### HTML Confirmation
```html
<audio controls>
<source src="http://127.0.0.1:9999/audio/1/stream" type="audio/mp3">
Your browser does not support the audio element.
</audio>
```
You can also listen to audio using VIDEO tag
```html
<video controls>
<source src="http://127.0.0.1:9999/audio/1/stream" type="audio/mp3">
Your browser does not support the video element.
</video>
```

View file

@ -1,21 +1,5 @@
# TODO(audio): update this file
# type AudioFileType {
# size: String
# duration: Float
# audio_codec: String
# sample_rate: Int
# bitrate: Int
# }
type AudioPathsType {
screenshot: String # Resolver
preview: String # Resolver
stream: String # Resolver
webp: String # Resolver
vtt: String # Resolver
sprite: String # Resolver
funscript: String # Resolver
caption: String # Resolver
}

View file

@ -108,6 +108,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchScenes(ctx),
},
AudioByID: &AudioLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchAudios(ctx),
},
GalleryByID: &GalleryLoader{
wait: wait,
maxBatch: maxBatch,
@ -148,6 +153,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchSceneCustomFields(ctx),
},
AudioCustomFields: &CustomFieldsLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchAudioCustomFields(ctx),
},
StudioByID: &StudioLoader{
wait: wait,
maxBatch: maxBatch,
@ -198,6 +208,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchScenesFileIDs(ctx),
},
AudioFiles: &AudioFileIDsLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchAudiosFileIDs(ctx),
},
ImageFiles: &ImageFileIDsLoader{
wait: wait,
maxBatch: maxBatch,
@ -289,6 +304,17 @@ func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models
}
}
func (m Middleware) fetchAudios(ctx context.Context) func(keys []int) ([]*models.Audio, []error) {
return func(keys []int) (ret []*models.Audio, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Audio.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchSceneCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
@ -301,6 +327,18 @@ func (m Middleware) fetchSceneCustomFields(ctx context.Context) func(keys []int)
}
}
func (m Middleware) fetchAudioCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Audio.GetCustomFieldsBulk(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
return func(keys []int) (ret []*models.Image, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
@ -497,6 +535,17 @@ func (m Middleware) fetchScenesFileIDs(ctx context.Context) func(keys []int) ([]
}
}
func (m Middleware) fetchAudiosFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
return func(keys []int) (ret [][]models.FileID, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Audio.GetManyFileIDs(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchImagesFileIDs(ctx context.Context) func(keys []int) ([][]models.FileID, []error) {
return func(keys []int) (ret [][]models.FileID, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {

View file

@ -1,5 +1,3 @@
// TODO(audio): update this file
package api
import (
@ -16,7 +14,7 @@ import (
func convertAudioFile(f models.File) (*models.AudioFile, error) {
vf, ok := f.(*models.AudioFile)
if !ok {
return nil, fmt.Errorf("file %T is not a video file", f)
return nil, fmt.Errorf("file %T is not a audio file", f)
}
return vf, nil
}
@ -109,25 +107,12 @@ func (r *audioResolver) Paths(ctx context.Context, obj *models.Audio) (*AudioPat
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
config := manager.GetInstance().Config
builder := urlbuilders.NewAudioURLBuilder(baseURL, obj)
screenshotPath := builder.GetScreenshotURL()
previewPath := builder.GetStreamPreviewURL()
streamPath := builder.GetStreamURL(config.GetAPIKey()).String()
webpPath := builder.GetStreamPreviewImageURL()
objHash := obj.GetHash(config.GetAudioFileNamingAlgorithm())
vttPath := builder.GetSpriteVTTURL(objHash)
spritePath := builder.GetSpriteURL(objHash)
funscriptPath := builder.GetFunscriptURL()
captionBasePath := builder.GetCaptionURL()
return &AudioPathsType{
Screenshot: &screenshotPath,
Preview: &previewPath,
Stream: &streamPath,
Webp: &webpPath,
Vtt: &vttPath,
Sprite: &spritePath,
Funscript: &funscriptPath,
Caption: &captionBasePath,
Stream: &streamPath,
Caption: &captionBasePath,
}, nil
}

View file

@ -10,7 +10,6 @@ import (
"github.com/99designs/gqlgen/graphql"
"github.com/stashapp/stash/pkg/audio"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
)
@ -118,14 +117,6 @@ func (r *queryResolver) FindAudios(
}
}
} else {
logger.Infof(
"FindAudios debug:\n audioFilter=%+v\n filter=%+v\n fields=%v\n repo=%+v\n repo.Audio=%T",
audioFilter,
filter,
fields,
r.repository,
r.repository.Audio,
)
result, err = r.repository.Audio.Query(ctx, models.AudioQueryOptions{
QueryOptions: models.QueryOptions{
FindFilter: filter,

View file

@ -39,13 +39,6 @@ func (rs audioRoutes) Routes() chi.Router {
// streaming endpoints
r.Get("/stream", rs.StreamDirect)
// TODO(audio): slightly difficult to support StreamHLS/StreamDASH...do last
// r.Get("/stream.m3u8", rs.StreamHLS)
// r.Get("/stream.m3u8/{segment}.ts", rs.StreamHLSSegment)
// r.Get("/stream.mpd", rs.StreamDASH)
// r.Get("/stream.mpd/{segment}_a.webm", rs.StreamDASHAudioSegment)
r.Get("/funscript", rs.Funscript)
r.Get("/caption", rs.CaptionLang)
})
@ -60,87 +53,6 @@ func (rs audioRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
ss.StreamAudioDirect(audio, w, r)
}
// func (rs audioRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
// rs.streamManifest(w, r, ffmpeg.StreamTypeHLS, "HLS")
// }
// func (rs audioRoutes) StreamDASH(w http.ResponseWriter, r *http.Request) {
// rs.streamManifest(w, r, ffmpeg.StreamTypeDASHAudio, "DASH")
// }
// func (rs audioRoutes) streamManifest(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType, logName string) {
// audio := r.Context().Value(audioKey).(*models.Audio)
// streamManager := manager.GetInstance().StreamManager
// if streamManager == nil {
// http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable)
// return
// }
// f := audio.Files.Primary()
// if f == nil {
// return
// }
// if err := r.ParseForm(); err != nil {
// logger.Warnf("[transcode] error parsing query form: %v", err)
// }
// resolution := r.Form.Get("resolution")
// logger.Debugf("[transcode] returning %s manifest for audio %d", logName, audio.ID)
// streamManager.ServeManifest(w, r, streamType, f, resolution)
// }
// func (rs audioRoutes) StreamHLSSegment(w http.ResponseWriter, r *http.Request) {
// rs.streamSegment(w, r, ffmpeg.StreamTypeHLS)
// }
// func (rs audioRoutes) StreamDASHAudioSegment(w http.ResponseWriter, r *http.Request) {
// rs.streamSegment(w, r, ffmpeg.StreamTypeDASHAudio)
// }
// func (rs audioRoutes) streamSegment(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType) {
// audio := r.Context().Value(audioKey).(*models.Audio)
// streamManager := manager.GetInstance().StreamManager
// if streamManager == nil {
// http.Error(w, "Live transcoding disabled", http.StatusServiceUnavailable)
// return
// }
// f := audio.Files.Primary()
// if f == nil {
// return
// }
// if err := r.ParseForm(); err != nil {
// logger.Warnf("[transcode] error parsing query form: %v", err)
// }
// audioHash := audio.GetHash(config.GetInstance().GetAudioFileNamingAlgorithm())
// segment := chi.URLParam(r, "segment")
// resolution := r.Form.Get("resolution")
// options := ffmpeg.StreamOptions{
// StreamType: streamType,
// AudioFile: f,
// Resolution: resolution,
// Hash: audioHash,
// Segment: segment,
// }
// streamManager.ServeSegment(w, r, options)
// }
func (rs audioRoutes) Funscript(w http.ResponseWriter, r *http.Request) {
s := r.Context().Value(audioKey).(*models.Audio)
filepath := video.GetFunscriptPath(s.Path)
utils.ServeStaticFile(w, r, filepath)
}
func (rs audioRoutes) Caption(w http.ResponseWriter, r *http.Request, lang string, ext string) {
s := r.Context().Value(audioKey).(*models.Audio)

View file

@ -215,6 +215,7 @@ func Initialize() (*Server, error) {
r.Mount("/performer", server.getPerformerRoutes())
r.Mount("/scene", server.getSceneRoutes())
r.Mount("/audio", server.getAudioRoutes())
r.Mount("/gallery", server.getGalleryRoutes())
r.Mount("/image", server.getImageRoutes())
r.Mount("/studio", server.getStudioRoutes())
@ -369,6 +370,16 @@ func (s *Server) getSceneRoutes() chi.Router {
}.Routes()
}
func (s *Server) getAudioRoutes() chi.Router {
repo := s.manager.Repository
return audioRoutes{
routes: routes{txnManager: repo.TxnManager},
audioFinder: repo.Audio,
fileGetter: repo.File,
captionFinder: repo.File,
}.Routes()
}
func (s *Server) getGalleryRoutes() chi.Router {
repo := s.manager.Repository
return galleryRoutes{

View file

@ -1,5 +1,3 @@
// TODO(audio): updaqte this file
package urlbuilders
import (
@ -39,30 +37,6 @@ func (b AudioURLBuilder) GetStreamURL(apiKey string) *url.URL {
return u
}
func (b AudioURLBuilder) GetStreamPreviewURL() string {
return b.BaseURL + "/audio/" + b.AudioID + "/preview"
}
func (b AudioURLBuilder) GetStreamPreviewImageURL() string {
return b.BaseURL + "/audio/" + b.AudioID + "/webp"
}
func (b AudioURLBuilder) GetSpriteVTTURL(checksum string) string {
return b.BaseURL + "/audio/" + checksum + "_thumbs.vtt"
}
func (b AudioURLBuilder) GetSpriteURL(checksum string) string {
return b.BaseURL + "/audio/" + checksum + "_sprite.jpg"
}
func (b AudioURLBuilder) GetScreenshotURL() string {
return b.BaseURL + "/audio/" + b.AudioID + "/screenshot?t=" + b.UpdatedAt
}
func (b AudioURLBuilder) GetFunscriptURL() string {
return b.BaseURL + "/audio/" + b.AudioID + "/funscript"
}
func (b AudioURLBuilder) GetCaptionURL() string {
return b.BaseURL + "/audio/" + b.AudioID + "/caption"
}

View file

@ -1,4 +1,3 @@
// TODO(audio): update this file
package manager
import (
@ -18,17 +17,11 @@ type AudioStreamEndpoint struct {
}
var (
// TODO(audio): figure out what stream types we need, and what we can support
directAudioEndpointType = endpointType{
label: "Direct stream",
mimeType: ffmpeg.MimeMp3Audio,
extension: "",
}
mp3AudioEndpointType = endpointType{
label: "MP3",
mimeType: ffmpeg.MimeMp3Audio,
extension: ".mp3",
}
)
func GetAudioFileContainer(file *models.AudioFile) (ffmpeg.Container, error) {
@ -88,18 +81,7 @@ func GetAudioStreamPaths(audio *models.Audio, directStreamURL *url.URL, maxStrea
endpoints = append(endpoints, makeStreamEndpoint(directAudioEndpointType))
}
mp3Streams := []*AudioStreamEndpoint{}
hlsStreams := []*AudioStreamEndpoint{}
dashStreams := []*AudioStreamEndpoint{}
// TODO(audio): do we need the `if includeAudioStreamPath() {`?
mp3Streams = append(mp3Streams, makeStreamEndpoint(mp3AudioEndpointType))
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType))
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType))
endpoints = append(endpoints, mp3Streams...)
endpoints = append(endpoints, hlsStreams...)
endpoints = append(endpoints, dashStreams...)
// TODO(audio): can we return no urls?
return endpoints, nil
}

View file

@ -64,7 +64,8 @@ func (c *fingerprintCalculator) CalculateFingerprints(f *models.BaseFile, o file
var ret []models.Fingerprint
calculateMD5 := true
if useAsVideo(f.Path) {
// TODO(audio): should Audio's also use OSHash instead of md5 for default (if so, then will need to update Audios)
if useAsVideo(f.Path) || useAsAudio(f.Path) {
var (
fp *models.Fingerprint
err error

View file

@ -1,6 +1,3 @@
// TODO(audio): update this file to add Audio scanner, audioFileFilter, new file.FilteredHandler for audio.ScanHandler,
// TODO(audio): [con't] Add audio to extensionConfig, useAsAudio(), newExtensionConfig
package manager
import (
@ -17,6 +14,7 @@ import (
"github.com/99designs/gqlgen/graphql/handler/lru"
"github.com/remeh/sizedwaitgroup"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/audio"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/file/video"
"github.com/stashapp/stash/pkg/fsutil"
@ -708,28 +706,16 @@ func getScanHandlers(options ScanMetadataInput, taskQueue *job.TaskQueue, progre
Paths: instance.Paths,
},
},
// &file.FilteredHandler{
// Filter: file.FilterFunc(audioFileFilter),
// Handler: &audio.ScanHandler{
// CreatorUpdater: r.Audio,
// GalleryFinder: r.Gallery,
// SceneFinderUpdater: r.Scene,
// // ScanGenerator: &audioGenerators{
// // input: options,
// // taskQueue: taskQueue,
// // progress: progress,
// // paths: mgr.Paths,
// // sequentialScanning: c.GetSequentialScanning(),
// // },
// // ScanConfig: &scanConfig{
// // isGenerateThumbnails: options.ScanGenerateThumbnails,
// // isGenerateClipPreviews: options.ScanGenerateClipPreviews,
// // createGalleriesFromFolders: c.GetCreateGalleriesFromFolders(),
// // },
// PluginCache: pluginCache,
// Paths: instance.Paths,
// },
// },
&file.FilteredHandler{
Filter: file.FilterFunc(audioFileFilter),
Handler: &audio.ScanHandler{
CreatorUpdater: r.Audio,
CaptionUpdater: r.File,
PluginCache: pluginCache,
FileNamingAlgorithm: c.GetVideoFileNamingAlgorithm(),
Paths: mgr.Paths,
},
},
&file.FilteredHandler{
Filter: file.FilterFunc(galleryFileFilter),
Handler: &gallery.ScanHandler{

View file

@ -1,13 +1,9 @@
// TODO(audio): update this file
package audio
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/stashapp/stash/pkg/file/audio"
"github.com/stashapp/stash/pkg/logger"
@ -15,7 +11,6 @@ import (
"github.com/stashapp/stash/pkg/models/paths"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/plugin/hook"
"github.com/stashapp/stash/pkg/txn"
)
var (
@ -46,10 +41,10 @@ type ScanGenerator interface {
}
type ScanHandler struct {
CreatorUpdater ScanCreatorUpdater
GalleryFinderUpdater ScanGalleryFinderUpdater
CreatorUpdater ScanCreatorUpdater
ScanGenerator ScanGenerator
// TODO(audio): this PR has no generation
// ScanGenerator ScanGenerator
CaptionUpdater audio.CaptionUpdater
PluginCache *plugin.Cache
@ -61,9 +56,9 @@ func (h *ScanHandler) validate() error {
if h.CreatorUpdater == nil {
return errors.New("CreatorUpdater is required")
}
if h.ScanGenerator == nil {
return errors.New("ScanGenerator is required")
}
// if h.ScanGenerator == nil {
// return errors.New("ScanGenerator is required")
// }
if h.CaptionUpdater == nil {
return errors.New("CaptionUpdater is required")
}
@ -137,19 +132,15 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models.
}
}
if err := h.associateGallery(ctx, existing, f); err != nil {
return err
}
// do this after the commit so that cover generation doesn't hold up the transaction
txn.AddPostCommitHook(ctx, func(ctx context.Context) {
for _, s := range existing {
if err := h.ScanGenerator.Generate(ctx, s, AudioFile); err != nil {
// just log if cover generation fails. We can try again on rescan
logger.Errorf("Error generating content for %s: %v", AudioFile.Path, err)
}
}
})
// txn.AddPostCommitHook(ctx, func(ctx context.Context) {
// for _, s := range existing {
// if err := h.ScanGenerator.Generate(ctx, s, AudioFile); err != nil {
// // just log if cover generation fails. We can try again on rescan
// logger.Errorf("Error generating content for %s: %v", AudioFile.Path, err)
// }
// }
// })
return nil
}
@ -189,29 +180,3 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.
return nil
}
func (h *ScanHandler) associateGallery(ctx context.Context, existing []*models.Audio, f models.File) error {
audioIDs := make([]int, len(existing))
for i, s := range existing {
audioIDs[i] = s.ID
}
path := f.Base().Path
zipPath := strings.TrimSuffix(path, filepath.Ext(path)) + ".zip"
// find galleries with a file that matches
galleries, err := h.GalleryFinderUpdater.FindByPath(ctx, zipPath)
if err != nil {
return err
}
for _, gallery := range galleries {
// found related Audio
logger.Infof("associate: Audio %s is related to gallery: %d", path, gallery.ID)
if err := h.GalleryFinderUpdater.AddAudioIDs(ctx, gallery.ID, audioIDs); err != nil {
return err
}
}
return nil
}

View file

@ -20,7 +20,7 @@ func TestAssociateExisting_UpdatePartialOnContentChange(t *testing.T) {
)
existingFile := &models.AudioFile{
BaseFile: &models.BaseFile{ID: models.FileID(testFileID), Path: "test.mp4"},
BaseFile: &models.BaseFile{ID: models.FileID(testFileID), Path: "test.mp3"},
}
makeAudio := func() *models.Audio {
@ -84,10 +84,10 @@ func TestAssociateExisting_UpdatePartialOnNewFile(t *testing.T) {
)
existingFile := &models.AudioFile{
BaseFile: &models.BaseFile{ID: models.FileID(existFileID), Path: "existing.mp4"},
BaseFile: &models.BaseFile{ID: models.FileID(existFileID), Path: "existing.mp3"},
}
newFile := &models.AudioFile{
BaseFile: &models.BaseFile{ID: models.FileID(newFileID), Path: "new.mp4"},
BaseFile: &models.BaseFile{ID: models.FileID(newFileID), Path: "new.mp3"},
}
audio := &models.Audio{

View file

@ -18,10 +18,6 @@ var validForVp9 = []Container{Webm}
var validForHevcMkv = []Container{Mp4, Matroska}
var validForHevc = []Container{Mp4}
var validAudioForMkv = []ProbeAudioCodec{Aac, Mp3, Vorbis, Opus}
var validAudioForWebm = []ProbeAudioCodec{Vorbis, Opus}
var validAudioForMp4 = []ProbeAudioCodec{Aac, Mp3, Opus}
var (
// ErrUnsupportedVideoCodecForBrowser is returned when the video codec is not supported for browser streaming.
ErrUnsupportedVideoCodecForBrowser = errors.New("unsupported video codec for browser")
@ -81,12 +77,10 @@ func isValidAudio(audio ProbeAudioCodec, validCodecs []ProbeAudioCodec) bool {
// IsValidAudioForContainer returns true if the audio codec is valid for the container.
func IsValidAudioForContainer(audio ProbeAudioCodec, format Container) bool {
switch format {
case Matroska:
return isValidAudio(audio, validAudioForMkv)
case Webm:
return isValidAudio(audio, validAudioForWebm)
case Mp4:
return isValidAudio(audio, validAudioForMp4)
case Mp3Container:
return true
// TODO(audio): do we need to check ProbeAudioCodec for audio containers?
// return isValidAudio(audio, validAudioForMp3)
}
return false
}

View file

@ -14,6 +14,9 @@ const (
Flv Container = "flv"
Mpegts Container = "mpegts"
// TODO(audio): better way to do this, without suffic this clashes with `Mp3 ProbeAudioCodec`
Mp3Container Container = "mp3"
Aac ProbeAudioCodec = "aac"
Mp3 ProbeAudioCodec = "mp3"
Opus ProbeAudioCodec = "opus"

View file

@ -242,15 +242,6 @@ func (s Audio) GetHash(hashAlgorithm HashAlgorithm) string {
return ""
}
// AudioFileType represents the file metadata for a audio.
// type AudioFileType struct {
// Size *string `graphql:"size" json:"size"`
// Duration *float64 `graphql:"duration" json:"duration"`
// AudioCodec *string `graphql:"audio_codec" json:"audio_codec"`
// Samplerate *float64 `graphql:"sample_rate" json:"sample_rate"`
// Bitrate *int `graphql:"bitrate" json:"bitrate"`
// }
// TODO(audio): don't know if we need this, using VideoCaption for now due to `pkg/models/repository_file.go` and `FileReader` using
// type AudioCaption struct {
// LanguageCode string `json:"language_code"`

View file

@ -21,6 +21,7 @@ func NewPaths(generatedPath string, blobsPath string) Paths {
p.Generated = newGeneratedPaths(generatedPath)
p.Scene = newScenePaths(p)
p.Audio = newAudioPaths(p)
p.SceneMarkers = newSceneMarkerPaths(p)
p.Blobs = blobsPath

View file

@ -19,7 +19,7 @@ func newAudioPaths(p Paths) *audioPaths {
}
func (sp *audioPaths) GetTranscodePath(checksum string) string {
return filepath.Join(sp.Transcodes, checksum+".mp4")
return filepath.Join(sp.Transcodes, checksum+".mp3")
}
func (sp *audioPaths) GetStreamPath(audioPath string, checksum string) string {

View file

@ -83,6 +83,24 @@ func (f *videoFileRow) fromVideoFile(ff models.VideoFile) {
f.InteractiveSpeed = intFromPtr(ff.InteractiveSpeed)
}
type audioFileRow struct {
FileID models.FileID `db:"file_id"`
Format string `db:"format"`
Duration float64 `db:"duration"`
AudioCodec string `db:"audio_codec"`
SampleRate int64 `db:"sample_rate"`
BitRate int64 `db:"bit_rate"`
}
func (f *audioFileRow) fromAudioFile(ff models.AudioFile) {
f.FileID = ff.ID
f.Format = ff.Format
f.Duration = ff.Duration
f.AudioCodec = ff.AudioCodec
f.SampleRate = ff.SampleRate
f.BitRate = ff.BitRate
}
type imageFileRow struct {
FileID models.FileID `db:"file_id"`
Format string `db:"format"`
@ -145,6 +163,39 @@ func videoFileQueryColumns() []interface{} {
}
}
// we redefine this to change the columns around
// otherwise, we collide with the video file columns
type audioFileQueryRow struct {
FileID null.Int `db:"file_id_audio"`
Format null.String `db:"audio_format"`
Duration null.Float `db:"audio_duration"`
AudioCodec null.String `db:"audio_audio_codec"`
SampleRate null.Int `db:"audio_sample_rate"`
BitRate null.Int `db:"audio_bit_rate"`
}
func (f *audioFileQueryRow) resolve() *models.AudioFile {
return &models.AudioFile{
Format: f.Format.String,
Duration: f.Duration.Float64,
AudioCodec: f.AudioCodec.String,
SampleRate: f.SampleRate.Int64,
BitRate: f.BitRate.Int64,
}
}
func audioFileQueryColumns() []interface{} {
table := audioFileTableMgr.table
return []interface{}{
table.Col("file_id").As("file_id_audio"),
table.Col("format").As("audio_format"),
table.Col("duration").As("audio_duration"),
table.Col("audio_codec").As("audio_audio_codec"),
table.Col("sample_rate").As("audio_sample_rate"),
table.Col("bit_rate").As("audio_bit_rate"),
}
}
// we redefine this to change the columns around
// otherwise, we collide with the video file columns
type imageFileQueryRow struct {
@ -187,6 +238,7 @@ type fileQueryRow struct {
FolderPath null.String `db:"parent_folder_path"`
fingerprintQueryRow
videoFileQueryRow
audioFileQueryRow
imageFileQueryRow
}
@ -222,6 +274,12 @@ func (r *fileQueryRow) resolve() models.File {
ret = vf
}
if r.audioFileQueryRow.Format.Valid {
vf := r.audioFileQueryRow.resolve()
vf.BaseFile = basic
ret = vf
}
if r.imageFileQueryRow.Format.Valid {
imf := r.imageFileQueryRow.resolve()
imf.BaseFile = basic
@ -354,6 +412,10 @@ func (qb *FileStore) Create(ctx context.Context, f models.File) error {
if err := qb.createVideoFile(ctx, fileID, *ef); err != nil {
return err
}
case *models.AudioFile:
if err := qb.createAudioFile(ctx, fileID, *ef); err != nil {
return err
}
case *models.ImageFile:
if err := qb.createImageFile(ctx, fileID, *ef); err != nil {
return err
@ -391,6 +453,10 @@ func (qb *FileStore) Update(ctx context.Context, f models.File) error {
if err := qb.updateOrCreateVideoFile(ctx, id, *ef); err != nil {
return err
}
case *models.AudioFile:
if err := qb.updateOrCreateAudioFile(ctx, id, *ef); err != nil {
return err
}
case *models.ImageFile:
if err := qb.updateOrCreateImageFile(ctx, id, *ef); err != nil {
return err
@ -448,6 +514,37 @@ func (qb *FileStore) updateOrCreateVideoFile(ctx context.Context, id models.File
return nil
}
func (qb *FileStore) createAudioFile(ctx context.Context, id models.FileID, f models.AudioFile) error {
var r audioFileRow
r.fromAudioFile(f)
r.FileID = id
if _, err := audioFileTableMgr.insert(ctx, r); err != nil {
return err
}
return nil
}
func (qb *FileStore) updateOrCreateAudioFile(ctx context.Context, id models.FileID, f models.AudioFile) error {
exists, err := audioFileTableMgr.idExists(ctx, id)
if err != nil {
return err
}
if !exists {
return qb.createAudioFile(ctx, id, f)
}
var r audioFileRow
r.fromAudioFile(f)
r.FileID = id
if err := audioFileTableMgr.updateByID(ctx, id, r); err != nil {
return err
}
return nil
}
func (qb *FileStore) createImageFile(ctx context.Context, id models.FileID, f models.ImageFile) error {
var r imageFileRow
r.fromImageFile(f)
@ -485,6 +582,7 @@ func (qb *FileStore) selectDataset() *goqu.SelectDataset {
folderTable := folderTableMgr.table
fingerprintTable := fingerprintTableMgr.table
videoFileTable := videoFileTableMgr.table
audioFileTable := audioFileTableMgr.table
imageFileTable := imageFileTableMgr.table
zipFileTable := table.As("zip_files")
@ -509,6 +607,7 @@ func (qb *FileStore) selectDataset() *goqu.SelectDataset {
}
cols = append(cols, videoFileQueryColumns()...)
cols = append(cols, audioFileQueryColumns()...)
cols = append(cols, imageFileQueryRow{}.columns(imageFileTableMgr)...)
ret := dialect.From(table).Select(cols...)
@ -522,6 +621,9 @@ func (qb *FileStore) selectDataset() *goqu.SelectDataset {
).LeftJoin(
videoFileTable,
goqu.On(table.Col(idColumn).Eq(videoFileTable.Col(fileIDColumn))),
).LeftJoin(
audioFileTable,
goqu.On(table.Col(idColumn).Eq(audioFileTable.Col(fileIDColumn))),
).LeftJoin(
imageFileTable,
goqu.On(table.Col(idColumn).Eq(imageFileTable.Col(fileIDColumn))),
@ -540,6 +642,7 @@ func (qb *FileStore) countDataset() *goqu.SelectDataset {
folderTable := folderTableMgr.table
fingerprintTable := fingerprintTableMgr.table
videoFileTable := videoFileTableMgr.table
audioFileTable := audioFileTableMgr.table
imageFileTable := imageFileTableMgr.table
zipFileTable := table.As("zip_files")
@ -556,6 +659,9 @@ func (qb *FileStore) countDataset() *goqu.SelectDataset {
).LeftJoin(
videoFileTable,
goqu.On(table.Col(idColumn).Eq(videoFileTable.Col(fileIDColumn))),
).LeftJoin(
audioFileTable,
goqu.On(table.Col(idColumn).Eq(audioFileTable.Col(fileIDColumn))),
).LeftJoin(
imageFileTable,
goqu.On(table.Col(idColumn).Eq(imageFileTable.Col(fileIDColumn))),

View file

@ -18,7 +18,6 @@ fragment SlimAudioData on Audio {
paths {
stream
funscript
caption
}

View file

@ -28,7 +28,6 @@ fragment AudioData on Audio {
paths {
stream
funscript
caption
}