mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
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:
parent
23c413438f
commit
169bebeaf5
24 changed files with 378 additions and 440 deletions
|
|
@ -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
|
||||
153
.golangci.yml
153
.golangci.yml
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
```
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ fragment SlimAudioData on Audio {
|
|||
|
||||
paths {
|
||||
stream
|
||||
funscript
|
||||
caption
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ fragment AudioData on Audio {
|
|||
|
||||
paths {
|
||||
stream
|
||||
funscript
|
||||
caption
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue